import unittest from unittest.mock import patch import cv2 import numpy as np from copienator.ink_detection import detect_bounds class PaperRulingTests(unittest.TestCase): def scan(self, colour, *, warp=False): image = np.full((1200, 850, 3), 255, np.uint8) for y in range(20, 1200, 20): cv2.line(image, (0, y), (849, y), colour, 1) for x in range(10, 850, 20): cv2.line(image, (x, 0), (x, 1199), colour, 1) cv2.line(image, (80, 0), (80, 1199), (210, 70, 80), 2) cv2.putText(image, 'answer', (120, 410), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 60, 180), 2) cv2.putText(image, 'more', (120, 710), cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 60, 180), 2) if warp: yy, xx = np.indices(image.shape[:2], dtype=np.float32) image = cv2.remap(image, xx+2*np.sin(yy/95), yy, cv2.INTER_LINEAR, borderValue=(255,255,255)) return image def test_default_path_does_not_run_experimental_colour_model(self): image = self.scan((70, 110, 225)) with patch('copienator.ink_detection.ruling_suppression', side_effect=AssertionError('Expensive colour model invoked')): result = detect_bounds(image, dpi=150) self.assertLess(result['top_px'], 410) self.assertGreater(result['bottom_px'], 710) self.assertFalse(result.get('ruling_cleanup', False)) def test_saturated_coloured_grid_and_margin_do_not_seed_blank_edges(self): for colour in [(65, 65, 65), (70, 110, 225), (155, 170, 240)]: for warp in [False, True]: with self.subTest(colour=colour, warp=warp): image = self.scan(colour, warp=warp) debug = {} result = detect_bounds(image, dpi=150, colour_cleanup=True, diagnostics=debug) self.assertGreater(result['top_px'], 280) self.assertLess(result['bottom_px'], 810) self.assertTrue(result['ruling_cleanup']) self.assertTrue(debug['masks']['paper_suppressed'].any()) self.assertFalse(debug['masks']['foreground'][:250].any()) def test_writing_crossing_ruling_keeps_both_outer_bounds(self): image = self.scan((70, 110, 225)) cv2.putText(image, 'top', (120, 200), cv2.FONT_HERSHEY_SIMPLEX, .7, (190, 20, 20), 2) cv2.putText(image, 'bottom', (120, 1000), cv2.FONT_HERSHEY_SIMPLEX, .7, (20, 20, 20), 2) result = detect_bounds(image, dpi=150, colour_cleanup=True) self.assertLess(result['top_px'], 180) self.assertGreater(result['bottom_px'], 1000) def test_isolated_bars_and_axes_are_not_a_ruling_colour_family(self): for colour in [(190, 20, 20), (20, 20, 20), (20, 90, 190)]: with self.subTest(colour=colour): image = self.scan((70, 110, 225)) cv2.line(image, (100, 1020), (700, 1020), colour, 3) cv2.line(image, (425, 980), (425, 1120), colour, 3) result = detect_bounds(image, dpi=150, colour_cleanup=True) self.assertGreater(result['bottom_px'], 1120) def test_grid_colour_with_a_thicker_answer_bar_is_preserved(self): colour = (70, 110, 225) image = self.scan(colour) cv2.line(image, (150, 1020), (650, 1020), colour, 5) self.assertGreater(detect_bounds(image, dpi=150, colour_cleanup=True)['bottom_px'], 1020) def test_regular_fraction_bars_inside_an_answer_are_not_paper(self): image = np.full((1200,850,3),255,np.uint8) cv2.putText(image,'answer',(120,300),cv2.FONT_HERSHEY_SIMPLEX, 1,(20,60,180),2) for y in (400,550,700,850,1000): cv2.line(image,(130,y),(700,y),(20,20,20),3) result = detect_bounds(image,dpi=150, colour_cleanup=True) self.assertGreater(result['bottom_px'],1000) self.assertFalse(result.get('ruling_cleanup',False)) if __name__ == '__main__': unittest.main()