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