|
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) |
|
|
|
|
|
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): |
|
|
|
|
|
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]) |
|
|
|
|
|
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)] |
|
|
|
|
|
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) |
|
|
|
|
|
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(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]) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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]) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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]) |
|
|
|
|
|
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_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)] |
|
|
|
|
|
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_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) |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print("Saved '{}'.".format(out_img_file)) |
|
|
|
return width, row_height |
|
|
|
|
|
def make_2dgrid(input_list, num_rows=None, num_cols=None): |
|
|
|
|
|
|
|
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 |
|
|