Automatic page splitting

This commit is contained in:
sebastien committed 2026-11-03 22:08:36 +01:00
1 parent d4404e4c27
commit 78f3e4b2f0
13 files changed
+1959 -40

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
import tempfile
import unittest
import json
from pathlib import Path
import numpy as np
import pymupdf
from copienator.auto_page_split import analyse_ink, analyse_page, classify_format, plan_masks, run
class AutomaticSplitTests(unittest.TestCase):
def mask(self):
return np.zeros((300, 420), bool)
def plan(self, mask, kind='a3'):
return plan_masks(mask, 25.4, kind)
def test_formats_use_size_not_landscape_alone(self):
pt = 72/25.4
for w,h,expected in [(420,297,'a3'),(210,297,'a4'),(297,210,'a4'),
(297,420,'unknown'),(320,200,'unknown')]:
self.assertEqual(classify_format(w*pt,h*pt),expected)
def test_both_directions_extend_independently(self):
mask = self.mask()
mask[40:43,150:218] = True
mask[100:103,203:280] = True
plan = self.plan(mask)
self.assertEqual([x['owner'] for x in plan['crossings']],['left','right'])
self.assertEqual(plan['extensions_mm'],[10,9])
self.assertTrue(all(o['keep'] for o in plan['outputs']))
def test_disconnected_letters_form_a_line(self):
mask = self.mask()
for x in range(170,219,4):
mask[40:44,x:x+2] = True
plan = self.plan(mask)
self.assertGreater(plan['extensions_mm'][0],0)
self.assertEqual(plan['crossings'][0]['owner'],'left')
def test_detached_terminal_strokes_follow_supported_text_on_either_side(self):
mask=self.mask()
mask[40:44,160:199]=True
mask[41:45,205:219]=True
mask[100:104,221:260]=True
mask[101:105,201:215]=True
plan=self.plan(mask)
self.assertEqual([c['owner'] for c in plan['crossings']],['left','right'])
self.assertGreater(plan['extensions_mm'][0],0)
self.assertGreater(plan['extensions_mm'][1],0)
def test_fold_only_groups_are_artifacts_without_page_anchors(self):
mask=self.mask()
mask[40:43,205:215]=True
plan=self.plan(mask)
self.assertEqual(plan['crossings'][0]['owner'],'artifact')
self.assertEqual(plan['extensions_mm'],[0,0])
self.assertFalse(plan['review_reasons'])
def test_pale_ruling_does_not_attach_to_nearby_text_but_dark_bar_does(self):
mask=np.zeros((600,840),bool)
mask[100:103,418:468]=True
mask[100:120,490:580]=True
credible=np.zeros_like(mask)
credible[100:120,490:580]=True
for colour,owner in [((134,162,200),'artifact'),((80,80,80),'right')]:
with self.subTest(colour=colour):
rgb=np.full((*mask.shape,3),255,np.uint8)
rgb[mask]=(20,20,20)
rgb[100:103,418:468]=colour
plan=plan_masks(mask,200,'a3',content_ink=credible,rgb=rgb)
self.assertEqual(plan['crossings'][0]['owner'],owner)
def test_overflow_does_not_make_other_page_nonblank(self):
mask = self.mask()
mask[40:43,150:218] = True
plan = self.plan(mask)
self.assertEqual([o['state'] for o in plan['outputs']],['content','blank'])
self.assertFalse(plan['outputs'][1]['keep'])
def test_ambiguous_crossing_preserved_on_both(self):
mask = self.mask()
mask[40:43,180:250] = True
plan = self.plan(mask)
self.assertIn('ambiguous-centre-writing',plan['review_reasons'])
self.assertLess(plan['outputs'][1]['rect_px'][0],180)
self.assertGreater(plan['outputs'][0]['rect_px'][2],250)
def test_unknown_format_and_a4(self):
mask = self.mask()
self.assertTrue(self.plan(mask,kind='unknown')['outputs'][0]['keep'])
mask[40:44,50:54] = True
self.assertEqual(self.plan(mask,kind='a4')['outputs'][0]['rect_px'],[0,0,420,300])
def test_unseeded_scan_noise_does_not_veto_blank(self):
rgb = np.full((600,420,3),255,np.uint8)
rgb[200:203,200:202] = 195
plan = analyse_page(rgb,100,'a4')
self.assertEqual(plan['outputs'][0]['state'],'blank')
self.assertEqual(plan['analysis']['early_written'],[False])
def test_faint_strokes_near_reliable_ink_are_recovered(self):
rgb = np.full((600,420,3),255,np.uint8)
rgb[200:210,200:204] = 195
rgb[200:210,204:208] = 30
ink,_ = analyse_ink(rgb,100)
self.assertTrue(ink[200:210,204:208].any())
self.assertEqual(analyse_page(rgb,100,'a4')['outputs'][0]['state'],'content')
def test_spread_artifacts_cannot_veto_blank_logical_half(self):
spread=self.mask()
spread[100:105,208:213]=True
content=self.mask()
content[30:40,300:350]=True
plan=plan_masks(spread,25.4,'a3',content_ink=content)
self.assertEqual([o['state'] for o in plan['outputs']],['blank','content'])
self.assertEqual(plan['crossings'][0]['owner'],'artifact')
self.assertFalse(plan['review_reasons'])
def test_export_preserves_visible_rotation_cropbox_and_sources(self):
for rotation in (0,90,180,270):
with self.subTest(rotation=rotation), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root/'source.pdf'
with pymupdf.open() as doc:
# Crop offsets and all rotations; displayed geometry is A3.
w,h = (420,297) if rotation%180==0 else (297,420)
pt = 72/25.4
p = doc.new_page(width=w*pt+20,height=h*pt+30)
p.draw_rect(pymupdf.Rect(50,50,w*pt-50,h*pt-50),fill=(0,0,1))
p.draw_rect(pymupdf.Rect(100,100,150,180),fill=(1,0,0))
p.set_cropbox(pymupdf.Rect(10,15,w*pt+10,h*pt+15))
p.set_rotation(rotation)
doc.save(source)
before = source.read_bytes()
summary = run(source,root/'result',dpi=100)
self.assertEqual(summary['output_pages'],2)
self.assertEqual(source.read_bytes(),before)
with pymupdf.open(source) as original, pymupdf.open(root/'result/pdfs/source.pdf') as result:
outputs=json.loads((root/'result/report.json').read_text())['pages'][0]['outputs']
for page,output in zip(result,outputs):
self.assertEqual(page.rotation,0)
self.assertAlmostEqual(page.rect.height,original[0].rect.height,places=3)
# Ambiguous full-width rectangle is deliberately duplicated.
a = original[0].get_pixmap(matrix=pymupdf.Matrix(.3,.3))
b = page.get_pixmap(matrix=pymupdf.Matrix(.3,.3))
self.assertGreater(b.width, a.width*.8)
expected=original[0].get_pixmap(clip=pymupdf.Rect(output['crop_points']))
actual=page.get_pixmap()
aa=np.frombuffer(expected.samples,np.uint8).reshape(expected.height,expected.width,3)
bb=np.frombuffer(actual.samples,np.uint8).reshape(actual.height,actual.width,3)
h,w=min(aa.shape[0],bb.shape[0]),min(aa.shape[1],bb.shape[1])
self.assertLess(np.abs(aa[:h,:w].astype(float)-bb[:h,:w]).mean(),2)
with self.assertRaises(FileExistsError):
run(source,root/'result',dpi=100)
def test_all_blank_copy_has_report_without_invalid_empty_pdf(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory)
with pymupdf.open() as doc:
doc.new_page(width=210*72/25.4,height=297*72/25.4)
doc.save(root/'blank.pdf')
summary=run(root/'blank.pdf',root/'result',dpi=100)
self.assertEqual(summary['blank_removed'],1)
self.assertFalse((root/'result/pdfs/blank.pdf').exists())
self.assertTrue((root/'result/report.json').exists())
+204
View File
@@ -0,0 +1,204 @@
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
import numpy as np
import pymupdf
from copienator import EvaluationWorkspace, ExitCode
from copienator.commands import automatic_page_splitter as automatic
from copienator.commands.page_splitter import PDFPreviewer, _selected_inputs, main
from copienator.copy_errors import copy_errors
from copienator.orientation import load_provenance
from copienator_gui.workflow import build_command, build_workflow
class AutomaticPageSplitterTests(unittest.TestCase):
def make_source(self, root, *, blank=False):
(root / "labels").write_text("Ex 1\n")
source = root / "Copie01.pdf"
with pymupdf.open() as doc:
page = doc.new_page(width=420 * 72 / 25.4, height=297 * 72 / 25.4)
if not blank:
page.insert_text((60, 100), "Answer on the left page", fontsize=16)
page.insert_text((750, 100), "Answer on the right page", fontsize=16)
page.insert_text((560, 300), "x + y = z", fontsize=16)
doc.new_page(width=420 * 72 / 25.4, height=297 * 72 / 25.4)
page = doc.new_page(width=210 * 72 / 25.4, height=297 * 72 / 25.4)
page.insert_text((60, 100), "Exercise 3", fontsize=16)
doc.save(source)
return source
def test_gui_choices_build_manual_and_automatic_commands(self):
step = next(s for s in build_workflow(False) if s.id == "page_splitter")
self.assertEqual(
[v.label for v in step.variants], ["Manuellement", "Automatiquement"]
)
manual, auto = [
build_command(Path.cwd(), step, v, {"workers": 3}, "Exam")
for v in step.variants
]
self.assertNotIn("--automatic", manual)
self.assertNotIn("--workers", manual)
self.assertIn("--automatic", auto)
self.assertEqual(auto[auto.index("--workers") + 1], "3")
def test_cli_publishes_without_tk_and_preserves_original_and_provenance(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
with patch("copienator.commands.page_splitter.tk.Tk") as tk:
self.assertEqual(main([str(root), "--automatic", "--workers", "1"]), 0)
tk.assert_not_called()
preserved = workspace.original_copies_dir / source.name
self.assertEqual(preserved.read_bytes(), before)
self.assertFalse(source.exists())
output = workspace.copies_dir / source.name
with pymupdf.open(output) as doc:
self.assertEqual(len(doc), 3)
origins = load_provenance(workspace, output, len(doc))
self.assertEqual([o["origin"] for o in origins], [1, 1, 3])
self.assertGreater(doc[0].rect.width, 210 * 72 / 25.4)
saved = automatic.load_decisions(workspace, preserved)
self.assertEqual(len(saved), 3)
self.assertEqual(_selected_inputs(workspace, root), [preserved])
self.assertFalse(copy_errors(workspace))
runs = list(workspace.runs_dir.glob("page-split-*"))
self.assertTrue((runs[0] / "result/pdfs" / source.name).exists())
self.assertEqual(
automatic.run(workspace, [preserved], workers=1), ExitCode.SUCCESS
)
self.assertEqual(preserved.read_bytes(), before)
def test_processing_failure_leaves_existing_files_in_place(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
output = workspace.copies_dir / source.name
output.write_bytes(b"previous processed copy")
with (
patch.object(
automatic,
"analyse_batch",
side_effect=RuntimeError("analysis failed"),
),
self.assertRaisesRegex(RuntimeError, "analysis failed"),
):
automatic.run(workspace, [source], workers=1)
self.assertEqual(source.read_bytes(), before)
self.assertEqual(output.read_bytes(), b"previous processed copy")
self.assertFalse(workspace.original_copies_dir.exists())
def test_parallel_run_preserves_unselected_copies_and_ignores_stale_proposals(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
second = root / "Copie02.pdf"
shutil.copy2(source, second)
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
other = workspace.copies_dir / "Copie99.pdf"
other.write_bytes(b"unselected copy")
automatic.run(workspace, [source, second], workers=2)
self.assertEqual(other.read_bytes(), b"unselected copy")
original = workspace.original_copies_dir / source.name
self.assertIsNotNone(automatic.load_decisions(workspace, original))
output = workspace.copies_dir / source.name
output.write_bytes(b"edited after automatic splitting")
self.assertIsNone(automatic.load_decisions(workspace, original))
def test_wholly_blank_copy_is_preserved_and_flagged_without_stale_output(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root, blank=True)
before = source.read_bytes()
workspace = EvaluationWorkspace(root)
workspace.copies_dir.mkdir()
(workspace.copies_dir / source.name).write_bytes(b"stale output")
automatic.run(workspace, [source], workers=1)
self.assertFalse((workspace.copies_dir / source.name).exists())
self.assertEqual(
(workspace.original_copies_dir / source.name).read_bytes(), before
)
self.assertIn(source.name, copy_errors(workspace))
run = next(workspace.runs_dir.glob("page-split-*"))
self.assertEqual(
(run / "previous_copies" / source.name).read_bytes(), b"stale output"
)
def test_manual_acceptance_preserves_saved_overlap_blanks_and_order(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root)
workspace = EvaluationWorkspace(root)
automatic.run(workspace, [source], workers=1)
original = workspace.original_copies_dir / source.name
before = original.read_bytes()
output = workspace.copies_dir / source.name
expected = pymupdf.open(output)
self.addCleanup(expected.close)
preview = PDFPreviewer.__new__(PDFPreviewer)
preview.workspace = workspace
preview.inputs = [original]
preview.num = preview.global_rotation = 0
preview.history = []
preview.output_dir = None
preview.failed = False
preview.master = Mock()
preview.load_page = Mock()
self.assertTrue(preview.setup_next_file())
self.assertTrue(preview.automatic_review)
preview._initialize_current_page_settings()
for _ in range(3):
preview.confirm_and_next_page()
self.assertFalse(preview.failed)
self.assertEqual(
[s["keep"] for s in preview.page_settings], ["both", "none", "as_is"]
)
self.assertEqual(original.read_bytes(), before)
with pymupdf.open(output) as actual:
self.assertEqual(len(actual), len(expected))
for a, b in zip(actual, expected):
self.assertAlmostEqual(a.rect.width, b.rect.width, places=3)
aa, bb = a.get_pixmap(dpi=40), b.get_pixmap(dpi=40)
self.assertEqual((aa.width, aa.height), (bb.width, bb.height))
difference = np.abs(
np.frombuffer(aa.samples, np.uint8).astype(float)
- np.frombuffer(bb.samples, np.uint8)
)
self.assertLess(difference.mean(), 0.1)
def test_accepting_wholly_blank_proposal_never_creates_an_empty_pdf(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.make_source(root, blank=True)
workspace = EvaluationWorkspace(root)
automatic.run(workspace, [source], workers=1)
original = workspace.original_copies_dir / source.name
before = original.read_bytes()
preview = PDFPreviewer.__new__(PDFPreviewer)
preview.workspace = workspace
preview.inputs = [original]
preview.num = preview.global_rotation = 0
preview.history = []
preview.failed = False
preview.master = Mock()
preview.load_page = Mock()
preview.setup_next_file()
preview._initialize_current_page_settings()
preview.confirm_and_next_page()
self.assertFalse(preview.failed)
self.assertEqual(original.read_bytes(), before)
self.assertFalse((workspace.copies_dir / source.name).exists())
self.assertNotIn(source.name, copy_errors(workspace))
if __name__ == "__main__":
unittest.main()
+88
View File
@@ -0,0 +1,88 @@
import unittest
import cv2
import numpy as np
import pymupdf
from copienator.auto_page_split import _analyse_page_full as analyse_page
from copienator.fast_page_split import analyse_page_fast, analyse_pdf_page_fast, quick_page_content
class FastPageSplitTests(unittest.TestCase):
def test_direct_pdf_strip_respects_rotation_and_crop_offsets(self):
image = np.full((1400, 2000, 3), 255, np.uint8)
for y in range(150, 650, 70):
for x in (150, 1300):
cv2.putText(image, 'answer x y z', (x, y),
cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2)
cv2.putText(image, 'x + y = z', (870, 1000),
cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2)
for rotation in (0, 90, 180, 270):
with self.subTest(rotation=rotation), pymupdf.open() as doc:
source = np.ascontiguousarray(np.rot90(image, rotation//90))
h,w = source.shape[:2]
page = doc.new_page(width=w*72/150+20,height=h*72/150+30)
box = pymupdf.Rect(10,15,w*72/150+10,h*72/150+15)
pix = pymupdf.Pixmap(pymupdf.csRGB,w,h,source.tobytes(),False)
page.insert_image(box,pixmap=pix)
page.set_cropbox(box)
page.set_rotation(rotation)
rendered = page.get_pixmap(dpi=150,alpha=False)
full = np.frombuffer(rendered.samples,np.uint8).reshape(rendered.height,rendered.width,3)
reference = analyse_page(full,150,'a3')
fast,overview = analyse_pdf_page_fast(page,150,'a3')
self.assertIsNone(fast['analysis']['fallback'])
self.assertEqual(fast['size_px'],reference['size_px'])
self.assertLess(overview.size,full.size)
self.assertEqual([o['keep'] for o in fast['outputs']],[True,True])
for a,b in zip(fast['outputs'],reference['outputs']):
self.assertLessEqual(max(abs(x-y) for x,y in zip(a['rect_px'],b['rect_px'])),2)
def test_plain_blank_and_sparse_pencil_use_full_detector(self):
for pencil in (False, True):
with self.subTest(pencil=pencil):
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)
mask, early = quick_page_content(image, 150)
self.assertFalse(early)
expected = analyse_page(image, 150, 'a4')
actual = analyse_page_fast(image, 150, 'a4')
self.assertEqual(actual['outputs'][0]['keep'], expected['outputs'][0]['keep'])
self.assertEqual(actual['outputs'][0]['rect_px'], [0, 0, 850, 1200])
self.assertEqual(bool(mask.any()), pencil)
def test_compact_roi_maps_bounds_back_to_full_spread(self):
image = np.full((1400, 2000, 3), 255, np.uint8)
for y in range(150, 650, 70):
for x in (150, 1300):
cv2.putText(image, 'answer x y z', (x, y),
cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2)
cv2.putText(image, 'x + y = z', (870, 1000),
cv2.FONT_HERSHEY_SIMPLEX, 1, (20, 20, 20), 2)
for mirrored in (False, True):
with self.subTest(mirrored=mirrored):
rgb = image[:, ::-1].copy() if mirrored else image
reference = analyse_page(rgb, 150, 'a3')
fast = analyse_page_fast(rgb, 150, 'a3')
self.assertIsNone(fast['analysis']['fallback'])
self.assertEqual(fast['size_px'], [2000, 1400])
self.assertEqual(fast['extensions_mm'], reference['extensions_mm'])
self.assertEqual([o['rect_px'] for o in fast['outputs']],
[o['rect_px'] for o in reference['outputs']])
self.assertTrue(all(o['keep'] for o in fast['outputs']))
def test_ambiguous_or_wide_writing_falls_back_without_truncation(self):
image = np.full((700, 1000, 3), 255, np.uint8)
image[200:230, 300:650] = 20
reference = analyse_page(image, 100, 'a3')
fast = analyse_page_fast(image, 100, 'a3')
self.assertEqual(fast['analysis']['fallback'], 'full-spread')
self.assertEqual(fast['outputs'], reference['outputs'])
self.assertEqual(fast['crossings'], reference['crossings'])
def test_unknown_geometry_keeps_reference_behaviour(self):
image = np.full((400, 500, 3), 255, np.uint8)
self.assertEqual(analyse_page_fast(image, 100, 'unknown'),
analyse_page(image, 100, 'unknown'))
+107 -1
View File
@@ -3,10 +3,116 @@ import unittest
import cv2
import numpy as np
from copienator.ink_detection import detect_bounds, _paper_blur
from copienator.ink_detection import detect_bounds, detect_page_content, _paper_blur, _blank_content_support
class InkDetectionTests(unittest.TestCase):
def test_lightweight_content_masks_match_full_diagnostics(self):
for image,colour in [(self.scan(),False),(self.dark_grid(),False),
(self.scan(),True),(np.full((300,220,3),255,np.uint8),False)]:
with self.subTest(colour_cleanup=colour,shape=image.shape):
full,light = {},{}
expected=detect_bounds(image,dpi=150,diagnostics=full,colour_cleanup=colour)
actual=detect_bounds(image,dpi=150,content_masks=light,colour_cleanup=colour)
self.assertEqual(actual,expected)
self.assertEqual(set(light),{'foreground','weak_candidates'})
for name in light:
np.testing.assert_array_equal(light[name],full['masks'][name])
def test_blank_detection_ignores_remote_specks(self):
image = np.full((1200,850,3),255,np.uint8)
for x,y in ((150,200),(400,600),(700,1000)):
cv2.rectangle(image,(x,y),(x+3,y+4),(30,30,30),-1)
mask,result = detect_page_content(image,dpi=150)
self.assertFalse(mask.any())
self.assertGreater(result['content_unsupported_pixels'],0)
def test_isolated_question_numbers_remain_content(self):
for text in ('1','7','16','x'):
with self.subTest(text=text):
image = np.full((1200,850,3),255,np.uint8)
cv2.putText(image,text,(300,600),cv2.FONT_HERSHEY_SIMPLEX,.7,(30,30,30),2)
self.assertTrue(detect_page_content(image,dpi=150)[0].any())
def test_faint_edge_question_number_remains_content(self):
image = np.full((1200,850,3),255,np.uint8)
cv2.putText(image,'1',(1,600),cv2.FONT_HERSHEY_SIMPLEX,.7,(190,190,190),2)
self.assertTrue(detect_page_content(image,dpi=150)[0].any())
def test_isolated_circle_diagram_remains_content(self):
image = np.full((1200,850,3),255,np.uint8)
cv2.circle(image,(400,600),20,(30,30,30),2)
self.assertTrue(detect_page_content(image,dpi=150)[0].any())
def test_solid_ink_dots_do_not_establish_content_at_multiple_resolutions(self):
for dpi in (150,200,300):
px=dpi/25.4
mask=np.zeros((round(100*px),round(70*px)),np.uint8)
cv2.ellipse(mask,(round(30*px),round(45*px)),
(round(.6*px),round(.8*px)),0,0,360,1,-1)
self.assertFalse(_blank_content_support(mask>0,dpi)[0].any())
def test_compact_dot_is_kept_with_neighbouring_question_number(self):
mask=np.zeros((400,300),np.uint8)
cv2.putText(mask,'7',(100,200),cv2.FONT_HERSHEY_SIMPLEX,.7,1,2)
mask[194:200,117:122]=1
retained,_=_blank_content_support(mask>0,150)
self.assertTrue(retained[194:200,117:122].all())
def test_large_isolated_numbers_and_diagram_near_bottom_remain_content(self):
for text in ('1','7','x','?','2','16'):
with self.subTest(text=text):
image=np.full((1200,850,3),255,np.uint8)
cv2.putText(image,text,(300,1150),cv2.FONT_HERSHEY_SIMPLEX,
1.5,(30,30,30),2)
self.assertTrue(detect_page_content(image,200)[0].any())
image=np.full((1200,850,3),255,np.uint8)
cv2.circle(image,(400,1140),20,(30,30,30),2)
self.assertTrue(detect_page_content(image,200)[0].any())
def test_content_analysis_recognizes_blank_grid_without_changing_crop_fallback(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)
for y in (110,370,630,890,1130):
cv2.ellipse(image,(25,y),(12,20),0,0,300,(35,35,35),2)
before = detect_bounds(image,dpi=150)
mask,result = detect_page_content(image,dpi=150)
self.assertTrue(result['content_ruling_confirmed'])
self.assertFalse(mask.any())
self.assertEqual(detect_bounds(image,dpi=150),before)
def test_content_analysis_preserves_pencil_only_writing(self):
image = np.full((1200,850,3),255,np.uint8)
cv2.putText(image,'pencil',(100,600),cv2.FONT_HERSHEY_SIMPLEX,
1,(195,195,195),2)
mask,result = detect_page_content(image,dpi=150)
self.assertTrue(mask[560:610,90:250].any())
self.assertEqual(result['content_state'],'content')
def test_faint_scanner_border_does_not_count_as_pencil(self):
image = np.full((1200,850,3),255,np.uint8)
cv2.rectangle(image,(1,1),(848,300),(195,195,195),1)
mask,_ = detect_page_content(image,dpi=150)
self.assertFalse(mask.any())
def test_faint_unruled_diagram_is_retained(self):
image = np.full((1200,850,3),255,np.uint8)
cv2.rectangle(image,(100,100),(750,1100),(195,195,195),2)
mask,_ = detect_page_content(image,dpi=150)
self.assertTrue(mask.any())
def test_content_analysis_preserves_faint_grid_note_and_fraction_bar(self):
image = self.dark_grid()
cv2.putText(image,'pencil',(100,1100),cv2.FONT_HERSHEY_SIMPLEX,
.7,(190,190,190),2)
cv2.line(image,(150,1020),(700,1020),(0,0,0),3)
mask,_ = detect_page_content(image,dpi=150)
self.assertTrue(mask[1070:1110,90:250].any())
self.assertTrue(mask[1018:1023,150:700].any())
def test_fast_background_blur_matches_opencv_pixel_for_pixel(self):
rng = np.random.default_rng(42)
for shape in ((97,131), (241,319)):