Files
AnyImage/tests/anyimage/common/test_hdr_image.py
nan 489003c2cc Add new tests and functionality for image processing and geometry handling
- Introduced color_image.py for preparing standalone color images for tests.
- Added hdr_image.py to create HDR textures for testing.
- Implemented test_image_depth_panorama.py to evaluate panorama topology, UVs, and depth discontinuities.
- Created test_shadeless.py to validate the opacity and HDR color endpoints of the Shadeless group.
- Developed moge3_export.py for exporting dynamic MoGe-3 backbone and ONNX graphs.
- Added material.py to inherit material slots onto generated geometry.
- Built image_depth_panorama.py to construct a quad panorama with depth separation and sampling.
2026-09-10 14:42:17 +08:00

190 lines
8.2 KiB
Python

from types import SimpleNamespace
from unittest.mock import patch
import bpy
import numpy as np
import pytest
from PIL import Image
from anyimage.common.hdr_image import HdrBackgroundInput, recognition_rgba, save_hdr_result
from anyimage.common.image import cleanup_image_input, image_content_state, image_rgba, restore_image_content
from anyimage.common.image_target import ImageEditTarget
from anyimage.common import image_target as targets
from tests.support.image_texture import texture, texture_context
from tests.support.hdr_image import hdr_texture
@pytest.mark.parametrize("source_kind", ["generated", "file", "packed", "dirty"])
@pytest.mark.parametrize("owner_kind", ["texture", "empty"])
@pytest.mark.parametrize("shared", [False, True])
def test_hdr_commit_keeps_float_pixels_and_transaction(hdr_texture, tmp_path, source_kind, owner_kind, shared):
texture = hdr_texture
source = texture.image
rgba = image_rgba(source).copy()
if source_kind != "generated":
path = tmp_path / "source.exr"
save_hdr_result(rgba, path)
source = bpy.data.images.load(str(path), check_existing=False)
texture.image = source
if source_kind == "packed":
source.pack()
if source_kind == "dirty":
rgba[1, 1, 0] = 19
source.pixels.foreach_set(np.flipud(rgba).ravel())
source.use_fake_user = True
if owner_kind == "empty":
owner = bpy.data.objects.new("HDR Empty", None)
owner.empty_display_type = "IMAGE"
owner.data = source
bpy.context.scene.collection.objects.link(owner)
texture.image = None
context = SimpleNamespace(object=owner, space_data=None)
else:
owner = texture
context = texture_context(texture)
if shared:
other = texture.id_data.nodes.new("ShaderNodeTexImage")
other.image = source
target = ImageEditTarget.capture(context)
snapshot = HdrBackgroundInput.prepare(source)
original_state = image_content_state(source)
alpha = np.linspace(0.03, 0.91, 12, dtype=np.float32).reshape(3, 4)
alpha_path = tmp_path / "alpha.npy"
np.save(alpha_path, alpha)
try:
with Image.open(snapshot.path) as preview:
assert preview.size == (4, 3)
assert np.asarray(preview)[0, 0, :3].max() == 0
result = snapshot.apply(target, alpha_path)
expected = rgba.copy()
expected[..., 3] *= alpha
assert result.is_float and result.packed_file and result.alpha_mode == "CHANNEL_PACKED"
np.testing.assert_allclose(image_rgba(result), expected, atol=1e-7)
assert (result == source) == (not shared)
if shared:
np.testing.assert_array_equal(image_rgba(other.image), rgba)
else:
assert result.use_fake_user
# The encoded result restores the same ID and floats after a source-state restore.
result_state = image_content_state(result)
restore_image_content(result, original_state)
np.testing.assert_allclose(image_rgba(result), rgba, atol=1e-7)
restore_image_content(result, result_state)
np.testing.assert_allclose(image_rgba(result), expected, atol=1e-7)
blend_path = tmp_path / "roundtrip.blend"
result_name = result.name
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path))
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
restored = bpy.data.images[result_name]
assert restored.is_float and restored.packed_file
np.testing.assert_allclose(image_rgba(restored), expected, atol=1e-7)
finally:
cleanup_image_input(snapshot.path, True)
@pytest.mark.parametrize("invalid", ["shape", "dtype", "nan", "range", "corrupt", "source", "colorspace", "binding"])
def test_hdr_rejects_invalid_result_and_source_changes(hdr_texture, tmp_path, invalid):
target = ImageEditTarget.capture(texture_context(hdr_texture))
snapshot = HdrBackgroundInput.prepare(target.image)
alpha = np.ones((3, 4), np.float32)
if invalid == "shape": alpha = alpha[:1]
if invalid == "dtype": alpha = alpha.astype(np.float64)
if invalid == "nan": alpha[0, 0] = np.nan
if invalid == "range": alpha[0, 0] = 2
path = tmp_path / "alpha.npy"
np.save(path, alpha)
if invalid == "corrupt": path.write_bytes(b"broken")
if invalid == "source": target.image.pixels[4] = 20
if invalid == "colorspace": target.image.colorspace_settings.name = "ACEScg"
if invalid == "binding": hdr_texture.image = bpy.data.images.new("Other", width=1, height=1)
before = image_rgba(hdr_texture.image).copy()
count = len(bpy.data.images)
try:
with pytest.raises(RuntimeError):
snapshot.apply(target, path)
np.testing.assert_array_equal(image_rgba(hdr_texture.image), before)
assert len(bpy.data.images) == count
finally:
cleanup_image_input(snapshot.path, True)
def test_hdr_preview_is_fixed_and_preserves_source(hdr_texture):
image = hdr_texture.image
before = image_rgba(image).copy()
first = HdrBackgroundInput.prepare(image)
bpy.context.scene.view_settings.view_transform = "Standard"
bpy.context.scene.view_settings.exposure = 7
second = HdrBackgroundInput.prepare(image)
try:
with Image.open(first.path) as a, Image.open(second.path) as b:
np.testing.assert_array_equal(np.asarray(a), np.asarray(b))
np.testing.assert_array_equal(image_rgba(image), before)
np.testing.assert_array_equal(recognition_rgba(np.zeros((1, 1, 4), np.float32)), [[[0, 0, 0, 1]]])
finally:
cleanup_image_input(first.path, True)
cleanup_image_input(second.path, True)
def test_hdr_rejects_animation_before_export(hdr_texture):
hdr_texture.image.source = "SEQUENCE"
with pytest.raises(RuntimeError, match="still images"):
HdrBackgroundInput.prepare(hdr_texture.image)
def test_hdr_export_failure_releases_blender_resources(hdr_texture, tmp_path):
images, scenes = len(bpy.data.images), len(bpy.data.scenes)
blocked = tmp_path / "file"
blocked.write_bytes(b"not a directory")
with pytest.raises(RuntimeError):
save_hdr_result(image_rgba(hdr_texture.image), blocked / "output.exr")
assert (len(bpy.data.images), len(bpy.data.scenes)) == (images, scenes)
def test_straight_exr_only_changes_alpha(hdr_texture, tmp_path):
path = tmp_path / "straight.exr"
encoded = image_rgba(hdr_texture.image).copy()
save_hdr_result(encoded, path)
source = bpy.data.images.load(str(path), check_existing=False)
source.alpha_mode = "STRAIGHT"
hdr_texture.image = source
before = image_rgba(source).copy()
np.testing.assert_allclose(before[..., :3], encoded[..., :3] * encoded[..., 3:4], atol=1e-7)
snapshot = HdrBackgroundInput.prepare(source)
alpha_path = tmp_path / "alpha.npy"
np.save(alpha_path, np.full((3, 4), 0.3, np.float32))
try:
result = snapshot.apply(ImageEditTarget.capture(texture_context(hdr_texture)), alpha_path)
rgba = image_rgba(result)
np.testing.assert_array_equal(rgba[..., :3], before[..., :3])
np.testing.assert_allclose(rgba[..., 3], before[..., 3] * 0.3, atol=1e-7)
finally:
cleanup_image_input(snapshot.path, True)
def test_hdr_failed_commit_restores_unsaved_source(hdr_texture, tmp_path):
target = ImageEditTarget.capture(texture_context(hdr_texture))
snapshot = HdrBackgroundInput.prepare(target.image)
before = image_rgba(target.image).copy()
original_source = target.image.source
alpha_path = tmp_path / "alpha.npy"
np.save(alpha_path, np.full((3, 4), 0.3, np.float32))
calls = 0
def fail_first(image, state):
nonlocal calls
calls += 1
restore_image_content(image, state)
if calls == 1:
raise RuntimeError("Commit failed")
count = len(bpy.data.images)
try:
with patch.object(targets, "restore_image_content", side_effect=fail_first):
with pytest.raises(RuntimeError, match="Commit failed"):
snapshot.apply(target, alpha_path, prepare_undo=True)
np.testing.assert_array_equal(image_rgba(target.image), before)
assert target.image.source == original_source
assert len(bpy.data.images) == count
finally:
cleanup_image_input(snapshot.path, True)