Cropping investigation
This commit is contained in:
1 parent
48602c24f1
commit
bd1964ffcd
10 files changed
+814
-24
No files matched your search
@@ -0,0 +1,118 @@
|
||||
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()
|
||||
Reference in new issue
Block a user