119 lines
5.6 KiB
Python
119 lines
5.6 KiB
Python
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
from copienator.crop_diagnostics import check_protected_regions, save_diagnostics
|
|
from copienator.ink_detection import detect_bounds
|
|
|
|
|
|
class CropDiagnosticsTests(unittest.TestCase):
|
|
def scan(self, *, dark=False, skew=False):
|
|
rgb = np.full((900,650,3),255,np.uint8)
|
|
if dark:
|
|
for y in range(15,900,20):
|
|
cv2.line(rgb,(0,y),(649,y),(65,65,65),1)
|
|
for x in range(10,650,20):
|
|
cv2.line(rgb,(x,0),(x,899),(65,65,65),1)
|
|
cv2.putText(rgb,'answer',(100,400),cv2.FONT_HERSHEY_SIMPLEX,1,(20,50,180),2)
|
|
cv2.putText(rgb,'more',(100,560),cv2.FONT_HERSHEY_SIMPLEX,.8,(30,30,30),2)
|
|
if skew:
|
|
rgb = cv2.warpAffine(rgb,cv2.getRotationMatrix2D((325,450),2,1),
|
|
(650,900),borderValue=(255,255,255))
|
|
return rgb
|
|
|
|
def test_diagnostics_leave_decisions_and_json_contract_unchanged(self):
|
|
for dark,skew in [(False,False),(True,False),(True,True)]:
|
|
with self.subTest(dark=dark,skew=skew):
|
|
rgb = self.scan(dark=dark,skew=skew)
|
|
before = rgb.copy()
|
|
normal = detect_bounds(rgb,dpi=150)
|
|
debug = {'stale': 'discard this'}
|
|
observed = detect_bounds(rgb,dpi=150,diagnostics=debug)
|
|
self.assertEqual(normal,observed)
|
|
json.dumps(observed)
|
|
np.testing.assert_array_equal(rgb,before)
|
|
self.assertNotIn('stale',debug)
|
|
for name,mask in debug['masks'].items():
|
|
self.assertEqual(mask.shape,rgb.shape[:2],name)
|
|
self.assertEqual(mask.dtype,np.bool_,name)
|
|
masks = debug['masks']
|
|
self.assertFalse(np.any(masks['strong_seeds'] & ~masks['foreground']))
|
|
self.assertFalse(np.any(masks['recovered_weak'] & masks['strong_seeds']))
|
|
for item in debug['thresholds']:
|
|
for edge in ['top','bottom']:
|
|
for component in item['limiting_components'][edge]:
|
|
box = component['bbox_px']
|
|
self.assertEqual(box[1] if edge=='top' else box[3],
|
|
item['extent'][0 if edge=='top' else 1])
|
|
self.assertGreater(component['seed_pixels'],0)
|
|
|
|
def test_blank_fallback_has_empty_foreground_and_explanation(self):
|
|
rgb = np.full((400,300,3),255,np.uint8)
|
|
debug = {}
|
|
result = detect_bounds(rgb,diagnostics=debug)
|
|
self.assertEqual((result['top_px'],result['bottom_px']),(0,400))
|
|
self.assertFalse(debug['masks']['foreground'].any())
|
|
self.assertEqual(debug['edges']['bottom']['reason'],'no-reliable-ink-seeds')
|
|
|
|
def test_faint_unseeded_candidate_is_distinguished_from_recovered_ink(self):
|
|
rgb = self.scan()
|
|
cv2.putText(rgb,'weak',(100,800),cv2.FONT_HERSHEY_SIMPLEX,1,(130,160,205),2)
|
|
debug = {}
|
|
detect_bounds(rgb,dpi=150,diagnostics=debug)
|
|
self.assertTrue(debug['masks']['unretained_weak'][770:810,100:220].any())
|
|
self.assertFalse(debug['masks']['foreground'][770:810,100:220].any())
|
|
|
|
def test_exported_masks_match_actual_pixels_and_report_is_serializable(self):
|
|
rgb = self.scan(dark=True,skew=True)
|
|
debug = {}
|
|
result = detect_bounds(rgb,dpi=150,diagnostics=debug)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
files = save_diagnostics(rgb,result,debug,Path(tmp),width_mm=110,height_mm=152)
|
|
for name,path in files['masks'].items():
|
|
with Image.open(Path(tmp)/path) as im:
|
|
np.testing.assert_array_equal(np.array(im).astype(bool),debug['masks'][name])
|
|
report = json.loads((Path(tmp)/files['report']).read_text())
|
|
self.assertEqual(report['detector'],result)
|
|
self.assertEqual(report['shape'],list(rgb.shape[:2]))
|
|
for path in files['overlays'].values():
|
|
self.assertTrue((Path(tmp)/path).is_file())
|
|
|
|
|
|
class ProtectedContentTests(unittest.TestCase):
|
|
def check(self,top,bottom,box):
|
|
return check_protected_regions([dict(id='ink',label='Real ink',bbox_mm=box)],
|
|
dict(top_px=top,bottom_px=bottom),width_mm=80,height_mm=100,shape=(1000,800))[0]
|
|
|
|
def test_both_boundaries_can_clip_a_protected_region(self):
|
|
top = self.check(210,800,[10,20,50,30])
|
|
self.assertFalse(top['passed'])
|
|
self.assertEqual(top['clipped_edges'],['top'])
|
|
self.assertAlmostEqual(top['clipped_area_fraction'],.1)
|
|
bottom = self.check(100,790,[10,70,50,80])
|
|
self.assertFalse(bottom['passed'])
|
|
self.assertEqual(bottom['clipped_edges'],['bottom'])
|
|
self.assertAlmostEqual(bottom['clipped_area_fraction'],.1)
|
|
|
|
def test_contact_at_boundary_and_short_page_physical_height(self):
|
|
self.assertTrue(self.check(200,300,[10,20,50,30])['passed'])
|
|
row = check_protected_regions([dict(id='end',label='End',bbox_mm=[2,17,20,19])],
|
|
dict(top_px=0,bottom_px=180),width_mm=40,height_mm=20,shape=(200,400))[0]
|
|
self.assertFalse(row['passed'])
|
|
self.assertAlmostEqual(row['clipped_area_fraction'],.5)
|
|
|
|
def test_full_removal_and_invalid_ground_truth(self):
|
|
self.assertEqual(self.check(400,800,[10,20,50,30])['clipped_area_fraction'],1)
|
|
with self.assertRaises(ValueError):
|
|
self.check(0,1000,[10,20,90,30])
|
|
with self.assertRaises(ValueError):
|
|
self.check(0,1000,[10,30,50,20])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|