import os import textwrap from pathlib import Path from typing import List import cv2 import numpy as np import PIL from PIL import Image, ImageChops, ImageDraw, ImageFont kMinMargin = 10 def stack_images_horizontally(images: List, save_path=None): widths, heights = list(zip(*(i.size for i in images))) total_width = sum(widths) max_height = max(heights) new_im = Image.new("RGBA", (total_width, max_height)) x_offset = 0 for im in images: new_im.paste(im, (x_offset, 0)) x_offset += im.size[0] if save_path is not None: new_im.save(save_path) return new_im def stack_images_vertically(images: List, save_path=None): widths, heights = list(zip(*(i.size for i in images))) max_width = max(widths) total_height = sum(heights) new_im = Image.new("RGBA", (max_width, total_height)) y_offset = 0 for im in images: new_im.paste(im, (0, y_offset)) y_offset += im.size[1] if save_path is not None: new_im.save(save_path) return new_im def merge_images(images: List): if isinstance(images[0], Image.Image): return stack_images_horizontally(images) images = list(map(stack_images_horizontally, images)) return stack_images_vertically(images) def draw_text( image: PIL.Image, text: str, font_size=None, font_color=(0, 0, 0), max_seq_length=100, ): W, H = image.size S = max(W, H) font_path = os.path.join(cv2.__path__[0], "qt", "fonts", "DejaVuSans.ttf") font_size = max(int(S / 32), 20) if font_size is None else font_size font = ImageFont.truetype(font_path, size=font_size) text_wrapped = textwrap.fill(text, max_seq_length) w, h = font.getsize(text_wrapped) new_im = Image.new("RGBA", (W, H + h)) new_im.paste(image, (0, h)) draw = ImageDraw.Draw(new_im) draw.text((max((W - w) / 2, 0), 0), text_wrapped, font=font, fill=font_color) return new_im def to_white(img): new_img = Image.new("RGBA", img.size, "WHITE") new_img.paste(img, (0, 0), img) new_img.convert("RGB") return new_img def get_bbox(in_file, fuzz=17.5): im = Image.open(in_file) # bbox = im.convert("RGBa").getbbox() try: bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) except OSError as err: print(f"error {in_file}") raise OSError diff = ImageChops.difference(im, bg) offset = int(round(float(fuzz) / 100.0 * 255.0)) diff = ImageChops.add(diff, diff, 2.0, -offset) bbox = diff.getbbox() bx_min = max(bbox[0] - kMinMargin, 0) by_min = max(bbox[1] - kMinMargin, 0) bx_max = min(bbox[2] + kMinMargin, im.size[0]) by_max = min(bbox[3] + kMinMargin, im.size[1]) bbox_margin = (bx_min, by_min, bx_max, by_max) return bbox_margin def get_largest_bbox(in_files): largest_bbox = (float("Inf"), float("Inf"), -float("Inf"), -float("Inf")) for in_file in in_files: bbox = get_bbox(in_file) largest_bbox = ( min(bbox[0], largest_bbox[0]), min(bbox[1], largest_bbox[1]), max(bbox[2], largest_bbox[2]), max(bbox[3], largest_bbox[3]), ) return largest_bbox def trim(in_file, out_file, keep_ratio): # im = Image.open(in_file) # bbox = im.convert("RGBa").getbbox() bbox = get_bbox(in_file) trim_with_bbox(in_file, out_file, bbox, keep_ratio) def trim_with_bbox(in_file, out_file, bbox, keep_ratio): im = Image.open(in_file) if keep_ratio: w, h = im.size r = float(w) / h bx_min, by_min, bx_max, by_max = bbox[0], bbox[1], bbox[2], bbox[3] bw, bh = bx_max - bx_min, by_max - by_min bcx, bcy = 0.5 * (bx_min + bx_max), 0.5 * (by_min + by_max) br = float(bw) / bh if br > r: bh = int(round(bw / r)) by_min, by_max = int(round(bcy - 0.5 * bh)), int(round(bcy + 0.5 * bh)) if by_min < 0: by_min = 0 by_max = bh elif by_max > h: by_max = h by_min = h - bh assert bh >= bh elif br < r: bw = int(round(bh * r)) bx_min, bx_max = int(round(bcx - 0.5 * bw)), int(round(bcx + 0.5 * bw)) if bx_min < 0: bx_min = 0 bx_max = bw elif bx_max > w: bx_max = w bx_min = w - bw bbox = (bx_min, by_min, bx_max, by_max) im.crop(bbox).save(out_file, "png") def trim_with_largest_bbox(in_files, out_files, keep_ratio): assert len(in_files) == len(out_files) bbox = get_largest_bbox(in_files) for i in range(len(in_files)): trim_with_bbox(in_files[i], out_files[i], bbox, keep_ratio) def create_image_table_tight_centering( in_img_files, out_img_file, max_total_width=2560, draw_col_lines=[] ): n_rows = len(in_img_files) n_cols = len(in_img_files[0]) # Compute width and height of each image. width = 0 row_top = [float("Inf")] * n_rows row_bottom = [-float("Inf")] * n_rows for row in range(n_rows): for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_width = img_right - img_left width = max(width, img_width) row_top[row] = min(row_top[row], img_top) row_bottom[row] = max(row_bottom[row], img_bottom) row_height = [bottom - top for bottom, top in zip(row_bottom, row_top)] # Combine images. cmd = "convert " for row in range(n_rows): cmd += " \( " for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_h_center = 0.5 * (img_left + img_right) left = int(img_h_center - 0.5 * width) cmd += " \( {} ".format(in_img_files[row][col]) cmd += "-gravity NorthWest -crop {}x{}+{}+{} +repage \) ".format( width, row_height[row], left, row_top[row] ) cmd += " -gravity center -background white +append \) " cmd += "-append " + out_img_file print(cmd) os.system(cmd) # Draw lines for columns. for col in draw_col_lines: if col <= 0 or col >= n_cols: continue strokewidth = max(int(round(width * 0.005)), 1) pos = col * width cmd = "convert " + out_img_file + " -stroke black " cmd += "-strokewidth {} ".format(strokewidth) cmd += '-draw "line {0},0 {0},10000000" '.format(pos) + out_img_file os.system(cmd) # Resize the combined image if it is too large. print(n_cols * width) if (n_cols * width) > max_total_width: cmd = "convert {0} -resize {1}x +repage {0}".format( out_img_file, max_total_width ) print(cmd) os.system(cmd) print("Saved '{}'.".format(out_img_file)) return width, row_height def create_image_table_tight_centering_per_row( in_img_files, out_img_dir, max_total_width=1280, draw_col_lines=[] ): n_rows = len(in_img_files) n_cols = len(in_img_files[0]) # Compute width and height of each image. width = 0 row_top = [float("Inf")] * n_rows row_bottom = [-float("Inf")] * n_rows for row in range(n_rows): for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_width = img_right - img_left width = max(width, img_width) row_top[row] = min(row_top[row], img_top) row_bottom[row] = max(row_bottom[row], img_bottom) row_height = [bottom - top for bottom, top in zip(row_bottom, row_top)] if not os.path.exists(out_img_dir): os.makedirs(out_img_dir) # Combine images. for row in range(n_rows): out_img_file = os.path.join(out_img_dir, "{:02d}.png".format(row)) cmd = "convert " for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_h_center = 0.5 * (img_left + img_right) left = int(img_h_center - 0.5 * width) cmd += " \( {} ".format(in_img_files[row][col]) cmd += "-gravity NorthWest -crop {}x{}+{}+{} +repage \) ".format( width, row_height[row], left, row_top[row] ) cmd += " -gravity center -background white +append " + out_img_file print(cmd) os.system(cmd) # Draw lines for columns. for col in draw_col_lines: if col <= 0 or col >= n_cols: continue strokewidth = max(int(round(width * 0.005)), 1) pos = col * width cmd = "convert " + out_img_file + " -stroke black " cmd += "-strokewidth {} ".format(strokewidth) cmd += '-draw "line {0},0 {0},10000000" '.format(pos) + out_img_file os.system(cmd) print(cmd) # Resize the combined image if it is too large. print(n_cols * width) if (n_cols * width) > max_total_width: cmd = "convert {0} -resize {1}x +repage {0}".format( out_img_file, max_total_width ) print(cmd) os.system(cmd) print("Saved '{}'.".format(out_img_file)) return width, row_height def create_image_table_tight_centering_per_col( in_img_files, out_img_dir, max_width=2560, draw_col_lines=[] ): n_rows = len(in_img_files) n_cols = len(in_img_files[0]) # Compute width and height of each image. width = 0 row_top = [float("Inf")] * n_rows row_bottom = [-float("Inf")] * n_rows for row in range(n_rows): for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_width = img_right - img_left width = max(width, img_width) row_top[row] = min(row_top[row], img_top) row_bottom[row] = max(row_bottom[row], img_bottom) row_height = [bottom - top for bottom, top in zip(row_bottom, row_top)] if not os.path.exists(out_img_dir): os.makedirs(out_img_dir) # Combine images. for col in range(n_cols): out_img_file = os.path.join(out_img_dir, "{:02d}.png".format(col)) cmd = "convert " for row in range(n_rows): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_h_center = 0.5 * (img_left + img_right) left = int(img_h_center - 0.5 * width) cmd += " \( {} ".format(in_img_files[row][col]) cmd += "-gravity NorthWest -crop {}x{}+{}+{} +repage \) ".format( width, row_height[row], left, row_top[row] ) cmd += " -gravity center -background white -append " + out_img_file print(cmd) os.system(cmd) # Resize the combined image if it is too large. if width > max_width: cmd = "convert {0} -resize {1}x +repage {0}".format(out_img_file, max_width) print(cmd) os.system(cmd) print("Saved '{}'.".format(out_img_file)) return width, row_height def create_image_table_after_crop( in_img_files, out_img_file, lbox=None, tbox=None, rbox=None, dbox=None, max_total_width=2560, draw_col_lines=[], transpose=False, verbose=False, line_multi=None, ): out_img_file = str(out_img_file) if not isinstance(in_img_files[0], list): in_img_files = [in_img_files] in_img_files = [[x for x in row if len(str(x)) != 0] for row in in_img_files] if transpose: x = np.array(in_img_files) in_img_files = x.transpose().tolist() n_rows = len(in_img_files) n_cols = len(in_img_files[0]) # Compute width and height of each image. width = 0 row_top = [float("Inf")] * n_rows row_bottom = [-float("Inf")] * n_rows for row in range(n_rows): for col in range(n_cols): img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) # img_left, img_top, img_right, img_bottom = lbox, tbox, rbox, dbox img_left = img_left if lbox is None else lbox img_top = img_top if tbox is None else tbox img_right = img_right if rbox is None else rbox img_bottom = img_bottom if dbox is None else dbox img_width = img_right - img_left width = max(width, img_width) row_top[row] = min(row_top[row], img_top) row_bottom[row] = max(row_bottom[row], img_bottom) row_height = [bottom - top for bottom, top in zip(row_bottom, row_top)] # Combine images. cmd = "convert " for row in range(n_rows): cmd += " \( " for col in range(n_cols): # img_left, img_top, img_right, img_bottom = lbox, tbox, rbox, dbox img_left, img_top, img_right, img_bottom = get_bbox(in_img_files[row][col]) img_left = img_left if lbox is None else lbox img_top = img_top if tbox is None else tbox img_right = img_right if rbox is None else rbox img_bottom = img_bottom if dbox is None else dbox img_h_center = 0.5 * (img_left + img_right) left = int(img_h_center - 0.5 * width) cmd += " \( {} ".format(in_img_files[row][col]) cmd += "-gravity NorthWest -crop {}x{}+{}+{} +repage \) ".format( width, row_height[row], left, row_top[row] ) cmd += " -gravity center -background white +append \) " cmd += "-append " + out_img_file if verbose: print(cmd) os.system(cmd) # Draw lines for columns. for col in draw_col_lines: if col <= 0 or col >= n_cols: continue strokewidth = max(int(round(width * 0.005)), 1) if line_multi is not None: strokewidth *= line_multi pos = col * width cmd = "convert " + out_img_file + " -stroke black " cmd += "-strokewidth {} ".format(strokewidth) cmd += '-draw "line {0},0 {0},10000000" '.format(pos) + out_img_file if verbose: print(cmd) os.system(cmd) # Resize the combined image if it is too large. # print(n_cols * width) # if (n_cols * width) > max_total_width: # cmd = "convert {0} -resize {1}x +repage {0}".format( # out_img_file, max_total_width # ) # print(cmd) # os.system(cmd) print("Saved '{}'.".format(out_img_file)) return width, row_height def make_2dgrid(input_list, num_rows=None, num_cols=None): # if num_rows * num_cols != len(input_list): # raise Warning("Number of rows and columns do not match the length of the input list.") if num_rows is None and num_cols is not None: num_rows = len(input_list) // num_cols + 1 output_list = [] for i in range(num_rows): row = [] for j in range(num_cols): if i * num_cols + j >= len(input_list): break row.append(input_list[i * num_cols + j]) output_list.append(row) return output_list