178 lines
7.6 KiB
Python
178 lines
7.6 KiB
Python
import unittest
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from copienator.ink_detection import detect_bounds, _paper_blur
|
|
|
|
|
|
class InkDetectionTests(unittest.TestCase):
|
|
def test_fast_background_blur_matches_opencv_pixel_for_pixel(self):
|
|
rng = np.random.default_rng(42)
|
|
for shape in ((97,131), (241,319)):
|
|
gray = rng.integers(0,256,shape,dtype=np.uint8)
|
|
for dpi in (100,150,200,300):
|
|
with self.subTest(shape=shape,dpi=dpi):
|
|
expected = cv2.GaussianBlur(gray,(0,0),dpi/8)
|
|
np.testing.assert_array_equal(_paper_blur(gray,dpi/8),expected)
|
|
|
|
def scan(self):
|
|
image = np.full((1200, 850, 3), 255, np.uint8)
|
|
for y in range(20, 1200, 20):
|
|
cv2.line(image, (0, y), (849, y), (160, 155, 205), 1)
|
|
for x in range(10, 850, 20):
|
|
cv2.line(image, (x, 0), (x, 1199), (160, 155, 205), 1)
|
|
for y in (100, 180, 400, 480, 800, 880, 1050, 1130):
|
|
cv2.ellipse(image, (25, y), (12, 20), 0, 0, 300, (155,155,155), 2)
|
|
cv2.putText(image, 'x + y = 2', (110, 400), cv2.FONT_HERSHEY_SIMPLEX,
|
|
1, (20, 90, 190), 2)
|
|
cv2.putText(image, 'answer = 42', (110, 700), cv2.FONT_HERSHEY_SIMPLEX,
|
|
1, (20, 90, 190), 2)
|
|
return image
|
|
|
|
def test_crops_past_holes_and_grid(self):
|
|
r = detect_bounds(self.scan(), dpi=150)
|
|
self.assertGreater(r['top_px'], 250)
|
|
self.assertLess(r['top_px'], 370)
|
|
self.assertGreater(r['bottom_px'], 700)
|
|
self.assertLess(r['bottom_px'], 800)
|
|
|
|
def test_isolated_margin_note_survives(self):
|
|
image = self.scan()
|
|
cv2.putText(image, '1', (4, 1110), cv2.FONT_HERSHEY_SIMPLEX,
|
|
.65, (20, 90, 190), 2)
|
|
self.assertGreater(detect_bounds(image, dpi=150)['bottom_px'], 1110)
|
|
|
|
def test_long_fraction_bar_on_grid_survives(self):
|
|
for colour in ((20,90,190), (20,20,20), (190,20,20)):
|
|
with self.subTest(colour=colour):
|
|
image = self.scan()
|
|
cv2.line(image, (100, 1020), (700, 1020), colour, 3)
|
|
self.assertGreater(detect_bounds(image, dpi=150)['bottom_px'], 1020)
|
|
|
|
def test_black_annotation_survives(self):
|
|
image = self.scan()
|
|
cv2.putText(image, 'note', (600, 1100), cv2.FONT_HERSHEY_SIMPLEX,
|
|
.7, (30,30,30), 2)
|
|
self.assertGreater(detect_bounds(image, dpi=150)['bottom_px'], 1100)
|
|
|
|
def test_blank_and_pencil_only_pages_are_retained(self):
|
|
for pencil in (False, True):
|
|
image = np.full((1200,850,3),255,np.uint8)
|
|
if pencil:
|
|
cv2.putText(image, 'pencil', (100,600), cv2.FONT_HERSHEY_SIMPLEX,
|
|
1, (195,195,195), 2)
|
|
r = detect_bounds(image, dpi=150)
|
|
self.assertEqual((r['top_px'],r['bottom_px']), (0,1200))
|
|
self.assertEqual(r['status'], 'review-no-ink-seeds')
|
|
|
|
def test_skewed_colour_scan(self):
|
|
matrix = cv2.getRotationMatrix2D((425,600),2,1)
|
|
image = cv2.warpAffine(self.scan(),matrix,(850,1200),borderValue=(255,255,255))
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertGreater(r['top_px'],250)
|
|
self.assertLess(r['top_px'],370)
|
|
self.assertGreater(r['bottom_px'],715)
|
|
self.assertLess(r['bottom_px'],820)
|
|
|
|
def test_weak_stroke_attached_to_ink_is_recovered(self):
|
|
image = np.full((1200,850,3),255,np.uint8)
|
|
cv2.rectangle(image,(400,500),(410,530),(20,90,190),-1)
|
|
cv2.rectangle(image,(400,531),(410,540),(130,170,205),-1)
|
|
r = detect_bounds(image,dpi=150,padding_mm=0,min_crop_mm=0)
|
|
self.assertGreaterEqual(r['bottom_px'],541)
|
|
|
|
def dark_grid(self):
|
|
image = np.full((1200,850,3),255,np.uint8)
|
|
for y in range(20,1200,20):
|
|
cv2.line(image,(0,y),(849,y),(65,65,65),1)
|
|
for x in range(10,850,20):
|
|
cv2.line(image,(x,0),(x,1199),(65,65,65),1)
|
|
cv2.putText(image,'x + y = 2',(100,400),cv2.FONT_HERSHEY_SIMPLEX,
|
|
1,(20,20,20),2)
|
|
cv2.putText(image,'answer = 42',(100,700),cv2.FONT_HERSHEY_SIMPLEX,
|
|
1,(20,20,20),2)
|
|
return image
|
|
|
|
def test_dark_grid_does_not_seed_entire_page(self):
|
|
r = detect_bounds(self.dark_grid(),dpi=150)
|
|
self.assertTrue(r['paper_cleanup'])
|
|
self.assertGreater(r['top_px'],250)
|
|
self.assertLess(r['top_px'],370)
|
|
self.assertGreater(r['bottom_px'],700)
|
|
self.assertLess(r['bottom_px'],820)
|
|
|
|
def test_faint_isolated_note_on_dark_grid_survives(self):
|
|
image = self.dark_grid()
|
|
cv2.putText(image,'pencil',(100,1100),cv2.FONT_HERSHEY_SIMPLEX,
|
|
.7,(190,190,190),2)
|
|
self.assertGreater(detect_bounds(image,dpi=150)['bottom_px'],1100)
|
|
|
|
def test_sparse_central_pencil_on_dark_grid_survives(self):
|
|
image = self.dark_grid()
|
|
# Thin, pale handwriting crossing the ruling is split into sparse
|
|
# components. Its central position distinguishes it from page holes.
|
|
cv2.putText(image,'result',(330,1090),cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,
|
|
.85,(155,155,155),1)
|
|
self.assertGreater(detect_bounds(image,dpi=150)['bottom_px'],1090)
|
|
|
|
def test_large_unruled_diagram_is_not_paper(self):
|
|
image = np.full((1200,850,3),255,np.uint8)
|
|
cv2.rectangle(image,(100,100),(750,1100),(20,20,20),3)
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertFalse(r['paper_cleanup'])
|
|
self.assertLess(r['top_px'],100)
|
|
self.assertGreater(r['bottom_px'],1100)
|
|
|
|
def test_dark_grid_preserves_black_fraction_bar(self):
|
|
image = self.dark_grid()
|
|
cv2.line(image,(150,1020),(700,1020),(0,0,0),3)
|
|
self.assertGreater(detect_bounds(image,dpi=150)['bottom_px'],1020)
|
|
|
|
def test_disconnected_dark_grid_is_still_recognized(self):
|
|
image = self.dark_grid()
|
|
for x in range(170,850,170):
|
|
image[:,x:x+5] = 255
|
|
for y in range(200,1200,200):
|
|
image[y:y+5,:] = 255
|
|
cv2.putText(image,'x + y = 2',(100,400),cv2.FONT_HERSHEY_SIMPLEX,
|
|
1,(20,20,20),2)
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertTrue(r['paper_cleanup'])
|
|
self.assertGreater(r['top_px'],250)
|
|
self.assertLess(r['top_px'],370)
|
|
self.assertGreater(r['bottom_px'],700)
|
|
self.assertLess(r['bottom_px'],850)
|
|
|
|
def test_repeated_dark_holes_on_either_side(self):
|
|
for right in (False, True):
|
|
with self.subTest(right=right):
|
|
image = self.dark_grid()
|
|
x = 820 if right else 25
|
|
for y in (110, 370, 630, 890, 1130):
|
|
cv2.ellipse(image, (x,y), (12,20), 0, 0, 300, (35,35,35), 2)
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertLess(r['bottom_px'],850)
|
|
# The same column can contain handwriting as well as holes.
|
|
cv2.putText(image,'7',(x-5,1060),cv2.FONT_HERSHEY_SIMPLEX,
|
|
.7,(20,20,20),2)
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertGreater(r['bottom_px'],1060)
|
|
|
|
def test_faint_page_number_in_outer_band_does_not_block_crop(self):
|
|
image = self.dark_grid()
|
|
cv2.putText(image,'4/',(3,1160),cv2.FONT_HERSHEY_SIMPLEX,
|
|
.55,(130,130,130),1)
|
|
self.assertLess(detect_bounds(image,dpi=150)['bottom_px'],850)
|
|
|
|
def test_faded_neutral_grid(self):
|
|
image = self.dark_grid()
|
|
image[np.all(image == 65,axis=2)] = 145
|
|
r = detect_bounds(image,dpi=150)
|
|
self.assertTrue(r['paper_cleanup'])
|
|
self.assertLess(r['bottom_px'],850)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|