feat(facades): 0.1.4 — module kit, textured materials, theatre trim (#30)
Merged PR #30: 0.1.4 — facade module kit, textured materials, theatre trim Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit was merged in pull request #30.
This commit is contained in:
1 parent
d6d3d54810
commit
5e2c5e29c0
90 files changed
+2592
-413
No files matched your search
@@ -0,0 +1,139 @@
|
||||
"""Tests for Tools/visual_loop.py — metrics, comparison and feedback rules.
|
||||
|
||||
Synthetic 8-bit images are written to a temp directory and measured, so the
|
||||
numbers can be reasoned about directly (a mid-grey flat frame has near-zero edge
|
||||
density and contrast; a checkerboard has a lot of both).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, os.path.join(ROOT, "Tools"))
|
||||
|
||||
import visual_loop as vl # noqa: E402
|
||||
|
||||
|
||||
def write_image(path, kind, size=(64, 64)):
|
||||
img = Image.new("RGB", size)
|
||||
if kind == "flat":
|
||||
img.paste((128, 128, 128), (0, 0, size[0], size[1]))
|
||||
elif kind == "dark":
|
||||
img.paste((10, 10, 10), (0, 0, size[0], size[1]))
|
||||
elif kind == "checker":
|
||||
px = img.load()
|
||||
for y in range(size[1]):
|
||||
for x in range(size[0]):
|
||||
px[x, y] = (240, 240, 240) if (x // 4 + y // 4) % 2 else (20, 20, 20)
|
||||
elif kind == "sky":
|
||||
# bright top third, mid stone bottom: composition like an elevation
|
||||
px = img.load()
|
||||
for y in range(size[1]):
|
||||
for x in range(size[0]):
|
||||
px[x, y] = (150, 190, 230) if y < size[1] // 3 else (170, 165, 155)
|
||||
img.save(path)
|
||||
return path
|
||||
|
||||
|
||||
class MetricsTest(unittest.TestCase):
|
||||
def test_flat_frame_has_no_edges_and_no_contrast(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = vl.metrics(write_image(os.path.join(tmp, "a.png"), "flat"))
|
||||
self.assertEqual(m["size"], [64, 64])
|
||||
self.assertAlmostEqual(m["mean_luma"], 128.0, delta=1.0)
|
||||
self.assertLess(m["rms_contrast"], 1.0)
|
||||
self.assertLess(m["edge_density"], vl.MIN_EDGE_DENSITY)
|
||||
|
||||
def test_checker_frame_has_edges_and_contrast(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = vl.metrics(write_image(os.path.join(tmp, "b.png"), "checker"))
|
||||
self.assertGreater(m["edge_density"], 0.01)
|
||||
self.assertGreater(m["rms_contrast"], 50.0)
|
||||
|
||||
def test_composition_grid_is_nine_cells(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = vl.metrics(write_image(os.path.join(tmp, "c.png"), "sky"))
|
||||
self.assertEqual(len(m["grid_luma"]), 9)
|
||||
# sky row should be brighter than the stone rows
|
||||
self.assertGreater(m["grid_luma"][0], m["grid_luma"][8])
|
||||
|
||||
def test_dominant_palette_is_reported(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
m = vl.metrics(write_image(os.path.join(tmp, "d.png"), "flat"))
|
||||
self.assertTrue(m["dominant"])
|
||||
self.assertAlmostEqual(m["dominant"][0]["share"], 1.0, delta=0.05)
|
||||
|
||||
|
||||
class CompareTest(unittest.TestCase):
|
||||
def _metrics(self, kind):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
return vl.metrics(write_image(os.path.join(tmp, "x.png"), kind))
|
||||
|
||||
def test_identical_images_score_high(self):
|
||||
m = self._metrics("checker")
|
||||
delta = vl.compare(m, m)
|
||||
self.assertGreater(delta["score"], 0.9)
|
||||
self.assertEqual(delta["delta_luma"], 0.0)
|
||||
|
||||
def test_flat_against_checker_scores_low_on_edges(self):
|
||||
flat, checker = self._metrics("flat"), self._metrics("checker")
|
||||
delta = vl.compare(flat, checker)
|
||||
self.assertLess(delta["edge_ratio"], 0.5)
|
||||
self.assertLess(delta["score"], 0.9)
|
||||
|
||||
def test_dark_against_bright_reports_a_luma_delta(self):
|
||||
dark, bright = self._metrics("dark"), self._metrics("checker")
|
||||
delta = vl.compare(dark, bright)
|
||||
self.assertLess(delta["delta_luma"], -50)
|
||||
|
||||
|
||||
class FeedbackTest(unittest.TestCase):
|
||||
def test_low_edge_ratio_names_the_modelling_skills(self):
|
||||
notes = vl.feedback("x", {}, {}, {
|
||||
"delta_luma": 0, "contrast_ratio": 1.0, "edge_ratio": 0.3,
|
||||
"palette_distance": 10, "composition_correlation": 0.8, "score": 0.5})
|
||||
joined = " ".join(notes)
|
||||
self.assertIn("Relief", joined)
|
||||
self.assertIn("blender-modeler", joined)
|
||||
|
||||
def test_bright_and_compressed_flags_lighting_and_lookdev(self):
|
||||
notes = vl.feedback("x", {}, {}, {
|
||||
"delta_luma": 40, "contrast_ratio": 0.5, "edge_ratio": 1.0,
|
||||
"palette_distance": 10, "composition_correlation": 0.8, "score": 0.5})
|
||||
joined = " ".join(notes)
|
||||
self.assertIn("Too bright", joined)
|
||||
self.assertIn("lookdev", joined)
|
||||
|
||||
def test_framing_mismatch_names_the_camera_skill(self):
|
||||
notes = vl.feedback("x", {}, {}, {
|
||||
"delta_luma": 0, "contrast_ratio": 1.0, "edge_ratio": 1.0,
|
||||
"palette_distance": 10, "composition_correlation": 0.1, "score": 0.5})
|
||||
self.assertIn("camera-cinematography", " ".join(notes))
|
||||
|
||||
def test_no_delta_reports_no_action(self):
|
||||
notes = vl.feedback("x", {}, {}, {
|
||||
"delta_luma": 0, "contrast_ratio": 1.0, "edge_ratio": 1.0,
|
||||
"palette_distance": 5, "composition_correlation": 0.9, "score": 0.95})
|
||||
self.assertIn("No metric is outside its tolerance", notes[0])
|
||||
|
||||
|
||||
class TargetSpecTest(unittest.TestCase):
|
||||
def test_every_target_has_expectation_and_reference(self):
|
||||
for target in vl.load_targets():
|
||||
self.assertTrue(target["expect"].strip(), target["name"])
|
||||
self.assertTrue(target["references"], target["name"])
|
||||
self.assertTrue(target["shot"].startswith("Screenshots/demo/"),
|
||||
f"{target['name']} must be a demo-map shot")
|
||||
|
||||
def test_iteration_output_is_gitignored(self):
|
||||
# Iterations are deliberately uncommitted until the owner confirms a shot.
|
||||
ignore = open(os.path.join(ROOT, ".gitignore"), encoding="utf-8").read()
|
||||
self.assertIn("/Art/reference/iterations/", ignore)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user