refactor: remove pack-owned classifiers from core API

This commit is contained in:
benjcooley
2026-09-03 14:04:04 -07:00
parent e88c901199
commit 2076700f5e
4 changed files with 0 additions and 729 deletions
-456
View File
@@ -1423,55 +1423,6 @@ class ClipSegRef(_TypedRef):
return result[0], result[1]
class ImageClassifierRef(_TypedRef):
KIND = "IMAGE_CLASSIFIER"
async def classify(
self, images: ImageRef, use_accelerator: bool = True,
top_k: int = 5,
) -> list[list[dict[str, Any]]]:
"""Classify a host-side image batch and return bounded label scores."""
return await current_runtime().ops.apply(
"image_classifier.classify", self, {
"images": images,
"use_accelerator": bool(use_accelerator),
"top_k": int(top_k),
})
async def predict_scores(
self, images: ImageRef,
) -> "ClassifierScoresRef":
"""Run a multi-label classifier and retain its score matrix host-side."""
return await current_runtime().ops.apply(
"image_classifier.predict_scores", self, {"images": images})
class ClassifierScoresRef(_TypedRef):
"""Opaque bounded batch-by-class scores from an image classifier."""
KIND = "CLASSIFIER_SCORES"
async def shape(self) -> tuple[int, int]:
result = await current_runtime().ops.apply(
"classifier_scores.shape", self, {})
return int(result[0]), int(result[1])
async def select_above(
self, batch_index: int, start: int, end: int, threshold: float,
offset: int = 0, limit: int = 512,
) -> dict[str, Any]:
"""Page score/index pairs above a threshold in one class range."""
return await current_runtime().ops.apply(
"classifier_scores.select_above", self, {
"batch_index": int(batch_index),
"start": int(start),
"end": int(end),
"threshold": float(threshold),
"offset": int(offset),
"limit": int(limit),
})
class SemanticSegmentationRef(_TypedRef):
"""Opaque fixed-architecture semantic segmentation model."""
@@ -2314,18 +2265,6 @@ class ModelsDomain(Protocol):
dtype: str = "float16",
) -> PowerPaintRef: ...
async def load_clipseg(self, model: str) -> ClipSegRef: ...
async def load_image_classifier(
self, model: str, architecture: str, labels: list[str],
) -> ImageClassifierRef: ...
async def load_onnx_image_classifier(
self, model: str, input_layout: str = "NHWC",
channel_order: str = "BGR", resize_mode: str = "fit_pad",
input_scale: float = 255.0,
pad_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
mean: tuple[float, float, float] = (0.0, 0.0, 0.0),
std: tuple[float, float, float] = (1.0, 1.0, 1.0),
activation: str = "identity", resize_filter: str = "lanczos",
) -> ImageClassifierRef: ...
async def load_segformer(
self, model: str, variant: str, num_labels: int,
) -> SemanticSegmentationRef: ...
@@ -3268,170 +3207,6 @@ _CLIPSEG_CACHE = WeightCache(
)
@dataclass
class _ImageClassifierEntry:
model: Any
processor: Any
architecture: str
num_labels: int
lock: threading.Lock = field(default_factory=threading.Lock)
def _load_image_classifier_weight(
path: str, architecture: str,
) -> _ImageClassifierEntry:
"""Build one closed image-classifier architecture from SafeTensors."""
import torch
from safetensors.torch import load_file
from transformers import (
BeitConfig,
BeitForImageClassification,
BeitImageProcessor,
ConvNextImageProcessor,
ResNetConfig,
ResNetForImageClassification,
ViTConfig,
ViTForImageClassification,
ViTImageProcessor,
)
state = load_file(path, device="cpu")
if not state:
raise ValueError("classifier SafeTensors file contains no weights")
heads = {
"vit-base-patch16-224": "classifier.weight",
"beit-base-patch16-224": "classifier.weight",
"resnet-50-224": "classifier.1.weight",
}
if architecture not in heads:
raise ValueError("image classifier architecture is not supported")
head = state.get(heads[architecture])
if not isinstance(head, torch.Tensor) or head.ndim != 2:
raise ValueError("classifier weights have no compatible output head")
num_labels = int(head.shape[0])
if not 1 <= num_labels <= 10_000:
raise ValueError("classifier output count is outside the safe range")
floating_dtypes = {
value.dtype for value in state.values()
if isinstance(value, torch.Tensor) and value.is_floating_point()
}
if len(floating_dtypes) != 1:
raise ValueError("classifier weights must use one floating-point dtype")
dtype = next(iter(floating_dtypes))
if dtype not in (torch.float16, torch.bfloat16, torch.float32):
raise ValueError("classifier weights use an unsupported dtype")
if architecture == "vit-base-patch16-224":
config = ViTConfig(
num_labels=num_labels,
attention_probs_dropout_prob=0.0,
encoder_stride=16,
hidden_act="gelu",
hidden_dropout_prob=0.0,
hidden_size=768,
image_size=224,
initializer_range=0.02,
intermediate_size=3072,
layer_norm_eps=1e-12,
num_attention_heads=12,
num_channels=3,
num_hidden_layers=12,
patch_size=16,
qkv_bias=True,
)
model = ViTForImageClassification(config)
processor = ViTImageProcessor(
do_resize=True,
size={"height": 224, "width": 224},
resample=2,
do_rescale=True,
rescale_factor=1.0 / 255.0,
do_normalize=True,
image_mean=(0.5, 0.5, 0.5),
image_std=(0.5, 0.5, 0.5),
)
elif architecture == "beit-base-patch16-224":
config = BeitConfig(
num_labels=num_labels,
attention_probs_dropout_prob=0.0,
drop_path_rate=0.1,
hidden_act="gelu",
hidden_dropout_prob=0.0,
hidden_size=768,
image_size=224,
initializer_range=0.02,
intermediate_size=3072,
layer_norm_eps=1e-12,
layer_scale_init_value=0.1,
num_attention_heads=12,
num_channels=3,
num_hidden_layers=12,
patch_size=16,
use_absolute_position_embeddings=False,
use_mask_token=False,
use_mean_pooling=True,
use_relative_position_bias=True,
use_shared_relative_position_bias=False,
)
model = BeitForImageClassification(config)
processor = BeitImageProcessor(
do_resize=True,
size={"height": 224, "width": 224},
resample=2,
do_rescale=True,
rescale_factor=1.0 / 255.0,
do_normalize=True,
do_center_crop=False,
crop_size={"height": 224, "width": 224},
do_reduce_labels=False,
image_mean=(0.5, 0.5, 0.5),
image_std=(0.5, 0.5, 0.5),
)
else:
config = ResNetConfig(
num_labels=num_labels,
depths=[3, 4, 6, 3],
downsample_in_first_stage=False,
embedding_size=64,
hidden_act="relu",
hidden_sizes=[256, 512, 1024, 2048],
layer_type="bottleneck",
num_channels=3,
out_features=["stage4"],
out_indices=[4],
)
model = ResNetForImageClassification(config)
processor = ConvNextImageProcessor(
do_resize=True,
size={"shortest_edge": 224},
resample=3,
do_rescale=True,
rescale_factor=1.0 / 255.0,
do_normalize=True,
image_mean=(0.485, 0.456, 0.406),
image_std=(0.229, 0.224, 0.225),
)
model = model.to(dtype=dtype)
model.load_state_dict(state, strict=True)
model.eval()
return _ImageClassifierEntry(
model=model,
processor=processor,
architecture=architecture,
num_labels=num_labels,
)
_IMAGE_CLASSIFIER_CACHE = WeightCache(
load=_loader("_load_image_classifier_weight"),
max_entries=3,
release=_release_model_to_cpu,
)
@dataclass
class _TextEncoderEntry:
clip: Any
@@ -3856,99 +3631,6 @@ def _validate_onnx_weight_file(path: str) -> None:
raise ValueError("ONNX model failed structural validation") from exc
@dataclass
class _OnnxImageClassifierEntry:
session: Any
input_name: str
output_name: str
input_height: int
input_width: int
class_count: int
input_layouts: frozenset[str]
lock: threading.Lock = field(default_factory=threading.Lock)
def _load_onnx_image_classifier(path: str) -> _OnnxImageClassifierEntry:
_validate_onnx_weight_file(path)
try:
import onnxruntime as ort
except ImportError as exc:
raise RuntimeError(
"ONNX image classification requires onnxruntime") from exc
options = ort.SessionOptions()
options.log_severity_level = 3
available = set(ort.get_available_providers())
providers = [
provider for provider in (
"CUDAExecutionProvider", "CPUExecutionProvider")
if provider in available
]
if not providers:
raise RuntimeError("ONNX Runtime has no supported execution provider")
try:
session = ort.InferenceSession(
path, sess_options=options, providers=providers)
except Exception as exc:
raise ValueError("ONNX image classifier could not be loaded") from exc
inputs = session.get_inputs()
outputs = session.get_outputs()
if len(inputs) != 1 or len(outputs) != 1:
raise ValueError("ONNX image classifier must have one input and output")
model_input = inputs[0]
model_output = outputs[0]
if model_input.type != "tensor(float)" or model_output.type not in {
"tensor(float)", "tensor(float16)", "tensor(double)",
}:
raise ValueError("ONNX image classifier must use floating-point tensors")
input_shape = model_input.shape
output_shape = model_output.shape
if len(input_shape) != 4 or len(output_shape) != 2:
raise ValueError("ONNX image classifier has an invalid tensor rank")
# WD-style NHWC and common NCHW models are both admitted. The selected
# layout is checked again when the loader binds preprocessing options.
nhwc = input_shape[3] == 3
nchw = input_shape[1] == 3
if not nhwc and not nchw:
raise ValueError("ONNX image classifier must consume three channels")
if nhwc and nchw:
raise ValueError("ONNX classifier channel layout is ambiguous")
height = input_shape[1] if nhwc else input_shape[2]
width = input_shape[2] if nhwc else input_shape[3]
class_count = output_shape[1]
if (type(height) is not int or type(width) is not int
or not 1 <= height <= 4096 or not 1 <= width <= 4096):
raise ValueError("ONNX classifier spatial dimensions must be fixed")
if type(class_count) is not int or not 1 <= class_count <= 16_384:
raise ValueError("ONNX classifier output count is outside the safe range")
return _OnnxImageClassifierEntry(
session=session,
input_name=model_input.name,
output_name=model_output.name,
input_height=height,
input_width=width,
class_count=class_count,
input_layouts=frozenset(
layout for layout, valid in (("NHWC", nhwc), ("NCHW", nchw))
if valid),
)
_ONNX_IMAGE_CLASSIFIER_CACHE = WeightCache(
load=_loader("_load_onnx_image_classifier"), max_entries=3)
@dataclass
class _SamEntry:
model: Any
@@ -5176,8 +4858,6 @@ class _InProcessModels:
_TEXT_GENERATOR_CACHE.clear()
_INPAINT_MODEL_CACHE.clear()
_CLIPSEG_CACHE.clear()
_IMAGE_CLASSIFIER_CACHE.clear()
_ONNX_IMAGE_CLASSIFIER_CACHE.clear()
_TEXT_ENCODER_CACHE.clear()
_LANGUAGE_MODEL_CACHE.clear()
InProcessLlamaCpp().clear()
@@ -5199,128 +4879,6 @@ class _InProcessModels:
return ClipSegRef._wrap(await current_runtime().refs.create(
"CLIPSEGMODEL", value)) # type: ignore[return-value]
async def load_image_classifier(
self, model: str, architecture: str, labels: list[str],
) -> ImageClassifierRef:
import folder_paths
model = self._model_name(model, "image classifier weight")
if not model.lower().endswith(".safetensors"):
raise ValueError("image classifier weights must use SafeTensors")
architecture = str(architecture)
if architecture not in {
"vit-base-patch16-224",
"beit-base-patch16-224",
"resnet-50-224",
}:
raise ValueError("image classifier architecture is not supported")
if not isinstance(labels, (list, tuple)):
raise TypeError("image classifier labels must be a list")
labels = tuple(str(label) for label in labels)
if (not labels or len(labels) > 10_000
or any(not label or len(label) > 256 for label in labels)):
raise ValueError("image classifier labels are invalid")
path = folder_paths.get_full_path_or_raise("detection", model)
entry = await asyncio.to_thread(
_IMAGE_CLASSIFIER_CACHE.get, path, architecture)
if len(labels) != entry.num_labels:
raise ValueError(
"image classifier labels do not match the weight output count")
value = {
"model": entry.model,
"processor": entry.processor,
"architecture": entry.architecture,
"labels": labels,
"lock": entry.lock,
}
return ImageClassifierRef._wrap(await current_runtime().refs.create(
"IMAGE_CLASSIFIER", value)) # type: ignore[return-value]
async def load_onnx_image_classifier(
self, model: str, input_layout: str = "NHWC",
channel_order: str = "BGR", resize_mode: str = "fit_pad",
input_scale: float = 255.0,
pad_color: tuple[float, float, float] = (1.0, 1.0, 1.0),
mean: tuple[float, float, float] = (0.0, 0.0, 0.0),
std: tuple[float, float, float] = (1.0, 1.0, 1.0),
activation: str = "identity", resize_filter: str = "lanczos",
) -> ImageClassifierRef:
"""Bind a self-contained standard ONNX image classifier.
Preprocessing is a closed, reusable transform. Labels, category
ranges, thresholds, exclusions, and output formatting remain node
code; the host only retains and pages the numeric score matrix.
"""
import math
import folder_paths
model = self._model_name(model, "ONNX image classifier")
if not model.lower().endswith(".onnx"):
raise ValueError("ONNX image classifiers must use .onnx files")
input_layout = str(input_layout).upper()
channel_order = str(channel_order).upper()
resize_mode = str(resize_mode).lower()
activation = str(activation).lower()
resize_filter = str(resize_filter).lower()
if input_layout not in {"NHWC", "NCHW"}:
raise ValueError("ONNX classifier layout must be NHWC or NCHW")
if channel_order not in {"RGB", "BGR"}:
raise ValueError("ONNX classifier channel order must be RGB or BGR")
if resize_mode not in {"fit_pad", "stretch"}:
raise ValueError("ONNX classifier resize mode is not supported")
if activation not in {"identity", "sigmoid", "softmax"}:
raise ValueError("ONNX classifier activation is not supported")
if resize_filter not in {"nearest", "bilinear", "bicubic", "lanczos"}:
raise ValueError("ONNX classifier resize filter is not supported")
input_scale = float(input_scale)
if not math.isfinite(input_scale) or not 0 < input_scale <= 65_535:
raise ValueError("ONNX classifier input scale is invalid")
def triple(
value: Any, field_name: str, *, nonzero: bool = False,
unit: bool = False,
) -> tuple[float, float, float]:
if not isinstance(value, (list, tuple)) or len(value) != 3:
raise ValueError(
f"ONNX classifier {field_name} must have three values")
result = tuple(float(item) for item in value)
if (any(not math.isfinite(item) or abs(item) > 1_000_000
for item in result)
or (nonzero and any(item == 0 for item in result))
or (unit and any(not 0 <= item <= 1 for item in result))):
raise ValueError(f"ONNX classifier {field_name} is invalid")
return result # type: ignore[return-value]
pad_color = triple(pad_color, "pad color", unit=True)
mean = triple(mean, "mean")
std = triple(std, "standard deviation", nonzero=True)
path = folder_paths.get_full_path_or_raise("onnx", model)
entry = await asyncio.to_thread(_ONNX_IMAGE_CLASSIFIER_CACHE.get, path)
if input_layout not in entry.input_layouts:
raise ValueError(
f"ONNX classifier tensor is not laid out as {input_layout}")
value = {
"secure_kind": "image_classifier.onnx",
"session": entry.session,
"input_name": entry.input_name,
"output_name": entry.output_name,
"input_height": entry.input_height,
"input_width": entry.input_width,
"class_count": entry.class_count,
"input_layout": input_layout,
"channel_order": channel_order,
"resize_mode": resize_mode,
"input_scale": input_scale,
"pad_color": pad_color,
"mean": mean,
"std": std,
"activation": activation,
"resize_filter": resize_filter,
"lock": entry.lock,
}
return ImageClassifierRef._wrap(await current_runtime().refs.create(
"IMAGE_CLASSIFIER", value)) # type: ignore[return-value]
async def load_segformer(
self, model: str, variant: str, num_labels: int,
) -> SemanticSegmentationRef:
@@ -9696,12 +9254,6 @@ class InProcessOps:
"style_model.apply": self._style_model_apply,
"clipseg.predict_mask": _vendor_ops.clipseg_predict_mask,
"clipseg.segment": _vendor_ops.clipseg_segment,
"image_classifier.classify": _vendor_ops.image_classifier_classify,
"image_classifier.predict_scores":
_vendor_ops.image_classifier_predict_scores,
"classifier_scores.shape": _vendor_ops.classifier_scores_shape,
"classifier_scores.select_above":
_vendor_ops.classifier_scores_select_above,
"semantic_segmentation.mask": _vendor_ops.semantic_segmentation_mask,
"object_detector.detect": self._object_detector_detect,
"inpaint_model.inpaint": _vendor_ops.inpaint_model_inpaint,
@@ -13751,18 +13303,10 @@ def _ref_type_for(v: Any) -> tuple[type, str]:
return SamModelRef, "SAM_MODEL"
if v.get("secure_kind") == "object_detector.rt_detr" and "model" in v:
return ObjectDetectorRef, "OBJECT_DETECTOR"
if v.get("secure_kind") == "classifier_scores.v1" and "scores" in v:
return ClassifierScoresRef, "CLASSIFIER_SCORES"
if v.get("secure_kind") == "image_classifier.onnx" and set(v) >= {
"session", "input_name", "output_name", "class_count", "lock",
}:
return ImageClassifierRef, "IMAGE_CLASSIFIER"
if v.get("secure_kind") == "powerpaint.pipeline" and set(v) >= {
"powerpaint", "clip",
}:
return PowerPaintRef, "POWERPAINT_MODEL"
if set(v) >= {"model", "processor", "architecture", "labels"}:
return ImageClassifierRef, "IMAGE_CLASSIFIER"
if set(v) >= {"model", "processor"}:
return ClipSegRef, "CLIPSEGMODEL"
if "samples" in v:
-4
View File
@@ -17,7 +17,6 @@ from ._sdk import ( # noqa: F401
ControlNetWeightsRef,
Context,
CivitaiDomain,
ClassifierScoresRef,
ClosureRef,
ClosuresDomain,
ExecutionDomain,
@@ -28,7 +27,6 @@ from ._sdk import ( # noqa: F401
GuiderRef,
HuggingFaceWeight,
ImgBBDomain,
ImageClassifierRef,
InpaintModelRef,
ImagePreprocessorRef,
ImageRef,
@@ -81,8 +79,6 @@ __all__ = [
"UpscaleModelRef",
"ValueRef",
"ImageRef",
"ImageClassifierRef",
"ClassifierScoresRef",
"InpaintModelRef",
"ImagePreprocessorRef",
"ImgBBDomain",
-43
View File
@@ -390,30 +390,6 @@ class AssetRef(Ref):
class ClipSegRef(Ref):
KIND: str
class ImageClassifierRef(Ref):
KIND: str
async def classify(
self,
images: ImageRef,
use_accelerator: bool = ...,
top_k: int = ...,
) -> list[list[dict[str, Any]]]: ...
async def predict_scores(
self, images: ImageRef,
) -> ClassifierScoresRef: ...
class ClassifierScoresRef(Ref):
async def shape(self) -> tuple[int, int]: ...
async def select_above(
self,
batch_index: int,
start: int,
end: int,
threshold: float,
offset: int = ...,
limit: int = ...,
) -> dict[str, Any]: ...
class SemanticSegmentationRef(Ref):
KIND: str
async def mask(
@@ -705,19 +681,6 @@ class ModelsDomain(Protocol):
revision: str = ...,
sha256: Optional[str] = ...,
) -> str: ...
async def load_onnx_image_classifier(
self,
model: str,
input_layout: str = ...,
channel_order: str = ...,
resize_mode: str = ...,
input_scale: float = ...,
pad_color: tuple[float, float, float] = ...,
mean: tuple[float, float, float] = ...,
std: tuple[float, float, float] = ...,
activation: str = ...,
resize_filter: str = ...,
) -> ImageClassifierRef: ...
async def list_diffusion_models(
self, include_connectors: bool = ...
) -> list[str]: ...
@@ -797,12 +760,6 @@ class ModelsDomain(Protocol):
dtype: str = ...,
) -> PowerPaintRef: ...
async def load_clipseg(self, model: str) -> ClipSegRef: ...
async def load_image_classifier(
self,
model: str,
architecture: str,
labels: list[str],
) -> ImageClassifierRef: ...
async def load_segformer(
self,
model: str,
-226
View File
@@ -15,10 +15,8 @@ from . import _sdk
if TYPE_CHECKING:
from ._sdk import (
ClassifierScoresRef,
ClipSegRef,
ControlNetWeightsRef,
ImageClassifierRef,
ImagePreprocessorRef,
ImageRef,
InpaintModelRef,
@@ -416,230 +414,6 @@ async def clipseg_predict_mask(clipseg: "ClipSegRef", images: "ImageRef", text:
await rt.refs.create("MASK", result)
) # type: ignore[return-value]
async def image_classifier_classify(classifier: "ImageClassifierRef", images: "ImageRef",
use_accelerator: bool = True, top_k: int = 5,
) -> list[list[dict[str, Any]]]:
from contextlib import nullcontext
import numpy as np
import torch
from PIL import Image
import comfy.model_management
top_k = int(top_k)
if not 1 <= top_k <= 1000:
raise ValueError("image classifier top_k must be in [1, 1000]")
rt = _sdk.current_runtime()
bundle = await rt.refs.resolve(classifier)
pixels = await rt.refs.resolve(images)
if pixels.ndim != 4 or pixels.shape[-1] < 3:
raise ValueError("classifier images must be a non-empty BHWC batch")
if not 1 <= len(pixels) <= 4096:
raise ValueError("classifier batch size must be in [1, 4096]")
labels = tuple(bundle["labels"])
if not labels:
raise ValueError("image classifier has no labels")
model = bundle["model"]
processor = bundle["processor"]
model_lock = bundle.get("lock")
offload_device = comfy.model_management.unet_offload_device()
if use_accelerator:
device = comfy.model_management.get_torch_device()
dtype = comfy.model_management.unet_dtype()
else:
device = torch.device("cpu")
dtype = torch.float32
with model_lock if model_lock is not None else nullcontext():
model.to(dtype).to(device)
try:
source = [Image.fromarray(np.clip(
image.detach().cpu().numpy()[..., :3] * 255.0,
0, 255,
).astype(np.uint8), mode="RGB") for image in pixels]
inputs = processor(images=source, return_tensors="pt")
inputs = {
key: value.to(device) for key, value in inputs.items()
}
autocast = (
dtype != torch.float32
and not comfy.model_management.is_device_mps(device)
)
scope = (
torch.autocast(
comfy.model_management.get_autocast_device(device),
dtype=dtype,
) if autocast else nullcontext()
)
with scope, torch.inference_mode():
logits = model(**inputs).logits
scores = torch.softmax(logits.float(), dim=-1)
finally:
model.to(offload_device)
if scores.ndim != 2 or scores.shape[1] != len(labels):
raise RuntimeError("image classifier returned an invalid score shape")
count = min(top_k, len(labels))
values, indices = torch.topk(scores.cpu(), count, dim=-1)
return [[
{"label": labels[int(index)], "score": float(score)}
for score, index in zip(row_scores, row_indices)
] for row_scores, row_indices in zip(values, indices)]
async def image_classifier_predict_scores(classifier: "ImageClassifierRef", images: "ImageRef",
) -> "ClassifierScoresRef":
import numpy as np
import torch
from PIL import Image
rt = _sdk.current_runtime()
bundle = await rt.refs.resolve(classifier)
if (not isinstance(bundle, dict)
or bundle.get("secure_kind") != "image_classifier.onnx"):
raise TypeError(
"predict_scores requires a validated ONNX image classifier")
pixels = await rt.refs.resolve(images)
if (not isinstance(pixels, torch.Tensor) or pixels.ndim != 4
or pixels.shape[-1] < 3 or not 1 <= len(pixels) <= 64):
raise ValueError(
"ONNX classifier images must be a 1-64 item BHWC RGB batch")
height, width = map(int, pixels.shape[1:3])
if (height <= 0 or width <= 0
or height * width * len(pixels) > 268_435_456
or not bool(torch.isfinite(pixels[..., :3]).all())):
raise ValueError("ONNX classifier image values are invalid")
target_height = int(bundle["input_height"])
target_width = int(bundle["input_width"])
resampling = {
"nearest": Image.Resampling.NEAREST,
"bilinear": Image.Resampling.BILINEAR,
"bicubic": Image.Resampling.BICUBIC,
"lanczos": Image.Resampling.LANCZOS,
}[bundle["resize_filter"]]
pad = tuple(
int(round(float(value) * 255.0)) for value in bundle["pad_color"])
mean = np.asarray(bundle["mean"], dtype=np.float32)
std = np.asarray(bundle["std"], dtype=np.float32)
input_scale = float(bundle["input_scale"])
def infer() -> np.ndarray:
rows = []
with bundle["lock"]:
for frame in pixels:
source_array = np.clip(
frame.detach().cpu().numpy()[..., :3] * 255.0,
0, 255,
).astype(np.uint8)
source = Image.fromarray(source_array)
if bundle["resize_mode"] == "fit_pad":
ratio = min(
target_width / source.width,
target_height / source.height,
)
resized_size = (
max(1, int(source.width * ratio)),
max(1, int(source.height * ratio)),
)
resized = source.resize(resized_size, resampling)
prepared = Image.new(
"RGB", (target_width, target_height), pad)
prepared.paste(resized, (
(target_width - resized_size[0]) // 2,
(target_height - resized_size[1]) // 2,
))
else:
prepared = source.resize(
(target_width, target_height), resampling)
array = np.asarray(prepared, dtype=np.float32)
array = array * (input_scale / 255.0)
if bundle["channel_order"] == "BGR":
array = array[..., ::-1]
array = (array - mean) / std
if bundle["input_layout"] == "NCHW":
array = np.transpose(array, (2, 0, 1))
model_input = np.ascontiguousarray(
array[None, ...], dtype=np.float32)
output = bundle["session"].run(
[bundle["output_name"]],
{bundle["input_name"]: model_input},
)[0]
output = np.asarray(output)
if output.shape != (1, int(bundle["class_count"])):
raise RuntimeError(
"ONNX classifier returned an invalid score shape")
row = output[0].astype(np.float32, copy=False)
if bundle["activation"] == "sigmoid":
row = 1.0 / (1.0 + np.exp(-np.clip(row, -80, 80)))
elif bundle["activation"] == "softmax":
shifted = row - np.max(row)
exponent = np.exp(shifted)
row = exponent / np.sum(exponent)
if not np.isfinite(row).all():
raise RuntimeError(
"ONNX classifier returned non-finite scores")
rows.append(row.astype(np.float32, copy=True))
return np.stack(rows, axis=0)
scores = await asyncio.to_thread(infer)
value = {"secure_kind": "classifier_scores.v1", "scores": scores}
return _sdk.ClassifierScoresRef._wrap(await rt.refs.create(
"CLASSIFIER_SCORES", value)) # type: ignore[return-value]
async def classifier_scores_shape(scores: "ClassifierScoresRef",
) -> tuple[int, int]:
import numpy as np
bundle = await _sdk.current_runtime().refs.resolve(scores)
value = bundle.get("scores") if isinstance(bundle, dict) else None
if (not isinstance(value, np.ndarray) or value.ndim != 2
or not 1 <= value.shape[0] <= 64
or not 1 <= value.shape[1] <= 16_384):
raise TypeError("CLASSIFIER_SCORES handle is invalid")
return int(value.shape[0]), int(value.shape[1])
async def classifier_scores_select_above(scores: "ClassifierScoresRef", batch_index: int,
start: int, end: int, threshold: float,
offset: int = 0, limit: int = 512,
) -> dict[str, Any]:
import math
import numpy as np
bundle = await _sdk.current_runtime().refs.resolve(scores)
value = bundle.get("scores") if isinstance(bundle, dict) else None
if (not isinstance(value, np.ndarray) or value.ndim != 2
or not 1 <= value.shape[0] <= 64
or not 1 <= value.shape[1] <= 16_384):
raise TypeError("CLASSIFIER_SCORES handle is invalid")
batch_index = int(batch_index)
start = int(start)
end = int(end)
offset = int(offset)
limit = int(limit)
threshold = float(threshold)
if not 0 <= batch_index < value.shape[0]:
raise ValueError("classifier score batch index is invalid")
if not 0 <= start <= end <= value.shape[1]:
raise ValueError("classifier score class range is invalid")
if not math.isfinite(threshold) or abs(threshold) > 1_000_000:
raise ValueError("classifier score threshold is invalid")
if not 0 <= offset <= value.shape[1]:
raise ValueError("classifier score page offset is invalid")
if not 1 <= limit <= 512:
raise ValueError("classifier score page limit must be in [1, 512]")
matches = np.flatnonzero(value[batch_index, start:end] > threshold)
matches = matches.astype(np.int64, copy=False) + start
selected = matches[offset:offset + limit]
next_offset = offset + len(selected)
return {
"items": [{
"index": int(index),
"score": float(value[batch_index, index]),
} for index in selected],
"next_offset": (
next_offset if next_offset < len(matches) else None),
}
async def semantic_segmentation_mask(segmentation: "SemanticSegmentationRef", image: "ImageRef",
classes: list[int],
) -> "MaskRef":