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