Skip to content

Snapshot and observation

See the observation guide for view conventions, render-pass semantics, installation, offline assets, and pixel picking.

Capture one deterministic WebGL raster and optional raw pass array.

A Figure camera is used when neither view nor camera is supplied; otherwise named presets frame the compiled scene. Explicit config, resolution, and background override Figure intent.

Parameters:

Name Type Description Default
root Layer | Figure

Layer or Figure to capture.

required
view ViewPreset | None

Named camera preset and result label.

None
pass_name PassName

Beauty, albedo, depth, normal, element-ID, or layer-ID pass.

'beauty'
resolution tuple[int, int] | None

Output width and height in pixels.

None
camera CameraState | None

Explicit camera overriding the named preset.

None
config Config | None

Explicit invocation configuration.

None
backend BackendName

Capture backend; currently only webgl is supported.

'webgl'
background Literal['light', 'dark'] | None

Light or dark studio background.

None
up_axis Literal['y', 'z']

Coordinate convention used by named camera presets.

'y'
filename str | Path | None

Optional PNG path; raw pass data is saved beside it as NPY.

None
timeout float

Chromium capture timeout in seconds.

60.0

Returns:

Type Description
Snapshot

Snapshot containing the image, raw data, camera, matrices, and metadata.

Source code in hakowan/workflow/observation.py
def snapshot(
    root: Layer | Figure,
    *,
    view: ViewPreset | None = None,
    pass_name: PassName = "beauty",
    resolution: tuple[int, int] | None = None,
    camera: CameraState | None = None,
    config: Config | None = None,
    backend: BackendName = "webgl",
    background: Literal["light", "dark"] | None = None,
    up_axis: Literal["y", "z"] = "y",
    filename: str | Path | None = None,
    timeout: float = 60.0,
) -> Snapshot:
    """Capture one deterministic WebGL raster and optional raw pass array.

    A Figure camera is used when neither ``view`` nor ``camera`` is supplied;
    otherwise named presets frame the compiled scene. Explicit ``config``,
    ``resolution``, and ``background`` override Figure intent.

    Args:
        root: Layer or Figure to capture.
        view: Named camera preset and result label.
        pass_name: Beauty, albedo, depth, normal, element-ID, or layer-ID pass.
        resolution: Output width and height in pixels.
        camera: Explicit camera overriding the named preset.
        config: Explicit invocation configuration.
        backend: Capture backend; currently only ``webgl`` is supported.
        background: Light or dark studio background.
        up_axis: Coordinate convention used by named camera presets.
        filename: Optional PNG path; raw pass data is saved beside it as NPY.
        timeout: Chromium capture timeout in seconds.

    Returns:
        Snapshot containing the image, raw data, camera, matrices, and metadata.

    """
    if backend != "webgl":
        raise NotImplementedError("snapshot() currently supports backend='webgl'.")
    figure = root if isinstance(root, Figure) else None
    runtime_layer, resolved_config, resolution, background, explicit_config = (
        _capture_context(root, config, resolution, background)
    )
    label: str = view or "isometric"
    if camera is None and view is None:
        if (
            figure is not None
            and not explicit_config
            and figure.scene.camera is not None
        ):
            camera = _camera_state_from_figure(figure.scene.camera)
            label = "figure"
        elif explicit_config and resolved_config is not None:
            camera = _camera_state_from_config(resolved_config)
            label = "config"
    result = _capture(
        runtime_layer,
        [label],
        [pass_name],
        resolution,
        cameras={label: camera} if camera is not None else None,
        config=resolved_config,
        up_axis=up_axis,
        background=background,
        timeout=timeout,
    )
    item = result.snapshots[(label, pass_name)]
    if filename is not None:
        path = Path(filename)
        path.parent.mkdir(parents=True, exist_ok=True)
        item.image.save(path)
        if item.data is not None:
            data_path = path.with_suffix(".npy")
            np.save(data_path, item.data, allow_pickle=False)
            object.__setattr__(item, "data_path", data_path)
        object.__setattr__(item, "path", path)
    return item

Capture a deterministic multi-view, multi-pass inspection bundle.

Defaults to front, right, top, and isometric views with beauty, depth, and normal passes. Figure output passes are inherited when present. Request depth plus both ID passes to enable picking and structured visibility queries.

Parameters:

Name Type Description Default
root Layer | Figure

Layer or Figure to inspect.

required
views Sequence[str] | None

Ordered named view presets or labels with explicit camera overrides.

None
passes Sequence[PassName] | None

Ordered semantic pass names.

None
resolution tuple[int, int] | None

Width and height shared by every capture.

None
cameras dict[str, CameraState] | None

Explicit CameraState overrides keyed by view label.

None
config Config | None

Explicit invocation configuration.

None
backend BackendName

Capture backend; currently only webgl is supported.

'webgl'
background Literal['light', 'dark'] | None

Light or dark studio background.

None
up_axis Literal['y', 'z']

Coordinate convention used by named camera presets.

'y'
output_dir str | Path | None

Optional directory for PNG, NPY, contact-sheet, and manifest files.

None
timeout float

Chromium capture timeout in seconds.

60.0

Returns:

Type Description
Observation

Observation containing snapshots, structured scene evidence, and manifest.

Source code in hakowan/workflow/observation.py
def observe(
    root: Layer | Figure,
    *,
    views: Sequence[str] | None = None,
    passes: Sequence[PassName] | None = None,
    resolution: tuple[int, int] | None = None,
    cameras: dict[str, CameraState] | None = None,
    config: Config | None = None,
    backend: BackendName = "webgl",
    background: Literal["light", "dark"] | None = None,
    up_axis: Literal["y", "z"] = "y",
    output_dir: str | Path | None = None,
    timeout: float = 60.0,
) -> Observation:
    """Capture a deterministic multi-view, multi-pass inspection bundle.

    Defaults to front, right, top, and isometric views with beauty, depth, and
    normal passes. Figure output passes are inherited when present. Request
    depth plus both ID passes to enable picking and structured visibility queries.

    Args:
        root: Layer or Figure to inspect.
        views: Ordered named view presets or labels with explicit camera overrides.
        passes: Ordered semantic pass names.
        resolution: Width and height shared by every capture.
        cameras: Explicit CameraState overrides keyed by view label.
        config: Explicit invocation configuration.
        backend: Capture backend; currently only ``webgl`` is supported.
        background: Light or dark studio background.
        up_axis: Coordinate convention used by named camera presets.
        output_dir: Optional directory for PNG, NPY, contact-sheet, and manifest files.
        timeout: Chromium capture timeout in seconds.

    Returns:
        Observation containing snapshots, structured scene evidence, and manifest.

    """
    if backend != "webgl":
        raise NotImplementedError("observe() currently supports backend='webgl'.")
    figure = root if isinstance(root, Figure) else None
    runtime_layer, resolved_config, resolution, background, explicit_config = (
        _capture_context(root, config, resolution, background)
    )
    if views is None:
        if (
            figure is not None
            and not explicit_config
            and figure.scene.camera is not None
        ):
            views = ("figure",)
            cameras = {
                **(cameras or {}),
                "figure": _camera_state_from_figure(figure.scene.camera),
            }
        else:
            views = ("front", "right", "top", "isometric")
    if passes is None:
        if (
            figure is not None
            and not explicit_config
            and figure.scene.output is not None
        ):
            passes = tuple(
                "element_id" if item == "facet_id" else item
                for item in figure.scene.output.passes
            )
        else:
            passes = ("beauty", "depth", "normal")
    assert views is not None
    assert passes is not None
    result = _capture(
        runtime_layer,
        views,
        passes,
        resolution,
        cameras=cameras,
        config=resolved_config,
        background=background,
        up_axis=up_axis,
        timeout=timeout,
    )
    contact_sheet = _contact_sheet(result.snapshots, views, passes)
    manifest = {
        "version": "1.0",
        "backend": "webgl",
        "scene": result.scene_summary.to_dict(),
        "legends": [legend.to_dict() for legend in result.scene.legends],
        "annotations": [
            annotation.to_dict() for annotation in result.scene.annotations
        ],
        "snapshots": [
            result.snapshots[(view, pass_name)].to_manifest()
            for view in views
            for pass_name in passes
        ],
        "diagnostics": [item.to_dict() for item in result.diagnostics],
    }
    observation = Observation(
        snapshots=result.snapshots,
        scene_summary=result.scene_summary,
        diagnostics=result.diagnostics,
        contact_sheet=contact_sheet,
        manifest=manifest,
        _scene=result.scene,
    )
    manifest.update(_query_manifest(observation))
    if output_dir is not None:
        observation.save(output_dir)
    return observation

Resolved deterministic camera used for one or more snapshots.

scale is the full vertical extent for orthographic cameras and is None for perspective cameras.

Source code in hakowan/workflow/observation.py
@dataclass(frozen=True, slots=True)
class CameraState:
    """Resolved deterministic camera used for one or more snapshots.

    ``scale`` is the full vertical extent for orthographic cameras and is
    ``None`` for perspective cameras.
    """

    eye: tuple[float, float, float]
    target: tuple[float, float, float]
    up: tuple[float, float, float]
    fov: float = 35.0
    near: float = 0.01
    far: float = 100.0
    mode: ProjectionMode = "perspective"
    scale: float | None = None

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe camera-state mapping."""
        return asdict(self)

to_dict()

Return a JSON-safe camera-state mapping.

Source code in hakowan/workflow/observation.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe camera-state mapping."""
    return asdict(self)

One deterministic camera/pass raster and its machine-readable state.

Source code in hakowan/workflow/observation.py
@dataclass(frozen=True, slots=True)
class Snapshot:
    """One deterministic camera/pass raster and its machine-readable state."""

    image: Image.Image = field(repr=False, compare=False)
    data: npt.NDArray | None = field(default=None, repr=False, compare=False)
    view: str = "custom"
    pass_name: PassName = "beauty"
    camera: CameraState = field(
        default_factory=lambda: CameraState(
            eye=(0.0, 0.0, 5.0),
            target=(0.0, 0.0, 0.0),
            up=(0.0, 1.0, 0.0),
        )
    )
    world_to_camera: npt.NDArray[np.float64] = field(
        default_factory=lambda: np.eye(4), repr=False, compare=False
    )
    projection: npt.NDArray[np.float64] = field(
        default_factory=lambda: np.eye(4), repr=False, compare=False
    )
    bounds: npt.NDArray[np.float64] = field(
        default_factory=lambda: np.zeros((2, 3)), repr=False, compare=False
    )
    depth_range: tuple[float, float] | None = None
    diagnostics: tuple[Diagnostic, ...] = ()
    path: Path | None = None
    data_path: Path | None = None

    def to_manifest(self) -> dict[str, Any]:
        """Return paths, hashes, camera state, matrices, bounds, and diagnostics."""
        return {
            "view": self.view,
            "pass": self.pass_name,
            "path": str(self.path) if self.path is not None else None,
            "mime_type": "image/png",
            "sha256": _file_sha256(self.path),
            "data_path": str(self.data_path) if self.data_path is not None else None,
            "data_mime_type": "application/x-npy"
            if self.data_path is not None
            else None,
            "data_sha256": _file_sha256(self.data_path),
            "size": list(self.image.size),
            "camera": self.camera.to_dict(),
            "world_to_camera": self.world_to_camera.tolist(),
            "projection": self.projection.tolist(),
            "bounds": self.bounds.tolist(),
            "depth_range": list(self.depth_range) if self.depth_range else None,
            "data_dtype": str(self.data.dtype) if self.data is not None else None,
            "background_id": int(BACKGROUND_ID)
            if self.pass_name in {"element_id", "layer_id"}
            else None,
            "diagnostics": [item.to_dict() for item in self.diagnostics],
        }

to_manifest()

Return paths, hashes, camera state, matrices, bounds, and diagnostics.

Source code in hakowan/workflow/observation.py
def to_manifest(self) -> dict[str, Any]:
    """Return paths, hashes, camera state, matrices, bounds, and diagnostics."""
    return {
        "view": self.view,
        "pass": self.pass_name,
        "path": str(self.path) if self.path is not None else None,
        "mime_type": "image/png",
        "sha256": _file_sha256(self.path),
        "data_path": str(self.data_path) if self.data_path is not None else None,
        "data_mime_type": "application/x-npy"
        if self.data_path is not None
        else None,
        "data_sha256": _file_sha256(self.data_path),
        "size": list(self.image.size),
        "camera": self.camera.to_dict(),
        "world_to_camera": self.world_to_camera.tolist(),
        "projection": self.projection.tolist(),
        "bounds": self.bounds.tolist(),
        "depth_range": list(self.depth_range) if self.depth_range else None,
        "data_dtype": str(self.data.dtype) if self.data is not None else None,
        "background_id": int(BACKGROUND_ID)
        if self.pass_name in {"element_id", "layer_id"}
        else None,
        "diagnostics": [item.to_dict() for item in self.diagnostics],
    }

Multi-view, multi-pass evidence bundle for visual examination.

Source code in hakowan/workflow/observation.py
@dataclass(slots=True)
class Observation:
    """Multi-view, multi-pass evidence bundle for visual examination."""

    snapshots: dict[tuple[str, str], Snapshot]
    scene_summary: SceneSummary
    diagnostics: tuple[Diagnostic, ...] = ()
    contact_sheet: Image.Image | None = field(default=None, repr=False)
    manifest: dict[str, Any] = field(default_factory=dict)
    _scene: Scene | None = field(default=None, repr=False)

    def snapshot(self, view: str, pass_name: str) -> Snapshot:
        """Return the snapshot captured for ``view`` and ``pass_name``."""
        return self.snapshots[(view, pass_name)]

    def save(self, directory: str | Path) -> None:
        """Write PNGs, raw NumPy pass arrays, a contact sheet, and manifest."""
        output = Path(directory)
        output.mkdir(parents=True, exist_ok=True)
        for (view, pass_name), item in self.snapshots.items():
            _validate_artifact_label(view, "view")
            _validate_artifact_label(pass_name, "pass")
            path = output / f"{view}_{pass_name}.png"
            item.image.save(path)
            object.__setattr__(item, "path", path)
            if item.data is not None:
                data_path = output / f"{view}_{pass_name}.npy"
                np.save(data_path, item.data, allow_pickle=False)
                object.__setattr__(item, "data_path", data_path)
        if self.contact_sheet is not None:
            self.contact_sheet.save(output / "contact_sheet.png")
        self.manifest["snapshots"] = [
            self.snapshots[key].to_manifest() for key in self.snapshots
        ]
        contact_sheet_path = output / "contact_sheet.png"
        self.manifest["contact_sheet"] = (
            str(contact_sheet_path) if self.contact_sheet is not None else None
        )
        self.manifest["contact_sheet_metadata"] = (
            {
                "path": str(contact_sheet_path),
                "mime_type": "image/png",
                "size": list(self.contact_sheet.size),
                "sha256": _file_sha256(contact_sheet_path),
            }
            if self.contact_sheet is not None
            else None
        )
        (output / "manifest.json").write_text(
            json.dumps(self.manifest, indent=2, sort_keys=True) + "\n",
            encoding="utf-8",
        )

    def pick(self, view: str, pixel: tuple[int, int]) -> PixelHit | None:
        """Resolve IDs, depth, world position, normal, and attributes at a pixel.

        The observation must contain depth, element-ID, and layer-ID passes for
        the requested view. Background pixels return ``None``.
        """
        x, y = pixel
        depth_snapshot = self.snapshots.get((view, "depth"))
        element_snapshot = self.snapshots.get((view, "element_id"))
        layer_snapshot = self.snapshots.get((view, "layer_id"))
        if depth_snapshot is None or element_snapshot is None or layer_snapshot is None:
            raise ObservationError(
                "pick() requires depth, element_id, and layer_id passes for the view."
            )
        width, height = depth_snapshot.image.size
        if not (0 <= x < width and 0 <= y < height):
            raise IndexError(f"Pixel {pixel} is outside {width}x{height}.")
        assert depth_snapshot.data is not None
        assert element_snapshot.data is not None
        assert layer_snapshot.data is not None
        depth = float(depth_snapshot.data[y, x])
        element_id = int(element_snapshot.data[y, x])
        layer_id = int(layer_snapshot.data[y, x])
        if (
            not np.isfinite(depth)
            or element_id == int(BACKGROUND_ID)
            or layer_id == int(BACKGROUND_ID)
        ):
            return None
        position = _unproject_pixel(
            pixel,
            (width, height),
            depth,
            depth_snapshot.world_to_camera,
            depth_snapshot.projection,
            mode=depth_snapshot.camera.mode,
        )
        normal = _normal_at(self.snapshots.get((view, "normal")), pixel)
        attributes = _attributes_at(self._scene, layer_id, element_id)
        return PixelHit(
            view=view,
            pixel=pixel,
            layer_id=layer_id,
            element_id=element_id,
            depth=depth,
            world_position=(float(position[0]), float(position[1]), float(position[2])),
            normal=normal,
            attributes=attributes,
        )

    def region(
        self,
        x0: int,
        y0: int,
        x1: int,
        y1: int,
        *,
        view: str | None = None,
    ) -> RegionSummary:
        """Summarize IDs and coverage inside half-open pixel bounds."""
        return _region(self, x0, y0, x1, y1, view=view)

    def visible_elements(
        self, layer: str | int | None = None, *, view: str | None = None
    ) -> tuple[LayerVisibility, ...]:
        """Return visibility records for all layers or one ID/name selector."""
        return _visible_elements(self, layer, view=view)

    def attribute_extrema(
        self,
        attribute: str,
        *,
        layer: str | int | None = None,
        view: str | None = None,
        bounds: tuple[int, int, int, int] | None = None,
    ) -> tuple[AttributeVisibility, ...]:
        """Summarize a numeric attribute over visible source elements.

        ``bounds`` optionally restricts the query to a half-open pixel region.
        Vector extrema are selected by magnitude while component-wise statistics
        and the original extremum samples are retained.
        """
        return _attribute_extrema(
            self, attribute, layer=layer, view=view, bounds=bounds
        )

    def occlusion_report(
        self, *, view: str | None = None, layer: str | int | None = None
    ) -> tuple[OcclusionRecord, ...]:
        """Report depth-ordered projected overlap for selected views or layers."""
        return _occlusion_report(self, view=view, layer=layer)

    def visual_evidence(self) -> dict[str, Any]:
        """Return compact framing, visibility, depth, and contrast evidence."""
        return _visual_evidence(self)

    def visual_diagnostics(
        self,
        *,
        min_occupancy: float = 0.02,
        max_occupancy: float = 0.95,
        max_clipped_fraction: float = 0.05,
        min_contrast: float = 0.08,
    ) -> tuple[Diagnostic, ...]:
        """Diagnose deterministic visual failures against explicit thresholds."""
        return _visual_diagnostics(
            self,
            min_occupancy=min_occupancy,
            max_occupancy=max_occupancy,
            max_clipped_fraction=max_clipped_fraction,
            min_contrast=min_contrast,
        )

attribute_extrema(attribute, *, layer=None, view=None, bounds=None)

Summarize a numeric attribute over visible source elements.

bounds optionally restricts the query to a half-open pixel region. Vector extrema are selected by magnitude while component-wise statistics and the original extremum samples are retained.

Source code in hakowan/workflow/observation.py
def attribute_extrema(
    self,
    attribute: str,
    *,
    layer: str | int | None = None,
    view: str | None = None,
    bounds: tuple[int, int, int, int] | None = None,
) -> tuple[AttributeVisibility, ...]:
    """Summarize a numeric attribute over visible source elements.

    ``bounds`` optionally restricts the query to a half-open pixel region.
    Vector extrema are selected by magnitude while component-wise statistics
    and the original extremum samples are retained.
    """
    return _attribute_extrema(
        self, attribute, layer=layer, view=view, bounds=bounds
    )

occlusion_report(*, view=None, layer=None)

Report depth-ordered projected overlap for selected views or layers.

Source code in hakowan/workflow/observation.py
def occlusion_report(
    self, *, view: str | None = None, layer: str | int | None = None
) -> tuple[OcclusionRecord, ...]:
    """Report depth-ordered projected overlap for selected views or layers."""
    return _occlusion_report(self, view=view, layer=layer)

pick(view, pixel)

Resolve IDs, depth, world position, normal, and attributes at a pixel.

The observation must contain depth, element-ID, and layer-ID passes for the requested view. Background pixels return None.

Source code in hakowan/workflow/observation.py
def pick(self, view: str, pixel: tuple[int, int]) -> PixelHit | None:
    """Resolve IDs, depth, world position, normal, and attributes at a pixel.

    The observation must contain depth, element-ID, and layer-ID passes for
    the requested view. Background pixels return ``None``.
    """
    x, y = pixel
    depth_snapshot = self.snapshots.get((view, "depth"))
    element_snapshot = self.snapshots.get((view, "element_id"))
    layer_snapshot = self.snapshots.get((view, "layer_id"))
    if depth_snapshot is None or element_snapshot is None or layer_snapshot is None:
        raise ObservationError(
            "pick() requires depth, element_id, and layer_id passes for the view."
        )
    width, height = depth_snapshot.image.size
    if not (0 <= x < width and 0 <= y < height):
        raise IndexError(f"Pixel {pixel} is outside {width}x{height}.")
    assert depth_snapshot.data is not None
    assert element_snapshot.data is not None
    assert layer_snapshot.data is not None
    depth = float(depth_snapshot.data[y, x])
    element_id = int(element_snapshot.data[y, x])
    layer_id = int(layer_snapshot.data[y, x])
    if (
        not np.isfinite(depth)
        or element_id == int(BACKGROUND_ID)
        or layer_id == int(BACKGROUND_ID)
    ):
        return None
    position = _unproject_pixel(
        pixel,
        (width, height),
        depth,
        depth_snapshot.world_to_camera,
        depth_snapshot.projection,
        mode=depth_snapshot.camera.mode,
    )
    normal = _normal_at(self.snapshots.get((view, "normal")), pixel)
    attributes = _attributes_at(self._scene, layer_id, element_id)
    return PixelHit(
        view=view,
        pixel=pixel,
        layer_id=layer_id,
        element_id=element_id,
        depth=depth,
        world_position=(float(position[0]), float(position[1]), float(position[2])),
        normal=normal,
        attributes=attributes,
    )

region(x0, y0, x1, y1, *, view=None)

Summarize IDs and coverage inside half-open pixel bounds.

Source code in hakowan/workflow/observation.py
def region(
    self,
    x0: int,
    y0: int,
    x1: int,
    y1: int,
    *,
    view: str | None = None,
) -> RegionSummary:
    """Summarize IDs and coverage inside half-open pixel bounds."""
    return _region(self, x0, y0, x1, y1, view=view)

save(directory)

Write PNGs, raw NumPy pass arrays, a contact sheet, and manifest.

Source code in hakowan/workflow/observation.py
def save(self, directory: str | Path) -> None:
    """Write PNGs, raw NumPy pass arrays, a contact sheet, and manifest."""
    output = Path(directory)
    output.mkdir(parents=True, exist_ok=True)
    for (view, pass_name), item in self.snapshots.items():
        _validate_artifact_label(view, "view")
        _validate_artifact_label(pass_name, "pass")
        path = output / f"{view}_{pass_name}.png"
        item.image.save(path)
        object.__setattr__(item, "path", path)
        if item.data is not None:
            data_path = output / f"{view}_{pass_name}.npy"
            np.save(data_path, item.data, allow_pickle=False)
            object.__setattr__(item, "data_path", data_path)
    if self.contact_sheet is not None:
        self.contact_sheet.save(output / "contact_sheet.png")
    self.manifest["snapshots"] = [
        self.snapshots[key].to_manifest() for key in self.snapshots
    ]
    contact_sheet_path = output / "contact_sheet.png"
    self.manifest["contact_sheet"] = (
        str(contact_sheet_path) if self.contact_sheet is not None else None
    )
    self.manifest["contact_sheet_metadata"] = (
        {
            "path": str(contact_sheet_path),
            "mime_type": "image/png",
            "size": list(self.contact_sheet.size),
            "sha256": _file_sha256(contact_sheet_path),
        }
        if self.contact_sheet is not None
        else None
    )
    (output / "manifest.json").write_text(
        json.dumps(self.manifest, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )

snapshot(view, pass_name)

Return the snapshot captured for view and pass_name.

Source code in hakowan/workflow/observation.py
def snapshot(self, view: str, pass_name: str) -> Snapshot:
    """Return the snapshot captured for ``view`` and ``pass_name``."""
    return self.snapshots[(view, pass_name)]

visible_elements(layer=None, *, view=None)

Return visibility records for all layers or one ID/name selector.

Source code in hakowan/workflow/observation.py
def visible_elements(
    self, layer: str | int | None = None, *, view: str | None = None
) -> tuple[LayerVisibility, ...]:
    """Return visibility records for all layers or one ID/name selector."""
    return _visible_elements(self, layer, view=view)

visual_diagnostics(*, min_occupancy=0.02, max_occupancy=0.95, max_clipped_fraction=0.05, min_contrast=0.08)

Diagnose deterministic visual failures against explicit thresholds.

Source code in hakowan/workflow/observation.py
def visual_diagnostics(
    self,
    *,
    min_occupancy: float = 0.02,
    max_occupancy: float = 0.95,
    max_clipped_fraction: float = 0.05,
    min_contrast: float = 0.08,
) -> tuple[Diagnostic, ...]:
    """Diagnose deterministic visual failures against explicit thresholds."""
    return _visual_diagnostics(
        self,
        min_occupancy=min_occupancy,
        max_occupancy=max_occupancy,
        max_clipped_fraction=max_clipped_fraction,
        min_contrast=min_contrast,
    )

visual_evidence()

Return compact framing, visibility, depth, and contrast evidence.

Source code in hakowan/workflow/observation.py
def visual_evidence(self) -> dict[str, Any]:
    """Return compact framing, visibility, depth, and contrast evidence."""
    return _visual_evidence(self)

Geometry and data resolved at one observation pixel.

Source code in hakowan/workflow/observation.py
@dataclass(frozen=True, slots=True)
class PixelHit:
    """Geometry and data resolved at one observation pixel."""

    view: str
    pixel: tuple[int, int]
    layer_id: int
    element_id: int
    depth: float
    world_position: tuple[float, float, float]
    normal: tuple[float, float, float] | None
    attributes: dict[str, Any]

Normalized scene bounds, coordinate convention, and layer summaries.

Source code in hakowan/workflow/observation.py
@dataclass(frozen=True, slots=True)
class SceneSummary:
    """Normalized scene bounds, coordinate convention, and layer summaries."""

    bounds: tuple[tuple[float, float, float], tuple[float, float, float]]
    center: tuple[float, float, float]
    radius: float
    up_axis: Literal["y", "z"]
    layers: tuple[LayerSummary, ...]

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe scene summary."""
        return {
            "bounds": [list(self.bounds[0]), list(self.bounds[1])],
            "center": list(self.center),
            "radius": self.radius,
            "up_axis": self.up_axis,
            "layers": [layer.to_dict() for layer in self.layers],
        }

to_dict()

Return a JSON-safe scene summary.

Source code in hakowan/workflow/observation.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe scene summary."""
    return {
        "bounds": [list(self.bounds[0]), list(self.bounds[1])],
        "center": list(self.center),
        "radius": self.radius,
        "up_axis": self.up_axis,
        "layers": [layer.to_dict() for layer in self.layers],
    }

Geometry counts and identity for one compiled observation layer.

Source code in hakowan/workflow/observation.py
@dataclass(frozen=True, slots=True)
class LayerSummary:
    """Geometry counts and identity for one compiled observation layer."""

    id: int
    name: str
    mark: str
    vertex_count: int
    facet_count: int

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe layer summary."""
        return asdict(self)

to_dict()

Return a JSON-safe layer summary.

Source code in hakowan/workflow/observation.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe layer summary."""
    return asdict(self)

Layer and background coverage within a half-open pixel rectangle.

Source code in hakowan/workflow/observation_queries.py
@dataclass(frozen=True, slots=True)
class RegionSummary:
    """Layer and background coverage within a half-open pixel rectangle."""

    view: str
    bounds: tuple[int, int, int, int]
    pixel_count: int
    background_pixel_count: int
    background_fraction: float
    layers: tuple[LayerVisibility, ...]

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe region summary with nested layer records."""
        return {
            **asdict(self),
            "layers": [layer.to_dict() for layer in self.layers],
        }

to_dict()

Return a JSON-safe region summary with nested layer records.

Source code in hakowan/workflow/observation_queries.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe region summary with nested layer records."""
    return {
        **asdict(self),
        "layers": [layer.to_dict() for layer in self.layers],
    }

Visible pixels and elements for one layer in one captured view.

Source code in hakowan/workflow/observation_queries.py
@dataclass(frozen=True, slots=True)
class LayerVisibility:
    """Visible pixels and elements for one layer in one captured view."""

    view: str
    layer_id: int
    name: str
    mark: str
    visible_pixel_count: int
    visible_element_ids: tuple[int, ...]
    total_element_count: int | None
    visible_element_fraction: float | None
    visible_bounds: tuple[int, int, int, int] | None
    projected_bounds: tuple[int, int, int, int] | None
    depth_range: tuple[float, float] | None

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe visibility record."""
        return asdict(self)

to_dict()

Return a JSON-safe visibility record.

Source code in hakowan/workflow/observation_queries.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe visibility record."""
    return asdict(self)

Statistics for visible source elements carrying one numeric attribute.

Source code in hakowan/workflow/observation_queries.py
@dataclass(frozen=True, slots=True)
class AttributeVisibility:
    """Statistics for visible source elements carrying one numeric attribute."""

    view: str
    layer_id: int
    layer_name: str
    attribute: str
    element: str
    channels: int
    sample_count: int
    criterion: str
    minimum: float | tuple[float, ...]
    maximum: float | tuple[float, ...]
    mean: float | tuple[float, ...]
    minimum_element_id: int
    maximum_element_id: int
    minimum_sample: float | tuple[float, ...]
    maximum_sample: float | tuple[float, ...]

    def to_dict(self) -> dict[str, Any]:
        """Return JSON-safe visible-attribute statistics."""
        return asdict(self)

to_dict()

Return JSON-safe visible-attribute statistics.

Source code in hakowan/workflow/observation_queries.py
def to_dict(self) -> dict[str, Any]:
    """Return JSON-safe visible-attribute statistics."""
    return asdict(self)

Projected overlap where one layer is entirely closer than another.

Source code in hakowan/workflow/observation_queries.py
@dataclass(frozen=True, slots=True)
class OcclusionRecord:
    """Projected overlap where one layer is entirely closer than another."""

    view: str
    occluded_layer_id: int
    occluded_layer_name: str
    occluder_layer_id: int
    occluder_layer_name: str
    projected_coverage: float
    fully_hidden: bool
    occluded_projected_bounds: tuple[int, int, int, int]
    occluder_visible_bounds: tuple[int, int, int, int]
    occluded_depth_range: tuple[float, float]
    occluder_depth_range: tuple[float, float]

    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-safe occlusion record."""
        return asdict(self)

to_dict()

Return a JSON-safe occlusion record.

Source code in hakowan/workflow/observation_queries.py
def to_dict(self) -> dict[str, Any]:
    """Return a JSON-safe occlusion record."""
    return asdict(self)

Bases: RuntimeError

Raised when deterministic observation capture cannot complete.

Source code in hakowan/workflow/observation.py
class ObservationError(RuntimeError):
    """Raised when deterministic observation capture cannot complete."""