WIP
This commit is contained in:
@@ -1,556 +1,170 @@
|
||||
"""
|
||||
Tests for expand_bbox function.
|
||||
Tests for expand_bbox function with uniform pixel padding.
|
||||
|
||||
Tests verify that bbox expansion works correctly with center-point scaling,
|
||||
directional compensation, max padding clamping, and image boundary handling.
|
||||
Verifies that bbox expansion adds uniform padding on all sides,
|
||||
clamps to image boundaries, and returns integer tuples.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shared.bbox import (
|
||||
expand_bbox,
|
||||
ScaleStrategy,
|
||||
FIELD_SCALE_STRATEGIES,
|
||||
DEFAULT_STRATEGY,
|
||||
)
|
||||
from shared.bbox import expand_bbox
|
||||
from shared.bbox.scale_strategy import UNIFORM_PAD
|
||||
|
||||
|
||||
class TestExpandBboxCenterScaling:
|
||||
"""Tests for center-point based scaling."""
|
||||
class TestExpandBboxUniformPadding:
|
||||
"""Tests for uniform padding on all sides."""
|
||||
|
||||
def test_center_scaling_expands_symmetrically(self):
|
||||
"""Verify bbox expands symmetrically around center when no extra ratios."""
|
||||
# 100x50 bbox at (100, 200)
|
||||
bbox = (100, 200, 200, 250)
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.2, # 20% wider
|
||||
scale_y=1.4, # 40% taller
|
||||
max_pad_x=1000, # Large to avoid clamping
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_adds_uniform_pad_on_all_sides(self):
|
||||
"""Verify default pad is applied equally on all four sides."""
|
||||
bbox = (100, 200, 300, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# Original: width=100, height=50
|
||||
# New: width=120, height=70
|
||||
# Center: (150, 225)
|
||||
# Expected: x0=150-60=90, x1=150+60=210, y0=225-35=190, y1=225+35=260
|
||||
assert result[0] == 90 # x0
|
||||
assert result[1] == 190 # y0
|
||||
assert result[2] == 210 # x1
|
||||
assert result[3] == 260 # y1
|
||||
|
||||
def test_no_scaling_returns_original(self):
|
||||
"""Verify scale=1.0 with no extras returns original bbox."""
|
||||
bbox = (100, 200, 200, 250)
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
assert result == (
|
||||
100 - UNIFORM_PAD,
|
||||
200 - UNIFORM_PAD,
|
||||
300 + UNIFORM_PAD,
|
||||
250 + UNIFORM_PAD,
|
||||
)
|
||||
|
||||
def test_custom_pad_value(self):
|
||||
"""Verify custom pad overrides default."""
|
||||
bbox = (100, 200, 300, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
pad=20,
|
||||
)
|
||||
|
||||
assert result == (100, 200, 200, 250)
|
||||
assert result == (80, 180, 320, 270)
|
||||
|
||||
|
||||
class TestExpandBboxDirectionalCompensation:
|
||||
"""Tests for directional compensation (extra ratios)."""
|
||||
|
||||
def test_extra_top_expands_upward(self):
|
||||
"""Verify extra_top_ratio adds expansion toward top."""
|
||||
bbox = (100, 200, 200, 250) # width=100, height=50
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_top_ratio=0.5, # Add 50% of height to top
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_zero_pad_returns_original(self):
|
||||
"""Verify pad=0 returns original bbox as integers."""
|
||||
bbox = (100, 200, 300, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
pad=0,
|
||||
)
|
||||
|
||||
# extra_top = 50 * 0.5 = 25
|
||||
assert result[0] == 100 # x0 unchanged
|
||||
assert result[1] == 175 # y0 = 200 - 25
|
||||
assert result[2] == 200 # x1 unchanged
|
||||
assert result[3] == 250 # y1 unchanged
|
||||
assert result == (100, 200, 300, 250)
|
||||
|
||||
def test_extra_left_expands_leftward(self):
|
||||
"""Verify extra_left_ratio adds expansion toward left."""
|
||||
bbox = (100, 200, 200, 250) # width=100
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_left_ratio=0.8, # Add 80% of width to left
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_all_field_types_get_same_padding(self):
|
||||
"""Verify no field-specific expansion -- same result regardless of field."""
|
||||
bbox = (100, 200, 300, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
result_a = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
result_b = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
# extra_left = 100 * 0.8 = 80
|
||||
assert result[0] == 20 # x0 = 100 - 80
|
||||
assert result[1] == 200 # y0 unchanged
|
||||
assert result[2] == 200 # x1 unchanged
|
||||
assert result[3] == 250 # y1 unchanged
|
||||
|
||||
def test_extra_right_expands_rightward(self):
|
||||
"""Verify extra_right_ratio adds expansion toward right."""
|
||||
bbox = (100, 200, 200, 250) # width=100
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_right_ratio=0.3, # Add 30% of width to right
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# extra_right = 100 * 0.3 = 30
|
||||
assert result[0] == 100 # x0 unchanged
|
||||
assert result[1] == 200 # y0 unchanged
|
||||
assert result[2] == 230 # x1 = 200 + 30
|
||||
assert result[3] == 250 # y1 unchanged
|
||||
|
||||
def test_extra_bottom_expands_downward(self):
|
||||
"""Verify extra_bottom_ratio adds expansion toward bottom."""
|
||||
bbox = (100, 200, 200, 250) # height=50
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_bottom_ratio=0.4, # Add 40% of height to bottom
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# extra_bottom = 50 * 0.4 = 20
|
||||
assert result[0] == 100 # x0 unchanged
|
||||
assert result[1] == 200 # y0 unchanged
|
||||
assert result[2] == 200 # x1 unchanged
|
||||
assert result[3] == 270 # y1 = 250 + 20
|
||||
|
||||
def test_combined_scaling_and_directional(self):
|
||||
"""Verify scale + directional compensation work together."""
|
||||
bbox = (100, 200, 200, 250) # width=100, height=50
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.2, # 20% wider -> 120 width
|
||||
scale_y=1.0, # no height change
|
||||
extra_left_ratio=0.5, # Add 50% of width to left
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# Center: x=150
|
||||
# After scale: width=120 -> x0=150-60=90, x1=150+60=210
|
||||
# After extra_left: x0 = 90 - (100 * 0.5) = 40
|
||||
assert result[0] == 40 # x0
|
||||
assert result[2] == 210 # x1
|
||||
|
||||
|
||||
class TestExpandBboxMaxPadClamping:
|
||||
"""Tests for max padding clamping."""
|
||||
|
||||
def test_max_pad_x_limits_horizontal_expansion(self):
|
||||
"""Verify max_pad_x limits expansion on left and right."""
|
||||
bbox = (100, 200, 200, 250) # width=100
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=2.0, # Double width (would add 50 each side)
|
||||
scale_y=1.0,
|
||||
max_pad_x=30, # Limit to 30 pixels each side
|
||||
max_pad_y=1000,
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# Scale would make: x0=100, x1=200 -> x0=50, x1=250 (50px each side)
|
||||
# But max_pad_x=30 limits to: x0=70, x1=230
|
||||
assert result[0] == 70 # x0 = 100 - 30
|
||||
assert result[2] == 230 # x1 = 200 + 30
|
||||
|
||||
def test_max_pad_y_limits_vertical_expansion(self):
|
||||
"""Verify max_pad_y limits expansion on top and bottom."""
|
||||
bbox = (100, 200, 200, 250) # height=50
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=3.0, # Triple height (would add 50 each side)
|
||||
max_pad_x=1000,
|
||||
max_pad_y=20, # Limit to 20 pixels each side
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# Scale would make: y0=175, y1=275 (50px each side)
|
||||
# But max_pad_y=20 limits to: y0=180, y1=270
|
||||
assert result[1] == 180 # y0 = 200 - 20
|
||||
assert result[3] == 270 # y1 = 250 + 20
|
||||
|
||||
def test_max_pad_preserves_asymmetry(self):
|
||||
"""Verify max_pad clamping preserves asymmetric expansion."""
|
||||
bbox = (100, 200, 200, 250) # width=100
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_left_ratio=1.0, # 100px left expansion
|
||||
extra_right_ratio=0.0, # No right expansion
|
||||
max_pad_x=50, # Limit to 50 pixels
|
||||
max_pad_y=1000,
|
||||
)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
|
||||
# Left would expand 100, clamped to 50
|
||||
# Right stays at 0
|
||||
assert result[0] == 50 # x0 = 100 - 50
|
||||
assert result[2] == 200 # x1 unchanged
|
||||
assert result_a == result_b
|
||||
|
||||
|
||||
class TestExpandBboxImageBoundaryClamping:
|
||||
"""Tests for image boundary clamping."""
|
||||
"""Tests for clamping to image boundaries."""
|
||||
|
||||
def test_clamps_to_left_boundary(self):
|
||||
"""Verify x0 is clamped to 0."""
|
||||
bbox = (10, 200, 110, 250) # Close to left edge
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_left_ratio=0.5, # Would push x0 below 0
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_clamps_x0_to_zero(self):
|
||||
bbox = (5, 200, 100, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
assert result[0] == 0 # Clamped to 0
|
||||
assert result[0] == 0
|
||||
|
||||
def test_clamps_to_top_boundary(self):
|
||||
"""Verify y0 is clamped to 0."""
|
||||
bbox = (100, 10, 200, 60) # Close to top edge
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_top_ratio=0.5, # Would push y0 below 0
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_clamps_y0_to_zero(self):
|
||||
bbox = (100, 3, 300, 50)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
assert result[1] == 0 # Clamped to 0
|
||||
assert result[1] == 0
|
||||
|
||||
def test_clamps_to_right_boundary(self):
|
||||
"""Verify x1 is clamped to image_width."""
|
||||
bbox = (900, 200, 990, 250) # Close to right edge
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_right_ratio=0.5, # Would push x1 beyond image_width
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_clamps_x1_to_image_width(self):
|
||||
bbox = (900, 200, 995, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
assert result[2] == 1000 # Clamped to image_width
|
||||
assert result[2] == 1000
|
||||
|
||||
def test_clamps_to_bottom_boundary(self):
|
||||
"""Verify y1 is clamped to image_height."""
|
||||
bbox = (100, 940, 200, 990) # Close to bottom edge
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.0,
|
||||
scale_y=1.0,
|
||||
extra_bottom_ratio=0.5, # Would push y1 beyond image_height
|
||||
max_pad_x=1000,
|
||||
max_pad_y=1000,
|
||||
)
|
||||
def test_clamps_y1_to_image_height(self):
|
||||
bbox = (100, 900, 300, 995)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test_field",
|
||||
strategies={"test_field": strategy},
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
assert result[3] == 1000 # Clamped to image_height
|
||||
assert result[3] == 1000
|
||||
|
||||
def test_corner_bbox_clamps_multiple_sides(self):
|
||||
"""Bbox near top-left corner clamps both x0 and y0."""
|
||||
bbox = (2, 3, 50, 60)
|
||||
|
||||
class TestExpandBboxUnknownField:
|
||||
"""Tests for unknown field handling."""
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
def test_unknown_field_uses_default_strategy(self):
|
||||
"""Verify unknown field types use DEFAULT_STRATEGY."""
|
||||
bbox = (100, 200, 200, 250)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="unknown_field_xyz",
|
||||
)
|
||||
|
||||
# DEFAULT_STRATEGY: scale_x=1.15, scale_y=1.15
|
||||
# Original: width=100, height=50
|
||||
# New: width=115, height=57.5
|
||||
# Center: (150, 225)
|
||||
# x0 = 150 - 57.5 = 92.5 -> 92
|
||||
# x1 = 150 + 57.5 = 207.5 -> 207
|
||||
# y0 = 225 - 28.75 = 196.25 -> 196
|
||||
# y1 = 225 + 28.75 = 253.75 -> 253
|
||||
# But max_pad_x=50 may clamp...
|
||||
# Left pad = 100 - 92.5 = 7.5 (< 50, ok)
|
||||
# Right pad = 207.5 - 200 = 7.5 (< 50, ok)
|
||||
assert result[0] == 92
|
||||
assert result[2] == 207
|
||||
|
||||
|
||||
class TestExpandBboxWithRealStrategies:
|
||||
"""Tests using actual FIELD_SCALE_STRATEGIES."""
|
||||
|
||||
def test_ocr_number_expands_significantly_upward(self):
|
||||
"""Verify ocr_number field gets significant upward expansion."""
|
||||
bbox = (100, 200, 200, 230) # Small height=30
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="ocr_number",
|
||||
)
|
||||
|
||||
# extra_top_ratio=0.60 -> 30 * 0.6 = 18 extra top
|
||||
# y0 should decrease significantly
|
||||
assert result[1] < 200 - 10 # At least 10px upward expansion
|
||||
|
||||
def test_bankgiro_expands_significantly_leftward(self):
|
||||
"""Verify bankgiro field gets significant leftward expansion."""
|
||||
bbox = (200, 200, 300, 230) # width=100
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="bankgiro",
|
||||
)
|
||||
|
||||
# extra_left_ratio=0.80 -> 100 * 0.8 = 80 extra left
|
||||
# x0 should decrease significantly
|
||||
assert result[0] < 200 - 30 # At least 30px leftward expansion
|
||||
|
||||
def test_amount_expands_rightward(self):
|
||||
"""Verify amount field gets rightward expansion for currency."""
|
||||
bbox = (100, 200, 200, 230) # width=100
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="amount",
|
||||
)
|
||||
|
||||
# extra_right_ratio=0.30 -> 100 * 0.3 = 30 extra right
|
||||
# x1 should increase
|
||||
assert result[2] > 200 + 10 # At least 10px rightward expansion
|
||||
assert result[0] == 0
|
||||
assert result[1] == 0
|
||||
assert result[2] == 50 + UNIFORM_PAD
|
||||
assert result[3] == 60 + UNIFORM_PAD
|
||||
|
||||
|
||||
class TestExpandBboxReturnType:
|
||||
"""Tests for return type and value format."""
|
||||
|
||||
def test_returns_tuple_of_four_ints(self):
|
||||
"""Verify return type is tuple of 4 integers."""
|
||||
bbox = (100.5, 200.3, 200.7, 250.9)
|
||||
bbox = (100.5, 200.3, 300.7, 250.9)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="invoice_number",
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 4
|
||||
assert all(isinstance(v, int) for v in result)
|
||||
|
||||
def test_returns_valid_bbox_format(self):
|
||||
"""Verify returned bbox has x0 < x1 and y0 < y1."""
|
||||
bbox = (100, 200, 200, 250)
|
||||
def test_float_bbox_floors_correctly(self):
|
||||
"""Verify float coordinates are converted to int properly."""
|
||||
bbox = (100.7, 200.3, 300.2, 250.8)
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="invoice_number",
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000, pad=0)
|
||||
|
||||
# int() truncates toward zero
|
||||
assert result == (100, 200, 300, 250)
|
||||
|
||||
def test_returns_valid_bbox_ordering(self):
|
||||
"""Verify x0 < x1 and y0 < y1."""
|
||||
bbox = (100, 200, 300, 250)
|
||||
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
x0, y0, x1, y1 = result
|
||||
assert x0 < x1, "x0 should be less than x1"
|
||||
assert y0 < y1, "y0 should be less than y1"
|
||||
assert x0 < x1
|
||||
assert y0 < y1
|
||||
|
||||
|
||||
class TestManualLabelMode:
|
||||
"""Tests for manual_mode parameter."""
|
||||
class TestExpandBboxEdgeCases:
|
||||
"""Tests for edge cases."""
|
||||
|
||||
def test_manual_mode_uses_minimal_padding(self):
|
||||
"""Verify manual_mode uses MANUAL_LABEL_STRATEGY with minimal padding."""
|
||||
bbox = (100, 200, 200, 250) # width=100, height=50
|
||||
def test_small_bbox_with_large_pad(self):
|
||||
"""Pad larger than bbox still works correctly."""
|
||||
bbox = (100, 200, 105, 203) # 5x3 pixel bbox
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="bankgiro", # Would normally expand left significantly
|
||||
manual_mode=True,
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000, pad=50)
|
||||
|
||||
# MANUAL_LABEL_STRATEGY: scale=1.0, max_pad=10
|
||||
# Should only add 10px padding each side (but scale=1.0 means no scaling)
|
||||
# Actually with scale=1.0, no extra ratios, we get 0 expansion from scaling
|
||||
# Only max_pad=10 applies as a limit, but there's no expansion to limit
|
||||
# So result should be same as original
|
||||
assert result == (100, 200, 200, 250)
|
||||
assert result == (50, 150, 155, 253)
|
||||
|
||||
def test_manual_mode_ignores_field_type(self):
|
||||
"""Verify manual_mode ignores field-specific strategies."""
|
||||
bbox = (100, 200, 200, 250)
|
||||
def test_bbox_at_origin(self):
|
||||
bbox = (0, 0, 50, 30)
|
||||
|
||||
# Different fields should give same result in manual_mode
|
||||
result_bankgiro = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="bankgiro",
|
||||
manual_mode=True,
|
||||
)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
result_ocr = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="ocr_number",
|
||||
manual_mode=True,
|
||||
)
|
||||
assert result[0] == 0
|
||||
assert result[1] == 0
|
||||
|
||||
assert result_bankgiro == result_ocr
|
||||
def test_bbox_at_image_edge(self):
|
||||
bbox = (950, 970, 1000, 1000)
|
||||
|
||||
def test_manual_mode_vs_auto_mode_different(self):
|
||||
"""Verify manual_mode produces different results than auto mode."""
|
||||
bbox = (100, 200, 200, 250)
|
||||
result = expand_bbox(bbox=bbox, image_width=1000, image_height=1000)
|
||||
|
||||
auto_result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="bankgiro", # Has extra_left_ratio=0.80
|
||||
manual_mode=False,
|
||||
)
|
||||
|
||||
manual_result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="bankgiro",
|
||||
manual_mode=True,
|
||||
)
|
||||
|
||||
# Auto mode should expand more (especially to the left for bankgiro)
|
||||
assert auto_result[0] < manual_result[0] # Auto x0 is more left
|
||||
|
||||
def test_manual_mode_clamps_to_image_bounds(self):
|
||||
"""Verify manual_mode still respects image boundaries."""
|
||||
bbox = (5, 5, 50, 50) # Close to top-left corner
|
||||
|
||||
result = expand_bbox(
|
||||
bbox=bbox,
|
||||
image_width=1000,
|
||||
image_height=1000,
|
||||
field_type="test",
|
||||
manual_mode=True,
|
||||
)
|
||||
|
||||
# Should clamp to 0
|
||||
assert result[0] >= 0
|
||||
assert result[1] >= 0
|
||||
assert result[2] == 1000
|
||||
assert result[3] == 1000
|
||||
|
||||
@@ -1,192 +1,24 @@
|
||||
"""
|
||||
Tests for ScaleStrategy configuration.
|
||||
Tests for simplified scale strategy configuration.
|
||||
|
||||
Tests verify that scale strategies are properly defined, immutable,
|
||||
and cover all required fields.
|
||||
Verifies that UNIFORM_PAD constant is properly defined
|
||||
and replaces the old field-specific strategies.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shared.bbox import (
|
||||
ScaleStrategy,
|
||||
DEFAULT_STRATEGY,
|
||||
MANUAL_LABEL_STRATEGY,
|
||||
FIELD_SCALE_STRATEGIES,
|
||||
)
|
||||
from shared.fields import CLASS_NAMES
|
||||
from shared.bbox.scale_strategy import UNIFORM_PAD
|
||||
|
||||
|
||||
class TestScaleStrategyDataclass:
|
||||
"""Tests for ScaleStrategy dataclass behavior."""
|
||||
class TestUniformPad:
|
||||
"""Tests for UNIFORM_PAD constant."""
|
||||
|
||||
def test_default_strategy_values(self):
|
||||
"""Verify default strategy has expected default values."""
|
||||
strategy = ScaleStrategy()
|
||||
assert strategy.scale_x == 1.15
|
||||
assert strategy.scale_y == 1.15
|
||||
assert strategy.extra_top_ratio == 0.0
|
||||
assert strategy.extra_bottom_ratio == 0.0
|
||||
assert strategy.extra_left_ratio == 0.0
|
||||
assert strategy.extra_right_ratio == 0.0
|
||||
assert strategy.max_pad_x == 50
|
||||
assert strategy.max_pad_y == 50
|
||||
def test_uniform_pad_is_integer(self):
|
||||
assert isinstance(UNIFORM_PAD, int)
|
||||
|
||||
def test_scale_strategy_immutability(self):
|
||||
"""Verify ScaleStrategy is frozen (immutable)."""
|
||||
strategy = ScaleStrategy()
|
||||
with pytest.raises(AttributeError):
|
||||
strategy.scale_x = 2.0 # type: ignore
|
||||
def test_uniform_pad_value_is_15(self):
|
||||
"""15px at 150 DPI provides ~2.5mm real-world padding."""
|
||||
assert UNIFORM_PAD == 15
|
||||
|
||||
def test_custom_strategy_values(self):
|
||||
"""Verify custom values are properly set."""
|
||||
strategy = ScaleStrategy(
|
||||
scale_x=1.5,
|
||||
scale_y=1.8,
|
||||
extra_top_ratio=0.6,
|
||||
extra_left_ratio=0.8,
|
||||
max_pad_x=100,
|
||||
max_pad_y=150,
|
||||
)
|
||||
assert strategy.scale_x == 1.5
|
||||
assert strategy.scale_y == 1.8
|
||||
assert strategy.extra_top_ratio == 0.6
|
||||
assert strategy.extra_left_ratio == 0.8
|
||||
assert strategy.max_pad_x == 100
|
||||
assert strategy.max_pad_y == 150
|
||||
|
||||
|
||||
class TestDefaultStrategy:
|
||||
"""Tests for DEFAULT_STRATEGY constant."""
|
||||
|
||||
def test_default_strategy_is_scale_strategy(self):
|
||||
"""Verify DEFAULT_STRATEGY is a ScaleStrategy instance."""
|
||||
assert isinstance(DEFAULT_STRATEGY, ScaleStrategy)
|
||||
|
||||
def test_default_strategy_matches_default_values(self):
|
||||
"""Verify DEFAULT_STRATEGY has same values as ScaleStrategy()."""
|
||||
expected = ScaleStrategy()
|
||||
assert DEFAULT_STRATEGY == expected
|
||||
|
||||
|
||||
class TestManualLabelStrategy:
|
||||
"""Tests for MANUAL_LABEL_STRATEGY constant."""
|
||||
|
||||
def test_manual_label_strategy_is_scale_strategy(self):
|
||||
"""Verify MANUAL_LABEL_STRATEGY is a ScaleStrategy instance."""
|
||||
assert isinstance(MANUAL_LABEL_STRATEGY, ScaleStrategy)
|
||||
|
||||
def test_manual_label_strategy_has_no_scaling(self):
|
||||
"""Verify MANUAL_LABEL_STRATEGY has scale factors of 1.0."""
|
||||
assert MANUAL_LABEL_STRATEGY.scale_x == 1.0
|
||||
assert MANUAL_LABEL_STRATEGY.scale_y == 1.0
|
||||
|
||||
def test_manual_label_strategy_has_no_directional_expansion(self):
|
||||
"""Verify MANUAL_LABEL_STRATEGY has no directional expansion."""
|
||||
assert MANUAL_LABEL_STRATEGY.extra_top_ratio == 0.0
|
||||
assert MANUAL_LABEL_STRATEGY.extra_bottom_ratio == 0.0
|
||||
assert MANUAL_LABEL_STRATEGY.extra_left_ratio == 0.0
|
||||
assert MANUAL_LABEL_STRATEGY.extra_right_ratio == 0.0
|
||||
|
||||
def test_manual_label_strategy_has_small_max_pad(self):
|
||||
"""Verify MANUAL_LABEL_STRATEGY has small max padding."""
|
||||
assert MANUAL_LABEL_STRATEGY.max_pad_x <= 15
|
||||
assert MANUAL_LABEL_STRATEGY.max_pad_y <= 15
|
||||
|
||||
|
||||
class TestFieldScaleStrategies:
|
||||
"""Tests for FIELD_SCALE_STRATEGIES dictionary."""
|
||||
|
||||
def test_all_class_names_have_strategies(self):
|
||||
"""Verify all field class names have defined strategies."""
|
||||
for class_name in CLASS_NAMES:
|
||||
assert class_name in FIELD_SCALE_STRATEGIES, (
|
||||
f"Missing strategy for field: {class_name}"
|
||||
)
|
||||
|
||||
def test_strategies_are_scale_strategy_instances(self):
|
||||
"""Verify all strategies are ScaleStrategy instances."""
|
||||
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
|
||||
assert isinstance(strategy, ScaleStrategy), (
|
||||
f"Strategy for {field_name} is not a ScaleStrategy"
|
||||
)
|
||||
|
||||
def test_scale_values_are_greater_than_one(self):
|
||||
"""Verify all scale values are >= 1.0 (expansion, not contraction)."""
|
||||
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
|
||||
assert strategy.scale_x >= 1.0, (
|
||||
f"{field_name} scale_x should be >= 1.0"
|
||||
)
|
||||
assert strategy.scale_y >= 1.0, (
|
||||
f"{field_name} scale_y should be >= 1.0"
|
||||
)
|
||||
|
||||
def test_extra_ratios_are_non_negative(self):
|
||||
"""Verify all extra ratios are >= 0."""
|
||||
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
|
||||
assert strategy.extra_top_ratio >= 0, (
|
||||
f"{field_name} extra_top_ratio should be >= 0"
|
||||
)
|
||||
assert strategy.extra_bottom_ratio >= 0, (
|
||||
f"{field_name} extra_bottom_ratio should be >= 0"
|
||||
)
|
||||
assert strategy.extra_left_ratio >= 0, (
|
||||
f"{field_name} extra_left_ratio should be >= 0"
|
||||
)
|
||||
assert strategy.extra_right_ratio >= 0, (
|
||||
f"{field_name} extra_right_ratio should be >= 0"
|
||||
)
|
||||
|
||||
def test_max_pad_values_are_positive(self):
|
||||
"""Verify all max_pad values are > 0."""
|
||||
for field_name, strategy in FIELD_SCALE_STRATEGIES.items():
|
||||
assert strategy.max_pad_x > 0, (
|
||||
f"{field_name} max_pad_x should be > 0"
|
||||
)
|
||||
assert strategy.max_pad_y > 0, (
|
||||
f"{field_name} max_pad_y should be > 0"
|
||||
)
|
||||
|
||||
|
||||
class TestSpecificFieldStrategies:
|
||||
"""Tests for specific field strategy configurations."""
|
||||
|
||||
def test_ocr_number_expands_upward(self):
|
||||
"""Verify ocr_number strategy expands upward to capture label."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["ocr_number"]
|
||||
assert strategy.extra_top_ratio > 0.0
|
||||
assert strategy.extra_top_ratio >= 0.5 # Significant upward expansion
|
||||
|
||||
def test_bankgiro_expands_leftward(self):
|
||||
"""Verify bankgiro strategy expands leftward to capture prefix."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["bankgiro"]
|
||||
assert strategy.extra_left_ratio > 0.0
|
||||
assert strategy.extra_left_ratio >= 0.5 # Significant leftward expansion
|
||||
|
||||
def test_plusgiro_expands_leftward(self):
|
||||
"""Verify plusgiro strategy expands leftward to capture prefix."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["plusgiro"]
|
||||
assert strategy.extra_left_ratio > 0.0
|
||||
assert strategy.extra_left_ratio >= 0.5
|
||||
|
||||
def test_amount_expands_rightward(self):
|
||||
"""Verify amount strategy expands rightward for currency symbol."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["amount"]
|
||||
assert strategy.extra_right_ratio > 0.0
|
||||
|
||||
def test_invoice_date_expands_upward(self):
|
||||
"""Verify invoice_date strategy expands upward to capture label."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["invoice_date"]
|
||||
assert strategy.extra_top_ratio > 0.0
|
||||
|
||||
def test_invoice_due_date_expands_upward_and_leftward(self):
|
||||
"""Verify invoice_due_date strategy expands both up and left."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["invoice_due_date"]
|
||||
assert strategy.extra_top_ratio > 0.0
|
||||
assert strategy.extra_left_ratio > 0.0
|
||||
|
||||
def test_payment_line_has_minimal_expansion(self):
|
||||
"""Verify payment_line has conservative expansion (machine code)."""
|
||||
strategy = FIELD_SCALE_STRATEGIES["payment_line"]
|
||||
# Payment line is machine-readable, needs minimal expansion
|
||||
assert strategy.scale_x <= 1.2
|
||||
assert strategy.scale_y <= 1.3
|
||||
def test_uniform_pad_is_positive(self):
|
||||
assert UNIFORM_PAD > 0
|
||||
|
||||
Reference in New Issue
Block a user