From 418fbe9a2fe278fe2cdce51f5ad0c940362af631 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 14:28:08 -0700 Subject: [PATCH 01/17] Let workflows request the anomaly map through run_classification ModelsProvider.run_classification gains include_anomaly_map; the server adapter sets it on ClassificationInferenceRequest only when asked, so the request a classification block builds is unchanged. Co-Authored-By: Claude Fable 5.1 --- .../interfaces/workflows_models_provider.py | 5 +++ .../test_workflows_models_provider.py | 35 +++++++++++++++++++ .../prototypes/models_provider.py | 1 + 3 files changed, 41 insertions(+) diff --git a/inference/core/interfaces/workflows_models_provider.py b/inference/core/interfaces/workflows_models_provider.py index c11abeaf1e..94d643178d 100644 --- a/inference/core/interfaces/workflows_models_provider.py +++ b/inference/core/interfaces/workflows_models_provider.py @@ -159,7 +159,11 @@ def run_classification( disable_active_learning: Optional[bool] = None, active_learning_target_dataset: Optional[str] = None, inference_kwargs: Optional[Dict[str, Any]] = None, + include_anomaly_map: bool = False, ) -> List[dict]: + # Set only when asked, so the request a classification block builds is + # field-for-field what it was before the anomaly block existed. + anomaly_arguments = {"include_anomaly_map": True} if include_anomaly_map else {} request = ClassificationInferenceRequest( api_key=api_key, model_id=model_id, @@ -168,6 +172,7 @@ def run_classification( disable_active_learning=disable_active_learning, source=_WORKFLOW_SOURCE, active_learning_target_dataset=active_learning_target_dataset, + **anomaly_arguments, ) return self._dump( self._infer(model_id=model_id, request=request, **(inference_kwargs or {})) diff --git a/tests/inference/unit_tests/core/interfaces/test_workflows_models_provider.py b/tests/inference/unit_tests/core/interfaces/test_workflows_models_provider.py index 2a24f2dd08..e80d400a32 100644 --- a/tests/inference/unit_tests/core/interfaces/test_workflows_models_provider.py +++ b/tests/inference/unit_tests/core/interfaces/test_workflows_models_provider.py @@ -426,6 +426,41 @@ def test_run_semantic_segmentation_still_rejects_an_explicit_none_confidence() - manager.infer_from_request_sync.assert_not_called() +def test_run_classification_requests_the_anomaly_map_when_asked() -> None: + """The anomaly detection block asks for the heatmap through the port; the + request field defaults to False so plain classification is unchanged.""" + from inference.core.entities.responses.inference import ( + ClassificationInferenceResponse, + ) + + manager = manager_returning( + ClassificationInferenceResponse( + image=InferenceResponseImage(width=10, height=20), + predictions=[], + top="normal", + confidence=0.9, + ) + ) + ModelManagerModelsProvider(manager).run_classification( + model_id="m/1", + images=IMAGES, + api_key="k", + confidence="default", + include_anomaly_map=True, + ) + request = captured_request(manager) + assert request.include_anomaly_map is True + assert "include_anomaly_map" in request.model_fields_set + + manager.infer_from_request_sync.reset_mock() + ModelManagerModelsProvider(manager).run_classification( + model_id="m/1", images=IMAGES, api_key="k", confidence="default" + ) + request = captured_request(manager) + assert request.include_anomaly_map is False + assert "include_anomaly_map" not in request.model_fields_set + + def test_run_classification_forwards_extra_inference_kwargs() -> None: """multi_label v2/v3 pass `confidence` to the model call as well as into the request; the port carries it as `inference_kwargs`.""" diff --git a/workflows/roboflow_workflows/prototypes/models_provider.py b/workflows/roboflow_workflows/prototypes/models_provider.py index 991eb33aec..3c66c5ff0a 100644 --- a/workflows/roboflow_workflows/prototypes/models_provider.py +++ b/workflows/roboflow_workflows/prototypes/models_provider.py @@ -105,6 +105,7 @@ def run_classification( disable_active_learning: Optional[bool] = None, active_learning_target_dataset: Optional[str] = None, inference_kwargs: Optional[Dict[str, Any]] = None, + include_anomaly_map: bool = False, ) -> List[dict]: ... def run_image_embeddings( From 9886d30efefc61807d177a5108d89cf3ee9101aa Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 14:28:09 -0700 Subject: [PATCH 02/17] Add the Anomaly Detection Model workflow block roboflow_core/anomaly_detection_model@v1 runs a PatchCore or FoundAD model and outputs predictions, anomaly_score, anomaly_threshold, is_anomalous and the float32 anomaly_map at network resolution. An anomaly_threshold input overrides the saved threshold. The tensor sibling carries the verdict in images_metadata and serialise_native_classification writes it through. Co-Authored-By: Claude Fable 5.1 --- .../core_steps/common/anomaly_detection.py | 181 ++++++ .../core_steps/common/serializers_tensor.py | 29 + .../roboflow/anomaly_detection/__init__.py | 0 .../models/roboflow/anomaly_detection/v1.py | 363 +++++++++++++ .../roboflow/anomaly_detection/v1_tensor.py | 514 ++++++++++++++++++ .../common/test_anomaly_detection.py | 118 ++++ .../roboflow/anomaly_detection/__init__.py | 0 .../test_hosted_api_resolution.py | 23 + .../roboflow/anomaly_detection/test_v1.py | 342 ++++++++++++ .../anomaly_detection/test_v1_tensor.py | 241 ++++++++ 10 files changed, 1811 insertions(+) create mode 100644 workflows/roboflow_workflows/core_steps/common/anomaly_detection.py create mode 100644 workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/__init__.py create mode 100644 workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py create mode 100644 workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py create mode 100644 workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py create mode 100644 workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/__init__.py create mode 100644 workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_hosted_api_resolution.py create mode 100644 workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py create mode 100644 workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py diff --git a/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py new file mode 100644 index 0000000000..bd49a6a737 --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py @@ -0,0 +1,181 @@ +"""Shared helpers for the anomaly detection (PatchCore, FoundAD) model blocks. + +The inference server answers an anomaly model through the classification route +with an ``AnomalyDetectionResponse``: the usual classification fields plus +``anomaly_score`` (raw, unbounded, larger means more anomalous), +``anomaly_threshold`` (the decision boundary saved with the model), +``is_anomalous`` and, on request, ``anomaly_map`` - the local evidence at the +network input resolution, serialised as ``{"shape": [h, w], "dtype": +"float32", "data": }``. + +``roboflow_workflows`` cannot import ``inference.core``, so the payload decoder +is duplicated here (it mirrors ``inference_sdk.http.utils.anomaly_maps``). +""" + +import base64 +import binascii +import math +from typing import Any, Dict, List, Mapping, Optional + +import numpy as np + +ANOMALY_SCORE_KEY = "anomaly_score" +ANOMALY_THRESHOLD_KEY = "anomaly_threshold" +IS_ANOMALOUS_KEY = "is_anomalous" +ANOMALY_MAP_KEY = "anomaly_map" +ANOMALY_METADATA_KEYS = (ANOMALY_SCORE_KEY, ANOMALY_THRESHOLD_KEY, IS_ANOMALOUS_KEY) + +ANOMALY_MAP_DTYPE = "float32" +_ANOMALY_MAP_NUMPY_DTYPE = np.dtype(" np.ndarray: + """Decode the ``anomaly_map`` payload of an anomaly detection response. + + Args: + payload (Mapping[str, Any]): Mapping with ``shape`` (``[height, width]``), + ``dtype`` (``"float32"``) and ``data`` (base64 of row-major + little-endian float32 bytes), as the server serialises it. + + Returns: + Writable float32 array of shape ``(height, width)`` at the network input + resolution; stretch it to the image size to overlay it. + + Raises: + ValueError: If the dtype is not float32, the shape is not two positive + ints, the data is not valid base64, or its length does not match + the shape. + """ + if payload.get("dtype") != ANOMALY_MAP_DTYPE: + raise ValueError( + f"Expected anomaly map dtype {ANOMALY_MAP_DTYPE!r}, got " + f"{payload.get('dtype')!r}" + ) + shape = payload.get("shape") + if ( + not isinstance(shape, (list, tuple)) + or len(shape) != 2 + or not all(isinstance(side, int) and side > 0 for side in shape) + ): + raise ValueError(f"Anomaly map shape must be two positive ints, got {shape!r}") + try: + data = base64.b64decode(payload.get("data", ""), validate=True) + except (binascii.Error, ValueError, TypeError) as error: + raise ValueError("Anomaly map payload is not valid base64") from error + expected_bytes = shape[0] * shape[1] * _ANOMALY_MAP_NUMPY_DTYPE.itemsize + if len(data) != expected_bytes: + raise ValueError( + f"Anomaly map payload holds {len(data)} bytes, expected {expected_bytes} " + f"for shape {tuple(shape)}" + ) + values = np.frombuffer(data, dtype=_ANOMALY_MAP_NUMPY_DTYPE).reshape(shape) + return values.astype(np.float32, copy=True) + + +def coerce_anomaly_map(value: Any) -> Optional[np.ndarray]: + """The map as a float32 ``(h, w)`` array, whatever form the response carried. + + Args: + value (Any): ``None`` (map not requested), the serialised payload + mapping (local execution dumps the response as is), an ``np.ndarray`` + (``inference_sdk`` decodes the payload on the remote path) or a + nested float list from an older server. + + Returns: + ``None`` when ``value`` is ``None``, else a float32 ``(h, w)`` array. + + Raises: + ValueError: If the value is not one of the forms above or is not 2D. + """ + if value is None: + return None + if isinstance(value, Mapping): + return decode_anomaly_map(value) + try: + array = np.asarray(value, dtype=np.float32) + except (TypeError, ValueError) as error: + raise ValueError( + f"Anomaly map must be a payload mapping, an array or a nested list, " + f"got {type(value).__name__}" + ) from error + if array.ndim != 2: + raise ValueError(f"Anomaly map must be 2D, got shape {tuple(array.shape)}") + return array + + +def require_anomaly_response(prediction: Dict[str, Any], model_id: str) -> None: + """Fail loudly when a classification response is not an anomaly response. + + Args: + prediction (Dict[str, Any]): The dumped classification response. + model_id (str): The model that produced it, for the error message. + + Raises: + ValueError: If ``anomaly_score``, ``anomaly_threshold`` or + ``is_anomalous`` is missing - the model is a plain classifier, not + a PatchCore or FoundAD anomaly model. + """ + missing = [key for key in ANOMALY_METADATA_KEYS if key not in prediction] + if missing: + raise ValueError( + f"Model {model_id!r} did not return an anomaly detection response " + f"(missing {missing}). The Anomaly Detection Model block needs a " + f"PatchCore or FoundAD model trained on an anomaly detection project; " + f"use the Single-Label Classification Model block for other classifiers." + ) + + +def validate_threshold_override(threshold: Optional[float]) -> Optional[float]: + """A finite float, or ``None`` to keep the threshold saved with the model. + + Raises: + ValueError: If ``threshold`` is not a finite number. + """ + if threshold is None: + return None + if isinstance(threshold, bool) or not isinstance(threshold, (int, float)): + raise ValueError( + f"anomaly_threshold must be a number, got {type(threshold).__name__}" + ) + value = float(threshold) + if not math.isfinite(value): + raise ValueError(f"anomaly_threshold must be finite, got {value}") + return value + + +def apply_threshold_override( + prediction: Dict[str, Any], threshold: Optional[float] +) -> Dict[str, Any]: + """Re-decide the verdict against a caller-supplied threshold, in place. + + ``is_anomalous`` becomes ``anomaly_score >= threshold`` and + ``anomaly_threshold`` reports the override. ``predictions`` is reordered so + the decided class comes first, and ``top`` / ``confidence`` follow it. The + per-class confidences are left as served: they are a sigmoid centred on the + threshold SAVED with the model, so under an override 0.5 no longer marks the + decision boundary. With ``threshold`` ``None`` the response is unchanged. + + Args: + prediction (Dict[str, Any]): A dumped anomaly detection response. + threshold (Optional[float]): The override, already validated. + + Returns: + The same dict. + """ + if threshold is None: + return prediction + is_anomalous = float(prediction[ANOMALY_SCORE_KEY]) >= threshold + prediction[ANOMALY_THRESHOLD_KEY] = threshold + prediction[IS_ANOMALOUS_KEY] = is_anomalous + decided_class = ANOMALOUS_CLASS_NAME if is_anomalous else NORMAL_CLASS_NAME + class_predictions: List[Dict[str, Any]] = list(prediction.get("predictions") or []) + decided = [p for p in class_predictions if p.get("class") == decided_class] + others = [p for p in class_predictions if p.get("class") != decided_class] + if decided: + prediction["predictions"] = decided + others + prediction["top"] = decided[0]["class"] + prediction["confidence"] = decided[0]["confidence"] + return prediction diff --git a/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py b/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py index 4381ac0084..87e722e20f 100644 --- a/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py +++ b/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py @@ -4,6 +4,12 @@ import numpy as np import supervision as sv import torch +from roboflow_workflows.core_steps.common.anomaly_detection import ( + ANOMALOUS_CLASS_NAME, + ANOMALY_METADATA_KEYS, + IS_ANOMALOUS_KEY, + NORMAL_CLASS_NAME, +) from roboflow_workflows.core_steps.common.keypoints import real_keypoints_count from roboflow_workflows.core_steps.common.serializers import ( _attach_parent_metadata_to_detection_dict, @@ -600,6 +606,14 @@ def _serialise_classification_model_style( key=lambda item: item["confidence"], reverse=True, ) + # An anomaly model's verdict is decided against a threshold on the raw + # score (possibly overridden by the block), not by the larger confidence: + # the decided class leads, as the numpy anomaly block orders it. + if IS_ANOMALOUS_KEY in image_metadata: + individual_classes_predictions = _decided_anomaly_class_first( + individual_classes_predictions, + is_anomalous=bool(image_metadata[IS_ANOMALOUS_KEY]), + ) result["predictions"] = individual_classes_predictions result["top"] = ( individual_classes_predictions[0]["class"] @@ -636,6 +650,12 @@ def _serialise_classification_model_style( result["predictions"] = predictions_dict result["predicted_classes"] = predicted_classes + # The numpy `AnomalyDetectionResponse` dumps its verdict right after the + # classification fields; the anomaly block keeps the raw map out of the dict. + for source_key in ANOMALY_METADATA_KEYS: + if source_key in image_metadata: + result[source_key] = image_metadata[source_key] + # Append order mirrors the numpy block's in-place writes after model_dump: # attach_prediction_type_info, then parent_id, then root_parent_id # (inference_id was already emitted first, matching the dump order). @@ -649,6 +669,15 @@ def _serialise_classification_model_style( return result +def _decided_anomaly_class_first( + class_predictions: List[dict], *, is_anomalous: bool +) -> List[dict]: + decided_class = ANOMALOUS_CLASS_NAME if is_anomalous else NORMAL_CLASS_NAME + decided = [p for p in class_predictions if p["class"] == decided_class] + others = [p for p in class_predictions if p["class"] != decided_class] + return decided + others + + def _serialise_classification_formatter_style( prediction: Union[ClassificationPrediction, MultiLabelClassificationPrediction], image_metadata: dict, diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/__init__.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py new file mode 100644 index 0000000000..0b9a4dba4d --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py @@ -0,0 +1,363 @@ +from typing import Any, Dict, List, Literal, Optional, Type, Union + +import numpy as np +from pydantic import ConfigDict, Field +from roboflow_workflows.core_steps.common.anomaly_detection import ( + ANOMALY_MAP_KEY, + ANOMALY_SCORE_KEY, + ANOMALY_THRESHOLD_KEY, + IS_ANOMALOUS_KEY, + apply_threshold_override, + coerce_anomaly_map, + require_anomaly_response, + validate_threshold_override, +) +from roboflow_workflows.core_steps.common.entities import StepExecutionMode +from roboflow_workflows.core_steps.common.utils import attach_prediction_type_info +from roboflow_workflows.environment import ( + HOSTED_CLASSIFICATION_URL, + LOCAL_INFERENCE_API_URL, + WORKFLOWS_REMOTE_API_KEY_TRANSPORT, + WORKFLOWS_REMOTE_API_TARGET, + WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, + WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_CONCURRENT_REQUESTS, +) +from roboflow_workflows.execution_engine.constants import ( + INFERENCE_ID_KEY, + PARENT_ID_KEY, + ROOT_PARENT_ID_KEY, +) +from roboflow_workflows.execution_engine.entities.base import ( + Batch, + OutputDefinition, + WorkflowImageData, +) +from roboflow_workflows.execution_engine.entities.types import ( + BOOLEAN_KIND, + CLASSIFICATION_PREDICTION_KIND, + FLOAT_KIND, + IMAGE_KIND, + INFERENCE_ID_KIND, + NUMPY_ARRAY_KIND, + ROBOFLOW_MODEL_ID_KIND, + ROBOFLOW_PROJECT_KIND, + ImageInputField, + RoboflowModelField, + Selector, +) +from roboflow_workflows.execution_engine.entities.workload import ( + Discovery, + RuntimeRestriction, + WorkOperation, +) +from roboflow_workflows.prototypes.block import ( + BlockResult, + DependentResource, + WorkflowBlock, + WorkflowBlockManifest, + roboflow_platform_model, + roboflow_platform_project, +) +from roboflow_workflows.prototypes.models_provider import ModelsProvider + +from inference_sdk import InferenceConfiguration, InferenceHTTPClient + +PREDICTION_TYPE = "classification" + +LONG_DESCRIPTION = """ +Run an anomaly detection model (PatchCore or FoundAD) trained on an anomaly +detection project hosted on Roboflow. + +An anomaly model learns what *normal* looks like from normal images only and +scores a new image by how far it departs from that. The block reports: + +- `predictions`: the classification-style response, with `top` being `normal` + or `anomalous` and the two class confidences. The anomalous confidence is a + sigmoid centred on the threshold saved with the model, so 0.5 marks the + decision boundary. +- `anomaly_score`: the raw image score (unbounded, larger means more anomalous). +- `anomaly_threshold`: the decision boundary in effect for this image. +- `is_anomalous`: `anomaly_score >= anomaly_threshold`. +- `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` + array at the network's input resolution (square), in the same units as + `anomaly_score`. The map covers the whole image as the network saw it, so + stretching it to the image size overlays it. Connect it to the **Anomaly + Heatmap Visualization** block to draw it. Set `include_anomaly_map` to + `False` to skip the map when only the verdict is needed. + +`anomaly_threshold` lets you tune the operating point without retraining: +lower it to catch more defects at the cost of more false alarms, raise it to +do the opposite. The override re-decides `is_anomalous` and `top`; the class +confidences stay relative to the saved threshold. + +You will need to set your Roboflow API key in your Inference environment to use +this block. To learn more about setting your Roboflow API key, [refer to the +Inference documentation](https://inference.roboflow.com/quickstart/configure_api_key/). +""" + + +class BlockManifest(WorkflowBlockManifest): + model_config = ConfigDict( + json_schema_extra={ + "name": "Anomaly Detection Model", + "version": "v1", + "short_description": "Score an image against a model of normal images and locate what differs.", + "long_description": LONG_DESCRIPTION, + "license": "Apache-2.0", + "block_type": "model", + "search_keywords": [ + "anomaly", + "anomaly detection", + "defect", + "PatchCore", + "FoundAD", + "heatmap", + ], + "ui_manifest": { + "section": "model", + "icon": "far fa-magnifying-glass-chart", + "blockPriority": 2.5, + "inference": True, + }, + }, + protected_namespaces=(), + ) + type: Literal["roboflow_core/anomaly_detection_model@v1"] + images: Selector(kind=[IMAGE_KIND]) = ImageInputField + model_id: Union[Selector(kind=[ROBOFLOW_MODEL_ID_KIND]), str] = RoboflowModelField + include_anomaly_map: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( + default=True, + description="Return the local anomaly map at the network input resolution. Disable it when only the verdict and score are needed.", + examples=[True, "$inputs.include_anomaly_map"], + ) + anomaly_threshold: Union[Optional[float], Selector(kind=[FLOAT_KIND])] = Field( + default=None, + description="Decision threshold on the raw anomaly score, replacing the one saved with the model. Lower it to flag more images as anomalous. Leave empty to use the saved threshold.", + examples=[1.25, "$inputs.anomaly_threshold"], + ) + disable_active_learning: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( + default=True, + description="Boolean flag to disable project-level active learning for this block.", + examples=[True, "$inputs.disable_active_learning"], + ) + active_learning_target_dataset: Union[ + Selector(kind=[ROBOFLOW_PROJECT_KIND]), Optional[str] + ] = Field( + default=None, + description="Target dataset for active learning, if enabled.", + examples=["my_project", "$inputs.al_target_project"], + ) + + @classmethod + def get_compatible_task_types(cls) -> Optional[List[str]]: + # Anomaly models are registered under the classification task. + return ["classification"] + + def discover_dependent_resources(self) -> Optional[List[DependentResource]]: + resources = [roboflow_platform_model(model_id=self.model_id)] + if self.disable_active_learning is True: + return resources + if self.active_learning_target_dataset is not None: + resources.append( + roboflow_platform_project( + project_url=self.active_learning_target_dataset + ) + ) + return resources + + @classmethod + def get_parameters_accepting_batches(cls) -> List[str]: + return ["images"] + + @classmethod + def describe_outputs(cls) -> List[OutputDefinition]: + return [ + OutputDefinition(name="predictions", kind=[CLASSIFICATION_PREDICTION_KIND]), + OutputDefinition(name=ANOMALY_SCORE_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=ANOMALY_THRESHOLD_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=IS_ANOMALOUS_KEY, kind=[BOOLEAN_KIND]), + OutputDefinition(name=ANOMALY_MAP_KEY, kind=[NUMPY_ARRAY_KIND]), + OutputDefinition(name=INFERENCE_ID_KEY, kind=[INFERENCE_ID_KIND]), + OutputDefinition(name="model_id", kind=[ROBOFLOW_MODEL_ID_KIND]), + ] + + @classmethod + def get_execution_engine_compatibility(cls) -> Optional[str]: + return ">=1.3.0,<2.0.0" + + def discover_work_operations(self) -> List[WorkOperation]: + return [WorkOperation.MODEL_INFERENCE] + + def get_actual_restrictions( + self, *, ignore_environment_restrictions: bool = False + ) -> Discovery[RuntimeRestriction]: + return Discovery[RuntimeRestriction]( + items=[], complete=True, unknown_reasons=[] + ) + + +class AnomalyDetectionModelBlockV1(WorkflowBlock): + + def __init__( + self, + model_manager: ModelsProvider, + api_key: Optional[str], + step_execution_mode: StepExecutionMode, + ): + self._model_manager = model_manager + self._api_key = api_key + self._step_execution_mode = step_execution_mode + + @classmethod + def get_init_parameters(cls) -> List[str]: + return ["model_manager", "api_key", "step_execution_mode"] + + @classmethod + def get_manifest(cls) -> Type[WorkflowBlockManifest]: + return BlockManifest + + def run( + self, + images: Batch[WorkflowImageData], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + threshold_override = validate_threshold_override(anomaly_threshold) + if self._step_execution_mode is StepExecutionMode.LOCAL: + return self.run_locally( + images=images, + model_id=model_id, + include_anomaly_map=include_anomaly_map, + anomaly_threshold=threshold_override, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + ) + elif self._step_execution_mode is StepExecutionMode.REMOTE: + return self.run_remotely( + images=images, + model_id=model_id, + include_anomaly_map=include_anomaly_map, + anomaly_threshold=threshold_override, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + ) + else: + raise ValueError( + f"Unknown step execution mode: {self._step_execution_mode}" + ) + + def run_locally( + self, + images: Batch[WorkflowImageData], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + inference_images = [i.to_inference_format(numpy_preferred=True) for i in images] + self._model_manager.add_model( + model_id=model_id, + api_key=self._api_key, + ) + # The request schema needs a confidence although an anomaly response never + # filters on it: the verdict comes from the threshold on the raw score. + predictions = self._model_manager.run_classification( + model_id=model_id, + images=inference_images, + api_key=self._api_key, + confidence="default", + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + include_anomaly_map=include_anomaly_map, + ) + return self._post_process_result( + predictions=predictions, + images=images, + model_id=model_id, + anomaly_threshold=anomaly_threshold, + ) + + def run_remotely( + self, + images: Batch[Optional[WorkflowImageData]], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + api_url = ( + LOCAL_INFERENCE_API_URL + if WORKFLOWS_REMOTE_API_TARGET != "hosted" + else HOSTED_CLASSIFICATION_URL + ) + client = InferenceHTTPClient( + api_url=api_url, + api_key=self._api_key, + ) + if WORKFLOWS_REMOTE_API_TARGET == "hosted": + client.select_api_v0() + client_config = InferenceConfiguration( + api_key_transport=WORKFLOWS_REMOTE_API_KEY_TRANSPORT, + include_anomaly_map=include_anomaly_map, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + max_batch_size=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, + max_concurrent_requests=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_CONCURRENT_REQUESTS, + source="workflow-execution", + ) + client.configure(inference_configuration=client_config) + non_empty_inference_images = [i.base64_image for i in images] + predictions = client.infer( + inference_input=non_empty_inference_images, + model_id=model_id, + ) + if not isinstance(predictions, list): + predictions = [predictions] + return self._post_process_result( + predictions=predictions, + images=images, + model_id=model_id, + anomaly_threshold=anomaly_threshold, + ) + + def _post_process_result( + self, + images: Batch[WorkflowImageData], + predictions: List[dict], + model_id: str, + anomaly_threshold: Optional[float], + ) -> BlockResult: + predictions = attach_prediction_type_info( + predictions=predictions, + prediction_type=PREDICTION_TYPE, + ) + results: List[Dict[str, Any]] = [] + for prediction, image in zip(predictions, images): + require_anomaly_response(prediction, model_id=model_id) + # The raw map leaves the response dict: it is its own output, and the + # dict is what reaches sinks and JSON outputs. + anomaly_map: Optional[np.ndarray] = coerce_anomaly_map( + prediction.pop(ANOMALY_MAP_KEY, None) + ) + prediction = apply_threshold_override(prediction, anomaly_threshold) + prediction[PARENT_ID_KEY] = image.parent_metadata.parent_id + prediction[ROOT_PARENT_ID_KEY] = ( + image.workflow_root_ancestor_metadata.parent_id + ) + results.append( + { + "predictions": prediction, + ANOMALY_SCORE_KEY: float(prediction[ANOMALY_SCORE_KEY]), + ANOMALY_THRESHOLD_KEY: float(prediction[ANOMALY_THRESHOLD_KEY]), + IS_ANOMALOUS_KEY: bool(prediction[IS_ANOMALOUS_KEY]), + ANOMALY_MAP_KEY: anomaly_map, + INFERENCE_ID_KEY: prediction.get(INFERENCE_ID_KEY), + "model_id": model_id, + } + ) + return results diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py new file mode 100644 index 0000000000..54f566c34d --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py @@ -0,0 +1,514 @@ +"""Tensor-native sibling of `roboflow_core/anomaly_detection_model@v1`. + +Under ENABLE_TENSOR_DATA_REPRESENTATION this block emits native +``inference_models.ClassificationPrediction`` objects under +``TENSOR_NATIVE_CLASSIFICATION_PREDICTION_KIND`` instead of the legacy +classification response dict. ``images_metadata[0]`` carries the anomaly +verdict (``anomaly_score``, ``anomaly_threshold``, ``is_anomalous``) next to +the usual producer contract, and ``serialise_native_classification`` writes +those keys into the flag-off dict shape. + +- LOCAL: ``ModelsProvider.run_tensor_native_inference`` returns ONE batched + ``ClassificationPrediction`` whose ``images_metadata[i]`` holds the per-image + verdict and, when requested, the raw ``anomaly_map`` ndarray. The block fans + it out into single-row predictions and moves the map to its own output. +- REMOTE: the ``AnomalyDetectionResponse`` dicts are rebuilt into native + predictions; the map arrives decoded by ``inference_sdk`` or as the wire + payload. +""" + +import uuid +from time import perf_counter +from typing import Any, Dict, List, Literal, Optional, Type, Union + +import numpy as np +import torch +from pydantic import ConfigDict, Field +from roboflow_workflows.core_steps.common.anomaly_detection import ( + ANOMALOUS_CLASS_NAME, + ANOMALY_MAP_KEY, + ANOMALY_METADATA_KEYS, + ANOMALY_SCORE_KEY, + ANOMALY_THRESHOLD_KEY, + IS_ANOMALOUS_KEY, + NORMAL_CLASS_NAME, + apply_threshold_override, + coerce_anomaly_map, + require_anomaly_response, + validate_threshold_override, +) +from roboflow_workflows.core_steps.common.entities import StepExecutionMode +from roboflow_workflows.environment import ( + HOSTED_CLASSIFICATION_URL, + LOCAL_INFERENCE_API_URL, + WORKFLOWS_IMAGE_TENSOR_DEVICE, + WORKFLOWS_REMOTE_API_KEY_TRANSPORT, + WORKFLOWS_REMOTE_API_TARGET, + WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, + WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_CONCURRENT_REQUESTS, +) +from roboflow_workflows.execution_engine.constants import ( + CLASS_NAMES_KEY, + CLASSIFICATION_STYLE_KEY, + CLASSIFICATION_STYLE_MODEL, + IMAGE_DIMENSIONS_KEY, + INFERENCE_ID_KEY, + PARENT_ID_KEY, + PREDICTION_TYPE_KEY, + ROOT_PARENT_ID_KEY, +) +from roboflow_workflows.execution_engine.entities.base import ( + Batch, + OutputDefinition, + WorkflowImageData, +) +from roboflow_workflows.execution_engine.entities.tensor_native_types import ( + TENSOR_NATIVE_CLASSIFICATION_PREDICTION_KIND, +) +from roboflow_workflows.execution_engine.entities.types import ( + BOOLEAN_KIND, + FLOAT_KIND, + IMAGE_KIND, + INFERENCE_ID_KIND, + NUMPY_ARRAY_KIND, + ROBOFLOW_MODEL_ID_KIND, + ROBOFLOW_PROJECT_KIND, + ImageInputField, + RoboflowModelField, + Selector, +) +from roboflow_workflows.execution_engine.entities.workload import ( + Discovery, + RuntimeRestriction, + WorkOperation, +) +from roboflow_workflows.prototypes.block import ( + BlockResult, + DependentResource, + WorkflowBlock, + WorkflowBlockManifest, + roboflow_platform_model, + roboflow_platform_project, +) +from roboflow_workflows.prototypes.models_provider import ModelsProvider + +from inference_models.models.base.classification import ClassificationPrediction +from inference_sdk import InferenceConfiguration, InferenceHTTPClient + +PREDICTION_TYPE = "classification" + +LONG_DESCRIPTION = """ +Run an anomaly detection model (PatchCore or FoundAD) trained on an anomaly +detection project hosted on Roboflow. + +An anomaly model learns what *normal* looks like from normal images only and +scores a new image by how far it departs from that. The block reports: + +- `predictions`: the classification-style response, with `top` being `normal` + or `anomalous` and the two class confidences. The anomalous confidence is a + sigmoid centred on the threshold saved with the model, so 0.5 marks the + decision boundary. +- `anomaly_score`: the raw image score (unbounded, larger means more anomalous). +- `anomaly_threshold`: the decision boundary in effect for this image. +- `is_anomalous`: `anomaly_score >= anomaly_threshold`. +- `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` + array at the network's input resolution (square), in the same units as + `anomaly_score`. The map covers the whole image as the network saw it, so + stretching it to the image size overlays it. Connect it to the **Anomaly + Heatmap Visualization** block to draw it. Set `include_anomaly_map` to + `False` to skip the map when only the verdict is needed. + +`anomaly_threshold` lets you tune the operating point without retraining: +lower it to catch more defects at the cost of more false alarms, raise it to +do the opposite. The override re-decides `is_anomalous` and `top`; the class +confidences stay relative to the saved threshold. + +You will need to set your Roboflow API key in your Inference environment to use +this block. To learn more about setting your Roboflow API key, [refer to the +Inference documentation](https://inference.roboflow.com/quickstart/configure_api_key/). +""" + + +class BlockManifest(WorkflowBlockManifest): + model_config = ConfigDict( + json_schema_extra={ + "name": "Anomaly Detection Model", + "version": "v1", + "short_description": "Score an image against a model of normal images and locate what differs.", + "long_description": LONG_DESCRIPTION, + "license": "Apache-2.0", + "block_type": "model", + "search_keywords": [ + "anomaly", + "anomaly detection", + "defect", + "PatchCore", + "FoundAD", + "heatmap", + ], + "ui_manifest": { + "section": "model", + "icon": "far fa-magnifying-glass-chart", + "blockPriority": 2.5, + "inference": True, + }, + }, + protected_namespaces=(), + ) + type: Literal["roboflow_core/anomaly_detection_model@v1"] + images: Selector(kind=[IMAGE_KIND]) = ImageInputField + model_id: Union[Selector(kind=[ROBOFLOW_MODEL_ID_KIND]), str] = RoboflowModelField + include_anomaly_map: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( + default=True, + description="Return the local anomaly map at the network input resolution. Disable it when only the verdict and score are needed.", + examples=[True, "$inputs.include_anomaly_map"], + ) + anomaly_threshold: Union[Optional[float], Selector(kind=[FLOAT_KIND])] = Field( + default=None, + description="Decision threshold on the raw anomaly score, replacing the one saved with the model. Lower it to flag more images as anomalous. Leave empty to use the saved threshold.", + examples=[1.25, "$inputs.anomaly_threshold"], + ) + disable_active_learning: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( + default=True, + description="Boolean flag to disable project-level active learning for this block.", + examples=[True, "$inputs.disable_active_learning"], + ) + active_learning_target_dataset: Union[ + Selector(kind=[ROBOFLOW_PROJECT_KIND]), Optional[str] + ] = Field( + default=None, + description="Target dataset for active learning, if enabled.", + examples=["my_project", "$inputs.al_target_project"], + ) + + @classmethod + def get_compatible_task_types(cls) -> Optional[List[str]]: + # Anomaly models are registered under the classification task. + return ["classification"] + + def discover_dependent_resources(self) -> Optional[List[DependentResource]]: + resources = [roboflow_platform_model(model_id=self.model_id)] + if self.disable_active_learning is True: + return resources + if self.active_learning_target_dataset is not None: + resources.append( + roboflow_platform_project( + project_url=self.active_learning_target_dataset + ) + ) + return resources + + @classmethod + def get_parameters_accepting_batches(cls) -> List[str]: + return ["images"] + + @classmethod + def describe_outputs(cls) -> List[OutputDefinition]: + return [ + OutputDefinition( + name="predictions", + kind=[TENSOR_NATIVE_CLASSIFICATION_PREDICTION_KIND], + ), + OutputDefinition(name=ANOMALY_SCORE_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=ANOMALY_THRESHOLD_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=IS_ANOMALOUS_KEY, kind=[BOOLEAN_KIND]), + OutputDefinition(name=ANOMALY_MAP_KEY, kind=[NUMPY_ARRAY_KIND]), + OutputDefinition(name=INFERENCE_ID_KEY, kind=[INFERENCE_ID_KIND]), + OutputDefinition(name="model_id", kind=[ROBOFLOW_MODEL_ID_KIND]), + ] + + @classmethod + def get_execution_engine_compatibility(cls) -> Optional[str]: + return ">=1.3.0,<2.0.0" + + def discover_work_operations(self) -> List[WorkOperation]: + return [WorkOperation.MODEL_INFERENCE] + + def get_actual_restrictions( + self, *, ignore_environment_restrictions: bool = False + ) -> Discovery[RuntimeRestriction]: + return Discovery[RuntimeRestriction]( + items=[], complete=True, unknown_reasons=[] + ) + + +class AnomalyDetectionModelBlockV1(WorkflowBlock): + + def __init__( + self, + model_manager: ModelsProvider, + api_key: Optional[str], + step_execution_mode: StepExecutionMode, + ): + self._model_manager = model_manager + self._api_key = api_key + self._step_execution_mode = step_execution_mode + + @classmethod + def get_init_parameters(cls) -> List[str]: + return ["model_manager", "api_key", "step_execution_mode"] + + @classmethod + def get_manifest(cls) -> Type[WorkflowBlockManifest]: + return BlockManifest + + def run( + self, + images: Batch[WorkflowImageData], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + threshold_override = validate_threshold_override(anomaly_threshold) + if self._step_execution_mode is StepExecutionMode.LOCAL: + return self.run_locally( + images=images, + model_id=model_id, + include_anomaly_map=include_anomaly_map, + anomaly_threshold=threshold_override, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + ) + elif self._step_execution_mode is StepExecutionMode.REMOTE: + return self.run_remotely( + images=images, + model_id=model_id, + include_anomaly_map=include_anomaly_map, + anomaly_threshold=threshold_override, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + ) + else: + raise ValueError( + f"Unknown step execution mode: {self._step_execution_mode}" + ) + + def run_locally( + self, + images: Batch[WorkflowImageData], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + # Feed the representation already materialised on the images: GPU tensors + # (RGB) only when every image in the batch has one, else numpy (BGR). + if all(image.is_tensor_materialised() for image in images): + model_inputs = [image.tensor_image for image in images] + image_color_format = "rgb" + else: + model_inputs = [image.numpy_image for image in images] + image_color_format = "bgr" + self._model_manager.add_model(model_id=model_id, api_key=self._api_key) + inference_start = perf_counter() + batched_prediction: ClassificationPrediction = ( + self._model_manager.run_tensor_native_inference( + model_id=model_id, + images=model_inputs, + input_color_format=image_color_format, + include_anomaly_map=include_anomaly_map, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + ) + ) + inference_time = perf_counter() - inference_start + class_names = _class_names_map(self._model_manager.get_class_names(model_id)) + model_metadata = batched_prediction.images_metadata or [] + if len(model_metadata) != len(images): + raise ValueError( + f"Model {model_id!r} did not return an anomaly detection prediction: " + f"expected per-image metadata for {len(images)} images, got " + f"{len(model_metadata)}. The Anomaly Detection Model block needs a " + f"PatchCore or FoundAD model." + ) + results: List[dict] = [] + for index, (image, image_model_metadata) in enumerate( + zip(images, model_metadata) + ): + require_anomaly_response(image_model_metadata, model_id=model_id) + verdict = {key: image_model_metadata[key] for key in ANOMALY_METADATA_KEYS} + verdict = apply_threshold_override(verdict, anomaly_threshold) + anomaly_map = coerce_anomaly_map(image_model_metadata.get(ANOMALY_MAP_KEY)) + inference_id = str(uuid.uuid4()) + image_metadata = _build_image_metadata( + image=image, + class_names=class_names, + inference_id=inference_id, + verdict=verdict, + inference_time=inference_time, + ) + prediction = ClassificationPrediction( + class_id=_decided_class_id(verdict).to(WORKFLOWS_IMAGE_TENSOR_DEVICE), + confidence=batched_prediction.confidence[index : index + 1].to( + WORKFLOWS_IMAGE_TENSOR_DEVICE + ), + images_metadata=[image_metadata], + ) + results.append( + _result( + prediction=prediction, + verdict=verdict, + anomaly_map=anomaly_map, + inference_id=inference_id, + model_id=model_id, + ) + ) + return results + + def run_remotely( + self, + images: Batch[Optional[WorkflowImageData]], + model_id: str, + include_anomaly_map: bool, + anomaly_threshold: Optional[float], + disable_active_learning: Optional[bool], + active_learning_target_dataset: Optional[str], + ) -> BlockResult: + api_url = ( + LOCAL_INFERENCE_API_URL + if WORKFLOWS_REMOTE_API_TARGET != "hosted" + else HOSTED_CLASSIFICATION_URL + ) + client = InferenceHTTPClient( + api_url=api_url, + api_key=self._api_key, + ) + if WORKFLOWS_REMOTE_API_TARGET == "hosted": + client.select_api_v0() + client_config = InferenceConfiguration( + api_key_transport=WORKFLOWS_REMOTE_API_KEY_TRANSPORT, + include_anomaly_map=include_anomaly_map, + disable_active_learning=disable_active_learning, + active_learning_target_dataset=active_learning_target_dataset, + max_batch_size=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, + max_concurrent_requests=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_CONCURRENT_REQUESTS, + source="workflow-execution", + ) + client.configure(inference_configuration=client_config) + non_empty_inference_images = [i.base64_image for i in images] + responses = client.infer( + inference_input=non_empty_inference_images, + model_id=model_id, + ) + if not isinstance(responses, list): + responses = [responses] + results: List[dict] = [] + for image, response in zip(images, responses): + require_anomaly_response(response, model_id=model_id) + anomaly_map = coerce_anomaly_map(response.pop(ANOMALY_MAP_KEY, None)) + response = apply_threshold_override(response, anomaly_threshold) + verdict = {key: response[key] for key in ANOMALY_METADATA_KEYS} + inference_id = response.get(INFERENCE_ID_KEY) or str(uuid.uuid4()) + prediction = _native_prediction_from_response( + image=image, + response=response, + verdict=verdict, + inference_id=inference_id, + ) + results.append( + _result( + prediction=prediction, + verdict=verdict, + anomaly_map=anomaly_map, + inference_id=inference_id, + model_id=model_id, + ) + ) + return results + + +def _class_names_map(class_names: List[str]) -> Dict[int, str]: + return {index: name for index, name in enumerate(class_names)} + + +def _decided_class_id(verdict: Dict[str, Any]) -> torch.Tensor: + # The two-class convention of the anomaly models: 0 is normal, 1 is anomalous. + class_id = 1 if bool(verdict[IS_ANOMALOUS_KEY]) else 0 + return torch.tensor([class_id], dtype=torch.long) + + +def _result( + prediction: ClassificationPrediction, + verdict: Dict[str, Any], + anomaly_map: Optional[np.ndarray], + inference_id: str, + model_id: str, +) -> dict: + return { + "predictions": prediction, + ANOMALY_SCORE_KEY: float(verdict[ANOMALY_SCORE_KEY]), + ANOMALY_THRESHOLD_KEY: float(verdict[ANOMALY_THRESHOLD_KEY]), + IS_ANOMALOUS_KEY: bool(verdict[IS_ANOMALOUS_KEY]), + ANOMALY_MAP_KEY: anomaly_map, + INFERENCE_ID_KEY: inference_id, + "model_id": model_id, + } + + +def _build_image_metadata( + image: WorkflowImageData, + class_names: Dict[int, str], + inference_id: str, + verdict: Dict[str, Any], + inference_time: Optional[float] = None, +) -> dict: + height, width = image._read_shape_without_materialization() + metadata = { + CLASS_NAMES_KEY: class_names, + CLASSIFICATION_STYLE_KEY: CLASSIFICATION_STYLE_MODEL, + PREDICTION_TYPE_KEY: PREDICTION_TYPE, + IMAGE_DIMENSIONS_KEY: [height, width], + INFERENCE_ID_KEY: inference_id, + PARENT_ID_KEY: image.parent_metadata.parent_id, + ROOT_PARENT_ID_KEY: image.workflow_root_ancestor_metadata.parent_id, + # No classification confidence cutoff: an anomaly response always reports + # both classes, whatever their confidence. + ANOMALY_SCORE_KEY: float(verdict[ANOMALY_SCORE_KEY]), + ANOMALY_THRESHOLD_KEY: float(verdict[ANOMALY_THRESHOLD_KEY]), + IS_ANOMALOUS_KEY: bool(verdict[IS_ANOMALOUS_KEY]), + } + if inference_time is not None: + metadata["time"] = inference_time + return metadata + + +def _native_prediction_from_response( + image: WorkflowImageData, + response: dict, + verdict: Dict[str, Any], + inference_id: str, +) -> ClassificationPrediction: + """Rebuild a native single-row ``ClassificationPrediction`` from an + ``AnomalyDetectionResponse`` dict. Both classes are always present in the + response, so the dense confidence vector is complete.""" + class_names: Dict[int, str] = {} + confidence_by_id: Dict[int, float] = {} + for entry in response.get("predictions", []) or []: + class_id = int(entry["class_id"]) + class_names[class_id] = str(entry.get("class", class_id)) + confidence_by_id[class_id] = float(entry.get("confidence", 0.0)) + num_classes = (max(class_names.keys()) + 1) if class_names else 2 + for class_id, default_name in enumerate((NORMAL_CLASS_NAME, ANOMALOUS_CLASS_NAME)): + class_names.setdefault(class_id, default_name) + for class_id in range(num_classes): + class_names.setdefault(class_id, str(class_id)) + confidence_vector = [confidence_by_id.get(i, 0.0) for i in range(num_classes)] + image_metadata = _build_image_metadata( + image=image, + class_names=class_names, + inference_id=inference_id, + verdict=verdict, + inference_time=response.get("time"), + ) + return ClassificationPrediction( + class_id=_decided_class_id(verdict).to(WORKFLOWS_IMAGE_TENSOR_DEVICE), + confidence=torch.tensor( + [confidence_vector], + dtype=torch.float32, + device=WORKFLOWS_IMAGE_TENSOR_DEVICE, + ), + images_metadata=[image_metadata], + ) diff --git a/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py new file mode 100644 index 0000000000..6ec30d1006 --- /dev/null +++ b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py @@ -0,0 +1,118 @@ +import base64 + +import numpy as np +import pytest +from roboflow_workflows.core_steps.common.anomaly_detection import ( + apply_threshold_override, + coerce_anomaly_map, + decode_anomaly_map, + require_anomaly_response, + validate_threshold_override, +) + + +def _payload(values: np.ndarray) -> dict: + array = np.ascontiguousarray(values, dtype=" None: + # given + values = np.array([[0.0, 1.5], [-2.25, 1e6]], dtype=np.float32) + + # when + decoded = decode_anomaly_map(_payload(values)) + + # then + assert decoded.dtype == np.float32 + assert decoded.flags.writeable + np.testing.assert_array_equal(decoded, values) + + +@pytest.mark.parametrize( + "broken", + [ + {"shape": [2, 2], "dtype": "float16", "data": ""}, + {"shape": [2], "dtype": "float32", "data": ""}, + {"shape": [2, 2], "dtype": "float32", "data": "not base64!!"}, + { + "shape": [2, 2], + "dtype": "float32", + "data": base64.b64encode(b"\0" * 4).decode(), + }, + ], +) +def test_decode_anomaly_map_rejects_malformed_payloads(broken: dict) -> None: + with pytest.raises(ValueError): + decode_anomaly_map(broken) + + +def test_coerce_anomaly_map_accepts_every_wire_form() -> None: + # given + values = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32) + + # when / then + assert coerce_anomaly_map(None) is None + np.testing.assert_array_equal(coerce_anomaly_map(_payload(values)), values) + np.testing.assert_array_equal(coerce_anomaly_map(values), values) + np.testing.assert_array_equal(coerce_anomaly_map(values.tolist()), values) + with pytest.raises(ValueError): + coerce_anomaly_map(np.zeros((2, 2, 2), dtype=np.float32)) + + +def test_require_anomaly_response_names_the_model_and_missing_keys() -> None: + with pytest.raises(ValueError, match="my/1") as error: + require_anomaly_response({"top": "cat", "confidence": 0.9}, model_id="my/1") + assert "anomaly_score" in str(error.value) + + +@pytest.mark.parametrize("invalid", [float("nan"), float("inf"), "0.5", True]) +def test_validate_threshold_override_rejects_non_finite_or_non_numbers(invalid) -> None: + with pytest.raises(ValueError): + validate_threshold_override(invalid) + + +def test_validate_threshold_override_keeps_none_and_casts_ints() -> None: + assert validate_threshold_override(None) is None + assert validate_threshold_override(2) == 2.0 + + +def test_apply_threshold_override_re_decides_and_reorders_classes() -> None: + # given: the model said normal at its saved threshold of 2.0 + response = { + "predictions": [ + {"class": "normal", "class_id": 0, "confidence": 0.73}, + {"class": "anomalous", "class_id": 1, "confidence": 0.27}, + ], + "top": "normal", + "confidence": 0.73, + "anomaly_score": 1.5, + "anomaly_threshold": 2.0, + "is_anomalous": False, + } + + # when: lowering the threshold below the score flips the verdict + result = apply_threshold_override(response, 1.0) + + # then + assert result is response + assert result["is_anomalous"] is True + assert result["anomaly_threshold"] == 1.0 + assert result["top"] == "anomalous" + assert result["confidence"] == 0.27 + assert [p["class"] for p in result["predictions"]] == ["anomalous", "normal"] + + +def test_apply_threshold_override_without_override_is_a_no_op() -> None: + # given + response = {"anomaly_score": 1.5, "anomaly_threshold": 2.0, "is_anomalous": False} + + # when + result = apply_threshold_override(dict(response), None) + + # then + assert result == response diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/__init__.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_hosted_api_resolution.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_hosted_api_resolution.py new file mode 100644 index 0000000000..1656992121 --- /dev/null +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_hosted_api_resolution.py @@ -0,0 +1,23 @@ +import pytest + +from tests.unit_tests.core_steps.models.roboflow._hosted_api_resolution import ( + assert_hosted_selects_v0, + assert_non_hosted_uses_local, +) + +FAMILY = "anomaly_detection" +HOSTED_URL_ATTR = "HOSTED_CLASSIFICATION_URL" +BLOCK_CLASSES = { + "v1": "AnomalyDetectionModelBlockV1", + "v1_tensor": "AnomalyDetectionModelBlockV1", +} + + +@pytest.mark.parametrize("version", list(BLOCK_CLASSES)) +def test_hosted_remote_execution_selects_v0_and_hosted_url(version): + assert_hosted_selects_v0(FAMILY, version, BLOCK_CLASSES[version], HOSTED_URL_ATTR) + + +@pytest.mark.parametrize("version", list(BLOCK_CLASSES)) +def test_non_hosted_remote_execution_uses_local_url_and_no_v0(version): + assert_non_hosted_uses_local(FAMILY, version, BLOCK_CLASSES[version]) diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py new file mode 100644 index 0000000000..b4a19d643f --- /dev/null +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py @@ -0,0 +1,342 @@ +import base64 +from typing import Any, Dict, List +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +from pydantic import ValidationError +from roboflow_workflows.core_steps.common.entities import StepExecutionMode +from roboflow_workflows.core_steps.models.roboflow.anomaly_detection import v1 +from roboflow_workflows.core_steps.models.roboflow.anomaly_detection.v1 import ( + AnomalyDetectionModelBlockV1, + BlockManifest, +) +from roboflow_workflows.execution_engine.entities.base import ( + Batch, + ImageParentMetadata, + WorkflowImageData, +) + +TYPE = "roboflow_core/anomaly_detection_model@v1" +MAP = np.array([[0.0, 1.0], [2.0, 4.0]], dtype=np.float32) + + +def _map_payload(values: np.ndarray = MAP) -> dict: + array = np.ascontiguousarray(values, dtype=" Dict[str, Any]: + is_anomalous = score >= threshold + anomalous_confidence = 0.75 if is_anomalous else 0.25 + classes = [ + {"class": "anomalous", "class_id": 1, "confidence": anomalous_confidence}, + {"class": "normal", "class_id": 0, "confidence": 1 - anomalous_confidence}, + ] + if not is_anomalous: + classes.reverse() + response = { + "inference_id": "iid-1", + "time": 0.01, + "image": {"width": 4, "height": 3}, + "predictions": classes, + "top": classes[0]["class"], + "confidence": classes[0]["confidence"], + "anomaly_score": score, + "anomaly_threshold": threshold, + "is_anomalous": is_anomalous, + } + if anomaly_map is not None: + response["anomaly_map"] = anomaly_map + return response + + +def _image(parent_id: str = "img") -> WorkflowImageData: + return WorkflowImageData( + parent_metadata=ImageParentMetadata(parent_id=parent_id), + numpy_image=np.zeros((3, 4, 3), dtype=np.uint8), + ) + + +class _StubProvider: + def __init__(self, responses: List[Dict[str, Any]]): + self.responses = responses + self.add_model = MagicMock() + self.calls: List[Dict[str, Any]] = [] + + def run_classification(self, **kwargs: Any) -> List[dict]: + self.calls.append(kwargs) + return [dict(response) for response in self.responses] + + +def _block(provider: Any, mode: StepExecutionMode) -> AnomalyDetectionModelBlockV1: + return AnomalyDetectionModelBlockV1( + model_manager=provider, api_key="key", step_execution_mode=mode + ) + + +def _run( + block: AnomalyDetectionModelBlockV1, images: List[WorkflowImageData], **overrides +): + kwargs = dict( + model_id="ws/anomaly/1", + include_anomaly_map=True, + anomaly_threshold=None, + disable_active_learning=True, + active_learning_target_dataset=None, + ) + kwargs.update(overrides) + return block.run( + images=Batch.init(content=images, indices=[(i,) for i in range(len(images))]), + **kwargs + ) + + +@pytest.mark.parametrize("images_field_alias", ["images", "image"]) +def test_manifest_validation_when_minimal_config_is_given( + images_field_alias: str, +) -> None: + # given + data = { + "type": TYPE, + "name": "anomaly", + images_field_alias: "$inputs.image", + "model_id": "ws/anomaly/1", + } + + # when + result = BlockManifest.model_validate(data) + + # then + assert result == BlockManifest( + type=TYPE, name="anomaly", images="$inputs.image", model_id="ws/anomaly/1" + ) + assert result.include_anomaly_map is True + assert result.anomaly_threshold is None + + +@pytest.mark.parametrize( + "field, value", + [ + ("include_anomaly_map", "$inputs.include_map"), + ("include_anomaly_map", False), + ("anomaly_threshold", "$inputs.threshold"), + ("anomaly_threshold", 1.25), + ("anomaly_threshold", None), + ], +) +def test_manifest_accepts_selectors_and_literals(field: str, value: Any) -> None: + # given + data = { + "type": TYPE, + "name": "anomaly", + "images": "$inputs.image", + "model_id": "a/1", + } + data[field] = value + + # when + result = BlockManifest.model_validate(data) + + # then + assert getattr(result, field) == value + + +@pytest.mark.parametrize("field", ["type", "name", "images", "model_id"]) +def test_manifest_validation_when_required_field_is_missing(field: str) -> None: + # given + data = { + "type": TYPE, + "name": "anomaly", + "images": "$inputs.image", + "model_id": "a/1", + } + del data[field] + + # when / then + with pytest.raises(ValidationError): + BlockManifest.model_validate(data) + + +@pytest.mark.parametrize( + "field, value", + [ + ("anomaly_threshold", "high"), + ("include_anomaly_map", "maybe"), + ("model_id", None), + ], +) +def test_manifest_validation_when_field_has_invalid_type( + field: str, value: Any +) -> None: + # given + data = { + "type": TYPE, + "name": "anomaly", + "images": "$inputs.image", + "model_id": "a/1", + } + data[field] = value + + # when / then + with pytest.raises(ValidationError): + BlockManifest.model_validate(data) + + +def test_manifest_outputs_are_declared_in_contract_order() -> None: + names = [output.name for output in BlockManifest.describe_outputs()] + assert names == [ + "predictions", + "anomaly_score", + "anomaly_threshold", + "is_anomalous", + "anomaly_map", + "inference_id", + "model_id", + ] + + +def test_run_locally_forwards_the_map_flag_and_decodes_the_payload() -> None: + # given + provider = _StubProvider([_response(anomaly_map=_map_payload())]) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image()]) + + # then + provider.add_model.assert_called_once_with(model_id="ws/anomaly/1", api_key="key") + assert provider.calls[0]["include_anomaly_map"] is True + assert provider.calls[0]["images"] == [ + {"type": "numpy_object", "value": provider.calls[0]["images"][0]["value"]} + ] + assert len(result) == 1 + assert result[0]["anomaly_score"] == 3.0 + assert result[0]["anomaly_threshold"] == 2.0 + assert result[0]["is_anomalous"] is True + assert result[0]["inference_id"] == "iid-1" + assert result[0]["model_id"] == "ws/anomaly/1" + np.testing.assert_array_equal(result[0]["anomaly_map"], MAP) + assert result[0]["anomaly_map"].dtype == np.float32 + predictions = result[0]["predictions"] + assert "anomaly_map" not in predictions + assert predictions["top"] == "anomalous" + assert predictions["prediction_type"] == "classification" + assert predictions["parent_id"] == "img" + assert predictions["root_parent_id"] == "img" + + +def test_run_locally_without_map_outputs_none() -> None: + # given + provider = _StubProvider([_response()]) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image()], include_anomaly_map=False) + + # then + assert provider.calls[0]["include_anomaly_map"] is False + assert result[0]["anomaly_map"] is None + + +def test_run_locally_applies_the_threshold_override() -> None: + # given: the model says normal (1.5 < 2.0) + provider = _StubProvider([_response(score=1.5, threshold=2.0)]) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image()], anomaly_threshold=1.0) + + # then + assert result[0]["is_anomalous"] is True + assert result[0]["anomaly_threshold"] == 1.0 + assert result[0]["anomaly_score"] == 1.5 + assert result[0]["predictions"]["top"] == "anomalous" + assert result[0]["predictions"]["anomaly_threshold"] == 1.0 + assert result[0]["predictions"]["is_anomalous"] is True + + +def test_run_rejects_a_plain_classifier_response() -> None: + # given + plain = { + "predictions": [{"class": "cat", "class_id": 0, "confidence": 0.9}], + "top": "cat", + "confidence": 0.9, + } + block = _block(_StubProvider([plain]), StepExecutionMode.LOCAL) + + # when / then + with pytest.raises(ValueError, match="ws/anomaly/1"): + _run(block, [_image()]) + + +@pytest.mark.parametrize("invalid", [float("nan"), "0.5"]) +def test_run_rejects_an_invalid_threshold_override(invalid: Any) -> None: + block = _block(_StubProvider([_response()]), StepExecutionMode.LOCAL) + with pytest.raises(ValueError): + _run(block, [_image()], anomaly_threshold=invalid) + + +def test_run_locally_handles_a_batch_in_order() -> None: + # given + provider = _StubProvider([_response(score=5.0), _response(score=0.5)]) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image("a"), _image("b")]) + + # then + assert [r["is_anomalous"] for r in result] == [True, False] + assert [r["predictions"]["parent_id"] for r in result] == ["a", "b"] + + +@pytest.mark.parametrize( + "served_map", [_map_payload(), MAP.copy()], ids=["payload", "ndarray"] +) +def test_run_remotely_configures_the_client_and_accepts_both_map_forms( + served_map, +) -> None: + # given + block = _block(MagicMock(), StepExecutionMode.REMOTE) + with patch.object(v1, "InferenceHTTPClient") as client_cls: + client = client_cls.return_value + client.infer.return_value = [_response(anomaly_map=served_map)] + + # when + result = _run(block, [_image()], anomaly_threshold=2.5) + + # then + configuration = client.configure.call_args.kwargs["inference_configuration"] + assert configuration.include_anomaly_map is True + assert configuration.disable_active_learning is True + assert configuration.source == "workflow-execution" + client.infer.assert_called_once() + assert client.infer.call_args.kwargs["model_id"] == "ws/anomaly/1" + np.testing.assert_array_equal(result[0]["anomaly_map"], MAP) + assert result[0]["anomaly_threshold"] == 2.5 + assert result[0]["is_anomalous"] is True + assert "anomaly_map" not in result[0]["predictions"] + + +def test_run_remotely_wraps_a_single_response() -> None: + # given + block = _block(MagicMock(), StepExecutionMode.REMOTE) + with patch.object(v1, "InferenceHTTPClient") as client_cls: + client_cls.return_value.infer.return_value = _response() + + # when + result = _run(block, [_image()], include_anomaly_map=False) + + # then + assert len(result) == 1 + assert result[0]["anomaly_map"] is None + configuration = client_cls.return_value.configure.call_args.kwargs[ + "inference_configuration" + ] + assert configuration.include_anomaly_map is False diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py new file mode 100644 index 0000000000..f6f7a2f8ee --- /dev/null +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py @@ -0,0 +1,241 @@ +from typing import Any, Dict, List +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest +import torch +from roboflow_workflows.core_steps.common.entities import StepExecutionMode +from roboflow_workflows.core_steps.common.serializers_tensor import ( + serialise_native_classification, +) +from roboflow_workflows.core_steps.models.roboflow.anomaly_detection import v1_tensor +from roboflow_workflows.core_steps.models.roboflow.anomaly_detection.v1 import ( + BlockManifest as NumpyManifest, +) +from roboflow_workflows.core_steps.models.roboflow.anomaly_detection.v1_tensor import ( + AnomalyDetectionModelBlockV1, + BlockManifest, +) +from roboflow_workflows.execution_engine.entities.base import ( + Batch, + ImageParentMetadata, + WorkflowImageData, +) + +from inference_models.models.base.classification import ClassificationPrediction + +MAP = np.array([[0.0, 1.0], [2.0, 4.0]], dtype=np.float32) + + +def _image(parent_id: str = "img") -> WorkflowImageData: + return WorkflowImageData( + parent_metadata=ImageParentMetadata(parent_id=parent_id), + numpy_image=np.zeros((3, 4, 3), dtype=np.uint8), + ) + + +def _batched(scores: List[float], threshold: float = 2.0, with_map: bool = True): + metadata = [] + for score in scores: + entry: Dict[str, Any] = { + "anomaly_score": score, + "anomaly_threshold": threshold, + "is_anomalous": score >= threshold, + } + if with_map: + entry["anomaly_map"] = MAP.copy() + metadata.append(entry) + anomalous = torch.tensor([[0.75 if s >= threshold else 0.25] for s in scores]) + return ClassificationPrediction( + class_id=torch.tensor([int(s >= threshold) for s in scores]), + confidence=torch.cat([1 - anomalous, anomalous], dim=1).float(), + images_metadata=metadata, + ) + + +class _StubProvider: + def __init__(self, prediction: ClassificationPrediction): + self.prediction = prediction + self.add_model = MagicMock() + self.calls: List[Dict[str, Any]] = [] + + def run_tensor_native_inference(self, model_id: str, **kwargs: Any): + self.calls.append({"model_id": model_id, **kwargs}) + return self.prediction + + def get_class_names(self, model_id: str) -> List[str]: + return ["normal", "anomalous"] + + +def _block(provider: Any, mode: StepExecutionMode) -> AnomalyDetectionModelBlockV1: + return AnomalyDetectionModelBlockV1( + model_manager=provider, api_key="key", step_execution_mode=mode + ) + + +def _run(block, images: List[WorkflowImageData], **overrides): + kwargs = dict( + model_id="ws/anomaly/1", + include_anomaly_map=True, + anomaly_threshold=None, + disable_active_learning=True, + active_learning_target_dataset=None, + ) + kwargs.update(overrides) + return block.run( + images=Batch.init(content=images, indices=[(i,) for i in range(len(images))]), + **kwargs, + ) + + +def _remote_response(score: float = 3.0, threshold: float = 2.0) -> dict: + is_anomalous = score >= threshold + anomalous_confidence = 0.75 if is_anomalous else 0.25 + classes = [ + {"class": "anomalous", "class_id": 1, "confidence": anomalous_confidence}, + {"class": "normal", "class_id": 0, "confidence": 1 - anomalous_confidence}, + ] + if not is_anomalous: + classes.reverse() + return { + "inference_id": "iid-1", + "time": 0.01, + "image": {"width": 4, "height": 3}, + "predictions": classes, + "top": classes[0]["class"], + "confidence": classes[0]["confidence"], + "anomaly_score": score, + "anomaly_threshold": threshold, + "is_anomalous": is_anomalous, + "anomaly_map": MAP.copy(), + } + + +def test_sibling_manifests_declare_the_same_surface() -> None: + assert set(BlockManifest.model_fields) == set(NumpyManifest.model_fields) + numpy_outputs = [ + (o.name, [k.name for k in o.kind]) for o in NumpyManifest.describe_outputs() + ] + tensor_outputs = [ + (o.name, [k.name for k in o.kind]) for o in BlockManifest.describe_outputs() + ] + assert numpy_outputs == tensor_outputs + + +def test_run_locally_fans_out_the_batched_prediction() -> None: + # given + provider = _StubProvider(_batched([3.0, 0.5])) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image("a"), _image("b")]) + + # then + provider.add_model.assert_called_once_with(model_id="ws/anomaly/1", api_key="key") + assert provider.calls[0]["include_anomaly_map"] is True + assert provider.calls[0]["input_color_format"] == "bgr" + assert [r["is_anomalous"] for r in result] == [True, False] + assert [r["anomaly_score"] for r in result] == [3.0, 0.5] + assert [r["anomaly_threshold"] for r in result] == [2.0, 2.0] + for entry in result: + np.testing.assert_array_equal(entry["anomaly_map"], MAP) + prediction = entry["predictions"] + assert isinstance(prediction, ClassificationPrediction) + assert prediction.class_id.shape == (1,) + assert prediction.confidence.shape == (1, 2) + metadata = prediction.images_metadata[0] + assert metadata["class_names"] == {0: "normal", 1: "anomalous"} + assert "anomaly_map" not in metadata + assert metadata["inference_id"] == entry["inference_id"] + assert result[0]["predictions"].class_id.tolist() == [1] + assert result[1]["predictions"].class_id.tolist() == [0] + assert result[0]["predictions"].images_metadata[0]["parent_id"] == "a" + assert result[1]["predictions"].images_metadata[0]["parent_id"] == "b" + + +def test_run_locally_without_map_and_with_override() -> None: + # given: the model says normal at 2.0; the override at 1.0 flips it + provider = _StubProvider(_batched([1.5], with_map=False)) + block = _block(provider, StepExecutionMode.LOCAL) + + # when + result = _run(block, [_image()], include_anomaly_map=False, anomaly_threshold=1.0) + + # then + assert provider.calls[0]["include_anomaly_map"] is False + assert result[0]["anomaly_map"] is None + assert result[0]["is_anomalous"] is True + assert result[0]["anomaly_threshold"] == 1.0 + assert result[0]["predictions"].class_id.tolist() == [1] + assert result[0]["predictions"].images_metadata[0]["is_anomalous"] is True + + +def test_run_locally_rejects_a_plain_classifier() -> None: + # given: a classifier returns no per-image anomaly metadata + plain = ClassificationPrediction( + class_id=torch.tensor([0]), confidence=torch.tensor([[0.9, 0.1]]) + ) + block = _block(_StubProvider(plain), StepExecutionMode.LOCAL) + + # when / then + with pytest.raises(ValueError, match="ws/anomaly/1"): + _run(block, [_image()]) + + +def test_serialised_local_prediction_matches_the_numpy_dict_shape() -> None: + # given + provider = _StubProvider(_batched([1.5])) + block = _block(provider, StepExecutionMode.LOCAL) + result = _run(block, [_image()], anomaly_threshold=1.0) + + # when + serialised = serialise_native_classification(result[0]["predictions"]) + + # then: the override decided "anomalous" although its confidence is lower + assert list(serialised.keys()) == [ + "inference_id", + "time", + "image", + "predictions", + "top", + "confidence", + "anomaly_score", + "anomaly_threshold", + "is_anomalous", + "prediction_type", + "parent_id", + "root_parent_id", + ] + assert serialised["image"] == {"width": 4, "height": 3} + assert [p["class"] for p in serialised["predictions"]] == ["anomalous", "normal"] + assert serialised["top"] == "anomalous" + assert serialised["confidence"] == 0.25 + assert serialised["anomaly_score"] == 1.5 + assert serialised["anomaly_threshold"] == 1.0 + assert serialised["is_anomalous"] is True + assert "anomaly_map" not in serialised + + +def test_run_remotely_rebuilds_a_native_prediction() -> None: + # given + block = _block(MagicMock(), StepExecutionMode.REMOTE) + with patch.object(v1_tensor, "InferenceHTTPClient") as client_cls: + client = client_cls.return_value + client.infer.return_value = [_remote_response()] + + # when + result = _run(block, [_image()]) + + # then + configuration = client.configure.call_args.kwargs["inference_configuration"] + assert configuration.include_anomaly_map is True + prediction = result[0]["predictions"] + assert isinstance(prediction, ClassificationPrediction) + assert prediction.class_id.tolist() == [1] + assert prediction.confidence.tolist() == [[0.25, 0.75]] + assert prediction.images_metadata[0]["class_names"] == {0: "normal", 1: "anomalous"} + assert prediction.images_metadata[0]["inference_id"] == "iid-1" + np.testing.assert_array_equal(result[0]["anomaly_map"], MAP) + serialised = serialise_native_classification(prediction) + assert serialised["top"] == "anomalous" + assert serialised["anomaly_score"] == 3.0 From 87e5ec75991b5cf14e83c86f4b23ed0d93535656 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 14:28:09 -0700 Subject: [PATCH 03/17] Add the Anomaly Heatmap Visualization workflow block roboflow_core/anomaly_heatmap_visualization@v1 stretches an anomaly map onto its image and blends it on a scale anchored to the threshold, with the Roboflow app's ramp or an OpenCV colour map. Co-Authored-By: Claude Fable 5.1 --- .../anomaly_heatmap/__init__.py | 0 .../visualizations/anomaly_heatmap/v1.py | 201 ++++++++++++ .../anomaly_heatmap/v1_tensor.py | 215 +++++++++++++ .../common/annotators/anomaly_heatmap.py | 171 ++++++++++ .../visualizations/test_anomaly_heatmap.py | 295 ++++++++++++++++++ 5 files changed, 882 insertions(+) create mode 100644 workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/__init__.py create mode 100644 workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py create mode 100644 workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py create mode 100644 workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py create mode 100644 workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py diff --git a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/__init__.py b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py new file mode 100644 index 0000000000..cb8dbf8d90 --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py @@ -0,0 +1,201 @@ +from typing import List, Literal, Optional, Type, Union + +import numpy as np +from pydantic import ConfigDict, Field +from roboflow_workflows.core_steps.visualizations.common.annotators.anomaly_heatmap import ( + AnomalyHeatmapAnnotator, +) +from roboflow_workflows.core_steps.visualizations.common.base import ( + OUTPUT_IMAGE_KEY, + VisualizationBlock, + VisualizationManifest, +) +from roboflow_workflows.execution_engine.entities.base import WorkflowImageData +from roboflow_workflows.execution_engine.entities.types import ( + FLOAT_KIND, + FLOAT_ZERO_TO_ONE_KIND, + NUMPY_ARRAY_KIND, + STRING_KIND, + FloatZeroToOne, + Selector, +) +from roboflow_workflows.execution_engine.entities.workload import ( + Discovery, + RuntimeRestriction, + WorkOperation, +) +from roboflow_workflows.prototypes.block import ( + BlockResult, + DependentResource, + WorkflowBlockManifest, +) + +TYPE: str = "roboflow_core/anomaly_heatmap_visualization@v1" +SHORT_DESCRIPTION = ( + "Overlay an anomaly detection model's anomaly map on the image as a heatmap." +) +LONG_DESCRIPTION = """ +Draw the anomaly map of an **Anomaly Detection Model** (PatchCore or FoundAD) over +the image it was computed for, so the parts of the image that depart from what the +model learned as normal light up. + +## How This Block Works + +1. Takes the image and the model's `anomaly_map` and `anomaly_threshold` as input +2. Stretches the map (served at the network's square input resolution) onto the image +3. Places every map value on a fixed scale anchored to the threshold: + `t = value / (value + threshold)`, so zero is transparent, the threshold sits at + the middle of the ramp, and the ramp saturates as the evidence grows +4. Colours the scale with the chosen `color_map` and blends it over the image at + the chosen `opacity` + +The scale depends only on the model's threshold, never on the image, so the same +colour means the same evidence on every frame and normal images show their map too. +The default `ROBOFLOW` ramp is the yellow-to-red ramp the Roboflow app draws in its +model preview, with transparency that fades out where the evidence is low. The +OpenCV ramps (`JET`, `TURBO`, `INFERNO`, `VIRIDIS`, `HOT`) are opaque colour maps +over the same scale; use `opacity` to see the image through them. + +Pass the model block's `anomaly_threshold` output so the ramp follows the threshold +in effect (the saved one, or the block's override). + +## Common Use Cases + +- **Defect localisation**: show an operator where an anomalous part differs from normal +- **Model review**: compare what the model reacts to against the labelled defect +- **Reports and alerts**: attach the heatmap to a notification or dataset upload + +## Connecting to Other Blocks + +The annotated image can be connected to other visualization blocks (for example the +Classification Label Visualization to print the verdict on top), data storage blocks, +webhook and notification blocks, or video outputs. +""" + + +class AnomalyHeatmapManifest(VisualizationManifest): + type: Literal[f"{TYPE}"] + model_config = ConfigDict( + json_schema_extra={ + "name": "Anomaly Heatmap Visualization", + "version": "v1", + "short_description": SHORT_DESCRIPTION, + "long_description": LONG_DESCRIPTION, + "license": "Apache-2.0", + "block_type": "visualization", + "search_keywords": [ + "anomaly", + "anomaly detection", + "heatmap", + "defect", + "PatchCore", + "FoundAD", + ], + "ui_manifest": { + "section": "visualization", + "icon": "far fa-fire", + "blockPriority": 3, + "warnings": [ + { + "property": "copy_image", + "value": False, + "message": "This setting will mutate its input image. If the input is used by other blocks, it may cause unexpected behavior.", + } + ], + }, + } + ) + + anomaly_map: Selector(kind=[NUMPY_ARRAY_KIND]) = Field( # type: ignore + description="The anomaly map from the Anomaly Detection Model block: raw local anomaly evidence at the network input resolution.", + examples=["$steps.anomaly_model.anomaly_map"], + ) + anomaly_threshold: Union[float, Selector(kind=[FLOAT_KIND])] = Field( # type: ignore + description="The decision threshold on the raw anomaly score that anchors the colour ramp; the threshold maps to the middle of the ramp. Connect the model block's anomaly_threshold output so the ramp follows the threshold in effect.", + examples=["$steps.anomaly_model.anomaly_threshold", 1.25], + ) + color_map: Union[ + Literal["ROBOFLOW", "JET", "TURBO", "INFERNO", "VIRIDIS", "HOT"], + Selector(kind=[STRING_KIND]), + ] = Field( # type: ignore + default="ROBOFLOW", + description="Colour ramp for the heatmap. ROBOFLOW is the Roboflow app's yellow-to-red ramp, transparent where the evidence is low; the others are opaque OpenCV colour maps.", + examples=["ROBOFLOW", "JET", "$inputs.color_map"], + ) + opacity: Union[FloatZeroToOne, Selector(kind=[FLOAT_ZERO_TO_ONE_KIND])] = Field( # type: ignore + default=0.75, + description="Opacity of the heatmap overlay, from 0.0 (invisible) to 1.0 (the ramp's own transparency only).", + examples=[0.75, "$inputs.opacity"], + ) + + @classmethod + def get_execution_engine_compatibility(cls) -> Optional[str]: + return ">=1.3.0,<2.0.0" + + def discover_work_operations(self) -> List[WorkOperation]: + return [WorkOperation.VISUALIZATION] + + def get_actual_restrictions( + self, *, ignore_environment_restrictions: bool = False + ) -> Discovery[RuntimeRestriction]: + return Discovery[RuntimeRestriction]( + items=[], complete=True, unknown_reasons=[] + ) + + def discover_dependent_resources(self) -> List[DependentResource]: + return [] + + +class AnomalyHeatmapVisualizationBlockV1(VisualizationBlock): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.annotatorCache = {} + + @classmethod + def get_manifest(cls) -> Type[WorkflowBlockManifest]: + return AnomalyHeatmapManifest + + def getAnnotator( + self, + color_map: str, + opacity: float, + ) -> AnomalyHeatmapAnnotator: + key = "_".join(map(str, [color_map, opacity])) + if key not in self.annotatorCache: + self.annotatorCache[key] = AnomalyHeatmapAnnotator( + color_map=color_map, + opacity=opacity, + ) + return self.annotatorCache[key] + + def run( + self, + image: WorkflowImageData, + copy_image: bool, + anomaly_map: Optional[np.ndarray], + anomaly_threshold: float, + color_map: str, + opacity: float, + ) -> BlockResult: + if anomaly_map is None: + raise ValueError( + "Anomaly Heatmap Visualization received no anomaly map. Enable " + "`include_anomaly_map` on the Anomaly Detection Model block that " + "feeds this step." + ) + annotator = self.getAnnotator(color_map, opacity) + scene = image.numpy_image + if copy_image: + scene = scene.copy() + else: + image.declare_numpy_image_mutated() + annotated_image = annotator.annotate( + scene=scene, + anomaly_map=np.asarray(anomaly_map, dtype=np.float32), + threshold=float(anomaly_threshold), + ) + return { + OUTPUT_IMAGE_KEY: WorkflowImageData.copy_and_replace( + origin_image_data=image, numpy_image=annotated_image + ) + } diff --git a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py new file mode 100644 index 0000000000..f0b3f0f65e --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py @@ -0,0 +1,215 @@ +"""Tensor-native sibling of `roboflow_core/anomaly_heatmap_visualization@v1`. + +Under ENABLE_TENSOR_DATA_REPRESENTATION the `numpy_array` kind may carry a +``torch.Tensor``; the map is materialised to numpy and drawn with the same +annotator as the numpy sibling. +""" + +from typing import Any, List, Literal, Optional, Type, Union + +import numpy as np +import torch +from pydantic import ConfigDict, Field +from roboflow_workflows.core_steps.visualizations.common.annotators.anomaly_heatmap import ( + AnomalyHeatmapAnnotator, +) +from roboflow_workflows.core_steps.visualizations.common.base_tensor import ( + OUTPUT_IMAGE_KEY, + VisualizationBlock, + VisualizationManifest, +) +from roboflow_workflows.execution_engine.entities.base import WorkflowImageData +from roboflow_workflows.execution_engine.entities.types import ( + FLOAT_KIND, + FLOAT_ZERO_TO_ONE_KIND, + NUMPY_ARRAY_KIND, + STRING_KIND, + FloatZeroToOne, + Selector, +) +from roboflow_workflows.execution_engine.entities.workload import ( + Discovery, + RuntimeRestriction, + WorkOperation, +) +from roboflow_workflows.prototypes.block import ( + BlockResult, + DependentResource, + WorkflowBlockManifest, +) + +TYPE: str = "roboflow_core/anomaly_heatmap_visualization@v1" +SHORT_DESCRIPTION = ( + "Overlay an anomaly detection model's anomaly map on the image as a heatmap." +) +LONG_DESCRIPTION = """ +Draw the anomaly map of an **Anomaly Detection Model** (PatchCore or FoundAD) over +the image it was computed for, so the parts of the image that depart from what the +model learned as normal light up. + +## How This Block Works + +1. Takes the image and the model's `anomaly_map` and `anomaly_threshold` as input +2. Stretches the map (served at the network's square input resolution) onto the image +3. Places every map value on a fixed scale anchored to the threshold: + `t = value / (value + threshold)`, so zero is transparent, the threshold sits at + the middle of the ramp, and the ramp saturates as the evidence grows +4. Colours the scale with the chosen `color_map` and blends it over the image at + the chosen `opacity` + +The scale depends only on the model's threshold, never on the image, so the same +colour means the same evidence on every frame and normal images show their map too. +The default `ROBOFLOW` ramp is the yellow-to-red ramp the Roboflow app draws in its +model preview, with transparency that fades out where the evidence is low. The +OpenCV ramps (`JET`, `TURBO`, `INFERNO`, `VIRIDIS`, `HOT`) are opaque colour maps +over the same scale; use `opacity` to see the image through them. + +Pass the model block's `anomaly_threshold` output so the ramp follows the threshold +in effect (the saved one, or the block's override). + +## Common Use Cases + +- **Defect localisation**: show an operator where an anomalous part differs from normal +- **Model review**: compare what the model reacts to against the labelled defect +- **Reports and alerts**: attach the heatmap to a notification or dataset upload + +## Connecting to Other Blocks + +The annotated image can be connected to other visualization blocks (for example the +Classification Label Visualization to print the verdict on top), data storage blocks, +webhook and notification blocks, or video outputs. +""" + + +class AnomalyHeatmapManifest(VisualizationManifest): + type: Literal[f"{TYPE}"] + model_config = ConfigDict( + json_schema_extra={ + "name": "Anomaly Heatmap Visualization", + "version": "v1", + "short_description": SHORT_DESCRIPTION, + "long_description": LONG_DESCRIPTION, + "license": "Apache-2.0", + "block_type": "visualization", + "search_keywords": [ + "anomaly", + "anomaly detection", + "heatmap", + "defect", + "PatchCore", + "FoundAD", + ], + "ui_manifest": { + "section": "visualization", + "icon": "far fa-fire", + "blockPriority": 3, + "warnings": [ + { + "property": "copy_image", + "value": False, + "message": "This setting will mutate its input image. If the input is used by other blocks, it may cause unexpected behavior.", + } + ], + }, + } + ) + + anomaly_map: Selector(kind=[NUMPY_ARRAY_KIND]) = Field( # type: ignore + description="The anomaly map from the Anomaly Detection Model block: raw local anomaly evidence at the network input resolution.", + examples=["$steps.anomaly_model.anomaly_map"], + ) + anomaly_threshold: Union[float, Selector(kind=[FLOAT_KIND])] = Field( # type: ignore + description="The decision threshold on the raw anomaly score that anchors the colour ramp; the threshold maps to the middle of the ramp. Connect the model block's anomaly_threshold output so the ramp follows the threshold in effect.", + examples=["$steps.anomaly_model.anomaly_threshold", 1.25], + ) + color_map: Union[ + Literal["ROBOFLOW", "JET", "TURBO", "INFERNO", "VIRIDIS", "HOT"], + Selector(kind=[STRING_KIND]), + ] = Field( # type: ignore + default="ROBOFLOW", + description="Colour ramp for the heatmap. ROBOFLOW is the Roboflow app's yellow-to-red ramp, transparent where the evidence is low; the others are opaque OpenCV colour maps.", + examples=["ROBOFLOW", "JET", "$inputs.color_map"], + ) + opacity: Union[FloatZeroToOne, Selector(kind=[FLOAT_ZERO_TO_ONE_KIND])] = Field( # type: ignore + default=0.75, + description="Opacity of the heatmap overlay, from 0.0 (invisible) to 1.0 (the ramp's own transparency only).", + examples=[0.75, "$inputs.opacity"], + ) + + @classmethod + def get_execution_engine_compatibility(cls) -> Optional[str]: + return ">=1.3.0,<2.0.0" + + def discover_work_operations(self) -> List[WorkOperation]: + return [WorkOperation.VISUALIZATION] + + def get_actual_restrictions( + self, *, ignore_environment_restrictions: bool = False + ) -> Discovery[RuntimeRestriction]: + return Discovery[RuntimeRestriction]( + items=[], complete=True, unknown_reasons=[] + ) + + def discover_dependent_resources(self) -> List[DependentResource]: + return [] + + +class AnomalyHeatmapVisualizationBlockV1(VisualizationBlock): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.annotatorCache = {} + + @classmethod + def get_manifest(cls) -> Type[WorkflowBlockManifest]: + return AnomalyHeatmapManifest + + def getAnnotator( + self, + color_map: str, + opacity: float, + ) -> AnomalyHeatmapAnnotator: + key = "_".join(map(str, [color_map, opacity])) + if key not in self.annotatorCache: + self.annotatorCache[key] = AnomalyHeatmapAnnotator( + color_map=color_map, + opacity=opacity, + ) + return self.annotatorCache[key] + + def run( + self, + image: WorkflowImageData, + copy_image: bool, + anomaly_map: Optional[Any], + anomaly_threshold: float, + color_map: str, + opacity: float, + ) -> BlockResult: + if anomaly_map is None: + raise ValueError( + "Anomaly Heatmap Visualization received no anomaly map. Enable " + "`include_anomaly_map` on the Anomaly Detection Model block that " + "feeds this step." + ) + annotator = self.getAnnotator(color_map, opacity) + scene = image.numpy_image + if copy_image: + scene = scene.copy() + else: + image.declare_numpy_image_mutated() + annotated_image = annotator.annotate( + scene=scene, + anomaly_map=_as_numpy_map(anomaly_map), + threshold=float(anomaly_threshold), + ) + return { + OUTPUT_IMAGE_KEY: WorkflowImageData.copy_and_replace( + origin_image_data=image, numpy_image=annotated_image + ) + } + + +def _as_numpy_map(anomaly_map: Any) -> np.ndarray: + if isinstance(anomaly_map, torch.Tensor): + return anomaly_map.detach().cpu().numpy().astype(np.float32, copy=False) + return np.asarray(anomaly_map, dtype=np.float32) diff --git a/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py b/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py new file mode 100644 index 0000000000..f9f0092f66 --- /dev/null +++ b/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py @@ -0,0 +1,171 @@ +"""Colourise an anomaly map and blend it over an image. + +Anomaly map values share the units of the model's anomaly score and are +unbounded above, so every value is placed on a fixed saturating scale anchored +to the model's threshold: ``t = v / (v + threshold)``. Zero is transparent, the +threshold sits at the ramp's midpoint, and the ramp saturates as values grow. +The scale depends only on the model, never on the image, so the same colour +means the same evidence on every image and normal images show their map too. +This is local anomaly evidence, not a defect mask. + +The ``ROBOFLOW`` ramp reproduces the stops the Roboflow app draws in its +inference preview; the other ramps are OpenCV colour maps over the same scale. +""" + +from typing import Dict, List, Tuple + +import cv2 +import numpy as np + +ROBOFLOW_COLOR_MAP = "ROBOFLOW" + +# Stops on the [0, 1) scale, where 0.5 is the saved threshold: (position, (r, g, b), alpha). +ROBOFLOW_COLOR_STOPS: List[Tuple[float, Tuple[int, int, int], float]] = [ + (0.0, (255, 214, 0), 0.0), + (0.25, (255, 214, 0), 0.55), + (0.5, (255, 122, 0), 0.85), + (1.0, (220, 30, 30), 1.0), +] + +OPENCV_COLOR_MAPS: Dict[str, int] = { + "JET": cv2.COLORMAP_JET, + "TURBO": cv2.COLORMAP_TURBO, + "INFERNO": cv2.COLORMAP_INFERNO, + "VIRIDIS": cv2.COLORMAP_VIRIDIS, + "HOT": cv2.COLORMAP_HOT, +} + +SUPPORTED_COLOR_MAPS = (ROBOFLOW_COLOR_MAP, *OPENCV_COLOR_MAPS) + + +def normalize_anomaly_map(anomaly_map: np.ndarray, threshold: float) -> np.ndarray: + """Map raw anomaly values onto ``[0, 1)``: 0 at or below zero, 0.5 at the threshold. + + Args: + anomaly_map (np.ndarray): Raw map in the units of the anomaly score. + threshold (float): The model's decision threshold; must be positive and finite. + + Returns: + A float32 array of the same shape with values in ``[0, 1)``. + + Raises: + ValueError: If ``threshold`` is not a positive finite number. + """ + if not np.isfinite(threshold) or threshold <= 0: + raise ValueError( + f"Anomaly threshold must be a positive finite number to anchor the " + f"heatmap ramp, got {threshold!r}" + ) + positive = np.maximum(anomaly_map.astype(np.float32, copy=False), 0.0) + normalized = positive / (positive + np.float32(threshold)) + return normalized.astype(np.float32, copy=False) + + +def roboflow_ramp(normalized: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Colour a normalised map with the Roboflow app's yellow-to-red ramp. + + Args: + normalized (np.ndarray): Values in ``[0, 1]`` (clipped otherwise). + + Returns: + ``(bgr, alpha)``: a uint8 ``(h, w, 3)`` BGR image and a float32 ``(h, w)`` + alpha in ``[0, 1]`` interpolated from the ramp stops. + """ + t = np.clip(normalized, 0.0, 1.0).astype(np.float32, copy=False) + positions = [stop[0] for stop in ROBOFLOW_COLOR_STOPS] + channels = [] + for channel_index in range(3): + channel_values = [stop[1][channel_index] for stop in ROBOFLOW_COLOR_STOPS] + channels.append(np.interp(t, positions, channel_values)) + alphas = [stop[2] for stop in ROBOFLOW_COLOR_STOPS] + alpha = np.interp(t, positions, alphas).astype(np.float32) + rgb = np.stack(channels, axis=-1) + bgr = np.rint(rgb[..., ::-1]).astype(np.uint8) + return bgr, alpha + + +def colorize_anomaly_map( + normalized: np.ndarray, color_map: str +) -> Tuple[np.ndarray, np.ndarray]: + """Colour a normalised map with the named ramp. + + Args: + normalized (np.ndarray): Values in ``[0, 1]``. + color_map (str): ``"ROBOFLOW"`` or one of the OpenCV names in + ``OPENCV_COLOR_MAPS``. + + Returns: + ``(bgr, alpha)`` as :func:`roboflow_ramp`. OpenCV ramps are opaque + (alpha 1 everywhere); the blend opacity alone controls their strength. + + Raises: + ValueError: If ``color_map`` is not supported. + """ + name = str(color_map).upper() + if name == ROBOFLOW_COLOR_MAP: + return roboflow_ramp(normalized) + if name not in OPENCV_COLOR_MAPS: + raise ValueError( + f"Unsupported anomaly heatmap color_map {color_map!r}; " + f"expected one of {list(SUPPORTED_COLOR_MAPS)}" + ) + scaled = np.rint(np.clip(normalized, 0.0, 1.0) * 255.0).astype(np.uint8) + bgr = cv2.applyColorMap(scaled, OPENCV_COLOR_MAPS[name]) + alpha = np.ones(normalized.shape, dtype=np.float32) + return bgr, alpha + + +class AnomalyHeatmapAnnotator: + """Blend a colourised anomaly map over a BGR scene.""" + + def __init__(self, color_map: str, opacity: float): + if str(color_map).upper() not in SUPPORTED_COLOR_MAPS: + raise ValueError( + f"Unsupported anomaly heatmap color_map {color_map!r}; " + f"expected one of {list(SUPPORTED_COLOR_MAPS)}" + ) + if not 0.0 <= opacity <= 1.0: + raise ValueError(f"opacity must be within [0, 1], got {opacity!r}") + self.color_map = str(color_map).upper() + self.opacity = float(opacity) + + def annotate( + self, scene: np.ndarray, anomaly_map: np.ndarray, threshold: float + ) -> np.ndarray: + """Draw the map onto ``scene`` in place and return it. + + Args: + scene (np.ndarray): uint8 BGR ``(h, w, 3)`` or grayscale ``(h, w)`` + image; a grayscale scene is promoted to BGR. + anomaly_map (np.ndarray): 2D raw map at any resolution; it is + stretched to the scene with bilinear interpolation. + threshold (float): The threshold anchoring the ramp. + + Returns: + The annotated uint8 BGR image. + + Raises: + ValueError: If the map is not 2D or the threshold is invalid. + """ + anomaly_map = np.asarray(anomaly_map, dtype=np.float32) + if anomaly_map.ndim != 2: + raise ValueError( + f"Anomaly map must be 2D, got shape {tuple(anomaly_map.shape)}" + ) + if scene.ndim == 2: + scene = cv2.cvtColor(scene, cv2.COLOR_GRAY2BGR) + height, width = scene.shape[:2] + + normalized = normalize_anomaly_map(anomaly_map, threshold=threshold) + if normalized.shape != (height, width): + normalized = cv2.resize( + normalized, (width, height), interpolation=cv2.INTER_LINEAR + ) + bgr, alpha = colorize_anomaly_map(normalized, color_map=self.color_map) + + weight = (alpha * self.opacity)[..., None] + blended = ( + scene.astype(np.float32) * (1.0 - weight) + bgr.astype(np.float32) * weight + ) + np.copyto(scene, np.rint(blended).astype(np.uint8)) + return scene diff --git a/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py b/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py new file mode 100644 index 0000000000..b185c08302 --- /dev/null +++ b/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py @@ -0,0 +1,295 @@ +import numpy as np +import pytest +import torch +from pydantic import ValidationError +from roboflow_workflows.core_steps.visualizations.anomaly_heatmap.v1 import ( + AnomalyHeatmapManifest, + AnomalyHeatmapVisualizationBlockV1, +) +from roboflow_workflows.core_steps.visualizations.anomaly_heatmap.v1_tensor import ( + AnomalyHeatmapManifest as TensorManifest, +) +from roboflow_workflows.core_steps.visualizations.anomaly_heatmap.v1_tensor import ( + AnomalyHeatmapVisualizationBlockV1 as TensorBlock, +) +from roboflow_workflows.core_steps.visualizations.common.annotators.anomaly_heatmap import ( + AnomalyHeatmapAnnotator, + colorize_anomaly_map, + normalize_anomaly_map, +) +from roboflow_workflows.execution_engine.entities.base import ( + ImageParentMetadata, + WorkflowImageData, +) + +TYPE = "roboflow_core/anomaly_heatmap_visualization@v1" + + +@pytest.mark.parametrize("manifest_class", [AnomalyHeatmapManifest, TensorManifest]) +@pytest.mark.parametrize("images_field_alias", ["images", "image"]) +def test_manifest_validation_when_valid_manifest_is_given( + manifest_class, images_field_alias: str +) -> None: + # given + data = { + "type": TYPE, + "name": "heatmap", + images_field_alias: "$inputs.image", + "anomaly_map": "$steps.anomaly.anomaly_map", + "anomaly_threshold": "$steps.anomaly.anomaly_threshold", + "color_map": "JET", + "opacity": 0.5, + } + + # when + result = manifest_class.model_validate(data) + + # then + assert result == manifest_class( + type=TYPE, + name="heatmap", + images="$inputs.image", + anomaly_map="$steps.anomaly.anomaly_map", + anomaly_threshold="$steps.anomaly.anomaly_threshold", + color_map="JET", + opacity=0.5, + ) + assert result.copy_image is True + + +def test_manifest_defaults_and_literal_threshold() -> None: + result = AnomalyHeatmapManifest.model_validate( + { + "type": TYPE, + "name": "heatmap", + "image": "$inputs.image", + "anomaly_map": "$steps.anomaly.anomaly_map", + "anomaly_threshold": 1.5, + } + ) + assert result.color_map == "ROBOFLOW" + assert result.opacity == 0.75 + assert result.anomaly_threshold == 1.5 + + +def test_sibling_manifests_declare_the_same_surface() -> None: + assert set(AnomalyHeatmapManifest.model_fields) == set(TensorManifest.model_fields) + assert [o.name for o in AnomalyHeatmapManifest.describe_outputs()] == [ + o.name for o in TensorManifest.describe_outputs() + ] + + +@pytest.mark.parametrize( + "field, value", + [ + ("anomaly_map", "not a selector"), + ("anomaly_threshold", "high"), + ("color_map", "RAINBOW"), + ("opacity", 1.5), + ], +) +def test_manifest_validation_when_field_is_invalid(field: str, value) -> None: + data = { + "type": TYPE, + "name": "heatmap", + "image": "$inputs.image", + "anomaly_map": "$steps.anomaly.anomaly_map", + "anomaly_threshold": 1.5, + } + data[field] = value + with pytest.raises(ValidationError): + AnomalyHeatmapManifest.model_validate(data) + + +@pytest.mark.parametrize("field", ["anomaly_map", "anomaly_threshold"]) +def test_manifest_validation_when_required_field_is_missing(field: str) -> None: + data = { + "type": TYPE, + "name": "heatmap", + "image": "$inputs.image", + "anomaly_map": "$steps.anomaly.anomaly_map", + "anomaly_threshold": 1.5, + } + del data[field] + with pytest.raises(ValidationError): + AnomalyHeatmapManifest.model_validate(data) + + +def test_normalize_anomaly_map_anchors_the_threshold_at_half() -> None: + # given + values = np.array([[-1.0, 0.0], [2.0, 6.0]], dtype=np.float32) + + # when + normalized = normalize_anomaly_map(values, threshold=2.0) + + # then + np.testing.assert_allclose(normalized, [[0.0, 0.0], [0.5, 0.75]]) + with pytest.raises(ValueError): + normalize_anomaly_map(values, threshold=0.0) + + +def test_roboflow_ramp_hits_its_stops() -> None: + # given + normalized = np.array([[0.0, 0.25, 0.5, 1.0]], dtype=np.float32) + + # when + bgr, alpha = colorize_anomaly_map(normalized, color_map="ROBOFLOW") + + # then: stops are (255,214,0,a0), (255,214,0,.55), (255,122,0,.85), (220,30,30,1) + assert bgr.tolist() == [ + [[0, 214, 255], [0, 214, 255], [0, 122, 255], [30, 30, 220]] + ] + np.testing.assert_allclose(alpha, [[0.0, 0.55, 0.85, 1.0]]) + + +def test_opencv_ramp_is_opaque() -> None: + bgr, alpha = colorize_anomaly_map(np.full((2, 2), 0.5, dtype=np.float32), "JET") + assert bgr.shape == (2, 2, 3) and bgr.dtype == np.uint8 + np.testing.assert_array_equal(alpha, np.ones((2, 2), dtype=np.float32)) + with pytest.raises(ValueError): + colorize_anomaly_map(np.zeros((2, 2), dtype=np.float32), "RAINBOW") + + +def _scene(height: int = 8, width: int = 8) -> np.ndarray: + return np.full((height, width, 3), 100, dtype=np.uint8) + + +def test_annotator_blends_the_threshold_colour_at_the_ramp_midpoint() -> None: + # given: a map at exactly the threshold everywhere, stretched 2x2 -> 8x8 + annotator = AnomalyHeatmapAnnotator(color_map="ROBOFLOW", opacity=1.0) + scene = _scene() + + # when + annotated = annotator.annotate(scene, np.full((2, 2), 2.0, dtype=np.float32), 2.0) + + # then: 100*(1-0.85) + (0,122,255)*0.85 per BGR channel + assert annotated is scene + assert annotated.shape == (8, 8, 3) + expected = np.rint(100 * 0.15 + np.array([0, 122, 255]) * 0.85).astype(np.uint8) + assert annotated[4, 4].tolist() == expected.tolist() + + +def test_annotator_leaves_zero_evidence_untouched_and_scales_by_opacity() -> None: + # given + annotator = AnomalyHeatmapAnnotator(color_map="ROBOFLOW", opacity=0.5) + scene = _scene() + anomaly_map = np.zeros((2, 2), dtype=np.float32) + anomaly_map[1, 1] = 1e9 # saturates the ramp + + # when + annotated = annotator.annotate(scene, anomaly_map, 2.0) + + # then + assert annotated[0, 0].tolist() == [100, 100, 100] + expected = np.rint(100 * 0.5 + np.array([30, 30, 220]) * 0.5).astype(np.uint8) + assert annotated[7, 7].tolist() == expected.tolist() + + +def test_annotator_promotes_grayscale_scenes_and_rejects_bad_maps() -> None: + annotator = AnomalyHeatmapAnnotator(color_map="HOT", opacity=0.5) + annotated = annotator.annotate( + np.zeros((4, 4), dtype=np.uint8), np.ones((2, 2), dtype=np.float32), 1.0 + ) + assert annotated.shape == (4, 4, 3) + with pytest.raises(ValueError): + annotator.annotate(_scene(), np.ones((2, 2, 2), dtype=np.float32), 1.0) + with pytest.raises(ValueError): + AnomalyHeatmapAnnotator(color_map="RAINBOW", opacity=0.5) + + +def _image(scene: np.ndarray) -> WorkflowImageData: + return WorkflowImageData( + parent_metadata=ImageParentMetadata(parent_id="img"), numpy_image=scene + ) + + +@pytest.mark.parametrize( + "block_class", [AnomalyHeatmapVisualizationBlockV1, TensorBlock] +) +def test_block_copies_the_input_by_default(block_class) -> None: + # given + block = block_class() + scene = _scene() + image = _image(scene) + + # when + result = block.run( + image=image, + copy_image=True, + anomaly_map=np.full((2, 2), 2.0, dtype=np.float32), + anomaly_threshold=2.0, + color_map="ROBOFLOW", + opacity=1.0, + ) + + # then + output = result["image"] + assert output.numpy_image.shape == (8, 8, 3) + assert output.parent_metadata.parent_id == "img" + assert scene[4, 4].tolist() == [100, 100, 100] + assert output.numpy_image[4, 4].tolist() != [100, 100, 100] + assert output.numpy_image is not scene + + +@pytest.mark.parametrize( + "block_class", [AnomalyHeatmapVisualizationBlockV1, TensorBlock] +) +def test_block_mutates_the_input_when_asked(block_class) -> None: + # given + block = block_class() + scene = _scene() + + # when + result = block.run( + image=_image(scene), + copy_image=False, + anomaly_map=np.full((2, 2), 2.0, dtype=np.float32), + anomaly_threshold=2.0, + color_map="JET", + opacity=0.75, + ) + + # then + assert result["image"].numpy_image is scene + assert scene[4, 4].tolist() != [100, 100, 100] + + +@pytest.mark.parametrize( + "block_class", [AnomalyHeatmapVisualizationBlockV1, TensorBlock] +) +def test_block_requires_a_map(block_class) -> None: + with pytest.raises(ValueError, match="include_anomaly_map"): + block_class().run( + image=_image(_scene()), + copy_image=True, + anomaly_map=None, + anomaly_threshold=2.0, + color_map="ROBOFLOW", + opacity=1.0, + ) + + +def test_tensor_block_accepts_a_torch_map() -> None: + # given + scene = _scene() + + # when + result = TensorBlock().run( + image=_image(scene), + copy_image=True, + anomaly_map=torch.full((2, 2), 2.0), + anomaly_threshold=2.0, + color_map="ROBOFLOW", + opacity=1.0, + ) + + # then + expected = np.rint(100 * 0.15 + np.array([0, 122, 255]) * 0.85).astype(np.uint8) + assert result["image"].numpy_image[4, 4].tolist() == expected.tolist() + + +def test_annotator_is_cached_per_style() -> None: + block = AnomalyHeatmapVisualizationBlockV1() + first = block.getAnnotator("ROBOFLOW", 0.5) + assert block.getAnnotator("ROBOFLOW", 0.5) is first + assert block.getAnnotator("JET", 0.5) is not first From 18bc3742a304df04f302774259c707b9e990d943 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 14:28:09 -0700 Subject: [PATCH 04/17] Register the anomaly detection blocks and note them in the changelog Co-Authored-By: Claude Fable 5.1 --- workflows/CHANGELOG.md | 2 ++ .../roboflow_workflows/core_steps/loader.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/workflows/CHANGELOG.md b/workflows/CHANGELOG.md index 6de6efea74..be4cd79b72 100644 --- a/workflows/CHANGELOG.md +++ b/workflows/CHANGELOG.md @@ -24,6 +24,8 @@ for contributor and maintainer responsibilities. ### Added +- Anomaly Detection Model block (`roboflow_core/anomaly_detection_model@v1`): runs a PatchCore or FoundAD model trained on a Roboflow anomaly detection project and outputs the classification-style `predictions`, the raw `anomaly_score`, the `anomaly_threshold` in effect, the `is_anomalous` verdict and the `anomaly_map` (a float32 `numpy_array` at the network input resolution). `include_anomaly_map` skips the map; `anomaly_threshold` overrides the threshold saved with the model. `ModelsProvider.run_classification` gains `include_anomaly_map`. +- Anomaly Heatmap Visualization block (`roboflow_core/anomaly_heatmap_visualization@v1`): stretches an anomaly map onto its image and blends it as a heatmap on a scale anchored to the threshold (`value / (value + threshold)`), with the Roboflow app's ramp or an OpenCV colour map. - The nine Roboflow platform blocks are core blocks of this package again: dataset upload v1 and v2, custom metadata, model monitoring inference aggregator, vision events, vision event bundle, asset library attributes, visual search and visual search classifier. They live in `core_steps.sinks.roboflow` and `core_steps.integrations.roboflow`, so a standalone install now lists 248 blocks. Inference 1.6.1 to 1.7.2 shipped them as the `inference.roboflow_workflows_plugin` plugin; that plugin is now an empty compatibility stub. - `RoboflowPlatformClient` gains the operations these blocks call: `get_roboflow_workspace`, `add_custom_metadata`, `register_image_at_roboflow`, `annotate_image_at_roboflow`, `update_image_metadata_at_roboflow`, `batch_update_image_metadata_at_roboflow`, `search_project_images_at_roboflow`, `send_inference_results_to_model_monitoring`, `get_device_id`, `get_server_version` and `get_system_info`. The offline client refuses the API operations. A host can register a fallback client for these blocks with `set_platform_blocks_client_factory`, used when a block gets no client, the offline client, or a client that does not implement these operations itself. - Vision events sends its cloud image upload and event request only through a client that implements these operations. With the offline client it returns `error_status` with the offline refusal and sends nothing; writing to a local event store is unaffected. diff --git a/workflows/roboflow_workflows/core_steps/loader.py b/workflows/roboflow_workflows/core_steps/loader.py index fbb5fe6549..579fc2bbb9 100644 --- a/workflows/roboflow_workflows/core_steps/loader.py +++ b/workflows/roboflow_workflows/core_steps/loader.py @@ -891,6 +891,14 @@ from roboflow_workflows.core_steps.models.roboflow.multi_class_classification.v3 import ( RoboflowClassificationModelBlockV3, ) +if ENABLE_TENSOR_DATA_REPRESENTATION: + from roboflow_workflows.core_steps.models.roboflow.anomaly_detection.v1_tensor import ( + AnomalyDetectionModelBlockV1, + ) +else: + from roboflow_workflows.core_steps.models.roboflow.anomaly_detection.v1 import ( + AnomalyDetectionModelBlockV1, + ) if ENABLE_TENSOR_DATA_REPRESENTATION: from roboflow_workflows.core_steps.models.roboflow.multi_label_classification.v1_tensor import ( RoboflowMultiLabelClassificationModelBlockV1, @@ -1285,6 +1293,14 @@ ) # Visualizers +if not ENABLE_TENSOR_DATA_REPRESENTATION: + from roboflow_workflows.core_steps.visualizations.anomaly_heatmap.v1 import ( + AnomalyHeatmapVisualizationBlockV1, + ) +else: + from roboflow_workflows.core_steps.visualizations.anomaly_heatmap.v1_tensor import ( + AnomalyHeatmapVisualizationBlockV1, + ) if not ENABLE_TENSOR_DATA_REPRESENTATION: from roboflow_workflows.core_steps.visualizations.background_color.v1 import ( BackgroundColorVisualizationBlockV1, @@ -1796,6 +1812,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]: SpaceXAIBlockV2, SpaceXAIBlockV3, CosineSimilarityBlockV1, + AnomalyHeatmapVisualizationBlockV1, BackgroundColorVisualizationBlockV1, BarcodeDetectorBlockV1, BlurVisualizationBlockV1, @@ -1942,6 +1959,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]: RoboflowKeypointDetectionModelBlockV3, RoboflowClassificationModelBlockV2, RoboflowClassificationModelBlockV3, + AnomalyDetectionModelBlockV1, RoboflowMultiLabelClassificationModelBlockV2, RoboflowMultiLabelClassificationModelBlockV3, ActionRecognitionModelBlockV1, From 46c79d0b4680128a7db7438add5be41d6d5b5e9d Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 18:16:30 -0700 Subject: [PATCH 05/17] Require inference-sdk 1.7.2 for the workflows package The anomaly detection block passes `include_anomaly_map` to `InferenceConfiguration` on its remote path. The field shipped in inference-sdk 1.6.1 and 1.7.2 but not in 1.7.0 or 1.7.1, whose frozen dataclass rejects the keyword, so the floor moves to the release that has it (the lock already resolves 1.7.2). Co-Authored-By: Claude Fable 5.1 --- workflows/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/pyproject.toml b/workflows/pyproject.toml index 40d3d76bf3..86042df3a8 100644 --- a/workflows/pyproject.toml +++ b/workflows/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "torch>=2.0.1,<3.0.0", "torchvision", "inference-models>=0.37.1", - "inference-sdk>=1.7.0", + "inference-sdk>=1.7.2", "msgpack", "websocket-client~=1.9.0", "openai>=1.17.0,<2.0.0", From 3e57b3b64fb7fe876a458eb8282d222b747cbfd4 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Thu, 15 Oct 2026 18:21:47 -0700 Subject: [PATCH 06/17] chore(workflows): refresh uv.lock for inference-sdk>=1.7.2 Co-Authored-By: Claude Fable 5.1 --- workflows/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflows/uv.lock b/workflows/uv.lock index 364d1663fb..92fb548672 100644 --- a/workflows/uv.lock +++ b/workflows/uv.lock @@ -4149,7 +4149,7 @@ requires-dist = [ { name = "filelock", specifier = ">=3.20.3,<=4.0.0" }, { name = "httpx", specifier = "~=0.28.1" }, { name = "inference-models", specifier = ">=0.37.1" }, - { name = "inference-sdk", specifier = ">=1.7.0" }, + { name = "inference-sdk", specifier = ">=1.7.2" }, { name = "matplotlib" }, { name = "msgpack" }, { name = "networkx", specifier = ">=3.0.0,<4.0.0" }, From 78bd5e8eb8a08f97f202c4a5cde53861dcd5f815 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 15:04:07 -0700 Subject: [PATCH 07/17] Let the request confidence decide the anomaly verdict PatchCore and FoundAD now call an image anomalous when its anomalous confidence, sigmoid((score - threshold) / scale), reaches the request confidence instead of always deciding at the saved threshold. The models take recommended_parameters, so "best" resolves to the model-eval value and "default" to 0.5, which reproduces the calibrated decision. A classification request that omits confidence (body or legacy query) gets "best" for anomaly models; plain classifiers keep their 0.4 default. The response carries the calibration scale as anomaly_scale so callers can re-decide against their own threshold with the same rule. Co-Authored-By: Claude Opus 5.5 --- .../core/entities/responses/inference.py | 8 +- inference/core/interfaces/http/http_api.py | 8 +- .../core/models/inference_models_adapters.py | 19 +++- .../models/common/anomaly_detection.py | 71 ++++++++++++-- .../foundad_anomaly_detection_torch.py | 16 +++- .../patchcore_anomaly_detection_torch.py | 16 +++- .../models/common/test_anomaly_detection.py | 96 ++++++++++++++++++- ..._legacy_http_route_forwards_anomaly_map.py | 28 ++++++ .../models/test_anomaly_detection.py | 34 ++++++- 9 files changed, 269 insertions(+), 27 deletions(-) diff --git a/inference/core/entities/responses/inference.py b/inference/core/entities/responses/inference.py index 4d338cf580..772813042d 100644 --- a/inference/core/entities/responses/inference.py +++ b/inference/core/entities/responses/inference.py @@ -355,7 +355,13 @@ class AnomalyDetectionResponse(ClassificationInferenceResponse): anomaly_threshold: float = Field( description="Decision threshold fitted on validation images" ) - is_anomalous: bool + anomaly_scale: float = Field( + description="Calibration scale: the anomalous confidence is " + "sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale)" + ) + is_anomalous: bool = Field( + description="Whether the anomalous confidence reached the request confidence" + ) anomaly_map: Optional[AnomalyMapPayload] = Field( default=None, description="Raw local anomaly evidence at the network input resolution", diff --git a/inference/core/interfaces/http/http_api.py b/inference/core/interfaces/http/http_api.py index cbb970b84a..37bed16daa 100644 --- a/inference/core/interfaces/http/http_api.py +++ b/inference/core/interfaces/http/http_api.py @@ -5072,11 +5072,17 @@ def legacy_infer_from_request( args = {"keypoint_confidence": keypoint_confidence} elif task_type == "semantic-segmentation": inference_request_type = SemanticSegmentationInferenceRequest + # Leave an omitted confidence unset so anomaly detection models can + # tell it apart from an explicit one and use the model-eval value. + if ( + task_type != "classification" + or "confidence" in request.query_params + ): + args["confidence"] = confidence inference_request = inference_request_type( api_key=api_key, model_id=model_id, image=request_image, - confidence=confidence, iou_threshold=overlap, max_detections=max_detections, visualization_labels=labels, diff --git a/inference/core/models/inference_models_adapters.py b/inference/core/models/inference_models_adapters.py index 4f55a92edc..a07ee1d88f 100644 --- a/inference/core/models/inference_models_adapters.py +++ b/inference/core/models/inference_models_adapters.py @@ -1834,6 +1834,16 @@ def _reshape_classification_confidences( class InferenceModelsAnomalyDetectionAdapter(InferenceModelsClassificationAdapter): """Serves PatchCore and FoundAD through the classification route.""" + def infer_from_request( + self, + request: ClassificationInferenceRequest, + ) -> Union[List[InferenceResponse], InferenceResponse]: + # The classification request defaults confidence to 0.4; a caller who sends + # none gets the model-eval recommendation instead. + if "confidence" not in request.model_fields_set: + request = request.model_copy(update={"confidence": "best"}) + return super().infer_from_request(request) + def postprocess( self, predictions: Any, @@ -1841,7 +1851,9 @@ def postprocess( **kwargs, ) -> List[AnomalyDetectionResponse]: post_processed_predictions = self._model.post_process( - predictions, include_anomaly_map=kwargs.get("include_anomaly_map", False) + predictions, + include_anomaly_map=kwargs.get("include_anomaly_map", False), + confidence=kwargs["confidence"], ) return prepare_anomaly_detection_response( post_processed_predictions, @@ -1855,8 +1867,8 @@ def prepare_anomaly_detection_response( image_sizes: List[Tuple[int, int]], class_names: List[str], ) -> List[AnomalyDetectionResponse]: - # The decision comes from the threshold saved with the model, so both classes - # are always reported and the request confidence does not filter them. + # The request confidence decides the verdict instead of filtering classes, so + # both classes are always reported, the decided one first. batch_confidences = _reshape_classification_confidences( confidence=post_processed_predictions.confidence.cpu(), expected_num_images=len(image_sizes), @@ -1886,6 +1898,7 @@ def prepare_anomaly_detection_response( confidence=class_predictions[0]["confidence"], anomaly_score=image_metadata["anomaly_score"], anomaly_threshold=image_metadata["anomaly_threshold"], + anomaly_scale=image_metadata["anomaly_scale"], is_anomalous=image_metadata["is_anomalous"], anomaly_map=( AnomalyMapPayload(**encode_anomaly_map(anomaly_map)) diff --git a/inference_models/inference_models/models/common/anomaly_detection.py b/inference_models/inference_models/models/common/anomaly_detection.py index 432e888f8c..3259f23d85 100644 --- a/inference_models/inference_models/models/common/anomaly_detection.py +++ b/inference_models/inference_models/models/common/anomaly_detection.py @@ -19,10 +19,16 @@ from pydantic import BaseModel, ConfigDict, Field, PositiveInt, ValidationError from inference_models import ClassificationPrediction -from inference_models.entities import ColorFormat +from inference_models.entities import ColorFormat, Confidence from inference_models.errors import CorruptedModelPackageError, ModelInputError +from inference_models.models.common.roboflow.post_processing import ConfidenceFilter +from inference_models.weights_providers.entities import RecommendedParameters ANOMALY_CLASS_NAMES = ["normal", "anomalous"] +ANOMALOUS_CLASS_ID = 1 +# The anomalous confidence is exactly 0.5 at the saved threshold, so 0.5 reproduces +# the decision calibrated during training. +ANOMALY_DEFAULT_CONFIDENCE = 0.5 CORRUPTED_PACKAGE_HELP_URL = "https://inference-models.roboflow.com/errors/model-loading/#corruptedmodelpackageerror" MODEL_INPUT_HELP_URL = ( "https://inference-models.roboflow.com/errors/input-validation/#modelinputerror" @@ -220,6 +226,8 @@ def post_process_anomaly_scores( model_results: AnomalyRawPrediction, calibration: AnomalyCalibration, include_anomaly_map: bool, + confidence: Confidence, + recommended_parameters: Optional[RecommendedParameters], ) -> ClassificationPrediction: """Turn raw anomaly scores into the shared classification prediction. @@ -228,36 +236,47 @@ def post_process_anomaly_scores( maps for one batch, as produced by the model forward pass. calibration (AnomalyCalibration): Threshold and scale saved with the model. include_anomaly_map (bool): If True, each image's metadata carries its map. + confidence (Confidence): Minimum anomalous confidence for the anomalous + verdict: a number in ``[0, 1]``, ``"best"`` for the model-eval + recommendation, or ``"default"`` for 0.5 (the saved threshold). + recommended_parameters (Optional[RecommendedParameters]): Model-eval + recommendations ``"best"`` resolves against. Returns: - ClassificationPrediction over ``normal`` and ``anomalous``. ``class_id`` - follows the saved threshold and ``confidence`` maps the threshold to - exactly 0.5. ``images_metadata[i]`` holds ``anomaly_score``, - ``anomaly_threshold``, ``is_anomalous`` and, when requested, - ``anomaly_map``: a float32 array of shape ``(image_size, image_size)`` - in network input coordinates, covering the whole (square-resized) image. + ClassificationPrediction over ``normal`` and ``anomalous``. The anomalous + confidence is ``sigmoid((score - threshold) / scale)``, exactly 0.5 at the + saved threshold, and ``class_id`` is anomalous when it reaches + ``confidence``. ``images_metadata[i]`` holds ``anomaly_score``, + ``anomaly_threshold`` (the saved threshold), ``anomaly_scale``, + ``is_anomalous`` and, when requested, ``anomaly_map``: a float32 array of + shape ``(image_size, image_size)`` in network input coordinates, covering + the whole (square-resized) image. Raises: CorruptedModelPackageError: If the model produced non-finite values. + ModelInputError: If ``confidence`` is not a number in ``[0, 1]``, + ``"best"`` or ``"default"``. """ + minimum_confidence = resolve_anomalous_confidence( + confidence=confidence, recommended_parameters=recommended_parameters + ) scores = model_results.scores.double() if not torch.isfinite(scores).all() or not torch.isfinite(model_results.maps).all(): raise CorruptedModelPackageError( message="Anomaly detection model produced non-finite scores.", help_url=CORRUPTED_PACKAGE_HELP_URL, ) - # The threshold maps to exactly 0.5, so the decision below and the argmax of - # the two class confidences always agree. margin = (scores - calibration.threshold) / calibration.scale anomalous_confidence = torch.sigmoid( margin.clamp(-MAX_CONFIDENCE_MARGIN, MAX_CONFIDENCE_MARGIN) ) - is_anomalous = scores >= calibration.threshold + is_anomalous = anomalous_confidence >= minimum_confidence images_metadata = [] for index in range(len(model_results.original_sizes_hw)): metadata = { "anomaly_score": scores[index].item(), "anomaly_threshold": calibration.threshold, + "anomaly_scale": calibration.scale, "is_anomalous": bool(is_anomalous[index].item()), } if include_anomaly_map: @@ -273,3 +292,35 @@ def post_process_anomaly_scores( ).float(), images_metadata=images_metadata, ) + + +def resolve_anomalous_confidence( + confidence: Confidence, + recommended_parameters: Optional[RecommendedParameters], +) -> float: + """The anomalous confidence an image must reach to be called anomalous. + + Raises: + ModelInputError: If ``confidence`` is not a number in ``[0, 1]``, + ``"best"`` or ``"default"``. + """ + if isinstance(confidence, (int, float)) and not isinstance(confidence, bool): + if not 0 <= confidence <= 1: + raise ModelInputError( + message=f"Anomaly confidence must be between 0 and 1, got {confidence}.", + help_url=MODEL_INPUT_HELP_URL, + ) + confidence = float(confidence) + elif confidence not in ("best", "default"): + raise ModelInputError( + message='Anomaly confidence must be a number, "best" or "default".', + help_url=MODEL_INPUT_HELP_URL, + ) + threshold = ConfidenceFilter( + confidence=confidence, + recommended_parameters=recommended_parameters, + default_confidence=ANOMALY_DEFAULT_CONFIDENCE, + ).get_threshold(ANOMALY_CLASS_NAMES) + if isinstance(threshold, torch.Tensor): + return float(threshold[ANOMALOUS_CLASS_ID]) + return threshold diff --git a/inference_models/inference_models/models/foundad/foundad_anomaly_detection_torch.py b/inference_models/inference_models/models/foundad/foundad_anomaly_detection_torch.py index 0dcaad95f3..3e4eeb6939 100644 --- a/inference_models/inference_models/models/foundad/foundad_anomaly_detection_torch.py +++ b/inference_models/inference_models/models/foundad/foundad_anomaly_detection_torch.py @@ -8,7 +8,7 @@ from inference_models import ClassificationModel, ClassificationPrediction from inference_models.configuration import DEFAULT_DEVICE -from inference_models.entities import ColorFormat +from inference_models.entities import ColorFormat, Confidence from inference_models.errors import CorruptedModelPackageError from inference_models.models.common.anomaly_detection import ( ANOMALY_CLASS_NAMES, @@ -21,6 +21,7 @@ pre_process_anomaly_images, ) from inference_models.models.common.model_packages import get_model_package_contents +from inference_models.weights_providers.entities import RecommendedParameters # Settings of the FoundAD recipe used by Roboflow training # (https://github.com/ymxlzgy/FoundAD). @@ -152,6 +153,7 @@ def from_pretrained( cls, model_name_or_path: str, device: torch.device = DEFAULT_DEVICE, + recommended_parameters: Optional[RecommendedParameters] = None, **kwargs, ) -> "FoundADForAnomalyDetectionTorch": model_package_content = get_model_package_contents( @@ -183,17 +185,24 @@ def from_pretrained( weights_only=True, )["state"] model.load_state_dict(state) - return cls(model=model.to(device).eval(), manifest=manifest, device=device) + return cls( + model=model.to(device).eval(), + manifest=manifest, + device=device, + recommended_parameters=recommended_parameters, + ) def __init__( self, model: FoundADModel, manifest: AnomalyModelManifest, device: torch.device, + recommended_parameters: Optional[RecommendedParameters] = None, ): self._model = model self._manifest = manifest self._device = device + self.recommended_parameters = recommended_parameters @property def class_names(self) -> List[str]: @@ -233,10 +242,13 @@ def post_process( self, model_results: AnomalyRawPrediction, include_anomaly_map: bool = False, + confidence: Confidence = "best", **kwargs, ) -> ClassificationPrediction: return post_process_anomaly_scores( model_results=model_results, calibration=self._manifest.calibration, include_anomaly_map=include_anomaly_map, + confidence=confidence, + recommended_parameters=self.recommended_parameters, ) diff --git a/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py b/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py index 208f8ff3a8..2f6023083f 100644 --- a/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py +++ b/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py @@ -8,7 +8,7 @@ from inference_models import ClassificationModel, ClassificationPrediction from inference_models.configuration import DEFAULT_DEVICE -from inference_models.entities import ColorFormat +from inference_models.entities import ColorFormat, Confidence from inference_models.errors import CorruptedModelPackageError from inference_models.models.common.anomaly_detection import ( ANOMALY_CLASS_NAMES, @@ -21,6 +21,7 @@ pre_process_anomaly_images, ) from inference_models.models.common.model_packages import get_model_package_contents +from inference_models.weights_providers.entities import RecommendedParameters # Settings of the PatchCore recipe used by Roboflow training # (https://github.com/amazon-science/patchcore-inspection). @@ -162,6 +163,7 @@ def from_pretrained( cls, model_name_or_path: str, device: torch.device = DEFAULT_DEVICE, + recommended_parameters: Optional[RecommendedParameters] = None, **kwargs, ) -> "PatchCoreForAnomalyDetectionTorch": model_package_content = get_model_package_contents( @@ -193,17 +195,24 @@ def from_pretrained( image_size=manifest.config.image_size, ) model.backbone.load_state_dict(state["backbone"]) - return cls(model=model.to(device).eval(), manifest=manifest, device=device) + return cls( + model=model.to(device).eval(), + manifest=manifest, + device=device, + recommended_parameters=recommended_parameters, + ) def __init__( self, model: PatchCoreModel, manifest: AnomalyModelManifest, device: torch.device, + recommended_parameters: Optional[RecommendedParameters] = None, ): self._model = model self._manifest = manifest self._device = device + self.recommended_parameters = recommended_parameters @property def class_names(self) -> List[str]: @@ -243,10 +252,13 @@ def post_process( self, model_results: AnomalyRawPrediction, include_anomaly_map: bool = False, + confidence: Confidence = "best", **kwargs, ) -> ClassificationPrediction: return post_process_anomaly_scores( model_results=model_results, calibration=self._manifest.calibration, include_anomaly_map=include_anomaly_map, + confidence=confidence, + recommended_parameters=self.recommended_parameters, ) diff --git a/inference_models/tests/unit_tests/models/common/test_anomaly_detection.py b/inference_models/tests/unit_tests/models/common/test_anomaly_detection.py index 6b748c0edc..91bc696cbd 100644 --- a/inference_models/tests/unit_tests/models/common/test_anomaly_detection.py +++ b/inference_models/tests/unit_tests/models/common/test_anomaly_detection.py @@ -17,6 +17,7 @@ post_process_anomaly_scores, pre_process_anomaly_images, ) +from inference_models.weights_providers.entities import RecommendedParameters IMAGE_SIZE = 32 @@ -149,7 +150,11 @@ def test_score_at_threshold_is_anomalous_with_confidence_one_half() -> None: calibration = AnomalyCalibration(threshold=2.0, scale=0.5) prediction = post_process_anomaly_scores( - raw_prediction([1.0, 2.0, 1e9]), calibration, include_anomaly_map=False + raw_prediction([1.0, 2.0, 1e9]), + calibration, + include_anomaly_map=False, + confidence="default", + recommended_parameters=None, ) assert prediction.class_id.tolist() == [0, 1, 1] @@ -163,14 +168,95 @@ def test_score_at_threshold_is_anomalous_with_confidence_one_half() -> None: ] assert prediction.images_metadata[0]["anomaly_score"] == 1.0 assert prediction.images_metadata[0]["anomaly_threshold"] == 2.0 + assert prediction.images_metadata[0]["anomaly_scale"] == 0.5 assert "anomaly_map" not in prediction.images_metadata[0] +@pytest.mark.parametrize( + "confidence, expected_is_anomalous", + [ + (0.5, [False, True, True]), + (0.25, [True, True, True]), + (0.75, [False, False, True]), + ], +) +def test_confidence_moves_the_decision_and_keeps_the_saved_threshold( + confidence: float, expected_is_anomalous: list +) -> None: + calibration = AnomalyCalibration(threshold=2.0, scale=0.5) + + # Anomalous confidences: sigmoid(-0.4) ~ 0.40, sigmoid(0) = 0.5, sigmoid(2) ~ 0.88. + prediction = post_process_anomaly_scores( + raw_prediction([1.8, 2.0, 3.0]), + calibration, + include_anomaly_map=False, + confidence=confidence, + recommended_parameters=None, + ) + + assert [m["is_anomalous"] for m in prediction.images_metadata] == ( + expected_is_anomalous + ) + assert prediction.class_id.tolist() == [int(v) for v in expected_is_anomalous] + assert {m["anomaly_threshold"] for m in prediction.images_metadata} == {2.0} + + +@pytest.mark.parametrize( + "recommended_parameters, expected_is_anomalous", + [ + (None, [False, True]), + (RecommendedParameters(confidence=0.7), [False, False]), + ( + RecommendedParameters( + confidence=0.7, per_class_confidence={"anomalous": 0.3} + ), + [True, True], + ), + ], +) +def test_best_confidence_follows_model_eval_recommendation( + recommended_parameters, expected_is_anomalous: list +) -> None: + calibration = AnomalyCalibration(threshold=2.0, scale=0.5) + + prediction = post_process_anomaly_scores( + raw_prediction([1.8, 2.0]), + calibration, + include_anomaly_map=False, + confidence="best", + recommended_parameters=recommended_parameters, + ) + + assert [m["is_anomalous"] for m in prediction.images_metadata] == ( + expected_is_anomalous + ) + + +@pytest.mark.parametrize("confidence", [-0.1, 1.5, True, "high"]) +def test_invalid_confidence_is_rejected(confidence) -> None: + calibration = AnomalyCalibration(threshold=2.0, scale=0.5) + + with pytest.raises(ModelInputError, match="confidence"): + post_process_anomaly_scores( + raw_prediction([1.0]), + calibration, + include_anomaly_map=False, + confidence=confidence, + recommended_parameters=None, + ) + + def test_anomaly_map_is_returned_at_network_resolution() -> None: calibration = AnomalyCalibration(threshold=2.0, scale=0.5) raw = raw_prediction([1.0], size_hw=(10, 20)) - prediction = post_process_anomaly_scores(raw, calibration, include_anomaly_map=True) + prediction = post_process_anomaly_scores( + raw, + calibration, + include_anomaly_map=True, + confidence="default", + recommended_parameters=None, + ) anomaly_map = prediction.images_metadata[0]["anomaly_map"] # The map keeps the network's square geometry whatever the image size was. @@ -184,5 +270,9 @@ def test_non_finite_scores_are_rejected() -> None: with pytest.raises(CorruptedModelPackageError, match="non-finite"): post_process_anomaly_scores( - raw_prediction([float("nan")]), calibration, include_anomaly_map=False + raw_prediction([float("nan")]), + calibration, + include_anomaly_map=False, + confidence="default", + recommended_parameters=None, ) diff --git a/tests/inference/unit_tests/core/interfaces/http/test_legacy_http_route_forwards_anomaly_map.py b/tests/inference/unit_tests/core/interfaces/http/test_legacy_http_route_forwards_anomaly_map.py index 2cfd6fa2f5..7104d82a3e 100644 --- a/tests/inference/unit_tests/core/interfaces/http/test_legacy_http_route_forwards_anomaly_map.py +++ b/tests/inference/unit_tests/core/interfaces/http/test_legacy_http_route_forwards_anomaly_map.py @@ -25,3 +25,31 @@ def test_legacy_classification_route_forwards_include_anomaly_map( assert response.status_code == 200, response.text inference_request = model_manager.infer_from_request_sync.call_args.args[1] assert inference_request.include_anomaly_map is include_anomaly_map + + +def test_legacy_classification_route_leaves_omitted_confidence_unset( + monkeypatch, +) -> None: + interface, model_manager = _build_interface(monkeypatch, task_type="classification") + + with TestClient(interface.app) as client: + omitted = client.post( + "/dummy-dataset/1", + params={"api_key": "query-api-key", "image": "https://example.com/a.jpg"}, + ) + omitted_request = model_manager.infer_from_request_sync.call_args.args[1] + explicit = client.post( + "/dummy-dataset/1", + params={ + "api_key": "query-api-key", + "image": "https://example.com/a.jpg", + "confidence": "0.4", + }, + ) + explicit_request = model_manager.infer_from_request_sync.call_args.args[1] + + assert omitted.status_code == 200, omitted.text + assert explicit.status_code == 200, explicit.text + assert "confidence" not in omitted_request.model_fields_set + assert "confidence" in explicit_request.model_fields_set + assert explicit_request.confidence == 0.4 diff --git a/tests/inference/unit_tests/models/test_anomaly_detection.py b/tests/inference/unit_tests/models/test_anomaly_detection.py index a3d57291a5..d4e4a7f102 100644 --- a/tests/inference/unit_tests/models/test_anomaly_detection.py +++ b/tests/inference/unit_tests/models/test_anomaly_detection.py @@ -30,6 +30,7 @@ def prediction(anomalous_confidence=0.5, is_anomalous=True, anomaly_map=None): metadata = { "anomaly_score": 2.0, "anomaly_threshold": 2.0, + "anomaly_scale": 0.5, "is_anomalous": is_anomalous, } if anomaly_map is not None: @@ -94,6 +95,7 @@ def predict(): assert body["anomaly_score"] == 2.0 assert body["is_anomalous"] is True assert body["anomaly_threshold"] == 2.0 + assert body["anomaly_scale"] == 0.5 assert body["anomaly_map"]["shape"] == [10, 20] assert body["anomaly_map"]["dtype"] == "float32" # The map is served at network resolution, never at the image size. @@ -102,17 +104,24 @@ def predict(): ) -def test_request_and_sdk_forward_optional_map_to_the_model(): +def anomaly_adapter() -> InferenceModelsAnomalyDetectionAdapter: model = object.__new__(InferenceModelsAnomalyDetectionAdapter) model.class_names = CLASS_NAMES model._model = Mock(resolved_model=None) model._model.post_process.return_value = prediction() + return model + + +def png_image() -> dict: image = BytesIO() Image.new("RGB", (20, 10)).save(image, format="PNG") + return {"type": "base64", "value": base64.b64encode(image.getvalue()).decode()} + + +def test_request_and_sdk_forward_optional_map_to_the_model(): + model = anomaly_adapter() request = ClassificationInferenceRequest( - model_id="defects/1", - image={"type": "base64", "value": base64.b64encode(image.getvalue()).decode()}, - include_anomaly_map=True, + model_id="defects/1", image=png_image(), include_anomaly_map=True ) response = model.infer_from_request(request) assert response.top == "anomalous" @@ -121,7 +130,10 @@ def test_request_and_sdk_forward_optional_map_to_the_model(): bgr_images = model._model.pre_process.call_args.args[0] assert bgr_images[0].shape == (10, 20, 3) assert model._model.pre_process.call_args.kwargs["input_color_format"] == "bgr" - assert model._model.post_process.call_args.kwargs == {"include_anomaly_map": True} + assert model._model.post_process.call_args.kwargs == { + "include_anomaly_map": True, + "confidence": "best", + } assert ( InferenceConfiguration(include_anomaly_map=True).to_classification_parameters()[ "include_anomaly_map" @@ -132,3 +144,15 @@ def test_request_and_sdk_forward_optional_map_to_the_model(): "include_anomaly_map" not in InferenceConfiguration().to_classification_parameters() ) + + +def test_explicit_request_confidence_decides_the_verdict(): + model = anomaly_adapter() + # 0.4 is also the request default; sent explicitly it must not become "best". + request = ClassificationInferenceRequest( + model_id="defects/1", image=png_image(), confidence=0.4 + ) + + model.infer_from_request(request) + + assert model._model.post_process.call_args.kwargs["confidence"] == 0.4 From dc30a76ed999b3cac37f910872577bebbff12934 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 15:04:07 -0700 Subject: [PATCH 08/17] Add a confidence input to the Anomaly Detection Model block The block sends confidence (default 0.5) to the server, which decides against the saved threshold. With an anomaly_threshold override the block re-decides with the same rule relative to the override, recomputing both class confidences from the served anomaly_scale, so the two inputs compose. Co-Authored-By: Claude Opus 5.5 --- .../core_steps/common/anomaly_detection.py | 102 +++++++++++++----- .../core_steps/common/serializers_tensor.py | 6 +- .../models/roboflow/anomaly_detection/v1.py | 50 ++++++--- .../roboflow/anomaly_detection/v1_tensor.py | 61 ++++++++--- .../common/test_anomaly_detection.py | 44 +++++--- .../roboflow/anomaly_detection/test_v1.py | 25 ++++- .../anomaly_detection/test_v1_tensor.py | 28 ++++- 7 files changed, 244 insertions(+), 72 deletions(-) diff --git a/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py index bd49a6a737..5c73357acc 100644 --- a/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py +++ b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py @@ -4,7 +4,10 @@ with an ``AnomalyDetectionResponse``: the usual classification fields plus ``anomaly_score`` (raw, unbounded, larger means more anomalous), ``anomaly_threshold`` (the decision boundary saved with the model), -``is_anomalous`` and, on request, ``anomaly_map`` - the local evidence at the +``anomaly_scale`` (the anomalous confidence is +``sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale)``), +``is_anomalous`` (the anomalous confidence reached the request confidence) and, +on request, ``anomaly_map`` - the local evidence at the network input resolution, serialised as ``{"shape": [h, w], "dtype": "float32", "data": }``. @@ -21,15 +24,24 @@ ANOMALY_SCORE_KEY = "anomaly_score" ANOMALY_THRESHOLD_KEY = "anomaly_threshold" +ANOMALY_SCALE_KEY = "anomaly_scale" IS_ANOMALOUS_KEY = "is_anomalous" ANOMALY_MAP_KEY = "anomaly_map" -ANOMALY_METADATA_KEYS = (ANOMALY_SCORE_KEY, ANOMALY_THRESHOLD_KEY, IS_ANOMALOUS_KEY) +ANOMALY_METADATA_KEYS = ( + ANOMALY_SCORE_KEY, + ANOMALY_THRESHOLD_KEY, + ANOMALY_SCALE_KEY, + IS_ANOMALOUS_KEY, +) ANOMALY_MAP_DTYPE = "float32" _ANOMALY_MAP_NUMPY_DTYPE = np.dtype(" np.ndarray: @@ -114,9 +126,9 @@ def require_anomaly_response(prediction: Dict[str, Any], model_id: str) -> None: model_id (str): The model that produced it, for the error message. Raises: - ValueError: If ``anomaly_score``, ``anomaly_threshold`` or - ``is_anomalous`` is missing - the model is a plain classifier, not - a PatchCore or FoundAD anomaly model. + ValueError: If ``anomaly_score``, ``anomaly_threshold``, + ``anomaly_scale`` or ``is_anomalous`` is missing - the model is a + plain classifier, not a PatchCore or FoundAD anomaly model. """ missing = [key for key in ANOMALY_METADATA_KEYS if key not in prediction] if missing: @@ -146,36 +158,78 @@ def validate_threshold_override(threshold: Optional[float]) -> Optional[float]: return value +def anomalous_confidence(score: float, threshold: float, scale: float) -> float: + """``sigmoid((score - threshold) / scale)``, exactly 0.5 at ``threshold``.""" + margin = (score - threshold) / scale + margin = max(-MAX_CONFIDENCE_MARGIN, min(MAX_CONFIDENCE_MARGIN, margin)) + return 1.0 / (1.0 + math.exp(-margin)) + + +def redecide_at_threshold( + verdict: Dict[str, Any], threshold: float, confidence: float +) -> float: + """Re-decide a verdict against a caller-supplied threshold, in place. + + The rule is the server's with the override in place of the saved threshold: + ``is_anomalous`` becomes ``sigmoid((anomaly_score - threshold) / + anomaly_scale) >= confidence`` and ``anomaly_threshold`` reports the + override. + + Args: + verdict (Dict[str, Any]): Holds ``anomaly_score`` and ``anomaly_scale``. + threshold (float): The override, already validated. + confidence (float): The anomalous confidence an image must reach. + + Returns: + The anomalous confidence relative to the override. + """ + anomalous = anomalous_confidence( + score=float(verdict[ANOMALY_SCORE_KEY]), + threshold=threshold, + scale=float(verdict[ANOMALY_SCALE_KEY]), + ) + verdict[ANOMALY_THRESHOLD_KEY] = threshold + verdict[IS_ANOMALOUS_KEY] = anomalous >= confidence + return anomalous + + def apply_threshold_override( - prediction: Dict[str, Any], threshold: Optional[float] + prediction: Dict[str, Any], threshold: Optional[float], confidence: float ) -> Dict[str, Any]: - """Re-decide the verdict against a caller-supplied threshold, in place. + """Re-decide a dumped anomaly detection response against an override, in place. - ``is_anomalous`` becomes ``anomaly_score >= threshold`` and - ``anomaly_threshold`` reports the override. ``predictions`` is reordered so - the decided class comes first, and ``top`` / ``confidence`` follow it. The - per-class confidences are left as served: they are a sigmoid centred on the - threshold SAVED with the model, so under an override 0.5 no longer marks the - decision boundary. With ``threshold`` ``None`` the response is unchanged. + See ``redecide_at_threshold`` for the verdict. ``predictions`` is rebuilt with + both class confidences relative to the override, the decided class first, and + ``top`` / ``confidence`` follow it. With ``threshold`` ``None`` the response + is unchanged: the server already decided against the saved threshold with the + same ``confidence``. Args: prediction (Dict[str, Any]): A dumped anomaly detection response. threshold (Optional[float]): The override, already validated. + confidence (float): The anomalous confidence an image must reach. Returns: The same dict. """ if threshold is None: return prediction - is_anomalous = float(prediction[ANOMALY_SCORE_KEY]) >= threshold - prediction[ANOMALY_THRESHOLD_KEY] = threshold - prediction[IS_ANOMALOUS_KEY] = is_anomalous - decided_class = ANOMALOUS_CLASS_NAME if is_anomalous else NORMAL_CLASS_NAME - class_predictions: List[Dict[str, Any]] = list(prediction.get("predictions") or []) - decided = [p for p in class_predictions if p.get("class") == decided_class] - others = [p for p in class_predictions if p.get("class") != decided_class] - if decided: - prediction["predictions"] = decided + others - prediction["top"] = decided[0]["class"] - prediction["confidence"] = decided[0]["confidence"] + anomalous = redecide_at_threshold(prediction, threshold, confidence) + class_predictions: List[Dict[str, Any]] = [ + { + "class_id": 0, + "class": NORMAL_CLASS_NAME, + "confidence": round(1 - anomalous, 4), + }, + { + "class_id": 1, + "class": ANOMALOUS_CLASS_NAME, + "confidence": round(anomalous, 4), + }, + ] + if prediction[IS_ANOMALOUS_KEY]: + class_predictions.reverse() + prediction["predictions"] = class_predictions + prediction["top"] = class_predictions[0]["class"] + prediction["confidence"] = class_predictions[0]["confidence"] return prediction diff --git a/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py b/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py index 87e722e20f..65f1c29fe8 100644 --- a/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py +++ b/workflows/roboflow_workflows/core_steps/common/serializers_tensor.py @@ -606,9 +606,9 @@ def _serialise_classification_model_style( key=lambda item: item["confidence"], reverse=True, ) - # An anomaly model's verdict is decided against a threshold on the raw - # score (possibly overridden by the block), not by the larger confidence: - # the decided class leads, as the numpy anomaly block orders it. + # An anomaly model's verdict is decided by the request confidence, not by + # the larger class confidence: the decided class leads, as the numpy + # anomaly block orders it. if IS_ANOMALOUS_KEY in image_metadata: individual_classes_predictions = _decided_anomaly_class_first( individual_classes_predictions, diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py index 0b9a4dba4d..8763431c5a 100644 --- a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py @@ -36,11 +36,13 @@ BOOLEAN_KIND, CLASSIFICATION_PREDICTION_KIND, FLOAT_KIND, + FLOAT_ZERO_TO_ONE_KIND, IMAGE_KIND, INFERENCE_ID_KIND, NUMPY_ARRAY_KIND, ROBOFLOW_MODEL_ID_KIND, ROBOFLOW_PROJECT_KIND, + FloatZeroToOne, ImageInputField, RoboflowModelField, Selector, @@ -72,12 +74,13 @@ scores a new image by how far it departs from that. The block reports: - `predictions`: the classification-style response, with `top` being `normal` - or `anomalous` and the two class confidences. The anomalous confidence is a - sigmoid centred on the threshold saved with the model, so 0.5 marks the - decision boundary. + or `anomalous` and the two class confidences. The anomalous confidence is + `sigmoid((anomaly_score - anomaly_threshold) / scale)`, with the scale saved + with the model, so it is exactly 0.5 at the threshold. - `anomaly_score`: the raw image score (unbounded, larger means more anomalous). -- `anomaly_threshold`: the decision boundary in effect for this image. -- `is_anomalous`: `anomaly_score >= anomaly_threshold`. +- `anomaly_threshold`: the threshold in effect for this image (saved with the + model, or the override). +- `is_anomalous`: whether the anomalous confidence reached `confidence`. - `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` array at the network's input resolution (square), in the same units as `anomaly_score`. The map covers the whole image as the network saw it, so @@ -85,10 +88,12 @@ Heatmap Visualization** block to draw it. Set `include_anomaly_map` to `False` to skip the map when only the verdict is needed. -`anomaly_threshold` lets you tune the operating point without retraining: -lower it to catch more defects at the cost of more false alarms, raise it to -do the opposite. The override re-decides `is_anomalous` and `top`; the class -confidences stay relative to the saved threshold. +`confidence` sets the operating point without retraining: the default 0.5 +reproduces the decision calibrated during training; lower it to catch more +defects at the cost of more false alarms, raise it to do the opposite. +`anomaly_threshold` replaces the saved threshold on the raw score; the class +confidences are then recomputed relative to it and `confidence` applies to them +the same way. You will need to set your Roboflow API key in your Inference environment to use this block. To learn more about setting your Roboflow API key, [refer to the @@ -130,9 +135,17 @@ class BlockManifest(WorkflowBlockManifest): description="Return the local anomaly map at the network input resolution. Disable it when only the verdict and score are needed.", examples=[True, "$inputs.include_anomaly_map"], ) + confidence: Union[ + FloatZeroToOne, + Selector(kind=[FLOAT_ZERO_TO_ONE_KIND]), + ] = Field( + default=0.5, + description="Minimum anomalous confidence for an anomalous verdict. 0.5 reproduces the decision calibrated during training; lower it to flag more images as anomalous.", + examples=[0.5, "$inputs.confidence"], + ) anomaly_threshold: Union[Optional[float], Selector(kind=[FLOAT_KIND])] = Field( default=None, - description="Decision threshold on the raw anomaly score, replacing the one saved with the model. Lower it to flag more images as anomalous. Leave empty to use the saved threshold.", + description="Threshold on the raw anomaly score, replacing the one saved with the model; the anomalous confidence is recomputed relative to it. Leave empty to use the saved threshold.", examples=[1.25, "$inputs.anomaly_threshold"], ) disable_active_learning: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( @@ -221,6 +234,7 @@ def run( images: Batch[WorkflowImageData], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -231,6 +245,7 @@ def run( images=images, model_id=model_id, include_anomaly_map=include_anomaly_map, + confidence=confidence, anomaly_threshold=threshold_override, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, @@ -240,6 +255,7 @@ def run( images=images, model_id=model_id, include_anomaly_map=include_anomaly_map, + confidence=confidence, anomaly_threshold=threshold_override, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, @@ -254,6 +270,7 @@ def run_locally( images: Batch[WorkflowImageData], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -263,13 +280,11 @@ def run_locally( model_id=model_id, api_key=self._api_key, ) - # The request schema needs a confidence although an anomaly response never - # filters on it: the verdict comes from the threshold on the raw score. predictions = self._model_manager.run_classification( model_id=model_id, images=inference_images, api_key=self._api_key, - confidence="default", + confidence=confidence, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, include_anomaly_map=include_anomaly_map, @@ -278,6 +293,7 @@ def run_locally( predictions=predictions, images=images, model_id=model_id, + confidence=confidence, anomaly_threshold=anomaly_threshold, ) @@ -286,6 +302,7 @@ def run_remotely( images: Batch[Optional[WorkflowImageData]], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -304,6 +321,7 @@ def run_remotely( client_config = InferenceConfiguration( api_key_transport=WORKFLOWS_REMOTE_API_KEY_TRANSPORT, include_anomaly_map=include_anomaly_map, + confidence_threshold=confidence, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, max_batch_size=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, @@ -322,6 +340,7 @@ def run_remotely( predictions=predictions, images=images, model_id=model_id, + confidence=confidence, anomaly_threshold=anomaly_threshold, ) @@ -330,6 +349,7 @@ def _post_process_result( images: Batch[WorkflowImageData], predictions: List[dict], model_id: str, + confidence: float, anomaly_threshold: Optional[float], ) -> BlockResult: predictions = attach_prediction_type_info( @@ -344,7 +364,9 @@ def _post_process_result( anomaly_map: Optional[np.ndarray] = coerce_anomaly_map( prediction.pop(ANOMALY_MAP_KEY, None) ) - prediction = apply_threshold_override(prediction, anomaly_threshold) + prediction = apply_threshold_override( + prediction, anomaly_threshold, confidence + ) prediction[PARENT_ID_KEY] = image.parent_metadata.parent_id prediction[ROOT_PARENT_ID_KEY] = ( image.workflow_root_ancestor_metadata.parent_id diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py index 54f566c34d..292f1f1ff7 100644 --- a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py @@ -4,7 +4,8 @@ ``inference_models.ClassificationPrediction`` objects under ``TENSOR_NATIVE_CLASSIFICATION_PREDICTION_KIND`` instead of the legacy classification response dict. ``images_metadata[0]`` carries the anomaly -verdict (``anomaly_score``, ``anomaly_threshold``, ``is_anomalous``) next to +verdict (``anomaly_score``, ``anomaly_threshold``, ``anomaly_scale``, +``is_anomalous``) next to the usual producer contract, and ``serialise_native_classification`` writes those keys into the flag-off dict shape. @@ -28,12 +29,14 @@ ANOMALOUS_CLASS_NAME, ANOMALY_MAP_KEY, ANOMALY_METADATA_KEYS, + ANOMALY_SCALE_KEY, ANOMALY_SCORE_KEY, ANOMALY_THRESHOLD_KEY, IS_ANOMALOUS_KEY, NORMAL_CLASS_NAME, apply_threshold_override, coerce_anomaly_map, + redecide_at_threshold, require_anomaly_response, validate_threshold_override, ) @@ -68,11 +71,13 @@ from roboflow_workflows.execution_engine.entities.types import ( BOOLEAN_KIND, FLOAT_KIND, + FLOAT_ZERO_TO_ONE_KIND, IMAGE_KIND, INFERENCE_ID_KIND, NUMPY_ARRAY_KIND, ROBOFLOW_MODEL_ID_KIND, ROBOFLOW_PROJECT_KIND, + FloatZeroToOne, ImageInputField, RoboflowModelField, Selector, @@ -105,12 +110,13 @@ scores a new image by how far it departs from that. The block reports: - `predictions`: the classification-style response, with `top` being `normal` - or `anomalous` and the two class confidences. The anomalous confidence is a - sigmoid centred on the threshold saved with the model, so 0.5 marks the - decision boundary. + or `anomalous` and the two class confidences. The anomalous confidence is + `sigmoid((anomaly_score - anomaly_threshold) / scale)`, with the scale saved + with the model, so it is exactly 0.5 at the threshold. - `anomaly_score`: the raw image score (unbounded, larger means more anomalous). -- `anomaly_threshold`: the decision boundary in effect for this image. -- `is_anomalous`: `anomaly_score >= anomaly_threshold`. +- `anomaly_threshold`: the threshold in effect for this image (saved with the + model, or the override). +- `is_anomalous`: whether the anomalous confidence reached `confidence`. - `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` array at the network's input resolution (square), in the same units as `anomaly_score`. The map covers the whole image as the network saw it, so @@ -118,10 +124,12 @@ Heatmap Visualization** block to draw it. Set `include_anomaly_map` to `False` to skip the map when only the verdict is needed. -`anomaly_threshold` lets you tune the operating point without retraining: -lower it to catch more defects at the cost of more false alarms, raise it to -do the opposite. The override re-decides `is_anomalous` and `top`; the class -confidences stay relative to the saved threshold. +`confidence` sets the operating point without retraining: the default 0.5 +reproduces the decision calibrated during training; lower it to catch more +defects at the cost of more false alarms, raise it to do the opposite. +`anomaly_threshold` replaces the saved threshold on the raw score; the class +confidences are then recomputed relative to it and `confidence` applies to them +the same way. You will need to set your Roboflow API key in your Inference environment to use this block. To learn more about setting your Roboflow API key, [refer to the @@ -163,9 +171,17 @@ class BlockManifest(WorkflowBlockManifest): description="Return the local anomaly map at the network input resolution. Disable it when only the verdict and score are needed.", examples=[True, "$inputs.include_anomaly_map"], ) + confidence: Union[ + FloatZeroToOne, + Selector(kind=[FLOAT_ZERO_TO_ONE_KIND]), + ] = Field( + default=0.5, + description="Minimum anomalous confidence for an anomalous verdict. 0.5 reproduces the decision calibrated during training; lower it to flag more images as anomalous.", + examples=[0.5, "$inputs.confidence"], + ) anomaly_threshold: Union[Optional[float], Selector(kind=[FLOAT_KIND])] = Field( default=None, - description="Decision threshold on the raw anomaly score, replacing the one saved with the model. Lower it to flag more images as anomalous. Leave empty to use the saved threshold.", + description="Threshold on the raw anomaly score, replacing the one saved with the model; the anomalous confidence is recomputed relative to it. Leave empty to use the saved threshold.", examples=[1.25, "$inputs.anomaly_threshold"], ) disable_active_learning: Union[bool, Selector(kind=[BOOLEAN_KIND])] = Field( @@ -257,6 +273,7 @@ def run( images: Batch[WorkflowImageData], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -267,6 +284,7 @@ def run( images=images, model_id=model_id, include_anomaly_map=include_anomaly_map, + confidence=confidence, anomaly_threshold=threshold_override, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, @@ -276,6 +294,7 @@ def run( images=images, model_id=model_id, include_anomaly_map=include_anomaly_map, + confidence=confidence, anomaly_threshold=threshold_override, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, @@ -290,6 +309,7 @@ def run_locally( images: Batch[WorkflowImageData], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -310,6 +330,7 @@ def run_locally( images=model_inputs, input_color_format=image_color_format, include_anomaly_map=include_anomaly_map, + confidence=confidence, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, ) @@ -330,7 +351,14 @@ def run_locally( ): require_anomaly_response(image_model_metadata, model_id=model_id) verdict = {key: image_model_metadata[key] for key in ANOMALY_METADATA_KEYS} - verdict = apply_threshold_override(verdict, anomaly_threshold) + confidence_row = batched_prediction.confidence[index : index + 1] + if anomaly_threshold is not None: + anomalous = redecide_at_threshold( + verdict, anomaly_threshold, confidence + ) + confidence_row = torch.tensor( + [[1 - anomalous, anomalous]], dtype=torch.float32 + ) anomaly_map = coerce_anomaly_map(image_model_metadata.get(ANOMALY_MAP_KEY)) inference_id = str(uuid.uuid4()) image_metadata = _build_image_metadata( @@ -342,9 +370,7 @@ def run_locally( ) prediction = ClassificationPrediction( class_id=_decided_class_id(verdict).to(WORKFLOWS_IMAGE_TENSOR_DEVICE), - confidence=batched_prediction.confidence[index : index + 1].to( - WORKFLOWS_IMAGE_TENSOR_DEVICE - ), + confidence=confidence_row.to(WORKFLOWS_IMAGE_TENSOR_DEVICE), images_metadata=[image_metadata], ) results.append( @@ -363,6 +389,7 @@ def run_remotely( images: Batch[Optional[WorkflowImageData]], model_id: str, include_anomaly_map: bool, + confidence: float, anomaly_threshold: Optional[float], disable_active_learning: Optional[bool], active_learning_target_dataset: Optional[str], @@ -381,6 +408,7 @@ def run_remotely( client_config = InferenceConfiguration( api_key_transport=WORKFLOWS_REMOTE_API_KEY_TRANSPORT, include_anomaly_map=include_anomaly_map, + confidence_threshold=confidence, disable_active_learning=disable_active_learning, active_learning_target_dataset=active_learning_target_dataset, max_batch_size=WORKFLOWS_REMOTE_EXECUTION_MAX_STEP_BATCH_SIZE, @@ -399,7 +427,7 @@ def run_remotely( for image, response in zip(images, responses): require_anomaly_response(response, model_id=model_id) anomaly_map = coerce_anomaly_map(response.pop(ANOMALY_MAP_KEY, None)) - response = apply_threshold_override(response, anomaly_threshold) + response = apply_threshold_override(response, anomaly_threshold, confidence) verdict = {key: response[key] for key in ANOMALY_METADATA_KEYS} inference_id = response.get(INFERENCE_ID_KEY) or str(uuid.uuid4()) prediction = _native_prediction_from_response( @@ -468,6 +496,7 @@ def _build_image_metadata( # both classes, whatever their confidence. ANOMALY_SCORE_KEY: float(verdict[ANOMALY_SCORE_KEY]), ANOMALY_THRESHOLD_KEY: float(verdict[ANOMALY_THRESHOLD_KEY]), + ANOMALY_SCALE_KEY: float(verdict[ANOMALY_SCALE_KEY]), IS_ANOMALOUS_KEY: bool(verdict[IS_ANOMALOUS_KEY]), } if inference_time is not None: diff --git a/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py index 6ec30d1006..a88cbc6ce3 100644 --- a/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py +++ b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py @@ -81,38 +81,58 @@ def test_validate_threshold_override_keeps_none_and_casts_ints() -> None: assert validate_threshold_override(2) == 2.0 -def test_apply_threshold_override_re_decides_and_reorders_classes() -> None: - # given: the model said normal at its saved threshold of 2.0 - response = { +def _normal_response() -> dict: + # The model said normal at its saved threshold of 2.0 with scale 0.5. + return { "predictions": [ - {"class": "normal", "class_id": 0, "confidence": 0.73}, - {"class": "anomalous", "class_id": 1, "confidence": 0.27}, + {"class": "normal", "class_id": 0, "confidence": 0.7311}, + {"class": "anomalous", "class_id": 1, "confidence": 0.2689}, ], "top": "normal", - "confidence": 0.73, + "confidence": 0.7311, "anomaly_score": 1.5, "anomaly_threshold": 2.0, + "anomaly_scale": 0.5, "is_anomalous": False, } - # when: lowering the threshold below the score flips the verdict - result = apply_threshold_override(response, 1.0) + +def test_apply_threshold_override_re_decides_and_recomputes_confidences() -> None: + # given + response = _normal_response() + + # when: relative to 1.0 the anomalous confidence is sigmoid(1) ~ 0.7311 + result = apply_threshold_override(response, 1.0, confidence=0.5) # then assert result is response assert result["is_anomalous"] is True assert result["anomaly_threshold"] == 1.0 assert result["top"] == "anomalous" - assert result["confidence"] == 0.27 - assert [p["class"] for p in result["predictions"]] == ["anomalous", "normal"] + assert result["confidence"] == 0.7311 + assert result["predictions"] == [ + {"class_id": 1, "class": "anomalous", "confidence": 0.7311}, + {"class_id": 0, "class": "normal", "confidence": 0.2689}, + ] + + +def test_confidence_applies_relative_to_the_threshold_override() -> None: + # when: the same override, but the anomalous confidence must reach 0.8 + result = apply_threshold_override(_normal_response(), 1.0, confidence=0.8) + + # then + assert result["is_anomalous"] is False + assert result["anomaly_threshold"] == 1.0 + assert result["top"] == "normal" + assert result["confidence"] == 0.2689 def test_apply_threshold_override_without_override_is_a_no_op() -> None: # given - response = {"anomaly_score": 1.5, "anomaly_threshold": 2.0, "is_anomalous": False} + response = _normal_response() # when - result = apply_threshold_override(dict(response), None) + result = apply_threshold_override(_normal_response(), None, confidence=0.1) # then assert result == response diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py index b4a19d643f..c3e15ecfcf 100644 --- a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py @@ -50,6 +50,7 @@ def _response( "confidence": classes[0]["confidence"], "anomaly_score": score, "anomaly_threshold": threshold, + "anomaly_scale": 0.5, "is_anomalous": is_anomalous, } if anomaly_map is not None: @@ -87,6 +88,7 @@ def _run( kwargs = dict( model_id="ws/anomaly/1", include_anomaly_map=True, + confidence=0.5, anomaly_threshold=None, disable_active_learning=True, active_learning_target_dataset=None, @@ -118,6 +120,7 @@ def test_manifest_validation_when_minimal_config_is_given( type=TYPE, name="anomaly", images="$inputs.image", model_id="ws/anomaly/1" ) assert result.include_anomaly_map is True + assert result.confidence == 0.5 assert result.anomaly_threshold is None @@ -126,6 +129,8 @@ def test_manifest_validation_when_minimal_config_is_given( [ ("include_anomaly_map", "$inputs.include_map"), ("include_anomaly_map", False), + ("confidence", "$inputs.confidence"), + ("confidence", 0.3), ("anomaly_threshold", "$inputs.threshold"), ("anomaly_threshold", 1.25), ("anomaly_threshold", None), @@ -168,6 +173,7 @@ def test_manifest_validation_when_required_field_is_missing(field: str) -> None: "field, value", [ ("anomaly_threshold", "high"), + ("confidence", 1.5), ("include_anomaly_map", "maybe"), ("model_id", None), ], @@ -213,6 +219,7 @@ def test_run_locally_forwards_the_map_flag_and_decodes_the_payload() -> None: # then provider.add_model.assert_called_once_with(model_id="ws/anomaly/1", api_key="key") assert provider.calls[0]["include_anomaly_map"] is True + assert provider.calls[0]["confidence"] == 0.5 assert provider.calls[0]["images"] == [ {"type": "numpy_object", "value": provider.calls[0]["images"][0]["value"]} ] @@ -253,15 +260,30 @@ def test_run_locally_applies_the_threshold_override() -> None: # when result = _run(block, [_image()], anomaly_threshold=1.0) - # then + # then: relative to 1.0 the anomalous confidence is sigmoid(1) ~ 0.7311 assert result[0]["is_anomalous"] is True assert result[0]["anomaly_threshold"] == 1.0 assert result[0]["anomaly_score"] == 1.5 assert result[0]["predictions"]["top"] == "anomalous" + assert result[0]["predictions"]["confidence"] == 0.7311 assert result[0]["predictions"]["anomaly_threshold"] == 1.0 assert result[0]["predictions"]["is_anomalous"] is True +def test_run_locally_sends_confidence_and_applies_it_under_an_override() -> None: + # given + provider = _StubProvider([_response(score=1.5, threshold=2.0)]) + block = _block(provider, StepExecutionMode.LOCAL) + + # when: the anomalous confidence relative to 1.0 (~0.73) misses 0.8 + result = _run(block, [_image()], confidence=0.8, anomaly_threshold=1.0) + + # then + assert provider.calls[0]["confidence"] == 0.8 + assert result[0]["is_anomalous"] is False + assert result[0]["predictions"]["top"] == "normal" + + def test_run_rejects_a_plain_classifier_response() -> None: # given plain = { @@ -314,6 +336,7 @@ def test_run_remotely_configures_the_client_and_accepts_both_map_forms( # then configuration = client.configure.call_args.kwargs["inference_configuration"] assert configuration.include_anomaly_map is True + assert configuration.confidence_threshold == 0.5 assert configuration.disable_active_learning is True assert configuration.source == "workflow-execution" client.infer.assert_called_once() diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py index f6f7a2f8ee..a2839a39e7 100644 --- a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py @@ -40,6 +40,7 @@ def _batched(scores: List[float], threshold: float = 2.0, with_map: bool = True) entry: Dict[str, Any] = { "anomaly_score": score, "anomaly_threshold": threshold, + "anomaly_scale": 0.5, "is_anomalous": score >= threshold, } if with_map: @@ -77,6 +78,7 @@ def _run(block, images: List[WorkflowImageData], **overrides): kwargs = dict( model_id="ws/anomaly/1", include_anomaly_map=True, + confidence=0.5, anomaly_threshold=None, disable_active_learning=True, active_learning_target_dataset=None, @@ -106,6 +108,7 @@ def _remote_response(score: float = 3.0, threshold: float = 2.0) -> dict: "confidence": classes[0]["confidence"], "anomaly_score": score, "anomaly_threshold": threshold, + "anomaly_scale": 0.5, "is_anomalous": is_anomalous, "anomaly_map": MAP.copy(), } @@ -163,13 +166,32 @@ def test_run_locally_without_map_and_with_override() -> None: # then assert provider.calls[0]["include_anomaly_map"] is False + assert provider.calls[0]["confidence"] == 0.5 assert result[0]["anomaly_map"] is None assert result[0]["is_anomalous"] is True assert result[0]["anomaly_threshold"] == 1.0 assert result[0]["predictions"].class_id.tolist() == [1] + # Both confidences are recomputed relative to the override: sigmoid(1) ~ 0.7311. + assert result[0]["predictions"].confidence[0].tolist() == pytest.approx( + [0.2689, 0.7311], abs=1e-4 + ) assert result[0]["predictions"].images_metadata[0]["is_anomalous"] is True +def test_run_locally_applies_confidence_under_an_override() -> None: + # given + provider = _StubProvider(_batched([1.5], with_map=False)) + block = _block(provider, StepExecutionMode.LOCAL) + + # when: the anomalous confidence relative to 1.0 (~0.73) misses 0.8 + result = _run(block, [_image()], confidence=0.8, anomaly_threshold=1.0) + + # then + assert provider.calls[0]["confidence"] == 0.8 + assert result[0]["is_anomalous"] is False + assert result[0]["predictions"].class_id.tolist() == [0] + + def test_run_locally_rejects_a_plain_classifier() -> None: # given: a classifier returns no per-image anomaly metadata plain = ClassificationPrediction( @@ -191,7 +213,7 @@ def test_serialised_local_prediction_matches_the_numpy_dict_shape() -> None: # when serialised = serialise_native_classification(result[0]["predictions"]) - # then: the override decided "anomalous" although its confidence is lower + # then: the override decided "anomalous" with confidences relative to it assert list(serialised.keys()) == [ "inference_id", "time", @@ -201,6 +223,7 @@ def test_serialised_local_prediction_matches_the_numpy_dict_shape() -> None: "confidence", "anomaly_score", "anomaly_threshold", + "anomaly_scale", "is_anomalous", "prediction_type", "parent_id", @@ -209,7 +232,7 @@ def test_serialised_local_prediction_matches_the_numpy_dict_shape() -> None: assert serialised["image"] == {"width": 4, "height": 3} assert [p["class"] for p in serialised["predictions"]] == ["anomalous", "normal"] assert serialised["top"] == "anomalous" - assert serialised["confidence"] == 0.25 + assert serialised["confidence"] == pytest.approx(0.7311, abs=1e-4) assert serialised["anomaly_score"] == 1.5 assert serialised["anomaly_threshold"] == 1.0 assert serialised["is_anomalous"] is True @@ -229,6 +252,7 @@ def test_run_remotely_rebuilds_a_native_prediction() -> None: # then configuration = client.configure.call_args.kwargs["inference_configuration"] assert configuration.include_anomaly_map is True + assert configuration.confidence_threshold == 0.5 prediction = result[0]["predictions"] assert isinstance(prediction, ClassificationPrediction) assert prediction.class_id.tolist() == [1] From aa13bc1062e057e2147af652a5e211ddb18a6a38 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 15:42:48 -0700 Subject: [PATCH 09/17] Apply the anomaly confidence contract in the new inference server The inference_server legacy routes and translation now leave an omitted classification confidence unset, so anomaly models resolve it to the model-eval value instead of the 0.4 request default; the anomaly response and the typed serializers carry anomaly_scale. The classification action declares its confidence param, and the workflows provider accepts include_anomaly_map like the protocol requires. Co-Authored-By: Claude Opus 5.5 --- .../registry_defaults.py | 66 +++++++++++++++---- .../serializers_typed.py | 8 ++- .../tests/unit_tests/test_model_manager.py | 25 +++---- .../unit_tests/test_registry_defaults.py | 44 ++++++++++--- .../unit_tests/test_serializers_typed.py | 9 +++ .../handlers/classification/introspection.py | 8 ++- .../inference_server/legacy/entities.py | 11 +++- .../inference_server/legacy/router.py | 16 +++-- .../inference_server/legacy/translation.py | 9 ++- .../workflows/models_provider.py | 7 +- .../legacy/test_translation_classification.py | 25 ++++++- .../unit_tests/test_handler_classification.py | 2 + .../workflows/test_models_provider.py | 27 +++++++- 13 files changed, 211 insertions(+), 46 deletions(-) diff --git a/inference_model_manager/inference_model_manager/registry_defaults.py b/inference_model_manager/inference_model_manager/registry_defaults.py index 0be440af0e..f64e38e6f5 100644 --- a/inference_model_manager/inference_model_manager/registry_defaults.py +++ b/inference_model_manager/inference_model_manager/registry_defaults.py @@ -188,8 +188,16 @@ def _p(*dicts: dict) -> dict: _P_SAM2_SEGMENT = _p( _P_SAM_SEGMENT_COMMON, { - "load_from_mask_input_cache": {"type": "bool", "required": False, "default": False}, - "save_to_mask_input_cache": {"type": "bool", "required": False, "default": False}, + "load_from_mask_input_cache": { + "type": "bool", + "required": False, + "default": False, + }, + "save_to_mask_input_cache": { + "type": "bool", + "required": False, + "default": False, + }, }, ) @@ -413,6 +421,7 @@ def _unpack_config(cfg: tuple) -> tuple: _p( _P_IMAGES, {"include_anomaly_map": {"type": "bool", "required": False}}, + _P_CONFIDENCE, _K_PREPROC, ), "validate_images_required", @@ -639,7 +648,13 @@ def _unpack_config(cfg: tuple) -> tuple: True, _p( _P_VLM_PROMPT, - {"enable_thinking": {"type": "bool", "required": False, "default": False}}, + { + "enable_thinking": { + "type": "bool", + "required": False, + "default": False, + } + }, ), "validate_images_and_prompt", "serialize_text", @@ -653,7 +668,13 @@ def _unpack_config(cfg: tuple) -> tuple: True, _p( _P_VLM_PROMPT, - {"images_to_single_prompt": {"type": "bool", "required": False, "default": True}}, + { + "images_to_single_prompt": { + "type": "bool", + "required": False, + "default": True, + } + }, ), "validate_images_and_prompt", "serialize_text", @@ -677,7 +698,11 @@ def _unpack_config(cfg: tuple) -> tuple: "caption", "caption_image", True, - _p(_P_IMAGES, {"granularity": {"type": "str", "required": False}}, _P_MAX_NEW_TOKENS), + _p( + _P_IMAGES, + {"granularity": {"type": "str", "required": False}}, + _P_MAX_NEW_TOKENS, + ), "validate_images_required", "serialize_text", "roboflow-text-v1", @@ -829,7 +854,11 @@ def _unpack_config(cfg: tuple) -> tuple: "caption", "caption", True, - _p(_P_IMAGES, {"length": {"type": "str", "required": False}}, _P_MAX_NEW_TOKENS), + _p( + _P_IMAGES, + {"length": {"type": "str", "required": False}}, + _P_MAX_NEW_TOKENS, + ), "validate_images_required", "serialize_text", "roboflow-text-v1", @@ -921,8 +950,16 @@ def _unpack_config(cfg: tuple) -> tuple: "image": {"type": "image", "required": True}, "bboxes": {"type": "list", "required": True}, "state_dict": {"type": "dict", "required": False}, - "clear_old_points": {"type": "bool", "required": False, "default": True}, - "normalize_coords": {"type": "bool", "required": False, "default": True}, + "clear_old_points": { + "type": "bool", + "required": False, + "default": True, + }, + "normalize_coords": { + "type": "bool", + "required": False, + "default": True, + }, "frame_idx": {"type": "int", "required": False, "default": 0}, }, "validate_passthrough", @@ -1074,9 +1111,16 @@ def lazy_register_by_names(mro_names: list[str]) -> None: continue placeholder = type(name, (), {}) for cfg in config: - action_name, method, default, params, val_name, ser_name, resp_type, aliases = ( - _unpack_config(cfg) - ) + ( + action_name, + method, + default, + params, + val_name, + ser_name, + resp_type, + aliases, + ) = _unpack_config(cfg) registry.register( placeholder, action_name, diff --git a/inference_model_manager/inference_model_manager/serializers_typed.py b/inference_model_manager/inference_model_manager/serializers_typed.py index 573823539d..5f3a7c57d7 100644 --- a/inference_model_manager/inference_model_manager/serializers_typed.py +++ b/inference_model_manager/inference_model_manager/serializers_typed.py @@ -98,7 +98,13 @@ def serialize_detections_compact(output: Any, model: Any) -> dict: # Classification (single-label) # --------------------------------------------------------------------------- -_ANOMALY_FIELDS = ("anomaly_score", "anomaly_threshold", "is_anomalous", "anomaly_map") +_ANOMALY_FIELDS = ( + "anomaly_score", + "anomaly_threshold", + "anomaly_scale", + "is_anomalous", + "anomaly_map", +) def _anomaly_fields(output: Any, index: int = 0) -> dict: diff --git a/inference_model_manager/tests/unit_tests/test_model_manager.py b/inference_model_manager/tests/unit_tests/test_model_manager.py index 9d773f9922..93f855d88a 100644 --- a/inference_model_manager/tests/unit_tests/test_model_manager.py +++ b/inference_model_manager/tests/unit_tests/test_model_manager.py @@ -565,11 +565,19 @@ def forward(self, pre_processed_images, **kwargs): original_sizes_hw=[(8, 8), (8, 8)], ) - def post_process(self, model_results, include_anomaly_map=False, **kwargs): + def post_process( + self, + model_results, + include_anomaly_map=False, + confidence="best", + **kwargs, + ): return post_process_anomaly_scores( model_results, calibration=AnomalyCalibration(threshold=2.0, scale=1.0), include_anomaly_map=include_anomaly_map, + confidence=confidence, + recommended_parameters=None, ) mm = ModelManager() @@ -1100,9 +1108,7 @@ def test_unload_releases_cuda_cache(self, monkeypatch): import inference_model_manager.model_manager as mm_mod calls = [] - monkeypatch.setattr( - mm_mod, "_try_release_cuda_memory", lambda: calls.append(1) - ) + monkeypatch.setattr(mm_mod, "_try_release_cuda_memory", lambda: calls.append(1)) mm = ModelManager() _patch_create_backend(mm, {}) mm.load("model-a", api_key="") @@ -1225,8 +1231,7 @@ def load_model(name): errors.append(exc) threads = [ - threading.Thread(target=load_model, args=(f"new{i}",)) - for i in range(n_new) + threading.Thread(target=load_model, args=(f"new{i}",)) for i in range(n_new) ] for t in threads: t.start() @@ -1465,11 +1470,10 @@ class TestStreamPipeline: @pytest.fixture def segmentation_manager(self, monkeypatch): + from inference_model_manager.registry_defaults import lazy_register from inference_models.models.base.instance_segmentation import ( InstanceSegmentationModel, ) - - from inference_model_manager.registry_defaults import lazy_register from tests.unit_tests.test_stream_pipeline import FakeSegmentationModel lazy_register(InstanceSegmentationModel) @@ -1697,9 +1701,7 @@ def test_unload_shuts_the_pipeline_down(self, segmentation_manager): assert mm.flush_model_stream_pipeline("seg/1") is None def test_unload_resolves_an_in_flight_frame(self, segmentation_manager): - from inference_models.models.base.async_handoff import ( - get_async_response_future, - ) + from inference_models.models.base.async_handoff import get_async_response_future mm, _, models = segmentation_manager mm.load("seg/1", api_key="") @@ -1824,7 +1826,6 @@ def test_pipeline_is_drained_after_the_last_admitted_frame( from inference_models.models.base.async_handoff import ( get_async_response_context_id, ) - from tests.unit_tests.test_stream_pipeline import ( FakeSegmentationModel, queues_of, diff --git a/inference_model_manager/tests/unit_tests/test_registry_defaults.py b/inference_model_manager/tests/unit_tests/test_registry_defaults.py index 7497251809..1bd3dc49e0 100644 --- a/inference_model_manager/tests/unit_tests/test_registry_defaults.py +++ b/inference_model_manager/tests/unit_tests/test_registry_defaults.py @@ -285,7 +285,12 @@ def test_cosmos3_edge_reasoner_action_config(): assert prompt_action[2] is True assert prompt_action[3]["images"] == {"type": "image", "required": True} assert prompt_action[3]["prompt"] == {"type": "str", "required": True} - for param in ("max_new_tokens", "do_sample", "skip_special_tokens", "return_thinking"): + for param in ( + "max_new_tokens", + "do_sample", + "skip_special_tokens", + "return_thinking", + ): assert param in prompt_action[3], param assert prompt_action[3][param]["required"] is False assert prompt_action[3]["max_new_tokens"]["type"] == "int" @@ -364,7 +369,11 @@ def test_model_owned_defaults_not_injected(): def test_per_family_detector_contracts(): - from inference_model_manager.registry_defaults import _K_ISEG, _ACTION_CONFIGS, _unpack_config + from inference_model_manager.registry_defaults import ( + _ACTION_CONFIGS, + _K_ISEG, + _unpack_config, + ) def params_of(key): return _unpack_config(_ACTION_CONFIGS[key][0])[3] @@ -514,8 +523,8 @@ def test_per_family_contracts_have_no_default_keys(): def test_vlm_generation_params(): from inference_model_manager.registry_defaults import ( - _P_VLM_PROMPT, _ACTION_CONFIGS, + _P_VLM_PROMPT, _unpack_config, ) @@ -550,8 +559,8 @@ def test_vllm_proxy_action_configs_equal_the_hf_rows(proxy_name, hf_name): def test_vlm_generation_contracts_match_model_signatures(): from inference_model_manager.registry_defaults import ( - _P_GEN_FLAGS, _ACTION_CONFIGS, + _P_GEN_FLAGS, _unpack_config, ) @@ -581,10 +590,18 @@ def cfgs_of(key): assert g4["enable_thinking"] == {"type": "bool", "required": False} sv = cfgs_of("SmolVLMHF")["prompt"][3] - assert sv["images_to_single_prompt"] == {"type": "bool", "required": False, "default": True} + assert sv["images_to_single_prompt"] == { + "type": "bool", + "required": False, + "default": True, + } q35 = cfgs_of("Qwen35HF")["prompt"][3] - assert q35["enable_thinking"] == {"type": "bool", "required": False, "default": False} + assert q35["enable_thinking"] == { + "type": "bool", + "required": False, + "default": False, + } cosmos = cfgs_of("Cosmos3EdgeReasoner")["prompt"][3] assert set(cosmos) == { @@ -614,7 +631,12 @@ def cfgs_of(key): glm = cfgs_of("GlmOcrHF") for action in ("recognize_text", "recognize_table", "recognize_formula"): p = glm[action][3] - assert set(p) == {"images", "max_new_tokens", "do_sample", "skip_special_tokens"}, action + assert set(p) == { + "images", + "max_new_tokens", + "do_sample", + "skip_special_tokens", + }, action for name, spec in p.items(): assert "default" not in spec, (action, name) gp = glm["prompt"][3] @@ -653,8 +675,14 @@ def test_classification_declares_include_anomaly_map(): params = _unpack_config(_ACTION_CONFIGS["ClassificationModel"][0])[3] - assert set(params) == {"images", "include_anomaly_map", *PREPROC_FLAGS} + assert set(params) == { + "images", + "include_anomaly_map", + "confidence", + *PREPROC_FLAGS, + } assert params["include_anomaly_map"] == {"type": "bool", "required": False} + assert params["confidence"] == {"type": "confidence", "required": False} def test_roboflow_trained_families_declare_pre_processing_flags(): diff --git a/inference_model_manager/tests/unit_tests/test_serializers_typed.py b/inference_model_manager/tests/unit_tests/test_serializers_typed.py index b1b9291945..747f93fec7 100644 --- a/inference_model_manager/tests/unit_tests/test_serializers_typed.py +++ b/inference_model_manager/tests/unit_tests/test_serializers_typed.py @@ -205,6 +205,7 @@ def test_text_single(self): def test_serialize_gaze_compact(): from types import SimpleNamespace + from inference_model_manager.serializers_typed import serialize_gaze_compact out = SimpleNamespace(yaw=[0.1, 0.2], pitch=[-0.3, 0.0]) @@ -218,6 +219,7 @@ def test_serialize_gaze_compact(): def test_serialize_gaze_compact_batch(): from types import SimpleNamespace + from inference_model_manager.serializers_typed import serialize_gaze_compact outs = [ @@ -234,6 +236,7 @@ def test_serialize_gaze_compact_batch(): def test_serialize_structured_ocr_compact(): from types import SimpleNamespace + from inference_model_manager.serializers_typed import ( serialize_structured_ocr_compact, ) @@ -264,6 +267,7 @@ def test_serialize_structured_ocr_compact(): def test_serialize_sam_segmentation_compact(): from types import SimpleNamespace + from inference_model_manager.serializers_typed import ( serialize_sam_segmentation_compact, ) @@ -282,6 +286,7 @@ def test_serialize_sam_segmentation_compact(): def test_serialize_sam_segmentation_compact_single(): from types import SimpleNamespace + from inference_model_manager.serializers_typed import ( serialize_sam_segmentation_compact, ) @@ -388,6 +393,7 @@ def _anomaly(include_map: bool, *, is_anomalous: bool = True): metadata = { "anomaly_score": 3.5, "anomaly_threshold": 2.0, + "anomaly_scale": 0.5, "is_anomalous": is_anomalous, } if include_map: @@ -465,6 +471,8 @@ def _two_image_anomaly_prediction(include_anomaly_map: bool): raw, calibration=AnomalyCalibration(threshold=2.0, scale=1.0), include_anomaly_map=include_anomaly_map, + confidence="default", + recommended_parameters=None, ) return prediction @@ -480,6 +488,7 @@ def test_each_image_keeps_its_own_anomaly_fields(self): assert [first["anomaly_score"], second["anomaly_score"]] == [0.5, 3.0] assert [first["is_anomalous"], second["is_anomalous"]] == [False, True] assert first["anomaly_threshold"] == second["anomaly_threshold"] == 2.0 + assert first["anomaly_scale"] == second["anomaly_scale"] == 1.0 assert np.asarray(first["anomaly_map"]).tolist() == [[0.0, 0.5], [0.5, 0.5]] assert np.asarray(second["anomaly_map"]).tolist() == [[1.0, 2.0], [3.0, 3.0]] diff --git a/inference_server/inference_server/handlers/classification/introspection.py b/inference_server/inference_server/handlers/classification/introspection.py index c008e5dcf3..ce59786aac 100644 --- a/inference_server/inference_server/handlers/classification/introspection.py +++ b/inference_server/inference_server/handlers/classification/introspection.py @@ -1,6 +1,11 @@ from __future__ import annotations -from inference_model_manager.registry_defaults import _K_PREPROC, _P_IMAGES, _p +from inference_model_manager.registry_defaults import ( + _K_PREPROC, + _P_CONFIDENCE, + _P_IMAGES, + _p, +) from inference_server.framework.entities import ModelInterfaceDescription @@ -10,6 +15,7 @@ def get_classification_interface() -> ModelInterfaceDescription: params=_p( _P_IMAGES, {"include_anomaly_map": {"type": "bool", "required": False}}, + _P_CONFIDENCE, _K_PREPROC, ), output_schema={ diff --git a/inference_server/inference_server/legacy/entities.py b/inference_server/inference_server/legacy/entities.py index c396c3254d..4e7eebfb49 100644 --- a/inference_server/inference_server/legacy/entities.py +++ b/inference_server/inference_server/legacy/entities.py @@ -1678,7 +1678,8 @@ class AnomalyDetectionResponse(ClassificationInferenceResponse): Attributes: anomaly_score (float): Raw anomaly score; larger means more anomalous. anomaly_threshold (float): Decision threshold fitted on validation images. - is_anomalous (bool): Whether the score reached the threshold. + anomaly_scale (float): Calibration scale of the anomalous confidence. + is_anomalous (bool): Whether the anomalous confidence reached the request confidence. anomaly_map (Optional[List[List[float]]]): Raw local anomaly evidence in original image coordinates. """ @@ -1688,7 +1689,13 @@ class AnomalyDetectionResponse(ClassificationInferenceResponse): anomaly_threshold: float = Field( description="Decision threshold fitted on validation images" ) - is_anomalous: bool = Field(description="Whether the score reached the threshold") + anomaly_scale: float = Field( + description="Calibration scale: the anomalous confidence is " + "sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale)" + ) + is_anomalous: bool = Field( + description="Whether the anomalous confidence reached the request confidence" + ) anomaly_map: Optional[List[List[float]]] = Field( default=None, description="Raw local anomaly evidence in original image coordinates", diff --git a/inference_server/inference_server/legacy/router.py b/inference_server/inference_server/legacy/router.py index a168eb2159..b1cab49c3d 100644 --- a/inference_server/inference_server/legacy/router.py +++ b/inference_server/inference_server/legacy/router.py @@ -21,8 +21,15 @@ from inference_model_manager.registry_defaults import IMAGE_EMBEDDINGS_ACTION from inference_sdk.http.utils.aliases import resolve_roboflow_model_alias -from inference_server import configuration, platform_http, server_identity, telemetry +from inference_server import ( + configuration, + pingback, + platform_http, + server_identity, + telemetry, +) from inference_server.dependencies import get_model_manager +from inference_server.hosted.common import service_secret_is_valid from inference_server.legacy.action_recognition import ( ACTION_RECOGNITION_TASK, classify_video, @@ -145,8 +152,6 @@ encode_image_to_jpeg_bytes, render_visualization, ) -from inference_server import pingback -from inference_server.hosted.common import service_secret_is_valid from inference_server.prometheus import measure_inference from inference_server.routing import ( IMAGE_EMBEDDINGS, @@ -1132,12 +1137,15 @@ async def legacy_infer_from_request( extra_args = {"keypoint_confidence": keypoint_confidence} elif route.task_type == "semantic-segmentation": request_type = SemanticSegmentationInferenceRequest + # Leave an omitted confidence unset so anomaly detection models can tell it + # apart from an explicit one and use the model-eval value. + if route.task_type != "classification" or "confidence" in request.query_params: + extra_args["confidence"] = confidence try: inference_request = request_type( api_key=resolved_key, model_id=model_id, image=request_image, - confidence=confidence, iou_threshold=overlap, max_detections=max_detections, visualization_labels=labels, diff --git a/inference_server/inference_server/legacy/translation.py b/inference_server/inference_server/legacy/translation.py index a342c09505..495a13cec8 100644 --- a/inference_server/inference_server/legacy/translation.py +++ b/inference_server/inference_server/legacy/translation.py @@ -110,7 +110,13 @@ def roboflow_confidence(value: Any) -> Optional[Union[float, str]]: def build_task_params(task_type: str, action: str, request: Any, route: Route) -> dict: params: dict = {} confidence = roboflow_confidence(getattr(request, "confidence", None)) - if confidence is not None: + # Single-label classifiers ignore confidence in the model; anomaly detection + # models decide with it, and use the model-eval value when the caller sent none + # (the request field defaults to 0.4). + caller_sent_confidence = task_type != "classification" or ( + "confidence" in getattr(request, "model_fields_set", ()) + ) + if confidence is not None and caller_sent_confidence: params["confidence"] = confidence for field in _DISABLE_PREPROC_FIELDS: if getattr(request, field, False): @@ -446,6 +452,7 @@ def _repack_anomaly_detection_response( confidence=class_predictions[0]["confidence"], anomaly_score=metadata["anomaly_score"], anomaly_threshold=metadata["anomaly_threshold"], + anomaly_scale=metadata["anomaly_scale"], is_anomalous=metadata["is_anomalous"], anomaly_map=( np.asarray(anomaly_map).tolist() if anomaly_map is not None else None diff --git a/inference_server/inference_server/workflows/models_provider.py b/inference_server/inference_server/workflows/models_provider.py index f3d076b2e3..baf23c2e33 100644 --- a/inference_server/inference_server/workflows/models_provider.py +++ b/inference_server/inference_server/workflows/models_provider.py @@ -8,7 +8,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union import torch -from inference_sdk.http.utils.aliases import resolve_roboflow_model_alias from roboflow_workflows import environment from roboflow_workflows.prototypes.models_provider import ( UNSET, @@ -27,6 +26,7 @@ get_async_response_future, ) from inference_models.utils import model_blob_cache +from inference_sdk.http.utils.aliases import resolve_roboflow_model_alias from inference_server import pingback, telemetry from inference_server.gateway import _load_failure from inference_server.legacy.action_recognition import ( @@ -323,7 +323,11 @@ def run_classification( disable_active_learning: Optional[bool] = None, active_learning_target_dataset: Optional[str] = None, inference_kwargs: Optional[Dict[str, Any]] = None, + include_anomaly_map: bool = False, ) -> List[dict]: + # Set only when asked, so the request a classification block builds is + # field-for-field what it was before the anomaly block existed. + anomaly_arguments = {"include_anomaly_map": True} if include_anomaly_map else {} request = ClassificationInferenceRequest( api_key=api_key, model_id=model_id, @@ -332,6 +336,7 @@ def run_classification( source=_WORKFLOW_SOURCE, active_learning_target_dataset=active_learning_target_dataset, confidence=confidence, + **anomaly_arguments, ) return self._dump( self._run_cv(model_id, request, api_key, extra_params=inference_kwargs) diff --git a/inference_server/tests/unit_tests/legacy/test_translation_classification.py b/inference_server/tests/unit_tests/legacy/test_translation_classification.py index 35bad6cc1a..eddfae7485 100644 --- a/inference_server/tests/unit_tests/legacy/test_translation_classification.py +++ b/inference_server/tests/unit_tests/legacy/test_translation_classification.py @@ -5,9 +5,9 @@ import numpy as np import pytest import torch -from inference_models.models.base.classification import ClassificationPrediction from PIL import Image +from inference_models.models.base.classification import ClassificationPrediction from inference_server.legacy.bridge import Route from inference_server.legacy.entities import ( AnomalyDetectionResponse, @@ -38,6 +38,7 @@ def test_anomaly_metadata_repacks_as_anomaly_detection_response(): { "anomaly_score": 1.5, "anomaly_threshold": 1.0, + "anomaly_scale": 0.5, "is_anomalous": True, "anomaly_map": np.array([[0.1, 0.2], [0.3, 0.4]], dtype=np.float32), } @@ -57,6 +58,7 @@ def test_anomaly_metadata_repacks_as_anomaly_detection_response(): assert [p["class"] for p in d["predictions"]] == ["anomalous", "normal"] assert d["anomaly_score"] == 1.5 assert d["anomaly_threshold"] == 1.0 + assert d["anomaly_scale"] == 0.5 assert d["is_anomalous"] is True assert d["anomaly_map"] == [ [pytest.approx(0.1), pytest.approx(0.2)], @@ -69,7 +71,12 @@ def test_anomaly_metadata_without_map_leaves_anomaly_map_out(): class_id=torch.tensor([0]), confidence=torch.tensor([[0.7, 0.3]]), images_metadata=[ - {"anomaly_score": 0.5, "anomaly_threshold": 1.0, "is_anomalous": False} + { + "anomaly_score": 0.5, + "anomaly_threshold": 1.0, + "anomaly_scale": 0.5, + "is_anomalous": False, + } ], ) resp = repack_prediction( @@ -114,6 +121,20 @@ def test_build_task_params_forwards_include_anomaly_map_for_classification(): } +def test_build_task_params_leaves_an_omitted_classification_confidence_unset(): + # The request defaults confidence to 0.4; an anomaly model must not decide at + # it, so only a confidence the caller sent reaches the model. + request = ClassificationInferenceRequest(model_id="ds/1", image=IMG) + route = _anomaly_route() + + assert "confidence" not in build_task_params( + "classification", "infer", request, route + ) + assert build_task_params("multi-label-classification", "infer", request, route) == { + "confidence": 0.4 + } + + def test_build_task_params_forwards_disable_preproc_flags_for_classification(): request = ClassificationInferenceRequest( model_id="ds/1", image=IMG, disable_preproc_grayscale=True diff --git a/inference_server/tests/unit_tests/test_handler_classification.py b/inference_server/tests/unit_tests/test_handler_classification.py index 59cca2151a..71e4f01742 100644 --- a/inference_server/tests/unit_tests/test_handler_classification.py +++ b/inference_server/tests/unit_tests/test_handler_classification.py @@ -27,6 +27,8 @@ def _anomaly_prediction(include_anomaly_map: bool) -> ClassificationPrediction: raw, calibration=AnomalyCalibration(threshold=2.0, scale=1.0), include_anomaly_map=include_anomaly_map, + confidence="default", + recommended_parameters=None, ) return prediction diff --git a/inference_server/tests/unit_tests/workflows/test_models_provider.py b/inference_server/tests/unit_tests/workflows/test_models_provider.py index a9a47a1084..1676ba12fe 100644 --- a/inference_server/tests/unit_tests/workflows/test_models_provider.py +++ b/inference_server/tests/unit_tests/workflows/test_models_provider.py @@ -13,7 +13,6 @@ ) from inference_models.errors import ModelInputError, ModelNotFoundError - from inference_server.gateway import ModelManagerGateway from inference_server.legacy.bridge import ( LegacyModelBridge, @@ -351,6 +350,29 @@ def test_classification_inference_kwargs_override_action_params(): assert b.calls[0][2]["confidence"] == 0.9 +def test_classification_forwards_anomaly_map_flag_and_confidence_to_the_model(): + b = FakeSyncBridge() + b.routes["cls/1"] = Route( + model_id="cls/1", + registry_id="cls/1", + task_type="classification", + action="infer", + actions={"infer"}, + class_names=["cat"], + ) + b.predictions[("cls/1", "infer")] = SimpleNamespace( + confidence=np.array([0.9]), class_id=np.array([0]) + ) + GatewayModelsProvider(b, api_key=None).run_classification( + "cls/1", + [{"type": "numpy_object", "value": np.zeros((4, 6, 3), np.uint8)}], + confidence=0.7, + include_anomaly_map=True, + ) + assert b.calls[0][2]["include_anomaly_map"] is True + assert b.calls[0][2]["confidence"] == 0.7 + + def test_get_class_names_and_contains(): p = GatewayModelsProvider(_od_bridge(), api_key=None) assert p.get_class_names("ds/1") == ["cat"] and "ds/1" in p and "x/1" not in p @@ -1238,12 +1260,11 @@ def __init__(self, pipeline): self.unloaded = False def infer(self, route, api_key, action, images, params, record=True): + from inference_model_manager.model_manager import ModelManager from inference_models.models.base.async_handoff import ( get_async_response_context_id, ) - from inference_model_manager.model_manager import ModelManager - self.calls.append( (route.model_id, action, dict(params), [i.data for i in images]) ) From 89db3e5ffe21700e08224becc92e85c8d727a99a Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 15:42:48 -0700 Subject: [PATCH 10/17] Anchor the anomaly heatmap at the block's decision The Anomaly Detection Model block outputs anomaly_decision_threshold, the raw score at which the anomalous confidence meets its confidence, and the heatmap docs point its anomaly_threshold input at it, so the middle of the ramp marks the verdict at any confidence. Co-Authored-By: Claude Opus 5.5 --- .../core_steps/common/anomaly_detection.py | 17 +++++++++++++++++ .../models/roboflow/anomaly_detection/v1.py | 12 ++++++++++++ .../roboflow/anomaly_detection/v1_tensor.py | 14 ++++++++++++++ .../visualizations/anomaly_heatmap/v1.py | 9 +++++---- .../anomaly_heatmap/v1_tensor.py | 9 +++++---- .../common/test_anomaly_detection.py | 18 ++++++++++++++++++ .../roboflow/anomaly_detection/test_v1.py | 4 ++++ .../anomaly_detection/test_v1_tensor.py | 1 + 8 files changed, 76 insertions(+), 8 deletions(-) diff --git a/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py index 5c73357acc..1354ffbbf2 100644 --- a/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py +++ b/workflows/roboflow_workflows/core_steps/common/anomaly_detection.py @@ -25,6 +25,7 @@ ANOMALY_SCORE_KEY = "anomaly_score" ANOMALY_THRESHOLD_KEY = "anomaly_threshold" ANOMALY_SCALE_KEY = "anomaly_scale" +ANOMALY_DECISION_THRESHOLD_KEY = "anomaly_decision_threshold" IS_ANOMALOUS_KEY = "is_anomalous" ANOMALY_MAP_KEY = "anomaly_map" ANOMALY_METADATA_KEYS = ( @@ -158,6 +159,22 @@ def validate_threshold_override(threshold: Optional[float]) -> Optional[float]: return value +def decision_threshold(threshold: float, scale: float, confidence: float) -> float: + """The raw score at which the anomalous confidence equals ``confidence``. + + ``threshold + scale * logit(confidence)``, so ``threshold`` itself at 0.5. The + logit is clipped like the sigmoid, keeping the result finite at 0 and 1. + """ + if confidence <= 0: + margin = -MAX_CONFIDENCE_MARGIN + elif confidence >= 1: + margin = MAX_CONFIDENCE_MARGIN + else: + margin = math.log(confidence / (1 - confidence)) + margin = max(-MAX_CONFIDENCE_MARGIN, min(MAX_CONFIDENCE_MARGIN, margin)) + return threshold + scale * margin + + def anomalous_confidence(score: float, threshold: float, scale: float) -> float: """``sigmoid((score - threshold) / scale)``, exactly 0.5 at ``threshold``.""" margin = (score - threshold) / scale diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py index 8763431c5a..728bb0a823 100644 --- a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1.py @@ -3,12 +3,15 @@ import numpy as np from pydantic import ConfigDict, Field from roboflow_workflows.core_steps.common.anomaly_detection import ( + ANOMALY_DECISION_THRESHOLD_KEY, ANOMALY_MAP_KEY, + ANOMALY_SCALE_KEY, ANOMALY_SCORE_KEY, ANOMALY_THRESHOLD_KEY, IS_ANOMALOUS_KEY, apply_threshold_override, coerce_anomaly_map, + decision_threshold, require_anomaly_response, validate_threshold_override, ) @@ -81,6 +84,9 @@ - `anomaly_threshold`: the threshold in effect for this image (saved with the model, or the override). - `is_anomalous`: whether the anomalous confidence reached `confidence`. +- `anomaly_decision_threshold`: the raw score at which the anomalous confidence + equals `confidence` (`anomaly_threshold` itself at 0.5). Connect it to the + heatmap's `anomaly_threshold` so the middle of its ramp marks the decision. - `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` array at the network's input resolution (square), in the same units as `anomaly_score`. The map covers the whole image as the network saw it, so @@ -188,6 +194,7 @@ def describe_outputs(cls) -> List[OutputDefinition]: OutputDefinition(name="predictions", kind=[CLASSIFICATION_PREDICTION_KIND]), OutputDefinition(name=ANOMALY_SCORE_KEY, kind=[FLOAT_KIND]), OutputDefinition(name=ANOMALY_THRESHOLD_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=ANOMALY_DECISION_THRESHOLD_KEY, kind=[FLOAT_KIND]), OutputDefinition(name=IS_ANOMALOUS_KEY, kind=[BOOLEAN_KIND]), OutputDefinition(name=ANOMALY_MAP_KEY, kind=[NUMPY_ARRAY_KIND]), OutputDefinition(name=INFERENCE_ID_KEY, kind=[INFERENCE_ID_KIND]), @@ -376,6 +383,11 @@ def _post_process_result( "predictions": prediction, ANOMALY_SCORE_KEY: float(prediction[ANOMALY_SCORE_KEY]), ANOMALY_THRESHOLD_KEY: float(prediction[ANOMALY_THRESHOLD_KEY]), + ANOMALY_DECISION_THRESHOLD_KEY: decision_threshold( + threshold=float(prediction[ANOMALY_THRESHOLD_KEY]), + scale=float(prediction[ANOMALY_SCALE_KEY]), + confidence=confidence, + ), IS_ANOMALOUS_KEY: bool(prediction[IS_ANOMALOUS_KEY]), ANOMALY_MAP_KEY: anomaly_map, INFERENCE_ID_KEY: prediction.get(INFERENCE_ID_KEY), diff --git a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py index 292f1f1ff7..33ba19f189 100644 --- a/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py +++ b/workflows/roboflow_workflows/core_steps/models/roboflow/anomaly_detection/v1_tensor.py @@ -27,6 +27,7 @@ from pydantic import ConfigDict, Field from roboflow_workflows.core_steps.common.anomaly_detection import ( ANOMALOUS_CLASS_NAME, + ANOMALY_DECISION_THRESHOLD_KEY, ANOMALY_MAP_KEY, ANOMALY_METADATA_KEYS, ANOMALY_SCALE_KEY, @@ -36,6 +37,7 @@ NORMAL_CLASS_NAME, apply_threshold_override, coerce_anomaly_map, + decision_threshold, redecide_at_threshold, require_anomaly_response, validate_threshold_override, @@ -117,6 +119,9 @@ - `anomaly_threshold`: the threshold in effect for this image (saved with the model, or the override). - `is_anomalous`: whether the anomalous confidence reached `confidence`. +- `anomaly_decision_threshold`: the raw score at which the anomalous confidence + equals `confidence` (`anomaly_threshold` itself at 0.5). Connect it to the + heatmap's `anomaly_threshold` so the middle of its ramp marks the decision. - `anomaly_map`: the local anomaly evidence as a float32 `(height, width)` array at the network's input resolution (square), in the same units as `anomaly_score`. The map covers the whole image as the network saw it, so @@ -227,6 +232,7 @@ def describe_outputs(cls) -> List[OutputDefinition]: ), OutputDefinition(name=ANOMALY_SCORE_KEY, kind=[FLOAT_KIND]), OutputDefinition(name=ANOMALY_THRESHOLD_KEY, kind=[FLOAT_KIND]), + OutputDefinition(name=ANOMALY_DECISION_THRESHOLD_KEY, kind=[FLOAT_KIND]), OutputDefinition(name=IS_ANOMALOUS_KEY, kind=[BOOLEAN_KIND]), OutputDefinition(name=ANOMALY_MAP_KEY, kind=[NUMPY_ARRAY_KIND]), OutputDefinition(name=INFERENCE_ID_KEY, kind=[INFERENCE_ID_KIND]), @@ -377,6 +383,7 @@ def run_locally( _result( prediction=prediction, verdict=verdict, + confidence=confidence, anomaly_map=anomaly_map, inference_id=inference_id, model_id=model_id, @@ -440,6 +447,7 @@ def run_remotely( _result( prediction=prediction, verdict=verdict, + confidence=confidence, anomaly_map=anomaly_map, inference_id=inference_id, model_id=model_id, @@ -461,6 +469,7 @@ def _decided_class_id(verdict: Dict[str, Any]) -> torch.Tensor: def _result( prediction: ClassificationPrediction, verdict: Dict[str, Any], + confidence: float, anomaly_map: Optional[np.ndarray], inference_id: str, model_id: str, @@ -469,6 +478,11 @@ def _result( "predictions": prediction, ANOMALY_SCORE_KEY: float(verdict[ANOMALY_SCORE_KEY]), ANOMALY_THRESHOLD_KEY: float(verdict[ANOMALY_THRESHOLD_KEY]), + ANOMALY_DECISION_THRESHOLD_KEY: decision_threshold( + threshold=float(verdict[ANOMALY_THRESHOLD_KEY]), + scale=float(verdict[ANOMALY_SCALE_KEY]), + confidence=confidence, + ), IS_ANOMALOUS_KEY: bool(verdict[IS_ANOMALOUS_KEY]), ANOMALY_MAP_KEY: anomaly_map, INFERENCE_ID_KEY: inference_id, diff --git a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py index cb8dbf8d90..df755c53ff 100644 --- a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py +++ b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1.py @@ -56,8 +56,9 @@ OpenCV ramps (`JET`, `TURBO`, `INFERNO`, `VIRIDIS`, `HOT`) are opaque colour maps over the same scale; use `opacity` to see the image through them. -Pass the model block's `anomaly_threshold` output so the ramp follows the threshold -in effect (the saved one, or the block's override). +Pass the model block's `anomaly_decision_threshold` output so the middle of the ramp +marks the block's decision (its threshold and `confidence`); at the default confidence +of 0.5 it equals `anomaly_threshold`. ## Common Use Cases @@ -111,8 +112,8 @@ class AnomalyHeatmapManifest(VisualizationManifest): examples=["$steps.anomaly_model.anomaly_map"], ) anomaly_threshold: Union[float, Selector(kind=[FLOAT_KIND])] = Field( # type: ignore - description="The decision threshold on the raw anomaly score that anchors the colour ramp; the threshold maps to the middle of the ramp. Connect the model block's anomaly_threshold output so the ramp follows the threshold in effect.", - examples=["$steps.anomaly_model.anomaly_threshold", 1.25], + description="The raw anomaly score that anchors the colour ramp; it maps to the middle of the ramp. Connect the model block's anomaly_decision_threshold output so the ramp follows the block's decision.", + examples=["$steps.anomaly_model.anomaly_decision_threshold", 1.25], ) color_map: Union[ Literal["ROBOFLOW", "JET", "TURBO", "INFERNO", "VIRIDIS", "HOT"], diff --git a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py index f0b3f0f65e..f5a0cbcc58 100644 --- a/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py +++ b/workflows/roboflow_workflows/core_steps/visualizations/anomaly_heatmap/v1_tensor.py @@ -64,8 +64,9 @@ OpenCV ramps (`JET`, `TURBO`, `INFERNO`, `VIRIDIS`, `HOT`) are opaque colour maps over the same scale; use `opacity` to see the image through them. -Pass the model block's `anomaly_threshold` output so the ramp follows the threshold -in effect (the saved one, or the block's override). +Pass the model block's `anomaly_decision_threshold` output so the middle of the ramp +marks the block's decision (its threshold and `confidence`); at the default confidence +of 0.5 it equals `anomaly_threshold`. ## Common Use Cases @@ -119,8 +120,8 @@ class AnomalyHeatmapManifest(VisualizationManifest): examples=["$steps.anomaly_model.anomaly_map"], ) anomaly_threshold: Union[float, Selector(kind=[FLOAT_KIND])] = Field( # type: ignore - description="The decision threshold on the raw anomaly score that anchors the colour ramp; the threshold maps to the middle of the ramp. Connect the model block's anomaly_threshold output so the ramp follows the threshold in effect.", - examples=["$steps.anomaly_model.anomaly_threshold", 1.25], + description="The raw anomaly score that anchors the colour ramp; it maps to the middle of the ramp. Connect the model block's anomaly_decision_threshold output so the ramp follows the block's decision.", + examples=["$steps.anomaly_model.anomaly_decision_threshold", 1.25], ) color_map: Union[ Literal["ROBOFLOW", "JET", "TURBO", "INFERNO", "VIRIDIS", "HOT"], diff --git a/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py index a88cbc6ce3..035e23bebc 100644 --- a/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py +++ b/workflows/tests/unit_tests/core_steps/common/test_anomaly_detection.py @@ -5,6 +5,7 @@ from roboflow_workflows.core_steps.common.anomaly_detection import ( apply_threshold_override, coerce_anomaly_map, + decision_threshold, decode_anomaly_map, require_anomaly_response, validate_threshold_override, @@ -136,3 +137,20 @@ def test_apply_threshold_override_without_override_is_a_no_op() -> None: # then assert result == response + + +@pytest.mark.parametrize( + "confidence, expected", + [ + (0.5, 2.0), + (0.8, 2.0 + 0.5 * 1.3862944), + (0.0, 2.0 - 0.5 * 60), + (1.0, 2.0 + 0.5 * 60), + ], +) +def test_decision_threshold_is_where_the_anomalous_confidence_meets_confidence( + confidence: float, expected: float +) -> None: + assert decision_threshold(threshold=2.0, scale=0.5, confidence=confidence) == ( + pytest.approx(expected) + ) diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py index c3e15ecfcf..dcde2bd8c8 100644 --- a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1.py @@ -201,6 +201,7 @@ def test_manifest_outputs_are_declared_in_contract_order() -> None: "predictions", "anomaly_score", "anomaly_threshold", + "anomaly_decision_threshold", "is_anomalous", "anomaly_map", "inference_id", @@ -226,6 +227,7 @@ def test_run_locally_forwards_the_map_flag_and_decodes_the_payload() -> None: assert len(result) == 1 assert result[0]["anomaly_score"] == 3.0 assert result[0]["anomaly_threshold"] == 2.0 + assert result[0]["anomaly_decision_threshold"] == 2.0 assert result[0]["is_anomalous"] is True assert result[0]["inference_id"] == "iid-1" assert result[0]["model_id"] == "ws/anomaly/1" @@ -282,6 +284,8 @@ def test_run_locally_sends_confidence_and_applies_it_under_an_override() -> None assert provider.calls[0]["confidence"] == 0.8 assert result[0]["is_anomalous"] is False assert result[0]["predictions"]["top"] == "normal" + # 1.0 + 0.5 * logit(0.8): the score the override needs for an anomalous verdict. + assert result[0]["anomaly_decision_threshold"] == pytest.approx(1.6931, abs=1e-4) def test_run_rejects_a_plain_classifier_response() -> None: diff --git a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py index a2839a39e7..a6c4ab503b 100644 --- a/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py +++ b/workflows/tests/unit_tests/core_steps/models/roboflow/anomaly_detection/test_v1_tensor.py @@ -190,6 +190,7 @@ def test_run_locally_applies_confidence_under_an_override() -> None: assert provider.calls[0]["confidence"] == 0.8 assert result[0]["is_anomalous"] is False assert result[0]["predictions"].class_id.tolist() == [0] + assert result[0]["anomaly_decision_threshold"] == pytest.approx(1.6931, abs=1e-4) def test_run_locally_rejects_a_plain_classifier() -> None: From 4c8ad8e568a37e1f9e3cee8a7bad72b019e34dcb Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 16:48:02 -0700 Subject: [PATCH 11/17] Test anomaly detection end to end against the reference models A workflow integration test chains the Anomaly Detection Model block into the heatmap for the public FoundAD and PatchCore models of the anomaly reference project, under both tensor representations: the verdict on a clearly anomalous and a clearly normal image, confidence moving it, and the threshold override composing with confidence. The hosted platform tests run the same models on staging and prod through API v0 and v1, checking the verdict rule against the served score, threshold and scale. A decision threshold at a very low confidence can reach zero, so the heatmap now saturates past a non-positive anchor, the limit of its ramp, instead of rejecting it. Co-Authored-By: Claude Opus 5.5 --- .../hosted_platform_tests/conftest.py | 11 + .../test_anomaly_detection_models.py | 123 ++++++++++ .../execution/assets/anomaly_anomalous.jpg | Bin 0 -> 199408 bytes .../execution/assets/anomaly_normal.jpg | Bin 0 -> 190753 bytes .../execution/assets/image_credits.txt | 3 +- .../integration_tests/execution/conftest.py | 10 + .../test_workflow_with_anomaly_detection.py | 212 ++++++++++++++++++ .../common/annotators/anomaly_heatmap.py | 22 +- .../visualizations/test_anomaly_heatmap.py | 16 +- 9 files changed, 387 insertions(+), 10 deletions(-) create mode 100644 tests/inference/hosted_platform_tests/test_anomaly_detection_models.py create mode 100644 tests/workflows/integration_tests/execution/assets/anomaly_anomalous.jpg create mode 100644 tests/workflows/integration_tests/execution/assets/anomaly_normal.jpg create mode 100644 tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py diff --git a/tests/inference/hosted_platform_tests/conftest.py b/tests/inference/hosted_platform_tests/conftest.py index 99ae0b640e..c5638b1301 100644 --- a/tests/inference/hosted_platform_tests/conftest.py +++ b/tests/inference/hosted_platform_tests/conftest.py @@ -151,6 +151,8 @@ class PlatformEnvironment(Enum): "yolov8n-pose-640": "yolov8n-pose-640", "rfdetr-od": "rfdetr-nano", "rfdetr-is": "rfdetr-seg-nano", + "anomaly-foundad": "paul-guerrie-tang1/anomaly-det-algbq-1-foundad-t1", + "anomaly-patchcore": "paul-guerrie-tang1/anomaly-det-algbq-1-patchcore-t2", }, PlatformEnvironment.ROBOFLOW_STAGING_LAMBDA: { "object-detection": "eye-detection/35", @@ -161,6 +163,8 @@ class PlatformEnvironment(Enum): "yolov8n-pose-640": "microsoft-coco-pose/1", "rfdetr-od": "rfdetr-nano", "rfdetr-is": "asl-instance-seg/160", + "anomaly-foundad": "paul-guerrie/anomaly-det-1-foundad-t1", + "anomaly-patchcore": "paul-guerrie/anomaly-det-1-patchcore-t2", }, } MODELS_TO_BE_USED[PlatformEnvironment.ROBOFLOW_STAGING_SERVERLESS] = MODELS_TO_BE_USED[ @@ -261,6 +265,13 @@ def multi_class_classification_model_id( return MODELS_TO_BE_USED[platform_environment]["multi_class_classification"] +@pytest.fixture(scope="session", params=["anomaly-foundad", "anomaly-patchcore"]) +def anomaly_detection_model_id( + platform_environment: PlatformEnvironment, request: pytest.FixtureRequest +) -> str: + return MODELS_TO_BE_USED[platform_environment][request.param] + + @pytest.fixture(scope="session") def detection_model_id(platform_environment: PlatformEnvironment) -> str: return MODELS_TO_BE_USED[platform_environment]["object-detection"] diff --git a/tests/inference/hosted_platform_tests/test_anomaly_detection_models.py b/tests/inference/hosted_platform_tests/test_anomaly_detection_models.py new file mode 100644 index 0000000000..ea10d7898c --- /dev/null +++ b/tests/inference/hosted_platform_tests/test_anomaly_detection_models.py @@ -0,0 +1,123 @@ +import math +import os + +import cv2 +import numpy as np +import pytest + +from inference_sdk import InferenceConfiguration, InferenceHTTPClient +from tests.inference.hosted_platform_tests.conftest import ( + ROBOFLOW_API_KEY, + apply_auth_mode, +) + +# Images of the anomaly reference project, far from the threshold on every model. +ASSETS_DIR = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "..", + "workflows", + "integration_tests", + "execution", + "assets", + ) +) +ANOMALOUS_IMAGE = cv2.imread(os.path.join(ASSETS_DIR, "anomaly_anomalous.jpg")) +NORMAL_IMAGE = cv2.imread(os.path.join(ASSETS_DIR, "anomaly_normal.jpg")) + + +def _client(service_url: str, auth_mode: str, api_v0: bool, **configuration): + client = InferenceHTTPClient(api_url=service_url, api_key=ROBOFLOW_API_KEY) + if api_v0: + client.select_api_v0() + client.configure(InferenceConfiguration(**configuration)) + return apply_auth_mode(client, auth_mode) + + +def _anomalous_confidence(response: dict) -> float: + margin = (response["anomaly_score"] - response["anomaly_threshold"]) / response[ + "anomaly_scale" + ] + return 1 / (1 + math.exp(-margin)) + + +@pytest.mark.flaky(retries=4, delay=1) +@pytest.mark.parametrize("api_v0", [True, False], ids=["v0", "v1"]) +def test_anomaly_model_decides_at_the_calibrated_threshold_by_default( + classification_service_url: str, + anomaly_detection_model_id: str, + auth_mode: str, + api_v0: bool, +) -> None: + # given + client = _client(classification_service_url, auth_mode, api_v0) + + # when + anomalous, normal = ( + client.infer(image, model_id=anomaly_detection_model_id) + for image in (ANOMALOUS_IMAGE, NORMAL_IMAGE) + ) + + # then + assert anomalous["is_anomalous"] is True and anomalous["top"] == "anomalous" + assert normal["is_anomalous"] is False and normal["top"] == "normal" + for response in (anomalous, normal): + assert response["anomaly_scale"] > 0 + assert "anomaly_map" not in response or response["anomaly_map"] is None + anomalous_confidence = _anomalous_confidence(response) + served = {p["class"]: p["confidence"] for p in response["predictions"]} + assert served["anomalous"] == pytest.approx(anomalous_confidence, abs=1e-4) + assert response["is_anomalous"] is (anomalous_confidence >= 0.5) + + +@pytest.mark.flaky(retries=4, delay=1) +@pytest.mark.parametrize("api_v0", [True, False], ids=["v0", "v1"]) +def test_anomaly_model_verdict_follows_the_request_confidence( + classification_service_url: str, + anomaly_detection_model_id: str, + auth_mode: str, + api_v0: bool, +) -> None: + # given + default_response = _client(classification_service_url, auth_mode, api_v0).infer( + NORMAL_IMAGE, model_id=anomaly_detection_model_id + ) + anomalous_confidence = _anomalous_confidence(default_response) + below = _client( + classification_service_url, + auth_mode, + api_v0, + confidence_threshold=round(anomalous_confidence / 2, 4), + ) + + # when + response = below.infer(NORMAL_IMAGE, model_id=anomaly_detection_model_id) + + # then: the threshold in effect never moves, only the verdict does + assert default_response["is_anomalous"] is False + assert response["is_anomalous"] is True and response["top"] == "anomalous" + assert response["anomaly_threshold"] == default_response["anomaly_threshold"] + assert response["anomaly_score"] == default_response["anomaly_score"] + + +@pytest.mark.flaky(retries=4, delay=1) +def test_anomaly_model_serves_the_map_at_network_resolution_on_request( + classification_service_url: str, + anomaly_detection_model_id: str, + auth_mode: str, +) -> None: + # given + client = _client( + classification_service_url, auth_mode, False, include_anomaly_map=True + ) + + # when + response = client.infer(ANOMALOUS_IMAGE, model_id=anomaly_detection_model_id) + + # then + anomaly_map = response["anomaly_map"] + assert isinstance(anomaly_map, np.ndarray) + assert anomaly_map.dtype == np.float32 + assert anomaly_map.ndim == 2 and anomaly_map.shape[0] == anomaly_map.shape[1] + assert float(anomaly_map.max()) > 0 diff --git a/tests/workflows/integration_tests/execution/assets/anomaly_anomalous.jpg b/tests/workflows/integration_tests/execution/assets/anomaly_anomalous.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c4cd73146eb8403e7d0231da4cb7d1f18882101 GIT binary patch literal 199408 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

C+vdoH0e)&nLf! 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%0Ab&8n;vTx?;J@;BdevjoohHXKn||!-`-=FEB%a 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 axtxvLF}4?zJOtZNz&5-m)###_BmddrK5V=I literal 0 HcmV?d00001 diff --git a/tests/workflows/integration_tests/execution/assets/anomaly_normal.jpg b/tests/workflows/integration_tests/execution/assets/anomaly_normal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42d11762952097597dcd9c9494c7f8b2e45cfaac GIT binary patch literal 190753 zcmbTd1z1#F)G#`Pgb0X$Gy~F|gYs`!otA7 z#JGcniHV7gjdk}P5$-)4oO@&s2=R%i$idW9

&^qfpIkJ#xbDVZO$uygb9^Yeq5 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 da+DLgGjr#moOqqHy{FvWN2zMv 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

~X!gwgwU?IboNZgWlM}wYQXW8^Ls2Zl|O4 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 LYk;6hj)(pa>p29K literal 0 HcmV?d00001 diff --git a/tests/workflows/integration_tests/execution/assets/image_credits.txt b/tests/workflows/integration_tests/execution/assets/image_credits.txt index 635034fc9d..3f837d8689 100644 --- a/tests/workflows/integration_tests/execution/assets/image_credits.txt +++ b/tests/workflows/integration_tests/execution/assets/image_credits.txt @@ -4,4 +4,5 @@ dogs.jpg: https://www.pexels.com/photo/brown-and-white-dogs-sitting-on-field-356 multi-fruit.jpg: https://www.freepik.com/free-photo/front-close-view-organic-nutrition-source-fresh-bananas-bundle-red-apples-orange-with-stem-dark-background_17119128.htm multi_line_text.jpg: https://www.pexels.com/photo/illuminated-qoute-board-2255441/ asl_image.jpg: https://universe.roboflow.com/david-lee-d0rhs/american-sign-language-letters -car.jpg: https://pixabay.com/photos/car-vehicle-sports-car-auto-63930/ \ No newline at end of file +car.jpg: https://pixabay.com/photos/car-vehicle-sports-car-auto-63930/ +anomaly_anomalous.jpg, anomaly_normal.jpg: https://universe.roboflow.com/paul-guerrie-tang1/anomaly-det-algbq diff --git a/tests/workflows/integration_tests/execution/conftest.py b/tests/workflows/integration_tests/execution/conftest.py index 206121f661..28bfc682a5 100644 --- a/tests/workflows/integration_tests/execution/conftest.py +++ b/tests/workflows/integration_tests/execution/conftest.py @@ -55,6 +55,16 @@ def multi_line_text_image() -> np.ndarray: return cv2.imread(os.path.join(ASSETS_DIR, "multi_line_text.jpg")) +@pytest.fixture(scope="function") +def anomalous_image() -> np.ndarray: + return cv2.imread(os.path.join(ASSETS_DIR, "anomaly_anomalous.jpg")) + + +@pytest.fixture(scope="function") +def normal_image() -> np.ndarray: + return cv2.imread(os.path.join(ASSETS_DIR, "anomaly_normal.jpg")) + + @pytest.fixture(scope="function") def stitch_left_image() -> np.ndarray: return cv2.imread(os.path.join(ASSETS_DIR, "stitch", "v_left.jpeg")) diff --git a/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py b/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py new file mode 100644 index 0000000000..7d143e8e7f --- /dev/null +++ b/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py @@ -0,0 +1,212 @@ +import numpy as np +import pytest + +from inference.core.env import ( + ENABLE_TENSOR_DATA_REPRESENTATION, + WORKFLOWS_MAX_CONCURRENT_STEPS, +) +from inference.core.managers.base import ModelManager +from inference.core.workflows.core_steps.common.entities import StepExecutionMode +from inference.core.workflows.core_steps.common.serializers_tensor import ( + serialise_native_classification, +) +from inference.core.workflows.execution_engine.core import ExecutionEngine +from tests.workflows.integration_tests.execution.workflows_gallery_collector.decorators import ( + add_to_workflows_gallery, +) + +# Public models of the anomaly reference project, trained on the same data. +FOUNDAD_MODEL_ID = "paul-guerrie-tang1/anomaly-det-algbq-1-foundad-t1" +PATCHCORE_MODEL_ID = "paul-guerrie-tang1/anomaly-det-algbq-1-patchcore-t2" + +WORKFLOW_DEFINITION = { + "version": "1.0", + "inputs": [ + {"type": "WorkflowImage", "name": "image"}, + { + "type": "WorkflowParameter", + "name": "model_id", + "default_value": FOUNDAD_MODEL_ID, + }, + {"type": "WorkflowParameter", "name": "confidence", "default_value": 0.5}, + {"type": "WorkflowParameter", "name": "anomaly_threshold"}, + ], + "steps": [ + { + "type": "roboflow_core/anomaly_detection_model@v1", + "name": "model", + "images": "$inputs.image", + "model_id": "$inputs.model_id", + "confidence": "$inputs.confidence", + "anomaly_threshold": "$inputs.anomaly_threshold", + }, + { + "type": "roboflow_core/anomaly_heatmap_visualization@v1", + "name": "heatmap", + "image": "$inputs.image", + "anomaly_map": "$steps.model.anomaly_map", + "anomaly_threshold": "$steps.model.anomaly_decision_threshold", + }, + ], + "outputs": [ + { + "type": "JsonField", + "name": "predictions", + "selector": "$steps.model.predictions", + }, + { + "type": "JsonField", + "name": "is_anomalous", + "selector": "$steps.model.is_anomalous", + }, + { + "type": "JsonField", + "name": "anomaly_score", + "selector": "$steps.model.anomaly_score", + }, + { + "type": "JsonField", + "name": "anomaly_threshold", + "selector": "$steps.model.anomaly_threshold", + }, + { + "type": "JsonField", + "name": "anomaly_decision_threshold", + "selector": "$steps.model.anomaly_decision_threshold", + }, + { + "type": "JsonField", + "name": "heatmap", + "selector": "$steps.heatmap.image", + }, + ], +} + + +def _engine(model_manager: ModelManager, roboflow_api_key: str) -> ExecutionEngine: + return ExecutionEngine.init( + workflow_definition=WORKFLOW_DEFINITION, + init_parameters={ + "workflows_core.model_manager": model_manager, + "workflows_core.api_key": roboflow_api_key, + "workflows_core.step_execution_mode": StepExecutionMode.LOCAL, + }, + max_concurrent_steps=WORKFLOWS_MAX_CONCURRENT_STEPS, + ) + + +def _decision(result: dict): + predictions = result["predictions"] + if ENABLE_TENSOR_DATA_REPRESENTATION: + # The flag ships a native ClassificationPrediction; the serializer restores + # the numpy dict shape. + predictions = serialise_native_classification(prediction=predictions) + return result["is_anomalous"], predictions["top"] + + +@add_to_workflows_gallery( + category="Workflows with visualization blocks", + use_case_title="Workflow with anomaly detection and its heatmap", + workflow_name_in_app="anomaly-detection-heatmap", + use_case_description=""" +This workflow runs an anomaly detection model (FoundAD or PatchCore) and overlays its +anomaly map on the image. The heatmap is anchored at the block's decision threshold, +so the middle of the colour ramp marks the anomalous verdict at any confidence. + """, + workflow_definition=WORKFLOW_DEFINITION, +) +@pytest.mark.parametrize("model_id", [FOUNDAD_MODEL_ID, PATCHCORE_MODEL_ID]) +def test_anomaly_detection_workflow_decides_and_draws_the_heatmap( + model_manager: ModelManager, + anomalous_image: np.ndarray, + normal_image: np.ndarray, + roboflow_api_key: str, + model_id: str, +) -> None: + # given + execution_engine = _engine(model_manager, roboflow_api_key) + + # when + result = execution_engine.run( + runtime_parameters={ + "image": [anomalous_image, normal_image], + "model_id": model_id, + } + ) + + # then + assert len(result) == 2, "Expected one output per input image" + assert _decision(result[0]) == (True, "anomalous") + assert _decision(result[1]) == (False, "normal") + for image_result, image in zip(result, (anomalous_image, normal_image)): + # At the default confidence the decision sits at the saved threshold. + assert image_result["anomaly_decision_threshold"] == pytest.approx( + image_result["anomaly_threshold"] + ) + assert image_result["heatmap"].numpy_image.shape == image.shape + assert not np.array_equal(image_result["heatmap"].numpy_image, image) + + +@pytest.mark.parametrize("model_id", [FOUNDAD_MODEL_ID, PATCHCORE_MODEL_ID]) +def test_anomaly_detection_workflow_confidence_moves_the_decision( + model_manager: ModelManager, + anomalous_image: np.ndarray, + normal_image: np.ndarray, + roboflow_api_key: str, + model_id: str, +) -> None: + # given + execution_engine = _engine(model_manager, roboflow_api_key) + + # when: every anomalous confidence reaches 0, none reaches 1 + lenient = execution_engine.run( + runtime_parameters={ + "image": normal_image, + "model_id": model_id, + "confidence": 0.0, + } + ) + strict = execution_engine.run( + runtime_parameters={ + "image": anomalous_image, + "model_id": model_id, + "confidence": 1.0, + } + ) + + # then + assert _decision(lenient[0]) == (True, "anomalous") + assert _decision(strict[0]) == (False, "normal") + assert lenient[0]["anomaly_decision_threshold"] < lenient[0]["anomaly_threshold"] + assert strict[0]["anomaly_decision_threshold"] > strict[0]["anomaly_threshold"] + # Past a decision threshold below zero the whole map is anomalous evidence. + assert lenient[0]["heatmap"].numpy_image.shape == normal_image.shape + + +@pytest.mark.parametrize("model_id", [FOUNDAD_MODEL_ID, PATCHCORE_MODEL_ID]) +def test_anomaly_detection_workflow_threshold_override_composes_with_confidence( + model_manager: ModelManager, + anomalous_image: np.ndarray, + roboflow_api_key: str, + model_id: str, +) -> None: + # given + execution_engine = _engine(model_manager, roboflow_api_key) + baseline = execution_engine.run( + runtime_parameters={"image": anomalous_image, "model_id": model_id} + )[0] + above_the_score = baseline["anomaly_score"] * 2 + + # when + result = execution_engine.run( + runtime_parameters={ + "image": anomalous_image, + "model_id": model_id, + "anomaly_threshold": above_the_score, + } + )[0] + + # then + assert _decision(result) == (False, "normal") + assert result["anomaly_threshold"] == above_the_score + assert result["anomaly_decision_threshold"] == pytest.approx(above_the_score) diff --git a/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py b/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py index f9f0092f66..522a59897f 100644 --- a/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py +++ b/workflows/roboflow_workflows/core_steps/visualizations/common/annotators/anomaly_heatmap.py @@ -3,7 +3,9 @@ Anomaly map values share the units of the model's anomaly score and are unbounded above, so every value is placed on a fixed saturating scale anchored to the model's threshold: ``t = v / (v + threshold)``. Zero is transparent, the -threshold sits at the ramp's midpoint, and the ramp saturates as values grow. +threshold sits at the ramp's midpoint, and the ramp saturates as values grow. A +threshold at or below zero (a decision threshold at a very low confidence) is the +limit of that scale: every positive value is past it, so the ramp saturates. The scale depends only on the model, never on the image, so the same colour means the same evidence on every image and normal images show their map too. This is local anomaly evidence, not a defect mask. @@ -39,24 +41,28 @@ def normalize_anomaly_map(anomaly_map: np.ndarray, threshold: float) -> np.ndarray: - """Map raw anomaly values onto ``[0, 1)``: 0 at or below zero, 0.5 at the threshold. + """Map raw anomaly values onto ``[0, 1]``: 0 at or below zero, 0.5 at the threshold. Args: anomaly_map (np.ndarray): Raw map in the units of the anomaly score. - threshold (float): The model's decision threshold; must be positive and finite. + threshold (float): The decision threshold anchoring the ramp. At or below + zero every positive value is past it and maps to 1, the limit of + ``v / (v + threshold)`` as the threshold falls to zero. Returns: - A float32 array of the same shape with values in ``[0, 1)``. + A float32 array of the same shape with values in ``[0, 1]``. Raises: - ValueError: If ``threshold`` is not a positive finite number. + ValueError: If ``threshold`` is not finite. """ - if not np.isfinite(threshold) or threshold <= 0: + if not np.isfinite(threshold): raise ValueError( - f"Anomaly threshold must be a positive finite number to anchor the " - f"heatmap ramp, got {threshold!r}" + f"Anomaly threshold must be finite to anchor the heatmap ramp, " + f"got {threshold!r}" ) positive = np.maximum(anomaly_map.astype(np.float32, copy=False), 0.0) + if threshold <= 0: + return (positive > 0).astype(np.float32) normalized = positive / (positive + np.float32(threshold)) return normalized.astype(np.float32, copy=False) diff --git a/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py b/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py index b185c08302..9df1d6d9c6 100644 --- a/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py +++ b/workflows/tests/unit_tests/core_steps/visualizations/test_anomaly_heatmap.py @@ -125,7 +125,21 @@ def test_normalize_anomaly_map_anchors_the_threshold_at_half() -> None: # then np.testing.assert_allclose(normalized, [[0.0, 0.0], [0.5, 0.75]]) with pytest.raises(ValueError): - normalize_anomaly_map(values, threshold=0.0) + normalize_anomaly_map(values, threshold=float("nan")) + + +@pytest.mark.parametrize("threshold", [0.0, -0.5]) +def test_normalize_anomaly_map_saturates_past_a_non_positive_threshold( + threshold: float, +) -> None: + # given: a decision threshold at a very low confidence can reach zero + values = np.array([[-1.0, 0.0], [1e-3, 6.0]], dtype=np.float32) + + # when + normalized = normalize_anomaly_map(values, threshold=threshold) + + # then: every positive value is past the decision; zero stays transparent + np.testing.assert_array_equal(normalized, [[0.0, 0.0], [1.0, 1.0]]) def test_roboflow_ramp_hits_its_stops() -> None: From 72264f84fe880eb07a0e4c03285735656a03e8e9 Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 17:30:04 -0700 Subject: [PATCH 12/17] Skip the anomaly workflow test on the legacy model runtime PatchCore and FoundAD are registered only under USE_INFERENCE_MODELS; the legacy loader falls back to getWeights, which serves a workspace-qualified model only to its own workspace, so the test cannot run on that leg. Co-Authored-By: Claude Opus 5.5 --- .../execution/test_workflow_with_anomaly_detection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py b/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py index 7d143e8e7f..03cf4e93f9 100644 --- a/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py +++ b/tests/workflows/integration_tests/execution/test_workflow_with_anomaly_detection.py @@ -3,6 +3,7 @@ from inference.core.env import ( ENABLE_TENSOR_DATA_REPRESENTATION, + USE_INFERENCE_MODELS, WORKFLOWS_MAX_CONCURRENT_STEPS, ) from inference.core.managers.base import ModelManager @@ -15,6 +16,11 @@ add_to_workflows_gallery, ) +pytestmark = pytest.mark.skipif( + not USE_INFERENCE_MODELS, + reason="PatchCore and FoundAD load only under the inference_models runtime", +) + # Public models of the anomaly reference project, trained on the same data. FOUNDAD_MODEL_ID = "paul-guerrie-tang1/anomaly-det-algbq-1-foundad-t1" PATCHCORE_MODEL_ID = "paul-guerrie-tang1/anomaly-det-algbq-1-patchcore-t2" From 39f66c185bd1e34b3f101a13bcf45068c51ca15d Mon Sep 17 00:00:00 2001 From: Peter Robicheaux Date: Mon, 19 Oct 2026 22:20:54 -0700 Subject: [PATCH 13/17] Bound PatchCore's distance matrix by elements, not rows A 640px PatchCore model has 6400 patches per image and a memory bank of ~140k entries, so a 4096-row chunk was a 2.13 GiB distance matrix plus temporaries per request. Concurrent model-eval requests ran the GPU out of memory and the server answered 500. Co-Authored-By: Claude Opus 5.5 --- .../patchcore_anomaly_detection_torch.py | 22 ++++++++++++------- .../unit_tests/models/test_patchcore_torch.py | 11 +++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py b/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py index 2f6023083f..5e3b63289d 100644 --- a/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py +++ b/inference_models/inference_models/models/patchcore/patchcore_anomaly_detection_torch.py @@ -29,8 +29,10 @@ EMBEDDING_DIMENSION = 1024 MAP_SMOOTHING_SIGMA = 4.0 MAP_SMOOTHING_RADIUS = 16 # scipy.ndimage.gaussian_filter default: truncate=4.0 -# Bounds the (patches x memory bank) distance matrix held in memory at once. -NEAREST_NEIGHBOURS_CHUNK_SIZE = 4096 +# Bounds the (patches x memory bank) distance matrix held in memory at once, in +# elements (256 MiB in fp32). A 640px model has 6400 patches per image and a memory +# bank of ~10^5 entries, so a row count alone would let a single request take GiBs. +NEAREST_NEIGHBOURS_CHUNK_ELEMENTS = 2**26 class PatchCoreModel(nn.Module): @@ -98,12 +100,16 @@ def embed(self, images: torch.Tensor) -> Tuple[torch.Tensor, Tuple[int, int]]: def score_patches(self, embeddings: torch.Tensor) -> torch.Tensor: """Mean squared L2 distance to the nearest memory bank entries.""" scores = [] - for chunk in embeddings.split(NEAREST_NEIGHBOURS_CHUNK_SIZE): - distances = ( - (chunk**2).sum(dim=1, keepdim=True) - - 2 * chunk @ self.memory_bank.T - + self.memory_bank_squared_norms[None] - ).clamp(min=0) + chunk_size = max(1, NEAREST_NEIGHBOURS_CHUNK_ELEMENTS // len(self.memory_bank)) + for chunk in embeddings.split(chunk_size): + # In place, so the chunk's distance matrix is the only one held. + distances = torch.addmm( + self.memory_bank_squared_norms[None], + chunk, + self.memory_bank.T, + alpha=-2, + ) + distances.add_((chunk**2).sum(dim=1, keepdim=True)).clamp_(min=0) nearest = distances.topk(self.neighbors, dim=1, largest=False).values scores.append(nearest.mean(dim=1)) return torch.cat(scores) diff --git a/inference_models/tests/unit_tests/models/test_patchcore_torch.py b/inference_models/tests/unit_tests/models/test_patchcore_torch.py index d09d5b5a68..57fecadb31 100644 --- a/inference_models/tests/unit_tests/models/test_patchcore_torch.py +++ b/inference_models/tests/unit_tests/models/test_patchcore_torch.py @@ -87,9 +87,18 @@ def save_package( return str(package_dir) +@pytest.mark.parametrize( + "chunk_elements", + [patchcore_anomaly_detection_torch.NEAREST_NEIGHBOURS_CHUNK_ELEMENTS, 120, 10], +) def test_patch_scores_are_mean_squared_distances_to_nearest_memory_entries( - tiny_backbone: None, + tiny_backbone: None, monkeypatch: pytest.MonkeyPatch, chunk_elements: int ) -> None: + monkeypatch.setattr( + patchcore_anomaly_detection_torch, + "NEAREST_NEIGHBOURS_CHUNK_ELEMENTS", + chunk_elements, + ) torch.manual_seed(0) memory_bank = torch.randn(50, EMBEDDING_DIMENSION) embeddings = torch.randn(7, EMBEDDING_DIMENSION) From 3e67da6d02fb35c598d556d73d475329dc487a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20P=C4=99czek?= Date: Tue, 20 Oct 2026 12:04:18 +0200 Subject: [PATCH 14/17] Bump inference version and workflows dependency --- inference/core/version.py | 2 +- workflows/pyproject.toml | 2 +- workflows/uv.lock | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/inference/core/version.py b/inference/core/version.py index 68e6fa3378..46cf645d76 100644 --- a/inference/core/version.py +++ b/inference/core/version.py @@ -1,4 +1,4 @@ -__version__ = "1.8.1" +__version__ = "1.8.2" if __name__ == "__main__": diff --git a/workflows/pyproject.toml b/workflows/pyproject.toml index a652f52d16..791ae6ef09 100644 --- a/workflows/pyproject.toml +++ b/workflows/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "torch>=2.0.1,<3.0.0", "torchvision", "inference-models>=0.39.5rc1", - "inference-sdk>=1.7.2", + "inference-sdk>=1.8.0", "msgpack", "websocket-client~=1.9.0", "openai>=1.17.0,<2.0.0", diff --git a/workflows/uv.lock b/workflows/uv.lock index e9734ce418..705a68ae6b 100644 --- a/workflows/uv.lock +++ b/workflows/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10, <3.14" resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'darwin'", @@ -1577,7 +1577,7 @@ wheels = [ [[package]] name = "inference-sdk" -version = "1.7.2" +version = "1.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1594,7 +1594,7 @@ dependencies = [ { name = "urllib3" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/7b/726fc47d541b633c41d7ded6dbad206aee1ec67d4a1df0e6875816d6fcc3/inference_sdk-1.7.2-py3-none-any.whl", hash = "sha256:94711e6e756e26a7c5e62eb58e2110926721df7691ce926d0e219b8352370cb3", size = 105201, upload-time = "2026-09-25T19:24:47.147Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/6fafb460ef153c2d641e35040d4334264a0f8c86d5ce63219eac319e19b3/inference_sdk-1.8.1-py3-none-any.whl", hash = "sha256:5c7293f174532b3523bafa59aa696b909f315b9bfaf6436300acb58e6986d8ae", size = 108404, upload-time = "2026-10-19T15:57:59.291Z" }, ] [[package]] @@ -2945,6 +2945,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] +[[package]] +name = "pkgconfig" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/fd/0adde075cd3bfecd557bc7d757e00e231d34d8a6edb4c8d1642759254c21/pkgconfig-1.6.0.tar.gz", hash = "sha256:4a5a6631ce937fafac457104a40d558785a658bbdca5c49b6295bc3fd651907f", size = 5691, upload-time = "2026-03-06T11:26:01.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/6f/f7ec07fba48f07c555cc4099481df644fbbc12067879072c17ac229f6556/pkgconfig-1.6.0-py3-none-any.whl", hash = "sha256:98e71754855e9563838d952a160eb577edabb57782e49853edb5381927e6bea1", size = 7086, upload-time = "2026-03-06T11:26:07.688Z" }, +] + [[package]] name = "platformdirs" version = "4.12.1" @@ -3669,6 +3678,7 @@ version = "2.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, + { name = "pkgconfig" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/88/f73dae807ec68b228fba72507105e3ba80a561dc0bade0004ce24fd118fc/pyvips-2.2.3.tar.gz", hash = "sha256:43bceced0db492654c93008246a58a508e0373ae1621116b87b322f2ac72212f", size = 56626, upload-time = "2024-04-28T11:19:58.158Z" } @@ -4151,7 +4161,7 @@ requires-dist = [ { name = "filelock", specifier = ">=3.20.3,<=4.0.0" }, { name = "httpx", specifier = "~=0.28.1" }, { name = "inference-models", specifier = ">=0.39.5rc1" }, - { name = "inference-sdk", specifier = ">=1.7.2" }, + { name = "inference-sdk", specifier = ">=1.8.0" }, { name = "matplotlib" }, { name = "msgpack" }, { name = "networkx", specifier = ">=3.0.0,<4.0.0" }, From 774e1adce6747073d1e04c139933c48bf89b9c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20P=C4=99czek?= Date: Tue, 20 Oct 2026 12:11:31 +0200 Subject: [PATCH 15/17] Bump versions and align dependencies --- inference_models/docs/changelog.md | 11 ++++++++++- inference_models/pyproject.toml | 2 +- inference_models/uv.lock | 14 +++++++++++++- requirements/requirements.cpu.txt | 2 +- requirements/requirements.gpu.cu13.txt | 2 +- requirements/requirements.gpu.txt | 2 +- requirements/requirements.jetson.txt | 2 +- requirements/requirements.vino.txt | 2 +- requirements/requirements.workflows.txt | 2 +- workflows/CHANGELOG.md | 12 +++++++++--- workflows/pyproject.toml | 2 +- workflows/uv.lock | 2 +- 12 files changed, 41 insertions(+), 14 deletions(-) diff --git a/inference_models/docs/changelog.md b/inference_models/docs/changelog.md index 7b3e8f182c..265b50ae5f 100644 --- a/inference_models/docs/changelog.md +++ b/inference_models/docs/changelog.md @@ -1,9 +1,10 @@ # Changelog -## Unreleased +## `0.39.5` ### Added +- Anomaly detection models put `anomaly_scale` in `images_metadata[i]`, next to `anomaly_score`, `anomaly_threshold` and `is_anomalous`, so a caller can re-decide against its own threshold with the same rule. - `RoboflowModelMetadata.model_latency_ms` exposes the registry's `modelLatencyMs`. A non-numeric value is read as `None` instead of failing the metadata parse. - `SAM3Torch.segment_with_visual_prompts(...)` accepts `mask_format="rle"` to return @@ -46,6 +47,14 @@ inputs as RGB. - GLM-OCR table recognition sent the formula-recognition prompt. +### Changed + +- Anomaly detection (PatchCore, FoundAD): `is_anomalous` and `class_id` are decided by the request `confidence` — `sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale) >= confidence` — instead of always `anomaly_score >= anomaly_threshold`. `post_process(..., confidence=...)` accepts a number in `[0, 1]`, `"best"` (the model-eval recommendation through `recommended_parameters`, which both models now accept at load) or `"default"` (0.5, the calibrated decision, identical to the previous rule). + +### Fixed + +- PatchCore bounds its nearest-neighbour distance matrix by elements (`NEAREST_NEIGHBOURS_CHUNK_ELEMENTS = 2**26`, 256 MiB in fp32) instead of 4096 rows, and computes it in place. A 640 px model with a ~140k-entry memory bank allocated about 2 GiB per chunk per request and ran out of GPU memory under concurrent requests. + ## `0.39.4` ### Added diff --git a/inference_models/pyproject.toml b/inference_models/pyproject.toml index 661960563a..d33b0a8d71 100644 --- a/inference_models/pyproject.toml +++ b/inference_models/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "inference-models" -version = "0.39.5rc2" +version = "0.39.5" description = "The new inference engine for Computer Vision models" readme = "README.md" requires-python = ">=3.10,<3.14" diff --git a/inference_models/uv.lock b/inference_models/uv.lock index 88965d7af1..598384852e 100644 --- a/inference_models/uv.lock +++ b/inference_models/uv.lock @@ -38,6 +38,8 @@ conflicts = [[ { package = "inference-models", extra = "onnx-jp6-cu126" }, ]] +[manifest] + [[manifest.dependency-metadata]] name = "nvidia-cuda-runtime-cu12" @@ -1349,7 +1351,7 @@ wheels = [ [[package]] name = "inference-models" -version = "0.39.5rc2" +version = "0.39.5" source = { virtual = "." } dependencies = [ { name = "accelerate" }, @@ -4524,6 +4526,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] +[[package]] +name = "pkgconfig" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/fd/0adde075cd3bfecd557bc7d757e00e231d34d8a6edb4c8d1642759254c21/pkgconfig-1.6.0.tar.gz", hash = "sha256:4a5a6631ce937fafac457104a40d558785a658bbdca5c49b6295bc3fd651907f", size = 5691, upload-time = "2026-03-06T11:26:01.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/6f/f7ec07fba48f07c555cc4099481df644fbbc12067879072c17ac229f6556/pkgconfig-1.6.0-py3-none-any.whl", hash = "sha256:98e71754855e9563838d952a160eb577edabb57782e49853edb5381927e6bea1", size = 7086, upload-time = "2026-03-06T11:26:07.688Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -5105,6 +5116,7 @@ version = "2.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, + { name = "pkgconfig" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/88/f73dae807ec68b228fba72507105e3ba80a561dc0bade0004ce24fd118fc/pyvips-2.2.3.tar.gz", hash = "sha256:43bceced0db492654c93008246a58a508e0373ae1621116b87b322f2ac72212f", size = 56626, upload-time = "2024-04-28T11:19:58.158Z" } diff --git a/requirements/requirements.cpu.txt b/requirements/requirements.cpu.txt index 4f53f93faa..5e311683ea 100644 --- a/requirements/requirements.cpu.txt +++ b/requirements/requirements.cpu.txt @@ -1,3 +1,3 @@ onnxruntime>=1.15.1,<1.22.0 nvidia-ml-py<13.0.0 -inference-models[torch-cpu,onnx-cpu]==0.39.5rc2 # keep in sync between requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt +inference-models[torch-cpu,onnx-cpu]==0.39.5 # keep in sync between requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt diff --git a/requirements/requirements.gpu.cu13.txt b/requirements/requirements.gpu.cu13.txt index d8422cf1d4..b87d626190 100644 --- a/requirements/requirements.gpu.cu13.txt +++ b/requirements/requirements.gpu.cu13.txt @@ -4,6 +4,6 @@ # is a CUDA 12 build), so the Dockerfile installs it in a dedicated step from # Microsoft's onnxruntime-cuda-13 feed. A plain pin here would silently # resolve to the CUDA 12 wheel. -inference-models[torch-cu130]==0.39.5rc2 +inference-models[torch-cu130]==0.39.5 pynvvideocodec~=2.1.0; platform_machine == "x86_64" onnxruntime-gpu>=1.27.0,<1.30.0 diff --git a/requirements/requirements.gpu.txt b/requirements/requirements.gpu.txt index 9763e4d5ea..bd7802f176 100644 --- a/requirements/requirements.gpu.txt +++ b/requirements/requirements.gpu.txt @@ -1,3 +1,3 @@ onnxruntime-gpu>=1.15.1,<1.22.0 -inference-models[torch-cu124,onnx-cu12]==0.39.5rc2 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt +inference-models[torch-cu124,onnx-cu12]==0.39.5 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt pynvvideocodec>=2.1.0,<3.0.0; platform_machine == "x86_64" diff --git a/requirements/requirements.jetson.txt b/requirements/requirements.jetson.txt index 912879d821..963e7f13f8 100644 --- a/requirements/requirements.jetson.txt +++ b/requirements/requirements.jetson.txt @@ -1,3 +1,3 @@ pypdfium2>=4.11.0,<5.0.0 PyYAML~=6.0.0 -inference-models==0.39.5rc2 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt +inference-models==0.39.5 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt diff --git a/requirements/requirements.vino.txt b/requirements/requirements.vino.txt index 791da50a3d..2eda4e6873 100644 --- a/requirements/requirements.vino.txt +++ b/requirements/requirements.vino.txt @@ -1,2 +1,2 @@ onnxruntime-openvino>=1.15.0,<1.22.0 -inference-models[torch-cpu]==0.39.5rc2 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt +inference-models[torch-cpu]==0.39.5 # keep in sync between requirements.jetson requirements.gpu.txt, requirements.cpu.txt, requirements.vino.txt diff --git a/requirements/requirements.workflows.txt b/requirements/requirements.workflows.txt index 97d0fa5719..31fe3cf4fd 100644 --- a/requirements/requirements.workflows.txt +++ b/requirements/requirements.workflows.txt @@ -1,2 +1,2 @@ # keep in sync with workflows/pyproject.toml version -roboflow-workflows[enterprise]==0.2.11rc1 +roboflow-workflows[enterprise]==0.2.11 diff --git a/workflows/CHANGELOG.md b/workflows/CHANGELOG.md index 3885db6c49..2818338201 100644 --- a/workflows/CHANGELOG.md +++ b/workflows/CHANGELOG.md @@ -14,12 +14,20 @@ Earlier engine changes and migration guidance remain in the See the [engine release rule](../.cursor/rules/execution-engine-version-changelog.mdc) for contributor and maintainer responsibilities. -## Unreleased +## `0.2.11` + +Bundled execution engine: `1.16.1`. ### Added +- Anomaly Detection Model block (`roboflow_core/anomaly_detection_model@v1`): runs a PatchCore or FoundAD model trained on a Roboflow anomaly detection project (served on the classification route). Outputs the classification-style `predictions` (plus `anomaly_score`, `anomaly_threshold`, `anomaly_scale`, `is_anomalous`), the raw `anomaly_score`, the `anomaly_threshold` in effect, the `anomaly_decision_threshold` (the raw score at which the anomalous confidence equals `confidence`), the `is_anomalous` verdict and the `anomaly_map` (a float32 `numpy_array` at the network input resolution; `None` when `include_anomaly_map` is off). `confidence` (default 0.5, the calibrated decision) is the anomalous confidence an image must reach; `anomaly_threshold` overrides the threshold saved with the model and re-decides with the same rule. `ModelsProvider.run_classification` gains `include_anomaly_map`. +- Anomaly Heatmap Visualization block (`roboflow_core/anomaly_heatmap_visualization@v1`): stretches an anomaly map onto its image (bilinear) and blends it as a heatmap on a ramp anchored at its `anomaly_threshold` input, meant to be wired to the model block's `anomaly_decision_threshold`: `t = max(v, 0) / (max(v, 0) + anchor)`; an anchor at or below zero saturates every positive value. `color_map` is the Roboflow app's ramp (`ROBOFLOW`) or an OpenCV map (`JET`, `TURBO`, `INFERNO`, `VIRIDIS`, `HOT`); `opacity` and `copy_image` as the other visualization blocks. - `roboflow_workflows.http_contract` (`describe`, `entities`, `errors`) — the HTTP surface `inference-server` imports to expose workflow routes. +### Changed + +- `inference-sdk>=1.8.0`: the hosted (API v0) request carries `include_anomaly_map` from 1.8.0, which the Anomaly Detection Model block needs in REMOTE execution. The anomaly blocks need `inference-models` 0.39.5 (`anomaly_scale`). + ## `0.2.10` Bundled execution engine: `1.16.1`. @@ -38,8 +46,6 @@ Bundled execution engine: `1.16.1`. ### Added -- Anomaly Detection Model block (`roboflow_core/anomaly_detection_model@v1`): runs a PatchCore or FoundAD model trained on a Roboflow anomaly detection project and outputs the classification-style `predictions`, the raw `anomaly_score`, the `anomaly_threshold` in effect, the `is_anomalous` verdict and the `anomaly_map` (a float32 `numpy_array` at the network input resolution). `include_anomaly_map` skips the map; `anomaly_threshold` overrides the threshold saved with the model. `ModelsProvider.run_classification` gains `include_anomaly_map`. -- Anomaly Heatmap Visualization block (`roboflow_core/anomaly_heatmap_visualization@v1`): stretches an anomaly map onto its image and blends it as a heatmap on a scale anchored to the threshold (`value / (value + threshold)`), with the Roboflow app's ramp or an OpenCV colour map. - The nine Roboflow platform blocks are core blocks of this package again: dataset upload v1 and v2, custom metadata, model monitoring inference aggregator, vision events, vision event bundle, asset library attributes, visual search and visual search classifier. They live in `core_steps.sinks.roboflow` and `core_steps.integrations.roboflow`, so a standalone install now lists 248 blocks. Inference 1.6.1 to 1.7.2 shipped them as the `inference.roboflow_workflows_plugin` plugin; that plugin is now an empty compatibility stub. - `RoboflowPlatformClient` gains the operations these blocks call: `get_roboflow_workspace`, `add_custom_metadata`, `register_image_at_roboflow`, `annotate_image_at_roboflow`, `update_image_metadata_at_roboflow`, `batch_update_image_metadata_at_roboflow`, `search_project_images_at_roboflow`, `send_inference_results_to_model_monitoring`, `get_device_id`, `get_server_version` and `get_system_info`. The offline client refuses the API operations. A host can register a fallback client for these blocks with `set_platform_blocks_client_factory`, used when a block gets no client, the offline client, or a client that does not implement these operations itself. - Vision events sends its cloud image upload and event request only through a client that implements these operations. With the offline client it returns `error_status` with the offline refusal and sends nothing; writing to a local event store is unaffected. diff --git a/workflows/pyproject.toml b/workflows/pyproject.toml index 791ae6ef09..c74f8f3cf8 100644 --- a/workflows/pyproject.toml +++ b/workflows/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roboflow-workflows" -version = "0.2.11rc1" +version = "0.2.11" description = "Roboflow Workflows execution engine and block library" readme = "README.md" license = {file = "LICENSE"} diff --git a/workflows/uv.lock b/workflows/uv.lock index 705a68ae6b..f65c0350ee 100644 --- a/workflows/uv.lock +++ b/workflows/uv.lock @@ -4086,7 +4086,7 @@ wheels = [ [[package]] name = "roboflow-workflows" -version = "0.2.11rc1" +version = "0.2.11" source = { editable = "." } dependencies = [ { name = "anthropic" }, From ed19a780863b7e566603e89227a04810ffd82fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20P=C4=99czek?= Date: Tue, 20 Oct 2026 12:19:57 +0200 Subject: [PATCH 16/17] Fix tensor-native variant in server --- .../workflows/tensor_native.py | 14 +++- .../workflows/test_tensor_native.py | 77 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/inference_server/inference_server/workflows/tensor_native.py b/inference_server/inference_server/workflows/tensor_native.py index 331bb4221a..0458b3c55d 100644 --- a/inference_server/inference_server/workflows/tensor_native.py +++ b/inference_server/inference_server/workflows/tensor_native.py @@ -48,7 +48,11 @@ "object-detection": _DETECTION_PARAMS | _COLOR_FORMAT_PARAM, "instance-segmentation": _DETECTION_PARAMS | _COLOR_FORMAT_PARAM, "keypoint-detection": _DETECTION_PARAMS | _COLOR_FORMAT_PARAM, - "classification": _DETECTION_PARAMS | _COLOR_FORMAT_PARAM, + # Anomaly detection models (PatchCore, FoundAD) are served as classification; + # the flag asks them for the anomaly map, as on the legacy request path. + "classification": _DETECTION_PARAMS + | _COLOR_FORMAT_PARAM + | frozenset({"include_anomaly_map"}), "multi-label-classification": _DETECTION_PARAMS | _COLOR_FORMAT_PARAM, "semantic-segmentation": frozenset({"confidence"}) | _COLOR_FORMAT_PARAM, "depth-estimation": _COLOR_FORMAT_PARAM, @@ -84,12 +88,20 @@ } +# Prediction fields that stay host-side: a model fills them with numpy arrays +# (e.g. the anomaly map in `ClassificationPrediction.images_metadata`), and the +# tensor blocks read them as numpy, exactly as they do with an in-process model. +_HOST_SIDE_FIELDS = frozenset({"images_metadata"}) + + # NOTE: bfloat16 tensors were widened to float32 on the wire; the original precision is not restored. def numpy_to_tensors(result: Any, device: Any) -> Any: if isinstance(result, np.ndarray): return torch.as_tensor(result, device=device) if dataclasses.is_dataclass(result) and not isinstance(result, type): for field in dataclasses.fields(result): + if field.name in _HOST_SIDE_FIELDS: + continue object.__setattr__( result, field.name, diff --git a/inference_server/tests/unit_tests/workflows/test_tensor_native.py b/inference_server/tests/unit_tests/workflows/test_tensor_native.py index 1b1e982aae..1e5c97ab91 100644 --- a/inference_server/tests/unit_tests/workflows/test_tensor_native.py +++ b/inference_server/tests/unit_tests/workflows/test_tensor_native.py @@ -121,6 +121,83 @@ def test_classification_is_one_batched_prediction(): ) +def test_native_params_forwards_include_anomaly_map_for_classification(): + params = native_params( + "classification", + {"confidence": 0.5, "include_anomaly_map": True, "input_color_format": "bgr"}, + ) + assert params == { + "confidence": 0.5, + "include_anomaly_map": True, + "input_color_format": "bgr", + } + + +def test_native_params_drops_include_anomaly_map_for_other_task_types(): + params = native_params( + "object-detection", + {"confidence": 0.5, "include_anomaly_map": True, "input_color_format": "bgr"}, + ) + assert "include_anomaly_map" not in params + + +def test_numpy_to_tensors_keeps_images_metadata_host_side(): + from inference_models.models.base.classification import ClassificationPrediction + + anomaly_map = np.zeros((4, 4), dtype=np.float32) + restored = numpy_to_tensors( + ClassificationPrediction( + class_id=np.array([1]), + confidence=np.array([[0.2, 0.8]], dtype=np.float32), + images_metadata=[{"anomaly_score": 1.5, "anomaly_map": anomaly_map}], + ), + device=None, + ) + assert isinstance(restored.class_id, torch.Tensor) + assert isinstance(restored.confidence, torch.Tensor) + assert restored.images_metadata[0]["anomaly_map"] is anomaly_map + + +def test_classification_forwards_anomaly_map_flag_and_keeps_the_map_as_numpy(): + from inference_models.models.base.classification import ClassificationPrediction + + bridge = FakeSyncBridge() + bridge.routes["anomaly/1"] = Route( + model_id="anomaly/1", + registry_id="anomaly/1", + task_type="classification", + action="infer", + actions={"infer"}, + class_names=["normal", "anomalous"], + ) + anomaly_map = np.ones((8, 8), dtype=np.float32) + bridge.predictions[("anomaly/1", "infer")] = ClassificationPrediction( + class_id=np.array([1]), + confidence=np.array([[0.3, 0.7]], dtype=np.float32), + images_metadata=[ + { + "anomaly_score": 2.0, + "anomaly_threshold": 1.0, + "anomaly_scale": 0.5, + "is_anomalous": True, + "anomaly_map": anomaly_map, + } + ], + ) + out = GatewayModelsProvider(bridge, api_key="k").run_tensor_native_inference( + model_id="anomaly/1", + images=[np.zeros((2, 2, 3), np.uint8)], + input_color_format="bgr", + include_anomaly_map=True, + confidence=0.5, + ) + assert bridge.calls[0][2]["include_anomaly_map"] is True + assert bridge.calls[0][2]["confidence"] == 0.5 + assert isinstance(out, ClassificationPrediction) + assert isinstance(out.class_id, torch.Tensor) + assert out.images_metadata[0]["anomaly_map"] is anomaly_map + + def test_multi_label_classification_is_one_prediction_per_image(): from inference_models.models.base.classification import ( MultiLabelClassificationPrediction, From be2cdd911550aadf9ffc7ba83fc09afe15fe7f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20P=C4=99czek?= Date: Tue, 20 Oct 2026 12:31:28 +0200 Subject: [PATCH 17/17] Adjust docs --- .../docs/models/foundad-anomaly-detection.md | 17 +++++++++++------ .../docs/models/patchcore-anomaly-detection.md | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/inference_models/docs/models/foundad-anomaly-detection.md b/inference_models/docs/models/foundad-anomaly-detection.md index bd303e448e..c0b7cee41f 100644 --- a/inference_models/docs/models/foundad-anomaly-detection.md +++ b/inference_models/docs/models/foundad-anomaly-detection.md @@ -38,7 +38,7 @@ The package contains the DINOv3 encoder and the predictor. Loading does not down |---------|-----------| | **Training** | ✅ Train custom models on Roboflow | | **Upload Weights** | ❌ Not supported | -| **Workflows** | ✅ Use in [Workflows](https://docs.roboflow.com/workflows) via Classification block | +| **Workflows** | ✅ Use in [Workflows](https://docs.roboflow.com/workflows) via the Anomaly Detection Model block | | **Self-Hosting** | ✅ Deploy with `inference-models` | ## Usage Example @@ -53,11 +53,12 @@ model = AutoModel.from_pretrained( ) image = cv2.imread("path/to/image.jpg") -prediction = model(image, include_anomaly_map=True) +prediction = model(image, include_anomaly_map=True, confidence=0.5) result = prediction.images_metadata[0] print(model.class_names[prediction.class_id[0].item()]) # "normal" or "anomalous" print(result["anomaly_score"], result["anomaly_threshold"], result["is_anomalous"]) +print(result["anomaly_scale"]) # sigmoid((score - threshold) / scale) is the anomalous confidence heatmap = result["anomaly_map"] # float32 array of shape (image_size, image_size) overlay = cv2.resize(heatmap, (image.shape[1], image.shape[0])) # stretch onto the image ``` @@ -66,11 +67,15 @@ overlay = cv2.resize(heatmap, (image.shape[1], image.shape[0])) # stretch onto The model returns a `ClassificationPrediction` over the fixed classes `normal` and `anomalous`: -- `class_id` follows the threshold saved with the model: `anomalous` when `anomaly_score >= anomaly_threshold`. -- `confidence` is a monotonic transform of the score margin. The threshold maps to exactly `0.5`, so the - argmax always agrees with `class_id`. It is not an estimated defect probability. +- `confidence` is the anomalous confidence `sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale)`, + with the threshold and scale calibrated at training, so it is exactly `0.5` at the saved threshold. It is + not an estimated defect probability. +- `class_id` is `anomalous` when that confidence reaches the `confidence` passed to the model: a number in + `[0, 1]`, `"best"` (the model-eval recommendation, the default) or `"default"` (`0.5`, which reproduces the + decision calibrated at training). The argmax of `confidence` agrees with `class_id` only at `0.5`. - `images_metadata[i]` holds `anomaly_score` (raw score, larger means more anomalous), `anomaly_threshold`, - `is_anomalous` and, when `include_anomaly_map=True` is passed, `anomaly_map`. + `anomaly_scale`, `is_anomalous` and, when `include_anomaly_map=True` is passed, `anomaly_map`. A caller can + re-decide against its own threshold with the same rule. The score of an image does not depend on the other images in the batch. The heatmap is local evidence for the score, not a supervised segmentation mask. It is returned at the network input resolution (a square of the diff --git a/inference_models/docs/models/patchcore-anomaly-detection.md b/inference_models/docs/models/patchcore-anomaly-detection.md index 02d93974ac..2951444e7f 100644 --- a/inference_models/docs/models/patchcore-anomaly-detection.md +++ b/inference_models/docs/models/patchcore-anomaly-detection.md @@ -38,7 +38,7 @@ The package contains the full WideResNet50 backbone and the memory bank. Loading |---------|-----------| | **Training** | ✅ Train custom models on Roboflow | | **Upload Weights** | ❌ Not supported | -| **Workflows** | ✅ Use in [Workflows](https://docs.roboflow.com/workflows) via Classification block | +| **Workflows** | ✅ Use in [Workflows](https://docs.roboflow.com/workflows) via the Anomaly Detection Model block | | **Self-Hosting** | ✅ Deploy with `inference-models` | ## Usage Example @@ -53,11 +53,12 @@ model = AutoModel.from_pretrained( ) image = cv2.imread("path/to/image.jpg") -prediction = model(image, include_anomaly_map=True) +prediction = model(image, include_anomaly_map=True, confidence=0.5) result = prediction.images_metadata[0] print(model.class_names[prediction.class_id[0].item()]) # "normal" or "anomalous" print(result["anomaly_score"], result["anomaly_threshold"], result["is_anomalous"]) +print(result["anomaly_scale"]) # sigmoid((score - threshold) / scale) is the anomalous confidence heatmap = result["anomaly_map"] # float32 array of shape (image_size, image_size) overlay = cv2.resize(heatmap, (image.shape[1], image.shape[0])) # stretch onto the image ``` @@ -66,11 +67,15 @@ overlay = cv2.resize(heatmap, (image.shape[1], image.shape[0])) # stretch onto The model returns a `ClassificationPrediction` over the fixed classes `normal` and `anomalous`: -- `class_id` follows the threshold saved with the model: `anomalous` when `anomaly_score >= anomaly_threshold`. -- `confidence` is a monotonic transform of the score margin. The threshold maps to exactly `0.5`, so the - argmax always agrees with `class_id`. It is not an estimated defect probability. +- `confidence` is the anomalous confidence `sigmoid((anomaly_score - anomaly_threshold) / anomaly_scale)`, + with the threshold and scale calibrated at training, so it is exactly `0.5` at the saved threshold. It is + not an estimated defect probability. +- `class_id` is `anomalous` when that confidence reaches the `confidence` passed to the model: a number in + `[0, 1]`, `"best"` (the model-eval recommendation, the default) or `"default"` (`0.5`, which reproduces the + decision calibrated at training). The argmax of `confidence` agrees with `class_id` only at `0.5`. - `images_metadata[i]` holds `anomaly_score` (raw score, larger means more anomalous), `anomaly_threshold`, - `is_anomalous` and, when `include_anomaly_map=True` is passed, `anomaly_map`. + `anomaly_scale`, `is_anomalous` and, when `include_anomaly_map=True` is passed, `anomaly_map`. A caller can + re-decide against its own threshold with the same rule. The score of an image does not depend on the other images in the batch. The heatmap is local evidence for the score, not a supervised segmentation mask. It is returned at the network input resolution (a square of the

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