diff --git a/base_proxy.py b/base_proxy.py index 188b25b..efdd868 100644 --- a/base_proxy.py +++ b/base_proxy.py @@ -3,6 +3,24 @@ from pathlib import Path CARD_W_INCHES = 2.48031 # 63mm CARD_H_INCHES = 3.46457 # 88mm +W_INCHES_EXPECTED = 2.72 +H_INCHES_EXPECTED = 3.7 + LEAVE_BLEED_INCHES = 0.04 BASE_DIR = Path(__file__).parent + +SAVE_INTERMEDIATE_FOR_DEBUG = False + + +def make_intermediate_name(input_path: Path, infix: str): + return BASE_DIR / 'cropped_cards' / f'{input_path.stem}.{infix}.jpg' + + +def in_to_px(inches: float, dpi: int) -> int: + return round(inches * dpi) + + +def mil_to_px(mils: int, dpi: int) -> int: + return int(mils * dpi // 1000) + diff --git a/border_detection.py b/border_detection.py new file mode 100644 index 0000000..0a6ac22 --- /dev/null +++ b/border_detection.py @@ -0,0 +1,179 @@ +import dataclasses +from typing import Dict, List, Tuple + +from PIL import ImageDraw, Image + +from base_proxy import * + + +TOP = 'top' +BOTTOM = 'bottom' +LEFT = 'left' +RIGHT = 'right' + + +@dataclasses.dataclass +class PixelColorBuckets: + blacks: int = 0 + whites: int = 0 + others: int = 0 + + +def border_color_to_rgb(border_color: str) -> Tuple[int, int, int]: + return { + Borders.SOLID_BLACK: (0, 0, 0), + Borders.SOLID_WHITE: (255, 255, 255), + }.get(border_color) + + +@dataclasses.dataclass +class Borders: + SOLID_BLACK = 'solid_black' + SOLID_WHITE = 'solid_white' + BORDERLESS = 'borderless' + UNKNOWN = 'unknown' + + dpi: float = None + color: Dict[str, str] = dataclasses.field(default_factory=dict) + thickness: Dict[str, int] = dataclasses.field(default_factory=dict) + + def inform_color(self, edge: str, all_color_buckets: Dict[int, PixelColorBuckets]) -> None: + any_others = any(b.others > 0 for b in all_color_buckets.values()) + any_blacks = any(b.blacks > 0 for b in all_color_buckets.values()) + any_whites = any(b.whites > 0 for b in all_color_buckets.values()) + if any_others: + self.color[edge] = Borders.BORDERLESS + elif any_blacks and any_whites: + self.color[edge] = Borders.UNKNOWN + elif any_blacks: + self.color[edge] = Borders.SOLID_BLACK + elif any_whites: + self.color[edge] = Borders.SOLID_WHITE + else: + self.color[edge] = Borders.UNKNOWN + + def inform_depth(self, edge: str, depths: Dict[int, int]) -> None: + self.thickness[edge] = min(depths.values()) + + def get_border_thickness_in(self, edge: str) -> float: + return self.thickness[edge] / self.dpi + + +@dataclasses.dataclass +class CardBox: + left: int + top: int + width: int + height: int + + def get_sample_xs(self, sample_count: int) -> List[int]: + xs = [] + for i in range(sample_count): + x = self.left + (i * (self.width - 1)) // (sample_count - 1) + xs.append(x) + return xs + + def get_sample_ys(self, sample_count: int) -> List[int]: + ys = [] + for i in range(sample_count): + y = self.top + (i * (self.height - 1)) // (sample_count - 1) + ys.append(y) + return ys + + +def categorize_pixels_in_line(im: Image, start: Tuple[int, int], increment: Tuple[int, int], depth: int) -> PixelColorBuckets: + color_buckets = PixelColorBuckets() + + xy = start + for n in range(depth): + pixel = im.getpixel(xy) + if pixel == (0, 0, 0): + color_buckets.blacks += 1 + elif pixel == (255, 255, 255): + color_buckets.whites += 1 + else: + color_buckets.others += 1 + xy = (xy[0] + increment[0], xy[1] + increment[1]) + + return color_buckets + + +def detect_depth_in_column(im: Image, color: str, fixed_x: int, start: int, increment: int, max_depth: int) -> int: + expected_rgb = border_color_to_rgb(color) + y = start + for n in range(max_depth): + pixel = im.getpixel((fixed_x, y)) + if expected_rgb != pixel: + return n - 1 + y += increment + else: + return max_depth + + +def detect_borders(loaded_input_card: 'InputCardImage') -> Borders: + dpi: int = loaded_input_card.input_dpi + im: Image = loaded_input_card.original_im + assert dpi is not None + assert im is not None + + input_w = im.size[0] + input_h = im.size[1] + card_w = in_to_px(CARD_W_INCHES, dpi) + card_h = in_to_px(CARD_H_INCHES, dpi) + bleed_left = (input_w - card_w) // 2 + bleed_top = (input_h - card_h) // 2 + card_box = CardBox(bleed_left, bleed_top, card_w, card_h) + + if SAVE_INTERMEDIATE_FOR_DEBUG: + draw = ImageDraw.Draw(im) + + borders = Borders() + borders.dpi = dpi + + depth_to_calibrate = mil_to_px(10, dpi) + max_depth_to_check = in_to_px(0.5, dpi) + border_depth = 0 + + # Sample 30 lines + xs = card_box.get_sample_xs(30) + ys = card_box.get_sample_ys(30) + + # Determine top and bottom border color + for edge, start_y, increment_y in [(TOP, 0, +1), (BOTTOM, input_h - 1, -1)]: + all_color_buckets = {} + for fixed_x in xs: + color_buckets = categorize_pixels_in_line(im, (fixed_x, start_y), (0, increment_y), depth_to_calibrate) + all_color_buckets[fixed_x] = color_buckets + borders.inform_color(edge, all_color_buckets) + + # Determine left and right border color + for edge, start_x, increment_x in [(LEFT, 0, +1), (RIGHT, input_w - 1, -1)]: + all_color_buckets = {} + for fixed_y in ys: + color_buckets = categorize_pixels_in_line(im, (start_x, fixed_y), (increment_x, 0), depth_to_calibrate) + all_color_buckets[fixed_y] = color_buckets + borders.inform_color(edge, all_color_buckets) + + # Determine top and bottom border thickness + for edge, start, increment in [(TOP, 0, +1), (BOTTOM, input_h - 1, -1)]: + all_depths = {} + color = borders.color[edge] + if color in (Borders.SOLID_BLACK, Borders.SOLID_WHITE): + for fixed_x in xs: + depth = detect_depth_in_column(im, color, fixed_x, start, increment, max_depth_to_check) + all_depths[fixed_x] = depth + if SAVE_INTERMEDIATE_FOR_DEBUG: + draw.line([(fixed_x, start), (fixed_x, start + increment * depth)], fill='magenta') + + borders.inform_depth(edge, all_depths) + + if SAVE_INTERMEDIATE_FOR_DEBUG: + depth_calculated = borders.thickness[edge] + border_y_calculated = start + increment * depth_calculated + draw.line([(0, border_y_calculated), (input_w - 1, border_y_calculated)], fill='magenta') + + if SAVE_INTERMEDIATE_FOR_DEBUG: + path = make_intermediate_name(loaded_input_card.path, 'step0bordercheck') + im.save(path, 'JPEG', dpi=(dpi, dpi), quality=100, subsampling=0) + + return borders diff --git a/card_images.py b/card_images.py index 3c2ab58..6f01fb8 100644 --- a/card_images.py +++ b/card_images.py @@ -2,25 +2,24 @@ from pathlib import Path from PIL import Image -from base_proxy import BASE_DIR +from base_proxy import * +from border_detection import Borders, detect_borders from sheet_layout import SheetLayout, CardImageTransfer -W_INCHES_EXPECTED = 2.72 -H_INCHES_EXPECTED = 3.7 - -SAVE_INTERMEDIATE_FOR_DEBUG = False - - -def make_intermediate_name(input_path: Path, infix: str): - return BASE_DIR / 'cropped_cards' / f'{input_path.stem}.{infix}.jpg' - - class InputCardImage: def __init__(self, path: Path): self.path = path self.input_dpi = None self.original_im = None + self.borders = None + + def get_borders(self) -> Borders: + if self.borders is None: + self.load() + self.borders = detect_borders(self) + self.unload() + return self.borders def load(self) -> None: with Image.open(self.path) as im: @@ -35,9 +34,11 @@ class InputCardImage: self.input_dpi = dpi_by_w self.original_im = rgb_im - def get_cropped_and_resized(self, layout: SheetLayout, transfer: CardImageTransfer) -> Image: - new_dpi = layout.dpi + def unload(self) -> None: + self.input_dpi = None + self.original_im = None + def get_cropped_and_resized(self, layout: SheetLayout, transfer: CardImageTransfer) -> Image: self.load() im = self.original_im print(f'Input is {im.size[0]} x {im.size[1]}') @@ -72,4 +73,6 @@ class InputCardImage: if SAVE_INTERMEDIATE_FOR_DEBUG: im.save(make_intermediate_name(self.path, 'step2crop'), 'JPEG', dpi=(layout.dpi, layout.dpi), quality=100, subsampling=0) + self.unload() + return im diff --git a/crop_card.py b/crop_card.py index 2759076..9eb6a44 100644 --- a/crop_card.py +++ b/crop_card.py @@ -13,6 +13,9 @@ from sheet_layout import SheetLayout, CardImageTransfer import util +DRY_RUN = False + + CUT_GUIDE_WIDTH_INCHES = 0.006 LENGTH_CUT_GUIDE_INCHES = 0.04 @@ -96,8 +99,9 @@ def create_sheet(input_cards: List[InputCardImage], sheet_filepath: Path, layout draw.line([(right_x, bottom_y), (right_x - guide_len, bottom_y)], fill = color, width = guide_thickness) # Save sheet - # sheet.save(sheet_filepath, 'JPEG', dpi=(layout.dpi, layout.dpi), quality=100, subsampling=0, icc_profile=icc_profile.tobytes()) - sheet.save(sheet_filepath, 'JPEG', dpi=(layout.dpi, layout.dpi), quality=100, subsampling=0) + if not DRY_RUN: + # sheet.save(sheet_filepath, 'JPEG', dpi=(layout.dpi, layout.dpi), quality=100, subsampling=0, icc_profile=icc_profile.tobytes()) + sheet.save(sheet_filepath, 'JPEG', dpi=(layout.dpi, layout.dpi), quality=100, subsampling=0) def main(): @@ -106,6 +110,8 @@ def main(): print(all_input_cards_paths) all_input_cards = [InputCardImage(p) for p in all_input_cards_paths] + for input_card in all_input_cards: + print(f'{input_card.path} has borders {input_card.get_borders()}') layout = sheet_layout.letter_portrait_3x3_sheet_layout_120milborder() sheets_dir = BASE_DIR / 'sheets' diff --git a/sheet_layout.py b/sheet_layout.py index 7bd4902..ddf33a6 100644 --- a/sheet_layout.py +++ b/sheet_layout.py @@ -2,7 +2,7 @@ import dataclasses import math from typing import List, Tuple -from base_proxy import CARD_W_INCHES, CARD_H_INCHES, LEAVE_BLEED_INCHES +from base_proxy import * @dataclasses.dataclass @@ -122,10 +122,10 @@ class SheetLayout: print(self.__dict__) def in_to_px(self, inches: float) -> int: - return round(inches * self.dpi) + return in_to_px(inches, self.dpi) def mil_to_px(self, mils: int) -> int: - return mils * self.dpi // 1000 + return mil_to_px(mils, self.dpi) def create_card_image_transfer(self, y_index: int, x_index: int) -> CardImageTransfer: top_row = y_index == 0