def _read_image_and_resize(img_entry: Union[str, 'numpy.array'], img_width: int, img_height: int, should_resize: bool, num_channels: int, resize_method: str, user_specified_num_channels: int): """ :param img_entry Union[str, 'numpy.array']: if str file path to the image else numpy.array of the image itself :param img_width: expected width of the image :param img_height: expected height of the image :param should_resize: Should the image be resized? :param resize_method: type of resizing method :param num_channels: expected number of channels in the first image :param user_specified_num_channels: did the user specify num channels? :return: image object Helper method to read and resize an image according to model defn. If the user doesn't specify a number of channels, we use the first image in the dataset as the source of truth. If any image in the dataset doesn't have the same number of channels as the first image, raise an exception. If the user specifies a number of channels, we try to convert all the images to the specifications by dropping channels/padding 0 channels """ img = read_image(img_entry) if img is None: logger.info(f"{img_entry} cannot be read") return None img_num_channels = num_channels_in_image(img) if img_num_channels == 1: img = img.reshape((img.shape[0], img.shape[1], 1)) if should_resize: img = resize_image(img, (img_height, img_width), resize_method) if user_specified_num_channels is True: # convert to greyscale if needed if num_channels == 1 and (img_num_channels == 3 or img_num_channels == 4): img = greyscale(img) img_num_channels = 1 # Number of channels is specified by the user img_padded = np.zeros((img_height, img_width, num_channels), dtype=np.uint8) min_num_channels = min(num_channels, img_num_channels) img_padded[:, :, :min_num_channels] = img[:, :, :min_num_channels] img = img_padded if img_num_channels != num_channels: logger.warning( "Image has {0} channels, where as {1} " "channels are expected. Dropping/adding channels " "with 0s as appropriate".format(img_num_channels, num_channels)) else: # If the image isn't like the first image, raise exception if img_num_channels != num_channels: raise ValueError( 'Image has {0} channels, unlike the first image, which ' 'has {1} channels. Make sure all the images have the same ' 'number of channels or use the num_channels property in ' 'image preprocessing'.format(img_num_channels, num_channels)) if img.shape[0] != img_height or img.shape[1] != img_width: raise ValueError( "Images are not of the same size. " "Expected size is {0}, " "current image size is {1}." "Images are expected to be all of the same size " "or explicit image width and height are expected " "to be provided. " "Additional information: " "https://ludwig-ai.github.io/ludwig-docs/user_guide/#image-features-preprocessing" .format([img_height, img_width, num_channels], img.shape)) return img
def _finalize_preprocessing_parameters( preprocessing_parameters: dict, first_img_entry: Union[str, 'numpy.array'], src_path: str, input_feature_col: np.array): """ Helper method to determine the height, width and number of channels for preprocessing the image data. This is achieved by looking at the parameters provided by the user. When there are some missing parameters, we fall back on to the first image in the dataset. The assumption being that all the images in the data are expected be of the same size with the same number of channels """ first_image = read_image(first_img_entry) explicit_height_width = HEIGHT in preprocessing_parameters or WIDTH in preprocessing_parameters explicit_num_channels = NUM_CHANNELS in preprocessing_parameters inferred_sample = None if preprocessing_parameters[INFER_IMAGE_DIMENSIONS] and not ( explicit_height_width and explicit_num_channels): sample_size = min( len(input_feature_col), preprocessing_parameters[INFER_IMAGE_SAMPLE_SIZE]) sample = [ read_image(get_image_from_path(src_path, img)) for img in input_feature_col.head(sample_size) ] inferred_sample = [img for img in sample if img is not None] if len(inferred_sample) == 0: raise ValueError( "No readable images in sample, image dimensions cannot be inferred" ) should_resize = False if explicit_height_width: should_resize = True try: height = int(preprocessing_parameters[HEIGHT]) width = int(preprocessing_parameters[WIDTH]) except ValueError as e: raise ValueError('Image height and width must be set and have ' 'positive integer values: ' + str(e)) if height <= 0 or width <= 0: raise ValueError( 'Image height and width must be positive integers') else: # User hasn't specified height and width. # Default to inferring from sample or first image. if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]: should_resize = True height_avg = min( sum(x.shape[0] for x in inferred_sample) / len(inferred_sample), preprocessing_parameters[INFER_IMAGE_MAX_HEIGHT]) width_avg = min( sum(x.shape[1] for x in inferred_sample) / len(inferred_sample), preprocessing_parameters[INFER_IMAGE_MAX_WIDTH]) height, width = round(height_avg), round(width_avg) logger.debug("Inferring height: {0} and width: {1}".format( height, width)) elif first_image is not None: height, width = first_image.shape[0], first_image.shape[1] else: raise ValueError( "Explicit image width/height are not set, infer_image_dimensions is false, " "and first image cannot be read, so image dimensions are unknown" ) if explicit_num_channels: # User specified num_channels in the model/feature config user_specified_num_channels = True num_channels = preprocessing_parameters[NUM_CHANNELS] else: user_specified_num_channels = False if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]: user_specified_num_channels = True num_channels = round( sum(num_channels_in_image(x) for x in inferred_sample) / len(inferred_sample)) elif first_image is not None: num_channels = num_channels_in_image(first_image) else: raise ValueError( "Explicit image num channels is not set, infer_image_dimensions is false, " "and first image cannot be read, so image num channels is unknown" ) assert isinstance( num_channels, int), ValueError('Number of image channels needs to be an integer') return (should_resize, width, height, num_channels, user_specified_num_channels, first_image)
def _finalize_preprocessing_parameters( preprocessing_parameters: dict, first_img_entry: Union[str, 'numpy.array'], src_path: str, input_feature_col: np.array): """ Helper method to determine the height, width and number of channels for preprocessing the image data. This is achieved by looking at the parameters provided by the user. When there are some missing parameters, we fall back on to the first image in the dataset. The assumption being that all the images in the data are expected be of the same size with the same number of channels """ first_image = read_image(first_img_entry) first_img_height = first_image.shape[0] first_img_width = first_image.shape[1] first_img_num_channels = num_channels_in_image(first_image) should_resize = False if (HEIGHT in preprocessing_parameters or WIDTH in preprocessing_parameters): should_resize = True try: height = int(preprocessing_parameters[HEIGHT]) width = int(preprocessing_parameters[WIDTH]) except ValueError as e: raise ValueError('Image height and width must be set and have ' 'positive integer values: ' + str(e)) if height <= 0 or width <= 0: raise ValueError( 'Image height and width must be positive integers') else: # User hasn't specified height and width. # Default to first image, or infer from sample. height, width = first_img_height, first_img_width if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]: should_resize = True sample_size = min( len(input_feature_col), preprocessing_parameters[INFER_IMAGE_SAMPLE_SIZE]) sample_images = [ read_image(get_image_from_path(src_path, img)) for img in input_feature_col[:sample_size] ] if sample_images: height_avg = min( sum(x.shape[0] for x in sample_images) / len(sample_images), preprocessing_parameters[INFER_IMAGE_MAX_HEIGHT]) width_avg = min( sum(x.shape[1] for x in sample_images) / len(sample_images), preprocessing_parameters[INFER_IMAGE_MAX_WIDTH]) height, width = round(height_avg), round(width_avg) logger.debug("Inferring height: {0} and width: {1}".format( height, width)) else: logger.warning( "Sample set for inference is empty, default to height and width of first image" ) if NUM_CHANNELS in preprocessing_parameters: # User specified num_channels in the model/feature config user_specified_num_channels = True num_channels = preprocessing_parameters[NUM_CHANNELS] else: user_specified_num_channels = False num_channels = first_img_num_channels assert isinstance( num_channels, int), ValueError('Number of image channels needs to be an integer') return (should_resize, width, height, num_channels, user_specified_num_channels, first_image)