test(visual): a shadow-gap measurement, and the spawn height cleared

Tools/shadow_gap.py measures the vertical pixel gap between an object's lowest pixels and the
top of its shadow, as a fraction of the object's height, and calls it contact or floating. It
classifies a shadow by per-channel scaling - a shadow darkens every channel by the same
proportion, an object does not - which separates the two where an absolute delta cannot. Four
unit tests cover contact, floating, an empty box and the border median.

On the real spawn view it returns 'unknown': the crop contains two surfaces (pale wall behind,
dark road below) and one global background cannot classify both. Recorded as the next step, not
presented as working.

The spawn height is measured and cleared: tracing down from 30 m, the topmost surface under the
player is Z 366 and the capsule's feet are at 369 - 3 cm, the capsule skin. If the character
still reads as floating, the gap is between the rendered pavement and its collision surface, not
the spawn Z, and that is what to measure next.
This commit is contained in:
zwitschi committed 2026-11-09 17:07:23 +01:00
1 parent dde451fd69
commit f1d6283384
5 files changed
+230 -1

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
"""Tests for the pure parts of Tools/shadow_gap.py."""
import os
import sys
import unittest
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import shadow_gap as sg # noqa: E402
def synthetic(object_bottom, shadow_top, size=(80, 120), background=(200, 200, 196)):
"""A grey frame with a dark block (object) and a dark strip (shadow) below it."""
image = Image.new("RGB", size, background)
pixels = image.load()
for y in range(20, object_bottom):
for x in range(30, 50):
pixels[x, y] = (18, 18, 20) # object: far darker than the surface
for y in range(shadow_top, min(size[1] - 2, shadow_top + 12)):
for x in range(30, 50):
# shadow: a darkened version of the surface, as a real shadow is
pixels[x, y] = (140, 140, 138)
return image
class MeasureTests(unittest.TestCase):
def test_contact_reads_as_contact(self):
image = synthetic(object_bottom=60, shadow_top=61)
image.save("/tmp/gigline-shadow-contact.png")
result = sg.measure("/tmp/gigline-shadow-contact.png", (0.2, 0.05, 0.8, 0.95))
self.assertEqual(result["verdict"], "contact")
self.assertLessEqual(result["gap_px"], 2)
def test_a_gap_reads_as_floating(self):
image = synthetic(object_bottom=60, shadow_top=100)
image.save("/tmp/gigline-shadow-float.png")
result = sg.measure("/tmp/gigline-shadow-float.png", (0.2, 0.05, 0.8, 0.95))
self.assertEqual(result["verdict"], "floating")
self.assertGreater(result["gap_px"], result["object_height_px"] * 0.12)
def test_empty_box_reports_an_error(self):
image = Image.new("RGB", (40, 40), (200, 200, 200))
image.save("/tmp/gigline-shadow-empty.png")
result = sg.measure("/tmp/gigline-shadow-empty.png", (0.1, 0.1, 0.9, 0.9))
self.assertIn("error", result)
class BorderTests(unittest.TestCase):
def test_border_median_is_the_frame_colour(self):
image = Image.new("RGB", (40, 40), (180, 190, 200))
self.assertEqual(sg.border_median(image), (180, 190, 200))
if __name__ == "__main__":
unittest.main()