# Classical CV blocks

Category dir: `inference/core/workflows/core_steps/classical_cv/**`. See the universal invariants section in `SKILL.md` first.

**When to reach for it.** Non-model, OpenCV/NumPy-based image operations: preprocessing (grayscale, blur, threshold, morphology, contrast), classical detectors/matchers (SIFT, template matching, contours), and image-level measurements/analytics (dominant color, pixel color count, camera focus, size/distance measurement). Reach here when you need a deterministic, dependency-light transform or a cheap heuristic rather than a learned model. Note: QR/barcode detection live under `core_steps/models/third_party/**`, not here.

**Mental model.** Every block is the standard `WorkflowBlockManifest` + `WorkflowBlock` pair — nothing category-specific in the base classes. They are **non-batch, single-image-in / N-things-out, and stateless**: no block in this dir overrides `accepts_batch_input`, declares `WorkflowVideoMetadata`, or offsets dimensionality. `run` receives one `WorkflowImageData`, reads `image.numpy_image` (BGR), and returns a dict keyed by output name. Two output archetypes:
- **Image-producing** (grayscale, contours, sift, camera_focus): emit a new image via `WorkflowImageData.copy_and_replace(origin_image_data=image, numpy_image=...)` under the shared `OUTPUT_IMAGE_KEY` (imported from `visualizations/common/base.py`), preserving parent metadata/coordinates.
- **Data-producing** (dominant_color, pixel_color_count, size_measurement, sift_comparison): emit scalars/lists/detections under custom output keys, often typed with **classical-CV-specific kinds** (`RGB_COLOR_KIND`, `CONTOURS_KIND`, `IMAGE_KEYPOINTS_KIND`, `NUMPY_ARRAY_KIND`, `LIST_OF_VALUES_KIND`).

All carry the `json_schema_extra` block-type `"classical_computer_vision"` (except size/distance measurement which are tagged `"transformation"`) and `ui_manifest.section: "classical_cv"` with `opencv: True`. All pin `get_execution_engine_compatibility` to `>=1.3.0,<2.0.0`.

**Nuances an author MUST know.**
- **Custom data kinds need serializers to survive REMOTE / API boundaries.** In `loader.py`, `RGB_COLOR_KIND` has a deserializer registered (`deserialize_rgb_color_kind` under `KINDS_DESERIALIZERS`). But `CONTOURS_KIND`, `IMAGE_KEYPOINTS_KIND`, and the `NUMPY_ARRAY_KIND` SIFT descriptors have **no dedicated `KINDS_SERIALIZERS`/`KINDS_DESERIALIZERS` entry** — they fall through to `serialize_wildcard_kind`. If you invent a new classical-CV data kind, register a serializer/deserializer pair in `loader.py` or it won't cleanly cross the JSON boundary. See `contours/v1.py` (`hierarchy` is a raw cv2 `np.ndarray`) and `sift/v1.py` (`keypoints` are hand-serialized to dicts precisely because `cv2.KeyPoint` isn't JSON-safe).
- **`sv.Detections` you build by hand must be fully populated.** `template_matching/v1.py` shows the contract: after constructing `sv.Detections(xyxy=..., confidence=..., class_id=..., data={CLASS_NAME_DATA_FIELD: ...})`, you must attach `PARENT_ID_KEY`, `PREDICTION_TYPE_KEY`, `DETECTION_ID_KEY`, `IMAGE_DIMENSIONS_KEY`, then call `attach_parents_coordinates_to_sv_detections(...)` — downstream blocks (crop, visualize, filter) depend on this metadata density. Return `sv.Detections.empty()` on zero matches, never `None`.
- **Coordinate systems & color order are on you.** OpenCV is BGR; RGB-facing blocks reverse channels explicitly (`convert_color_to_bgr_tuple` in `pixel_color_count/v1.py` does `color[::-1]`; `dominant_color/v1.py` returns `reversed(dominant_color)`). Detectors that "work on grayscale" convert internally (`cv2.cvtColor(..., COLOR_BGR2GRAY)`) rather than requiring a grayscale input.
- **Output-key contract must match `describe_outputs` exactly.** Use the module-level `OUTPUT_KEY`/`OUTPUT_IMAGE_KEY` constants consistently across `describe_outputs` and the `run` return dict (see `OUTPUT_KEY = "dimensions"` in `size_measurement/v1.py`).
- **Polymorphic inputs are common.** `sift_comparison/v2.py` accepts either `IMAGE_KIND` or `NUMPY_ARRAY_KIND` per input (`isinstance(input_1, WorkflowImageData)` branch) and returns a full output dict even on the early-exit path (<2 descriptors) — every declared output key must appear in **every** return branch or the engine complains.
- **Measurement blocks consume predictions, not images.** `size_measurement`/`distance_measurement` take `OBJECT_DETECTION_PREDICTION_KIND`/`INSTANCE_SEGMENTATION_PREDICTION_KIND` (plus `LIST_OF_VALUES_KIND` polygons) and prefer mask contours over bbox when a mask is present (`get_detection_dimensions`), so they sit downstream of models — reference-object scaling, not raw pixels.

**Example blocks to pattern-match (simplest → representative).**
- `classical_cv/convert_grayscale/v1.py` — the minimal image-in/image-out template: read `numpy_image`, one `cv2` call, `copy_and_replace`, return under `OUTPUT_IMAGE_KEY`. Read this first for the skeleton.
- `classical_cv/pixel_color_count/v1.py` — simplest data-out block; `RGB_COLOR_KIND` input parsing (hex/tuple/string) and RGB↔BGR conversion.
- `classical_cv/contours/v1.py` — emitting a **new data kind** (`CONTOURS_KIND`) alongside an image, plus a raw-`np.ndarray` `hierarchy` output that rides the wildcard serializer.
- `classical_cv/template_matching/v1.py` — the canonical "hand-build `sv.Detections`" block: full metadata attachment, NMS, `attach_parents_coordinates_to_sv_detections`, empty-case handling. Read this before producing any detections from classical CV.
- `classical_cv/sift/v1.py` + `classical_cv/sift_comparison/v2.py` — the paired feature-extraction / feature-consumption idiom: `sift` emits `IMAGE_KEYPOINTS_KIND` (dict-serialized) + `NUMPY_ARRAY_KIND` descriptors; `sift_comparison@v2` shows polymorphic image-or-descriptor inputs and all-branches-return outputs.
- `classical_cv/camera_focus/v2.py` — most elaborate: image + optional detections in, per-bbox `LIST_OF_VALUES_KIND` out, and an identity-preserving optimization (`if result_image is image.numpy_image: output = image`) to avoid a needless copy. The "optional detections drive per-region output" pattern shared with `size_measurement`.
