Files
AnyImage/tests/anyimage/common/test_alpha_presentation.py
nan 938d95db0f 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.
2026-09-12 19:20:06 +08:00

224 lines
9.8 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 import image as images
from tests.support.color_image import color_image
@pytest.mark.parametrize("floating", [False, True])
def test_owned_color_images_preserve_source_and_use_premul(floating):
original = bpy.data.images.new("Alpha source", width=2, height=2, alpha=True, float_buffer=floating)
if floating:
original.alpha_mode = "PREMUL"
rgba = np.tile(np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), (2, 2, 1))
original.pixels.foreach_set(rgba.ravel())
source = SimpleNamespace(data=original)
expected = images.image_rgba(original)
cropped = color_image(source.data, (0, 0, 2, 2), expected)
edited = images.create_image_edit_result(original, rgba.ravel(), (2, 2))
try:
assert original.alpha_mode == ("PREMUL" if floating else "STRAIGHT")
np.testing.assert_allclose(images.image_rgba(original), expected)
assert edited.alpha_mode == original.alpha_mode
for image in (cropped,):
assert image.alpha_mode == original.alpha_mode
np.testing.assert_allclose(images.image_rgba(image), expected, atol=1 / 255)
finally:
for image in (cropped, edited, original):
bpy.data.images.remove(image)
def test_job_loader_preserves_edit_alpha_policy(tmp_path):
rgba = np.array([[[51, 102, 204, 64]]], dtype=np.uint8)
Image.fromarray(rgba).save(tmp_path / "foreground.png")
original = bpy.data.images.new("Job source", width=1, height=1, alpha=True)
result = images.load_image_edit_result(original, tmp_path / "foreground.png")
try:
assert result.alpha_mode == "STRAIGHT"
assert result.packed_file is not None
assert original.alpha_mode == "STRAIGHT"
finally:
bpy.data.images.remove(result)
bpy.data.images.remove(original)
def test_business_pixels_ignore_premul_read_interpretation():
original = bpy.data.images.new("Premul read source", width=1, height=1, alpha=True)
original.alpha_mode = "PREMUL"
expected = np.array([0.4, 0.25, 0.1, 0.5], dtype=np.float32)
result = images.create_image_edit_result(original, expected, (1, 1))
try:
result.reload()
interpreted = np.empty(4, dtype=np.float32)
result.pixels.foreach_get(interpreted)
assert interpreted[0] > expected[0]
np.testing.assert_allclose(images.image_pixels(result), expected, atol=1 / 255)
assert result.alpha_mode == "PREMUL"
finally:
bpy.data.images.remove(result)
bpy.data.images.remove(original)
@pytest.mark.parametrize("dirty", [False, True])
def test_business_read_preserves_shared_packed_source(dirty):
original = bpy.data.images.new("Read integrity", width=1, height=1, alpha=True)
original.alpha_mode = "PREMUL"
result = images.create_image_edit_result(original, np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), (1, 1))
owner = bpy.data.objects.new("Read owner", None)
owner.empty_display_type = "IMAGE"
owner.data = result
try:
result.reload()
if dirty:
result.pixels.foreach_set([0.9, 0.1, 0.3, 0.5])
result.update()
before = np.array(result.pixels[:])
packed = bytes(result.packed_file.data)
state = (result.alpha_mode, result.is_dirty, result.users)
count = len(bpy.data.images)
for _ in range(3):
pixels = images.image_pixels(result)
np.testing.assert_allclose(pixels, before if dirty else [0.2, 0.4, 0.8, 0.25], atol=1 / 255)
np.testing.assert_array_equal(result.pixels[:], before)
assert (result.alpha_mode, result.is_dirty, result.users) == state
assert bytes(result.packed_file.data) == packed
assert owner.data == result
assert len(bpy.data.images) == count
finally:
bpy.data.objects.remove(owner)
bpy.data.images.remove(result)
bpy.data.images.remove(original)
@pytest.mark.parametrize("floating", [False, True])
def test_generated_premul_reads_current_pixels(floating):
source = bpy.data.images.new("Generated read", width=1, height=1, alpha=True, float_buffer=floating)
try:
source.alpha_mode = "PREMUL"
source.pixels.foreach_set([0.2, 0.4, 0.8, 0.25])
before = np.array(source.pixels[:])
np.testing.assert_array_equal(images.image_pixels(source), before)
np.testing.assert_array_equal(source.pixels[:], before)
assert source.alpha_mode == "PREMUL"
finally:
bpy.data.images.remove(source)
@pytest.mark.parametrize("packed", [False, True])
@pytest.mark.parametrize("floating", [False, True])
@pytest.mark.parametrize("dirty", [False, True])
def test_file_read_preserves_byte_and_native_float_cache(tmp_path, packed, floating, dirty):
path = tmp_path / ("source.exr" if floating else "source.png")
if floating:
generated = bpy.data.images.new("EXR source", width=1, height=1, alpha=True, float_buffer=True)
try:
generated.pixels.foreach_set([0.2, 0.4, 0.8, 0.25])
generated.file_format = "OPEN_EXR"
generated.filepath_raw = str(path)
generated.save()
finally:
bpy.data.images.remove(generated)
else:
Image.fromarray(np.array([[[51, 102, 204, 64]]], dtype=np.uint8)).save(path)
source = bpy.data.images.load(str(path), check_existing=False)
try:
if packed:
source.pack()
source.alpha_mode = "PREMUL"
if dirty:
source.pixels.foreach_set([0.9, 0.1, 0.3, 0.5])
source.update()
before = np.array(source.pixels[:])
assert source.is_dirty == dirty
assert source.is_float == floating
expected = before if floating or dirty else np.array([51, 102, 204, 64]) / 255
count = len(bpy.data.images)
np.testing.assert_allclose(images.image_pixels(source), expected, atol=1e-7)
np.testing.assert_array_equal(source.pixels[:], before)
assert source.is_dirty == dirty and source.alpha_mode == "PREMUL"
assert len(bpy.data.images) == count
finally:
bpy.data.images.remove(source)
def test_isolated_decode_failure_releases_copy():
class FailedReader:
@property
def alpha_mode(self):
return "PREMUL"
@alpha_mode.setter
def alpha_mode(self, value):
raise RuntimeError("Decode failed")
reader = FailedReader()
source = SimpleNamespace(alpha_mode="PREMUL", is_dirty=False, is_float=False,
source="FILE", copy=lambda: reader)
with patch.object(images, "bpy", SimpleNamespace(data=SimpleNamespace(images=SimpleNamespace(remove=lambda value: removed.append(value))))):
removed = []
with pytest.raises(RuntimeError, match="Decode failed"):
images.image_pixels(source)
assert removed == [reader]
assert source.alpha_mode == "PREMUL"
def test_mask_pack_reopen_extend_and_repeat_frame_preserve_hidden_color(tmp_path):
from anyimage.common.selection import SelectionMask
from anyimage.operators.mask_tool import apply_alpha_mask
from anyimage.operators.frame_tool.projection import frame_source_projection
from anyimage.operators.frame_tool.compositing import composite_frame_pixels
source = bpy.data.images.new("Roundtrip source", width=2, height=2, alpha=True)
rgba = np.tile(np.array([0.2, 0.4, 0.8, 1], dtype=np.float32), (2, 2, 1))
full = SelectionMask(np.ones((2, 2)), (0, 0, 2, 2))
result = images.create_image_edit_result(source, apply_alpha_mask(rgba, full, "SUBTRACT"), (2, 2))
result.use_fake_user = True
name = result.name
bpy.data.images.remove(source)
path = str(tmp_path / "mask.blend")
bpy.ops.wm.save_as_mainfile(filepath=path)
bpy.ops.wm.open_mainfile(filepath=path)
result = bpy.data.images[name]
try:
restored = images.image_rgba(result)
np.testing.assert_allclose(restored[..., :3], rgba[..., :3], atol=1 / 255)
np.testing.assert_array_equal(restored[..., 3], 0)
added = images.create_image_edit_result(result, apply_alpha_mask(restored, full, "EXTEND"), (2, 2))
bpy.data.images.remove(result)
result = added
for _ in range(2):
source = frame_source_projection(np.eye(4), np.eye(4), np.eye(4), (-1, 1, -1, 1), (2, 2), perspective=False)
rgba_read = images.image_rgba(result)
source.update(rgba=rgba_read, pixels=images.premultiplied_rgba(images.image_pixels(result), (2, 2)),
image_size=(2, 2), active=True, name=result.name)
pixels = composite_frame_pixels((source,), ((0, 2), (2, 2), (2, 0), (0, 0)), (2, 2))
next_result = images.create_image_edit_result(result, pixels, (2, 2))
bpy.data.images.remove(result)
result = next_result
result.reload()
assert result.alpha_mode == "STRAIGHT" and result.packed_file is not None
np.testing.assert_allclose(images.image_rgba(result), rgba, atol=1 / 255)
finally:
bpy.data.images.remove(result)
def test_job_loader_reads_only_the_returned_file(tmp_path):
Image.new("RGBA", (2, 2), (255, 0, 0, 255)).save(tmp_path / "foreground.png")
Image.new("RGBA", (4, 4), (0, 0, 255, 255)).save(tmp_path / "upscale.png")
original = bpy.data.images.new("Source", width=1, height=1, alpha=True)
result = images.load_image_edit_result(original, tmp_path / "upscale.png")
try:
assert tuple(result.size) == (4, 4)
assert result.source == "FILE"
assert result.packed_file is not None
with pytest.raises(RuntimeError, match="Color result image does not exist"):
images.load_image_edit_result(original, tmp_path / "missing.png")
finally:
bpy.data.images.remove(result)
bpy.data.images.remove(original)