Skip to content

Commit ac34301

Browse files
feat(yolo26-sem): YOLO26 semantic segmentation via inference_models (ONNX + TorchScript + TRT) (#2372)
* feat(yolo26-sem): YOLO26 semantic segmentation via inference_models (ONNX + TorchScript) Adds public-pretrained YOLO26-Sem support (Ultralytics 8.4.52/53, Cityscapes pretrains, 1024x1024) to inference through inference_models. This PR covers the ONNX and TorchScript backends; the TRT backend ships in a follow-up once we can build and validate the engines. Pieces: 1. Shared semantic-seg post-processing helper `post_process_semantic_segmentation_logits()` in `inference_models/models/common/roboflow/post_processing.py`. Handles the softmax -> argmax -> letterbox-crop -> resize pipeline. All three DeepLabV3+ backends (ONNX, Torch, TRT) refactored to delegate here, replacing 3 near-identical inline implementations. 2. YOLO26ForSemanticSegmentation{Onnx,TorchScript} classes inheriting SemanticSegmentationModel and delegating post-processing to the shared helper. Reuses INFERENCE_MODELS_YOLO26_DEFAULT_CONFIDENCE (no new constant). 3. Two registry tuples in models_registry.py for (yolo26, semantic-segmentation, {ONNX, TORCH_SCRIPT}). One dispatch entry in inference/models/utils.py routing ("semantic-segmentation", "yolo26") -> existing generic InferenceModelsSemanticSegmentationAdapter. Tests: - Unit: import smoke for ONNX + TorchScript, registry lookup for both backends, synthetic-tensor coverage for the shared post-processing helper (winning-class collapse + sub-threshold background fallback). - Integration ONNX path validated end-to-end against staging for all five sizes (yolo26{n,s,m,l,x}-sem-1024) with the bus.jpg test image through /infer/semantic_segmentation. Follow-up PR adds the TRT backend (class + registry tuple + integration test) once the trt-compiler image is rebuilt against this PR and the engine packages are available to test against. * fix(yolo26-sem): require background class in semantic-seg packages Replace the silent `background_class_id = -1` fallback with a shared `resolve_background_class_id()` helper that raises CorruptedModelPackageError when `class_names.txt` has no `background` entry. A negative background id is never a valid output: it aliases a real class via negative indexing in downstream consumers (`class_names[-1]`, palette LUTs) and breaks the platform 0=background convention, silently corrupting the segmentation map. Failing loud at load time surfaces a misbuilt package instead. The conversion side (roboflow-model-conversion#92) guarantees `background` is prepended, so correctly-built packages are unaffected. Wires the helper into both YOLO26-sem classes and all three DeepLabV3+ backends, removing the duplicated try/except idiom. * feat(yolo26-sem): TRT backend for YOLO26 semantic segmentation (#2379) * fix(yolo26-sem): guard TorchScript load with torchscript_global_lock torch.jit.load shares a non-thread-safe process-global; wrap the load in torchscript_global_lock(torchscript_state_global_lock) and accept the lock in from_pretrained, matching the other YOLO26 TorchScript classes. Addresses review feedback on #2372. * bump requirements inference_models==0.28.7 --------- Co-authored-by: Paweł Pęczek <146137186+PawelPeczek-Roboflow@users.noreply.github.com>
1 parent b872b73 commit ac34301

23 files changed

Lines changed: 1487 additions & 392 deletions

inference/models/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,12 @@ def get_roboflow_model(*args, **kwargs):
990990
category=InferenceModelsStackMissing,
991991
)
992992

993+
# YOLO26 semantic segmentation is inference_models-only (no legacy implementation),
994+
# so we add entries directly rather than swapping existing ones.
995+
ROBOFLOW_MODEL_TYPES[("semantic-segmentation", "yolo26")] = (
996+
InferenceModelsSemanticSegmentationAdapter
997+
)
998+
993999
# YOLOLite is inference_models-only (no legacy implementation),
9941000
# so we add entries directly rather than swapping existing ones.
9951001
for variant in [

inference_models/docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## `0.28.7`
4+
5+
- Added YOLO26 semantic segmentation support (ONNX, TorchScript, and TensorRT backends).
6+
37
## `0.28.6`
48

59
- torch.jit.load/script share a process-global which is not thread-safe, introduced lock to prevent race conditions when loading SAM3 and other torchscript models

inference_models/inference_models/models/auto_loaders/models_registry.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ class RegistryEntry:
255255
module_name="inference_models.models.yolo26.yolo26_instance_segmentation_trt",
256256
class_name="YOLO26ForInstanceSegmentationTRT",
257257
),
258+
("yolo26", SEMANTIC_SEGMENTATION_TASK, BackendType.ONNX): LazyClass(
259+
module_name="inference_models.models.yolo26.yolo26_semantic_segmentation_onnx",
260+
class_name="YOLO26ForSemanticSegmentationOnnx",
261+
),
262+
("yolo26", SEMANTIC_SEGMENTATION_TASK, BackendType.TORCH_SCRIPT): LazyClass(
263+
module_name="inference_models.models.yolo26.yolo26_semantic_segmentation_torch_script",
264+
class_name="YOLO26ForSemanticSegmentationTorchScript",
265+
),
266+
("yolo26", SEMANTIC_SEGMENTATION_TASK, BackendType.TRT): LazyClass(
267+
module_name="inference_models.models.yolo26.yolo26_semantic_segmentation_trt",
268+
class_name="YOLO26ForSemanticSegmentationTRT",
269+
),
258270
("yololite", OBJECT_DETECTION_TASK, BackendType.ONNX): RegistryEntry(
259271
model_class=LazyClass(
260272
module_name="inference_models.models.yololite.yololite_object_detection_onnx",

inference_models/inference_models/models/common/roboflow/model_packages.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ def parse_class_names_file(class_names_path: str) -> List[str]:
2828
) from error
2929

3030

31+
def resolve_background_class_id(class_names: List[str]) -> int:
32+
"""Return the index of the `background` class for a semantic-segmentation
33+
package.
34+
35+
Roboflow semantic segmentation maps every pixel to a class id, with one
36+
class reserved for background (sub-threshold / unlabeled pixels). A valid,
37+
in-range background id is required - a negative sentinel would alias a real
38+
class via negative indexing in downstream consumers (`class_names[-1]`,
39+
palette LUTs, the 0=background platform convention), silently corrupting
40+
the segmentation map. Packages must therefore declare a `background` class.
41+
"""
42+
try:
43+
return [c.lower() for c in class_names].index("background")
44+
except ValueError as error:
45+
raise CorruptedModelPackageError(
46+
message="Semantic segmentation model package does not define a `background` class in "
47+
"`class_names.txt`. A background class is required so that sub-threshold pixels map to a "
48+
"valid class id. If you created the model package manually, prepend `background` to the class "
49+
"names. If the weights are hosted on the Roboflow platform - contact support.",
50+
help_url="https://inference-models.roboflow.com/errors/model-loading/#corruptedmodelpackageerror",
51+
) from error
52+
53+
3154
PADDING_VALUES_MAPPING = {
3255
"black edges": 0,
3356
"grey edges": 127,

inference_models/inference_models/models/common/roboflow/post_processing.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from inference_models.configuration import INFERENCE_MODELS_DEFAULT_CONFIDENCE
88
from inference_models.entities import Confidence, ImageDimensions
99
from inference_models.logger import LOGGER
10+
from inference_models.models.base.semantic_segmentation import (
11+
SemanticSegmentationResult,
12+
)
1013
from inference_models.models.common.rle_utils import torch_mask_to_coco_rle
1114
from inference_models.models.common.roboflow.model_packages import (
1215
PreProcessingMetadata,
@@ -723,3 +726,148 @@ def _resolve_fallback_threshold(
723726
):
724727
return recommended_parameters.confidence
725728
return default_confidence
729+
730+
731+
def post_process_semantic_segmentation_logits(
732+
model_results: torch.Tensor,
733+
pre_processing_meta: List[PreProcessingMetadata],
734+
class_names: List[str],
735+
background_class_id: int,
736+
device: torch.device,
737+
confidence: Confidence,
738+
recommended_parameters: Optional[RecommendedParameters],
739+
default_confidence: float,
740+
) -> List[SemanticSegmentationResult]:
741+
"""Shared post-processing for semantic-segmentation models that emit
742+
(B, K, H, W) float logits. Used by DeepLabV3+ and YOLO26-sem.
743+
744+
Steps: crop out letterbox padding → resize back to pre-letterbox size →
745+
softmax over classes → argmax → place into original-image canvas if
746+
static_crop was applied → apply per-class confidence threshold
747+
(sub-threshold pixels collapse to background_class_id).
748+
"""
749+
confidence_filter = ConfidenceFilter(
750+
confidence=confidence,
751+
recommended_parameters=recommended_parameters,
752+
default_confidence=default_confidence,
753+
)
754+
results: List[SemanticSegmentationResult] = []
755+
for image_results, image_metadata in zip(model_results, pre_processing_meta):
756+
inference_size = image_metadata.inference_size
757+
mask_h_scale = model_results.shape[2] / inference_size.height
758+
mask_w_scale = model_results.shape[3] / inference_size.width
759+
mask_pad_top, mask_pad_bottom, mask_pad_left, mask_pad_right = (
760+
round(mask_h_scale * image_metadata.pad_top),
761+
round(mask_h_scale * image_metadata.pad_bottom),
762+
round(mask_w_scale * image_metadata.pad_left),
763+
round(mask_w_scale * image_metadata.pad_right),
764+
)
765+
_, mh, mw = image_results.shape
766+
if (
767+
mask_pad_top < 0
768+
or mask_pad_bottom < 0
769+
or mask_pad_left < 0
770+
or mask_pad_right < 0
771+
):
772+
image_results = torch.nn.functional.pad(
773+
image_results,
774+
(
775+
abs(min(mask_pad_left, 0)),
776+
abs(min(mask_pad_right, 0)),
777+
abs(min(mask_pad_top, 0)),
778+
abs(min(mask_pad_bottom, 0)),
779+
),
780+
"constant",
781+
background_class_id,
782+
)
783+
padded_mask_offset_top = max(mask_pad_top, 0)
784+
padded_mask_offset_bottom = max(mask_pad_bottom, 0)
785+
padded_mask_offset_left = max(mask_pad_left, 0)
786+
padded_mask_offset_right = max(mask_pad_right, 0)
787+
image_results = image_results[
788+
:,
789+
padded_mask_offset_top : image_results.shape[1]
790+
- padded_mask_offset_bottom,
791+
padded_mask_offset_left : image_results.shape[2]
792+
- padded_mask_offset_right,
793+
]
794+
else:
795+
image_results = image_results[
796+
:,
797+
mask_pad_top : mh - mask_pad_bottom,
798+
mask_pad_left : mw - mask_pad_right,
799+
]
800+
if (
801+
image_results.shape[1] != image_metadata.size_after_pre_processing.height
802+
or image_results.shape[2] != image_metadata.size_after_pre_processing.width
803+
):
804+
image_results = functional.resize(
805+
image_results,
806+
[
807+
image_metadata.size_after_pre_processing.height,
808+
image_metadata.size_after_pre_processing.width,
809+
],
810+
interpolation=functional.InterpolationMode.BILINEAR,
811+
)
812+
image_results = torch.nn.functional.softmax(image_results, dim=0)
813+
image_confidence, image_class_ids = torch.max(image_results, dim=0)
814+
if len(class_names) == image_results.shape[0] + 1:
815+
image_class_ids = image_class_ids + 1
816+
if (
817+
image_metadata.static_crop_offset.offset_x > 0
818+
or image_metadata.static_crop_offset.offset_y > 0
819+
):
820+
original_size_confidence_canvas = torch.zeros(
821+
(
822+
image_metadata.original_size.height,
823+
image_metadata.original_size.width,
824+
),
825+
device=device,
826+
dtype=image_confidence.dtype,
827+
)
828+
original_size_confidence_canvas[
829+
image_metadata.static_crop_offset.offset_y : image_metadata.static_crop_offset.offset_y
830+
+ image_confidence.shape[0],
831+
image_metadata.static_crop_offset.offset_x : image_metadata.static_crop_offset.offset_x
832+
+ image_confidence.shape[1],
833+
] = image_confidence
834+
original_size_confidence_class_id_canvas = (
835+
torch.ones(
836+
(
837+
image_metadata.original_size.height,
838+
image_metadata.original_size.width,
839+
),
840+
device=device,
841+
dtype=image_class_ids.dtype,
842+
)
843+
* background_class_id
844+
)
845+
original_size_confidence_class_id_canvas[
846+
image_metadata.static_crop_offset.offset_y : image_metadata.static_crop_offset.offset_y
847+
+ image_class_ids.shape[0],
848+
image_metadata.static_crop_offset.offset_x : image_metadata.static_crop_offset.offset_x
849+
+ image_class_ids.shape[1],
850+
] = image_class_ids
851+
image_class_ids = original_size_confidence_class_id_canvas
852+
image_confidence = original_size_confidence_canvas
853+
threshold = confidence_filter.get_threshold(class_names)
854+
if isinstance(threshold, torch.Tensor):
855+
threshold = threshold.to(
856+
dtype=image_confidence.dtype, device=image_confidence.device
857+
)
858+
below = image_confidence < (
859+
threshold[image_class_ids.long()]
860+
if isinstance(threshold, torch.Tensor)
861+
else threshold
862+
)
863+
image_class_ids = image_class_ids.clone()
864+
image_confidence = image_confidence.clone()
865+
image_class_ids[below] = background_class_id
866+
image_confidence[below] = 0.0
867+
results.append(
868+
SemanticSegmentationResult(
869+
segmentation_map=image_class_ids,
870+
confidence=image_confidence,
871+
)
872+
)
873+
return results

inference_models/inference_models/models/deep_lab_v3_plus/deep_lab_v3_plus_segmentation_onnx.py

Lines changed: 11 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import List, Optional, Tuple, Union
33

44
import torch
5-
from torchvision.transforms import functional
65

76
from inference_models import ColorFormat, SemanticSegmentationModel
87
from inference_models.configuration import (
@@ -31,8 +30,11 @@
3130
ResizeMode,
3231
parse_class_names_file,
3332
parse_inference_config,
33+
resolve_background_class_id,
34+
)
35+
from inference_models.models.common.roboflow.post_processing import (
36+
post_process_semantic_segmentation_logits,
3437
)
35-
from inference_models.models.common.roboflow.post_processing import ConfidenceFilter
3638
from inference_models.models.common.roboflow.pre_processing import (
3739
pre_process_network_input,
3840
)
@@ -92,10 +94,7 @@ def from_pretrained(
9294
class_names = parse_class_names_file(
9395
class_names_path=model_package_content["class_names.txt"]
9496
)
95-
try:
96-
background_class_id = [c.lower() for c in class_names].index("background")
97-
except ValueError:
98-
background_class_id = -1
97+
background_class_id = resolve_background_class_id(class_names)
9998
inference_config = parse_inference_config(
10099
config_path=model_package_content["inference_config.json"],
101100
allowed_resize_modes={
@@ -194,128 +193,13 @@ def post_process(
194193
confidence: Confidence = "default",
195194
**kwargs,
196195
) -> List[SemanticSegmentationResult]:
197-
confidence_filter = ConfidenceFilter(
196+
return post_process_semantic_segmentation_logits(
197+
model_results=model_results,
198+
pre_processing_meta=pre_processing_meta,
199+
class_names=self._class_names,
200+
background_class_id=self._background_class_id,
201+
device=self._device,
198202
confidence=confidence,
199203
recommended_parameters=self.recommended_parameters,
200204
default_confidence=INFERENCE_MODELS_DEEP_LAB_V3_PLUS_DEFAULT_CONFIDENCE,
201205
)
202-
results = []
203-
for image_results, image_metadata in zip(model_results, pre_processing_meta):
204-
inference_size = image_metadata.inference_size
205-
mask_h_scale = model_results.shape[2] / inference_size.height
206-
mask_w_scale = model_results.shape[3] / inference_size.width
207-
mask_pad_top, mask_pad_bottom, mask_pad_left, mask_pad_right = (
208-
round(mask_h_scale * image_metadata.pad_top),
209-
round(mask_h_scale * image_metadata.pad_bottom),
210-
round(mask_w_scale * image_metadata.pad_left),
211-
round(mask_w_scale * image_metadata.pad_right),
212-
)
213-
_, mh, mw = image_results.shape
214-
if (
215-
mask_pad_top < 0
216-
or mask_pad_bottom < 0
217-
or mask_pad_left < 0
218-
or mask_pad_right < 0
219-
):
220-
image_results = torch.nn.functional.pad(
221-
image_results,
222-
(
223-
abs(min(mask_pad_left, 0)),
224-
abs(min(mask_pad_right, 0)),
225-
abs(min(mask_pad_top, 0)),
226-
abs(min(mask_pad_bottom, 0)),
227-
),
228-
"constant",
229-
self._background_class_id,
230-
)
231-
padded_mask_offset_top = max(mask_pad_top, 0)
232-
padded_mask_offset_bottom = max(mask_pad_bottom, 0)
233-
padded_mask_offset_left = max(mask_pad_left, 0)
234-
padded_mask_offset_right = max(mask_pad_right, 0)
235-
image_results = image_results[
236-
:,
237-
padded_mask_offset_top : image_results.shape[1]
238-
- padded_mask_offset_bottom,
239-
padded_mask_offset_left : image_results.shape[2]
240-
- padded_mask_offset_right,
241-
]
242-
else:
243-
image_results = image_results[
244-
:,
245-
mask_pad_top : mh - mask_pad_bottom,
246-
mask_pad_left : mw - mask_pad_right,
247-
]
248-
if (
249-
image_results.shape[1]
250-
!= image_metadata.size_after_pre_processing.height
251-
or image_results.shape[2]
252-
!= image_metadata.size_after_pre_processing.width
253-
):
254-
image_results = functional.resize(
255-
image_results,
256-
[
257-
image_metadata.size_after_pre_processing.height,
258-
image_metadata.size_after_pre_processing.width,
259-
],
260-
interpolation=functional.InterpolationMode.BILINEAR,
261-
)
262-
image_results = torch.nn.functional.softmax(image_results, dim=0)
263-
image_confidence, image_class_ids = torch.max(image_results, dim=0)
264-
if (
265-
image_metadata.static_crop_offset.offset_x > 0
266-
or image_metadata.static_crop_offset.offset_y > 0
267-
):
268-
original_size_confidence_canvas = torch.zeros(
269-
(
270-
image_metadata.original_size.height,
271-
image_metadata.original_size.width,
272-
),
273-
device=self._device,
274-
dtype=image_confidence.dtype,
275-
)
276-
original_size_confidence_canvas[
277-
image_metadata.static_crop_offset.offset_y : image_metadata.static_crop_offset.offset_y
278-
+ image_confidence.shape[0],
279-
image_metadata.static_crop_offset.offset_x : image_metadata.static_crop_offset.offset_x
280-
+ image_confidence.shape[1],
281-
] = image_confidence
282-
original_size_confidence_class_id_canvas = (
283-
torch.ones(
284-
(
285-
image_metadata.original_size.height,
286-
image_metadata.original_size.width,
287-
),
288-
device=self._device,
289-
dtype=image_class_ids.dtype,
290-
)
291-
* self._background_class_id
292-
)
293-
original_size_confidence_class_id_canvas[
294-
image_metadata.static_crop_offset.offset_y : image_metadata.static_crop_offset.offset_y
295-
+ image_class_ids.shape[0],
296-
image_metadata.static_crop_offset.offset_x : image_metadata.static_crop_offset.offset_x
297-
+ image_class_ids.shape[1],
298-
] = image_class_ids
299-
image_class_ids = original_size_confidence_class_id_canvas
300-
image_confidence = original_size_confidence_canvas
301-
threshold = confidence_filter.get_threshold(self.class_names)
302-
if isinstance(threshold, torch.Tensor):
303-
threshold = threshold.to(
304-
dtype=image_confidence.dtype, device=image_confidence.device
305-
)
306-
below = image_confidence < (
307-
threshold[image_class_ids.long()]
308-
if isinstance(threshold, torch.Tensor)
309-
else threshold
310-
)
311-
image_class_ids = image_class_ids.clone()
312-
image_confidence = image_confidence.clone()
313-
image_class_ids[below] = self._background_class_id
314-
image_confidence[below] = 0.0
315-
results.append(
316-
SemanticSegmentationResult(
317-
segmentation_map=image_class_ids,
318-
confidence=image_confidence,
319-
)
320-
)
321-
return results

0 commit comments

Comments
 (0)