feat(perception): evaluate fixed-class detector candidates
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Frozen raw-KB4 YOLOX provider for class-agnostic object proposals."""
|
||||
"""Versioned fixed-class detector providers for raw-KB4 object proposals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,10 +14,22 @@ from numpy.typing import NDArray
|
||||
|
||||
from .contracts import BoundingRegion2D, ObjectProposal2D
|
||||
from .providers import SourcePacket
|
||||
from .rf_detr_object_detector import (
|
||||
RF_DETR_CONFIG,
|
||||
RF_DETR_MODEL_ID,
|
||||
RF_DETR_MODEL_VERSION,
|
||||
RfDetrConfig,
|
||||
RfDetrDetection,
|
||||
RfDetrInferenceBackend,
|
||||
postprocess_rf_detr,
|
||||
preprocess_raw_kb4_rf_detr,
|
||||
)
|
||||
from .yolox_object_detector import (
|
||||
ALL_COCO_YOLOX_CONFIG,
|
||||
FROZEN_YOLOX_CONFIG,
|
||||
YOLOX_MODEL_ID,
|
||||
YOLOX_MODEL_VERSION,
|
||||
AllCocoYoloxConfig,
|
||||
FrozenYoloxConfig,
|
||||
ImageResizer,
|
||||
InferenceBackend,
|
||||
@@ -27,8 +39,12 @@ from .yolox_object_detector import (
|
||||
)
|
||||
|
||||
FROZEN_YOLOX_PROVIDER_ID: Final = "triton-yolox-s-raw-kb4/v1"
|
||||
ALL_COCO_YOLOX_PROVIDER_ID: Final = "triton-yolox-s-raw-kb4-all-coco/v2"
|
||||
FROZEN_YOLOX_MODEL_ID: Final = f"{YOLOX_MODEL_ID}:{YOLOX_MODEL_VERSION}"
|
||||
FROZEN_YOLOX_PREPROCESS_ID: Final = "raw-kb4-valid-fov-letterbox/v1"
|
||||
RF_DETR_SHADOW_PROVIDER_ID: Final = "triton-rf-detr-large-coco-risk-fp16-shadow/v0"
|
||||
RF_DETR_SHADOW_MODEL_ID: Final = f"{RF_DETR_MODEL_ID}:{RF_DETR_MODEL_VERSION}"
|
||||
RF_DETR_SHADOW_PREPROCESS_ID: Final = "raw-kb4-valid-fov-rgb-stretch-imagenet/v0"
|
||||
|
||||
|
||||
class DetectorProviderError(RuntimeError):
|
||||
@@ -57,7 +73,7 @@ class FrozenYoloxDetectorProvider:
|
||||
mask: NDArray[np.bool_],
|
||||
backend: InferenceBackend,
|
||||
resizer: ImageResizer | None = None,
|
||||
config: FrozenYoloxConfig = FROZEN_YOLOX_CONFIG,
|
||||
config: FrozenYoloxConfig | AllCocoYoloxConfig = FROZEN_YOLOX_CONFIG,
|
||||
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
||||
) -> None:
|
||||
if mask.shape != (600, 800) or mask.dtype != np.bool_ or not np.any(mask):
|
||||
@@ -95,7 +111,11 @@ class FrozenYoloxDetectorProvider:
|
||||
)
|
||||
output = self.backend.infer(tensor)
|
||||
postprocessed = postprocess_yolox(output, self.mask, config=self.config)
|
||||
proposals = proposals_from_detections(packet, postprocessed.detections)
|
||||
proposals = proposals_from_detections(
|
||||
packet,
|
||||
postprocessed.detections,
|
||||
provider_id=self.provider_id,
|
||||
)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._failed_frames += 1
|
||||
@@ -125,6 +145,8 @@ class FrozenYoloxDetectorProvider:
|
||||
def proposals_from_detections(
|
||||
packet: SourcePacket,
|
||||
detections: tuple[YoloxDetection, ...],
|
||||
*,
|
||||
provider_id: str = FROZEN_YOLOX_PROVIDER_ID,
|
||||
) -> tuple[ObjectProposal2D, ...]:
|
||||
envelope = packet.envelope
|
||||
return tuple(
|
||||
@@ -134,7 +156,7 @@ def proposals_from_detections(
|
||||
frame_id=envelope.frame_id,
|
||||
region=BoundingRegion2D(*detection.bbox_xyxy),
|
||||
objectness=detection.score,
|
||||
provider_id=FROZEN_YOLOX_PROVIDER_ID,
|
||||
provider_id=provider_id,
|
||||
model_id=FROZEN_YOLOX_MODEL_ID,
|
||||
preprocess_id=FROZEN_YOLOX_PREPROCESS_ID,
|
||||
semantic_hint=detection.label,
|
||||
@@ -144,12 +166,140 @@ def proposals_from_detections(
|
||||
)
|
||||
|
||||
|
||||
class AllCocoYoloxDetectorProvider(FrozenYoloxDetectorProvider):
|
||||
"""Emit every qualified COCO class without adding another inference pass."""
|
||||
|
||||
provider_id: str = ALL_COCO_YOLOX_PROVIDER_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
mask: NDArray[np.bool_],
|
||||
backend: InferenceBackend,
|
||||
resizer: ImageResizer | None = None,
|
||||
config: AllCocoYoloxConfig = ALL_COCO_YOLOX_CONFIG,
|
||||
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
mask=mask,
|
||||
backend=backend,
|
||||
resizer=resizer,
|
||||
config=config,
|
||||
clock_ns=clock_ns,
|
||||
)
|
||||
|
||||
|
||||
class RfDetrShadowDetectorProvider:
|
||||
"""Emit behavior-relevant fixed classes from one RF-DETR inference pass."""
|
||||
|
||||
provider_id: str = RF_DETR_SHADOW_PROVIDER_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
mask: NDArray[np.bool_],
|
||||
backend: RfDetrInferenceBackend,
|
||||
resizer: ImageResizer | None = None,
|
||||
config: RfDetrConfig = RF_DETR_CONFIG,
|
||||
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
||||
) -> None:
|
||||
if mask.shape != (600, 800) or mask.dtype != np.bool_ or not np.any(mask):
|
||||
raise DetectorProviderError("RF-DETR valid-FOV mask is incompatible")
|
||||
self.mask = np.asarray(mask, dtype=np.bool_)
|
||||
self.backend = backend
|
||||
self.resizer = resizer
|
||||
self.config = config
|
||||
self._clock_ns = clock_ns
|
||||
self._lock = Lock()
|
||||
self._input_frames = 0
|
||||
self._completed_frames = 0
|
||||
self._failed_frames = 0
|
||||
self._zero_proposal_frames = 0
|
||||
self._proposal_count = 0
|
||||
self._rejected: Counter[str] = Counter()
|
||||
self._core_duration_ns = 0
|
||||
|
||||
def detect(self, packet: SourcePacket) -> tuple[ObjectProposal2D, ...]:
|
||||
payload = packet.image_payload
|
||||
with self._lock:
|
||||
self._input_frames += 1
|
||||
started_ns = int(self._clock_ns())
|
||||
try:
|
||||
if not isinstance(payload, np.ndarray):
|
||||
raise DetectorProviderError("RF-DETR requires a decoded BGR image payload")
|
||||
image = np.asarray(payload)
|
||||
if image.dtype != np.uint8:
|
||||
raise DetectorProviderError("decoded BGR image must be uint8")
|
||||
tensor = preprocess_raw_kb4_rf_detr(
|
||||
image,
|
||||
self.mask,
|
||||
config=self.config,
|
||||
resizer=self.resizer,
|
||||
)
|
||||
output = self.backend.infer(tensor)
|
||||
postprocessed = postprocess_rf_detr(output, self.mask, config=self.config)
|
||||
proposals = proposals_from_rf_detr_detections(packet, postprocessed.detections)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._failed_frames += 1
|
||||
self._core_duration_ns += max(0, int(self._clock_ns()) - started_ns)
|
||||
raise
|
||||
with self._lock:
|
||||
self._completed_frames += 1
|
||||
self._proposal_count += len(proposals)
|
||||
self._zero_proposal_frames += not proposals
|
||||
self._rejected.update(dict(postprocessed.rejected))
|
||||
self._core_duration_ns += max(0, int(self._clock_ns()) - started_ns)
|
||||
return proposals
|
||||
|
||||
def snapshot(self) -> DetectorProviderSnapshot:
|
||||
with self._lock:
|
||||
return DetectorProviderSnapshot(
|
||||
input_frames=self._input_frames,
|
||||
completed_frames=self._completed_frames,
|
||||
failed_frames=self._failed_frames,
|
||||
zero_proposal_frames=self._zero_proposal_frames,
|
||||
proposal_count=self._proposal_count,
|
||||
rejected=tuple(sorted(self._rejected.items())),
|
||||
core_duration_ns=self._core_duration_ns,
|
||||
)
|
||||
|
||||
|
||||
def proposals_from_rf_detr_detections(
|
||||
packet: SourcePacket,
|
||||
detections: tuple[RfDetrDetection, ...],
|
||||
) -> tuple[ObjectProposal2D, ...]:
|
||||
envelope = packet.envelope
|
||||
return tuple(
|
||||
ObjectProposal2D(
|
||||
proposal_id=f"proposal-{envelope.sequence}-{index}",
|
||||
source_id=envelope.source_id,
|
||||
frame_id=envelope.frame_id,
|
||||
region=BoundingRegion2D(*detection.bbox_xyxy),
|
||||
objectness=detection.score,
|
||||
provider_id=RF_DETR_SHADOW_PROVIDER_ID,
|
||||
model_id=RF_DETR_SHADOW_MODEL_ID,
|
||||
preprocess_id=RF_DETR_SHADOW_PREPROCESS_ID,
|
||||
semantic_hint=detection.label,
|
||||
provider_tracklet=None,
|
||||
)
|
||||
for index, detection in enumerate(detections)
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ALL_COCO_YOLOX_PROVIDER_ID",
|
||||
"FROZEN_YOLOX_MODEL_ID",
|
||||
"FROZEN_YOLOX_PREPROCESS_ID",
|
||||
"FROZEN_YOLOX_PROVIDER_ID",
|
||||
"RF_DETR_SHADOW_MODEL_ID",
|
||||
"RF_DETR_SHADOW_PREPROCESS_ID",
|
||||
"RF_DETR_SHADOW_PROVIDER_ID",
|
||||
"DetectorProviderError",
|
||||
"DetectorProviderSnapshot",
|
||||
"AllCocoYoloxDetectorProvider",
|
||||
"FrozenYoloxDetectorProvider",
|
||||
"RfDetrShadowDetectorProvider",
|
||||
"proposals_from_detections",
|
||||
"proposals_from_rf_detr_detections",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
"""Contracts for the bounded M48S fixed-class detector tournament."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
WORKER_RUN_SCHEMA: Final = "missioncore.m48s-fixed-detector-candidate-worker/v0"
|
||||
TOURNAMENT_SCHEMA: Final = "missioncore.m48s-fixed-detector-tournament/v0"
|
||||
EXACT_FRAME_NAMES: Final = (
|
||||
"frame-000121.png",
|
||||
"frame-000131.png",
|
||||
"frame-000253.png",
|
||||
"frame-000275.png",
|
||||
"frame-000443.png",
|
||||
"frame-000463.png",
|
||||
"frame-001094.png",
|
||||
"frame-001228.png",
|
||||
"frame-001454.png",
|
||||
"frame-001856.png",
|
||||
"frame-002386.png",
|
||||
)
|
||||
RISK_GROUPS: Final[Mapping[str, frozenset[str]]] = {
|
||||
"person": frozenset({"person"}),
|
||||
"animal": frozenset(
|
||||
{"bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe"}
|
||||
),
|
||||
"light-road-user": frozenset({"bicycle", "motorcycle", "skateboard"}),
|
||||
"vehicle": frozenset({"car", "bus", "truck"}),
|
||||
}
|
||||
_RISK_LABELS: Final = frozenset().union(*RISK_GROUPS.values())
|
||||
_FALSE_AUTHORITY: Final = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class FixedClassTournamentError(ValueError):
|
||||
"""Raised when tournament evidence violates its bounded contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateDetection:
|
||||
"""One normalized COCO detection emitted by a candidate."""
|
||||
|
||||
class_id: int
|
||||
label: str
|
||||
score: float
|
||||
bbox_xyxy: tuple[float, float, float, float]
|
||||
valid_fov_fraction: float
|
||||
|
||||
@classmethod
|
||||
def from_document(cls, document: object) -> CandidateDetection:
|
||||
item = _mapping(document, "detection")
|
||||
box = item.get("bbox_xyxy")
|
||||
if not isinstance(box, list) or len(box) != 4:
|
||||
raise FixedClassTournamentError("detection bbox_xyxy must contain four numbers")
|
||||
values = (
|
||||
_number(box[0], "bbox coordinate"),
|
||||
_number(box[1], "bbox coordinate"),
|
||||
_number(box[2], "bbox coordinate"),
|
||||
_number(box[3], "bbox coordinate"),
|
||||
)
|
||||
x1, y1, x2, y2 = values
|
||||
if x2 <= x1 or y2 <= y1:
|
||||
raise FixedClassTournamentError("detection box must have positive area")
|
||||
score = _number(item.get("score"), "detection score")
|
||||
valid_fov_fraction = _number(
|
||||
item.get("valid_fov_fraction"), "detection valid-FOV fraction"
|
||||
)
|
||||
if not 0.0 <= score <= 1.0:
|
||||
raise FixedClassTournamentError("detection score must be in [0, 1]")
|
||||
if not 0.0 <= valid_fov_fraction <= 1.0:
|
||||
raise FixedClassTournamentError("valid-FOV fraction must be in [0, 1]")
|
||||
return cls(
|
||||
class_id=_integer(item.get("class_id"), "detection class id"),
|
||||
label=_text(item.get("label"), "detection label"),
|
||||
score=score,
|
||||
bbox_xyxy=values,
|
||||
valid_fov_fraction=valid_fov_fraction,
|
||||
)
|
||||
|
||||
@property
|
||||
def risk_group(self) -> str | None:
|
||||
for group, labels in RISK_GROUPS.items():
|
||||
if self.label in labels:
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateFrame:
|
||||
"""One exact-frame candidate result."""
|
||||
|
||||
frame_name: str
|
||||
source_sha256: str
|
||||
detections: tuple[CandidateDetection, ...]
|
||||
end_to_end_ms: float
|
||||
|
||||
@classmethod
|
||||
def from_document(cls, document: object) -> CandidateFrame:
|
||||
item = _mapping(document, "frame")
|
||||
detections = item.get("detections")
|
||||
if not isinstance(detections, list):
|
||||
raise FixedClassTournamentError("frame detections must be a list")
|
||||
return cls(
|
||||
frame_name=_text(item.get("frame_name"), "frame name"),
|
||||
source_sha256=_digest(item.get("source_sha256"), "source digest"),
|
||||
detections=tuple(CandidateDetection.from_document(value) for value in detections),
|
||||
end_to_end_ms=_nonnegative_number(
|
||||
_mapping(item.get("timing_ms"), "frame timing").get("end_to_end"),
|
||||
"frame end-to-end timing",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateWorkerRun:
|
||||
"""Validated raw Worker result for one candidate."""
|
||||
|
||||
profile_id: str
|
||||
provider_id: str
|
||||
upstream_revision: str
|
||||
checkpoint_sha256: str
|
||||
frames: tuple[CandidateFrame, ...]
|
||||
metrics: Mapping[str, object]
|
||||
authority: Mapping[str, bool]
|
||||
|
||||
@classmethod
|
||||
def from_document(cls, document: object) -> CandidateWorkerRun:
|
||||
root = _mapping(document, "worker result")
|
||||
if root.get("schema_version") != WORKER_RUN_SCHEMA:
|
||||
raise FixedClassTournamentError("unexpected candidate Worker schema")
|
||||
frames_raw = root.get("frames")
|
||||
if not isinstance(frames_raw, list):
|
||||
raise FixedClassTournamentError("worker result frames must be a list")
|
||||
frames = tuple(CandidateFrame.from_document(value) for value in frames_raw)
|
||||
if tuple(sorted(frame.frame_name for frame in frames)) != EXACT_FRAME_NAMES:
|
||||
raise FixedClassTournamentError("worker result does not contain the exact M48S slice")
|
||||
if len({frame.frame_name for frame in frames}) != len(EXACT_FRAME_NAMES):
|
||||
raise FixedClassTournamentError("worker result contains duplicate frames")
|
||||
execution = _mapping(root.get("execution"), "worker execution")
|
||||
if execution.get("inference_passes_per_evidence_frame") != 1:
|
||||
raise FixedClassTournamentError(
|
||||
"candidate must use one inference pass per evidence frame"
|
||||
)
|
||||
authority = _boolean_mapping(root.get("authority"), "worker authority")
|
||||
if authority != _FALSE_AUTHORITY:
|
||||
raise FixedClassTournamentError("candidate Worker result must retain false authority")
|
||||
completed = root.get("completed")
|
||||
if completed is not True:
|
||||
raise FixedClassTournamentError("candidate Worker result is incomplete")
|
||||
return cls(
|
||||
profile_id=_text(root.get("profile_id"), "profile id"),
|
||||
provider_id=_text(root.get("provider_id"), "provider id"),
|
||||
upstream_revision=_text(root.get("upstream_revision"), "upstream revision"),
|
||||
checkpoint_sha256=_digest(root.get("checkpoint_sha256"), "checkpoint digest"),
|
||||
frames=frames,
|
||||
metrics=_mapping(root.get("metrics"), "worker metrics"),
|
||||
authority=authority,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path) -> CandidateWorkerRun:
|
||||
try:
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise FixedClassTournamentError(
|
||||
f"cannot read candidate Worker result: {path}"
|
||||
) from error
|
||||
return cls.from_document(document)
|
||||
|
||||
def quality_summary(self, *, threshold: float) -> dict[str, object]:
|
||||
if not 0.0 <= threshold <= 1.0:
|
||||
raise FixedClassTournamentError("quality threshold must be in [0, 1]")
|
||||
selected = tuple(
|
||||
detection
|
||||
for frame in self.frames
|
||||
for detection in frame.detections
|
||||
if detection.score >= threshold
|
||||
)
|
||||
class_counts = Counter(item.label for item in selected)
|
||||
risk_counts = Counter(item.risk_group for item in selected if item.risk_group is not None)
|
||||
dog_frame = next(frame for frame in self.frames if frame.frame_name == "frame-000253.png")
|
||||
dog_detections = tuple(
|
||||
detection
|
||||
for detection in dog_frame.detections
|
||||
if detection.label == "dog" and detection.score >= threshold
|
||||
)
|
||||
return {
|
||||
"threshold": threshold,
|
||||
"detection_count": len(selected),
|
||||
"class_counts": dict(sorted(class_counts.items())),
|
||||
"risk_group_counts": dict(sorted(risk_counts.items())),
|
||||
"risk_detection_count": sum(1 for item in selected if item.label in _RISK_LABELS),
|
||||
"frame_000253_dog_detected": bool(dog_detections),
|
||||
"frame_000253_dog_max_score": (
|
||||
round(max(item.score for item in dog_detections), 6) if dog_detections else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
"""Return deterministic JSON bytes for immutable evidence identities."""
|
||||
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
"""Hash a file without loading it into memory."""
|
||||
|
||||
digest = hashlib.sha256()
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
except OSError as error:
|
||||
raise FixedClassTournamentError(f"cannot hash evidence file: {path}") from error
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def false_authority() -> dict[str, bool]:
|
||||
"""Return a fresh false-authority document."""
|
||||
|
||||
return dict(_FALSE_AUTHORITY)
|
||||
|
||||
|
||||
def _mapping(value: object, name: str) -> Mapping[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise FixedClassTournamentError(f"{name} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _boolean_mapping(value: object, name: str) -> Mapping[str, bool]:
|
||||
mapping = _mapping(value, name)
|
||||
if set(mapping) != set(_FALSE_AUTHORITY) or not all(
|
||||
isinstance(item, bool) for item in mapping.values()
|
||||
):
|
||||
raise FixedClassTournamentError(f"{name} must contain the exact boolean authority fields")
|
||||
return mapping
|
||||
|
||||
|
||||
def _text(value: object, name: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise FixedClassTournamentError(f"{name} must be non-empty text")
|
||||
return value
|
||||
|
||||
|
||||
def _digest(value: object, name: str) -> str:
|
||||
text = _text(value, name)
|
||||
if len(text) != 64 or any(character not in "0123456789abcdef" for character in text):
|
||||
raise FixedClassTournamentError(f"{name} must be a lowercase SHA-256 digest")
|
||||
return text
|
||||
|
||||
|
||||
def _integer(value: object, name: str) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
raise FixedClassTournamentError(f"{name} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, name: str) -> float:
|
||||
if isinstance(value, bool) or not isinstance(value, int | float):
|
||||
raise FixedClassTournamentError(f"{name} must be numeric")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _nonnegative_number(value: object, name: str) -> float:
|
||||
result = _number(value, name)
|
||||
if result < 0.0:
|
||||
raise FixedClassTournamentError(f"{name} must be non-negative")
|
||||
return result
|
||||
@@ -188,20 +188,26 @@ class DecodedRecordedSource:
|
||||
|
||||
def packets(self, stop_event: Event) -> Iterator[SourcePacket]:
|
||||
images = self.decoder.frames(stop_event)
|
||||
try:
|
||||
image: NDArray[np.uint8] | None = next(images)
|
||||
except StopIteration as exc:
|
||||
if stop_event.is_set():
|
||||
return
|
||||
raise RecordedSourceError("decoded image stream is empty") from exc
|
||||
for packet in self.source.packets(stop_event):
|
||||
try:
|
||||
image = next(images)
|
||||
except StopIteration as exc:
|
||||
if image is None:
|
||||
raise RecordedSourceError(
|
||||
"decoded image stream ended before source timeline"
|
||||
) from exc
|
||||
)
|
||||
if image.shape != (600, 800, 3) or image.dtype != np.uint8:
|
||||
raise RecordedSourceError("decoded image raster is incompatible")
|
||||
yield replace(packet, image_payload=image)
|
||||
if not stop_event.is_set():
|
||||
try:
|
||||
next(images)
|
||||
image = next(images)
|
||||
except StopIteration:
|
||||
image = None
|
||||
if not stop_event.is_set():
|
||||
if image is None:
|
||||
return
|
||||
raise RecordedSourceError("decoded image stream exceeds source timeline")
|
||||
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
"""Pinned RF-DETR-L TensorRT shadow detector for behavior-relevant COCO classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import math
|
||||
import urllib.parse
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, Protocol, cast
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from .yolox_object_detector import COCO_CLASSES, ImageResizer, OpenCvBilinearResizer
|
||||
|
||||
RF_DETR_MODEL_ID: Final = "rf_detr_large"
|
||||
RF_DETR_MODEL_VERSION: Final = 1
|
||||
RF_DETR_CHECKPOINT_SHA256: Final = (
|
||||
"0f4e20e19a99c0f8a62b5685f57f6c8b5c371c59081feda6752a0561a79ccf38"
|
||||
)
|
||||
RF_DETR_ONNX_SHA256: Final = (
|
||||
"9c1948e56bbb6ff03349012b8bb334cacaf8ae480f22caa0704ee70de9a72300"
|
||||
)
|
||||
RF_DETR_FP16_ONNX_SHA256: Final = (
|
||||
"9015fcc1317f268ce866bed6b5a33132c24963e1502b02f145fa184e11de5ecb"
|
||||
)
|
||||
RF_DETR_ENGINE_SHA256: Final = (
|
||||
"986399ce706b7380472cf5e473232249fed6e628971d8007f6609e83128d46b8"
|
||||
)
|
||||
COCO_SPARSE_IDS: Final = (
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
|
||||
43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
|
||||
61, 62, 63, 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
|
||||
82, 84, 85, 86, 87, 88, 89, 90,
|
||||
)
|
||||
COCO_SPARSE_TO_CONTIGUOUS: Final = {
|
||||
sparse_id: contiguous_id for contiguous_id, sparse_id in enumerate(COCO_SPARSE_IDS)
|
||||
}
|
||||
RISK_CLASS_IDS: Final = (
|
||||
0, # person
|
||||
1, # bicycle
|
||||
2, # car
|
||||
3, # motorcycle
|
||||
5, # bus
|
||||
7, # truck
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, # animals
|
||||
36, # skateboard / light road user proxy
|
||||
)
|
||||
_MEANS: Final = np.asarray((0.485, 0.456, 0.406), dtype=np.float32)
|
||||
_STDS: Final = np.asarray((0.229, 0.224, 0.225), dtype=np.float32)
|
||||
|
||||
|
||||
class RfDetrDetectorError(RuntimeError):
|
||||
"""The RF-DETR profile, tensor or inference response is incompatible."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RfDetrConfig:
|
||||
source_width: int = 800
|
||||
source_height: int = 600
|
||||
input_width: int = 704
|
||||
input_height: int = 704
|
||||
fill_value: int = 114
|
||||
minimum_score: float = 0.25
|
||||
target_class_ids: tuple[int, ...] = RISK_CLASS_IDS
|
||||
maximum_detections: int = 300
|
||||
minimum_box_area_pixels: float = 64.0
|
||||
maximum_box_area_fraction: float = 0.5
|
||||
minimum_valid_fov_fraction: float = 0.5
|
||||
require_center_inside_valid_fov: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
self.source_width,
|
||||
self.source_height,
|
||||
self.input_width,
|
||||
self.input_height,
|
||||
self.fill_value,
|
||||
self.minimum_score,
|
||||
self.target_class_ids,
|
||||
self.maximum_detections,
|
||||
self.minimum_box_area_pixels,
|
||||
self.maximum_box_area_fraction,
|
||||
self.minimum_valid_fov_fraction,
|
||||
self.require_center_inside_valid_fov,
|
||||
) != (
|
||||
800,
|
||||
600,
|
||||
704,
|
||||
704,
|
||||
114,
|
||||
0.25,
|
||||
RISK_CLASS_IDS,
|
||||
300,
|
||||
64.0,
|
||||
0.5,
|
||||
0.5,
|
||||
True,
|
||||
):
|
||||
raise RfDetrDetectorError("RF-DETR shadow profile cannot be tuned in place")
|
||||
|
||||
|
||||
RF_DETR_CONFIG: Final = RfDetrConfig()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RfDetrRawOutput:
|
||||
boxes: NDArray[np.float16]
|
||||
logits: NDArray[np.float16]
|
||||
|
||||
|
||||
class RfDetrInferenceBackend(Protocol):
|
||||
def infer(self, tensor: NDArray[np.float32]) -> RfDetrRawOutput: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RfDetrDetection:
|
||||
class_id: int
|
||||
label: str
|
||||
score: float
|
||||
bbox_xyxy: tuple[float, float, float, float]
|
||||
valid_fov_fraction: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 0 <= self.class_id < len(COCO_CLASSES):
|
||||
raise RfDetrDetectorError("RF-DETR class id is invalid")
|
||||
if self.label != COCO_CLASSES[self.class_id]:
|
||||
raise RfDetrDetectorError("RF-DETR class label is invalid")
|
||||
if not math.isfinite(self.score) or not 0.0 <= self.score <= 1.0:
|
||||
raise RfDetrDetectorError("RF-DETR score is invalid")
|
||||
x1, y1, x2, y2 = self.bbox_xyxy
|
||||
if not all(math.isfinite(value) for value in self.bbox_xyxy) or not (
|
||||
0.0 <= x1 < x2 <= 800.0 and 0.0 <= y1 < y2 <= 600.0
|
||||
):
|
||||
raise RfDetrDetectorError("RF-DETR source bounding box is invalid")
|
||||
if not 0.0 <= self.valid_fov_fraction <= 1.0:
|
||||
raise RfDetrDetectorError("RF-DETR valid-FOV fraction is invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RfDetrPostprocessResult:
|
||||
detections: tuple[RfDetrDetection, ...]
|
||||
rejected: tuple[tuple[str, int], ...]
|
||||
|
||||
|
||||
class TritonRfDetrHttpInferenceBackend:
|
||||
"""Persistent Triton V2 HTTP transport for the strongly typed FP16 engine."""
|
||||
|
||||
def __init__(self, endpoint: str, *, timeout_seconds: float = 60.0) -> None:
|
||||
parsed = urllib.parse.urlsplit(endpoint)
|
||||
if (
|
||||
parsed.scheme != "http"
|
||||
or not parsed.hostname
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
or parsed.query
|
||||
or parsed.fragment
|
||||
):
|
||||
raise RfDetrDetectorError("Triton endpoint must be an explicit HTTP origin")
|
||||
if not math.isfinite(timeout_seconds) or timeout_seconds <= 0:
|
||||
raise RfDetrDetectorError("Triton timeout must be positive")
|
||||
self.path = (
|
||||
f"{parsed.path.rstrip('/')}/v2/models/{RF_DETR_MODEL_ID}"
|
||||
f"/versions/{RF_DETR_MODEL_VERSION}/infer"
|
||||
)
|
||||
self.connection = http.client.HTTPConnection(
|
||||
parsed.hostname,
|
||||
parsed.port or 80,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.connection.close()
|
||||
|
||||
def infer(self, tensor: NDArray[np.float32]) -> RfDetrRawOutput:
|
||||
contiguous = np.ascontiguousarray(tensor, dtype=np.float32)
|
||||
if contiguous.shape != (1, 3, 704, 704) or not np.isfinite(contiguous).all():
|
||||
raise RfDetrDetectorError("Triton RF-DETR input tensor is incompatible")
|
||||
binary = contiguous.tobytes()
|
||||
header = {
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"shape": [1, 3, 704, 704],
|
||||
"datatype": "FP32",
|
||||
"parameters": {"binary_data_size": len(binary)},
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "dets", "parameters": {"binary_data": True}},
|
||||
{"name": "labels", "parameters": {"binary_data": True}},
|
||||
],
|
||||
}
|
||||
encoded = json.dumps(header, sort_keys=True, separators=(",", ":")).encode()
|
||||
self.connection.request(
|
||||
"POST",
|
||||
self.path,
|
||||
body=encoded + binary,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Inference-Header-Content-Length": str(len(encoded)),
|
||||
},
|
||||
)
|
||||
response = self.connection.getresponse()
|
||||
payload = response.read()
|
||||
if response.status != 200:
|
||||
raise RfDetrDetectorError(
|
||||
f"Triton RF-DETR inference failed with HTTP {response.status}"
|
||||
)
|
||||
header_value = response.getheader("Inference-Header-Content-Length")
|
||||
try:
|
||||
header_length = int(header_value or "")
|
||||
descriptor = json.loads(payload[:header_length])
|
||||
outputs = descriptor["outputs"]
|
||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||
raise RfDetrDetectorError("Triton RF-DETR output descriptor is invalid") from exc
|
||||
if not isinstance(outputs, list) or len(outputs) != 2:
|
||||
raise RfDetrDetectorError("Triton RF-DETR output count changed")
|
||||
offset = header_length
|
||||
arrays: dict[str, NDArray[np.float16]] = {}
|
||||
for output, expected_name, expected_shape in zip(
|
||||
outputs,
|
||||
("dets", "labels"),
|
||||
((1, 300, 4), (1, 300, 91)),
|
||||
strict=True,
|
||||
):
|
||||
try:
|
||||
name = output["name"]
|
||||
datatype = output["datatype"]
|
||||
shape = tuple(int(value) for value in output["shape"])
|
||||
byte_length = int(output["parameters"]["binary_data_size"])
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise RfDetrDetectorError(
|
||||
"Triton RF-DETR output descriptor is incomplete"
|
||||
) from exc
|
||||
expected_bytes = math.prod(expected_shape) * np.dtype("<f2").itemsize
|
||||
if (
|
||||
name != expected_name
|
||||
or datatype != "FP16"
|
||||
or shape != expected_shape
|
||||
or byte_length != expected_bytes
|
||||
or offset + byte_length > len(payload)
|
||||
):
|
||||
raise RfDetrDetectorError("Triton RF-DETR output identity changed")
|
||||
array = np.frombuffer(payload[offset : offset + byte_length], dtype="<f2")
|
||||
arrays[name] = np.asarray(array.reshape(shape), dtype=np.float16)
|
||||
offset += byte_length
|
||||
if offset != len(payload):
|
||||
raise RfDetrDetectorError("Triton RF-DETR output byte length changed")
|
||||
return RfDetrRawOutput(boxes=arrays["dets"], logits=arrays["labels"])
|
||||
|
||||
|
||||
def preprocess_raw_kb4_rf_detr(
|
||||
image_bgr: NDArray[np.uint8],
|
||||
mask: NDArray[np.bool_],
|
||||
*,
|
||||
config: RfDetrConfig = RF_DETR_CONFIG,
|
||||
resizer: ImageResizer | None = None,
|
||||
) -> NDArray[np.float32]:
|
||||
if image_bgr.shape != (config.source_height, config.source_width, 3):
|
||||
raise RfDetrDetectorError("raw KB4 image raster changed")
|
||||
if image_bgr.dtype != np.uint8 or mask.shape != image_bgr.shape[:2] or mask.dtype != np.bool_:
|
||||
raise RfDetrDetectorError("raw KB4 image or valid-FOV mask type changed")
|
||||
masked_bgr = np.where(mask[..., None], image_bgr, config.fill_value).astype(np.uint8)
|
||||
rgb = np.ascontiguousarray(masked_bgr[:, :, ::-1])
|
||||
resized = (resizer or OpenCvBilinearResizer()).resize(
|
||||
rgb,
|
||||
config.input_width,
|
||||
config.input_height,
|
||||
)
|
||||
if resized.shape != (config.input_height, config.input_width, 3):
|
||||
raise RfDetrDetectorError("resize backend returned an incompatible raster")
|
||||
normalized = resized.astype(np.float32) / 255.0
|
||||
normalized = (normalized - _MEANS) / _STDS
|
||||
return np.ascontiguousarray(normalized.transpose(2, 0, 1), dtype=np.float32)[None]
|
||||
|
||||
|
||||
def postprocess_rf_detr(
|
||||
output: RfDetrRawOutput,
|
||||
mask: NDArray[np.bool_],
|
||||
*,
|
||||
config: RfDetrConfig = RF_DETR_CONFIG,
|
||||
) -> RfDetrPostprocessResult:
|
||||
if output.boxes.shape != (1, 300, 4) or output.logits.shape != (1, 300, 91):
|
||||
raise RfDetrDetectorError("RF-DETR output tensor shapes are incompatible")
|
||||
if output.boxes.dtype != np.float16 or output.logits.dtype != np.float16:
|
||||
raise RfDetrDetectorError("RF-DETR output tensor types are incompatible")
|
||||
if not np.isfinite(output.boxes).all() or not np.isfinite(output.logits).all():
|
||||
raise RfDetrDetectorError("RF-DETR output contains non-finite values")
|
||||
if mask.shape != (config.source_height, config.source_width) or mask.dtype != np.bool_:
|
||||
raise RfDetrDetectorError("valid-FOV mask is incompatible")
|
||||
logits = output.logits[0].astype(np.float32)
|
||||
probabilities = 1.0 / (1.0 + np.exp(-np.clip(logits, -80.0, 80.0)))
|
||||
flattened = probabilities.reshape(-1)
|
||||
topk = np.argsort(-flattened, kind="stable")[: config.maximum_detections]
|
||||
integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
rejected: Counter[str] = Counter()
|
||||
result: list[RfDetrDetection] = []
|
||||
for flat_index in topk:
|
||||
score = float(flattened[flat_index])
|
||||
if score <= config.minimum_score:
|
||||
continue
|
||||
query_index = int(flat_index // output.logits.shape[2])
|
||||
sparse_class_id = int(flat_index % output.logits.shape[2])
|
||||
class_id = COCO_SPARSE_TO_CONTIGUOUS.get(sparse_class_id)
|
||||
if class_id is None:
|
||||
rejected["unmapped-class-slot"] += 1
|
||||
continue
|
||||
if class_id not in config.target_class_ids:
|
||||
rejected["non-risk-class"] += 1
|
||||
continue
|
||||
center_x, center_y, box_width, box_height = (
|
||||
float(value) for value in output.boxes[0, query_index].astype(np.float32)
|
||||
)
|
||||
box = np.asarray(
|
||||
(
|
||||
(center_x - box_width / 2.0) * config.source_width,
|
||||
(center_y - box_height / 2.0) * config.source_height,
|
||||
(center_x + box_width / 2.0) * config.source_width,
|
||||
(center_y + box_height / 2.0) * config.source_height,
|
||||
),
|
||||
dtype=np.float32,
|
||||
)
|
||||
box[[0, 2]] = np.clip(box[[0, 2]], 0, config.source_width)
|
||||
box[[1, 3]] = np.clip(box[[1, 3]], 0, config.source_height)
|
||||
fraction, center_inside, area = _valid_fraction(box, integral)
|
||||
if area < config.minimum_box_area_pixels:
|
||||
rejected["small-box"] += 1
|
||||
continue
|
||||
if area / (config.source_width * config.source_height) > config.maximum_box_area_fraction:
|
||||
rejected["large-box"] += 1
|
||||
continue
|
||||
if fraction < config.minimum_valid_fov_fraction:
|
||||
rejected["outside-valid-fov"] += 1
|
||||
continue
|
||||
if config.require_center_inside_valid_fov and not center_inside:
|
||||
rejected["center-outside-valid-fov"] += 1
|
||||
continue
|
||||
result.append(
|
||||
RfDetrDetection(
|
||||
class_id=class_id,
|
||||
label=COCO_CLASSES[class_id],
|
||||
score=round(score, 9),
|
||||
bbox_xyxy=cast(
|
||||
tuple[float, float, float, float],
|
||||
tuple(round(float(value), 6) for value in box),
|
||||
),
|
||||
valid_fov_fraction=round(fraction, 6),
|
||||
)
|
||||
)
|
||||
result.sort(key=lambda item: (-item.score, item.class_id))
|
||||
return RfDetrPostprocessResult(tuple(result), tuple(sorted(rejected.items())))
|
||||
|
||||
|
||||
def _valid_fraction(
|
||||
box: NDArray[np.float32], integral: NDArray[np.int64]
|
||||
) -> tuple[float, bool, float]:
|
||||
height = integral.shape[0] - 1
|
||||
width = integral.shape[1] - 1
|
||||
x1 = int(np.clip(math.floor(float(box[0])), 0, width))
|
||||
y1 = int(np.clip(math.floor(float(box[1])), 0, height))
|
||||
x2 = int(np.clip(math.ceil(float(box[2])), 0, width))
|
||||
y2 = int(np.clip(math.ceil(float(box[3])), 0, height))
|
||||
area = float(max(0, x2 - x1) * max(0, y2 - y1))
|
||||
if area <= 0:
|
||||
return 0.0, False, 0.0
|
||||
inside = integral[y2, x2] - integral[y1, x2] - integral[y2, x1] + integral[y1, x1]
|
||||
center_x = int(np.clip(round((float(box[0]) + float(box[2])) / 2.0), 0, width - 1))
|
||||
center_y = int(np.clip(round((float(box[1]) + float(box[3])) / 2.0), 0, height - 1))
|
||||
center_inside = bool(
|
||||
integral[center_y + 1, center_x + 1]
|
||||
- integral[center_y, center_x + 1]
|
||||
- integral[center_y + 1, center_x]
|
||||
+ integral[center_y, center_x]
|
||||
)
|
||||
return float(inside) / area, center_inside, area
|
||||
|
||||
|
||||
__all__ = [
|
||||
"COCO_SPARSE_IDS",
|
||||
"RF_DETR_CHECKPOINT_SHA256",
|
||||
"RF_DETR_CONFIG",
|
||||
"RF_DETR_ENGINE_SHA256",
|
||||
"RF_DETR_FP16_ONNX_SHA256",
|
||||
"RF_DETR_MODEL_ID",
|
||||
"RF_DETR_MODEL_VERSION",
|
||||
"RF_DETR_ONNX_SHA256",
|
||||
"RISK_CLASS_IDS",
|
||||
"RfDetrConfig",
|
||||
"RfDetrDetection",
|
||||
"RfDetrDetectorError",
|
||||
"RfDetrInferenceBackend",
|
||||
"RfDetrPostprocessResult",
|
||||
"RfDetrRawOutput",
|
||||
"TritonRfDetrHttpInferenceBackend",
|
||||
"postprocess_rf_detr",
|
||||
"preprocess_raw_kb4_rf_detr",
|
||||
]
|
||||
@@ -43,6 +43,7 @@ COCO_CLASSES: Final = (
|
||||
"oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
|
||||
"scissors", "teddy bear", "hair drier", "toothbrush",
|
||||
)
|
||||
ALL_COCO_CLASS_IDS: Final = tuple(range(len(COCO_CLASSES)))
|
||||
|
||||
|
||||
class YoloxDetectorError(RuntimeError):
|
||||
@@ -93,6 +94,58 @@ class FrozenYoloxConfig:
|
||||
FROZEN_YOLOX_CONFIG: Final = FrozenYoloxConfig()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AllCocoYoloxConfig:
|
||||
"""Versioned all-COCO shadow profile using the exact frozen YOLOX tensor."""
|
||||
|
||||
source_width: int = 800
|
||||
source_height: int = 600
|
||||
input_width: int = 640
|
||||
input_height: int = 640
|
||||
fill_value: int = 114
|
||||
minimum_score: float = 0.5
|
||||
nms_iou_threshold: float = 0.45
|
||||
target_class_ids: tuple[int, ...] = ALL_COCO_CLASS_IDS
|
||||
minimum_box_area_pixels: float = 64.0
|
||||
maximum_box_area_fraction: float = 0.5
|
||||
minimum_valid_fov_fraction: float = 0.5
|
||||
require_center_inside_valid_fov: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
self.source_width,
|
||||
self.source_height,
|
||||
self.input_width,
|
||||
self.input_height,
|
||||
self.fill_value,
|
||||
self.minimum_score,
|
||||
self.nms_iou_threshold,
|
||||
self.target_class_ids,
|
||||
self.minimum_box_area_pixels,
|
||||
self.maximum_box_area_fraction,
|
||||
self.minimum_valid_fov_fraction,
|
||||
self.require_center_inside_valid_fov,
|
||||
) != (
|
||||
800,
|
||||
600,
|
||||
640,
|
||||
640,
|
||||
114,
|
||||
0.5,
|
||||
0.45,
|
||||
ALL_COCO_CLASS_IDS,
|
||||
64.0,
|
||||
0.5,
|
||||
0.5,
|
||||
True,
|
||||
):
|
||||
raise YoloxDetectorError("all-COCO YOLOX profile cannot be tuned in place")
|
||||
|
||||
|
||||
ALL_COCO_YOLOX_CONFIG: Final = AllCocoYoloxConfig()
|
||||
type YoloxPostprocessConfig = FrozenYoloxConfig | AllCocoYoloxConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class YoloxDetection:
|
||||
class_id: int
|
||||
@@ -231,7 +284,7 @@ def preprocess_raw_kb4(
|
||||
image_bgr: NDArray[np.uint8],
|
||||
mask: NDArray[np.bool_],
|
||||
*,
|
||||
config: FrozenYoloxConfig = FROZEN_YOLOX_CONFIG,
|
||||
config: YoloxPostprocessConfig = FROZEN_YOLOX_CONFIG,
|
||||
resizer: ImageResizer | None = None,
|
||||
) -> NDArray[np.float32]:
|
||||
if image_bgr.shape != (config.source_height, config.source_width, 3):
|
||||
@@ -261,7 +314,7 @@ def postprocess_yolox(
|
||||
output: NDArray[np.float32],
|
||||
mask: NDArray[np.bool_],
|
||||
*,
|
||||
config: FrozenYoloxConfig = FROZEN_YOLOX_CONFIG,
|
||||
config: YoloxPostprocessConfig = FROZEN_YOLOX_CONFIG,
|
||||
) -> YoloxPostprocessResult:
|
||||
if output.shape != (1, 8400, 85) or not np.isfinite(output).all():
|
||||
raise YoloxDetectorError("YOLOX output tensor is incompatible")
|
||||
@@ -420,9 +473,10 @@ def _sha256(path: Path) -> str:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ALL_COCO_CLASS_IDS", "ALL_COCO_YOLOX_CONFIG", "COCO_CLASSES",
|
||||
"YOLOX_CONFIG_SHA256", "YOLOX_MODEL_ID", "YOLOX_MODEL_SHA256",
|
||||
"YOLOX_MODEL_VERSION", "YOLOX_VALID_FOV_SHA256", "FROZEN_YOLOX_CONFIG",
|
||||
"FrozenYoloxConfig",
|
||||
"AllCocoYoloxConfig", "FrozenYoloxConfig", "YoloxPostprocessConfig",
|
||||
"ImageResizer", "InferenceBackend", "OpenCvBilinearResizer",
|
||||
"TritonHttpInferenceBackend", "YoloxDetection", "YoloxDetectorError",
|
||||
"YoloxPostprocessResult", "load_valid_fov_mask", "postprocess_yolox",
|
||||
|
||||
Reference in New Issue
Block a user