# -*- coding: utf-8 -*- from __future__ import annotations import math from typing import Dict, List, Tuple, Any import numpy as np def sigmoid(x: np.ndarray) -> np.ndarray: return 1.0 / (1.0 + np.exp(-x)) def temp_scale(p: np.ndarray, temperature: float) -> np.ndarray: p = np.clip(p, 1e-8, 1 - 1e-8) z = np.log(p) - np.log(1 - p) return 1.0 / (1.0 + np.exp(-z / max(temperature, 1e-6))) def xywh2xyxy(xywh: np.ndarray) -> np.ndarray: x, y, w, h = xywh.T return np.stack([x - w/2, y - h/2, x + w/2, y + h/2], axis=1) def nms(boxes: np.ndarray, scores: np.ndarray, iou_thr: float, topk: int) -> List[int]: # simple NMS idxs = scores.argsort()[::-1] keep = [] while idxs.size > 0 and len(keep) < topk: i = idxs[0] keep.append(i) if idxs.size == 1: break ious = iou(boxes[i], boxes[idxs[1:]]) idxs = idxs[1:][ious < iou_thr] return keep def iou(a: np.ndarray, bs: np.ndarray) -> np.ndarray: ax1, ay1, ax2, ay2 = a bx1, by1, bx2, by2 = bs.T ix1, iy1 = np.maximum(ax1, bx1), np.maximum(ay1, by1) ix2, iy2 = np.minimum(ax2, bx2), np.minimum(ay2, by2) iw, ih = np.maximum(0.0, ix2 - ix1), np.maximum(0.0, iy2 - iy1) inter = iw * ih area_a = (ax2 - ax1) * (ay2 - ay1) area_b = (bx2 - bx1) * (by2 - by1) union = area_a + area_b - inter + 1e-9 return inter / union def scale_coords(xyxy: np.ndarray, img_meta: Tuple[int, int], imgsz: int) -> np.ndarray: # img_meta should be (w0, h0). Falls vertauscht geliefert, korrigieren wir unten adaptiv. def _scale(xyxy_in: np.ndarray, w0: int, h0: int) -> np.ndarray: r = min(imgsz / h0, imgsz / w0) nw, nh = int(round(w0 * r)), int(round(h0 * r)) pad_w, pad_h = (imgsz - nw) // 2, (imgsz - nh) // 2 x1, y1, x2, y2 = xyxy_in.T x1 = (x1 - pad_w) / r y1 = (y1 - pad_h) / r x2 = (x2 - pad_w) / r y2 = (y2 - pad_h) / r out = np.stack([x1, y1, x2, y2], axis=1) out[:, 0] = np.clip(out[:, 0], 0, w0 - 1) out[:, 1] = np.clip(out[:, 1], 0, h0 - 1) out[:, 2] = np.clip(out[:, 2], 0, w0 - 1) out[:, 3] = np.clip(out[:, 3], 0, h0 - 1) return out w0, h0 = img_meta out = _scale(xyxy, int(w0), int(h0)) # Heuristik: wenn Breite ~0 aber Höhe deutlich > 0 → probiere vertauschte Reihenfolge (h0,w0) w_pix = out[:, 2] - out[:, 0] h_pix = out[:, 3] - out[:, 1] if out.size and np.median(w_pix) < 2.0 and np.median(h_pix) > 5.0: out_alt = _scale(xyxy, int(h0), int(w0)) w_pix_alt = out_alt[:, 2] - out_alt[:, 0] if np.median(w_pix_alt) > np.median(w_pix): out = out_alt return out def postprocess_yolov8_seg(outputs: Dict[str, np.ndarray], imgsz: int, img_meta: Tuple[int, int], names: List[str], conf_thr: float, iou_thr: float, max_det: int, calibration: Dict[str, Any] | None) -> Dict[str, Any]: """ Minimal post-processor for YOLOv8-Seg ONNX export. Expects Ultralytics-like outputs: out0: [1, N, 84+?] (boxes xywh, objectness, class confs, mask coeffs) out1: proto: [1, C, H/4, W/4] (for masks) Layout can differ across versions/exports; we handle the common path: - boxes xywh in pixel coords on imgsz, then scaled back to original - score = obj * max(class_prob) - optional temperature calibration """ # Heuristic: find 'proto' (largest 4D tensor) and 'pred' (2D/3D) outs = list(outputs.values()) outs.sort(key=lambda a: len(a.shape)) pred = outs[-2] # [1,N,K] proto = outs[-1] # [1,C,H,W] – masks ignored in v1 (optional later) pred = np.squeeze(pred, axis=0) # [N,K] num_classes = len(names) # boxes b = pred[:, :4] # expected xywh # handle normalized boxes in [0..1] if np.nanmax(b) <= 1.5: b = b * float(imgsz) # scores obj = pred[:, 4:5] cls = pred[:, 5:5 + num_classes] cls = np.clip(cls, 0.0, 1.0) obj = np.clip(obj, 0.0, 1.0) cls_idx = cls.argmax(axis=1) cls_prob = cls.max(axis=1, keepdims=True) score = (obj * cls_prob).flatten() # calibration if calibration and "temperature" in calibration: score = temp_scale(score, float(calibration["temperature"])) # threshold m = score >= conf_thr if not np.any(m): return {"boxes": [], "scores": [], "labels": [], "masks": None} b = b[m]; score = score[m]; cls_idx = cls_idx[m] # decode xywh -> xyxy, scale back to original xyxy = xywh2xyxy(b) xyxy_scaled = scale_coords(xyxy, img_meta, imgsz) # heuristic: if widths collapse (~0) but heights not, try yxwh swap w_pix = (xyxy_scaled[:, 2] - xyxy_scaled[:, 0]) h_pix = (xyxy_scaled[:, 3] - xyxy_scaled[:, 1]) if len(w_pix) > 0 and (np.median(w_pix) < 2.0 and np.median(h_pix) > 5.0): b_swapped = b.copy() b_swapped[:, [0, 1]] = b_swapped[:, [1, 0]] # swap x<->y b_swapped[:, [2, 3]] = b_swapped[:, [3, 2]] # swap w<->h xyxy = xywh2xyxy(b_swapped) xyxy_scaled = scale_coords(xyxy, img_meta, imgsz) xyxy = xyxy_scaled # NMS keep = nms(xyxy, score, iou_thr=iou_thr, topk=max_det) xyxy = xyxy[keep]; score = score[keep]; cls_idx = cls_idx[keep] # nicer output xyxy = np.round(xyxy, 2) boxes = xyxy.tolist() scores = score.tolist() labels = [int(c) for c in cls_idx] return {"boxes": boxes, "scores": scores, "labels": labels, "masks": None}