Remove obsolete implementation and simplify project structure
This commit is contained in:
262
src/anyimage/operators/depth_plane/data.py
Normal file
262
src/anyimage/operators/depth_plane/data.py
Normal file
@@ -0,0 +1,262 @@
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user