Files
orisys-sensor-design/blender/scripts/test_page_sizing.py
T
2026-09-29 16:33:31 +08:00

191 lines
8.4 KiB
Python

"""Run with python -m pytest blender/scripts/test_page_sizing.py (no Blender needed).
The derivation is only usable because its own residual proves the layout is a
bounds-projection. These tests pin both halves: that a real projection recovers the measured
page, and that an unwrap is refused rather than silently sized.
"""
import unittest
from page_sizing import (
DEFAULT_PX_PER_MM,
DerivedPage,
PageSizingError,
derive_page,
)
#: Measured on the shipped blends (scale-normalised view matrix, so the camera object scale
#: is divided out). These are the numbers the feature exists to reproduce.
FINGER_UV_1_MM = (17.5116, 15.9772)
FINGER_UV_2_MM = (17.5114, 15.9820)
XYNOVA_UV_1_MM = (21.1989, 23.2003)
XYNOVA_UV_2_MM = (21.2001, 23.2003)
def projection_samples(width_mm, height_mm, steps=40, v_flip=False):
"""Samples of a bounds-projection: UV fills [0,1]^2 over a `width x height` view plane."""
uvs = []
planes = []
for i in range(steps + 1):
for j in range(steps + 1):
x = width_mm * i / steps
y = height_mm * j / steps
u = i / steps
v = (1.0 - j / steps) if v_flip else (j / steps)
uvs.append((u, v))
planes.append((x, y))
return uvs, planes
class DerivationTests(unittest.TestCase):
def test_recovers_the_finger_r01_uv_1_page(self):
page = derive_page(*projection_samples(*FINGER_UV_1_MM))
self.assertAlmostEqual(page.page_width_mm, FINGER_UV_1_MM[0], places=2)
self.assertAlmostEqual(page.page_height_mm, FINGER_UV_1_MM[1], places=2)
def test_recovers_every_measured_page(self):
for width_mm, height_mm in (
FINGER_UV_1_MM, FINGER_UV_2_MM, XYNOVA_UV_1_MM, XYNOVA_UV_2_MM
):
with self.subTest(size=(width_mm, height_mm)):
page = derive_page(*projection_samples(width_mm, height_mm))
self.assertAlmostEqual(page.page_width_mm, width_mm, delta=0.01)
self.assertAlmostEqual(page.page_height_mm, height_mm, delta=0.01)
def test_xynova_page_is_portrait_where_the_configured_page_was_landscape(self):
"""The defect in one assertion: 0.914 vs the configured 1.128."""
page = derive_page(*projection_samples(*XYNOVA_UV_1_MM))
self.assertLess(page.page_aspect, 1.0)
self.assertAlmostEqual(page.page_aspect, 0.9137, places=3)
configured = 18.5 / 16.4
self.assertGreater(abs(configured - page.page_aspect), 0.2)
def test_real_slopes_round_trip(self):
"""Feeding the slopes AND ranges measured off the blend recovers the measured extent.
The measured `uv_1` affine is u = 0.057105098 x + 0.472297765 over x in
[-8.270676, 9.240895]; the intercept is only consistent with that range, which is why
a synthetic range would fail the [0,1]^2 bounds check.
"""
x_min, x_max = -8.270676, 9.240895
y_min, y_max = -7.890431, 8.086782
slope_u, intercept_u = 0.057105098, 0.472297765
slope_v, intercept_v = 0.062589139, 0.493855294
uvs, planes = [], []
for i in range(101):
x = x_min + (x_max - x_min) * i / 100.0
y = y_min + (y_max - y_min) * i / 100.0
uvs.append((slope_u * x + intercept_u, slope_v * y + intercept_v))
planes.append((x, y))
page = derive_page(uvs, planes)
self.assertAlmostEqual(page.page_width_mm, 17.5116, delta=0.01)
self.assertAlmostEqual(page.page_height_mm, 15.9772, delta=0.01)
def test_residual_is_essentially_zero_for_a_projection(self):
page = derive_page(*projection_samples(20.0, 10.0))
self.assertLess(page.residual_u, 1e-12)
self.assertLess(page.residual_v, 1e-12)
def test_mirrored_v_axis_is_still_sized(self):
page = derive_page(*projection_samples(20.0, 10.0, v_flip=True))
self.assertAlmostEqual(page.page_width_mm, 20.0, delta=0.01)
self.assertAlmostEqual(page.page_height_mm, 10.0, delta=0.01)
class VerificationTests(unittest.TestCase):
def test_unwrap_is_refused_and_names_the_escape_hatch(self):
"""A non-affine u-vs-x map is the `compute` case in miniature."""
uvs, planes = [], []
for i in range(101):
x = i * 0.1
uvs.append((x / 10.0 + 0.05 * (x / 10.0) ** 2, i / 100.0))
planes.append((x, i * 0.1))
with self.assertRaises(PageSizingError) as caught:
derive_page(uvs, planes)
message = str(caught.exception)
self.assertIn("not a bounds-projection", message)
self.assertIn("page: fixed", message)
def test_residual_tolerance_boundary(self):
uvs, planes = projection_samples(20.0, 20.0, steps=20)
# Nudge a single sample just inside, then just outside, the tolerance.
for offset, should_pass in ((0.5e-5, True), (5e-5, False)):
nudged = list(uvs)
nudged[7] = (nudged[7][0] + offset, nudged[7][1])
if should_pass:
derive_page(nudged, planes)
else:
with self.assertRaises(PageSizingError):
derive_page(nudged, planes)
def test_uv_box_not_unit_square_is_refused(self):
"""A layout fitted to something other than the view bounds has no derivable page."""
uvs, planes = projection_samples(20.0, 10.0)
squashed = [(u * 0.5, v) for u, v in uvs]
with self.assertRaises(PageSizingError) as caught:
derive_page(squashed, planes)
self.assertIn("[0,1]^2", str(caught.exception))
def test_flat_axis_is_refused(self):
planes = [(x, 0.0) for x, _ in ((i * 0.1, 0.0) for i in range(11))]
uvs = [(i / 10.0, i / 10.0) for i in range(11)]
with self.assertRaises(PageSizingError) as caught:
derive_page(uvs, planes)
self.assertIn("no extent", str(caught.exception))
def test_too_few_samples_is_refused(self):
with self.assertRaises(PageSizingError):
derive_page([(0.0, 0.0)], [(0.0, 0.0)])
def test_mismatched_lengths_are_refused(self):
with self.assertRaises(PageSizingError) as caught:
derive_page([(0.0, 0.0), (1.0, 1.0)], [(0.0, 0.0)])
self.assertIn("parallel", str(caught.exception))
def test_bad_px_per_mm_is_refused(self):
uvs, planes = projection_samples(20.0, 10.0)
for bad in (0, -1.0, "many"):
with self.assertRaises(PageSizingError):
derive_page(uvs, planes, px_per_mm=bad)
class InvariantTests(unittest.TestCase):
def test_page_and_raster_aspects_are_equal_by_construction(self):
"""This is what stops `_warn_if_page_aspect_differs` firing."""
for width_mm, height_mm in (
(17.5116, 15.9772), (21.1989, 23.2003), (1.0, 1.0), (100.0, 3.0), (3.0, 100.0)
):
with self.subTest(size=(width_mm, height_mm)):
page = derive_page(*projection_samples(width_mm, height_mm))
self.assertAlmostEqual(page.page_aspect, page.raster_aspect, places=12)
def test_page_is_the_rounded_raster_over_the_density(self):
page = derive_page(*projection_samples(17.5116, 15.9772))
self.assertAlmostEqual(
page.page_width_mm, page.raster_width_px / DEFAULT_PX_PER_MM, places=12
)
self.assertAlmostEqual(
page.page_height_mm, page.raster_height_px / DEFAULT_PX_PER_MM, places=12
)
def test_default_density_reproduces_the_shipped_rasters(self):
"""200 px/mm is what 3280x16.4 and 3700x18.5 already are, exactly."""
self.assertEqual(DEFAULT_PX_PER_MM, 200.0)
self.assertAlmostEqual(3280 / 16.4, 200.0, places=9)
self.assertAlmostEqual(3200 / 16.0, 200.0, places=9)
self.assertAlmostEqual(3700 / 18.5, 200.0, places=9)
def test_rounding_error_stays_far_below_the_alignment_tolerance(self):
"""At 200 px/mm the page can be off by at most 0.0025 mm; tolerance is 0.1 mm."""
page = derive_page(*projection_samples(*XYNOVA_UV_1_MM))
self.assertLessEqual(abs(page.page_width_mm - XYNOVA_UV_1_MM[0]), 0.5 / 200.0)
self.assertLessEqual(abs(page.page_height_mm - XYNOVA_UV_1_MM[1]), 0.5 / 200.0)
def test_describe_is_readable(self):
page = derive_page(*projection_samples(20.0, 10.0))
self.assertIn("page 20.000 x 10.000 mm", page.describe())
self.assertIn("200 px/mm", page.describe())
if __name__ == "__main__":
unittest.main()