Source code for megadetector.visualization.render_images_with_thumbnails

"""

render_images_with_thumbnails.py

Renders an output image with one primary image and crops from many secondary images,
used primarily to check whether candidate repeat detections are actually false positives or not.

"""

#%% Imports

import math
import os
import random

from PIL import Image

from megadetector.visualization import visualization_utils as vis_utils
from megadetector.utils import path_utils


#%% Support functions

[docs] def crop_image_with_normalized_coordinates( image, bounding_box): """ Args: image (PIL.Image): image to crop bounding_box (tuple): tuple formatted as (x,y,w,h), where (0,0) is the upper-left of the image, and coordinates are normalized (so (0,0,1,1) is a box containing the entire image). Returns: PIL.Image: cropped image """ im_width, im_height = image.size (x_norm, y_norm, w_norm, h_norm) = bounding_box (x, y, w, h) = (x_norm * im_width, y_norm * im_height, w_norm * im_width, h_norm * im_height) return image.crop((x, y, x+w, y+h))
#%% Main function
[docs] def render_images_with_thumbnails( primary_image_filename, primary_image_width, secondary_image_filename_list, secondary_image_bounding_box_list, cropped_grid_width, output_image_filename, primary_image_location='right'): """ Given a primary image filename and a list of secondary images, writes to the provided output_image_filename an image where the one side is the primary image, and the other side is a grid of the secondary images, cropped according to the provided list of bounding boxes. The output file will be primary_image_width + cropped_grid_width pixels wide. The height of the output image will be determined by the original aspect ratio of the primary image. Args: primary_image_filename (str): filename of the primary image to load as str primary_image_width (int): width at which to render the primary image; if this is None, will render at the original image width secondary_image_filename_list (list): list of filenames of the secondary images secondary_image_bounding_box_list (list): list of tuples, one per secondary image. Each tuple is a bounding box of the secondary image, formatted as (x,y,w,h), where (0,0) is the upper-left of the image, and coordinates are normalized (so (0,0,1,1) is a box containing the entire image. cropped_grid_width (int): width of the cropped-image area output_image_filename (str): filename to write the output image primary_image_location (str, optional): 'right' or left'; reserving 'top', 'bottom', etc. for future use """ # Check to make sure the arguments are reasonable assert(len(secondary_image_filename_list) == len(secondary_image_bounding_box_list)), \ 'Length of secondary image list and bounding box list should be equal' assert primary_image_location in ['left','right'] # Load primary image and resize to desired width primary_image = vis_utils.load_image(primary_image_filename) if primary_image_width is not None: primary_image = vis_utils.resize_image(primary_image, primary_image_width, target_height=-1) # Compute the number of grid elements for the secondary images # to best fit the available aspect ratio grid_width = cropped_grid_width grid_height = primary_image.size[1] grid_aspect = grid_width / grid_height sample_crop_width = secondary_image_bounding_box_list[0][2] sample_crop_height = secondary_image_bounding_box_list[0][3] n_crops = len(secondary_image_filename_list) optimal_n_rows = None optimal_aspect_error = None for candidate_n_rows in range(1,n_crops+1): candidate_n_cols = math.ceil(n_crops / candidate_n_rows) candidate_grid_aspect = (candidate_n_cols*sample_crop_width) / \ (candidate_n_rows*sample_crop_height) aspect_error = abs(grid_aspect-candidate_grid_aspect) if optimal_n_rows is None or aspect_error < optimal_aspect_error: optimal_n_rows = candidate_n_rows optimal_aspect_error = aspect_error assert optimal_n_rows is not None grid_rows = optimal_n_rows grid_columns = math.ceil(n_crops/grid_rows) # Compute the width of each grid cell grid_cell_width = math.floor(grid_width / grid_columns) grid_cell_height = math.floor(grid_height / grid_rows) # Load secondary images and their associated bounding boxes. Iterate # through them, crop them, and save them to a list of cropped_images cropped_images = [] for (name, box) in zip(secondary_image_filename_list, secondary_image_bounding_box_list, strict=True): other_image = vis_utils.load_image(name) cropped_image = crop_image_with_normalized_coordinates( other_image, box) # Rescale this crop to fit within the desired grid cell size width_scale_factor = grid_cell_width / cropped_image.size[0] height_scale_factor = grid_cell_height / cropped_image.size[1] scale_factor = min(width_scale_factor,height_scale_factor) # Resize the cropped image, whether we're making it larger or smaller cropped_image = cropped_image.resize( ((int)(cropped_image.size[0] * scale_factor), (int)(cropped_image.size[1] * scale_factor))) cropped_images.append(cropped_image) # ...for each crop # Compute the final output image size. This will depend upon the aspect # ratio of the crops. output_image_width = primary_image.size[0] + grid_width output_image_height = primary_image.size[1] # Create blank output image output_image = Image.new('RGB', (output_image_width, output_image_height)) # Copy resized primary image to output image if primary_image_location == 'right': primary_image_x = grid_width else: primary_image_x = 0 output_image.paste(primary_image, (primary_image_x, 0)) # Compute the final locations of the secondary images in the output image i_row = 0; i_col = 0 for image in cropped_images: x = i_col * grid_cell_width if primary_image_location == 'left': x += primary_image.size[0] y = i_row * grid_cell_height output_image.paste(image, (x,y)) i_col += 1 if i_col >= grid_columns: i_col = 0 i_row += 1 # ...for each crop # Write output image to disk parent_dir = os.path.dirname(output_image_filename) if len(parent_dir) > 0: os.makedirs(parent_dir,exist_ok=True) output_image.save(output_image_filename)
# ...def render_images_with_thumbnails(...) #%% Command-line driver # This is just a test driver, this module is not meant to be run from the command line. def main(): # noqa # Load images from a test directory. # # Make the first image in the directory the primary image, # the remaining ones the comparison images. test_input_folder = os.path.expanduser('~/data/KRU-test') output_image_filename = os.path.expanduser('~/tmp/thumbnail_test.jpg') files = path_utils.find_images(test_input_folder) random.seed(0); random.shuffle(files) primary_image_filename = files[0] secondary_image_filename_list = [] secondary_image_bounding_box_list = [] # Initialize the x,y location of the bounding box box = (random.uniform(0.25, 0.75), random.uniform(0.25, 0.75)) # Create the list of secondary images and their bounding boxes for file in files[1:]: secondary_image_filename_list.append(file) secondary_image_bounding_box_list.append( (box[0] + random.uniform(-0.001, 0.001), box[1] + random.uniform(-0.001, 0.001), 0.2, 0.2)) primary_image_width = 1000 cropped_grid_width = 1000 render_images_with_thumbnails( primary_image_filename, primary_image_width, secondary_image_filename_list, secondary_image_bounding_box_list, cropped_grid_width, output_image_filename, 'right') path_utils.open_file(output_image_filename) if __name__ == '__main__': main()