Add new functionality for image rectification and depth metadata handling

- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

View File

@@ -78,24 +78,6 @@ class PreferencesTest(BlenderTestCase):
)
def test_manifest_rejects_old_mps_device(self):
properties = self.anyimage.properties
with (
patch.object(
self.anyimage.runtime,
"environment_manifest",
return_value={"devices": ["mps", "cpu"]},
),
patch.object(properties.sys, "platform", "darwin"),
patch.object(
properties.platform,
"machine",
return_value="arm64",
),
):
self.assertEqual(properties.supported_devices(), ["cpu"])
def test_windows_keeps_default_directml_device(self):
properties = self.anyimage.properties
with (
@@ -112,38 +94,17 @@ class PreferencesTest(BlenderTestCase):
)
def test_geometry_model_choices_match_moge_catalog(self):
properties = self.anyimage.properties
expected = tuple(properties.shared_model_catalog.MOGE_MODELS)
geometry = properties.geometry_model_enum_items(None, None)
self.assertEqual(tuple(item[0] for item in geometry), expected)
def test_model_tiers_only_appear_in_choices(self):
properties = self.anyimage.properties
preferences = self.anyimage.preferences
catalog = {model["key"]: model for model in properties.model_catalog()}
for items in (
properties.geometry_model_enum_items(None, None),
preferences.BACKGROUND_MODEL_ITEMS,
preferences.UPSCALE_MODEL_ITEMS,
):
for item, tier in zip(items, ("Fast", "Base", "Pro")):
label = catalog[item[0]]["label"]
self.assertNotIn(" | ", label)
self.assertEqual(item[1], f"{tier} | {label}")
self.assertEqual(properties.upscale_model_label("HAT_GAN_X4_SHARPER"), "HAT Sharper")
def test_model_preferences_keep_valid_choices_and_normalize_retired_keys(self):
def test_model_preferences_keep_valid_choices_and_reset_unknown_keys(self):
module = self.anyimage.preferences
stored = self.fake_bpy.context.preferences.addons["anyimage"].preferences
for attribute, reader, valid, retired, default in (
("geometry_model", module.configured_geometry_model, "MOGE3_VITL", "MOGE2_VITL_NORMAL", "MOGE2_VITS_NORMAL"),
("background_model", module.configured_background_model, "BEN2_BASE", "UNKNOWN", "BIREFNET_LITE"),
("upscale_model", module.configured_upscale_model, "REALESRGAN_X4PLUS", "PISA_SR", "REALESRGAN_GENERAL_WDN_X4V3"),
for attribute, reader, valid, default in (
("geometry_model", module.configured_geometry_model, "MOGE3_VITL", "MOGE2_VITS_NORMAL"),
("background_model", module.configured_background_model, "BEN2_BASE", "BIREFNET_LITE"),
("upscale_model", module.configured_upscale_model, "REALESRGAN_X4PLUS", "REALESRGAN_GENERAL_WDN_X4V3"),
):
setattr(stored, attribute, valid)
self.assertEqual(reader(), valid)
setattr(stored, attribute, retired)
setattr(stored, attribute, "UNKNOWN")
self.assertEqual(reader(), default)
self.assertEqual(getattr(stored, attribute), default)
self.assertEqual({item[0]: item[-1] for item in module.BACKGROUND_MODEL_ITEMS}["BEN2_BASE"], 0)
@@ -205,21 +166,18 @@ class PreferencesTest(BlenderTestCase):
def test_preferences_draws_upscale_model_download(self):
preference_module = importlib.import_module("anyimage.preferences")
downloads = []
properties = []
labels = []
boxes = []
class Layout:
enabled = True
def prop(self, _owner, name, **_options):
properties.append(name)
pass
def separator(self, **_options):
return None
def label(self, **_options):
labels.append(_options.get("text"))
pass
def row(self, **_options):
return self
@@ -231,7 +189,6 @@ class PreferencesTest(BlenderTestCase):
return self
def box(self):
boxes.append(self)
return self
def operator(self, identifier, **_options):
@@ -283,106 +240,10 @@ class PreferencesTest(BlenderTestCase):
[download.model for download in downloads],
["MOGE2_VITS_NORMAL", "BEN2_BASE", "HAT_GAN_X4_SHARPER"],
)
self.assertEqual(
[
label
for label in labels
if label
in {
"Server",
"AI",
"Tools",
"Frame Tool",
"Depth & Normal",
"Cutout Tool",
}
],
[
"Server",
"AI",
"Tools",
"Frame Tool",
"Depth & Normal",
"Cutout Tool",
],
)
self.assertEqual(len(boxes), 4)
self.assertIn("Shading", labels)
self.assertIn("adapt_material_to_view_transform", properties)
adaptation = preference_module.AnyImagePreferences.__annotations__[
"adapt_material_to_view_transform"
]
self.assertTrue(adaptation["default"])
self.assertNotIn("update", adaptation)
self.assertIn(preference_module.AnyImagePreferences, self.anyimage.CLASSES)
self.assertNotIn("Normal Model", labels)
self.assertIn("background_model", properties)
self.assertIn("upscale_model", properties)
self.assertIn("max_ai_input_size", properties)
self.assertIn("max_frame_resolution", properties)
self.assertIn("max_cutout_mesh_resolution", properties)
self.assertIn("balloon_profile_algorithm", properties)
balloon_profile = preference_module.AnyImagePreferences.__annotations__[
"balloon_profile_algorithm"
]
self.assertEqual(balloon_profile["default"], "POISSON")
self.assertEqual(
tuple(item[0] for item in balloon_profile["items"]),
("POISSON", "TEDDY"),
)
self.assertEqual(
preference_module.AnyImagePreferences.__annotations__["upscale_model"][
"name"
],
"Upscale Image Model",
)
self.assertEqual(
preference_module.AnyImagePreferences.__annotations__["geometry_model"][
"name"
],
"Generate Depth Map Model",
)
self.assertEqual(
preference_module.AnyImagePreferences.__annotations__["background_model"][
"name"
],
"Remove Background Model",
)
def test_ready_models_show_their_ai_feature(self):
preference_module = importlib.import_module("anyimage.preferences")
labels = []
class Layout:
def row(self, **_options):
return self
def label(self, **options):
labels.append((options.get("text"), options.get("icon")))
preferences = preference_module.AnyImagePreferences()
model = {"ready": True}
for ready_label in (
"AI Generate Depth Map",
"AI Remove Background",
"AI Upscale Image",
):
preferences._draw_model_status(Layout(), model, ready_label)
self.assertEqual(
labels,
[
("AI Generate Depth Map", "CHECKMARK"),
("AI Remove Background", "CHECKMARK"),
("AI Upscale Image", "CHECKMARK"),
],
)
def test_preferences_hide_ai_until_server_is_installed(self):
preference_module = importlib.import_module("anyimage.preferences")
labels = []
operators = []
class Layout:
@@ -392,7 +253,7 @@ class PreferencesTest(BlenderTestCase):
return self
def label(self, **options):
labels.append(options.get("text"))
pass
def prop(self, *_args, **_options):
return None
@@ -424,10 +285,6 @@ class PreferencesTest(BlenderTestCase):
):
preferences.draw(None)
self.assertEqual(
labels,
["Server", "Shading", "Tools", "Frame Tool", "Depth & Normal", "Cutout Tool"],
)
self.assertEqual(
operators,
["anyimage.setup_ai_environment", "anyimage.open_server_log"],
@@ -445,7 +302,7 @@ class PreferencesTest(BlenderTestCase):
operator.resolution_level = 5
operator.max_input_size = 1536
context = SimpleNamespace(
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1))
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
)
with (
patch.object(self.convert_to_plane, "require_environment"),
@@ -456,15 +313,12 @@ class PreferencesTest(BlenderTestCase):
parameters = operator.request(context)
self.assertEqual(parameters["max_input_size"], 1536)
self.assertNotIn("shape", parameters)
self.assertNotIn("polygon", parameters)
def test_upscale_execute_reads_model_and_size_from_preferences(self):
source_image = SimpleNamespace(
name="Original",
source="FILE",
anyimage_upscale_source=None,
)
source_object = SimpleNamespace(
name="Source",
@@ -475,7 +329,7 @@ class PreferencesTest(BlenderTestCase):
)
context = SimpleNamespace(
object=source_object,
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)),
scene=SimpleNamespace(anyimage_settings=SimpleNamespace()),
)
operator = self.upscale.UpscaleImage()
with (