feat(character): ComfyUI blueprints and a render-vs-blueprint evaluation
Two tools that turn character work into something measurable. - Tools/comfyui_character_blueprint.py generates a four-view sheet (front, side, back, head) plus a contact sheet from one description, with fixed seeds and a JSON record of prompts and prompt ids. First sheet: 'GigLine walker', lean pedestrian in a navy tee, jeans and sneakers. - Tools/character_match.py compares a rendered character against a sheet: figure aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette distance, and a side-by-side montage. The deltas stay separate on purpose - a single similarity score between a photoreal sheet and a low-detail body would be a number with no meaning. - comfyui_generate.py grew wait_for_image() so both tools share the polling path instead of duplicating it. - 13 unit tests for the colour maths, masks and palette; 95 tool tests pass. First real result, recorded rather than smoothed over: aspect delta 1.24, fill delta 0.12, mean deltaE 31.9, palette deltaE 25.4. The montage shows the reason - the model is a nude body and the blueprint wears a tee and jeans, so clothing dominates every metric. Figure segmentation is explicit (--render-box): auto-detection picked backdrop blobs on every attempt, and a background plate is the proper fix.
This commit is contained in:
1 parent
1fc5835327
commit
85a8437cbc
56 files changed
+841
-170
No files matched your search
@@ -0,0 +1,82 @@
|
||||
"""Tests for the pure parts of Tools/character_match.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 character_match as cm # noqa: E402
|
||||
|
||||
|
||||
class ColourTests(unittest.TestCase):
|
||||
def test_white_is_light_and_neutral(self):
|
||||
light, a, b = cm.rgb_to_lab((255, 255, 255))
|
||||
self.assertAlmostEqual(light, 100.0, delta=0.2)
|
||||
self.assertAlmostEqual(a, 0.0, delta=0.2)
|
||||
self.assertAlmostEqual(b, 0.0, delta=0.2)
|
||||
|
||||
def test_black_is_dark(self):
|
||||
self.assertAlmostEqual(cm.rgb_to_lab((0, 0, 0))[0], 0.0, delta=0.1)
|
||||
|
||||
def test_identical_colours_have_no_distance(self):
|
||||
self.assertAlmostEqual(cm.delta_e76(cm.rgb_to_lab((120, 90, 70)),
|
||||
cm.rgb_to_lab((120, 90, 70))), 0.0, delta=1e-9)
|
||||
|
||||
def test_red_green_are_far_apart(self):
|
||||
distance = cm.delta_e76(cm.rgb_to_lab((200, 40, 40)), cm.rgb_to_lab((40, 160, 40)))
|
||||
self.assertGreater(distance, 40.0)
|
||||
|
||||
|
||||
class MaskTests(unittest.TestCase):
|
||||
def _image(self, figure_colour, background_colour):
|
||||
image = Image.new("RGB", (60, 100), background_colour)
|
||||
pixels = image.load()
|
||||
for y in range(20, 90):
|
||||
for x in range(25, 35):
|
||||
pixels[x, y] = figure_colour
|
||||
return image
|
||||
|
||||
def test_saturated_figure_on_grey_is_found(self):
|
||||
mask = cm._subject_mask(self._image((150, 90, 60), (190, 190, 190)))
|
||||
box = cm.bbox(mask)
|
||||
self.assertIsNotNone(box)
|
||||
self.assertLess(box[0], 30)
|
||||
self.assertGreater(box[2], 30)
|
||||
|
||||
def test_grey_frame_yields_empty_mask(self):
|
||||
mask = cm._subject_mask(self._image((190, 190, 190), (190, 190, 190)))
|
||||
self.assertIsNone(cm.bbox(mask))
|
||||
|
||||
def test_stats_report_figure_proportions(self):
|
||||
mask = Image.new("L", (40, 80), 0)
|
||||
pixels = mask.load()
|
||||
for y in range(10, 70):
|
||||
for x in range(15, 25):
|
||||
pixels[x, y] = 255
|
||||
stats = cm.mask_stats(mask)
|
||||
self.assertEqual(stats["area"], 600)
|
||||
self.assertAlmostEqual(stats["aspect"], 6.0, delta=0.01)
|
||||
self.assertAlmostEqual(stats["fill"], 1.0, delta=0.01)
|
||||
|
||||
|
||||
class PaletteTests(unittest.TestCase):
|
||||
def test_dominant_colour_is_reported(self):
|
||||
colours = [(200, 40, 40)] * 90 + [(20, 60, 200)] * 10
|
||||
palette = cm.dominant(colours, count=2)
|
||||
self.assertEqual(len(palette), 2)
|
||||
red, _, _ = cm.rgb_to_lab(palette[0])
|
||||
self.assertGreater(red, 30.0)
|
||||
|
||||
def test_palette_distance_is_zero_for_the_same_palette(self):
|
||||
palette = cm.dominant([(200, 40, 40)] * 50)
|
||||
self.assertAlmostEqual(cm.palette_delta_e(palette, palette), 0.0, delta=1e-6)
|
||||
|
||||
def test_empty_palette_is_not_a_number(self):
|
||||
self.assertNotEqual(cm.palette_delta_e([], [(1, 2, 3)]),
|
||||
cm.palette_delta_e([], [(1, 2, 3)]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user