Download code/python/utils.py from APLUX-ModelFarm/YOLO26l-OBB: direct link, hf CLI and curl.
- Browser
- Download file 8.82 kB
-
https://huggingface.co/APLUX-ModelFarm/YOLO26l-OBB/resolve/main/code/python/utils.py
- Command line
-
hf download hf://APLUX-ModelFarm/YOLO26l-OBB/code/python/utils.py
-
curl -L -o utils.py https://huggingface.co/APLUX-ModelFarm/YOLO26l-OBB/resolve/main/code/python/utils.py
8.82 kB
| from __future__ import annotations | |
| from typing import Iterable, List, Sequence, Tuple | |
| import cv2 | |
| import numpy as np | |
| # DOTAv1 classes, in the order encoded by the exported model. | |
| CLASS_NAMES: Tuple[str, ...] = ( | |
| "plane", "ship", "storage tank", "baseball diamond", "tennis court", | |
| "basketball court", "ground track field", "harbor", "bridge", | |
| "large vehicle", "small vehicle", "helicopter", "roundabout", | |
| "soccer ball field", "swimming pool", | |
| ) | |
| # One BGR colour per class; distinct enough to tell adjacent classes apart. | |
| CLASS_COLORS: Tuple[Tuple[int, int, int], ...] = ( | |
| (0, 215, 255), (0, 140, 255), (255, 200, 0), (255, 120, 200), | |
| (90, 230, 0), (255, 90, 140), (200, 200, 0), (128, 0, 255), | |
| (0, 255, 180), (255, 0, 0), (0, 0, 255), (60, 76, 231), | |
| (255, 255, 0), (0, 165, 255), (147, 20, 255), | |
| ) | |
| LABEL_FG = (255, 255, 255) | |
| FONT = cv2.FONT_HERSHEY_SIMPLEX | |
| def letterbox( | |
| image: np.ndarray, | |
| input_size: int = 1024, | |
| pad_value: int = 114, | |
| ) -> Tuple[np.ndarray, float, int, int]: | |
| """Ultralytics-style letterbox: keep ratio, centered gray padding.""" | |
| height, width = image.shape[:2] | |
| ratio = min(input_size / width, input_size / height) | |
| resized_w, resized_h = round(width * ratio), round(height * ratio) | |
| resized = cv2.resize(image, (resized_w, resized_h), interpolation=cv2.INTER_LINEAR) | |
| pad_w, pad_h = input_size - resized_w, input_size - resized_h | |
| left, top = pad_w // 2, pad_h // 2 | |
| canvas = cv2.copyMakeBorder( | |
| resized, | |
| top, | |
| pad_h - top, | |
| left, | |
| pad_w - left, | |
| cv2.BORDER_CONSTANT, | |
| value=(pad_value, pad_value, pad_value), | |
| ) | |
| return canvas, ratio, left, top | |
| def preprocess_image( | |
| image: np.ndarray, | |
| input_size: int = 1024, | |
| pad_value: int = 114, | |
| ) -> Tuple[np.ndarray, float, int, int]: | |
| canvas, ratio, pad_x, pad_y = letterbox(image, input_size, pad_value) | |
| rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) | |
| tensor = (rgb.astype(np.float32) / 255.0)[None, :] | |
| return tensor, ratio, pad_x, pad_y | |
| def xywhr2xyxyxyxy(boxes: np.ndarray) -> np.ndarray: | |
| """Convert [cx, cy, w, h, angle] boxes to (N, 4, 2) corner points.""" | |
| boxes = np.asarray(boxes, dtype=np.float32).reshape(-1, 5) | |
| center = boxes[:, :2] | |
| w, h, angle = boxes[:, 2:3], boxes[:, 3:4], boxes[:, 4:5] | |
| cos, sin = np.cos(angle), np.sin(angle) | |
| vec1 = np.concatenate([w / 2 * cos, w / 2 * sin], axis=-1) | |
| vec2 = np.concatenate([-h / 2 * sin, h / 2 * cos], axis=-1) | |
| pt1 = center + vec1 + vec2 | |
| pt2 = center + vec1 - vec2 | |
| pt3 = center - vec1 - vec2 | |
| pt4 = center - vec1 + vec2 | |
| return np.stack([pt1, pt2, pt3, pt4], axis=1) | |
| def _covariance_matrix(boxes: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: | |
| """Gaussian covariance components (a, b, c) for xywhr boxes.""" | |
| w2 = boxes[:, 2] ** 2 / 12.0 | |
| h2 = boxes[:, 3] ** 2 / 12.0 | |
| angle = boxes[:, 4] | |
| cos2 = np.cos(angle) ** 2 | |
| sin2 = np.sin(angle) ** 2 | |
| a = w2 * cos2 + h2 * sin2 | |
| b = w2 * sin2 + h2 * cos2 | |
| c = (w2 - h2) * np.cos(angle) * np.sin(angle) | |
| return a, b, c | |
| def batch_probiou(obb1: np.ndarray, obb2: np.ndarray, eps: float = 1e-7) -> np.ndarray: | |
| """Probabilistic IoU between two sets of xywhr boxes -> shape (N, M). | |
| Mirrors ``ultralytics.utils.metrics.batch_probiou`` so on-device NMS | |
| matches the official OBB post-processing. | |
| """ | |
| obb1 = np.asarray(obb1, dtype=np.float32).reshape(-1, 5) | |
| obb2 = np.asarray(obb2, dtype=np.float32).reshape(-1, 5) | |
| x1, y1 = obb1[:, 0:1], obb1[:, 1:2] | |
| x2, y2 = obb2[None, :, 0], obb2[None, :, 1] | |
| a1, b1, c1 = (v[:, None] for v in _covariance_matrix(obb1)) | |
| a2, b2, c2 = (v[None, :] for v in _covariance_matrix(obb2)) | |
| denom = (a1 + a2) * (b1 + b2) - (c1 + c2) ** 2 + eps | |
| t1 = (((a1 + a2) * (y1 - y2) ** 2 + (b1 + b2) * (x1 - x2) ** 2) / denom) * 0.25 | |
| t2 = (((c1 + c2) * (x2 - x1) * (y1 - y2)) / denom) * 0.5 | |
| det = np.sqrt( | |
| np.clip(a1 * b1 - c1 ** 2, 0, None) * np.clip(a2 * b2 - c2 ** 2, 0, None) | |
| ) | |
| t3 = np.log(((a1 + a2) * (b1 + b2) - (c1 + c2) ** 2) / (4 * det + eps) + eps) * 0.5 | |
| bd = np.clip(t1 + t2 + t3, eps, 100.0) | |
| hd = np.sqrt(1.0 - np.exp(-bd) + eps) | |
| return 1.0 - hd | |
| def obb_nms(boxes: np.ndarray, scores: np.ndarray, iou_thres: float) -> List[int]: | |
| """Greedy NMS over xywhr boxes using probabilistic IoU.""" | |
| if len(boxes) == 0: | |
| return [] | |
| order = scores.argsort()[::-1] | |
| keep: List[int] = [] | |
| while order.size: | |
| i = int(order[0]) | |
| keep.append(i) | |
| if order.size == 1: | |
| break | |
| rest = order[1:] | |
| iou = batch_probiou(boxes[i:i + 1], boxes[rest])[0] | |
| order = rest[iou <= iou_thres] | |
| return keep | |
| def obb_postprocess( | |
| prediction: np.ndarray, | |
| ratio: float, | |
| pad_x: int, | |
| pad_y: int, | |
| original_shape: Sequence[int], | |
| conf_thres: float = 0.25, | |
| iou_thres: float = 0.45, | |
| max_det: int = 300, | |
| class_names: Sequence[str] = CLASS_NAMES, | |
| class_agnostic: bool = False, | |
| ) -> List[dict]: | |
| """Decode output0 `[1, 4+nc+1, anchors]` into oriented boxes. | |
| Channel order matches the Ultralytics OBB head: xywh, class scores, angle. | |
| """ | |
| prediction = np.asarray(prediction, dtype=np.float32) | |
| if prediction.ndim == 3: | |
| if prediction.shape[0] != 1: | |
| raise ValueError(f"Only batch size 1 is supported, got {prediction.shape}") | |
| prediction = prediction[0] | |
| if prediction.shape[0] < prediction.shape[1]: | |
| prediction = prediction.T # channel-first -> (anchors, channels) | |
| if prediction.ndim != 2 or prediction.shape[1] < 6: | |
| raise ValueError(f"Unexpected prediction shape: {prediction.shape}") | |
| nc = prediction.shape[1] - 5 | |
| class_scores = prediction[:, 4:4 + nc] | |
| class_ids = class_scores.argmax(axis=1) | |
| scores = class_scores[np.arange(class_scores.shape[0]), class_ids] | |
| mask = scores >= conf_thres | |
| if not np.any(mask): | |
| return [] | |
| xywh = prediction[mask, :4] | |
| angle = prediction[mask, 4 + nc] | |
| scores = scores[mask] | |
| class_ids = class_ids[mask] | |
| rboxes = np.concatenate([xywh, angle[:, None]], axis=1) | |
| # Class offsets keep NMS from suppressing different classes, like Ultralytics. | |
| nms_boxes = rboxes.copy() | |
| if not class_agnostic: | |
| offsets = class_ids.astype(np.float32) * 7680.0 | |
| nms_boxes[:, 0] += offsets | |
| nms_boxes[:, 1] += offsets | |
| keep = obb_nms(nms_boxes, scores, iou_thres)[:max_det] | |
| boxes = rboxes[keep].copy() | |
| boxes[:, 0] = (boxes[:, 0] - pad_x) / ratio | |
| boxes[:, 1] = (boxes[:, 1] - pad_y) / ratio | |
| boxes[:, 2] = boxes[:, 2] / ratio | |
| boxes[:, 3] = boxes[:, 3] / ratio | |
| corners = xywhr2xyxyxyxy(boxes) | |
| corners[..., 0].clip(0, original_shape[1], out=corners[..., 0]) | |
| corners[..., 1].clip(0, original_shape[0], out=corners[..., 1]) | |
| detections: List[dict] = [] | |
| for row, idx in enumerate(keep): | |
| cls_id = int(class_ids[idx]) | |
| detections.append({ | |
| "box": boxes[row].astype(np.float32), | |
| "corners": corners[row].astype(np.float32), | |
| "score": float(scores[idx]), | |
| "class_id": cls_id, | |
| "class_name": class_names[cls_id] if cls_id < len(class_names) else str(cls_id), | |
| }) | |
| return detections | |
| def draw_label(result: np.ndarray, text: str, x: int, bottom: int, color) -> None: | |
| """Filled label above ``(x, bottom)``; drops inside the image when it clips.""" | |
| scale, thickness, pad = 0.5, 1, 4 | |
| (text_w, text_h), _ = cv2.getTextSize(text, FONT, scale, thickness) | |
| height = text_h + 2 * pad | |
| top = bottom - height | |
| if top < 0: # no room above the box: draw the label just inside its top edge | |
| top = bottom | |
| right = min(result.shape[1] - 1, x + text_w + 2 * pad) | |
| cv2.rectangle(result, (x, top), (right, top + height), color, -1, cv2.LINE_AA) | |
| cv2.putText(result, text, (x + pad, top + text_h + pad), FONT, scale, | |
| LABEL_FG, thickness, cv2.LINE_AA) | |
| def draw_obb_res(image: np.ndarray, detections: Iterable[dict]) -> np.ndarray: | |
| result = image.astype(np.uint8).copy() | |
| for i, det in enumerate(detections, start=1): | |
| corners = np.asarray(det["corners"], dtype=np.float32) | |
| score = float(det["score"]) | |
| name = det["class_name"] | |
| color = CLASS_COLORS[det["class_id"] % len(CLASS_COLORS)] | |
| points = np.round(corners).astype(np.int32).reshape(-1, 1, 2) | |
| cv2.polylines(result, [points], True, color, 2, cv2.LINE_AA) | |
| top_x, top_y = np.round(corners[np.argmin(corners[:, 1])]).astype(int) | |
| draw_label(result, f"{name} {score:.2f}", int(top_x), int(top_y), color) | |
| print(i, det["class_id"], name, [round(float(v), 2) for v in det["box"]], score) | |
| return result | |