Skip to content

Working with Depth Maps & Auxiliary Images

HEIF containers support embedding auxiliary image tracks linked to a master image. Typical examples include: - Portrait Depth Maps: Stored by Apple and Android cameras to simulate shallow depth of field (bokeh). - Alpha Transparency Mattes: Fine segmentation masks separating subjects from backgrounds. - Custom AI Masks: Salient object segmentations or thermal imaging channels.

This tutorial demonstrates how to read, visualize, process, and write auxiliary depth tracks.


1. Extracting and Visualizing Depth Maps

pylibheif provides a first-class DepthMap domain entity accessible directly on image handles via handle.depth_map. It encapsulates decoding, physical unit conversion (ISO/IEC 23008-12 / ITU-T H.265), and microsecond pseudocolor visualization without requiring OpenCV or Matplotlib.

import pylibheif
import numpy as np

ctx = pylibheif.HeifContext()
ctx.read_from_file("portrait_photo.heic")
master_handle = ctx.get_primary_image_handle()

# Check for depth map via domain property
if master_handle.depth_map is not None:
    depth = master_handle.depth_map
    print(f"Master size: {master_handle.width}x{master_handle.height}")
    print(f"Depth size:  {depth.info.width}x{depth.info.height}")
    print(f"Bit depth:   {depth.info.bit_depth} bits")
    print(f"Type:        {depth.representation_type.name}")

    # Inspect physical metric depth range (if calibrated metadata is present)
    if depth.info.has_metric_depth:
        print(f"Physical range: {depth.info.near_distance:.2f}m ~ {depth.info.far_distance:.2f}m")
        # Convert to real-world metric distance in meters (float32 2D ndarray)
        metric_depth = depth.to_metric_depth()
        print(f"Center pixel distance: {metric_depth[depth.info.height // 2, depth.info.width // 2]:.2f} meters")

    # 1. Decode to normalized float32 [0.0, 1.0] NumPy array (0=closest, 1=farthest)
    depth_array = depth.decode(normalize=True)

    # 2. Render directly to a PIL Image with high-contrast scientific pseudocolor
    # Supported colormaps: "turbo" (default), "inferno", "viridis", "grayscale"
    # Zero external dependencies: uses precomputed 768-byte palette LUT (<0.1ms)
    depth_pil = depth.to_pillow(colormap="turbo")
    depth_pil.save("extracted_depth_turbo.png")

Apple Portrait Matte Masks

For photos captured on iPhones with Apple portrait segmentation, access the alpha subject matte directly via handle.portrait_matte:

if master_handle.portrait_matte is not None:
    matte_image = master_handle.portrait_matte.decode()
    matte_pil = master_handle.portrait_matte.to_pillow(colormap="grayscale")
    matte_pil.save("portrait_matte.png")

2. Applying Synthetic Bokeh Blur Using Depth

Using OpenCV and the decoded depth map, you can dynamically apply Gaussian blur to background pixels based on their camera distance:

import cv2
import numpy as np
import pylibheif

# 1. Decode master image (RGB)
ctx = pylibheif.HeifContext()
ctx.read_from_file("portrait_photo.heic")
master = ctx.get_primary_image_handle()
color_img = master.decode(pylibheif.HeifColorspace.RGB, pylibheif.HeifChroma.InterleavedRGB24)
color_array = np.asarray(color_img.get_plane(pylibheif.HeifChannel.Interleaved))

# 2. Decode normalized depth map directly using DepthMap entity
# Returns float32 array in [0.0, 1.0] where 0.0=foreground, 1.0=background
depth_array = master.depth_map.decode(normalize=True)
depth_resized = cv2.resize(depth_array, (master.width, master.height))

# 3. Create depth-of-field blur mask
# Pixels further away (higher normalized values) receive full Gaussian blur
blurred_bg = cv2.GaussianBlur(color_array, (31, 31), 0)

# Reshape mask for 3-channel alpha blending
alpha_mask = depth_resized[:, :, np.newaxis]

# Alpha blend: in-focus foreground + blurred background
bokeh_result = (color_array * (1.0 - alpha_mask) + blurred_bg * alpha_mask).astype(np.uint8)

cv2.imwrite("portrait_bokeh.jpg", cv2.cvtColor(bokeh_result, cv2.COLOR_RGB2BGR))

3. Writing Auxiliary Images (assign_auxiliary_image)

pylibheif provides ctx.assign_auxiliary_image() to attach any custom encoded image (e.g. depth map, matte mask) as an auxiliary track to a master image:

import pylibheif

# Setup context & HEVC encoder
ctx = pylibheif.HeifContext()
encoder = pylibheif.HeifEncoder(pylibheif.HeifCompressionFormat.HEVC)

# 1. Encode primary master image (RGB 1920x1080)
master_img = pylibheif.HeifImage(pylibheif.HeifColorspace.RGB, pylibheif.HeifChroma.InterleavedRGB24, 1920, 1080)
master_img.add_plane(pylibheif.HeifChannel.Interleaved, 1920, 1080, 8)
# (Fill master_img with pixel data...)
master_handle = ctx.encode_image(master_img, encoder)

# 2. Encode auxiliary depth map (Monochrome 960x540)
depth_img = pylibheif.HeifImage(pylibheif.HeifColorspace.Monochrome, pylibheif.HeifChroma.Monochrome, 960, 540)
depth_img.add_plane(pylibheif.HeifChannel.Y, 960, 540, 8)
# (Fill depth_img with depth data...)
depth_handle = ctx.encode_image(depth_img, encoder)

# 3. Assign as auxiliary track
# The auxiliary image is automatically marked as hidden (infe.hidden_item=true)
# and linked via standard 'auxl' and 'auxC' reference boxes.
ctx.assign_auxiliary_image(
    master_image=master_handle,
    auxiliary_image=depth_handle,
    auxiliary_type="urn:mpeg:hevc:2015:auxid:1"
)

# 4. Save container
ctx.write_to_file("output_with_depth.heic")
print("Saved HEIF image with embedded auxiliary depth track!")