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

@@ -18,21 +18,19 @@ def test_target_uses_edit_tree_and_survives_renaming(texture, tmp_path):
texture.id_data.nodes.active = other
context.space_data.edit_tree = None
write_result(tmp_path)
target.apply(tmp_path)
target.apply(tmp_path / "foreground.png")
assert tuple(texture.image.size) == (8, 6)
assert other.image is None
@pytest.mark.parametrize("color_space,alpha_mode", [
("sRGB", "PREMUL"),
("AgX Base sRGB", "STRAIGHT"),
("Filmic sRGB", "STRAIGHT"),
("sRGB", "STRAIGHT"),
("AgX Base sRGB", "PREMUL"),
@pytest.mark.parametrize("color_space,alpha_mode,filename,shared", [
("sRGB", "PREMUL", "foreground.png", True),
("AgX Base sRGB", "STRAIGHT", "upscale.png", False),
("Filmic sRGB", "STRAIGHT", "foreground.png", False),
("sRGB", "STRAIGHT", "upscale.png", True),
("AgX Base sRGB", "PREMUL", "foreground.png", True),
])
@pytest.mark.parametrize("prefix", ["image.", "upscale."])
@pytest.mark.parametrize("shared", [False, True])
def test_image_actions_preserve_source_color_space_and_alpha(texture, tmp_path, color_space, alpha_mode, prefix, shared):
def test_image_actions_preserve_source_color_space_and_alpha(texture, tmp_path, color_space, alpha_mode, filename, shared):
texture.image.colorspace_settings.name = color_space
texture.image.alpha_mode = alpha_mode
original = texture.image
@@ -41,8 +39,8 @@ def test_image_actions_preserve_source_color_space_and_alpha(texture, tmp_path,
other = texture.id_data.nodes.new("ShaderNodeTexImage")
other.image = original
target = targets.ImageEditTarget.capture(texture_context(texture))
write_result(tmp_path, prefix)
target.apply(tmp_path, frame_prefix=prefix)
write_result(tmp_path, filename)
target.apply(tmp_path / filename)
result = texture.image
assert result.colorspace_settings.name == color_space
assert result.alpha_mode == alpha_mode
@@ -102,7 +100,7 @@ def test_target_rejects_changed_identity_before_loading_result(texture, tmp_path
bpy.data.images.remove(texture.image)
count = len(bpy.data.images)
with pytest.raises(RuntimeError, match="no longer available"):
target.apply(tmp_path)
target.apply(tmp_path / "foreground.png")
assert len(bpy.data.images) == count
@@ -137,7 +135,7 @@ def test_texture_commit_preserves_structure_and_isolates_users(texture, tmp_path
shared = reference in {"same_tree", "other_material", "empty", "brush"}
links = [(link.from_socket, link.to_socket) for link in texture.id_data.links]
write_result(tmp_path)
target.apply(tmp_path)
target.apply(tmp_path / "foreground.png")
assert (texture.image == original) == (not shared)
assert tuple(texture.image.size) == (8, 6)
assert texture.image.colorspace_settings.name == "Non-Color"
@@ -163,63 +161,46 @@ def test_texture_commit_preserves_structure_and_isolates_users(texture, tmp_path
assert len(reopened.id_data.links) == len(links)
@pytest.mark.parametrize("shared", [False, True])
def test_animated_texture_preserves_playback_and_reopens(texture, tmp_path, shared):
write_result(tmp_path, count=4)
texture.image.source = "SEQUENCE"
texture.image.filepath_raw = str(tmp_path / "image.0001.png")
texture.image_user.frame_start = 9
texture.image_user.frame_offset = 1
texture.image_user.frame_duration = 2
texture.image_user.use_cyclic = True
context = texture_context(texture)
target = targets.ImageEditTarget.capture(context)
assert (target.start_frame, target.frame_count) == (2, 2)
if shared:
other = texture.id_data.nodes.new("ShaderNodeTexImage")
other.image = texture.image
with patch.object(targets, "prepare_image_input", return_value=(tmp_path, False)):
target.prepare(context)
assert context.scene.anyimage_settings.video_frames == 2
target.apply(tmp_path)
assert images.image_user_settings(texture) == (9, 0, 4, True)
assert texture.image_user.use_auto_refresh
assert texture.image.source == "SEQUENCE"
path = str(tmp_path / "animated.blend")
bpy.ops.wm.save_as_mainfile(filepath=path)
bpy.ops.wm.open_mainfile(filepath=path)
reopened = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
assert images.image_user_settings(reopened) == (9, 0, 4, True)
assert len(images.collect_sequence_frames(reopened.image)) == 4
@pytest.mark.parametrize("source", ["MOVIE", "SEQUENCE"])
def test_target_rejects_animated_image(texture, source):
texture.image.source = source
with pytest.raises(RuntimeError, match="Only still images"):
targets.ImageEditTarget.capture(texture_context(texture))
@pytest.mark.parametrize("shared", [False, True])
def test_texture_failed_commit_restores_dirty_pixels_and_playback(texture, tmp_path, shared):
def test_failed_texture_assignment_restores_source(texture, tmp_path, shared):
original = texture.image
if shared:
other = texture.id_data.nodes.new("ShaderNodeTexImage")
other.image = original
original.pixels.foreach_set(np.tile([0.1, 0.3, 0.7, 0.5], 12).astype(np.float32))
packed = bytes(original.packed_file.data)
pixels = images.image_pixels(original)
timing = images.image_user_settings(texture)
write_result(tmp_path, count=2)
result = bpy.data.images.load(str(tmp_path / "image.0001.png"))
result.source = "SEQUENCE"
result = images.load_image_edit_result(original, write_result(tmp_path))
count = len(bpy.data.images) - 1
def fail_configuration(user, *args, **kwargs):
user.frame_start = 80
user.use_auto_refresh = True
raise RuntimeError("Playback failed")
class FailingTarget:
failed = False
with patch.object(targets, "configure_image_user", side_effect=fail_configuration):
with pytest.raises(RuntimeError, match="Playback failed"):
targets.replace_texture_image(texture, result)
def __getattr__(self, name):
return getattr(texture, name)
@property
def image(self):
return texture.image
@image.setter
def image(self, value):
texture.image = value
if not self.failed:
self.failed = True
raise RuntimeError("Assignment failed")
with patch.object(targets, "has_other_image_user", return_value=shared):
with pytest.raises(RuntimeError, match="Assignment failed"):
targets.replace_texture_image(FailingTarget(), result)
assert texture.image == original
assert tuple(original.size) == (4, 3)
assert bytes(original.packed_file.data) == packed
np.testing.assert_array_equal(images.image_pixels(original), pixels)
assert images.image_user_settings(texture) == timing
assert not texture.image_user.use_auto_refresh
assert len(bpy.data.images) == count