Implement border detection

This commit is contained in:
henriquenakashima 2023-07-17 13:10:08 -04:00
parent b0eabd1297
commit 7eae871fc6
5 changed files with 224 additions and 18 deletions

View file

@ -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)

179
border_detection.py Normal file
View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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