-
Notifications
You must be signed in to change notification settings - Fork 312
Cosmos AnomalyGen: package runtime, packaging tool, workflow-block productionization #2897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
probicheaux
wants to merge
4
commits into
main
Choose a base branch
from
peter/cosmos-anomalygen
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9c5a5c4
Add NVIDIA Cosmos AnomalyGen (image-generation) with workflow block
probicheaux d2c07cc
Add AnomalyGen runtime module, packaging tool, production-recipe defa…
probicheaux 456946e
Fix runtime config override, set_device; declare block model dependency
probicheaux 533aa22
anomalygen: include the dinov2-large correspondence backbone in the p…
probicheaux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
298 changes: 298 additions & 0 deletions
298
inference/core/workflows/core_steps/models/foundation/cosmos_anomalygen/v1.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,298 @@ | ||
| from typing import List, Literal, Optional, Type, Union | ||
|
|
||
| import cv2 | ||
| import numpy as np | ||
| import supervision as sv | ||
| from pydantic import ConfigDict, Field | ||
| from supervision.draw.color import Color | ||
|
|
||
| from inference.core.env import ( | ||
| ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES, | ||
| ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES, | ||
| ) | ||
| from inference.core.roboflow_api import get_extra_weights_provider_headers | ||
| from inference.core.workflows.core_steps.common.entities import StepExecutionMode | ||
| from inference.core.workflows.execution_engine.entities.base import ( | ||
| OutputDefinition, | ||
| WorkflowImageData, | ||
| ) | ||
| from inference.core.workflows.execution_engine.entities.types import ( | ||
| BOOLEAN_KIND, | ||
| FLOAT_KIND, | ||
| IMAGE_KIND, | ||
| INSTANCE_SEGMENTATION_PREDICTION_KIND, | ||
| INTEGER_KIND, | ||
| ROBOFLOW_MODEL_ID_KIND, | ||
| STRING_KIND, | ||
| Selector, | ||
| ) | ||
| from inference.core.workflows.prototypes.block import ( | ||
| BlockResult, | ||
| DependentResource, | ||
| Runtime, | ||
| RuntimeRestriction, | ||
| Severity, | ||
| WorkflowBlock, | ||
| WorkflowBlockManifest, | ||
| roboflow_platform_model, | ||
| ) | ||
|
|
||
| LONG_DESCRIPTION = """ | ||
| Generate synthetic defect images with NVIDIA Cosmos AnomalyGen. | ||
|
|
||
| Given a clean (defect-free) image, a placement mask, and an anomaly type | ||
| (e.g. `wood+crack`), the model inpaints a realistic defect into the mask's | ||
| region. Fine-tuned per-defect checkpoints load through the same model id / | ||
| local package path as the base model. Useful for bootstrapping defect | ||
| detection datasets where real defect data is scarce. | ||
|
|
||
| The placement mask comes in as an instance segmentation prediction - draw it | ||
| upstream (e.g. a polygon zone converted to detections) or chain a | ||
| segmentation model. | ||
|
|
||
| Alongside the generated image the block reports `visibility` - the mean | ||
| absolute pixel change inside the placement mask (0-255 gray levels). The | ||
| model sometimes returns the canvas unchanged; filter or regenerate when | ||
| visibility is low (>=15 separated real defects from empty generations in | ||
| practice). | ||
| """ | ||
|
|
||
|
|
||
| class BlockManifest(WorkflowBlockManifest): | ||
| model_config = ConfigDict( | ||
| json_schema_extra={ | ||
| "name": "Cosmos AnomalyGen", | ||
| "version": "v1", | ||
| "short_description": "Inpaint realistic synthetic defects into clean images.", | ||
| "long_description": LONG_DESCRIPTION, | ||
| "license": "Other", | ||
| "block_type": "model", | ||
| "search_keywords": [ | ||
| "Cosmos", | ||
| "AnomalyGen", | ||
| "NVIDIA", | ||
| "defect", | ||
| "synthetic data", | ||
| "inpainting", | ||
| "anomaly", | ||
| ], | ||
| "ui_manifest": { | ||
| "section": "model", | ||
| "icon": "fal fa-hammer-crash", | ||
| "blockPriority": 5.9, | ||
| }, | ||
| }, | ||
| protected_namespaces=(), | ||
| ) | ||
| type: Literal["roboflow_core/cosmos_anomalygen@v1"] | ||
|
|
||
| image: Selector(kind=[IMAGE_KIND]) = Field( | ||
| description="The clean (defect-free) image to inpaint a defect into.", | ||
| examples=["$inputs.image"], | ||
| ) | ||
| segmentation_mask: Selector(kind=[INSTANCE_SEGMENTATION_PREDICTION_KIND]) = Field( | ||
| name="Placement Mask", | ||
| description="Segmentation prediction marking where the defect should appear.", | ||
| examples=["$steps.model.predictions"], | ||
| ) | ||
| anomaly_type: Union[Selector(kind=[STRING_KIND]), str] = Field( | ||
| description="The trained anomaly type to generate, as `<texture>+<defect_class>`.", | ||
| examples=["wood+crack", "$inputs.anomaly_type"], | ||
| ) | ||
| model_version: Union[Selector(kind=[ROBOFLOW_MODEL_ID_KIND]), str] = Field( | ||
| default="cosmos-anomalygen", | ||
| description="The Cosmos AnomalyGen checkpoint to use (model id or local package path).", | ||
| examples=["cosmos-anomalygen"], | ||
| ) | ||
| guidance: Union[Selector(kind=[FLOAT_KIND]), float] = Field( | ||
| default=1.5, | ||
| description=( | ||
| "Anomaly conditioning strength (upstream extrapolation scale; 1.5 " | ||
| "corresponds to a standard classifier-free-guidance scale of 2.5 and " | ||
| "is NVIDIA's production default). Higher values make defects more " | ||
| "pronounced." | ||
| ), | ||
| examples=[1.5], | ||
| ) | ||
| num_steps: Union[Selector(kind=[INTEGER_KIND]), int] = Field( | ||
| default=35, | ||
| description="Number of denoising steps.", | ||
| examples=[35], | ||
| ) | ||
| seed: Union[Selector(kind=[INTEGER_KIND]), int] = Field( | ||
| default=0, | ||
| description="Random seed for reproducible generation.", | ||
| examples=[0], | ||
| ) | ||
| crop_ratio: Union[Selector(kind=[FLOAT_KIND]), float] = Field( | ||
| default=4.0, | ||
| description=( | ||
| "Size of the generation window relative to the mask's bounding box. " | ||
| "Larger values give the model more context but resample a larger " | ||
| "region; for defects bigger than ~1/4 of the frame a smaller ratio " | ||
| "keeps the surrounding image sharp." | ||
| ), | ||
| examples=[4.0], | ||
| ) | ||
| poisson_blend: Union[Selector(kind=[BOOLEAN_KIND]), bool] = Field( | ||
| default=False, | ||
| description="Poisson-blend the generated region into the original image.", | ||
| examples=[False], | ||
| ) | ||
|
|
||
| @classmethod | ||
| def describe_outputs(cls) -> List[OutputDefinition]: | ||
| return [ | ||
| OutputDefinition( | ||
| name="image", | ||
| kind=[IMAGE_KIND], | ||
| description="The clean image with the synthetic defect inpainted.", | ||
| ), | ||
| OutputDefinition( | ||
| name="visibility", | ||
| kind=[FLOAT_KIND], | ||
| description=( | ||
| "Mean absolute pixel change inside the placement mask " | ||
| "(0-255). Low values mean the model returned the canvas " | ||
| "nearly unchanged; >=15 separated real defects from empty " | ||
| "generations in practice." | ||
| ), | ||
| ), | ||
| ] | ||
|
|
||
| @classmethod | ||
| def get_execution_engine_compatibility(cls) -> Optional[str]: | ||
| return ">=1.3.0,<2.0.0" | ||
|
|
||
| @classmethod | ||
| def get_restrictions(cls) -> List[RuntimeRestriction]: | ||
| return [ | ||
| RuntimeRestriction( | ||
| severity=Severity.HARD, | ||
| note="Requires a GPU; the diffusion model needs CUDA.", | ||
| applies_to_runtimes=[Runtime.SELF_HOSTED_CPU], | ||
| applies_to_step_execution_modes=[StepExecutionMode.LOCAL], | ||
| ), | ||
| RuntimeRestriction( | ||
| severity=Severity.HARD, | ||
| note=( | ||
| "Cosmos AnomalyGen has no remote endpoint - the block loads " | ||
| "the model in-process and only supports local execution." | ||
| ), | ||
| applies_to_runtimes=[Runtime.HOSTED_SERVERLESS], | ||
| applies_to_step_execution_modes=[StepExecutionMode.REMOTE], | ||
| ), | ||
| ] | ||
|
|
||
| @classmethod | ||
| def get_supported_model_variants(cls) -> Optional[List[str]]: | ||
| return ["cosmos-anomalygen"] | ||
|
|
||
| def discover_dependent_resources(self) -> Optional[List[DependentResource]]: | ||
| return [roboflow_platform_model(model_id=self.model_version)] | ||
|
|
||
|
|
||
| class CosmosAnomalyGenBlockV1(WorkflowBlock): | ||
| def __init__( | ||
| self, | ||
| api_key: Optional[str], | ||
| step_execution_mode: StepExecutionMode, | ||
| ): | ||
| if step_execution_mode is not StepExecutionMode.LOCAL: | ||
| raise NotImplementedError( | ||
| "Cosmos AnomalyGen only supports local execution - there is no " | ||
| "remote endpoint for this model." | ||
| ) | ||
| self._api_key = api_key | ||
| self._step_execution_mode = step_execution_mode | ||
| self._model = None | ||
| self._current_model_id: Optional[str] = None | ||
|
|
||
| @classmethod | ||
| def get_init_parameters(cls) -> List[str]: | ||
| return ["api_key", "step_execution_mode"] | ||
|
|
||
| @classmethod | ||
| def get_manifest(cls) -> Type[WorkflowBlockManifest]: | ||
| return BlockManifest | ||
|
|
||
| def run( | ||
| self, | ||
| image: WorkflowImageData, | ||
| segmentation_mask: sv.Detections, | ||
| anomaly_type: str, | ||
| model_version: str, | ||
| guidance: float, | ||
| num_steps: int, | ||
| seed: int, | ||
| crop_ratio: float, | ||
| poisson_blend: bool, | ||
| ) -> BlockResult: | ||
| model = self._resolve_model(model_id=model_version) | ||
| numpy_image = image.numpy_image | ||
| mask = rasterize_placement_mask( | ||
| image=numpy_image, segmentation_mask=segmentation_mask | ||
| ) | ||
| generated = model.generate( | ||
| image=numpy_image, | ||
| mask=mask, | ||
| anomaly_type=anomaly_type, | ||
| guidance=guidance, | ||
| num_steps=num_steps, | ||
| seed=seed, | ||
| num_images=1, | ||
| crop_ratio=crop_ratio, | ||
| poisson_blend=poisson_blend, | ||
| ) | ||
| return { | ||
| "image": WorkflowImageData.copy_and_replace( | ||
| origin_image_data=image, | ||
| numpy_image=generated[0], | ||
| ), | ||
| "visibility": compute_visibility( | ||
| original=numpy_image, generated=generated[0], mask=mask | ||
| ), | ||
| } | ||
|
|
||
| def _resolve_model(self, model_id: str): | ||
| if self._model is None or self._current_model_id != model_id: | ||
| from inference_models import AutoModel | ||
|
|
||
| extra_weights_provider_headers = get_extra_weights_provider_headers() | ||
| self._model = AutoModel.from_pretrained( | ||
| model_id_or_path=model_id, | ||
| api_key=self._api_key, | ||
| allow_untrusted_packages=ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES, | ||
| allow_direct_local_storage_loading=ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES, | ||
| weights_provider_extra_headers=extra_weights_provider_headers, | ||
| ) | ||
| self._current_model_id = model_id | ||
| return self._model | ||
|
|
||
|
|
||
| def rasterize_placement_mask( | ||
| image: np.ndarray, | ||
| segmentation_mask: sv.Detections, | ||
| ) -> np.ndarray: | ||
| black_image = np.zeros_like(image) | ||
| mask_annotator = sv.MaskAnnotator(color=Color.WHITE, opacity=1.0) | ||
| mask = mask_annotator.annotate(black_image, segmentation_mask) | ||
| return cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY) | ||
|
|
||
|
|
||
| def compute_visibility( | ||
| original: np.ndarray, | ||
| generated: np.ndarray, | ||
| mask: np.ndarray, | ||
| ) -> float: | ||
| """Mean absolute gray-level change (0-255) inside the placement mask. | ||
|
|
||
| The model sometimes returns the canvas unchanged; this is the measure | ||
| callers filter or regenerate on. | ||
| """ | ||
| original_gray = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY).astype(np.float32) | ||
| generated_gray = cv2.cvtColor(generated, cv2.COLOR_BGR2GRAY).astype(np.float32) | ||
| inside = mask >= 128 | ||
| if not inside.any(): | ||
| return 0.0 | ||
| return float(np.abs(generated_gray - original_gray)[inside].mean()) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium/High — declaring a model-manager dependency for a model this block loads via
AutoModel, contradicting the sam2_video/sam3_video policy the block claims to follow.This block loads its weights with
AutoModel.from_pretrained(...)in_resolve_model()(in-process, not through the model manager) — exactly likesegment_anything2_video/v1.py. Those blocks deliberately do not implementdiscover_dependent_resources():and are listed in
NON_MODEL_MANAGER_LOADERS_ALLOWLISTintests/workflows/unit_tests/core_steps/test_dependent_resources.py.By returning
roboflow_platform_model(model_id=...)here (defaulting torequired_action=EXECUTION→execution_location=ENVIRONMENT_DEFINED), this block opts into model-manager pre-loading. Concrete failure path: anInferencePipelineinitialized withworkflows_dependencies_pre_init=["roboflow_platform_model"]under LOCAL step execution →_is_locally_executed_platform_model()returnsTrue(EXECUTION + ENVIRONMENT_DEFINED + LOCAL) →_pre_load_roboflow_platform_models()callsmodel_manager.add_model(model_id="cosmos-anomalygen", ...)(inference/core/workflows/execution_engine/v1/core.py:191). Butcosmos-anomalygenis served only through theinference_modelsAutoModelpath (custom backend,generate()contract) — it is not a model-manager model, soadd_modelregisters the wrong thing / fails, and the block never uses that registration at runtime anyway (it re-loads viaAutoModel).Note CI is green here:
test_every_block_with_resource_kind_fields_declares_dependenciesonly checks that aroboflow_model_id-field block either overridesdiscover_dependent_resources()or is allowlisted — so declaring the override silences the guard without making the pre-load path correct.Recommend matching the cited precedent: drop this override (leave dependencies undeclared /
None) and addroboflow_core/cosmos_anomalygen@v1toNON_MODEL_MANAGER_LOADERS_ALLOWLIST. If instead the intent is genuine model-manager execution, the block must actually run through the model manager rather thanAutoModel.Reviewed at HEAD: 456946e