Files
AnyImage/src/anyimage/operators/depth_plane/data.py

263 lines
8.3 KiB
Python

from pathlib import Path
import bpy
from ..image import force_sequence_source, pack_sequence_if_possible
DEPTH_KEY_CANDIDATES = ("depth", "depths")
INTRINSICS_KEY_CANDIDATES = ("intrinsics", "ixts")
def create_depth_sequence_image(
name,
depth_frames,
scene,
frame_directory,
frame_prefix="depth",
):
"""Write one EXR per depth frame and return a SEQUENCE image datablock."""
import numpy as np
frame_directory = Path(frame_directory)
frame_directory.mkdir(parents=True, exist_ok=True)
height, width = depth_frames[0].shape
image = bpy.data.images.new(
name,
width=width,
height=height,
alpha=False,
float_buffer=True,
)
image.colorspace_settings.name = "Non-Color"
image_settings = scene.render.image_settings
original_file_format = image_settings.file_format
frame_paths = []
try:
image_settings.file_format = "OPEN_EXR"
for frame_index, depth_values in enumerate(depth_frames):
# NPZ arrays use a top-left origin, while Blender image pixels start below.
depth_pixels = np.flipud(depth_values)
rgba = np.empty((height, width, 4), dtype=np.float32)
rgba[..., 0] = depth_pixels
rgba[..., 1] = depth_pixels
rgba[..., 2] = depth_pixels
rgba[..., 3] = 1.0
image.pixels.foreach_set(rgba.ravel())
frame_path = (
frame_directory / f"{frame_prefix}.{frame_index + 1:04d}.exr"
)
image.save_render(str(frame_path), scene=scene)
if not frame_path.is_file():
raise RuntimeError(f"Blender did not write: {frame_path}")
frame_paths.append(frame_path)
finally:
image_settings.file_format = original_file_format
bpy.data.images.remove(image, do_unlink=True)
if not frame_paths:
raise RuntimeError("Unable to write the depth frame sequence")
try:
sequence_image = bpy.data.images.load(
str(frame_paths[0]),
check_existing=False,
)
force_sequence_source(sequence_image)
except (OSError, RuntimeError) as error:
if "sequence_image" in locals():
bpy.data.images.remove(sequence_image, do_unlink=True)
raise RuntimeError(
"Unable to load the depth frame sequence"
) from error
sequence_image.name = name
sequence_image.colorspace_settings.name = "Non-Color"
pack_sequence_if_possible(sequence_image, len(frame_paths))
return sequence_image
def scale_depth_values(depth, depth_scale):
return prepare_depth_values(depth) * depth_scale
def extract_depth_arrays(npz_path):
try:
import numpy as np
except ImportError as error:
raise RuntimeError("Blender Python does not provide NumPy") from error
try:
with np.load(npz_path, allow_pickle=False) as archive:
depth_key = next(
(
key
for key in DEPTH_KEY_CANDIDATES
if key in archive.files
),
None,
)
if depth_key is None:
keys = ", ".join(archive.files) or "none"
raise ValueError(
f"Mini NPZ has no depth array; available keys: {keys}"
)
depth = np.asarray(archive[depth_key], dtype=np.float32)
except OSError as error:
raise ValueError(f"Unable to read Mini NPZ: {npz_path}") from error
if depth.ndim >= 3 and depth.shape[-1] == 1:
depth = depth[..., 0]
if depth.ndim == 2:
depth = depth[None, ...]
if depth.ndim != 3:
raise ValueError(
f"Unsupported depth array shape: {tuple(depth.shape)}"
)
return [depth[index] for index in range(depth.shape[0])]
def extract_depth_array(npz_path):
return extract_depth_arrays(npz_path)[0]
def extract_intrinsics_array(npz_path):
try:
import numpy as np
except ImportError as error:
raise RuntimeError("Blender Python does not provide NumPy") from error
try:
with np.load(npz_path, allow_pickle=False) as archive:
intrinsics_key = next(
(
key
for key in INTRINSICS_KEY_CANDIDATES
if key in archive.files
),
None,
)
if intrinsics_key is None:
keys = ", ".join(archive.files) or "none"
raise ValueError(
"Mini NPZ has no camera intrinsics; "
f"available keys: {keys}"
)
intrinsics = np.asarray(
archive[intrinsics_key],
dtype=np.float32,
)
except OSError as error:
raise ValueError(f"Unable to read Mini NPZ: {npz_path}") from error
if intrinsics.ndim == 3:
intrinsics = intrinsics[0]
if intrinsics.shape != (3, 3):
raise ValueError(
f"Unsupported intrinsics shape: {tuple(intrinsics.shape)}"
)
return intrinsics
def prepare_depth_values(depth):
import numpy as np
valid = np.isfinite(depth)
if not valid.any():
raise ValueError("Depth array contains no finite values")
values = depth.astype(np.float32, copy=True)
values[~valid] = 0.0
return values
def depth_displacement_values(depth):
"""Convert raw DA3 camera distance to positive outward displacement."""
import numpy as np
return 1.0 - np.asarray(depth, dtype=np.float32)
def camera_plane_bounds(
source_object,
depth,
intrinsics,
displacement_strength,
reference_depth=None,
):
import numpy as np
scale = abs(float(displacement_strength))
if scale <= 1e-8:
raise ValueError("Displacement Strength must not be zero")
if reference_depth is None:
valid = np.isfinite(depth) & (depth > 0.0)
if not valid.any():
raise ValueError("Depth array contains no positive finite values")
reference_depth = float(np.median(depth[valid]))
reference_distance = reference_depth * scale
height, width = depth.shape
fx = float(intrinsics[0, 0])
fy = float(intrinsics[1, 1])
cx = float(intrinsics[0, 2])
cy = float(intrinsics[1, 2])
if fx <= 0.0 or fy <= 0.0:
raise ValueError("Camera intrinsics contain an invalid focal length")
source_width, source_height = source_object.data.size
if source_width <= 0 or source_height <= 0:
raise ValueError("The source image has no readable dimensions")
source_aspect = source_width / source_height
# Predicted fx/fy may differ even for square pixels. Preserve their projected
# area while enforcing the source image aspect ratio to avoid image stretch.
raw_width = width * reference_distance / fx
raw_height = height * reference_distance / fy
plane_area = raw_width * raw_height
plane_width = float(np.sqrt(plane_area * source_aspect))
plane_height = float(np.sqrt(plane_area / source_aspect))
x_min = -(cx / width) * plane_width
x_max = (1.0 - cx / width) * plane_width
y_min = -(1.0 - cy / height) * plane_height
y_max = (cy / height) * plane_height
# Preserve an intentional Image Empty origin offset around the camera frame.
offset = source_object.empty_image_offset
x_offset = (float(offset[0]) + 0.5) * plane_width
y_offset = (float(offset[1]) + 0.5) * plane_height
return (
(
x_min + x_offset,
x_max + x_offset,
y_min + y_offset,
y_max + y_offset,
),
reference_depth,
)
def create_depth_image(name, depth_values):
import numpy as np
height, width = depth_values.shape
image = bpy.data.images.new(
name,
width=width,
height=height,
alpha=False,
float_buffer=True,
)
image.colorspace_settings.name = "Non-Color"
image.file_format = "OPEN_EXR"
# NPZ arrays use a top-left origin, while Blender image pixels start below.
depth_pixels = np.flipud(depth_values)
rgba = np.empty((height, width, 4), dtype=np.float32)
rgba[..., 0] = depth_pixels
rgba[..., 1] = depth_pixels
rgba[..., 2] = depth_pixels
rgba[..., 3] = 1.0
image.pixels.foreach_set(rgba.ravel())
# Pack as EXR so values outside 0..1 survive saving and reopening the blend.
image.pack()
return image