单目3D初始代码
This commit is contained in:
9
ultralytics/models/__init__.py
Executable file
9
ultralytics/models/__init__.py
Executable file
@@ -0,0 +1,9 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .fastsam import FastSAM
|
||||
from .nas import NAS
|
||||
from .rtdetr import RTDETR
|
||||
from .sam import SAM
|
||||
from .yolo import YOLO, YOLOE, YOLOWorld
|
||||
|
||||
__all__ = "NAS", "RTDETR", "SAM", "YOLO", "YOLOE", "FastSAM", "YOLOWorld" # allow simpler import
|
||||
7
ultralytics/models/fastsam/__init__.py
Executable file
7
ultralytics/models/fastsam/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .model import FastSAM
|
||||
from .predict import FastSAMPredictor
|
||||
from .val import FastSAMValidator
|
||||
|
||||
__all__ = "FastSAM", "FastSAMPredictor", "FastSAMValidator"
|
||||
79
ultralytics/models/fastsam/model.py
Executable file
79
ultralytics/models/fastsam/model.py
Executable file
@@ -0,0 +1,79 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ultralytics.engine.model import Model
|
||||
|
||||
from .predict import FastSAMPredictor
|
||||
from .val import FastSAMValidator
|
||||
|
||||
|
||||
class FastSAM(Model):
|
||||
"""FastSAM model interface for Segment Anything tasks.
|
||||
|
||||
This class extends the base Model class to provide specific functionality for the FastSAM (Fast Segment Anything
|
||||
Model) implementation, allowing for efficient and accurate image segmentation with optional prompting support.
|
||||
|
||||
Attributes:
|
||||
model (str): Path to the pre-trained FastSAM model file.
|
||||
task (str): The task type, set to "segment" for FastSAM models.
|
||||
|
||||
Methods:
|
||||
predict: Perform segmentation prediction on image or video source with optional prompts.
|
||||
task_map: Returns mapping of segment task to predictor and validator classes.
|
||||
|
||||
Examples:
|
||||
Initialize FastSAM model and run prediction
|
||||
>>> from ultralytics import FastSAM
|
||||
>>> model = FastSAM("FastSAM-x.pt")
|
||||
>>> results = model.predict("ultralytics/assets/bus.jpg")
|
||||
|
||||
Run prediction with bounding box prompts
|
||||
>>> results = model.predict("image.jpg", bboxes=[[100, 100, 200, 200]])
|
||||
"""
|
||||
|
||||
def __init__(self, model: str | Path = "FastSAM-x.pt"):
|
||||
"""Initialize the FastSAM model with the specified pre-trained weights."""
|
||||
if str(model) == "FastSAM.pt":
|
||||
model = "FastSAM-x.pt"
|
||||
assert Path(model).suffix not in {".yaml", ".yml"}, "FastSAM only supports pre-trained weights."
|
||||
super().__init__(model=model, task="segment")
|
||||
|
||||
def predict(
|
||||
self,
|
||||
source,
|
||||
stream: bool = False,
|
||||
bboxes: list | None = None,
|
||||
points: list | None = None,
|
||||
labels: list | None = None,
|
||||
texts: list | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Perform segmentation prediction on image or video source.
|
||||
|
||||
Supports prompted segmentation with bounding boxes, points, labels, and texts. The method packages these prompts
|
||||
and passes them to the parent class predict method for processing.
|
||||
|
||||
Args:
|
||||
source (str | PIL.Image | np.ndarray): Input source for prediction, can be a file path, URL, PIL image, or
|
||||
numpy array.
|
||||
stream (bool): Whether to enable real-time streaming mode for video inputs.
|
||||
bboxes (list, optional): Bounding box coordinates for prompted segmentation in format [[x1, y1, x2, y2]].
|
||||
points (list, optional): Point coordinates for prompted segmentation in format [[x, y]].
|
||||
labels (list, optional): Class labels for prompted segmentation.
|
||||
texts (list, optional): Text prompts for segmentation guidance.
|
||||
**kwargs (Any): Additional keyword arguments passed to the predictor.
|
||||
|
||||
Returns:
|
||||
(list): List of Results objects containing the prediction results.
|
||||
"""
|
||||
prompts = dict(bboxes=bboxes, points=points, labels=labels, texts=texts)
|
||||
return super().predict(source, stream, prompts=prompts, **kwargs)
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Returns a dictionary mapping segment task to corresponding predictor and validator classes."""
|
||||
return {"segment": {"predictor": FastSAMPredictor, "validator": FastSAMValidator}}
|
||||
170
ultralytics/models/fastsam/predict.py
Executable file
170
ultralytics/models/fastsam/predict.py
Executable file
@@ -0,0 +1,170 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from ultralytics.models.yolo.segment import SegmentationPredictor
|
||||
from ultralytics.utils import DEFAULT_CFG
|
||||
from ultralytics.utils.metrics import box_iou
|
||||
from ultralytics.utils.ops import scale_masks
|
||||
from ultralytics.utils.torch_utils import TORCH_1_10
|
||||
|
||||
from .utils import adjust_bboxes_to_image_border
|
||||
|
||||
|
||||
class FastSAMPredictor(SegmentationPredictor):
|
||||
"""FastSAMPredictor is specialized for fast SAM (Segment Anything Model) segmentation prediction tasks.
|
||||
|
||||
This class extends the SegmentationPredictor, customizing the prediction pipeline specifically for fast SAM. It
|
||||
adjusts post-processing steps to incorporate mask prediction and non-maximum suppression while optimizing for
|
||||
single-class segmentation.
|
||||
|
||||
Attributes:
|
||||
prompts (dict): Dictionary containing prompt information for segmentation (bboxes, points, labels, texts).
|
||||
device (torch.device): Device on which model and tensors are processed.
|
||||
clip (Any, optional): CLIP model used for text-based prompting, loaded on demand.
|
||||
|
||||
Methods:
|
||||
postprocess: Apply postprocessing to FastSAM predictions and handle prompts.
|
||||
prompt: Perform image segmentation inference based on various prompt types.
|
||||
set_prompts: Set prompts to be used during inference.
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize the FastSAMPredictor with configuration and callbacks.
|
||||
|
||||
This initializes a predictor specialized for Fast SAM (Segment Anything Model) segmentation tasks. The predictor
|
||||
extends SegmentationPredictor with custom post-processing for mask prediction and non-maximum suppression
|
||||
optimized for single-class segmentation.
|
||||
|
||||
Args:
|
||||
cfg (dict): Configuration for the predictor.
|
||||
overrides (dict, optional): Configuration overrides.
|
||||
_callbacks (list, optional): List of callback functions.
|
||||
"""
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.prompts = {}
|
||||
|
||||
def postprocess(self, preds, img, orig_imgs):
|
||||
"""Apply postprocessing to FastSAM predictions and handle prompts.
|
||||
|
||||
Args:
|
||||
preds (list[torch.Tensor]): Raw predictions from the model.
|
||||
img (torch.Tensor): Input image tensor that was fed to the model.
|
||||
orig_imgs (list[np.ndarray]): Original images before preprocessing.
|
||||
|
||||
Returns:
|
||||
(list[Results]): Processed results with prompts applied.
|
||||
"""
|
||||
bboxes = self.prompts.pop("bboxes", None)
|
||||
points = self.prompts.pop("points", None)
|
||||
labels = self.prompts.pop("labels", None)
|
||||
texts = self.prompts.pop("texts", None)
|
||||
results = super().postprocess(preds, img, orig_imgs)
|
||||
for result in results:
|
||||
full_box = torch.tensor(
|
||||
[0, 0, result.orig_shape[1], result.orig_shape[0]], device=result.boxes.data.device, dtype=torch.float32
|
||||
)
|
||||
boxes = adjust_bboxes_to_image_border(result.boxes.xyxy, result.orig_shape)
|
||||
idx = torch.nonzero(box_iou(full_box[None], boxes) > 0.9).flatten()
|
||||
if idx.numel() != 0:
|
||||
result.boxes.xyxy[idx] = full_box
|
||||
|
||||
return self.prompt(results, bboxes=bboxes, points=points, labels=labels, texts=texts)
|
||||
|
||||
def prompt(self, results, bboxes=None, points=None, labels=None, texts=None):
|
||||
"""Perform image segmentation inference based on cues like bounding boxes, points, and text prompts.
|
||||
|
||||
Args:
|
||||
results (Results | list[Results]): Original inference results from FastSAM models without any prompts.
|
||||
bboxes (np.ndarray | list, optional): Bounding boxes with shape (N, 4), in XYXY format.
|
||||
points (np.ndarray | list, optional): Points indicating object locations with shape (N, 2), in pixels.
|
||||
labels (np.ndarray | list, optional): Labels for point prompts, shape (N, ). 1 = foreground, 0 = background.
|
||||
texts (str | list[str], optional): Textual prompts, a list containing string objects.
|
||||
|
||||
Returns:
|
||||
(list[Results]): Output results filtered and determined by the provided prompts.
|
||||
"""
|
||||
if bboxes is None and points is None and texts is None:
|
||||
return results
|
||||
prompt_results = []
|
||||
if not isinstance(results, list):
|
||||
results = [results]
|
||||
for result in results:
|
||||
if len(result) == 0:
|
||||
prompt_results.append(result)
|
||||
continue
|
||||
masks = result.masks.data
|
||||
if masks.shape[1:] != result.orig_shape:
|
||||
masks = (scale_masks(masks[None].float(), result.orig_shape)[0] > 0.5).byte()
|
||||
# bboxes prompt
|
||||
idx = torch.zeros(len(result), dtype=torch.bool, device=self.device)
|
||||
if bboxes is not None:
|
||||
bboxes = torch.as_tensor(bboxes, dtype=torch.int32, device=self.device)
|
||||
bboxes = bboxes[None] if bboxes.ndim == 1 else bboxes
|
||||
bbox_areas = (bboxes[:, 3] - bboxes[:, 1]) * (bboxes[:, 2] - bboxes[:, 0])
|
||||
mask_areas = torch.stack([masks[:, b[1] : b[3], b[0] : b[2]].sum(dim=(1, 2)) for b in bboxes])
|
||||
full_mask_areas = torch.sum(masks, dim=(1, 2))
|
||||
|
||||
union = bbox_areas[:, None] + full_mask_areas - mask_areas
|
||||
idx[torch.argmax(mask_areas / union, dim=1)] = True
|
||||
if points is not None:
|
||||
points = torch.as_tensor(points, dtype=torch.int32, device=self.device)
|
||||
points = points[None] if points.ndim == 1 else points
|
||||
if labels is None:
|
||||
labels = torch.ones(points.shape[0])
|
||||
labels = torch.as_tensor(labels, dtype=torch.int32, device=self.device)
|
||||
assert len(labels) == len(points), (
|
||||
f"Expected `labels` to have the same length as `points`, but got {len(labels)} and {len(points)}."
|
||||
)
|
||||
point_idx = (
|
||||
torch.ones(len(result), dtype=torch.bool, device=self.device)
|
||||
if labels.sum() == 0 # all negative points
|
||||
else torch.zeros(len(result), dtype=torch.bool, device=self.device)
|
||||
)
|
||||
for point, label in zip(points, labels):
|
||||
point_idx[torch.nonzero(masks[:, point[1], point[0]], as_tuple=True)[0]] = bool(label)
|
||||
idx |= point_idx
|
||||
if texts is not None:
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
crop_ims, filter_idx = [], []
|
||||
for i, b in enumerate(result.boxes.xyxy.tolist()):
|
||||
x1, y1, x2, y2 = (int(x) for x in b)
|
||||
if (masks[i].sum() if TORCH_1_10 else masks[i].sum(0).sum()) <= 100: # torch 1.9 bug workaround
|
||||
filter_idx.append(i)
|
||||
continue
|
||||
crop = result.orig_img[y1:y2, x1:x2] * masks[i, y1:y2, x1:x2, None].cpu().numpy()
|
||||
crop_ims.append(Image.fromarray(crop[:, :, ::-1]))
|
||||
similarity = self._clip_inference(crop_ims, texts)
|
||||
text_idx = torch.argmax(similarity, dim=-1) # (M, )
|
||||
if len(filter_idx):
|
||||
text_idx += (torch.tensor(filter_idx, device=self.device)[None] <= int(text_idx)).sum(0)
|
||||
idx[text_idx] = True
|
||||
|
||||
prompt_results.append(result[idx])
|
||||
|
||||
return prompt_results
|
||||
|
||||
def _clip_inference(self, images, texts):
|
||||
"""Perform CLIP inference to calculate similarity between images and text prompts.
|
||||
|
||||
Args:
|
||||
images (list[PIL.Image]): List of source images, each should be PIL.Image with RGB channel order.
|
||||
texts (list[str]): List of prompt texts, each should be a string object.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Similarity matrix between given images and texts with shape (M, N).
|
||||
"""
|
||||
from ultralytics.nn.text_model import CLIP
|
||||
|
||||
if not hasattr(self, "clip"):
|
||||
self.clip = CLIP("ViT-B/32", device=self.device)
|
||||
images = torch.stack([self.clip.image_preprocess(image).to(self.device) for image in images])
|
||||
image_features = self.clip.encode_image(images)
|
||||
text_features = self.clip.encode_text(self.clip.tokenize(texts))
|
||||
return text_features @ image_features.T # (M, N)
|
||||
|
||||
def set_prompts(self, prompts):
|
||||
"""Set prompts to be used during inference."""
|
||||
self.prompts = prompts
|
||||
23
ultralytics/models/fastsam/utils.py
Executable file
23
ultralytics/models/fastsam/utils.py
Executable file
@@ -0,0 +1,23 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
|
||||
def adjust_bboxes_to_image_border(boxes, image_shape, threshold=20):
|
||||
"""Adjust bounding boxes to stick to image border if they are within a certain threshold.
|
||||
|
||||
Args:
|
||||
boxes (torch.Tensor): Bounding boxes with shape (N, 4) in xyxy format.
|
||||
image_shape (tuple): Image dimensions as (height, width).
|
||||
threshold (int): Pixel threshold for considering a box close to the border.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Adjusted bounding boxes with shape (N, 4).
|
||||
"""
|
||||
# Image dimensions
|
||||
h, w = image_shape
|
||||
|
||||
# Adjust boxes that are close to image borders
|
||||
boxes[boxes[:, 0] < threshold, 0] = 0 # x1
|
||||
boxes[boxes[:, 1] < threshold, 1] = 0 # y1
|
||||
boxes[boxes[:, 2] > w - threshold, 2] = w # x2
|
||||
boxes[boxes[:, 3] > h - threshold, 3] = h # y2
|
||||
return boxes
|
||||
38
ultralytics/models/fastsam/val.py
Executable file
38
ultralytics/models/fastsam/val.py
Executable file
@@ -0,0 +1,38 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.models.yolo.segment import SegmentationValidator
|
||||
|
||||
|
||||
class FastSAMValidator(SegmentationValidator):
|
||||
"""Custom validation class for FastSAM (Segment Anything Model) segmentation in the Ultralytics YOLO framework.
|
||||
|
||||
Extends the SegmentationValidator class, customizing the validation process specifically for FastSAM. This class
|
||||
sets the task to 'segment' and uses the SegmentMetrics for evaluation. Additionally, plotting features are disabled
|
||||
to avoid errors during validation.
|
||||
|
||||
Attributes:
|
||||
dataloader (torch.utils.data.DataLoader): The data loader object used for validation.
|
||||
save_dir (Path): The directory where validation results will be saved.
|
||||
args (SimpleNamespace): Additional arguments for customization of the validation process.
|
||||
_callbacks (list): List of callback functions to be invoked during validation.
|
||||
metrics (SegmentMetrics): Segmentation metrics calculator for evaluation.
|
||||
|
||||
Methods:
|
||||
__init__: Initialize the FastSAMValidator with custom settings for FastSAM.
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None):
|
||||
"""Initialize the FastSAMValidator class, setting the task to 'segment' and metrics to SegmentMetrics.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to be used for validation.
|
||||
save_dir (Path, optional): Directory to save results.
|
||||
args (SimpleNamespace, optional): Configuration for the validator.
|
||||
_callbacks (list, optional): List of callback functions to be invoked during validation.
|
||||
|
||||
Notes:
|
||||
Plots for ConfusionMatrix and other related metrics are disabled in this class to avoid errors.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.args.task = "segment"
|
||||
self.args.plots = False # disable ConfusionMatrix and other plots to avoid errors
|
||||
7
ultralytics/models/nas/__init__.py
Executable file
7
ultralytics/models/nas/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .model import NAS
|
||||
from .predict import NASPredictor
|
||||
from .val import NASValidator
|
||||
|
||||
__all__ = "NAS", "NASPredictor", "NASValidator"
|
||||
98
ultralytics/models/nas/model.py
Executable file
98
ultralytics/models/nas/model.py
Executable file
@@ -0,0 +1,98 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.engine.model import Model
|
||||
from ultralytics.utils import DEFAULT_CFG_DICT
|
||||
from ultralytics.utils.downloads import attempt_download_asset
|
||||
from ultralytics.utils.patches import torch_load
|
||||
from ultralytics.utils.torch_utils import model_info
|
||||
|
||||
from .predict import NASPredictor
|
||||
from .val import NASValidator
|
||||
|
||||
|
||||
class NAS(Model):
|
||||
"""YOLO-NAS model for object detection.
|
||||
|
||||
This class provides an interface for the YOLO-NAS models and extends the `Model` class from Ultralytics engine. It
|
||||
is designed to facilitate the task of object detection using pre-trained or custom-trained YOLO-NAS models.
|
||||
|
||||
Attributes:
|
||||
model (torch.nn.Module): The loaded YOLO-NAS model.
|
||||
task (str): The task type for the model, defaults to 'detect'.
|
||||
predictor (NASPredictor): The predictor instance for making predictions.
|
||||
validator (NASValidator): The validator instance for model validation.
|
||||
|
||||
Methods:
|
||||
info: Log model information and return model details.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics import NAS
|
||||
>>> model = NAS("yolo_nas_s")
|
||||
>>> results = model.predict("ultralytics/assets/bus.jpg")
|
||||
|
||||
Notes:
|
||||
YOLO-NAS models only support pre-trained models. Do not provide YAML configuration files.
|
||||
"""
|
||||
|
||||
def __init__(self, model: str = "yolo_nas_s.pt") -> None:
|
||||
"""Initialize the NAS model with the provided or default model."""
|
||||
assert Path(model).suffix not in {".yaml", ".yml"}, "YOLO-NAS models only support pre-trained models."
|
||||
super().__init__(model, task="detect")
|
||||
|
||||
def _load(self, weights: str, task=None) -> None:
|
||||
"""Load an existing NAS model weights or create a new NAS model with pretrained weights.
|
||||
|
||||
Args:
|
||||
weights (str): Path to the model weights file or model name.
|
||||
task (str, optional): Task type for the model.
|
||||
"""
|
||||
import super_gradients
|
||||
|
||||
suffix = Path(weights).suffix
|
||||
if suffix == ".pt":
|
||||
self.model = torch_load(attempt_download_asset(weights))
|
||||
elif suffix == "":
|
||||
self.model = super_gradients.training.models.get(weights, pretrained_weights="coco")
|
||||
|
||||
# Override the forward method to ignore additional arguments
|
||||
def new_forward(x, *args, **kwargs):
|
||||
"""Ignore additional __call__ arguments."""
|
||||
return self.model._original_forward(x)
|
||||
|
||||
self.model._original_forward = self.model.forward
|
||||
self.model.forward = new_forward
|
||||
|
||||
# Standardize model attributes for compatibility
|
||||
self.model.fuse = lambda verbose=True: self.model
|
||||
self.model.stride = torch.tensor([32])
|
||||
self.model.names = dict(enumerate(self.model._class_names))
|
||||
self.model.is_fused = lambda: False # for info()
|
||||
self.model.yaml = {} # for info()
|
||||
self.model.pt_path = str(weights) # for export()
|
||||
self.model.task = "detect" # for export()
|
||||
self.model.args = {**DEFAULT_CFG_DICT, **self.overrides} # for export()
|
||||
self.model.eval()
|
||||
|
||||
def info(self, detailed: bool = False, verbose: bool = True) -> dict[str, Any]:
|
||||
"""Log model information.
|
||||
|
||||
Args:
|
||||
detailed (bool): Show detailed information about model.
|
||||
verbose (bool): Controls verbosity.
|
||||
|
||||
Returns:
|
||||
(tuple): Model information as a tuple of (layers, parameters, gradients, GFLOPs).
|
||||
"""
|
||||
return model_info(self.model, detailed=detailed, verbose=verbose, imgsz=640)
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return a dictionary mapping tasks to respective predictor and validator classes."""
|
||||
return {"detect": {"predictor": NASPredictor, "validator": NASValidator}}
|
||||
56
ultralytics/models/nas/predict.py
Executable file
56
ultralytics/models/nas/predict.py
Executable file
@@ -0,0 +1,56 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.models.yolo.detect.predict import DetectionPredictor
|
||||
from ultralytics.utils import ops
|
||||
|
||||
|
||||
class NASPredictor(DetectionPredictor):
|
||||
"""Ultralytics YOLO NAS Predictor for object detection.
|
||||
|
||||
This class extends the DetectionPredictor from Ultralytics engine and is responsible for post-processing the raw
|
||||
predictions generated by the YOLO NAS models. It applies operations like non-maximum suppression and scaling the
|
||||
bounding boxes to fit the original image dimensions.
|
||||
|
||||
Attributes:
|
||||
args (Namespace): Namespace containing various configurations for post-processing including confidence
|
||||
threshold, IoU threshold, agnostic NMS flag, maximum detections, and class filtering options.
|
||||
model (torch.nn.Module): The YOLO NAS model used for inference.
|
||||
batch (list): Batch of inputs for processing.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics import NAS
|
||||
>>> model = NAS("yolo_nas_s")
|
||||
>>> predictor = model.predictor
|
||||
|
||||
Assume that raw_preds, img, orig_imgs are available
|
||||
>>> results = predictor.postprocess(raw_preds, img, orig_imgs)
|
||||
|
||||
Notes:
|
||||
Typically, this class is not instantiated directly. It is used internally within the NAS class.
|
||||
"""
|
||||
|
||||
def postprocess(self, preds_in, img, orig_imgs):
|
||||
"""Postprocess NAS model predictions to generate final detection results.
|
||||
|
||||
This method takes raw predictions from a YOLO NAS model, converts bounding box formats, and applies
|
||||
post-processing operations to generate the final detection results compatible with Ultralytics result
|
||||
visualization and analysis tools.
|
||||
|
||||
Args:
|
||||
preds_in (list): Raw predictions from the NAS model, typically containing bounding boxes and class scores.
|
||||
img (torch.Tensor): Input image tensor that was fed to the model, with shape (B, C, H, W).
|
||||
orig_imgs (list | torch.Tensor | np.ndarray): Original images before preprocessing, used for scaling
|
||||
coordinates back to original dimensions.
|
||||
|
||||
Returns:
|
||||
(list): List of Results objects containing the processed predictions for each image in the batch.
|
||||
|
||||
Examples:
|
||||
>>> predictor = NAS("yolo_nas_s").predictor
|
||||
>>> results = predictor.postprocess(raw_preds, img, orig_imgs)
|
||||
"""
|
||||
boxes = ops.xyxy2xywh(preds_in[0][0]) # Convert bounding boxes from xyxy to xywh format
|
||||
preds = torch.cat((boxes, preds_in[0][1]), -1).permute(0, 2, 1) # Concatenate boxes with class scores
|
||||
return super().postprocess(preds, img, orig_imgs)
|
||||
38
ultralytics/models/nas/val.py
Executable file
38
ultralytics/models/nas/val.py
Executable file
@@ -0,0 +1,38 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.utils import ops
|
||||
|
||||
__all__ = ["NASValidator"]
|
||||
|
||||
|
||||
class NASValidator(DetectionValidator):
|
||||
"""Ultralytics YOLO NAS Validator for object detection.
|
||||
|
||||
Extends DetectionValidator from the Ultralytics models package and is designed to post-process the raw predictions
|
||||
generated by YOLO NAS models. It performs non-maximum suppression to remove overlapping and low-confidence boxes,
|
||||
ultimately producing the final detections.
|
||||
|
||||
Attributes:
|
||||
args (Namespace): Namespace containing various configurations for post-processing, such as confidence and IoU
|
||||
thresholds.
|
||||
lb (torch.Tensor): Optional tensor for multilabel NMS.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics import NAS
|
||||
>>> model = NAS("yolo_nas_s")
|
||||
>>> validator = model.validator
|
||||
>>> # Assumes that raw_preds are available
|
||||
>>> final_preds = validator.postprocess(raw_preds)
|
||||
|
||||
Notes:
|
||||
This class is generally not instantiated directly but is used internally within the NAS class.
|
||||
"""
|
||||
|
||||
def postprocess(self, preds_in):
|
||||
"""Apply Non-maximum suppression to prediction outputs."""
|
||||
boxes = ops.xyxy2xywh(preds_in[0][0]) # Convert bounding box format from xyxy to xywh
|
||||
preds = torch.cat((boxes, preds_in[0][1]), -1).permute(0, 2, 1) # Concatenate boxes with scores and permute
|
||||
return super().postprocess(preds)
|
||||
7
ultralytics/models/rtdetr/__init__.py
Executable file
7
ultralytics/models/rtdetr/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .model import RTDETR
|
||||
from .predict import RTDETRPredictor
|
||||
from .val import RTDETRValidator
|
||||
|
||||
__all__ = "RTDETR", "RTDETRPredictor", "RTDETRValidator"
|
||||
63
ultralytics/models/rtdetr/model.py
Executable file
63
ultralytics/models/rtdetr/model.py
Executable file
@@ -0,0 +1,63 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
"""
|
||||
Interface for Baidu's RT-DETR, a Vision Transformer-based real-time object detector.
|
||||
|
||||
RT-DETR offers real-time performance and high accuracy, excelling in accelerated backends like CUDA with TensorRT.
|
||||
It features an efficient hybrid encoder and IoU-aware query selection for enhanced detection accuracy.
|
||||
|
||||
References:
|
||||
https://arxiv.org/pdf/2304.08069.pdf
|
||||
"""
|
||||
|
||||
from ultralytics.engine.model import Model
|
||||
from ultralytics.nn.tasks import RTDETRDetectionModel
|
||||
from ultralytics.utils.torch_utils import TORCH_1_11
|
||||
|
||||
from .predict import RTDETRPredictor
|
||||
from .train import RTDETRTrainer
|
||||
from .val import RTDETRValidator
|
||||
|
||||
|
||||
class RTDETR(Model):
|
||||
"""Interface for Baidu's RT-DETR model, a Vision Transformer-based real-time object detector.
|
||||
|
||||
This model provides real-time performance with high accuracy. It supports efficient hybrid encoding, IoU-aware query
|
||||
selection, and adaptable inference speed.
|
||||
|
||||
Attributes:
|
||||
model (str): Path to the pre-trained model.
|
||||
|
||||
Methods:
|
||||
task_map: Return a task map for RT-DETR, associating tasks with corresponding Ultralytics classes.
|
||||
|
||||
Examples:
|
||||
Initialize RT-DETR with a pre-trained model
|
||||
>>> from ultralytics import RTDETR
|
||||
>>> model = RTDETR("rtdetr-l.pt")
|
||||
>>> results = model("image.jpg")
|
||||
"""
|
||||
|
||||
def __init__(self, model: str = "rtdetr-l.pt") -> None:
|
||||
"""Initialize the RT-DETR model with the given pre-trained model file.
|
||||
|
||||
Args:
|
||||
model (str): Path to the pre-trained model. Supports .pt, .yaml, and .yml formats.
|
||||
"""
|
||||
assert TORCH_1_11, "RTDETR requires torch>=1.11"
|
||||
super().__init__(model=model, task="detect")
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict:
|
||||
"""Return a task map for RT-DETR, associating tasks with corresponding Ultralytics classes.
|
||||
|
||||
Returns:
|
||||
(dict): A dictionary mapping task names to Ultralytics task classes for the RT-DETR model.
|
||||
"""
|
||||
return {
|
||||
"detect": {
|
||||
"predictor": RTDETRPredictor,
|
||||
"validator": RTDETRValidator,
|
||||
"trainer": RTDETRTrainer,
|
||||
"model": RTDETRDetectionModel,
|
||||
}
|
||||
}
|
||||
87
ultralytics/models/rtdetr/predict.py
Executable file
87
ultralytics/models/rtdetr/predict.py
Executable file
@@ -0,0 +1,87 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data.augment import LetterBox
|
||||
from ultralytics.engine.predictor import BasePredictor
|
||||
from ultralytics.engine.results import Results
|
||||
from ultralytics.utils import ops
|
||||
|
||||
|
||||
class RTDETRPredictor(BasePredictor):
|
||||
"""RT-DETR (Real-Time Detection Transformer) Predictor extending the BasePredictor class for making predictions.
|
||||
|
||||
This class leverages Vision Transformers to provide real-time object detection while maintaining high accuracy. It
|
||||
supports key features like efficient hybrid encoding and IoU-aware query selection.
|
||||
|
||||
Attributes:
|
||||
imgsz (int): Image size for inference (must be square and scale-filled).
|
||||
args (dict): Argument overrides for the predictor.
|
||||
model (torch.nn.Module): The loaded RT-DETR model.
|
||||
batch (list): Current batch of processed inputs.
|
||||
|
||||
Methods:
|
||||
postprocess: Postprocess raw model predictions to generate bounding boxes and confidence scores.
|
||||
pre_transform: Pre-transform input images before feeding them into the model for inference.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.rtdetr import RTDETRPredictor
|
||||
>>> args = dict(model="rtdetr-l.pt", source=ASSETS)
|
||||
>>> predictor = RTDETRPredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
"""
|
||||
|
||||
def postprocess(self, preds, img, orig_imgs):
|
||||
"""Postprocess the raw predictions from the model to generate bounding boxes and confidence scores.
|
||||
|
||||
The method filters detections based on confidence and class if specified in `self.args`. It converts model
|
||||
predictions to Results objects containing properly scaled bounding boxes.
|
||||
|
||||
Args:
|
||||
preds (list | tuple): List of [predictions, extra] from the model, where predictions contain bounding boxes
|
||||
and scores.
|
||||
img (torch.Tensor): Processed input images with shape (N, 3, H, W).
|
||||
orig_imgs (list | torch.Tensor): Original, unprocessed images.
|
||||
|
||||
Returns:
|
||||
(list[Results]): A list of Results objects containing the post-processed bounding boxes, confidence scores,
|
||||
and class labels.
|
||||
"""
|
||||
if not isinstance(preds, (list, tuple)): # list for PyTorch inference but list[0] Tensor for export inference
|
||||
preds = [preds, None]
|
||||
|
||||
nd = preds[0].shape[-1]
|
||||
bboxes, scores = preds[0].split((4, nd - 4), dim=-1)
|
||||
|
||||
if not isinstance(orig_imgs, list): # input images are a torch.Tensor, not a list
|
||||
orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)[..., ::-1]
|
||||
|
||||
results = []
|
||||
for bbox, score, orig_img, img_path in zip(bboxes, scores, orig_imgs, self.batch[0]): # (300, 4)
|
||||
bbox = ops.xywh2xyxy(bbox)
|
||||
max_score, cls = score.max(-1, keepdim=True) # (300, 1)
|
||||
idx = max_score.squeeze(-1) > self.args.conf # (300, )
|
||||
if self.args.classes is not None:
|
||||
idx = (cls == torch.tensor(self.args.classes, device=cls.device)).any(1) & idx
|
||||
pred = torch.cat([bbox, max_score, cls], dim=-1)[idx] # filter
|
||||
pred = pred[pred[:, 4].argsort(descending=True)][: self.args.max_det]
|
||||
oh, ow = orig_img.shape[:2]
|
||||
pred[..., [0, 2]] *= ow # scale x coordinates to original width
|
||||
pred[..., [1, 3]] *= oh # scale y coordinates to original height
|
||||
results.append(Results(orig_img, path=img_path, names=self.model.names, boxes=pred))
|
||||
return results
|
||||
|
||||
def pre_transform(self, im):
|
||||
"""Pre-transform input images before feeding them into the model for inference.
|
||||
|
||||
The input images are letterboxed to ensure a square aspect ratio and scale-filled.
|
||||
|
||||
Args:
|
||||
im (list[np.ndarray]): Input images of shape [(H, W, 3) x N].
|
||||
|
||||
Returns:
|
||||
(list): List of pre-transformed images ready for model inference.
|
||||
"""
|
||||
letterbox = LetterBox(self.imgsz, auto=False, scale_fill=True)
|
||||
return [letterbox(image=x) for x in im]
|
||||
89
ultralytics/models/rtdetr/train.py
Executable file
89
ultralytics/models/rtdetr/train.py
Executable file
@@ -0,0 +1,89 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
|
||||
from ultralytics.models.yolo.detect import DetectionTrainer
|
||||
from ultralytics.nn.tasks import RTDETRDetectionModel
|
||||
from ultralytics.utils import RANK, colorstr
|
||||
|
||||
from .val import RTDETRDataset, RTDETRValidator
|
||||
|
||||
|
||||
class RTDETRTrainer(DetectionTrainer):
|
||||
"""Trainer class for the RT-DETR model developed by Baidu for real-time object detection.
|
||||
|
||||
This class extends the DetectionTrainer class for YOLO to adapt to the specific features and architecture of
|
||||
RT-DETR. The model leverages Vision Transformers and has capabilities like IoU-aware query selection and adaptable
|
||||
inference speed.
|
||||
|
||||
Attributes:
|
||||
loss_names (tuple): Names of the loss components used for training.
|
||||
data (dict): Dataset configuration containing class count and other parameters.
|
||||
args (dict): Training arguments and hyperparameters.
|
||||
save_dir (Path): Directory to save training results.
|
||||
test_loader (DataLoader): DataLoader for validation/testing data.
|
||||
|
||||
Methods:
|
||||
get_model: Initialize and return an RT-DETR model for object detection tasks.
|
||||
build_dataset: Build and return an RT-DETR dataset for training or validation.
|
||||
get_validator: Return a DetectionValidator suitable for RT-DETR model validation.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.rtdetr.train import RTDETRTrainer
|
||||
>>> args = dict(model="rtdetr-l.yaml", data="coco8.yaml", imgsz=640, epochs=3)
|
||||
>>> trainer = RTDETRTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
|
||||
Notes:
|
||||
- F.grid_sample used in RT-DETR does not support the `deterministic=True` argument.
|
||||
- AMP training can lead to NaN outputs and may produce errors during bipartite graph matching.
|
||||
"""
|
||||
|
||||
def get_model(self, cfg: dict | None = None, weights: str | None = None, verbose: bool = True):
|
||||
"""Initialize and return an RT-DETR model for object detection tasks.
|
||||
|
||||
Args:
|
||||
cfg (dict, optional): Model configuration.
|
||||
weights (str, optional): Path to pre-trained model weights.
|
||||
verbose (bool): Verbose logging if True.
|
||||
|
||||
Returns:
|
||||
(RTDETRDetectionModel): Initialized model.
|
||||
"""
|
||||
model = RTDETRDetectionModel(cfg, nc=self.data["nc"], ch=self.data["channels"], verbose=verbose and RANK == -1)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
return model
|
||||
|
||||
def build_dataset(self, img_path: str, mode: str = "val", batch: int | None = None):
|
||||
"""Build and return an RT-DETR dataset for training or validation.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the folder containing images.
|
||||
mode (str): Dataset mode, either 'train' or 'val'.
|
||||
batch (int, optional): Batch size for rectangle training.
|
||||
|
||||
Returns:
|
||||
(RTDETRDataset): Dataset object for the specific mode.
|
||||
"""
|
||||
return RTDETRDataset(
|
||||
img_path=img_path,
|
||||
imgsz=self.args.imgsz,
|
||||
batch_size=batch,
|
||||
augment=mode == "train",
|
||||
hyp=self.args,
|
||||
rect=False,
|
||||
cache=self.args.cache or None,
|
||||
single_cls=self.args.single_cls or False,
|
||||
prefix=colorstr(f"{mode}: "),
|
||||
classes=self.args.classes,
|
||||
data=self.data,
|
||||
fraction=self.args.fraction if mode == "train" else 1.0,
|
||||
)
|
||||
|
||||
def get_validator(self):
|
||||
"""Return an RTDETRValidator suitable for RT-DETR model validation."""
|
||||
self.loss_names = "giou_loss", "cls_loss", "l1_loss"
|
||||
return RTDETRValidator(self.test_loader, save_dir=self.save_dir, args=copy(self.args))
|
||||
216
ultralytics/models/rtdetr/val.py
Executable file
216
ultralytics/models/rtdetr/val.py
Executable file
@@ -0,0 +1,216 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data import YOLODataset
|
||||
from ultralytics.data.augment import Compose, Format, v8_transforms
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.utils import colorstr, ops
|
||||
|
||||
__all__ = ("RTDETRValidator",) # tuple or list
|
||||
|
||||
|
||||
class RTDETRDataset(YOLODataset):
|
||||
"""Real-Time DEtection and TRacking (RT-DETR) dataset class extending the base YOLODataset class.
|
||||
|
||||
This specialized dataset class is designed for use with the RT-DETR object detection model and is optimized for
|
||||
real-time detection and tracking tasks.
|
||||
|
||||
Attributes:
|
||||
augment (bool): Whether to apply data augmentation.
|
||||
rect (bool): Whether to use rectangular training.
|
||||
use_segments (bool): Whether to use segmentation masks.
|
||||
use_keypoints (bool): Whether to use keypoint annotations.
|
||||
imgsz (int): Target image size for training.
|
||||
|
||||
Methods:
|
||||
load_image: Load one image from dataset index.
|
||||
build_transforms: Build transformation pipeline for the dataset.
|
||||
|
||||
Examples:
|
||||
Initialize an RT-DETR dataset
|
||||
>>> dataset = RTDETRDataset(img_path="path/to/images", imgsz=640)
|
||||
>>> image, hw0, hw = dataset.load_image(0)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, data=None, **kwargs):
|
||||
"""Initialize the RTDETRDataset class by inheriting from the YOLODataset class.
|
||||
|
||||
This constructor sets up a dataset specifically optimized for the RT-DETR (Real-Time DEtection and TRacking)
|
||||
model, building upon the base YOLODataset functionality.
|
||||
|
||||
Args:
|
||||
*args (Any): Variable length argument list passed to the parent YOLODataset class.
|
||||
data (dict | None): Dictionary containing dataset information. If None, default values will be used.
|
||||
**kwargs (Any): Additional keyword arguments passed to the parent YOLODataset class.
|
||||
"""
|
||||
super().__init__(*args, data=data, **kwargs)
|
||||
|
||||
def load_image(self, i, rect_mode=False):
|
||||
"""Load one image from dataset index 'i'.
|
||||
|
||||
Args:
|
||||
i (int): Index of the image to load.
|
||||
rect_mode (bool, optional): Whether to use rectangular mode for batch inference.
|
||||
|
||||
Returns:
|
||||
im (np.ndarray): Loaded image as a NumPy array.
|
||||
hw_original (tuple[int, int]): Original image dimensions in (height, width) format.
|
||||
hw_resized (tuple[int, int]): Resized image dimensions in (height, width) format.
|
||||
|
||||
Examples:
|
||||
Load an image from the dataset
|
||||
>>> dataset = RTDETRDataset(img_path="path/to/images")
|
||||
>>> image, hw0, hw = dataset.load_image(0)
|
||||
"""
|
||||
return super().load_image(i=i, rect_mode=rect_mode)
|
||||
|
||||
def build_transforms(self, hyp=None):
|
||||
"""Build transformation pipeline for the dataset.
|
||||
|
||||
Args:
|
||||
hyp (dict, optional): Hyperparameters for transformations.
|
||||
|
||||
Returns:
|
||||
(Compose): Composition of transformation functions.
|
||||
"""
|
||||
if self.augment:
|
||||
hyp.mosaic = hyp.mosaic if self.augment and not self.rect else 0.0
|
||||
hyp.mixup = hyp.mixup if self.augment and not self.rect else 0.0
|
||||
hyp.cutmix = hyp.cutmix if self.augment and not self.rect else 0.0
|
||||
transforms = v8_transforms(self, self.imgsz, hyp, stretch=True)
|
||||
else:
|
||||
# transforms = Compose([LetterBox(new_shape=(self.imgsz, self.imgsz), auto=False, scale_fill=True)])
|
||||
transforms = Compose([])
|
||||
transforms.append(
|
||||
Format(
|
||||
bbox_format="xywh",
|
||||
normalize=True,
|
||||
return_mask=self.use_segments,
|
||||
return_keypoint=self.use_keypoints,
|
||||
batch_idx=True,
|
||||
mask_ratio=hyp.mask_ratio,
|
||||
mask_overlap=hyp.overlap_mask,
|
||||
)
|
||||
)
|
||||
return transforms
|
||||
|
||||
|
||||
class RTDETRValidator(DetectionValidator):
|
||||
"""RTDETRValidator extends the DetectionValidator class to provide validation capabilities specifically tailored for
|
||||
the RT-DETR (Real-Time DETR) object detection model.
|
||||
|
||||
The class allows building of an RTDETR-specific dataset for validation, applies confidence thresholding for
|
||||
post-processing, and updates evaluation metrics accordingly.
|
||||
|
||||
Attributes:
|
||||
args (Namespace): Configuration arguments for validation.
|
||||
data (dict): Dataset configuration dictionary.
|
||||
|
||||
Methods:
|
||||
build_dataset: Build an RTDETR Dataset for validation.
|
||||
postprocess: Apply confidence thresholding to prediction outputs.
|
||||
|
||||
Examples:
|
||||
Initialize and run RT-DETR validation
|
||||
>>> from ultralytics.models.rtdetr import RTDETRValidator
|
||||
>>> args = dict(model="rtdetr-l.pt", data="coco8.yaml")
|
||||
>>> validator = RTDETRValidator(args=args)
|
||||
>>> validator()
|
||||
|
||||
Notes:
|
||||
For further details on the attributes and methods, refer to the parent DetectionValidator class.
|
||||
"""
|
||||
|
||||
def build_dataset(self, img_path, mode="val", batch=None):
|
||||
"""Build an RTDETR Dataset.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the folder containing images.
|
||||
mode (str, optional): `train` mode or `val` mode, users are able to customize different augmentations for
|
||||
each mode.
|
||||
batch (int, optional): Size of batches, this is for `rect`.
|
||||
|
||||
Returns:
|
||||
(RTDETRDataset): Dataset configured for RT-DETR validation.
|
||||
"""
|
||||
return RTDETRDataset(
|
||||
img_path=img_path,
|
||||
imgsz=self.args.imgsz,
|
||||
batch_size=batch,
|
||||
augment=False, # no augmentation
|
||||
hyp=self.args,
|
||||
rect=False, # no rect
|
||||
cache=self.args.cache or None,
|
||||
prefix=colorstr(f"{mode}: "),
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Return predictions unchanged as RT-DETR handles scaling in postprocessing."""
|
||||
return predn
|
||||
|
||||
def postprocess(
|
||||
self, preds: torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor]
|
||||
) -> list[dict[str, torch.Tensor]]:
|
||||
"""Apply confidence thresholding to prediction outputs.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor | list | tuple): Raw predictions from the model. If tensor, should have shape
|
||||
(batch_size, num_predictions, num_classes + 4) where last dimension contains bbox coords and
|
||||
class scores.
|
||||
|
||||
Returns:
|
||||
(list[dict[str, torch.Tensor]]): List of dictionaries for each image, each containing:
|
||||
- 'bboxes': Tensor of shape (N, 4) with bounding box coordinates
|
||||
- 'conf': Tensor of shape (N,) with confidence scores
|
||||
- 'cls': Tensor of shape (N,) with class indices
|
||||
"""
|
||||
if not isinstance(preds, (list, tuple)): # list for PyTorch inference but list[0] Tensor for export inference
|
||||
preds = [preds, None]
|
||||
|
||||
bs, _, nd = preds[0].shape
|
||||
bboxes, scores = preds[0].split((4, nd - 4), dim=-1)
|
||||
bboxes *= self.args.imgsz
|
||||
outputs = [torch.zeros((0, 6), device=bboxes.device)] * bs
|
||||
for i, bbox in enumerate(bboxes): # (300, 4)
|
||||
bbox = ops.xywh2xyxy(bbox)
|
||||
score, cls = scores[i].max(-1) # (300, )
|
||||
pred = torch.cat([bbox, score[..., None], cls[..., None]], dim=-1) # filter
|
||||
# Sort by confidence to correctly get internal metrics
|
||||
pred = pred[score.argsort(descending=True)]
|
||||
outputs[i] = pred[score > self.args.conf]
|
||||
|
||||
return [{"bboxes": x[:, :4], "conf": x[:, 4], "cls": x[:, 5]} for x in outputs]
|
||||
|
||||
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
|
||||
"""Serialize YOLO predictions to COCO json format.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Predictions dictionary containing 'bboxes', 'conf', and 'cls' keys with
|
||||
bounding box coordinates, confidence scores, and class predictions.
|
||||
pbatch (dict[str, Any]): Batch dictionary containing 'imgsz', 'ori_shape', 'ratio_pad', and 'im_file'.
|
||||
"""
|
||||
path = Path(pbatch["im_file"])
|
||||
stem = path.stem
|
||||
image_id = int(stem) if stem.isnumeric() else stem
|
||||
box = predn["bboxes"].clone()
|
||||
box[..., [0, 2]] *= pbatch["ori_shape"][1] / self.args.imgsz # native-space pred
|
||||
box[..., [1, 3]] *= pbatch["ori_shape"][0] / self.args.imgsz # native-space pred
|
||||
box = ops.xyxy2xywh(box) # xywh
|
||||
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
||||
for b, s, c in zip(box.tolist(), predn["conf"].tolist(), predn["cls"].tolist()):
|
||||
self.jdict.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"file_name": path.name,
|
||||
"category_id": self.class_map[int(c)],
|
||||
"bbox": [round(x, 3) for x in b],
|
||||
"score": round(s, 5),
|
||||
}
|
||||
)
|
||||
25
ultralytics/models/sam/__init__.py
Executable file
25
ultralytics/models/sam/__init__.py
Executable file
@@ -0,0 +1,25 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .model import SAM
|
||||
from .predict import (
|
||||
Predictor,
|
||||
SAM2DynamicInteractivePredictor,
|
||||
SAM2Predictor,
|
||||
SAM2VideoPredictor,
|
||||
SAM3Predictor,
|
||||
SAM3SemanticPredictor,
|
||||
SAM3VideoPredictor,
|
||||
SAM3VideoSemanticPredictor,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"SAM",
|
||||
"Predictor",
|
||||
"SAM2DynamicInteractivePredictor",
|
||||
"SAM2Predictor",
|
||||
"SAM2VideoPredictor",
|
||||
"SAM3Predictor",
|
||||
"SAM3SemanticPredictor",
|
||||
"SAM3VideoPredictor",
|
||||
"SAM3VideoSemanticPredictor",
|
||||
) # tuple or list of exportable items
|
||||
275
ultralytics/models/sam/amg.py
Executable file
275
ultralytics/models/sam/amg.py
Executable file
@@ -0,0 +1,275 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Generator
|
||||
from itertools import product
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def is_box_near_crop_edge(
|
||||
boxes: torch.Tensor, crop_box: list[int], orig_box: list[int], atol: float = 20.0
|
||||
) -> torch.Tensor:
|
||||
"""Determine if bounding boxes are near the edge of a cropped image region using a specified tolerance.
|
||||
|
||||
Args:
|
||||
boxes (torch.Tensor): Bounding boxes in XYXY format.
|
||||
crop_box (list[int]): Crop box coordinates in [x0, y0, x1, y1] format.
|
||||
orig_box (list[int]): Original image box coordinates in [x0, y0, x1, y1] format.
|
||||
atol (float, optional): Absolute tolerance for edge proximity detection.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Boolean tensor indicating which boxes are near crop edges.
|
||||
|
||||
Examples:
|
||||
>>> boxes = torch.tensor([[10, 10, 50, 50], [100, 100, 150, 150]])
|
||||
>>> crop_box = [0, 0, 200, 200]
|
||||
>>> orig_box = [0, 0, 300, 300]
|
||||
>>> near_edge = is_box_near_crop_edge(boxes, crop_box, orig_box, atol=20.0)
|
||||
"""
|
||||
crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device)
|
||||
orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device)
|
||||
boxes = uncrop_boxes_xyxy(boxes, crop_box).float()
|
||||
near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0)
|
||||
near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0)
|
||||
near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge)
|
||||
return torch.any(near_crop_edge, dim=1)
|
||||
|
||||
|
||||
def batch_iterator(batch_size: int, *args) -> Generator[list[Any]]:
|
||||
"""Yield batches of data from input arguments with specified batch size for efficient processing.
|
||||
|
||||
This function takes a batch size and any number of iterables, then yields batches of elements from those
|
||||
iterables. All input iterables must have the same length.
|
||||
|
||||
Args:
|
||||
batch_size (int): Size of each batch to yield.
|
||||
*args (Any): Variable length input iterables to batch. All iterables must have the same length.
|
||||
|
||||
Yields:
|
||||
(list[Any]): A list of batched elements from each input iterable.
|
||||
|
||||
Examples:
|
||||
>>> data = [1, 2, 3, 4, 5]
|
||||
>>> labels = ["a", "b", "c", "d", "e"]
|
||||
>>> for batch in batch_iterator(2, data, labels):
|
||||
... print(batch)
|
||||
[[1, 2], ['a', 'b']]
|
||||
[[3, 4], ['c', 'd']]
|
||||
[[5], ['e']]
|
||||
"""
|
||||
assert args and all(len(a) == len(args[0]) for a in args), "Batched iteration must have same-size inputs."
|
||||
n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0)
|
||||
for b in range(n_batches):
|
||||
yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args]
|
||||
|
||||
|
||||
def calculate_stability_score(masks: torch.Tensor, mask_threshold: float, threshold_offset: float) -> torch.Tensor:
|
||||
"""Compute the stability score for a batch of masks.
|
||||
|
||||
The stability score is the IoU between binary masks obtained by thresholding the predicted mask logits at high and
|
||||
low values.
|
||||
|
||||
Args:
|
||||
masks (torch.Tensor): Batch of predicted mask logits.
|
||||
mask_threshold (float): Threshold value for creating binary masks.
|
||||
threshold_offset (float): Offset applied to the threshold for creating high and low binary masks.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Stability scores for each mask in the batch.
|
||||
|
||||
Examples:
|
||||
>>> masks = torch.rand(10, 256, 256) # Batch of 10 masks
|
||||
>>> mask_threshold = 0.5
|
||||
>>> threshold_offset = 0.1
|
||||
>>> stability_scores = calculate_stability_score(masks, mask_threshold, threshold_offset)
|
||||
|
||||
Notes:
|
||||
- One mask is always contained inside the other.
|
||||
- Memory is saved by preventing unnecessary cast to torch.int64.
|
||||
"""
|
||||
intersections = (masks > (mask_threshold + threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
|
||||
unions = (masks > (mask_threshold - threshold_offset)).sum(-1, dtype=torch.int16).sum(-1, dtype=torch.int32)
|
||||
return intersections / unions
|
||||
|
||||
|
||||
def build_point_grid(n_per_side: int) -> np.ndarray:
|
||||
"""Generate a 2D grid of evenly spaced points in the range [0,1]x[0,1] for image segmentation tasks."""
|
||||
offset = 1 / (2 * n_per_side)
|
||||
points_one_side = np.linspace(offset, 1 - offset, n_per_side)
|
||||
points_x = np.tile(points_one_side[None, :], (n_per_side, 1))
|
||||
points_y = np.tile(points_one_side[:, None], (1, n_per_side))
|
||||
return np.stack([points_x, points_y], axis=-1).reshape(-1, 2)
|
||||
|
||||
|
||||
def build_all_layer_point_grids(n_per_side: int, n_layers: int, scale_per_layer: int) -> list[np.ndarray]:
|
||||
"""Generate point grids for multiple crop layers with varying scales and densities."""
|
||||
return [build_point_grid(int(n_per_side / (scale_per_layer**i))) for i in range(n_layers + 1)]
|
||||
|
||||
|
||||
def generate_crop_boxes(
|
||||
im_size: tuple[int, ...], n_layers: int, overlap_ratio: float
|
||||
) -> tuple[list[list[int]], list[int]]:
|
||||
"""Generate crop boxes of varying sizes for multiscale image processing, with layered overlapping regions.
|
||||
|
||||
Args:
|
||||
im_size (tuple[int, ...]): Height and width of the input image.
|
||||
n_layers (int): Number of layers to generate crop boxes for.
|
||||
overlap_ratio (float): Ratio of overlap between adjacent crop boxes.
|
||||
|
||||
Returns:
|
||||
crop_boxes (list[list[int]]): List of crop boxes in [x0, y0, x1, y1] format.
|
||||
layer_idxs (list[int]): List of layer indices corresponding to each crop box.
|
||||
|
||||
Examples:
|
||||
>>> im_size = (800, 1200) # Height, width
|
||||
>>> n_layers = 3
|
||||
>>> overlap_ratio = 0.25
|
||||
>>> crop_boxes, layer_idxs = generate_crop_boxes(im_size, n_layers, overlap_ratio)
|
||||
"""
|
||||
crop_boxes, layer_idxs = [], []
|
||||
im_h, im_w = im_size
|
||||
short_side = min(im_h, im_w)
|
||||
|
||||
# Original image
|
||||
crop_boxes.append([0, 0, im_w, im_h])
|
||||
layer_idxs.append(0)
|
||||
|
||||
def crop_len(orig_len, n_crops, overlap):
|
||||
"""Calculate the length of each crop given the original length, number of crops, and overlap."""
|
||||
return math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops)
|
||||
|
||||
for i_layer in range(n_layers):
|
||||
n_crops_per_side = 2 ** (i_layer + 1)
|
||||
overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side))
|
||||
|
||||
crop_w = crop_len(im_w, n_crops_per_side, overlap)
|
||||
crop_h = crop_len(im_h, n_crops_per_side, overlap)
|
||||
|
||||
crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)]
|
||||
crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)]
|
||||
|
||||
# Crops in XYWH format
|
||||
for x0, y0 in product(crop_box_x0, crop_box_y0):
|
||||
box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)]
|
||||
crop_boxes.append(box)
|
||||
layer_idxs.append(i_layer + 1)
|
||||
|
||||
return crop_boxes, layer_idxs
|
||||
|
||||
|
||||
def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: list[int]) -> torch.Tensor:
|
||||
"""Uncrop bounding boxes by adding the crop box offset to their coordinates."""
|
||||
x0, y0, _, _ = crop_box
|
||||
offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device)
|
||||
# Check if boxes has a channel dimension
|
||||
if len(boxes.shape) == 3:
|
||||
offset = offset.unsqueeze(1)
|
||||
return boxes + offset
|
||||
|
||||
|
||||
def uncrop_points(points: torch.Tensor, crop_box: list[int]) -> torch.Tensor:
|
||||
"""Uncrop points by adding the crop box offset to their coordinates."""
|
||||
x0, y0, _, _ = crop_box
|
||||
offset = torch.tensor([[x0, y0]], device=points.device)
|
||||
# Check if points has a channel dimension
|
||||
if len(points.shape) == 3:
|
||||
offset = offset.unsqueeze(1)
|
||||
return points + offset
|
||||
|
||||
|
||||
def uncrop_masks(masks: torch.Tensor, crop_box: list[int], orig_h: int, orig_w: int) -> torch.Tensor:
|
||||
"""Uncrop masks by padding them to the original image size, handling coordinate transformations."""
|
||||
x0, y0, x1, y1 = crop_box
|
||||
if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h:
|
||||
return masks
|
||||
# Coordinate transform masks
|
||||
pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0)
|
||||
pad = (x0, pad_x - x0, y0, pad_y - y0)
|
||||
return torch.nn.functional.pad(masks, pad, value=0)
|
||||
|
||||
|
||||
def remove_small_regions(mask: np.ndarray, area_thresh: float, mode: str) -> tuple[np.ndarray, bool]:
|
||||
"""Remove small disconnected regions or holes in a mask based on area threshold and mode.
|
||||
|
||||
Args:
|
||||
mask (np.ndarray): Binary mask to process.
|
||||
area_thresh (float): Area threshold below which regions will be removed.
|
||||
mode (str): Processing mode, either 'holes' to fill small holes or 'islands' to remove small disconnected
|
||||
regions.
|
||||
|
||||
Returns:
|
||||
processed_mask (np.ndarray): Processed binary mask with small regions removed.
|
||||
modified (bool): Whether any regions were modified.
|
||||
|
||||
Examples:
|
||||
>>> mask = np.zeros((100, 100), dtype=np.bool_)
|
||||
>>> mask[40:60, 40:60] = True # Create a square
|
||||
>>> mask[45:55, 45:55] = False # Create a hole
|
||||
>>> processed_mask, modified = remove_small_regions(mask, 50, "holes")
|
||||
"""
|
||||
import cv2 # type: ignore
|
||||
|
||||
assert mode in {"holes", "islands"}, f"Provided mode {mode} is invalid"
|
||||
correct_holes = mode == "holes"
|
||||
working_mask = (correct_holes ^ mask).astype(np.uint8)
|
||||
n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8)
|
||||
sizes = stats[:, -1][1:] # Row 0 is background label
|
||||
small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh]
|
||||
if not small_regions:
|
||||
return mask, False
|
||||
fill_labels = [0, *small_regions]
|
||||
if not correct_holes:
|
||||
# If every region is below threshold, keep largest
|
||||
fill_labels = [i for i in range(n_labels) if i not in fill_labels] or [int(np.argmax(sizes)) + 1]
|
||||
mask = np.isin(regions, fill_labels)
|
||||
return mask, True
|
||||
|
||||
|
||||
def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor:
|
||||
"""Calculate bounding boxes in XYXY format around binary masks.
|
||||
|
||||
Args:
|
||||
masks (torch.Tensor): Binary masks with shape (B, H, W) or (B, C, H, W).
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Bounding boxes in XYXY format with shape (B, 4) or (B, C, 4).
|
||||
|
||||
Notes:
|
||||
- Handles empty masks by returning zero boxes.
|
||||
- Preserves input tensor dimensions in the output.
|
||||
"""
|
||||
# torch.max below raises an error on empty inputs, just skip in this case
|
||||
if torch.numel(masks) == 0:
|
||||
return torch.zeros(*masks.shape[:-2], 4, device=masks.device)
|
||||
|
||||
# Normalize shape to CxHxW
|
||||
shape = masks.shape
|
||||
h, w = shape[-2:]
|
||||
masks = masks.flatten(0, -3) if len(shape) > 2 else masks.unsqueeze(0)
|
||||
# Get top and bottom edges
|
||||
in_height, _ = torch.max(masks, dim=-1)
|
||||
in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :]
|
||||
bottom_edges, _ = torch.max(in_height_coords, dim=-1)
|
||||
in_height_coords = in_height_coords + h * (~in_height)
|
||||
top_edges, _ = torch.min(in_height_coords, dim=-1)
|
||||
|
||||
# Get left and right edges
|
||||
in_width, _ = torch.max(masks, dim=-2)
|
||||
in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :]
|
||||
right_edges, _ = torch.max(in_width_coords, dim=-1)
|
||||
in_width_coords = in_width_coords + w * (~in_width)
|
||||
left_edges, _ = torch.min(in_width_coords, dim=-1)
|
||||
|
||||
# If the mask is empty the right edge will be to the left of the left edge.
|
||||
# Replace these boxes with [0, 0, 0, 0]
|
||||
empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges)
|
||||
out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1)
|
||||
out = out * (~empty_filter).unsqueeze(-1)
|
||||
|
||||
# Return to original shape
|
||||
return out.reshape(*shape[:-2], 4) if len(shape) > 2 else out[0]
|
||||
365
ultralytics/models/sam/build.py
Executable file
365
ultralytics/models/sam/build.py
Executable file
@@ -0,0 +1,365 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from functools import partial
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.utils.downloads import attempt_download_asset
|
||||
from ultralytics.utils.patches import torch_load
|
||||
|
||||
from .modules.decoders import MaskDecoder
|
||||
from .modules.encoders import FpnNeck, Hiera, ImageEncoder, ImageEncoderViT, MemoryEncoder, PromptEncoder
|
||||
from .modules.memory_attention import MemoryAttention, MemoryAttentionLayer
|
||||
from .modules.sam import SAM2Model, SAMModel
|
||||
from .modules.tiny_encoder import TinyViT
|
||||
from .modules.transformer import TwoWayTransformer
|
||||
|
||||
|
||||
def _load_checkpoint(model, checkpoint):
|
||||
"""Load checkpoint into model from file path."""
|
||||
if checkpoint is None:
|
||||
return model
|
||||
|
||||
checkpoint = attempt_download_asset(checkpoint)
|
||||
with open(checkpoint, "rb") as f:
|
||||
state_dict = torch_load(f)
|
||||
# Handle nested "model" key
|
||||
if "model" in state_dict and isinstance(state_dict["model"], dict):
|
||||
state_dict = state_dict["model"]
|
||||
model.load_state_dict(state_dict)
|
||||
return model
|
||||
|
||||
|
||||
def build_sam_vit_h(checkpoint=None):
|
||||
"""Build and return a Segment Anything Model (SAM) h-size model with specified encoder parameters."""
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1280,
|
||||
encoder_depth=32,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[7, 15, 23, 31],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_l(checkpoint=None):
|
||||
"""Build and return a Segment Anything Model (SAM) l-size model with specified encoder parameters."""
|
||||
return _build_sam(
|
||||
encoder_embed_dim=1024,
|
||||
encoder_depth=24,
|
||||
encoder_num_heads=16,
|
||||
encoder_global_attn_indexes=[5, 11, 17, 23],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam_vit_b(checkpoint=None):
|
||||
"""Build and return a Segment Anything Model (SAM) b-size model with specified encoder parameters."""
|
||||
return _build_sam(
|
||||
encoder_embed_dim=768,
|
||||
encoder_depth=12,
|
||||
encoder_num_heads=12,
|
||||
encoder_global_attn_indexes=[2, 5, 8, 11],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_mobile_sam(checkpoint=None):
|
||||
"""Build and return a Mobile Segment Anything Model (Mobile-SAM) for efficient image segmentation."""
|
||||
return _build_sam(
|
||||
encoder_embed_dim=[64, 128, 160, 320],
|
||||
encoder_depth=[2, 2, 6, 2],
|
||||
encoder_num_heads=[2, 4, 5, 10],
|
||||
encoder_global_attn_indexes=None,
|
||||
mobile_sam=True,
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam2_t(checkpoint=None):
|
||||
"""Build and return a Segment Anything Model 2 (SAM2) tiny-size model with specified architecture parameters."""
|
||||
return _build_sam2(
|
||||
encoder_embed_dim=96,
|
||||
encoder_stages=[1, 2, 7, 2],
|
||||
encoder_num_heads=1,
|
||||
encoder_global_att_blocks=[5, 7, 9],
|
||||
encoder_window_spec=[8, 4, 14, 7],
|
||||
encoder_backbone_channel_list=[768, 384, 192, 96],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam2_s(checkpoint=None):
|
||||
"""Build and return a small-size Segment Anything Model 2 (SAM2) with specified architecture parameters."""
|
||||
return _build_sam2(
|
||||
encoder_embed_dim=96,
|
||||
encoder_stages=[1, 2, 11, 2],
|
||||
encoder_num_heads=1,
|
||||
encoder_global_att_blocks=[7, 10, 13],
|
||||
encoder_window_spec=[8, 4, 14, 7],
|
||||
encoder_backbone_channel_list=[768, 384, 192, 96],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam2_b(checkpoint=None):
|
||||
"""Build and return a Segment Anything Model 2 (SAM2) base-size model with specified architecture parameters."""
|
||||
return _build_sam2(
|
||||
encoder_embed_dim=112,
|
||||
encoder_stages=[2, 3, 16, 3],
|
||||
encoder_num_heads=2,
|
||||
encoder_global_att_blocks=[12, 16, 20],
|
||||
encoder_window_spec=[8, 4, 14, 7],
|
||||
encoder_window_spatial_size=[14, 14],
|
||||
encoder_backbone_channel_list=[896, 448, 224, 112],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def build_sam2_l(checkpoint=None):
|
||||
"""Build and return a large-size Segment Anything Model 2 (SAM2) with specified architecture parameters."""
|
||||
return _build_sam2(
|
||||
encoder_embed_dim=144,
|
||||
encoder_stages=[2, 6, 36, 4],
|
||||
encoder_num_heads=2,
|
||||
encoder_global_att_blocks=[23, 33, 43],
|
||||
encoder_window_spec=[8, 4, 16, 8],
|
||||
encoder_backbone_channel_list=[1152, 576, 288, 144],
|
||||
checkpoint=checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def _build_sam(
|
||||
encoder_embed_dim,
|
||||
encoder_depth,
|
||||
encoder_num_heads,
|
||||
encoder_global_attn_indexes,
|
||||
checkpoint=None,
|
||||
mobile_sam=False,
|
||||
):
|
||||
"""Build a Segment Anything Model (SAM) with specified encoder parameters.
|
||||
|
||||
Args:
|
||||
encoder_embed_dim (int | list[int]): Embedding dimension for the encoder.
|
||||
encoder_depth (int | list[int]): Depth of the encoder.
|
||||
encoder_num_heads (int | list[int]): Number of attention heads in the encoder.
|
||||
encoder_global_attn_indexes (list[int] | None): Indexes for global attention in the encoder.
|
||||
checkpoint (str | None, optional): Path to the model checkpoint file.
|
||||
mobile_sam (bool, optional): Whether to build a Mobile-SAM model.
|
||||
|
||||
Returns:
|
||||
(SAMModel): A Segment Anything Model instance with the specified architecture.
|
||||
|
||||
Examples:
|
||||
>>> sam = _build_sam(768, 12, 12, [2, 5, 8, 11])
|
||||
>>> sam = _build_sam([64, 128, 160, 320], [2, 2, 6, 2], [2, 4, 5, 10], None, mobile_sam=True)
|
||||
"""
|
||||
prompt_embed_dim = 256
|
||||
image_size = 1024
|
||||
vit_patch_size = 16
|
||||
image_embedding_size = image_size // vit_patch_size
|
||||
image_encoder = (
|
||||
TinyViT(
|
||||
img_size=1024,
|
||||
in_chans=3,
|
||||
num_classes=1000,
|
||||
embed_dims=encoder_embed_dim,
|
||||
depths=encoder_depth,
|
||||
num_heads=encoder_num_heads,
|
||||
window_sizes=[7, 7, 14, 7],
|
||||
mlp_ratio=4.0,
|
||||
drop_rate=0.0,
|
||||
drop_path_rate=0.0,
|
||||
use_checkpoint=False,
|
||||
mbconv_expand_ratio=4.0,
|
||||
local_conv_size=3,
|
||||
layer_lr_decay=0.8,
|
||||
)
|
||||
if mobile_sam
|
||||
else ImageEncoderViT(
|
||||
depth=encoder_depth,
|
||||
embed_dim=encoder_embed_dim,
|
||||
img_size=image_size,
|
||||
mlp_ratio=4,
|
||||
norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
|
||||
num_heads=encoder_num_heads,
|
||||
patch_size=vit_patch_size,
|
||||
qkv_bias=True,
|
||||
use_rel_pos=True,
|
||||
global_attn_indexes=encoder_global_attn_indexes,
|
||||
window_size=14,
|
||||
out_chans=prompt_embed_dim,
|
||||
)
|
||||
)
|
||||
sam = SAMModel(
|
||||
image_encoder=image_encoder,
|
||||
prompt_encoder=PromptEncoder(
|
||||
embed_dim=prompt_embed_dim,
|
||||
image_embedding_size=(image_embedding_size, image_embedding_size),
|
||||
input_image_size=(image_size, image_size),
|
||||
mask_in_chans=16,
|
||||
),
|
||||
mask_decoder=MaskDecoder(
|
||||
num_multimask_outputs=3,
|
||||
transformer=TwoWayTransformer(
|
||||
depth=2,
|
||||
embedding_dim=prompt_embed_dim,
|
||||
mlp_dim=2048,
|
||||
num_heads=8,
|
||||
),
|
||||
transformer_dim=prompt_embed_dim,
|
||||
iou_head_depth=3,
|
||||
iou_head_hidden_dim=256,
|
||||
),
|
||||
pixel_mean=[123.675, 116.28, 103.53],
|
||||
pixel_std=[58.395, 57.12, 57.375],
|
||||
)
|
||||
if checkpoint is not None:
|
||||
sam = _load_checkpoint(sam, checkpoint)
|
||||
sam.eval()
|
||||
return sam
|
||||
|
||||
|
||||
def _build_sam2(
|
||||
encoder_embed_dim=1280,
|
||||
encoder_stages=(2, 6, 36, 4),
|
||||
encoder_num_heads=2,
|
||||
encoder_global_att_blocks=(7, 15, 23, 31),
|
||||
encoder_backbone_channel_list=(1152, 576, 288, 144),
|
||||
encoder_window_spatial_size=(7, 7),
|
||||
encoder_window_spec=(8, 4, 16, 8),
|
||||
checkpoint=None,
|
||||
):
|
||||
"""Build and return a Segment Anything Model 2 (SAM2) with specified architecture parameters.
|
||||
|
||||
Args:
|
||||
encoder_embed_dim (int, optional): Embedding dimension for the encoder.
|
||||
encoder_stages (list[int], optional): Number of blocks in each stage of the encoder.
|
||||
encoder_num_heads (int, optional): Number of attention heads in the encoder.
|
||||
encoder_global_att_blocks (list[int], optional): Indices of global attention blocks in the encoder.
|
||||
encoder_backbone_channel_list (list[int], optional): Channel dimensions for each level of the encoder backbone.
|
||||
encoder_window_spatial_size (list[int], optional): Spatial size of the window for position embeddings.
|
||||
encoder_window_spec (list[int], optional): Window specifications for each stage of the encoder.
|
||||
checkpoint (str | None, optional): Path to the checkpoint file for loading pre-trained weights.
|
||||
|
||||
Returns:
|
||||
(SAM2Model): A configured and initialized SAM2 model.
|
||||
|
||||
Examples:
|
||||
>>> sam2_model = _build_sam2(encoder_embed_dim=96, encoder_stages=[1, 2, 7, 2])
|
||||
>>> sam2_model.eval()
|
||||
"""
|
||||
image_encoder = ImageEncoder(
|
||||
trunk=Hiera(
|
||||
embed_dim=encoder_embed_dim,
|
||||
num_heads=encoder_num_heads,
|
||||
stages=encoder_stages,
|
||||
global_att_blocks=encoder_global_att_blocks,
|
||||
window_pos_embed_bkg_spatial_size=encoder_window_spatial_size,
|
||||
window_spec=encoder_window_spec,
|
||||
),
|
||||
neck=FpnNeck(
|
||||
d_model=256,
|
||||
backbone_channel_list=encoder_backbone_channel_list,
|
||||
fpn_top_down_levels=[2, 3],
|
||||
fpn_interp_model="nearest",
|
||||
),
|
||||
scalp=1,
|
||||
)
|
||||
memory_attention = MemoryAttention(d_model=256, pos_enc_at_input=True, num_layers=4, layer=MemoryAttentionLayer())
|
||||
memory_encoder = MemoryEncoder(out_dim=64)
|
||||
|
||||
is_sam2_1 = checkpoint is not None and "sam2.1" in checkpoint
|
||||
sam2 = SAM2Model(
|
||||
image_encoder=image_encoder,
|
||||
memory_attention=memory_attention,
|
||||
memory_encoder=memory_encoder,
|
||||
num_maskmem=7,
|
||||
image_size=1024,
|
||||
sigmoid_scale_for_mem_enc=20.0,
|
||||
sigmoid_bias_for_mem_enc=-10.0,
|
||||
use_mask_input_as_output_without_sam=True,
|
||||
directly_add_no_mem_embed=True,
|
||||
use_high_res_features_in_sam=True,
|
||||
multimask_output_in_sam=True,
|
||||
iou_prediction_use_sigmoid=True,
|
||||
use_obj_ptrs_in_encoder=True,
|
||||
add_tpos_enc_to_obj_ptrs=True,
|
||||
only_obj_ptrs_in_the_past_for_eval=True,
|
||||
pred_obj_scores=True,
|
||||
pred_obj_scores_mlp=True,
|
||||
fixed_no_obj_ptr=True,
|
||||
multimask_output_for_tracking=True,
|
||||
use_multimask_token_for_obj_ptr=True,
|
||||
multimask_min_pt_num=0,
|
||||
multimask_max_pt_num=1,
|
||||
use_mlp_for_obj_ptr_proj=True,
|
||||
compile_image_encoder=False,
|
||||
no_obj_embed_spatial=is_sam2_1,
|
||||
proj_tpos_enc_in_obj_ptrs=is_sam2_1,
|
||||
use_signed_tpos_enc_to_obj_ptrs=is_sam2_1,
|
||||
sam_mask_decoder_extra_args=dict(
|
||||
dynamic_multimask_via_stability=True,
|
||||
dynamic_multimask_stability_delta=0.05,
|
||||
dynamic_multimask_stability_thresh=0.98,
|
||||
),
|
||||
)
|
||||
|
||||
if checkpoint is not None:
|
||||
sam2 = _load_checkpoint(sam2, checkpoint)
|
||||
sam2.eval()
|
||||
return sam2
|
||||
|
||||
|
||||
sam_model_map = {
|
||||
"sam_h.pt": build_sam_vit_h,
|
||||
"sam_l.pt": build_sam_vit_l,
|
||||
"sam_b.pt": build_sam_vit_b,
|
||||
"mobile_sam.pt": build_mobile_sam,
|
||||
"sam2_t.pt": build_sam2_t,
|
||||
"sam2_s.pt": build_sam2_s,
|
||||
"sam2_b.pt": build_sam2_b,
|
||||
"sam2_l.pt": build_sam2_l,
|
||||
"sam2.1_t.pt": build_sam2_t,
|
||||
"sam2.1_s.pt": build_sam2_s,
|
||||
"sam2.1_b.pt": build_sam2_b,
|
||||
"sam2.1_l.pt": build_sam2_l,
|
||||
}
|
||||
|
||||
|
||||
def build_sam(ckpt="sam_b.pt"):
|
||||
"""Build and return a Segment Anything Model (SAM) based on the provided checkpoint.
|
||||
|
||||
Args:
|
||||
ckpt (str | Path, optional): Path to the checkpoint file or name of a pre-defined SAM model.
|
||||
|
||||
Returns:
|
||||
(SAMModel | SAM2Model): A configured and initialized SAM or SAM2 model instance.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the provided checkpoint is not a supported SAM model.
|
||||
|
||||
Examples:
|
||||
>>> sam_model = build_sam("sam_b.pt")
|
||||
>>> sam_model = build_sam("path/to/custom_checkpoint.pt")
|
||||
|
||||
Notes:
|
||||
Supported pre-defined models include:
|
||||
- SAM: 'sam_h.pt', 'sam_l.pt', 'sam_b.pt', 'mobile_sam.pt'
|
||||
- SAM2: 'sam2_t.pt', 'sam2_s.pt', 'sam2_b.pt', 'sam2_l.pt'
|
||||
"""
|
||||
model_builder = None
|
||||
ckpt = str(ckpt) # to allow Path ckpt types
|
||||
for k in sam_model_map.keys():
|
||||
if ckpt.endswith(k):
|
||||
model_builder = sam_model_map.get(k)
|
||||
|
||||
if not model_builder:
|
||||
raise FileNotFoundError(f"{ckpt} is not a supported SAM model. Available models are: \n {sam_model_map.keys()}")
|
||||
|
||||
return model_builder(ckpt)
|
||||
382
ultralytics/models/sam/build_sam3.py
Executable file
382
ultralytics/models/sam/build_sam3.py
Executable file
@@ -0,0 +1,382 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
from ultralytics.nn.modules.transformer import MLP
|
||||
from ultralytics.utils.patches import torch_load
|
||||
|
||||
from .modules.blocks import PositionEmbeddingSine, RoPEAttention
|
||||
from .modules.encoders import MemoryEncoder
|
||||
from .modules.memory_attention import MemoryAttention, MemoryAttentionLayer
|
||||
from .modules.sam import SAM3Model
|
||||
from .sam3.decoder import TransformerDecoder, TransformerDecoderLayer
|
||||
from .sam3.encoder import TransformerEncoderFusion, TransformerEncoderLayer
|
||||
from .sam3.geometry_encoders import SequenceGeometryEncoder
|
||||
from .sam3.maskformer_segmentation import PixelDecoder, UniversalSegmentationHead
|
||||
from .sam3.model_misc import DotProductScoring, TransformerWrapper
|
||||
from .sam3.necks import Sam3DualViTDetNeck
|
||||
from .sam3.sam3_image import SAM3SemanticModel
|
||||
from .sam3.text_encoder_ve import VETextEncoder
|
||||
from .sam3.vitdet import ViT
|
||||
from .sam3.vl_combiner import SAM3VLBackbone
|
||||
|
||||
|
||||
def _create_vision_backbone(compile_mode=None, enable_inst_interactivity=True) -> Sam3DualViTDetNeck:
|
||||
"""Create SAM3 visual backbone with ViT and neck."""
|
||||
# Position encoding
|
||||
position_encoding = PositionEmbeddingSine(
|
||||
num_pos_feats=256,
|
||||
normalize=True,
|
||||
scale=None,
|
||||
temperature=10000,
|
||||
)
|
||||
|
||||
# ViT backbone
|
||||
vit_backbone = ViT(
|
||||
img_size=1008,
|
||||
pretrain_img_size=336,
|
||||
patch_size=14,
|
||||
embed_dim=1024,
|
||||
depth=32,
|
||||
num_heads=16,
|
||||
mlp_ratio=4.625,
|
||||
norm_layer="LayerNorm",
|
||||
drop_path_rate=0.1,
|
||||
qkv_bias=True,
|
||||
use_abs_pos=True,
|
||||
tile_abs_pos=True,
|
||||
global_att_blocks=(7, 15, 23, 31),
|
||||
rel_pos_blocks=(),
|
||||
use_rope=True,
|
||||
use_interp_rope=True,
|
||||
window_size=24,
|
||||
pretrain_use_cls_token=True,
|
||||
retain_cls_token=False,
|
||||
ln_pre=True,
|
||||
ln_post=False,
|
||||
return_interm_layers=False,
|
||||
bias_patch_embed=False,
|
||||
compile_mode=compile_mode,
|
||||
)
|
||||
return Sam3DualViTDetNeck(
|
||||
position_encoding=position_encoding,
|
||||
d_model=256,
|
||||
scale_factors=[4.0, 2.0, 1.0, 0.5],
|
||||
trunk=vit_backbone,
|
||||
add_sam2_neck=enable_inst_interactivity,
|
||||
)
|
||||
|
||||
|
||||
def _create_sam3_transformer() -> TransformerWrapper:
|
||||
"""Create SAM3 detector encoder and decoder."""
|
||||
encoder: TransformerEncoderFusion = TransformerEncoderFusion(
|
||||
layer=TransformerEncoderLayer(
|
||||
d_model=256,
|
||||
dim_feedforward=2048,
|
||||
dropout=0.1,
|
||||
pos_enc_at_attn=True,
|
||||
pos_enc_at_cross_attn_keys=False,
|
||||
pos_enc_at_cross_attn_queries=False,
|
||||
pre_norm=True,
|
||||
self_attention=nn.MultiheadAttention(
|
||||
num_heads=8,
|
||||
dropout=0.1,
|
||||
embed_dim=256,
|
||||
batch_first=True,
|
||||
),
|
||||
cross_attention=nn.MultiheadAttention(
|
||||
num_heads=8,
|
||||
dropout=0.1,
|
||||
embed_dim=256,
|
||||
batch_first=True,
|
||||
),
|
||||
),
|
||||
num_layers=6,
|
||||
d_model=256,
|
||||
num_feature_levels=1,
|
||||
frozen=False,
|
||||
use_act_checkpoint=True,
|
||||
add_pooled_text_to_img_feat=False,
|
||||
pool_text_with_mask=True,
|
||||
)
|
||||
decoder: TransformerDecoder = TransformerDecoder(
|
||||
layer=TransformerDecoderLayer(
|
||||
d_model=256,
|
||||
dim_feedforward=2048,
|
||||
dropout=0.1,
|
||||
cross_attention=nn.MultiheadAttention(
|
||||
num_heads=8,
|
||||
dropout=0.1,
|
||||
embed_dim=256,
|
||||
),
|
||||
n_heads=8,
|
||||
use_text_cross_attention=True,
|
||||
),
|
||||
num_layers=6,
|
||||
num_queries=200,
|
||||
return_intermediate=True,
|
||||
box_refine=True,
|
||||
num_o2m_queries=0,
|
||||
dac=True,
|
||||
boxRPB="log",
|
||||
d_model=256,
|
||||
frozen=False,
|
||||
interaction_layer=None,
|
||||
dac_use_selfatt_ln=True,
|
||||
use_act_checkpoint=True,
|
||||
presence_token=True,
|
||||
)
|
||||
|
||||
return TransformerWrapper(encoder=encoder, decoder=decoder, d_model=256)
|
||||
|
||||
|
||||
def build_sam3_image_model(checkpoint_path: str, enable_segmentation: bool = True, compile: bool = False):
|
||||
"""Build SAM3 image model.
|
||||
|
||||
Args:
|
||||
checkpoint_path: Optional path to model checkpoint
|
||||
enable_segmentation: Whether to enable segmentation head
|
||||
compile: Whether to enable compilation of the model
|
||||
|
||||
Returns:
|
||||
A SAM3 image model
|
||||
"""
|
||||
try:
|
||||
import clip
|
||||
except ImportError:
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
|
||||
check_requirements("git+https://github.com/ultralytics/CLIP.git")
|
||||
import clip
|
||||
# Create visual components
|
||||
compile_mode = "default" if compile else None
|
||||
vision_encoder = _create_vision_backbone(compile_mode=compile_mode, enable_inst_interactivity=True)
|
||||
|
||||
# Create text components
|
||||
text_encoder = VETextEncoder(
|
||||
tokenizer=clip.simple_tokenizer.SimpleTokenizer(),
|
||||
d_model=256,
|
||||
width=1024,
|
||||
heads=16,
|
||||
layers=24,
|
||||
)
|
||||
|
||||
# Create visual-language backbone
|
||||
backbone = SAM3VLBackbone(visual=vision_encoder, text=text_encoder, scalp=1)
|
||||
|
||||
# Create transformer components
|
||||
transformer = _create_sam3_transformer()
|
||||
|
||||
# Create dot product scoring
|
||||
dot_prod_scoring = DotProductScoring(
|
||||
d_model=256,
|
||||
d_proj=256,
|
||||
prompt_mlp=MLP(
|
||||
input_dim=256,
|
||||
hidden_dim=2048,
|
||||
output_dim=256,
|
||||
num_layers=2,
|
||||
residual=True,
|
||||
out_norm=nn.LayerNorm(256),
|
||||
),
|
||||
)
|
||||
|
||||
# Create segmentation head if enabled
|
||||
segmentation_head = (
|
||||
UniversalSegmentationHead(
|
||||
hidden_dim=256,
|
||||
upsampling_stages=3,
|
||||
aux_masks=False,
|
||||
presence_head=False,
|
||||
dot_product_scorer=None,
|
||||
act_ckpt=True,
|
||||
cross_attend_prompt=nn.MultiheadAttention(
|
||||
num_heads=8,
|
||||
dropout=0,
|
||||
embed_dim=256,
|
||||
),
|
||||
pixel_decoder=PixelDecoder(
|
||||
num_upsampling_stages=3,
|
||||
interpolation_mode="nearest",
|
||||
hidden_dim=256,
|
||||
compile_mode=compile_mode,
|
||||
),
|
||||
)
|
||||
if enable_segmentation
|
||||
else None
|
||||
)
|
||||
|
||||
# Create geometry encoder
|
||||
input_geometry_encoder = SequenceGeometryEncoder(
|
||||
pos_enc=PositionEmbeddingSine(
|
||||
num_pos_feats=256,
|
||||
normalize=True,
|
||||
scale=None,
|
||||
temperature=10000,
|
||||
),
|
||||
encode_boxes_as_points=False,
|
||||
boxes_direct_project=True,
|
||||
boxes_pool=True,
|
||||
boxes_pos_enc=True,
|
||||
d_model=256,
|
||||
num_layers=3,
|
||||
layer=TransformerEncoderLayer(
|
||||
d_model=256,
|
||||
dim_feedforward=2048,
|
||||
dropout=0.1,
|
||||
pos_enc_at_attn=False,
|
||||
pre_norm=True,
|
||||
pos_enc_at_cross_attn_queries=False,
|
||||
pos_enc_at_cross_attn_keys=True,
|
||||
),
|
||||
use_act_ckpt=True,
|
||||
add_cls=True,
|
||||
add_post_encode_proj=True,
|
||||
)
|
||||
|
||||
# Create the SAM3SemanticModel model
|
||||
model = SAM3SemanticModel(
|
||||
backbone=backbone,
|
||||
transformer=transformer,
|
||||
input_geometry_encoder=input_geometry_encoder,
|
||||
segmentation_head=segmentation_head,
|
||||
num_feature_levels=1,
|
||||
o2m_mask_predict=True,
|
||||
dot_prod_scoring=dot_prod_scoring,
|
||||
use_instance_query=False,
|
||||
multimask_output=True,
|
||||
)
|
||||
|
||||
# Load checkpoint
|
||||
model = _load_checkpoint(model, checkpoint_path)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def build_interactive_sam3(checkpoint_path: str, compile=None, with_backbone=True) -> SAM3Model:
|
||||
"""Build the SAM3 Tracker module for video tracking.
|
||||
|
||||
Args:
|
||||
checkpoint_path (str): Path to model checkpoint.
|
||||
compile (str | None): Compilation mode for the vision backbone.
|
||||
with_backbone (bool): Whether to include the vision backbone in the model.
|
||||
|
||||
Returns:
|
||||
(SAM3Model): A configured and initialized SAM3 model.
|
||||
"""
|
||||
# Create model components
|
||||
memory_encoder = MemoryEncoder(out_dim=64, interpol_size=[1152, 1152])
|
||||
memory_attention = MemoryAttention(
|
||||
batch_first=True,
|
||||
d_model=256,
|
||||
pos_enc_at_input=True,
|
||||
layer=MemoryAttentionLayer(
|
||||
dim_feedforward=2048,
|
||||
dropout=0.1,
|
||||
pos_enc_at_attn=False,
|
||||
pos_enc_at_cross_attn_keys=True,
|
||||
pos_enc_at_cross_attn_queries=False,
|
||||
self_attn=RoPEAttention(
|
||||
embedding_dim=256,
|
||||
num_heads=1,
|
||||
downsample_rate=1,
|
||||
rope_theta=10000.0,
|
||||
feat_sizes=[72, 72],
|
||||
),
|
||||
d_model=256,
|
||||
cross_attn=RoPEAttention(
|
||||
embedding_dim=256,
|
||||
num_heads=1,
|
||||
downsample_rate=1,
|
||||
kv_in_dim=64,
|
||||
rope_theta=10000.0,
|
||||
feat_sizes=[72, 72],
|
||||
rope_k_repeat=True,
|
||||
),
|
||||
),
|
||||
num_layers=4,
|
||||
)
|
||||
|
||||
backbone = (
|
||||
SAM3VLBackbone(scalp=1, visual=_create_vision_backbone(compile_mode=compile), text=None)
|
||||
if with_backbone
|
||||
else None
|
||||
)
|
||||
model = SAM3Model(
|
||||
image_size=1008,
|
||||
image_encoder=backbone,
|
||||
memory_attention=memory_attention,
|
||||
memory_encoder=memory_encoder,
|
||||
backbone_stride=14,
|
||||
num_maskmem=7,
|
||||
sigmoid_scale_for_mem_enc=20.0,
|
||||
sigmoid_bias_for_mem_enc=-10.0,
|
||||
use_mask_input_as_output_without_sam=True,
|
||||
directly_add_no_mem_embed=True,
|
||||
use_high_res_features_in_sam=True,
|
||||
multimask_output_in_sam=True,
|
||||
iou_prediction_use_sigmoid=True,
|
||||
use_obj_ptrs_in_encoder=True,
|
||||
add_tpos_enc_to_obj_ptrs=True,
|
||||
only_obj_ptrs_in_the_past_for_eval=True,
|
||||
pred_obj_scores=True,
|
||||
pred_obj_scores_mlp=True,
|
||||
fixed_no_obj_ptr=True,
|
||||
multimask_output_for_tracking=True,
|
||||
use_multimask_token_for_obj_ptr=True,
|
||||
multimask_min_pt_num=0,
|
||||
multimask_max_pt_num=1,
|
||||
use_mlp_for_obj_ptr_proj=True,
|
||||
compile_image_encoder=False,
|
||||
no_obj_embed_spatial=True,
|
||||
proj_tpos_enc_in_obj_ptrs=True,
|
||||
use_signed_tpos_enc_to_obj_ptrs=True,
|
||||
sam_mask_decoder_extra_args=dict(
|
||||
dynamic_multimask_via_stability=True,
|
||||
dynamic_multimask_stability_delta=0.05,
|
||||
dynamic_multimask_stability_thresh=0.98,
|
||||
),
|
||||
)
|
||||
|
||||
# Load checkpoint if provided
|
||||
model = _load_checkpoint(model, checkpoint_path, interactive=True)
|
||||
|
||||
# Setup device and mode
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def _load_checkpoint(model, checkpoint, interactive=False):
|
||||
"""Load SAM3 model checkpoint from file."""
|
||||
with open(checkpoint, "rb") as f:
|
||||
ckpt = torch_load(f)
|
||||
if "model" in ckpt and isinstance(ckpt["model"], dict):
|
||||
ckpt = ckpt["model"]
|
||||
sam3_image_ckpt = {k.replace("detector.", ""): v for k, v in ckpt.items() if "detector" in k}
|
||||
if interactive:
|
||||
sam3_image_ckpt.update(
|
||||
{
|
||||
k.replace("backbone.vision_backbone", "image_encoder.vision_backbone"): v
|
||||
for k, v in sam3_image_ckpt.items()
|
||||
if "backbone.vision_backbone" in k
|
||||
}
|
||||
)
|
||||
sam3_image_ckpt.update(
|
||||
{
|
||||
k.replace("tracker.transformer.encoder", "memory_attention"): v
|
||||
for k, v in ckpt.items()
|
||||
if "tracker.transformer" in k
|
||||
}
|
||||
)
|
||||
sam3_image_ckpt.update(
|
||||
{
|
||||
k.replace("tracker.maskmem_backbone", "memory_encoder"): v
|
||||
for k, v in ckpt.items()
|
||||
if "tracker.maskmem_backbone" in k
|
||||
}
|
||||
)
|
||||
sam3_image_ckpt.update({k.replace("tracker.", ""): v for k, v in ckpt.items() if "tracker." in k})
|
||||
model.load_state_dict(sam3_image_ckpt, strict=False)
|
||||
return model
|
||||
169
ultralytics/models/sam/model.py
Executable file
169
ultralytics/models/sam/model.py
Executable file
@@ -0,0 +1,169 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
"""
|
||||
SAM model interface.
|
||||
|
||||
This module provides an interface to the Segment Anything Model (SAM) from Ultralytics, designed for real-time image
|
||||
segmentation tasks. The SAM model allows for promptable segmentation with unparalleled versatility in image analysis,
|
||||
and has been trained on the SA-1B dataset. It features zero-shot performance capabilities, enabling it to adapt to new
|
||||
image distributions and tasks without prior knowledge.
|
||||
|
||||
Key Features:
|
||||
- Promptable segmentation
|
||||
- Real-time performance
|
||||
- Zero-shot transfer capabilities
|
||||
- Trained on SA-1B dataset
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.engine.model import Model
|
||||
from ultralytics.utils.torch_utils import model_info
|
||||
|
||||
from .predict import Predictor, SAM2Predictor, SAM3Predictor
|
||||
|
||||
|
||||
class SAM(Model):
|
||||
"""SAM (Segment Anything Model) interface class for real-time image segmentation tasks.
|
||||
|
||||
This class provides an interface to the Segment Anything Model (SAM) from Ultralytics, designed for promptable
|
||||
segmentation with versatility in image analysis. It supports various prompts such as bounding boxes, points, or
|
||||
labels, and features zero-shot performance capabilities.
|
||||
|
||||
Attributes:
|
||||
model (torch.nn.Module): The loaded SAM model.
|
||||
is_sam2 (bool): Indicates whether the model is SAM2 variant.
|
||||
task (str): The task type, set to "segment" for SAM models.
|
||||
|
||||
Methods:
|
||||
predict: Perform segmentation prediction on the given image or video source.
|
||||
info: Log information about the SAM model.
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> results = sam.predict("image.jpg", points=[[500, 375]])
|
||||
>>> for r in results:
|
||||
... print(f"Detected {len(r.masks)} masks")
|
||||
"""
|
||||
|
||||
def __init__(self, model: str = "sam_b.pt") -> None:
|
||||
"""Initialize the SAM (Segment Anything Model) instance.
|
||||
|
||||
Args:
|
||||
model (str): Path to the pre-trained SAM model file. File should have a .pt or .pth extension.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If the model file extension is not .pt or .pth.
|
||||
"""
|
||||
if model and Path(model).suffix not in {".pt", ".pth"}:
|
||||
raise NotImplementedError("SAM prediction requires pre-trained *.pt or *.pth model.")
|
||||
self.is_sam2 = "sam2" in Path(model).stem
|
||||
self.is_sam3 = "sam3" in Path(model).stem
|
||||
super().__init__(model=model, task="segment")
|
||||
|
||||
def _load(self, weights: str, task=None):
|
||||
"""Load the specified weights into the SAM model.
|
||||
|
||||
Args:
|
||||
weights (str): Path to the weights file. Should be a .pt or .pth file containing the model parameters.
|
||||
task (str | None): Task name. If provided, it specifies the particular task the model is being loaded for.
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> sam._load("path/to/custom_weights.pt")
|
||||
"""
|
||||
if self.is_sam3:
|
||||
from .build_sam3 import build_interactive_sam3
|
||||
|
||||
self.model = build_interactive_sam3(weights)
|
||||
else:
|
||||
from .build import build_sam # slow import
|
||||
|
||||
self.model = build_sam(weights)
|
||||
|
||||
def predict(self, source, stream: bool = False, bboxes=None, points=None, labels=None, **kwargs):
|
||||
"""Perform segmentation prediction on the given image or video source.
|
||||
|
||||
Args:
|
||||
source (str | PIL.Image | np.ndarray): Path to the image or video file, or a PIL.Image object, or a
|
||||
np.ndarray object.
|
||||
stream (bool): If True, enables real-time streaming.
|
||||
bboxes (list[list[float]] | None): List of bounding box coordinates for prompted segmentation.
|
||||
points (list[list[float]] | None): List of points for prompted segmentation.
|
||||
labels (list[int] | None): List of labels for prompted segmentation.
|
||||
**kwargs (Any): Additional keyword arguments for prediction.
|
||||
|
||||
Returns:
|
||||
(list): The model predictions.
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> results = sam.predict("image.jpg", points=[[500, 375]])
|
||||
>>> for r in results:
|
||||
... print(f"Detected {len(r.masks)} masks")
|
||||
"""
|
||||
overrides = dict(conf=0.25, task="segment", mode="predict", imgsz=1024)
|
||||
kwargs = {**overrides, **kwargs}
|
||||
prompts = dict(bboxes=bboxes, points=points, labels=labels)
|
||||
return super().predict(source, stream, prompts=prompts, **kwargs)
|
||||
|
||||
def __call__(self, source=None, stream: bool = False, bboxes=None, points=None, labels=None, **kwargs):
|
||||
"""Perform segmentation prediction on the given image or video source.
|
||||
|
||||
This method is an alias for the 'predict' method, providing a convenient way to call the SAM model for
|
||||
segmentation tasks.
|
||||
|
||||
Args:
|
||||
source (str | PIL.Image | np.ndarray | None): Path to the image or video file, or a PIL.Image object, or a
|
||||
np.ndarray object.
|
||||
stream (bool): If True, enables real-time streaming.
|
||||
bboxes (list[list[float]] | None): List of bounding box coordinates for prompted segmentation.
|
||||
points (list[list[float]] | None): List of points for prompted segmentation.
|
||||
labels (list[int] | None): List of labels for prompted segmentation.
|
||||
**kwargs (Any): Additional keyword arguments to be passed to the predict method.
|
||||
|
||||
Returns:
|
||||
(list): The model predictions, typically containing segmentation masks and other relevant information.
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> results = sam("image.jpg", points=[[500, 375]])
|
||||
>>> print(f"Detected {len(results[0].masks)} masks")
|
||||
"""
|
||||
return self.predict(source, stream, bboxes, points, labels, **kwargs)
|
||||
|
||||
def info(self, detailed: bool = False, verbose: bool = True):
|
||||
"""Log information about the SAM model.
|
||||
|
||||
Args:
|
||||
detailed (bool): If True, displays detailed information about the model layers and operations.
|
||||
verbose (bool): If True, prints the information to the console.
|
||||
|
||||
Returns:
|
||||
(tuple): A tuple containing the model's information (string representations of the model).
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> info = sam.info()
|
||||
>>> print(info[0]) # Print summary information
|
||||
"""
|
||||
return model_info(self.model, detailed=detailed, verbose=verbose)
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, type[Predictor]]]:
|
||||
"""Provide a mapping from the 'segment' task to its corresponding 'Predictor'.
|
||||
|
||||
Returns:
|
||||
(dict[str, dict[str, type[Predictor]]]): A dictionary mapping the 'segment' task to its corresponding
|
||||
Predictor class. For SAM2 models, it maps to SAM2Predictor, otherwise to the standard Predictor.
|
||||
|
||||
Examples:
|
||||
>>> sam = SAM("sam_b.pt")
|
||||
>>> task_map = sam.task_map
|
||||
>>> print(task_map)
|
||||
{'segment': {'predictor': <class 'ultralytics.models.sam.predict.Predictor'>}}
|
||||
"""
|
||||
return {
|
||||
"segment": {"predictor": SAM2Predictor if self.is_sam2 else SAM3Predictor if self.is_sam3 else Predictor}
|
||||
}
|
||||
1
ultralytics/models/sam/modules/__init__.py
Executable file
1
ultralytics/models/sam/modules/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
1066
ultralytics/models/sam/modules/blocks.py
Executable file
1066
ultralytics/models/sam/modules/blocks.py
Executable file
File diff suppressed because it is too large
Load Diff
495
ultralytics/models/sam/modules/decoders.py
Executable file
495
ultralytics/models/sam/modules/decoders.py
Executable file
@@ -0,0 +1,495 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from ultralytics.nn.modules import MLP, LayerNorm2d
|
||||
|
||||
|
||||
class MaskDecoder(nn.Module):
|
||||
"""Decoder module for generating masks and their associated quality scores using a transformer architecture.
|
||||
|
||||
This class predicts masks given image and prompt embeddings, utilizing a transformer to process the inputs and
|
||||
generate mask predictions along with their quality scores.
|
||||
|
||||
Attributes:
|
||||
transformer_dim (int): Channel dimension for the transformer module.
|
||||
transformer (nn.Module): Transformer module used for mask prediction.
|
||||
num_multimask_outputs (int): Number of masks to predict for disambiguating masks.
|
||||
iou_token (nn.Embedding): Embedding for the IoU token.
|
||||
num_mask_tokens (int): Number of mask tokens.
|
||||
mask_tokens (nn.Embedding): Embedding for the mask tokens.
|
||||
output_upscaling (nn.Sequential): Neural network sequence for upscaling the output.
|
||||
output_hypernetworks_mlps (nn.ModuleList): Hypernetwork MLPs for generating masks.
|
||||
iou_prediction_head (nn.Module): MLP for predicting mask quality.
|
||||
|
||||
Methods:
|
||||
forward: Predict masks given image and prompt embeddings.
|
||||
predict_masks: Internal method for mask prediction.
|
||||
|
||||
Examples:
|
||||
>>> decoder = MaskDecoder(transformer_dim=256, transformer=transformer_module)
|
||||
>>> masks, iou_pred = decoder(
|
||||
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, multimask_output=True
|
||||
... )
|
||||
>>> print(f"Predicted masks shape: {masks.shape}, IoU predictions shape: {iou_pred.shape}")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
transformer_dim: int,
|
||||
transformer: nn.Module,
|
||||
num_multimask_outputs: int = 3,
|
||||
activation: type[nn.Module] = nn.GELU,
|
||||
iou_head_depth: int = 3,
|
||||
iou_head_hidden_dim: int = 256,
|
||||
) -> None:
|
||||
"""Initialize the MaskDecoder module for generating masks and their associated quality scores.
|
||||
|
||||
Args:
|
||||
transformer_dim (int): Channel dimension for the transformer module.
|
||||
transformer (nn.Module): Transformer module used for mask prediction.
|
||||
num_multimask_outputs (int): Number of masks to predict for disambiguating masks.
|
||||
activation (type[nn.Module]): Type of activation to use when upscaling masks.
|
||||
iou_head_depth (int): Depth of the MLP used to predict mask quality.
|
||||
iou_head_hidden_dim (int): Hidden dimension of the MLP used to predict mask quality.
|
||||
"""
|
||||
super().__init__()
|
||||
self.transformer_dim = transformer_dim
|
||||
self.transformer = transformer
|
||||
|
||||
self.num_multimask_outputs = num_multimask_outputs
|
||||
|
||||
self.iou_token = nn.Embedding(1, transformer_dim)
|
||||
self.num_mask_tokens = num_multimask_outputs + 1
|
||||
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
|
||||
|
||||
self.output_upscaling = nn.Sequential(
|
||||
nn.ConvTranspose2d(transformer_dim, transformer_dim // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(transformer_dim // 4),
|
||||
activation(),
|
||||
nn.ConvTranspose2d(transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2),
|
||||
activation(),
|
||||
)
|
||||
self.output_hypernetworks_mlps = nn.ModuleList(
|
||||
[MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for _ in range(self.num_mask_tokens)]
|
||||
)
|
||||
|
||||
self.iou_prediction_head = MLP(transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
multimask_output: bool,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Predict masks given image and prompt embeddings.
|
||||
|
||||
Args:
|
||||
image_embeddings (torch.Tensor): Embeddings from the image encoder.
|
||||
image_pe (torch.Tensor): Positional encoding with the shape of image_embeddings.
|
||||
sparse_prompt_embeddings (torch.Tensor): Embeddings of the points and boxes.
|
||||
dense_prompt_embeddings (torch.Tensor): Embeddings of the mask inputs.
|
||||
multimask_output (bool): Whether to return multiple masks or a single mask.
|
||||
|
||||
Returns:
|
||||
masks (torch.Tensor): Batched predicted masks.
|
||||
iou_pred (torch.Tensor): Batched predictions of mask quality.
|
||||
|
||||
Examples:
|
||||
>>> decoder = MaskDecoder(transformer_dim=256, transformer=transformer_module)
|
||||
>>> image_emb = torch.rand(1, 256, 64, 64)
|
||||
>>> image_pe = torch.rand(1, 256, 64, 64)
|
||||
>>> sparse_emb = torch.rand(1, 2, 256)
|
||||
>>> dense_emb = torch.rand(1, 256, 64, 64)
|
||||
>>> masks, iou_pred = decoder(image_emb, image_pe, sparse_emb, dense_emb, multimask_output=True)
|
||||
>>> print(f"Masks shape: {masks.shape}, IoU predictions shape: {iou_pred.shape}")
|
||||
"""
|
||||
masks, iou_pred = self.predict_masks(
|
||||
image_embeddings=image_embeddings,
|
||||
image_pe=image_pe,
|
||||
sparse_prompt_embeddings=sparse_prompt_embeddings,
|
||||
dense_prompt_embeddings=dense_prompt_embeddings,
|
||||
)
|
||||
|
||||
# Select the correct mask or masks for output
|
||||
mask_slice = slice(1, None) if multimask_output else slice(0, 1)
|
||||
masks = masks[:, mask_slice, :, :]
|
||||
iou_pred = iou_pred[:, mask_slice]
|
||||
|
||||
return masks, iou_pred
|
||||
|
||||
def predict_masks(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Predict masks and quality scores using image and prompt embeddings via transformer architecture."""
|
||||
# Concatenate output tokens
|
||||
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
|
||||
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.shape[0], -1, -1)
|
||||
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
|
||||
|
||||
# Expand per-image data in batch direction to be per-mask
|
||||
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
|
||||
src = src + dense_prompt_embeddings
|
||||
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
|
||||
b, c, h, w = src.shape
|
||||
|
||||
# Run the transformer
|
||||
hs, src = self.transformer(src, pos_src, tokens)
|
||||
iou_token_out = hs[:, 0, :]
|
||||
mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :]
|
||||
|
||||
# Upscale mask embeddings and predict masks using the mask tokens
|
||||
src = src.transpose(1, 2).view(b, c, h, w)
|
||||
upscaled_embedding = self.output_upscaling(src)
|
||||
hyper_in_list: list[torch.Tensor] = [
|
||||
self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) for i in range(self.num_mask_tokens)
|
||||
]
|
||||
hyper_in = torch.stack(hyper_in_list, dim=1)
|
||||
b, c, h, w = upscaled_embedding.shape
|
||||
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
|
||||
|
||||
# Generate mask quality predictions
|
||||
iou_pred = self.iou_prediction_head(iou_token_out)
|
||||
|
||||
return masks, iou_pred
|
||||
|
||||
|
||||
class SAM2MaskDecoder(nn.Module):
|
||||
"""Transformer-based decoder for predicting instance segmentation masks from image and prompt embeddings.
|
||||
|
||||
This class extends the functionality of the MaskDecoder, incorporating additional features such as high-resolution
|
||||
feature processing, dynamic multimask output, and object score prediction.
|
||||
|
||||
Attributes:
|
||||
transformer_dim (int): Channel dimension of the transformer.
|
||||
transformer (nn.Module): Transformer used to predict masks.
|
||||
num_multimask_outputs (int): Number of masks to predict when disambiguating masks.
|
||||
iou_token (nn.Embedding): Embedding for IOU token.
|
||||
num_mask_tokens (int): Total number of mask tokens.
|
||||
mask_tokens (nn.Embedding): Embedding for mask tokens.
|
||||
pred_obj_scores (bool): Whether to predict object scores.
|
||||
obj_score_token (nn.Embedding): Embedding for object score token.
|
||||
use_multimask_token_for_obj_ptr (bool): Whether to use multimask token for object pointer.
|
||||
output_upscaling (nn.Sequential): Upscaling layers for output.
|
||||
use_high_res_features (bool): Whether to use high-resolution features.
|
||||
conv_s0 (nn.Conv2d): Convolutional layer for high-resolution features (s0).
|
||||
conv_s1 (nn.Conv2d): Convolutional layer for high-resolution features (s1).
|
||||
output_hypernetworks_mlps (nn.ModuleList): List of MLPs for output hypernetworks.
|
||||
iou_prediction_head (MLP): MLP for IOU prediction.
|
||||
pred_obj_score_head (nn.Linear | MLP): Linear layer or MLP for object score prediction.
|
||||
dynamic_multimask_via_stability (bool): Whether to use dynamic multimask via stability.
|
||||
dynamic_multimask_stability_delta (float): Delta value for dynamic multimask stability.
|
||||
dynamic_multimask_stability_thresh (float): Threshold for dynamic multimask stability.
|
||||
|
||||
Methods:
|
||||
forward: Predict masks given image and prompt embeddings.
|
||||
predict_masks: Predict instance segmentation masks from image and prompt embeddings.
|
||||
_get_stability_scores: Compute mask stability scores based on IoU between thresholds.
|
||||
_dynamic_multimask_via_stability: Dynamically select the most stable mask output.
|
||||
|
||||
Examples:
|
||||
>>> image_embeddings = torch.rand(1, 256, 64, 64)
|
||||
>>> image_pe = torch.rand(1, 256, 64, 64)
|
||||
>>> sparse_prompt_embeddings = torch.rand(1, 2, 256)
|
||||
>>> dense_prompt_embeddings = torch.rand(1, 256, 64, 64)
|
||||
>>> decoder = SAM2MaskDecoder(256, transformer)
|
||||
>>> masks, iou_pred, sam_tokens_out, obj_score_logits = decoder.forward(
|
||||
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, True, False
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
transformer_dim: int,
|
||||
transformer: nn.Module,
|
||||
num_multimask_outputs: int = 3,
|
||||
activation: type[nn.Module] = nn.GELU,
|
||||
iou_head_depth: int = 3,
|
||||
iou_head_hidden_dim: int = 256,
|
||||
use_high_res_features: bool = False,
|
||||
iou_prediction_use_sigmoid=False,
|
||||
dynamic_multimask_via_stability=False,
|
||||
dynamic_multimask_stability_delta=0.05,
|
||||
dynamic_multimask_stability_thresh=0.98,
|
||||
pred_obj_scores: bool = False,
|
||||
pred_obj_scores_mlp: bool = False,
|
||||
use_multimask_token_for_obj_ptr: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the SAM2MaskDecoder module for predicting instance segmentation masks.
|
||||
|
||||
This decoder extends the functionality of MaskDecoder, incorporating additional features such as high-resolution
|
||||
feature processing, dynamic multimask output, and object score prediction.
|
||||
|
||||
Args:
|
||||
transformer_dim (int): Channel dimension of the transformer.
|
||||
transformer (nn.Module): Transformer used to predict masks.
|
||||
num_multimask_outputs (int): Number of masks to predict when disambiguating masks.
|
||||
activation (type[nn.Module]): Type of activation to use when upscaling masks.
|
||||
iou_head_depth (int): Depth of the MLP used to predict mask quality.
|
||||
iou_head_hidden_dim (int): Hidden dimension of the MLP used to predict mask quality.
|
||||
use_high_res_features (bool): Whether to use high-resolution features.
|
||||
iou_prediction_use_sigmoid (bool): Whether to use sigmoid for IOU prediction.
|
||||
dynamic_multimask_via_stability (bool): Whether to use dynamic multimask via stability.
|
||||
dynamic_multimask_stability_delta (float): Delta value for dynamic multimask stability.
|
||||
dynamic_multimask_stability_thresh (float): Threshold for dynamic multimask stability.
|
||||
pred_obj_scores (bool): Whether to predict object scores.
|
||||
pred_obj_scores_mlp (bool): Whether to use MLP for object score prediction.
|
||||
use_multimask_token_for_obj_ptr (bool): Whether to use multimask token for object pointer.
|
||||
"""
|
||||
super().__init__()
|
||||
self.transformer_dim = transformer_dim
|
||||
self.transformer = transformer
|
||||
|
||||
self.num_multimask_outputs = num_multimask_outputs
|
||||
|
||||
self.iou_token = nn.Embedding(1, transformer_dim)
|
||||
self.num_mask_tokens = num_multimask_outputs + 1
|
||||
self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim)
|
||||
|
||||
self.pred_obj_scores = pred_obj_scores
|
||||
if self.pred_obj_scores:
|
||||
self.obj_score_token = nn.Embedding(1, transformer_dim)
|
||||
self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr
|
||||
|
||||
self.output_upscaling = nn.Sequential(
|
||||
nn.ConvTranspose2d(transformer_dim, transformer_dim // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(transformer_dim // 4),
|
||||
activation(),
|
||||
nn.ConvTranspose2d(transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2),
|
||||
activation(),
|
||||
)
|
||||
self.use_high_res_features = use_high_res_features
|
||||
if use_high_res_features:
|
||||
self.conv_s0 = nn.Conv2d(transformer_dim, transformer_dim // 8, kernel_size=1, stride=1)
|
||||
self.conv_s1 = nn.Conv2d(transformer_dim, transformer_dim // 4, kernel_size=1, stride=1)
|
||||
|
||||
self.output_hypernetworks_mlps = nn.ModuleList(
|
||||
[MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) for _ in range(self.num_mask_tokens)]
|
||||
)
|
||||
|
||||
self.iou_prediction_head = MLP(
|
||||
transformer_dim,
|
||||
iou_head_hidden_dim,
|
||||
self.num_mask_tokens,
|
||||
iou_head_depth,
|
||||
sigmoid=iou_prediction_use_sigmoid,
|
||||
)
|
||||
if self.pred_obj_scores:
|
||||
self.pred_obj_score_head = nn.Linear(transformer_dim, 1)
|
||||
if pred_obj_scores_mlp:
|
||||
self.pred_obj_score_head = MLP(transformer_dim, transformer_dim, 1, 3)
|
||||
|
||||
# When outputting a single mask, optionally we can dynamically fall back to the best
|
||||
# multimask output token if the single mask output token gives low stability scores.
|
||||
self.dynamic_multimask_via_stability = dynamic_multimask_via_stability
|
||||
self.dynamic_multimask_stability_delta = dynamic_multimask_stability_delta
|
||||
self.dynamic_multimask_stability_thresh = dynamic_multimask_stability_thresh
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
multimask_output: bool,
|
||||
repeat_image: bool,
|
||||
high_res_features: list[torch.Tensor] | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Predict masks given image and prompt embeddings.
|
||||
|
||||
Args:
|
||||
image_embeddings (torch.Tensor): Embeddings from the image encoder with shape (B, C, H, W).
|
||||
image_pe (torch.Tensor): Positional encoding with the shape of image_embeddings (B, C, H, W).
|
||||
sparse_prompt_embeddings (torch.Tensor): Embeddings of the points and boxes with shape (B, N, C).
|
||||
dense_prompt_embeddings (torch.Tensor): Embeddings of the mask inputs with shape (B, C, H, W).
|
||||
multimask_output (bool): Whether to return multiple masks or a single mask.
|
||||
repeat_image (bool): Flag to repeat the image embeddings.
|
||||
high_res_features (list[torch.Tensor] | None, optional): Optional high-resolution features.
|
||||
|
||||
Returns:
|
||||
masks (torch.Tensor): Batched predicted masks with shape (B, N, H, W).
|
||||
iou_pred (torch.Tensor): Batched predictions of mask quality with shape (B, N).
|
||||
sam_tokens_out (torch.Tensor): Batched SAM token for mask output with shape (B, N, C).
|
||||
object_score_logits (torch.Tensor): Batched object score logits with shape (B, 1).
|
||||
|
||||
Examples:
|
||||
>>> image_embeddings = torch.rand(1, 256, 64, 64)
|
||||
>>> image_pe = torch.rand(1, 256, 64, 64)
|
||||
>>> sparse_prompt_embeddings = torch.rand(1, 2, 256)
|
||||
>>> dense_prompt_embeddings = torch.rand(1, 256, 64, 64)
|
||||
>>> decoder = SAM2MaskDecoder(256, transformer)
|
||||
>>> masks, iou_pred, sam_tokens_out, obj_score_logits = decoder.forward(
|
||||
... image_embeddings, image_pe, sparse_prompt_embeddings, dense_prompt_embeddings, True, False
|
||||
... )
|
||||
"""
|
||||
masks, iou_pred, mask_tokens_out, object_score_logits = self.predict_masks(
|
||||
image_embeddings=image_embeddings,
|
||||
image_pe=image_pe,
|
||||
sparse_prompt_embeddings=sparse_prompt_embeddings,
|
||||
dense_prompt_embeddings=dense_prompt_embeddings,
|
||||
repeat_image=repeat_image,
|
||||
high_res_features=high_res_features,
|
||||
)
|
||||
|
||||
# Select the correct mask or masks for output
|
||||
if multimask_output:
|
||||
masks = masks[:, 1:, :, :]
|
||||
iou_pred = iou_pred[:, 1:]
|
||||
elif self.dynamic_multimask_via_stability and not self.training:
|
||||
masks, iou_pred = self._dynamic_multimask_via_stability(masks, iou_pred)
|
||||
else:
|
||||
masks = masks[:, 0:1, :, :]
|
||||
iou_pred = iou_pred[:, 0:1]
|
||||
|
||||
if multimask_output and self.use_multimask_token_for_obj_ptr:
|
||||
sam_tokens_out = mask_tokens_out[:, 1:] # [b, 3, c] shape
|
||||
else:
|
||||
# Take the mask output token. Here we *always* use the token for single mask output.
|
||||
# At test time, even if we track after 1-click (and using multimask_output=True),
|
||||
# we still take the single mask token here. The rationale is that we always track
|
||||
# after multiple clicks during training, so the past tokens seen during training
|
||||
# are always the single mask token (and we'll let it be the object-memory token).
|
||||
sam_tokens_out = mask_tokens_out[:, 0:1] # [b, 1, c] shape
|
||||
|
||||
return masks, iou_pred, sam_tokens_out, object_score_logits
|
||||
|
||||
def predict_masks(
|
||||
self,
|
||||
image_embeddings: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
sparse_prompt_embeddings: torch.Tensor,
|
||||
dense_prompt_embeddings: torch.Tensor,
|
||||
repeat_image: bool,
|
||||
high_res_features: list[torch.Tensor] | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Predict instance segmentation masks from image and prompt embeddings using a transformer."""
|
||||
# Concatenate output tokens
|
||||
s = 0
|
||||
if self.pred_obj_scores:
|
||||
output_tokens = torch.cat(
|
||||
[
|
||||
self.obj_score_token.weight,
|
||||
self.iou_token.weight,
|
||||
self.mask_tokens.weight,
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
s = 1
|
||||
else:
|
||||
output_tokens = torch.cat([self.iou_token.weight, self.mask_tokens.weight], dim=0)
|
||||
output_tokens = output_tokens.unsqueeze(0).expand(sparse_prompt_embeddings.shape[0], -1, -1)
|
||||
tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1)
|
||||
|
||||
# Expand per-image data in batch direction to be per-mask
|
||||
if repeat_image:
|
||||
src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0)
|
||||
else:
|
||||
assert image_embeddings.shape[0] == tokens.shape[0]
|
||||
src = image_embeddings
|
||||
src = src + dense_prompt_embeddings
|
||||
assert image_pe.shape[0] == 1, "image_pe should have size 1 in batch dim (from `get_dense_pe()`)"
|
||||
pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0)
|
||||
b, c, h, w = src.shape
|
||||
|
||||
# Run the transformer
|
||||
hs, src = self.transformer(src, pos_src, tokens)
|
||||
iou_token_out = hs[:, s, :]
|
||||
mask_tokens_out = hs[:, s + 1 : (s + 1 + self.num_mask_tokens), :]
|
||||
|
||||
# Upscale mask embeddings and predict masks using the mask tokens
|
||||
src = src.transpose(1, 2).view(b, c, h, w)
|
||||
if not self.use_high_res_features or high_res_features is None:
|
||||
upscaled_embedding = self.output_upscaling(src)
|
||||
else:
|
||||
dc1, ln1, act1, dc2, act2 = self.output_upscaling
|
||||
feat_s0, feat_s1 = high_res_features
|
||||
upscaled_embedding = act1(ln1(dc1(src) + feat_s1))
|
||||
upscaled_embedding = act2(dc2(upscaled_embedding) + feat_s0)
|
||||
|
||||
hyper_in_list: list[torch.Tensor] = [
|
||||
self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) for i in range(self.num_mask_tokens)
|
||||
]
|
||||
hyper_in = torch.stack(hyper_in_list, dim=1)
|
||||
b, c, h, w = upscaled_embedding.shape
|
||||
masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w)
|
||||
|
||||
# Generate mask quality predictions
|
||||
iou_pred = self.iou_prediction_head(iou_token_out)
|
||||
if self.pred_obj_scores:
|
||||
assert s == 1
|
||||
object_score_logits = self.pred_obj_score_head(hs[:, 0, :])
|
||||
else:
|
||||
# Obj scores logits - default to 10.0, i.e. assuming the object is present, sigmoid(10)=1
|
||||
object_score_logits = 10.0 * iou_pred.new_ones(iou_pred.shape[0], 1)
|
||||
|
||||
return masks, iou_pred, mask_tokens_out, object_score_logits
|
||||
|
||||
def _get_stability_scores(self, mask_logits):
|
||||
"""Compute mask stability scores based on IoU between upper and lower thresholds."""
|
||||
mask_logits = mask_logits.flatten(-2)
|
||||
area_i = torch.sum(mask_logits > self.dynamic_multimask_stability_delta, dim=-1).float()
|
||||
area_u = torch.sum(mask_logits > -self.dynamic_multimask_stability_delta, dim=-1).float()
|
||||
return torch.where(area_u > 0, area_i / area_u, 1.0)
|
||||
|
||||
def _dynamic_multimask_via_stability(self, all_mask_logits, all_iou_scores):
|
||||
"""Dynamically select the most stable mask output based on stability scores and IoU predictions.
|
||||
|
||||
This method is used when outputting a single mask. If the stability score from the current single-mask output
|
||||
(based on output token 0) falls below a threshold, it instead selects from multi-mask outputs (based on output
|
||||
tokens 1-3) the mask with the highest predicted IoU score. This ensures a valid mask for both clicking and
|
||||
tracking scenarios.
|
||||
|
||||
Args:
|
||||
all_mask_logits (torch.Tensor): Logits for all predicted masks, shape (B, N, H, W) where B is batch size, N
|
||||
is number of masks (typically 4), and H, W are mask dimensions.
|
||||
all_iou_scores (torch.Tensor): Predicted IoU scores for all masks, shape (B, N).
|
||||
|
||||
Returns:
|
||||
mask_logits_out (torch.Tensor): Selected mask logits, shape (B, 1, H, W).
|
||||
iou_scores_out (torch.Tensor): Selected IoU scores, shape (B, 1).
|
||||
|
||||
Examples:
|
||||
>>> decoder = SAM2MaskDecoder(...)
|
||||
>>> all_mask_logits = torch.rand(2, 4, 256, 256) # 2 images, 4 masks each
|
||||
>>> all_iou_scores = torch.rand(2, 4)
|
||||
>>> mask_logits, iou_scores = decoder._dynamic_multimask_via_stability(all_mask_logits, all_iou_scores)
|
||||
>>> print(mask_logits.shape, iou_scores.shape)
|
||||
torch.Size([2, 1, 256, 256]) torch.Size([2, 1])
|
||||
"""
|
||||
# The best mask from multimask output tokens (1~3)
|
||||
multimask_logits = all_mask_logits[:, 1:, :, :]
|
||||
multimask_iou_scores = all_iou_scores[:, 1:]
|
||||
best_scores_inds = torch.argmax(multimask_iou_scores, dim=-1)
|
||||
batch_inds = torch.arange(multimask_iou_scores.shape[0], device=all_iou_scores.device)
|
||||
best_multimask_logits = multimask_logits[batch_inds, best_scores_inds]
|
||||
best_multimask_logits = best_multimask_logits.unsqueeze(1)
|
||||
best_multimask_iou_scores = multimask_iou_scores[batch_inds, best_scores_inds]
|
||||
best_multimask_iou_scores = best_multimask_iou_scores.unsqueeze(1)
|
||||
|
||||
# The mask from singlemask output token 0 and its stability score
|
||||
singlemask_logits = all_mask_logits[:, 0:1, :, :]
|
||||
singlemask_iou_scores = all_iou_scores[:, 0:1]
|
||||
stability_scores = self._get_stability_scores(singlemask_logits)
|
||||
is_stable = stability_scores >= self.dynamic_multimask_stability_thresh
|
||||
|
||||
# Dynamically fall back to best multimask output upon low stability scores.
|
||||
mask_logits_out = torch.where(
|
||||
is_stable[..., None, None].expand_as(singlemask_logits),
|
||||
singlemask_logits,
|
||||
best_multimask_logits,
|
||||
)
|
||||
iou_scores_out = torch.where(
|
||||
is_stable.expand_as(singlemask_iou_scores),
|
||||
singlemask_iou_scores,
|
||||
best_multimask_iou_scores,
|
||||
)
|
||||
return mask_logits_out, iou_scores_out
|
||||
794
ultralytics/models/sam/modules/encoders.py
Executable file
794
ultralytics/models/sam/modules/encoders.py
Executable file
@@ -0,0 +1,794 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ultralytics.nn.modules import LayerNorm2d
|
||||
|
||||
from .blocks import (
|
||||
Block,
|
||||
CXBlock,
|
||||
Fuser,
|
||||
MaskDownSampler,
|
||||
MultiScaleBlock,
|
||||
PatchEmbed,
|
||||
PositionEmbeddingRandom,
|
||||
PositionEmbeddingSine,
|
||||
)
|
||||
|
||||
|
||||
class ImageEncoderViT(nn.Module):
|
||||
"""An image encoder using Vision Transformer (ViT) architecture for encoding images into a compact latent space.
|
||||
|
||||
This class processes images by splitting them into patches, applying transformer blocks, and generating a final
|
||||
encoded representation through a neck module.
|
||||
|
||||
Attributes:
|
||||
img_size (int): Dimension of input images, assumed to be square.
|
||||
patch_embed (PatchEmbed): Module for patch embedding.
|
||||
pos_embed (nn.Parameter | None): Absolute positional embedding for patches.
|
||||
blocks (nn.ModuleList): List of transformer blocks for processing patch embeddings.
|
||||
neck (nn.Sequential): Neck module to further process the output.
|
||||
|
||||
Methods:
|
||||
forward: Process input through patch embedding, positional embedding, blocks, and neck.
|
||||
|
||||
Examples:
|
||||
>>> import torch
|
||||
>>> encoder = ImageEncoderViT(img_size=224, patch_size=16, embed_dim=768, depth=12, num_heads=12)
|
||||
>>> input_image = torch.randn(1, 3, 224, 224)
|
||||
>>> output = encoder(input_image)
|
||||
>>> print(output.shape)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
img_size: int = 1024,
|
||||
patch_size: int = 16,
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
depth: int = 12,
|
||||
num_heads: int = 12,
|
||||
mlp_ratio: float = 4.0,
|
||||
out_chans: int = 256,
|
||||
qkv_bias: bool = True,
|
||||
norm_layer: type[nn.Module] = nn.LayerNorm,
|
||||
act_layer: type[nn.Module] = nn.GELU,
|
||||
use_abs_pos: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
global_attn_indexes: tuple[int, ...] = (),
|
||||
) -> None:
|
||||
"""Initialize an ImageEncoderViT instance for encoding images using Vision Transformer architecture.
|
||||
|
||||
Args:
|
||||
img_size (int): Input image size, assumed to be square.
|
||||
patch_size (int): Size of image patches.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): Dimension of patch embeddings.
|
||||
depth (int): Number of transformer blocks.
|
||||
num_heads (int): Number of attention heads in each block.
|
||||
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
|
||||
out_chans (int): Number of output channels from the neck module.
|
||||
qkv_bias (bool): If True, adds learnable bias to query, key, value projections.
|
||||
norm_layer (type[nn.Module]): Type of normalization layer to use.
|
||||
act_layer (type[nn.Module]): Type of activation layer to use.
|
||||
use_abs_pos (bool): If True, uses absolute positional embeddings.
|
||||
use_rel_pos (bool): If True, adds relative positional embeddings to attention maps.
|
||||
rel_pos_zero_init (bool): If True, initializes relative positional parameters to zero.
|
||||
window_size (int): Size of attention window for windowed attention blocks.
|
||||
global_attn_indexes (tuple[int, ...]): Indices of blocks that use global attention.
|
||||
"""
|
||||
super().__init__()
|
||||
self.img_size = img_size
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
kernel_size=(patch_size, patch_size),
|
||||
stride=(patch_size, patch_size),
|
||||
in_chans=in_chans,
|
||||
embed_dim=embed_dim,
|
||||
)
|
||||
|
||||
self.pos_embed: nn.Parameter | None = None
|
||||
if use_abs_pos:
|
||||
# Initialize absolute positional embedding with pretrain image size
|
||||
self.pos_embed = nn.Parameter(torch.zeros(1, img_size // patch_size, img_size // patch_size, embed_dim))
|
||||
|
||||
self.blocks = nn.ModuleList()
|
||||
for i in range(depth):
|
||||
block = Block(
|
||||
dim=embed_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_ratio=mlp_ratio,
|
||||
qkv_bias=qkv_bias,
|
||||
norm_layer=norm_layer,
|
||||
act_layer=act_layer,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
window_size=window_size if i not in global_attn_indexes else 0,
|
||||
input_size=(img_size // patch_size, img_size // patch_size),
|
||||
)
|
||||
self.blocks.append(block)
|
||||
|
||||
self.neck = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
embed_dim,
|
||||
out_chans,
|
||||
kernel_size=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
nn.Conv2d(
|
||||
out_chans,
|
||||
out_chans,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(out_chans),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Process input through patch embedding, positional embedding, transformer blocks, and neck module."""
|
||||
x = self.patch_embed(x)
|
||||
if self.pos_embed is not None:
|
||||
pos_embed = (
|
||||
F.interpolate(self.pos_embed.permute(0, 3, 1, 2), scale_factor=self.img_size / 1024).permute(0, 2, 3, 1)
|
||||
if self.img_size != 1024
|
||||
else self.pos_embed
|
||||
)
|
||||
x = x + pos_embed
|
||||
for blk in self.blocks:
|
||||
x = blk(x)
|
||||
return self.neck(x.permute(0, 3, 1, 2))
|
||||
|
||||
|
||||
class PromptEncoder(nn.Module):
|
||||
"""Encode different types of prompts for input to SAM's mask decoder, producing sparse and dense embeddings.
|
||||
|
||||
Attributes:
|
||||
embed_dim (int): Dimension of the embeddings.
|
||||
input_image_size (tuple[int, int]): Size of the input image as (H, W).
|
||||
image_embedding_size (tuple[int, int]): Spatial size of the image embedding as (H, W).
|
||||
pe_layer (PositionEmbeddingRandom): Module for random position embedding.
|
||||
num_point_embeddings (int): Number of point embeddings for different types of points.
|
||||
point_embeddings (nn.ModuleList): List of point embeddings.
|
||||
not_a_point_embed (nn.Embedding): Embedding for points that are not part of any label.
|
||||
mask_input_size (tuple[int, int]): Size of the input mask.
|
||||
mask_downscaling (nn.Sequential): Neural network for downscaling the mask.
|
||||
no_mask_embed (nn.Embedding): Embedding for cases where no mask is provided.
|
||||
|
||||
Methods:
|
||||
get_dense_pe: Return the positional encoding used to encode point prompts.
|
||||
forward: Embed different types of prompts, returning both sparse and dense embeddings.
|
||||
|
||||
Examples:
|
||||
>>> prompt_encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
|
||||
>>> points = (torch.rand(1, 5, 2), torch.randint(0, 4, (1, 5)))
|
||||
>>> boxes = torch.rand(1, 2, 2)
|
||||
>>> masks = torch.rand(1, 1, 256, 256)
|
||||
>>> sparse_embeddings, dense_embeddings = prompt_encoder(points, boxes, masks)
|
||||
>>> print(sparse_embeddings.shape, dense_embeddings.shape)
|
||||
torch.Size([1, 7, 256]) torch.Size([1, 256, 64, 64])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim: int,
|
||||
image_embedding_size: tuple[int, int],
|
||||
input_image_size: tuple[int, int],
|
||||
mask_in_chans: int,
|
||||
activation: type[nn.Module] = nn.GELU,
|
||||
) -> None:
|
||||
"""Initialize the PromptEncoder module for encoding various types of prompts.
|
||||
|
||||
Args:
|
||||
embed_dim (int): The dimension of the embeddings.
|
||||
image_embedding_size (tuple[int, int]): The spatial size of the image embedding as (H, W).
|
||||
input_image_size (tuple[int, int]): The padded size of the input image as (H, W).
|
||||
mask_in_chans (int): The number of hidden channels used for encoding input masks.
|
||||
activation (type[nn.Module]): The activation function to use when encoding input masks.
|
||||
"""
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.input_image_size = input_image_size
|
||||
self.image_embedding_size = image_embedding_size
|
||||
self.pe_layer = PositionEmbeddingRandom(embed_dim // 2)
|
||||
|
||||
self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners
|
||||
point_embeddings = [nn.Embedding(1, embed_dim) for _ in range(self.num_point_embeddings)]
|
||||
self.point_embeddings = nn.ModuleList(point_embeddings)
|
||||
self.not_a_point_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
self.mask_input_size = (4 * image_embedding_size[0], 4 * image_embedding_size[1])
|
||||
self.mask_downscaling = nn.Sequential(
|
||||
nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans // 4),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2),
|
||||
LayerNorm2d(mask_in_chans),
|
||||
activation(),
|
||||
nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1),
|
||||
)
|
||||
self.no_mask_embed = nn.Embedding(1, embed_dim)
|
||||
|
||||
def get_dense_pe(self) -> torch.Tensor:
|
||||
"""Return the dense positional encoding used for encoding point prompts.
|
||||
|
||||
Generate a positional encoding for a dense set of points matching the shape of the image
|
||||
encoding. The encoding is used to provide spatial information to the model when processing point prompts.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Positional encoding tensor with shape (1, embed_dim, H, W), where H and W are the height and
|
||||
width of the image embedding size, respectively.
|
||||
|
||||
Examples:
|
||||
>>> prompt_encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
|
||||
>>> dense_pe = prompt_encoder.get_dense_pe()
|
||||
>>> print(dense_pe.shape)
|
||||
torch.Size([1, 256, 64, 64])
|
||||
"""
|
||||
return self.pe_layer(self.image_embedding_size).unsqueeze(0)
|
||||
|
||||
def _embed_points(self, points: torch.Tensor, labels: torch.Tensor, pad: bool) -> torch.Tensor:
|
||||
"""Embed point prompts by applying positional encoding and label-specific embeddings."""
|
||||
points = points + 0.5 # Shift to center of pixel
|
||||
if pad:
|
||||
padding_point = torch.zeros((points.shape[0], 1, 2), dtype=points.dtype, device=points.device)
|
||||
padding_label = -torch.ones((labels.shape[0], 1), dtype=labels.dtype, device=labels.device)
|
||||
points = torch.cat([points, padding_point], dim=1)
|
||||
labels = torch.cat([labels, padding_label], dim=1)
|
||||
point_embedding = self.pe_layer.forward_with_coords(points, self.input_image_size)
|
||||
point_embedding[labels == -1] = 0.0
|
||||
point_embedding[labels == -1] += self.not_a_point_embed.weight
|
||||
point_embedding[labels == 0] += self.point_embeddings[0].weight
|
||||
point_embedding[labels == 1] += self.point_embeddings[1].weight
|
||||
point_embedding[labels == 2] += self.point_embeddings[2].weight
|
||||
point_embedding[labels == 3] += self.point_embeddings[3].weight
|
||||
return point_embedding
|
||||
|
||||
def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor:
|
||||
"""Embed box prompts by applying positional encoding and adding corner embeddings."""
|
||||
boxes = boxes + 0.5 # Shift to center of pixel
|
||||
coords = boxes.reshape(-1, 2, 2)
|
||||
corner_embedding = self.pe_layer.forward_with_coords(coords, self.input_image_size)
|
||||
corner_embedding[:, 0, :] += self.point_embeddings[2].weight
|
||||
corner_embedding[:, 1, :] += self.point_embeddings[3].weight
|
||||
return corner_embedding
|
||||
|
||||
def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor:
|
||||
"""Embed mask inputs by downscaling and processing through convolutional layers."""
|
||||
return self.mask_downscaling(masks)
|
||||
|
||||
@staticmethod
|
||||
def _get_batch_size(
|
||||
points: tuple[torch.Tensor, torch.Tensor] | None,
|
||||
boxes: torch.Tensor | None,
|
||||
masks: torch.Tensor | None,
|
||||
) -> int:
|
||||
"""Get the batch size of the output given the batch size of the input prompts."""
|
||||
if points is not None:
|
||||
return points[0].shape[0]
|
||||
elif boxes is not None:
|
||||
return boxes.shape[0]
|
||||
elif masks is not None:
|
||||
return masks.shape[0]
|
||||
else:
|
||||
return 1
|
||||
|
||||
def forward(
|
||||
self,
|
||||
points: tuple[torch.Tensor, torch.Tensor] | None,
|
||||
boxes: torch.Tensor | None,
|
||||
masks: torch.Tensor | None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Embed different types of prompts, returning both sparse and dense embeddings.
|
||||
|
||||
Args:
|
||||
points (tuple[torch.Tensor, torch.Tensor] | None): Point coordinates and labels to embed. The first tensor
|
||||
contains coordinates of shape (B, N, 2), and the second tensor contains labels of shape (B, N).
|
||||
boxes (torch.Tensor | None): Boxes to embed with shape (B, M, 2, 2), where M is the number of boxes.
|
||||
masks (torch.Tensor | None): Masks to embed with shape (B, 1, H, W).
|
||||
|
||||
Returns:
|
||||
sparse_embeddings (torch.Tensor): Sparse embeddings for points and boxes with shape (B, N, embed_dim).
|
||||
dense_embeddings (torch.Tensor): Dense embeddings for masks of shape (B, embed_dim, embed_H, embed_W).
|
||||
|
||||
Examples:
|
||||
>>> encoder = PromptEncoder(256, (64, 64), (1024, 1024), 16)
|
||||
>>> points = (torch.rand(1, 5, 2), torch.randint(0, 4, (1, 5)))
|
||||
>>> boxes = torch.rand(1, 2, 2, 2)
|
||||
>>> masks = torch.rand(1, 1, 256, 256)
|
||||
>>> sparse_emb, dense_emb = encoder(points, boxes, masks)
|
||||
>>> print(sparse_emb.shape, dense_emb.shape)
|
||||
torch.Size([1, 7, 256]) torch.Size([1, 256, 64, 64])
|
||||
"""
|
||||
bs = self._get_batch_size(points, boxes, masks)
|
||||
sparse_embeddings = torch.empty(
|
||||
(bs, 0, self.embed_dim),
|
||||
dtype=self.point_embeddings[0].weight.dtype,
|
||||
device=self.point_embeddings[0].weight.device,
|
||||
)
|
||||
if points is not None:
|
||||
coords, labels = points
|
||||
point_embeddings = self._embed_points(coords, labels, pad=(boxes is None))
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1)
|
||||
if boxes is not None:
|
||||
box_embeddings = self._embed_boxes(boxes)
|
||||
sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1)
|
||||
|
||||
if masks is not None:
|
||||
dense_embeddings = self._embed_masks(masks)
|
||||
else:
|
||||
dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand(
|
||||
bs, -1, self.image_embedding_size[0], self.image_embedding_size[1]
|
||||
)
|
||||
|
||||
return sparse_embeddings, dense_embeddings
|
||||
|
||||
|
||||
class MemoryEncoder(nn.Module):
|
||||
"""Encode pixel features and masks into a memory representation for efficient image segmentation.
|
||||
|
||||
This class processes pixel-level features and masks, fusing them to generate encoded memory representations suitable
|
||||
for downstream tasks in image segmentation models like SAM (Segment Anything Model).
|
||||
|
||||
Attributes:
|
||||
mask_downsampler (MaskDownSampler): Module for downsampling input masks.
|
||||
pix_feat_proj (nn.Conv2d): Convolutional layer for projecting pixel features.
|
||||
fuser (Fuser): Module for fusing pixel features and masks.
|
||||
position_encoding (PositionEmbeddingSine): Module for adding positional encoding to features.
|
||||
out_proj (nn.Module): Output projection layer, either nn.Identity or nn.Conv2d.
|
||||
|
||||
Methods:
|
||||
forward: Process input pixel features and masks to generate encoded memory representations.
|
||||
|
||||
Examples:
|
||||
>>> import torch
|
||||
>>> encoder = MemoryEncoder(out_dim=256, in_dim=256)
|
||||
>>> pix_feat = torch.randn(1, 256, 64, 64)
|
||||
>>> masks = torch.randn(1, 1, 64, 64)
|
||||
>>> encoded_feat, pos = encoder(pix_feat, masks)
|
||||
>>> print(encoded_feat.shape, pos.shape)
|
||||
torch.Size([1, 256, 64, 64]) torch.Size([1, 128, 64, 64])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
out_dim,
|
||||
in_dim=256, # in_dim of pix_feats
|
||||
interpol_size: tuple[int, int] | None = None,
|
||||
):
|
||||
"""Initialize the MemoryEncoder for encoding pixel features and masks into memory representations.
|
||||
|
||||
This encoder processes pixel-level features and masks, fusing them to generate encoded memory representations
|
||||
suitable for downstream tasks in image segmentation models like SAM (Segment Anything Model).
|
||||
|
||||
Args:
|
||||
out_dim (int): Output dimension of the encoded features.
|
||||
in_dim (int): Input dimension of the pixel features.
|
||||
interpol_size (tuple[int, int] | None): Size to interpolate masks to. If None, uses the size of pixel
|
||||
features.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.mask_downsampler = MaskDownSampler(kernel_size=3, stride=2, padding=1, interpol_size=interpol_size)
|
||||
|
||||
self.pix_feat_proj = nn.Conv2d(in_dim, in_dim, kernel_size=1)
|
||||
self.fuser = Fuser(CXBlock(dim=256), num_layers=2)
|
||||
self.position_encoding = PositionEmbeddingSine(num_pos_feats=64)
|
||||
self.out_proj = nn.Identity()
|
||||
if out_dim != in_dim:
|
||||
self.out_proj = nn.Conv2d(in_dim, out_dim, kernel_size=1)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
pix_feat: torch.Tensor,
|
||||
masks: torch.Tensor,
|
||||
skip_mask_sigmoid: bool = False,
|
||||
) -> dict:
|
||||
"""Process pixel features and masks to generate encoded memory representations for segmentation."""
|
||||
if not skip_mask_sigmoid:
|
||||
masks = F.sigmoid(masks)
|
||||
masks = self.mask_downsampler(masks)
|
||||
|
||||
# Fuse pix_feats and downsampled masks, in case the visual features are on CPU, cast them to CUDA
|
||||
pix_feat = pix_feat.to(masks.device)
|
||||
|
||||
x = self.pix_feat_proj(pix_feat)
|
||||
x = x + masks
|
||||
x = self.fuser(x)
|
||||
x = self.out_proj(x)
|
||||
|
||||
pos = self.position_encoding(x).to(x.dtype)
|
||||
|
||||
return {"vision_features": x, "vision_pos_enc": [pos]}
|
||||
|
||||
|
||||
class ImageEncoder(nn.Module):
|
||||
"""Encode images using a trunk-neck architecture, producing multiscale features and positional encodings.
|
||||
|
||||
This class combines a trunk network for feature extraction with a neck network for feature refinement and positional
|
||||
encoding generation. It can optionally discard the lowest resolution features.
|
||||
|
||||
Attributes:
|
||||
trunk (nn.Module): The trunk network for initial feature extraction.
|
||||
neck (nn.Module): The neck network for feature refinement and positional encoding generation.
|
||||
scalp (int): Number of lowest resolution feature levels to discard.
|
||||
|
||||
Methods:
|
||||
forward: Process the input image through the trunk and neck networks.
|
||||
|
||||
Examples:
|
||||
>>> trunk = SomeTrunkNetwork()
|
||||
>>> neck = SomeNeckNetwork()
|
||||
>>> encoder = ImageEncoder(trunk, neck, scalp=1)
|
||||
>>> image = torch.randn(1, 3, 224, 224)
|
||||
>>> output = encoder(image)
|
||||
>>> print(output.keys())
|
||||
dict_keys(['vision_features', 'vision_pos_enc', 'backbone_fpn'])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
trunk: nn.Module,
|
||||
neck: nn.Module,
|
||||
scalp: int = 0,
|
||||
):
|
||||
"""Initialize the ImageEncoder with trunk and neck networks for feature extraction and refinement.
|
||||
|
||||
This encoder combines a trunk network for feature extraction with a neck network for feature refinement and
|
||||
positional encoding generation. It can optionally discard the lowest resolution features.
|
||||
|
||||
Args:
|
||||
trunk (nn.Module): The trunk network for initial feature extraction.
|
||||
neck (nn.Module): The neck network for feature refinement and positional encoding generation.
|
||||
scalp (int): Number of lowest resolution feature levels to discard.
|
||||
"""
|
||||
super().__init__()
|
||||
self.trunk = trunk
|
||||
self.neck = neck
|
||||
self.scalp = scalp
|
||||
assert self.trunk.channel_list == self.neck.backbone_channel_list, (
|
||||
f"Channel dims of trunk {self.trunk.channel_list} and neck {self.neck.backbone_channel_list} do not match."
|
||||
)
|
||||
|
||||
def forward(self, sample: torch.Tensor):
|
||||
"""Encode input through trunk and neck networks, returning multiscale features and positional encodings."""
|
||||
features, pos = self.neck(self.trunk(sample))
|
||||
if self.scalp > 0:
|
||||
# Discard the lowest resolution features
|
||||
features, pos = features[: -self.scalp], pos[: -self.scalp]
|
||||
|
||||
src = features[-1]
|
||||
return {
|
||||
"vision_features": src,
|
||||
"vision_pos_enc": pos,
|
||||
"backbone_fpn": features,
|
||||
}
|
||||
|
||||
|
||||
class FpnNeck(nn.Module):
|
||||
"""A Feature Pyramid Network (FPN) neck variant for multiscale feature fusion in object detection models.
|
||||
|
||||
This FPN variant removes the output convolution and uses bicubic interpolation for feature resizing, similar to ViT
|
||||
positional embedding interpolation.
|
||||
|
||||
Attributes:
|
||||
position_encoding (PositionEmbeddingSine): Sinusoidal positional encoding module.
|
||||
convs (nn.ModuleList): List of convolutional layers for each backbone level.
|
||||
backbone_channel_list (list[int]): List of channel dimensions from the backbone.
|
||||
fpn_interp_model (str): Interpolation mode for FPN feature resizing.
|
||||
fuse_type (str): Type of feature fusion, either 'sum' or 'avg'.
|
||||
fpn_top_down_levels (list[int]): Levels to have top-down features in outputs.
|
||||
|
||||
Methods:
|
||||
forward: Perform forward pass through the FPN neck.
|
||||
|
||||
Examples:
|
||||
>>> backbone_channels = [64, 128, 256, 512]
|
||||
>>> fpn_neck = FpnNeck(256, backbone_channels)
|
||||
>>> inputs = [torch.rand(1, c, 32, 32) for c in backbone_channels]
|
||||
>>> outputs, positions = fpn_neck(inputs)
|
||||
>>> print(len(outputs), len(positions))
|
||||
4 4
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
backbone_channel_list: list[int],
|
||||
kernel_size: int = 1,
|
||||
stride: int = 1,
|
||||
padding: int = 0,
|
||||
fpn_interp_model: str = "bilinear",
|
||||
fuse_type: str = "sum",
|
||||
fpn_top_down_levels: list[int] | None = None,
|
||||
):
|
||||
"""Initialize a modified Feature Pyramid Network (FPN) neck.
|
||||
|
||||
This FPN variant removes the output convolution and uses bicubic interpolation for feature resizing, similar to
|
||||
ViT positional embedding interpolation.
|
||||
|
||||
Args:
|
||||
d_model (int): Dimension of the model.
|
||||
backbone_channel_list (list[int]): List of channel dimensions from the backbone.
|
||||
kernel_size (int): Kernel size for the convolutional layers.
|
||||
stride (int): Stride for the convolutional layers.
|
||||
padding (int): Padding for the convolutional layers.
|
||||
fpn_interp_model (str): Interpolation mode for FPN feature resizing.
|
||||
fuse_type (str): Type of feature fusion, either 'sum' or 'avg'.
|
||||
fpn_top_down_levels (list[int] | None): Levels to have top-down features in outputs.
|
||||
"""
|
||||
super().__init__()
|
||||
self.position_encoding = PositionEmbeddingSine(num_pos_feats=256)
|
||||
self.convs = nn.ModuleList()
|
||||
self.backbone_channel_list = backbone_channel_list
|
||||
for dim in backbone_channel_list:
|
||||
current = nn.Sequential()
|
||||
current.add_module(
|
||||
"conv",
|
||||
nn.Conv2d(
|
||||
in_channels=dim,
|
||||
out_channels=d_model,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
),
|
||||
)
|
||||
|
||||
self.convs.append(current)
|
||||
self.fpn_interp_model = fpn_interp_model
|
||||
assert fuse_type in {"sum", "avg"}
|
||||
self.fuse_type = fuse_type
|
||||
|
||||
# Levels to have top-down features in its outputs
|
||||
# e.g. if fpn_top_down_levels is [2, 3], then only outputs of level 2 and 3
|
||||
# have top-down propagation, while outputs of level 0 and level 1 have only
|
||||
# lateral features from the same backbone level
|
||||
if fpn_top_down_levels is None:
|
||||
# Default is to have top-down features on all levels
|
||||
fpn_top_down_levels = range(len(self.convs))
|
||||
self.fpn_top_down_levels = list(fpn_top_down_levels)
|
||||
|
||||
def forward(self, xs: list[torch.Tensor]):
|
||||
"""Perform forward pass through the Feature Pyramid Network (FPN) neck.
|
||||
|
||||
This method processes a list of input tensors from the backbone through the FPN, applying lateral connections
|
||||
and top-down feature fusion. It generates output feature maps and corresponding positional encodings.
|
||||
|
||||
Args:
|
||||
xs (list[torch.Tensor]): List of input tensors from the backbone, each with shape (B, C, H, W).
|
||||
|
||||
Returns:
|
||||
out (list[torch.Tensor]): List of output feature maps after FPN processing, each with shape (B, d_model, H,
|
||||
W).
|
||||
pos (list[torch.Tensor]): List of positional encodings corresponding to each output feature map.
|
||||
|
||||
Examples:
|
||||
>>> fpn_neck = FpnNeck(d_model=256, backbone_channel_list=[64, 128, 256, 512])
|
||||
>>> inputs = [torch.rand(1, c, 32, 32) for c in [64, 128, 256, 512]]
|
||||
>>> outputs, positions = fpn_neck(inputs)
|
||||
>>> print(len(outputs), len(positions))
|
||||
4 4
|
||||
"""
|
||||
out = [None] * len(self.convs)
|
||||
pos = [None] * len(self.convs)
|
||||
assert len(xs) == len(self.convs)
|
||||
# FPN forward pass
|
||||
# see https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/fpn.py
|
||||
prev_features = None
|
||||
# Forward in top-down order (from low to high resolution)
|
||||
n = len(self.convs) - 1
|
||||
for i in range(n, -1, -1):
|
||||
x = xs[i]
|
||||
lateral_features = self.convs[n - i](x)
|
||||
if i in self.fpn_top_down_levels and prev_features is not None:
|
||||
top_down_features = F.interpolate(
|
||||
prev_features.to(dtype=x.dtype),
|
||||
scale_factor=2.0,
|
||||
mode=self.fpn_interp_model,
|
||||
align_corners=(None if self.fpn_interp_model == "nearest" else False),
|
||||
antialias=False,
|
||||
)
|
||||
prev_features = lateral_features + top_down_features
|
||||
if self.fuse_type == "avg":
|
||||
prev_features /= 2
|
||||
else:
|
||||
prev_features = lateral_features
|
||||
x_out = prev_features
|
||||
out[i] = x_out
|
||||
pos[i] = self.position_encoding(x_out).to(x_out.dtype)
|
||||
|
||||
return out, pos
|
||||
|
||||
|
||||
class Hiera(nn.Module):
|
||||
"""Hierarchical vision transformer for efficient multiscale feature extraction in image processing tasks.
|
||||
|
||||
This class implements a Hiera model, which is a hierarchical vision transformer architecture designed for efficient
|
||||
multiscale feature extraction. It uses a series of transformer blocks organized into stages, with optional pooling
|
||||
and global attention mechanisms.
|
||||
|
||||
Attributes:
|
||||
window_spec (tuple[int, ...]): Window sizes for each stage.
|
||||
q_stride (tuple[int, int]): Downsampling stride between stages.
|
||||
stage_ends (list[int]): Indices of the last block in each stage.
|
||||
q_pool_blocks (list[int]): Indices of blocks where pooling is applied.
|
||||
return_interm_layers (bool): Whether to return intermediate layer outputs.
|
||||
patch_embed (PatchEmbed): Module for patch embedding.
|
||||
global_att_blocks (tuple[int, ...]): Indices of blocks with global attention.
|
||||
window_pos_embed_bkg_spatial_size (tuple[int, int]): Spatial size for window positional embedding background.
|
||||
pos_embed (nn.Parameter): Positional embedding for the background.
|
||||
pos_embed_window (nn.Parameter): Positional embedding for the window.
|
||||
blocks (nn.ModuleList): List of MultiScaleBlock modules.
|
||||
channel_list (list[int]): List of output channel dimensions for each stage.
|
||||
|
||||
Methods:
|
||||
_get_pos_embed: Generate positional embeddings by interpolating and combining window and background embeddings.
|
||||
forward: Perform the forward pass through the Hiera model.
|
||||
|
||||
Examples:
|
||||
>>> model = Hiera(embed_dim=96, num_heads=1, stages=(2, 3, 16, 3))
|
||||
>>> input_tensor = torch.randn(1, 3, 224, 224)
|
||||
>>> output_features = model(input_tensor)
|
||||
>>> for feat in output_features:
|
||||
... print(feat.shape)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim: int = 96, # initial embed dim
|
||||
num_heads: int = 1, # initial number of heads
|
||||
drop_path_rate: float = 0.0, # stochastic depth
|
||||
q_pool: int = 3, # number of q_pool stages
|
||||
q_stride: tuple[int, int] = (2, 2), # downsample stride bet. stages
|
||||
stages: tuple[int, ...] = (2, 3, 16, 3), # blocks per stage
|
||||
dim_mul: float = 2.0, # dim_mul factor at stage shift
|
||||
head_mul: float = 2.0, # head_mul factor at stage shift
|
||||
window_pos_embed_bkg_spatial_size: tuple[int, int] = (14, 14),
|
||||
# window size per stage, when not using global att.
|
||||
window_spec: tuple[int, ...] = (
|
||||
8,
|
||||
4,
|
||||
14,
|
||||
7,
|
||||
),
|
||||
# global attn in these blocks
|
||||
global_att_blocks: tuple[int, ...] = (
|
||||
12,
|
||||
16,
|
||||
20,
|
||||
),
|
||||
return_interm_layers=True, # return feats from every stage
|
||||
):
|
||||
"""Initialize a Hiera model, a hierarchical vision transformer for efficient multiscale feature extraction.
|
||||
|
||||
Hiera is a hierarchical vision transformer architecture designed for efficient multiscale feature extraction in
|
||||
image processing tasks. It uses a series of transformer blocks organized into stages, with optional pooling and
|
||||
global attention mechanisms.
|
||||
|
||||
Args:
|
||||
embed_dim (int): Initial embedding dimension for the model.
|
||||
num_heads (int): Initial number of attention heads.
|
||||
drop_path_rate (float): Stochastic depth rate.
|
||||
q_pool (int): Number of query pooling stages.
|
||||
q_stride (tuple[int, int]): Downsampling stride between stages.
|
||||
stages (tuple[int, ...]): Number of blocks per stage.
|
||||
dim_mul (float): Dimension multiplier factor at stage transitions.
|
||||
head_mul (float): Head multiplier factor at stage transitions.
|
||||
window_pos_embed_bkg_spatial_size (tuple[int, int]): Spatial size for window positional embedding
|
||||
background.
|
||||
window_spec (tuple[int, ...]): Window sizes for each stage when not using global attention.
|
||||
global_att_blocks (tuple[int, ...]): Indices of blocks that use global attention.
|
||||
return_interm_layers (bool): Whether to return intermediate layer outputs.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
assert len(stages) == len(window_spec)
|
||||
self.window_spec = window_spec
|
||||
|
||||
depth = sum(stages)
|
||||
self.q_stride = q_stride
|
||||
self.stage_ends = [sum(stages[:i]) - 1 for i in range(1, len(stages) + 1)]
|
||||
assert 0 <= q_pool <= len(self.stage_ends[:-1])
|
||||
self.q_pool_blocks = [x + 1 for x in self.stage_ends[:-1]][:q_pool]
|
||||
self.return_interm_layers = return_interm_layers
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
embed_dim=embed_dim,
|
||||
kernel_size=(7, 7),
|
||||
stride=(4, 4),
|
||||
padding=(3, 3),
|
||||
)
|
||||
# Which blocks have global attention?
|
||||
self.global_att_blocks = global_att_blocks
|
||||
|
||||
# Windowed positional embedding (https://arxiv.org/abs/2311.05613)
|
||||
self.window_pos_embed_bkg_spatial_size = window_pos_embed_bkg_spatial_size
|
||||
self.pos_embed = nn.Parameter(torch.zeros(1, embed_dim, *self.window_pos_embed_bkg_spatial_size))
|
||||
self.pos_embed_window = nn.Parameter(torch.zeros(1, embed_dim, self.window_spec[0], self.window_spec[0]))
|
||||
|
||||
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule
|
||||
|
||||
cur_stage = 1
|
||||
self.blocks = nn.ModuleList()
|
||||
|
||||
for i in range(depth):
|
||||
dim_out = embed_dim
|
||||
# Lags by a block, so first block of next stage uses an initial window size
|
||||
# of previous stage and final window size of current stage
|
||||
window_size = self.window_spec[cur_stage - 1]
|
||||
|
||||
if self.global_att_blocks is not None:
|
||||
window_size = 0 if i in self.global_att_blocks else window_size
|
||||
|
||||
if i - 1 in self.stage_ends:
|
||||
dim_out = int(embed_dim * dim_mul)
|
||||
num_heads = int(num_heads * head_mul)
|
||||
cur_stage += 1
|
||||
|
||||
block = MultiScaleBlock(
|
||||
dim=embed_dim,
|
||||
dim_out=dim_out,
|
||||
num_heads=num_heads,
|
||||
drop_path=dpr[i],
|
||||
q_stride=self.q_stride if i in self.q_pool_blocks else None,
|
||||
window_size=window_size,
|
||||
)
|
||||
|
||||
embed_dim = dim_out
|
||||
self.blocks.append(block)
|
||||
|
||||
self.channel_list = (
|
||||
[self.blocks[i].dim_out for i in self.stage_ends[::-1]]
|
||||
if return_interm_layers
|
||||
else [self.blocks[-1].dim_out]
|
||||
)
|
||||
|
||||
def _get_pos_embed(self, hw: tuple[int, int]) -> torch.Tensor:
|
||||
"""Generate positional embeddings by interpolating and combining window and background embeddings."""
|
||||
h, w = hw
|
||||
window_embed = self.pos_embed_window
|
||||
pos_embed = F.interpolate(self.pos_embed, size=(h, w), mode="bicubic")
|
||||
pos_embed = pos_embed + window_embed.tile([x // y for x, y in zip(pos_embed.shape, window_embed.shape)])
|
||||
pos_embed = pos_embed.permute(0, 2, 3, 1)
|
||||
return pos_embed
|
||||
|
||||
def forward(self, x: torch.Tensor) -> list[torch.Tensor]:
|
||||
"""Perform forward pass through Hiera model, extracting multiscale features from input images.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Input tensor with shape (B, C, H, W) representing a batch of images.
|
||||
|
||||
Returns:
|
||||
(list[torch.Tensor]): List of feature maps at different scales, each with shape (B, C_i, H_i, W_i), where
|
||||
C_i is the channel dimension and H_i, W_i are the spatial dimensions at scale i. The list is ordered
|
||||
from highest resolution (fine features) to lowest resolution (coarse features) if return_interm_layers
|
||||
is True, otherwise contains only the final output.
|
||||
|
||||
Examples:
|
||||
>>> model = Hiera(embed_dim=96, num_heads=1, stages=(2, 3, 16, 3))
|
||||
>>> input_tensor = torch.randn(1, 3, 224, 224)
|
||||
>>> output_features = model(input_tensor)
|
||||
>>> for feat in output_features:
|
||||
... print(feat.shape)
|
||||
"""
|
||||
x = self.patch_embed(x)
|
||||
# x: (B, H, W, C)
|
||||
|
||||
# Add positional embedding
|
||||
x = x + self._get_pos_embed(x.shape[1:3])
|
||||
|
||||
outputs = []
|
||||
for i, blk in enumerate(self.blocks):
|
||||
x = blk(x)
|
||||
if (i == self.stage_ends[-1]) or (i in self.stage_ends and self.return_interm_layers):
|
||||
feats = x.permute(0, 3, 1, 2)
|
||||
outputs.append(feats)
|
||||
|
||||
return outputs
|
||||
298
ultralytics/models/sam/modules/memory_attention.py
Executable file
298
ultralytics/models/sam/modules/memory_attention.py
Executable file
@@ -0,0 +1,298 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from .blocks import RoPEAttention
|
||||
|
||||
|
||||
class MemoryAttentionLayer(nn.Module):
|
||||
"""Implements a memory attention layer with self-attention and cross-attention mechanisms for neural networks.
|
||||
|
||||
This class combines self-attention, cross-attention, and feedforward components to process input tensors and
|
||||
generate memory-based attention outputs.
|
||||
|
||||
Attributes:
|
||||
d_model (int): Dimensionality of the model.
|
||||
dim_feedforward (int): Dimensionality of the feedforward network.
|
||||
dropout_value (float): Dropout rate for regularization.
|
||||
self_attn (RoPEAttention): Self-attention mechanism using RoPE (Rotary Position Embedding).
|
||||
cross_attn_image (RoPEAttention): Cross-attention mechanism for image processing.
|
||||
linear1 (nn.Linear): First linear layer of the feedforward network.
|
||||
linear2 (nn.Linear): Second linear layer of the feedforward network.
|
||||
norm1 (nn.LayerNorm): Layer normalization for self-attention output.
|
||||
norm2 (nn.LayerNorm): Layer normalization for cross-attention output.
|
||||
norm3 (nn.LayerNorm): Layer normalization for feedforward network output.
|
||||
dropout1 (nn.Dropout): Dropout layer after self-attention.
|
||||
dropout2 (nn.Dropout): Dropout layer after cross-attention.
|
||||
dropout3 (nn.Dropout): Dropout layer after feedforward network.
|
||||
activation (nn.ReLU): Activation function for the feedforward network.
|
||||
pos_enc_at_attn (bool): Flag to add positional encoding at attention.
|
||||
pos_enc_at_cross_attn_queries (bool): Flag to add positional encoding to cross-attention queries.
|
||||
pos_enc_at_cross_attn_keys (bool): Flag to add positional encoding to cross-attention keys.
|
||||
|
||||
Methods:
|
||||
forward: Performs the full memory attention operation on input tensors.
|
||||
_forward_sa: Performs self-attention on input tensor.
|
||||
_forward_ca: Performs cross-attention between target and memory tensors.
|
||||
|
||||
Examples:
|
||||
>>> layer = MemoryAttentionLayer(d_model=256, dim_feedforward=2048, dropout=0.1)
|
||||
>>> tgt = torch.randn(1, 100, 256)
|
||||
>>> memory = torch.randn(1, 100, 64)
|
||||
>>> pos = torch.randn(1, 100, 256)
|
||||
>>> query_pos = torch.randn(1, 100, 256)
|
||||
>>> output = layer(tgt, memory, pos, query_pos)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 100, 256])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int = 256,
|
||||
dim_feedforward: int = 2048,
|
||||
dropout: float = 0.1,
|
||||
pos_enc_at_attn: bool = False,
|
||||
pos_enc_at_cross_attn_keys: bool = True,
|
||||
pos_enc_at_cross_attn_queries: bool = False,
|
||||
self_attn: nn.Module | None = None,
|
||||
cross_attn: nn.Module | None = None,
|
||||
):
|
||||
"""Initialize a memory attention layer with self-attention, cross-attention, and feedforward components.
|
||||
|
||||
Args:
|
||||
d_model (int): Dimensionality of the model.
|
||||
dim_feedforward (int): Dimensionality of the feedforward network.
|
||||
dropout (float): Dropout rate for regularization.
|
||||
pos_enc_at_attn (bool): Whether to add positional encoding at attention.
|
||||
pos_enc_at_cross_attn_keys (bool): Whether to add positional encoding to cross-attention keys.
|
||||
pos_enc_at_cross_attn_queries (bool): Whether to add positional encoding to cross-attention queries.
|
||||
self_attn (nn.Module | None): Custom self-attention module. If None, a default RoPEAttention is used.
|
||||
cross_attn (nn.Module | None): Custom cross-attention module. If None, a default RoPEAttention is used.
|
||||
"""
|
||||
super().__init__()
|
||||
self.d_model = d_model
|
||||
self.dim_feedforward = dim_feedforward
|
||||
self.dropout_value = dropout
|
||||
self.self_attn = self_attn or RoPEAttention(embedding_dim=256, num_heads=1, downsample_rate=1)
|
||||
self.cross_attn_image = cross_attn or RoPEAttention(
|
||||
rope_k_repeat=True,
|
||||
embedding_dim=256,
|
||||
num_heads=1,
|
||||
downsample_rate=1,
|
||||
kv_in_dim=64,
|
||||
)
|
||||
|
||||
# Implementation of Feedforward model
|
||||
self.linear1 = nn.Linear(d_model, dim_feedforward)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.linear2 = nn.Linear(dim_feedforward, d_model)
|
||||
|
||||
self.norm1 = nn.LayerNorm(d_model)
|
||||
self.norm2 = nn.LayerNorm(d_model)
|
||||
self.norm3 = nn.LayerNorm(d_model)
|
||||
self.dropout1 = nn.Dropout(dropout)
|
||||
self.dropout2 = nn.Dropout(dropout)
|
||||
self.dropout3 = nn.Dropout(dropout)
|
||||
|
||||
self.activation = nn.ReLU()
|
||||
|
||||
# Where to add pos enc
|
||||
self.pos_enc_at_attn = pos_enc_at_attn
|
||||
self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries
|
||||
self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys
|
||||
|
||||
def _forward_sa(self, tgt: torch.Tensor, query_pos: torch.Tensor | None) -> torch.Tensor:
|
||||
"""Perform self-attention on input tensor using positional encoding and RoPE attention mechanism."""
|
||||
tgt2 = self.norm1(tgt)
|
||||
q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2
|
||||
tgt2 = self.self_attn(q, k, v=tgt2)
|
||||
tgt = tgt + self.dropout1(tgt2)
|
||||
return tgt
|
||||
|
||||
def _forward_ca(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
query_pos: torch.Tensor | None,
|
||||
pos: torch.Tensor | None,
|
||||
num_k_exclude_rope: int = 0,
|
||||
) -> torch.Tensor:
|
||||
"""Perform cross-attention between target and memory tensors using RoPEAttention mechanism."""
|
||||
kwds = {}
|
||||
if num_k_exclude_rope > 0:
|
||||
assert isinstance(self.cross_attn_image, RoPEAttention)
|
||||
kwds = {"num_k_exclude_rope": num_k_exclude_rope}
|
||||
|
||||
# Cross-Attention
|
||||
tgt2 = self.norm2(tgt)
|
||||
tgt2 = self.cross_attn_image(
|
||||
q=tgt2 + query_pos if self.pos_enc_at_cross_attn_queries else tgt2,
|
||||
k=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
|
||||
v=memory,
|
||||
**kwds,
|
||||
)
|
||||
tgt = tgt + self.dropout2(tgt2)
|
||||
return tgt
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
pos: torch.Tensor | None = None,
|
||||
query_pos: torch.Tensor | None = None,
|
||||
num_k_exclude_rope: int = 0,
|
||||
) -> torch.Tensor:
|
||||
"""Process input tensors through self-attention, cross-attention, and feedforward network layers.
|
||||
|
||||
Args:
|
||||
tgt (torch.Tensor): Target tensor for self-attention with shape (N, L, D).
|
||||
memory (torch.Tensor): Memory tensor for cross-attention with shape (N, S, D).
|
||||
pos (torch.Tensor | None): Positional encoding for memory tensor.
|
||||
query_pos (torch.Tensor | None): Positional encoding for target tensor.
|
||||
num_k_exclude_rope (int): Number of keys to exclude from rotary position embedding.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Processed tensor after attention and feedforward layers with shape (N, L, D).
|
||||
"""
|
||||
tgt = self._forward_sa(tgt, query_pos)
|
||||
tgt = self._forward_ca(tgt, memory, query_pos, pos, num_k_exclude_rope)
|
||||
# MLP
|
||||
tgt2 = self.norm3(tgt)
|
||||
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2))))
|
||||
tgt = tgt + self.dropout3(tgt2)
|
||||
return tgt
|
||||
|
||||
|
||||
class MemoryAttention(nn.Module):
|
||||
"""Memory attention module for processing sequential data with self and cross-attention mechanisms.
|
||||
|
||||
This class implements a multi-layer attention mechanism that combines self-attention and cross-attention for
|
||||
processing sequential data, particularly useful in transformer-like architectures.
|
||||
|
||||
Attributes:
|
||||
d_model (int): The dimension of the model's hidden state.
|
||||
layers (nn.ModuleList): A list of MemoryAttentionLayer modules.
|
||||
num_layers (int): The number of attention layers.
|
||||
norm (nn.LayerNorm): Layer normalization applied to the output.
|
||||
pos_enc_at_input (bool): Whether to apply positional encoding at the input.
|
||||
batch_first (bool): Whether the input tensors are in batch-first format.
|
||||
|
||||
Methods:
|
||||
forward: Processes input tensors through the attention layers.
|
||||
|
||||
Examples:
|
||||
>>> d_model = 256
|
||||
>>> layer = MemoryAttentionLayer(d_model)
|
||||
>>> attention = MemoryAttention(d_model, pos_enc_at_input=True, layer=layer, num_layers=3)
|
||||
>>> curr = torch.randn(10, 32, d_model) # (seq_len, batch_size, d_model)
|
||||
>>> memory = torch.randn(20, 32, d_model) # (mem_len, batch_size, d_model)
|
||||
>>> curr_pos = torch.randn(10, 32, d_model)
|
||||
>>> memory_pos = torch.randn(20, 32, d_model)
|
||||
>>> output = attention(curr, memory, curr_pos, memory_pos)
|
||||
>>> print(output.shape)
|
||||
torch.Size([10, 32, 256])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
pos_enc_at_input: bool,
|
||||
layer: nn.Module,
|
||||
num_layers: int,
|
||||
batch_first: bool = True, # Do layers expect batch first input?
|
||||
):
|
||||
"""Initialize MemoryAttention with specified layers and normalization for sequential data processing.
|
||||
|
||||
This class implements a multi-layer attention mechanism that combines self-attention and cross-attention for
|
||||
processing sequential data, particularly useful in transformer-like architectures.
|
||||
|
||||
Args:
|
||||
d_model (int): The dimension of the model's hidden state.
|
||||
pos_enc_at_input (bool): Whether to apply positional encoding at the input.
|
||||
layer (nn.Module): The attention layer to be used in the module.
|
||||
num_layers (int): The number of attention layers.
|
||||
batch_first (bool): Whether the input tensors are in batch-first format.
|
||||
"""
|
||||
super().__init__()
|
||||
self.d_model = d_model
|
||||
self.layers = nn.ModuleList([copy.deepcopy(layer) for _ in range(num_layers)])
|
||||
self.num_layers = num_layers
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
self.pos_enc_at_input = pos_enc_at_input
|
||||
self.batch_first = batch_first
|
||||
|
||||
def forward(
|
||||
self,
|
||||
curr: torch.Tensor, # self-attention inputs
|
||||
memory: torch.Tensor, # cross-attention inputs
|
||||
curr_pos: torch.Tensor | None = None, # pos_enc for self-attention inputs
|
||||
memory_pos: torch.Tensor | None = None, # pos_enc for cross-attention inputs
|
||||
num_obj_ptr_tokens: int = 0, # number of object pointer *tokens*
|
||||
) -> torch.Tensor:
|
||||
"""Process inputs through attention layers, applying self and cross-attention with positional encoding.
|
||||
|
||||
Args:
|
||||
curr (torch.Tensor): Self-attention input tensor, representing the current state.
|
||||
memory (torch.Tensor): Cross-attention input tensor, representing memory information.
|
||||
curr_pos (torch.Tensor | None): Positional encoding for self-attention inputs.
|
||||
memory_pos (torch.Tensor | None): Positional encoding for cross-attention inputs.
|
||||
num_obj_ptr_tokens (int): Number of object pointer tokens to exclude from rotary position embedding.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Processed output tensor after applying attention layers and normalization.
|
||||
|
||||
Examples:
|
||||
>>> d_model = 256
|
||||
>>> layer = MemoryAttentionLayer(d_model)
|
||||
>>> attention = MemoryAttention(d_model, pos_enc_at_input=True, layer=layer, num_layers=3)
|
||||
>>> curr = torch.randn(10, 32, d_model) # (seq_len, batch_size, d_model)
|
||||
>>> memory = torch.randn(20, 32, d_model) # (mem_len, batch_size, d_model)
|
||||
>>> curr_pos = torch.randn(10, 32, d_model)
|
||||
>>> memory_pos = torch.randn(20, 32, d_model)
|
||||
>>> output = attention(curr, memory, curr_pos, memory_pos)
|
||||
>>> print(output.shape)
|
||||
torch.Size([10, 32, 256])
|
||||
"""
|
||||
if isinstance(curr, list):
|
||||
assert isinstance(curr_pos, list)
|
||||
assert len(curr) == len(curr_pos) == 1
|
||||
curr, curr_pos = curr[0], curr_pos[0]
|
||||
|
||||
assert curr.shape[1] == memory.shape[1], "Batch size must be the same for curr and memory"
|
||||
|
||||
output = curr
|
||||
if self.pos_enc_at_input and curr_pos is not None:
|
||||
output = output + 0.1 * curr_pos
|
||||
|
||||
if self.batch_first:
|
||||
# Convert to batch first
|
||||
output = output.transpose(0, 1)
|
||||
curr_pos = curr_pos.transpose(0, 1)
|
||||
memory = memory.transpose(0, 1)
|
||||
memory_pos = memory_pos.transpose(0, 1)
|
||||
|
||||
for layer in self.layers:
|
||||
kwds = {}
|
||||
if isinstance(layer.cross_attn_image, RoPEAttention):
|
||||
kwds = {"num_k_exclude_rope": num_obj_ptr_tokens}
|
||||
|
||||
output = layer(
|
||||
tgt=output,
|
||||
memory=memory,
|
||||
pos=memory_pos,
|
||||
query_pos=curr_pos,
|
||||
**kwds,
|
||||
)
|
||||
normed_output = self.norm(output)
|
||||
|
||||
if self.batch_first:
|
||||
# Convert back to seq first
|
||||
normed_output = normed_output.transpose(0, 1)
|
||||
curr_pos = curr_pos.transpose(0, 1)
|
||||
|
||||
return normed_output
|
||||
1159
ultralytics/models/sam/modules/sam.py
Executable file
1159
ultralytics/models/sam/modules/sam.py
Executable file
File diff suppressed because it is too large
Load Diff
979
ultralytics/models/sam/modules/tiny_encoder.py
Executable file
979
ultralytics/models/sam/modules/tiny_encoder.py
Executable file
@@ -0,0 +1,979 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# --------------------------------------------------------
|
||||
# TinyViT Model Architecture
|
||||
# Copyright (c) 2022 Microsoft
|
||||
# Adapted from LeViT and Swin Transformer
|
||||
# LeViT: (https://github.com/facebookresearch/levit)
|
||||
# Swin: (https://github.com/microsoft/swin-transformer)
|
||||
# Build the TinyViT Model
|
||||
# --------------------------------------------------------
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ultralytics.nn.modules import LayerNorm2d
|
||||
from ultralytics.utils.instance import to_2tuple
|
||||
|
||||
|
||||
class Conv2d_BN(torch.nn.Sequential):
|
||||
"""A sequential container that performs 2D convolution followed by batch normalization.
|
||||
|
||||
This module combines a 2D convolution layer with batch normalization, providing a common building block for
|
||||
convolutional neural networks. The batch normalization weights and biases are initialized to specific values for
|
||||
optimal training performance.
|
||||
|
||||
Attributes:
|
||||
c (torch.nn.Conv2d): 2D convolution layer.
|
||||
bn (torch.nn.BatchNorm2d): Batch normalization layer.
|
||||
|
||||
Examples:
|
||||
>>> conv_bn = Conv2d_BN(3, 64, ks=3, stride=1, pad=1)
|
||||
>>> input_tensor = torch.randn(1, 3, 224, 224)
|
||||
>>> output = conv_bn(input_tensor)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 64, 224, 224])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
a: int,
|
||||
b: int,
|
||||
ks: int = 1,
|
||||
stride: int = 1,
|
||||
pad: int = 0,
|
||||
dilation: int = 1,
|
||||
groups: int = 1,
|
||||
bn_weight_init: float = 1,
|
||||
):
|
||||
"""Initialize a sequential container with 2D convolution followed by batch normalization.
|
||||
|
||||
Args:
|
||||
a (int): Number of input channels.
|
||||
b (int): Number of output channels.
|
||||
ks (int, optional): Kernel size for the convolution.
|
||||
stride (int, optional): Stride for the convolution.
|
||||
pad (int, optional): Padding for the convolution.
|
||||
dilation (int, optional): Dilation factor for the convolution.
|
||||
groups (int, optional): Number of groups for the convolution.
|
||||
bn_weight_init (float, optional): Initial value for batch normalization weight.
|
||||
"""
|
||||
super().__init__()
|
||||
self.add_module("c", torch.nn.Conv2d(a, b, ks, stride, pad, dilation, groups, bias=False))
|
||||
bn = torch.nn.BatchNorm2d(b)
|
||||
torch.nn.init.constant_(bn.weight, bn_weight_init)
|
||||
torch.nn.init.constant_(bn.bias, 0)
|
||||
self.add_module("bn", bn)
|
||||
|
||||
|
||||
class PatchEmbed(nn.Module):
|
||||
"""Embed images into patches and project them into a specified embedding dimension.
|
||||
|
||||
This module converts input images into patch embeddings using a sequence of convolutional layers, effectively
|
||||
downsampling the spatial dimensions while increasing the channel dimension.
|
||||
|
||||
Attributes:
|
||||
patches_resolution (tuple[int, int]): Resolution of the patches after embedding.
|
||||
num_patches (int): Total number of patches.
|
||||
in_chans (int): Number of input channels.
|
||||
embed_dim (int): Dimension of the embedding.
|
||||
seq (nn.Sequential): Sequence of convolutional and activation layers for patch embedding.
|
||||
|
||||
Examples:
|
||||
>>> import torch
|
||||
>>> patch_embed = PatchEmbed(in_chans=3, embed_dim=96, resolution=224, activation=nn.GELU)
|
||||
>>> x = torch.randn(1, 3, 224, 224)
|
||||
>>> output = patch_embed(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 96, 56, 56])
|
||||
"""
|
||||
|
||||
def __init__(self, in_chans: int, embed_dim: int, resolution: int, activation):
|
||||
"""Initialize patch embedding with convolutional layers for image-to-patch conversion and projection.
|
||||
|
||||
Args:
|
||||
in_chans (int): Number of input channels.
|
||||
embed_dim (int): Dimension of the embedding.
|
||||
resolution (int): Input image resolution.
|
||||
activation (nn.Module): Activation function to use between convolutions.
|
||||
"""
|
||||
super().__init__()
|
||||
img_size: tuple[int, int] = to_2tuple(resolution)
|
||||
self.patches_resolution = (img_size[0] // 4, img_size[1] // 4)
|
||||
self.num_patches = self.patches_resolution[0] * self.patches_resolution[1]
|
||||
self.in_chans = in_chans
|
||||
self.embed_dim = embed_dim
|
||||
n = embed_dim
|
||||
self.seq = nn.Sequential(
|
||||
Conv2d_BN(in_chans, n // 2, 3, 2, 1),
|
||||
activation(),
|
||||
Conv2d_BN(n // 2, n, 3, 2, 1),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Process input tensor through patch embedding sequence, converting images to patch embeddings."""
|
||||
return self.seq(x)
|
||||
|
||||
|
||||
class MBConv(nn.Module):
|
||||
"""Mobile Inverted Bottleneck Conv (MBConv) layer, part of the EfficientNet architecture.
|
||||
|
||||
This module implements the mobile inverted bottleneck convolution with expansion, depthwise convolution, and
|
||||
projection phases, along with residual connections for improved gradient flow.
|
||||
|
||||
Attributes:
|
||||
in_chans (int): Number of input channels.
|
||||
hidden_chans (int): Number of hidden channels after expansion.
|
||||
out_chans (int): Number of output channels.
|
||||
conv1 (Conv2d_BN): First convolutional layer for channel expansion.
|
||||
act1 (nn.Module): First activation function.
|
||||
conv2 (Conv2d_BN): Depthwise convolutional layer.
|
||||
act2 (nn.Module): Second activation function.
|
||||
conv3 (Conv2d_BN): Final convolutional layer for projection.
|
||||
act3 (nn.Module): Third activation function.
|
||||
drop_path (nn.Module): Drop path layer (Identity for inference).
|
||||
|
||||
Examples:
|
||||
>>> in_chans, out_chans = 32, 64
|
||||
>>> mbconv = MBConv(in_chans, out_chans, expand_ratio=4, activation=nn.ReLU, drop_path=0.1)
|
||||
>>> x = torch.randn(1, in_chans, 56, 56)
|
||||
>>> output = mbconv(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 64, 56, 56])
|
||||
"""
|
||||
|
||||
def __init__(self, in_chans: int, out_chans: int, expand_ratio: float, activation, drop_path: float):
|
||||
"""Initialize the MBConv layer with specified input/output channels, expansion ratio, and activation.
|
||||
|
||||
Args:
|
||||
in_chans (int): Number of input channels.
|
||||
out_chans (int): Number of output channels.
|
||||
expand_ratio (float): Channel expansion ratio for the hidden layer.
|
||||
activation (nn.Module): Activation function to use.
|
||||
drop_path (float): Drop path rate for stochastic depth.
|
||||
"""
|
||||
super().__init__()
|
||||
self.in_chans = in_chans
|
||||
self.hidden_chans = int(in_chans * expand_ratio)
|
||||
self.out_chans = out_chans
|
||||
|
||||
self.conv1 = Conv2d_BN(in_chans, self.hidden_chans, ks=1)
|
||||
self.act1 = activation()
|
||||
|
||||
self.conv2 = Conv2d_BN(self.hidden_chans, self.hidden_chans, ks=3, stride=1, pad=1, groups=self.hidden_chans)
|
||||
self.act2 = activation()
|
||||
|
||||
self.conv3 = Conv2d_BN(self.hidden_chans, out_chans, ks=1, bn_weight_init=0.0)
|
||||
self.act3 = activation()
|
||||
|
||||
# NOTE: `DropPath` is needed only for training.
|
||||
# self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
|
||||
self.drop_path = nn.Identity()
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Implement the forward pass of MBConv, applying convolutions and skip connection."""
|
||||
shortcut = x
|
||||
x = self.conv1(x)
|
||||
x = self.act1(x)
|
||||
x = self.conv2(x)
|
||||
x = self.act2(x)
|
||||
x = self.conv3(x)
|
||||
x = self.drop_path(x)
|
||||
x += shortcut
|
||||
return self.act3(x)
|
||||
|
||||
|
||||
class PatchMerging(nn.Module):
|
||||
"""Merge neighboring patches in the feature map and project to a new dimension.
|
||||
|
||||
This class implements a patch merging operation that combines spatial information and adjusts the feature dimension
|
||||
using a series of convolutional layers with batch normalization. It effectively reduces spatial resolution while
|
||||
potentially increasing channel dimensions.
|
||||
|
||||
Attributes:
|
||||
input_resolution (tuple[int, int]): The input resolution (height, width) of the feature map.
|
||||
dim (int): The input dimension of the feature map.
|
||||
out_dim (int): The output dimension after merging and projection.
|
||||
act (nn.Module): The activation function used between convolutions.
|
||||
conv1 (Conv2d_BN): The first convolutional layer for dimension projection.
|
||||
conv2 (Conv2d_BN): The second convolutional layer for spatial merging.
|
||||
conv3 (Conv2d_BN): The third convolutional layer for final projection.
|
||||
|
||||
Examples:
|
||||
>>> input_resolution = (56, 56)
|
||||
>>> patch_merging = PatchMerging(input_resolution, dim=64, out_dim=128, activation=nn.ReLU)
|
||||
>>> x = torch.randn(4, 64, 56, 56)
|
||||
>>> output = patch_merging(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([4, 3136, 128])
|
||||
"""
|
||||
|
||||
def __init__(self, input_resolution: tuple[int, int], dim: int, out_dim: int, activation):
|
||||
"""Initialize the PatchMerging module for merging and projecting neighboring patches in feature maps.
|
||||
|
||||
Args:
|
||||
input_resolution (tuple[int, int]): The input resolution (height, width) of the feature map.
|
||||
dim (int): The input dimension of the feature map.
|
||||
out_dim (int): The output dimension after merging and projection.
|
||||
activation (nn.Module): The activation function used between convolutions.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.input_resolution = input_resolution
|
||||
self.dim = dim
|
||||
self.out_dim = out_dim
|
||||
self.act = activation()
|
||||
self.conv1 = Conv2d_BN(dim, out_dim, 1, 1, 0)
|
||||
stride_c = 1 if out_dim in {320, 448, 576} else 2
|
||||
self.conv2 = Conv2d_BN(out_dim, out_dim, 3, stride_c, 1, groups=out_dim)
|
||||
self.conv3 = Conv2d_BN(out_dim, out_dim, 1, 1, 0)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply patch merging and dimension projection to the input feature map."""
|
||||
if x.ndim == 3:
|
||||
H, W = self.input_resolution
|
||||
B = len(x)
|
||||
# (B, C, H, W)
|
||||
x = x.view(B, H, W, -1).permute(0, 3, 1, 2)
|
||||
|
||||
x = self.conv1(x)
|
||||
x = self.act(x)
|
||||
|
||||
x = self.conv2(x)
|
||||
x = self.act(x)
|
||||
x = self.conv3(x)
|
||||
return x.flatten(2).transpose(1, 2)
|
||||
|
||||
|
||||
class ConvLayer(nn.Module):
|
||||
"""Convolutional Layer featuring multiple MobileNetV3-style inverted bottleneck convolutions (MBConv).
|
||||
|
||||
This layer optionally applies downsample operations to the output and supports gradient checkpointing for memory
|
||||
efficiency during training.
|
||||
|
||||
Attributes:
|
||||
dim (int): Dimensionality of the input and output.
|
||||
input_resolution (tuple[int, int]): Resolution of the input image.
|
||||
depth (int): Number of MBConv layers in the block.
|
||||
use_checkpoint (bool): Whether to use gradient checkpointing to save memory.
|
||||
blocks (nn.ModuleList): List of MBConv layers.
|
||||
downsample (nn.Module | None): Function for downsampling the output.
|
||||
|
||||
Examples:
|
||||
>>> input_tensor = torch.randn(1, 64, 56, 56)
|
||||
>>> conv_layer = ConvLayer(64, (56, 56), depth=3, activation=nn.ReLU)
|
||||
>>> output = conv_layer(input_tensor)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 3136, 128])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
input_resolution: tuple[int, int],
|
||||
depth: int,
|
||||
activation,
|
||||
drop_path: float | list[float] = 0.0,
|
||||
downsample: nn.Module | None = None,
|
||||
use_checkpoint: bool = False,
|
||||
out_dim: int | None = None,
|
||||
conv_expand_ratio: float = 4.0,
|
||||
):
|
||||
"""Initialize the ConvLayer with the given dimensions and settings.
|
||||
|
||||
This layer consists of multiple MobileNetV3-style inverted bottleneck convolutions (MBConv) and optionally
|
||||
applies downsampling to the output.
|
||||
|
||||
Args:
|
||||
dim (int): The dimensionality of the input and output.
|
||||
input_resolution (tuple[int, int]): The resolution of the input image.
|
||||
depth (int): The number of MBConv layers in the block.
|
||||
activation (nn.Module): Activation function applied after each convolution.
|
||||
drop_path (float | list[float], optional): Drop path rate. Single float or a list of floats for each MBConv.
|
||||
downsample (nn.Module | None, optional): Function for downsampling the output. None to skip downsampling.
|
||||
use_checkpoint (bool, optional): Whether to use gradient checkpointing to save memory.
|
||||
out_dim (int | None, optional): Output dimensions. None means it will be the same as `dim`.
|
||||
conv_expand_ratio (float, optional): Expansion ratio for the MBConv layers.
|
||||
"""
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.depth = depth
|
||||
self.use_checkpoint = use_checkpoint
|
||||
|
||||
# Build blocks
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
MBConv(
|
||||
dim,
|
||||
dim,
|
||||
conv_expand_ratio,
|
||||
activation,
|
||||
drop_path[i] if isinstance(drop_path, list) else drop_path,
|
||||
)
|
||||
for i in range(depth)
|
||||
]
|
||||
)
|
||||
|
||||
# Patch merging layer
|
||||
self.downsample = (
|
||||
None
|
||||
if downsample is None
|
||||
else downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation)
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Process input through convolutional layers, applying MBConv blocks and optional downsampling."""
|
||||
for blk in self.blocks:
|
||||
x = torch.utils.checkpoint(blk, x) if self.use_checkpoint else blk(x) # warn: checkpoint is slow import
|
||||
return x if self.downsample is None else self.downsample(x)
|
||||
|
||||
|
||||
class MLP(nn.Module):
|
||||
"""Multi-layer Perceptron (MLP) module for transformer architectures.
|
||||
|
||||
This module applies layer normalization, two fully-connected layers with an activation function in between, and
|
||||
dropout. It is commonly used in transformer-based architectures for processing token embeddings.
|
||||
|
||||
Attributes:
|
||||
norm (nn.LayerNorm): Layer normalization applied to the input.
|
||||
fc1 (nn.Linear): First fully-connected layer.
|
||||
fc2 (nn.Linear): Second fully-connected layer.
|
||||
act (nn.Module): Activation function applied after the first fully-connected layer.
|
||||
drop (nn.Dropout): Dropout layer applied after the activation function.
|
||||
|
||||
Examples:
|
||||
>>> import torch
|
||||
>>> from torch import nn
|
||||
>>> mlp = MLP(in_features=256, hidden_features=512, out_features=256, activation=nn.GELU, drop=0.1)
|
||||
>>> x = torch.randn(32, 100, 256)
|
||||
>>> output = mlp(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([32, 100, 256])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_features: int,
|
||||
hidden_features: int | None = None,
|
||||
out_features: int | None = None,
|
||||
activation=nn.GELU,
|
||||
drop: float = 0.0,
|
||||
):
|
||||
"""Initialize a multi-layer perceptron with configurable input, hidden, and output dimensions.
|
||||
|
||||
Args:
|
||||
in_features (int): Number of input features.
|
||||
hidden_features (int | None, optional): Number of hidden features.
|
||||
out_features (int | None, optional): Number of output features.
|
||||
activation (nn.Module): Activation function applied after the first fully-connected layer.
|
||||
drop (float, optional): Dropout probability.
|
||||
"""
|
||||
super().__init__()
|
||||
out_features = out_features or in_features
|
||||
hidden_features = hidden_features or in_features
|
||||
self.norm = nn.LayerNorm(in_features)
|
||||
self.fc1 = nn.Linear(in_features, hidden_features)
|
||||
self.fc2 = nn.Linear(hidden_features, out_features)
|
||||
self.act = activation()
|
||||
self.drop = nn.Dropout(drop)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply MLP operations: layer norm, FC layers, activation, and dropout to the input tensor."""
|
||||
x = self.norm(x)
|
||||
x = self.fc1(x)
|
||||
x = self.act(x)
|
||||
x = self.drop(x)
|
||||
x = self.fc2(x)
|
||||
return self.drop(x)
|
||||
|
||||
|
||||
class Attention(torch.nn.Module):
|
||||
"""Multi-head attention module with spatial awareness and trainable attention biases.
|
||||
|
||||
This module implements a multi-head attention mechanism with support for spatial awareness, applying attention
|
||||
biases based on spatial resolution. It includes trainable attention biases for each unique offset between spatial
|
||||
positions in the resolution grid.
|
||||
|
||||
Attributes:
|
||||
num_heads (int): Number of attention heads.
|
||||
scale (float): Scaling factor for attention scores.
|
||||
key_dim (int): Dimensionality of the keys and queries.
|
||||
nh_kd (int): Product of num_heads and key_dim.
|
||||
d (int): Dimensionality of the value vectors.
|
||||
dh (int): Product of d and num_heads.
|
||||
attn_ratio (float): Attention ratio affecting the dimensions of the value vectors.
|
||||
norm (nn.LayerNorm): Layer normalization applied to input.
|
||||
qkv (nn.Linear): Linear layer for computing query, key, and value projections.
|
||||
proj (nn.Linear): Linear layer for final projection.
|
||||
attention_biases (nn.Parameter): Learnable attention biases.
|
||||
attention_bias_idxs (torch.Tensor): Indices for attention biases.
|
||||
ab (torch.Tensor): Cached attention biases for inference, deleted during training.
|
||||
|
||||
Examples:
|
||||
>>> attn = Attention(dim=256, key_dim=64, num_heads=8, resolution=(14, 14))
|
||||
>>> x = torch.randn(1, 196, 256)
|
||||
>>> output = attn(x)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 196, 256])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
key_dim: int,
|
||||
num_heads: int = 8,
|
||||
attn_ratio: float = 4,
|
||||
resolution: tuple[int, int] = (14, 14),
|
||||
):
|
||||
"""Initialize the Attention module for multi-head attention with spatial awareness.
|
||||
|
||||
This module implements a multi-head attention mechanism with support for spatial awareness, applying attention
|
||||
biases based on spatial resolution. It includes trainable attention biases for each unique offset between
|
||||
spatial positions in the resolution grid.
|
||||
|
||||
Args:
|
||||
dim (int): The dimensionality of the input and output.
|
||||
key_dim (int): The dimensionality of the keys and queries.
|
||||
num_heads (int, optional): Number of attention heads.
|
||||
attn_ratio (float, optional): Attention ratio, affecting the dimensions of the value vectors.
|
||||
resolution (tuple[int, int], optional): Spatial resolution of the input feature map.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
assert isinstance(resolution, tuple) and len(resolution) == 2, "'resolution' argument not tuple of length 2"
|
||||
self.num_heads = num_heads
|
||||
self.scale = key_dim**-0.5
|
||||
self.key_dim = key_dim
|
||||
self.nh_kd = nh_kd = key_dim * num_heads
|
||||
self.d = int(attn_ratio * key_dim)
|
||||
self.dh = int(attn_ratio * key_dim) * num_heads
|
||||
self.attn_ratio = attn_ratio
|
||||
h = self.dh + nh_kd * 2
|
||||
|
||||
self.norm = nn.LayerNorm(dim)
|
||||
self.qkv = nn.Linear(dim, h)
|
||||
self.proj = nn.Linear(self.dh, dim)
|
||||
|
||||
points = list(itertools.product(range(resolution[0]), range(resolution[1])))
|
||||
N = len(points)
|
||||
attention_offsets = {}
|
||||
idxs = []
|
||||
for p1 in points:
|
||||
for p2 in points:
|
||||
offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
|
||||
if offset not in attention_offsets:
|
||||
attention_offsets[offset] = len(attention_offsets)
|
||||
idxs.append(attention_offsets[offset])
|
||||
self.attention_biases = torch.nn.Parameter(torch.zeros(num_heads, len(attention_offsets)))
|
||||
self.register_buffer("attention_bias_idxs", torch.LongTensor(idxs).view(N, N), persistent=False)
|
||||
|
||||
@torch.no_grad()
|
||||
def train(self, mode: bool = True):
|
||||
"""Set the module in training mode and handle the 'ab' attribute for cached attention biases."""
|
||||
super().train(mode)
|
||||
if mode and hasattr(self, "ab"):
|
||||
del self.ab
|
||||
else:
|
||||
self.ab = self.attention_biases[:, self.attention_bias_idxs]
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply multi-head attention with spatial awareness and trainable attention biases."""
|
||||
B, N, _ = x.shape # B, N, C
|
||||
|
||||
# Normalization
|
||||
x = self.norm(x)
|
||||
|
||||
qkv = self.qkv(x)
|
||||
# (B, N, num_heads, d)
|
||||
q, k, v = qkv.view(B, N, self.num_heads, -1).split([self.key_dim, self.key_dim, self.d], dim=3)
|
||||
# (B, num_heads, N, d)
|
||||
q = q.permute(0, 2, 1, 3)
|
||||
k = k.permute(0, 2, 1, 3)
|
||||
v = v.permute(0, 2, 1, 3)
|
||||
self.ab = self.ab.to(self.attention_biases.device)
|
||||
|
||||
attn = (q @ k.transpose(-2, -1)) * self.scale + (
|
||||
self.attention_biases[:, self.attention_bias_idxs] if self.training else self.ab
|
||||
)
|
||||
attn = attn.softmax(dim=-1)
|
||||
x = (attn @ v).transpose(1, 2).reshape(B, N, self.dh)
|
||||
return self.proj(x)
|
||||
|
||||
|
||||
class TinyViTBlock(nn.Module):
|
||||
"""TinyViT Block that applies self-attention and a local convolution to the input.
|
||||
|
||||
This block is a key component of the TinyViT architecture, combining self-attention mechanisms with local
|
||||
convolutions to process input features efficiently. It supports windowed attention for computational efficiency and
|
||||
includes residual connections.
|
||||
|
||||
Attributes:
|
||||
dim (int): The dimensionality of the input and output.
|
||||
input_resolution (tuple[int, int]): Spatial resolution of the input feature map.
|
||||
num_heads (int): Number of attention heads.
|
||||
window_size (int): Size of the attention window.
|
||||
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
|
||||
drop_path (nn.Module): Stochastic depth layer, identity function during inference.
|
||||
attn (Attention): Self-attention module.
|
||||
mlp (MLP): Multi-layer perceptron module.
|
||||
local_conv (Conv2d_BN): Depth-wise local convolution layer.
|
||||
|
||||
Examples:
|
||||
>>> input_tensor = torch.randn(1, 196, 192)
|
||||
>>> block = TinyViTBlock(dim=192, input_resolution=(14, 14), num_heads=3)
|
||||
>>> output = block(input_tensor)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 196, 192])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
input_resolution: tuple[int, int],
|
||||
num_heads: int,
|
||||
window_size: int = 7,
|
||||
mlp_ratio: float = 4.0,
|
||||
drop: float = 0.0,
|
||||
drop_path: float = 0.0,
|
||||
local_conv_size: int = 3,
|
||||
activation=nn.GELU,
|
||||
):
|
||||
"""Initialize a TinyViT block with self-attention and local convolution.
|
||||
|
||||
This block is a key component of the TinyViT architecture, combining self-attention mechanisms with local
|
||||
convolutions to process input features efficiently.
|
||||
|
||||
Args:
|
||||
dim (int): Dimensionality of the input and output features.
|
||||
input_resolution (tuple[int, int]): Spatial resolution of the input feature map (height, width).
|
||||
num_heads (int): Number of attention heads.
|
||||
window_size (int, optional): Size of the attention window. Must be greater than 0.
|
||||
mlp_ratio (float, optional): Ratio of MLP hidden dimension to embedding dimension.
|
||||
drop (float, optional): Dropout rate.
|
||||
drop_path (float, optional): Stochastic depth rate.
|
||||
local_conv_size (int, optional): Kernel size of the local convolution.
|
||||
activation (nn.Module): Activation function for MLP.
|
||||
"""
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.num_heads = num_heads
|
||||
assert window_size > 0, "window_size must be greater than 0"
|
||||
self.window_size = window_size
|
||||
self.mlp_ratio = mlp_ratio
|
||||
|
||||
# NOTE: `DropPath` is needed only for training.
|
||||
# self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
|
||||
self.drop_path = nn.Identity()
|
||||
|
||||
assert dim % num_heads == 0, "dim must be divisible by num_heads"
|
||||
head_dim = dim // num_heads
|
||||
|
||||
window_resolution = (window_size, window_size)
|
||||
self.attn = Attention(dim, head_dim, num_heads, attn_ratio=1, resolution=window_resolution)
|
||||
|
||||
mlp_hidden_dim = int(dim * mlp_ratio)
|
||||
mlp_activation = activation
|
||||
self.mlp = MLP(in_features=dim, hidden_features=mlp_hidden_dim, activation=mlp_activation, drop=drop)
|
||||
|
||||
pad = local_conv_size // 2
|
||||
self.local_conv = Conv2d_BN(dim, dim, ks=local_conv_size, stride=1, pad=pad, groups=dim)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply self-attention, local convolution, and MLP operations to the input tensor."""
|
||||
h, w = self.input_resolution
|
||||
b, hw, c = x.shape # batch, height*width, channels
|
||||
assert hw == h * w, "input feature has wrong size"
|
||||
res_x = x
|
||||
if h == self.window_size and w == self.window_size:
|
||||
x = self.attn(x)
|
||||
else:
|
||||
x = x.view(b, h, w, c)
|
||||
pad_b = (self.window_size - h % self.window_size) % self.window_size
|
||||
pad_r = (self.window_size - w % self.window_size) % self.window_size
|
||||
padding = pad_b > 0 or pad_r > 0
|
||||
if padding:
|
||||
x = F.pad(x, (0, 0, 0, pad_r, 0, pad_b))
|
||||
|
||||
pH, pW = h + pad_b, w + pad_r
|
||||
nH = pH // self.window_size
|
||||
nW = pW // self.window_size
|
||||
|
||||
# Window partition
|
||||
x = (
|
||||
x.view(b, nH, self.window_size, nW, self.window_size, c)
|
||||
.transpose(2, 3)
|
||||
.reshape(b * nH * nW, self.window_size * self.window_size, c)
|
||||
)
|
||||
x = self.attn(x)
|
||||
|
||||
# Window reverse
|
||||
x = x.view(b, nH, nW, self.window_size, self.window_size, c).transpose(2, 3).reshape(b, pH, pW, c)
|
||||
if padding:
|
||||
x = x[:, :h, :w].contiguous()
|
||||
|
||||
x = x.view(b, hw, c)
|
||||
|
||||
x = res_x + self.drop_path(x)
|
||||
x = x.transpose(1, 2).reshape(b, c, h, w)
|
||||
x = self.local_conv(x)
|
||||
x = x.view(b, c, hw).transpose(1, 2)
|
||||
|
||||
return x + self.drop_path(self.mlp(x))
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
"""Return a string representation of the TinyViTBlock's parameters.
|
||||
|
||||
This method provides a formatted string containing key information about the TinyViTBlock, including its
|
||||
dimension, input resolution, number of attention heads, window size, and MLP ratio.
|
||||
|
||||
Returns:
|
||||
(str): A formatted string containing the block's parameters.
|
||||
|
||||
Examples:
|
||||
>>> block = TinyViTBlock(dim=192, input_resolution=(14, 14), num_heads=3, window_size=7, mlp_ratio=4.0)
|
||||
>>> print(block.extra_repr())
|
||||
dim=192, input_resolution=(14, 14), num_heads=3, window_size=7, mlp_ratio=4.0
|
||||
"""
|
||||
return (
|
||||
f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, "
|
||||
f"window_size={self.window_size}, mlp_ratio={self.mlp_ratio}"
|
||||
)
|
||||
|
||||
|
||||
class BasicLayer(nn.Module):
|
||||
"""A basic TinyViT layer for one stage in a TinyViT architecture.
|
||||
|
||||
This class represents a single layer in the TinyViT model, consisting of multiple TinyViT blocks and an optional
|
||||
downsampling operation. It processes features at a specific resolution and dimensionality within the overall
|
||||
architecture.
|
||||
|
||||
Attributes:
|
||||
dim (int): The dimensionality of the input and output features.
|
||||
input_resolution (tuple[int, int]): Spatial resolution of the input feature map.
|
||||
depth (int): Number of TinyViT blocks in this layer.
|
||||
use_checkpoint (bool): Whether to use gradient checkpointing to save memory.
|
||||
blocks (nn.ModuleList): List of TinyViT blocks that make up this layer.
|
||||
downsample (nn.Module | None): Downsample layer at the end of the layer, if specified.
|
||||
|
||||
Examples:
|
||||
>>> input_tensor = torch.randn(1, 3136, 192)
|
||||
>>> layer = BasicLayer(dim=192, input_resolution=(56, 56), depth=2, num_heads=3, window_size=7)
|
||||
>>> output = layer(input_tensor)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 784, 384])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
input_resolution: tuple[int, int],
|
||||
depth: int,
|
||||
num_heads: int,
|
||||
window_size: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
drop: float = 0.0,
|
||||
drop_path: float | list[float] = 0.0,
|
||||
downsample: nn.Module | None = None,
|
||||
use_checkpoint: bool = False,
|
||||
local_conv_size: int = 3,
|
||||
activation=nn.GELU,
|
||||
out_dim: int | None = None,
|
||||
):
|
||||
"""Initialize a BasicLayer in the TinyViT architecture.
|
||||
|
||||
This layer consists of multiple TinyViT blocks and an optional downsampling operation. It is designed to process
|
||||
feature maps at a specific resolution and dimensionality within the TinyViT model.
|
||||
|
||||
Args:
|
||||
dim (int): Dimensionality of the input and output features.
|
||||
input_resolution (tuple[int, int]): Spatial resolution of the input feature map (height, width).
|
||||
depth (int): Number of TinyViT blocks in this layer.
|
||||
num_heads (int): Number of attention heads in each TinyViT block.
|
||||
window_size (int): Size of the local window for attention computation.
|
||||
mlp_ratio (float, optional): Ratio of MLP hidden dimension to embedding dimension.
|
||||
drop (float, optional): Dropout rate.
|
||||
drop_path (float | list[float], optional): Stochastic depth rate. Can be a float or a list of floats for
|
||||
each block.
|
||||
downsample (nn.Module | None, optional): Downsampling layer at the end of the layer. None to skip
|
||||
downsampling.
|
||||
use_checkpoint (bool, optional): Whether to use gradient checkpointing to save memory.
|
||||
local_conv_size (int, optional): Kernel size for the local convolution in each TinyViT block.
|
||||
activation (nn.Module): Activation function used in the MLP.
|
||||
out_dim (int | None, optional): Output dimension after downsampling. None means it will be the same as dim.
|
||||
"""
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.input_resolution = input_resolution
|
||||
self.depth = depth
|
||||
self.use_checkpoint = use_checkpoint
|
||||
|
||||
# Build blocks
|
||||
self.blocks = nn.ModuleList(
|
||||
[
|
||||
TinyViTBlock(
|
||||
dim=dim,
|
||||
input_resolution=input_resolution,
|
||||
num_heads=num_heads,
|
||||
window_size=window_size,
|
||||
mlp_ratio=mlp_ratio,
|
||||
drop=drop,
|
||||
drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
|
||||
local_conv_size=local_conv_size,
|
||||
activation=activation,
|
||||
)
|
||||
for i in range(depth)
|
||||
]
|
||||
)
|
||||
|
||||
# Patch merging layer
|
||||
self.downsample = (
|
||||
None
|
||||
if downsample is None
|
||||
else downsample(input_resolution, dim=dim, out_dim=out_dim, activation=activation)
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Process input through TinyViT blocks and optional downsampling."""
|
||||
for blk in self.blocks:
|
||||
x = torch.utils.checkpoint(blk, x) if self.use_checkpoint else blk(x) # warn: checkpoint is slow import
|
||||
return x if self.downsample is None else self.downsample(x)
|
||||
|
||||
def extra_repr(self) -> str:
|
||||
"""Return a string with the layer's parameters for printing."""
|
||||
return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"
|
||||
|
||||
|
||||
class TinyViT(nn.Module):
|
||||
"""TinyViT: A compact vision transformer architecture for efficient image classification and feature extraction.
|
||||
|
||||
This class implements the TinyViT model, which combines elements of vision transformers and convolutional neural
|
||||
networks for improved efficiency and performance on vision tasks. It features hierarchical processing with patch
|
||||
embedding, multiple stages of attention and convolution blocks, and a feature refinement neck.
|
||||
|
||||
Attributes:
|
||||
img_size (int): Input image size.
|
||||
num_classes (int): Number of classification classes.
|
||||
depths (tuple[int, int, int, int]): Number of blocks in each stage.
|
||||
num_layers (int): Total number of layers in the network.
|
||||
mlp_ratio (float): Ratio of MLP hidden dimension to embedding dimension.
|
||||
patch_embed (PatchEmbed): Module for patch embedding.
|
||||
patches_resolution (tuple[int, int]): Resolution of embedded patches.
|
||||
layers (nn.ModuleList): List of network layers.
|
||||
norm_head (nn.LayerNorm): Layer normalization for the classifier head.
|
||||
head (nn.Linear): Linear layer for final classification.
|
||||
neck (nn.Sequential): Neck module for feature refinement.
|
||||
|
||||
Examples:
|
||||
>>> model = TinyViT(img_size=224, num_classes=1000)
|
||||
>>> x = torch.randn(1, 3, 224, 224)
|
||||
>>> features = model.forward_features(x)
|
||||
>>> print(features.shape)
|
||||
torch.Size([1, 256, 56, 56])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
img_size: int = 224,
|
||||
in_chans: int = 3,
|
||||
num_classes: int = 1000,
|
||||
embed_dims: tuple[int, int, int, int] = (96, 192, 384, 768),
|
||||
depths: tuple[int, int, int, int] = (2, 2, 6, 2),
|
||||
num_heads: tuple[int, int, int, int] = (3, 6, 12, 24),
|
||||
window_sizes: tuple[int, int, int, int] = (7, 7, 14, 7),
|
||||
mlp_ratio: float = 4.0,
|
||||
drop_rate: float = 0.0,
|
||||
drop_path_rate: float = 0.1,
|
||||
use_checkpoint: bool = False,
|
||||
mbconv_expand_ratio: float = 4.0,
|
||||
local_conv_size: int = 3,
|
||||
layer_lr_decay: float = 1.0,
|
||||
):
|
||||
"""Initialize the TinyViT model.
|
||||
|
||||
This constructor sets up the TinyViT architecture, including patch embedding, multiple layers of attention and
|
||||
convolution blocks, and a classification head.
|
||||
|
||||
Args:
|
||||
img_size (int, optional): Size of the input image.
|
||||
in_chans (int, optional): Number of input channels.
|
||||
num_classes (int, optional): Number of classes for classification.
|
||||
embed_dims (tuple[int, int, int, int], optional): Embedding dimensions for each stage.
|
||||
depths (tuple[int, int, int, int], optional): Number of blocks in each stage.
|
||||
num_heads (tuple[int, int, int, int], optional): Number of attention heads in each stage.
|
||||
window_sizes (tuple[int, int, int, int], optional): Window sizes for each stage.
|
||||
mlp_ratio (float, optional): Ratio of MLP hidden dim to embedding dim.
|
||||
drop_rate (float, optional): Dropout rate.
|
||||
drop_path_rate (float, optional): Stochastic depth rate.
|
||||
use_checkpoint (bool, optional): Whether to use checkpointing to save memory.
|
||||
mbconv_expand_ratio (float, optional): Expansion ratio for MBConv layer.
|
||||
local_conv_size (int, optional): Kernel size for local convolutions.
|
||||
layer_lr_decay (float, optional): Layer-wise learning rate decay factor.
|
||||
"""
|
||||
super().__init__()
|
||||
self.img_size = img_size
|
||||
self.num_classes = num_classes
|
||||
self.depths = depths
|
||||
self.num_layers = len(depths)
|
||||
self.mlp_ratio = mlp_ratio
|
||||
|
||||
activation = nn.GELU
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
in_chans=in_chans, embed_dim=embed_dims[0], resolution=img_size, activation=activation
|
||||
)
|
||||
|
||||
patches_resolution = self.patch_embed.patches_resolution
|
||||
self.patches_resolution = patches_resolution
|
||||
|
||||
# Stochastic depth
|
||||
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
|
||||
|
||||
# Build layers
|
||||
self.layers = nn.ModuleList()
|
||||
for i_layer in range(self.num_layers):
|
||||
kwargs = dict(
|
||||
dim=embed_dims[i_layer],
|
||||
input_resolution=(
|
||||
patches_resolution[0] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
|
||||
patches_resolution[1] // (2 ** (i_layer - 1 if i_layer == 3 else i_layer)),
|
||||
),
|
||||
# input_resolution=(patches_resolution[0] // (2 ** i_layer),
|
||||
# patches_resolution[1] // (2 ** i_layer)),
|
||||
depth=depths[i_layer],
|
||||
drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])],
|
||||
downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
|
||||
use_checkpoint=use_checkpoint,
|
||||
out_dim=embed_dims[min(i_layer + 1, len(embed_dims) - 1)],
|
||||
activation=activation,
|
||||
)
|
||||
if i_layer == 0:
|
||||
layer = ConvLayer(conv_expand_ratio=mbconv_expand_ratio, **kwargs)
|
||||
else:
|
||||
layer = BasicLayer(
|
||||
num_heads=num_heads[i_layer],
|
||||
window_size=window_sizes[i_layer],
|
||||
mlp_ratio=self.mlp_ratio,
|
||||
drop=drop_rate,
|
||||
local_conv_size=local_conv_size,
|
||||
**kwargs,
|
||||
)
|
||||
self.layers.append(layer)
|
||||
|
||||
# Classifier head
|
||||
self.norm_head = nn.LayerNorm(embed_dims[-1])
|
||||
self.head = nn.Linear(embed_dims[-1], num_classes) if num_classes > 0 else torch.nn.Identity()
|
||||
|
||||
# Init weights
|
||||
self.apply(self._init_weights)
|
||||
self.set_layer_lr_decay(layer_lr_decay)
|
||||
self.neck = nn.Sequential(
|
||||
nn.Conv2d(
|
||||
embed_dims[-1],
|
||||
256,
|
||||
kernel_size=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(256),
|
||||
nn.Conv2d(
|
||||
256,
|
||||
256,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=False,
|
||||
),
|
||||
LayerNorm2d(256),
|
||||
)
|
||||
|
||||
def set_layer_lr_decay(self, layer_lr_decay: float):
|
||||
"""Set layer-wise learning rate decay for the TinyViT model based on depth."""
|
||||
decay_rate = layer_lr_decay
|
||||
|
||||
# Layers -> blocks (depth)
|
||||
depth = sum(self.depths)
|
||||
lr_scales = [decay_rate ** (depth - i - 1) for i in range(depth)]
|
||||
|
||||
def _set_lr_scale(m, scale):
|
||||
"""Set the learning rate scale for each layer in the model based on the layer's depth."""
|
||||
for p in m.parameters():
|
||||
p.lr_scale = scale
|
||||
|
||||
self.patch_embed.apply(lambda x: _set_lr_scale(x, lr_scales[0]))
|
||||
i = 0
|
||||
for layer in self.layers:
|
||||
for block in layer.blocks:
|
||||
block.apply(lambda x: _set_lr_scale(x, lr_scales[i]))
|
||||
i += 1
|
||||
if layer.downsample is not None:
|
||||
layer.downsample.apply(lambda x: _set_lr_scale(x, lr_scales[i - 1]))
|
||||
assert i == depth
|
||||
for m in {self.norm_head, self.head}:
|
||||
m.apply(lambda x: _set_lr_scale(x, lr_scales[-1]))
|
||||
|
||||
for k, p in self.named_parameters():
|
||||
p.param_name = k
|
||||
|
||||
def _check_lr_scale(m):
|
||||
"""Check if the learning rate scale attribute is present in module's parameters."""
|
||||
for p in m.parameters():
|
||||
assert hasattr(p, "lr_scale"), p.param_name
|
||||
|
||||
self.apply(_check_lr_scale)
|
||||
|
||||
@staticmethod
|
||||
def _init_weights(m):
|
||||
"""Initialize weights for linear and normalization layers in the TinyViT model."""
|
||||
if isinstance(m, nn.Linear):
|
||||
# NOTE: This initialization is needed only for training.
|
||||
# trunc_normal_(m.weight, std=.02)
|
||||
if m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
elif isinstance(m, nn.LayerNorm):
|
||||
nn.init.constant_(m.bias, 0)
|
||||
nn.init.constant_(m.weight, 1.0)
|
||||
|
||||
@torch.jit.ignore
|
||||
def no_weight_decay_keywords(self):
|
||||
"""Return a set of keywords for parameters that should not use weight decay."""
|
||||
return {"attention_biases"}
|
||||
|
||||
def forward_features(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Process input through feature extraction layers, returning spatial features."""
|
||||
x = self.patch_embed(x) # x input is (N, C, H, W)
|
||||
|
||||
x = self.layers[0](x)
|
||||
start_i = 1
|
||||
|
||||
for i in range(start_i, len(self.layers)):
|
||||
layer = self.layers[i]
|
||||
x = layer(x)
|
||||
batch, _, channel = x.shape
|
||||
x = x.view(batch, self.patches_resolution[0] // 4, self.patches_resolution[1] // 4, channel)
|
||||
x = x.permute(0, 3, 1, 2)
|
||||
return self.neck(x)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""Perform the forward pass through the TinyViT model, extracting features from the input image."""
|
||||
return self.forward_features(x)
|
||||
|
||||
def set_imgsz(self, imgsz: list[int] = [1024, 1024]):
|
||||
"""Set image size to make model compatible with different image sizes."""
|
||||
imgsz = [s // 4 for s in imgsz]
|
||||
self.patches_resolution = imgsz
|
||||
for i, layer in enumerate(self.layers):
|
||||
input_resolution = (
|
||||
imgsz[0] // (2 ** (i - 1 if i == 3 else i)),
|
||||
imgsz[1] // (2 ** (i - 1 if i == 3 else i)),
|
||||
)
|
||||
layer.input_resolution = input_resolution
|
||||
if layer.downsample is not None:
|
||||
layer.downsample.input_resolution = input_resolution
|
||||
if isinstance(layer, BasicLayer):
|
||||
for b in layer.blocks:
|
||||
b.input_resolution = input_resolution
|
||||
344
ultralytics/models/sam/modules/transformer.py
Executable file
344
ultralytics/models/sam/modules/transformer.py
Executable file
@@ -0,0 +1,344 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
from ultralytics.nn.modules import MLPBlock
|
||||
|
||||
|
||||
class TwoWayTransformer(nn.Module):
|
||||
"""A Two-Way Transformer module for simultaneous attention to image and query points.
|
||||
|
||||
This class implements a specialized transformer decoder that attends to an input image using queries with supplied
|
||||
positional embeddings. It's useful for tasks like object detection, image segmentation, and point cloud processing.
|
||||
|
||||
Attributes:
|
||||
depth (int): Number of layers in the transformer.
|
||||
embedding_dim (int): Channel dimension for input embeddings.
|
||||
num_heads (int): Number of heads for multihead attention.
|
||||
mlp_dim (int): Internal channel dimension for the MLP block.
|
||||
layers (nn.ModuleList): List of TwoWayAttentionBlock layers composing the transformer.
|
||||
final_attn_token_to_image (Attention): Final attention layer from queries to image.
|
||||
norm_final_attn (nn.LayerNorm): Layer normalization applied to final queries.
|
||||
|
||||
Methods:
|
||||
forward: Process image and point embeddings through the transformer.
|
||||
|
||||
Examples:
|
||||
>>> transformer = TwoWayTransformer(depth=6, embedding_dim=256, num_heads=8, mlp_dim=2048)
|
||||
>>> image_embedding = torch.randn(1, 256, 32, 32)
|
||||
>>> image_pe = torch.randn(1, 256, 32, 32)
|
||||
>>> point_embedding = torch.randn(1, 100, 256)
|
||||
>>> output_queries, output_image = transformer(image_embedding, image_pe, point_embedding)
|
||||
>>> print(output_queries.shape, output_image.shape)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
depth: int,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int,
|
||||
activation: type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
) -> None:
|
||||
"""Initialize a Two-Way Transformer for simultaneous attention to image and query points.
|
||||
|
||||
Args:
|
||||
depth (int): Number of layers in the transformer.
|
||||
embedding_dim (int): Channel dimension for input embeddings.
|
||||
num_heads (int): Number of heads for multihead attention. Must divide embedding_dim.
|
||||
mlp_dim (int): Internal channel dimension for the MLP block.
|
||||
activation (type[nn.Module], optional): Activation function to use in the MLP block.
|
||||
attention_downsample_rate (int, optional): Downsampling rate for attention mechanism.
|
||||
"""
|
||||
super().__init__()
|
||||
self.depth = depth
|
||||
self.embedding_dim = embedding_dim
|
||||
self.num_heads = num_heads
|
||||
self.mlp_dim = mlp_dim
|
||||
self.layers = nn.ModuleList()
|
||||
|
||||
for i in range(depth):
|
||||
self.layers.append(
|
||||
TwoWayAttentionBlock(
|
||||
embedding_dim=embedding_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_dim=mlp_dim,
|
||||
activation=activation,
|
||||
attention_downsample_rate=attention_downsample_rate,
|
||||
skip_first_layer_pe=(i == 0),
|
||||
)
|
||||
)
|
||||
|
||||
self.final_attn_token_to_image = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
|
||||
self.norm_final_attn = nn.LayerNorm(embedding_dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
image_embedding: torch.Tensor,
|
||||
image_pe: torch.Tensor,
|
||||
point_embedding: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Process image and point embeddings through the Two-Way Transformer.
|
||||
|
||||
Args:
|
||||
image_embedding (torch.Tensor): Image to attend to, with shape (B, embedding_dim, H, W).
|
||||
image_pe (torch.Tensor): Positional encoding to add to the image, with same shape as image_embedding.
|
||||
point_embedding (torch.Tensor): Embedding to add to query points, with shape (B, N_points, embedding_dim).
|
||||
|
||||
Returns:
|
||||
queries (torch.Tensor): Processed point embeddings with shape (B, N_points, embedding_dim).
|
||||
keys (torch.Tensor): Processed image embeddings with shape (B, H*W, embedding_dim).
|
||||
"""
|
||||
# BxCxHxW -> BxHWxC == B x N_image_tokens x C
|
||||
image_embedding = image_embedding.flatten(2).permute(0, 2, 1)
|
||||
image_pe = image_pe.flatten(2).permute(0, 2, 1)
|
||||
|
||||
# Prepare queries
|
||||
queries = point_embedding
|
||||
keys = image_embedding
|
||||
|
||||
# Apply transformer blocks and final layernorm
|
||||
for layer in self.layers:
|
||||
queries, keys = layer(
|
||||
queries=queries,
|
||||
keys=keys,
|
||||
query_pe=point_embedding,
|
||||
key_pe=image_pe,
|
||||
)
|
||||
|
||||
# Apply the final attention layer from the points to the image
|
||||
q = queries + point_embedding
|
||||
k = keys + image_pe
|
||||
attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm_final_attn(queries)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class TwoWayAttentionBlock(nn.Module):
|
||||
"""A two-way attention block for simultaneous attention to image and query points.
|
||||
|
||||
This class implements a specialized transformer block with four main layers: self-attention on sparse inputs,
|
||||
cross-attention of sparse inputs to dense inputs, MLP block on sparse inputs, and cross-attention of dense inputs to
|
||||
sparse inputs.
|
||||
|
||||
Attributes:
|
||||
self_attn (Attention): Self-attention layer for queries.
|
||||
norm1 (nn.LayerNorm): Layer normalization after self-attention.
|
||||
cross_attn_token_to_image (Attention): Cross-attention layer from queries to keys.
|
||||
norm2 (nn.LayerNorm): Layer normalization after token-to-image attention.
|
||||
mlp (MLPBlock): MLP block for transforming query embeddings.
|
||||
norm3 (nn.LayerNorm): Layer normalization after MLP block.
|
||||
norm4 (nn.LayerNorm): Layer normalization after image-to-token attention.
|
||||
cross_attn_image_to_token (Attention): Cross-attention layer from keys to queries.
|
||||
skip_first_layer_pe (bool): Whether to skip positional encoding in the first layer.
|
||||
|
||||
Methods:
|
||||
forward: Apply self-attention and cross-attention to queries and keys.
|
||||
|
||||
Examples:
|
||||
>>> embedding_dim, num_heads = 256, 8
|
||||
>>> block = TwoWayAttentionBlock(embedding_dim, num_heads)
|
||||
>>> queries = torch.randn(1, 100, embedding_dim)
|
||||
>>> keys = torch.randn(1, 1000, embedding_dim)
|
||||
>>> query_pe = torch.randn(1, 100, embedding_dim)
|
||||
>>> key_pe = torch.randn(1, 1000, embedding_dim)
|
||||
>>> processed_queries, processed_keys = block(queries, keys, query_pe, key_pe)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
mlp_dim: int = 2048,
|
||||
activation: type[nn.Module] = nn.ReLU,
|
||||
attention_downsample_rate: int = 2,
|
||||
skip_first_layer_pe: bool = False,
|
||||
) -> None:
|
||||
"""Initialize a TwoWayAttentionBlock for simultaneous attention to image and query points.
|
||||
|
||||
This block implements a specialized transformer layer with four main components: self-attention on sparse
|
||||
inputs, cross-attention of sparse inputs to dense inputs, MLP block on sparse inputs, and cross-attention of
|
||||
dense inputs to sparse inputs.
|
||||
|
||||
Args:
|
||||
embedding_dim (int): Channel dimension of the embeddings.
|
||||
num_heads (int): Number of attention heads in the attention layers.
|
||||
mlp_dim (int, optional): Hidden dimension of the MLP block.
|
||||
activation (type[nn.Module], optional): Activation function for the MLP block.
|
||||
attention_downsample_rate (int, optional): Downsampling rate for the attention mechanism.
|
||||
skip_first_layer_pe (bool, optional): Whether to skip positional encoding in the first layer.
|
||||
"""
|
||||
super().__init__()
|
||||
self.self_attn = Attention(embedding_dim, num_heads)
|
||||
self.norm1 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.cross_attn_token_to_image = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
|
||||
self.norm2 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.mlp = MLPBlock(embedding_dim, mlp_dim, activation)
|
||||
self.norm3 = nn.LayerNorm(embedding_dim)
|
||||
|
||||
self.norm4 = nn.LayerNorm(embedding_dim)
|
||||
self.cross_attn_image_to_token = Attention(embedding_dim, num_heads, downsample_rate=attention_downsample_rate)
|
||||
|
||||
self.skip_first_layer_pe = skip_first_layer_pe
|
||||
|
||||
def forward(
|
||||
self, queries: torch.Tensor, keys: torch.Tensor, query_pe: torch.Tensor, key_pe: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Apply two-way attention to process query and key embeddings in a transformer block.
|
||||
|
||||
Args:
|
||||
queries (torch.Tensor): Query embeddings with shape (B, N_queries, embedding_dim).
|
||||
keys (torch.Tensor): Key embeddings with shape (B, N_keys, embedding_dim).
|
||||
query_pe (torch.Tensor): Positional encodings for queries with same shape as queries.
|
||||
key_pe (torch.Tensor): Positional encodings for keys with same shape as keys.
|
||||
|
||||
Returns:
|
||||
queries (torch.Tensor): Processed query embeddings with shape (B, N_queries, embedding_dim).
|
||||
keys (torch.Tensor): Processed key embeddings with shape (B, N_keys, embedding_dim).
|
||||
"""
|
||||
# Self attention block
|
||||
if self.skip_first_layer_pe:
|
||||
queries = self.self_attn(q=queries, k=queries, v=queries)
|
||||
else:
|
||||
q = queries + query_pe
|
||||
attn_out = self.self_attn(q=q, k=q, v=queries)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm1(queries)
|
||||
|
||||
# Cross attention block, tokens attending to image embedding
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys)
|
||||
queries = queries + attn_out
|
||||
queries = self.norm2(queries)
|
||||
|
||||
# MLP block
|
||||
mlp_out = self.mlp(queries)
|
||||
queries = queries + mlp_out
|
||||
queries = self.norm3(queries)
|
||||
|
||||
# Cross attention block, image embedding attending to tokens
|
||||
q = queries + query_pe
|
||||
k = keys + key_pe
|
||||
attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries)
|
||||
keys = keys + attn_out
|
||||
keys = self.norm4(keys)
|
||||
|
||||
return queries, keys
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""An attention layer with downscaling capability for embedding size after projection.
|
||||
|
||||
This class implements a multi-head attention mechanism with the option to downsample the internal dimension of
|
||||
queries, keys, and values.
|
||||
|
||||
Attributes:
|
||||
embedding_dim (int): Dimensionality of input embeddings.
|
||||
kv_in_dim (int): Dimensionality of key and value inputs.
|
||||
internal_dim (int): Internal dimension after downsampling.
|
||||
num_heads (int): Number of attention heads.
|
||||
q_proj (nn.Linear): Linear projection for queries.
|
||||
k_proj (nn.Linear): Linear projection for keys.
|
||||
v_proj (nn.Linear): Linear projection for values.
|
||||
out_proj (nn.Linear): Linear projection for output.
|
||||
|
||||
Methods:
|
||||
_separate_heads: Separate input tensor into attention heads.
|
||||
_recombine_heads: Recombine separated attention heads.
|
||||
forward: Compute attention output for given query, key, and value tensors.
|
||||
|
||||
Examples:
|
||||
>>> attn = Attention(embedding_dim=256, num_heads=8, downsample_rate=2)
|
||||
>>> q = torch.randn(1, 100, 256)
|
||||
>>> k = v = torch.randn(1, 50, 256)
|
||||
>>> output = attn(q, k, v)
|
||||
>>> print(output.shape)
|
||||
torch.Size([1, 100, 256])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int,
|
||||
num_heads: int,
|
||||
downsample_rate: int = 1,
|
||||
kv_in_dim: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Attention module with specified dimensions and settings.
|
||||
|
||||
Args:
|
||||
embedding_dim (int): Dimensionality of input embeddings.
|
||||
num_heads (int): Number of attention heads.
|
||||
downsample_rate (int, optional): Factor by which internal dimensions are downsampled.
|
||||
kv_in_dim (int | None, optional): Dimensionality of key and value inputs. If None, uses embedding_dim.
|
||||
|
||||
Raises:
|
||||
AssertionError: If num_heads does not evenly divide the internal dim (embedding_dim / downsample_rate).
|
||||
"""
|
||||
super().__init__()
|
||||
self.embedding_dim = embedding_dim
|
||||
self.kv_in_dim = kv_in_dim if kv_in_dim is not None else embedding_dim
|
||||
self.internal_dim = embedding_dim // downsample_rate
|
||||
self.num_heads = num_heads
|
||||
assert self.internal_dim % num_heads == 0, "num_heads must divide embedding_dim."
|
||||
|
||||
self.q_proj = nn.Linear(embedding_dim, self.internal_dim)
|
||||
self.k_proj = nn.Linear(self.kv_in_dim, self.internal_dim)
|
||||
self.v_proj = nn.Linear(self.kv_in_dim, self.internal_dim)
|
||||
self.out_proj = nn.Linear(self.internal_dim, embedding_dim)
|
||||
|
||||
@staticmethod
|
||||
def _separate_heads(x: torch.Tensor, num_heads: int) -> torch.Tensor:
|
||||
"""Separate the input tensor into the specified number of attention heads."""
|
||||
b, n, c = x.shape
|
||||
x = x.reshape(b, n, num_heads, c // num_heads)
|
||||
return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head
|
||||
|
||||
@staticmethod
|
||||
def _recombine_heads(x: Tensor) -> Tensor:
|
||||
"""Recombine separated attention heads into a single tensor."""
|
||||
b, n_heads, n_tokens, c_per_head = x.shape
|
||||
x = x.transpose(1, 2)
|
||||
return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C
|
||||
|
||||
def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
||||
"""Apply multi-head attention to query, key, and value tensors with optional downsampling.
|
||||
|
||||
Args:
|
||||
q (torch.Tensor): Query tensor with shape (B, N_q, embedding_dim).
|
||||
k (torch.Tensor): Key tensor with shape (B, N_k, kv_in_dim).
|
||||
v (torch.Tensor): Value tensor with shape (B, N_k, kv_in_dim).
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Output tensor after attention with shape (B, N_q, embedding_dim).
|
||||
"""
|
||||
# Input projections
|
||||
q = self.q_proj(q)
|
||||
k = self.k_proj(k)
|
||||
v = self.v_proj(v)
|
||||
|
||||
# Separate into heads
|
||||
q = self._separate_heads(q, self.num_heads)
|
||||
k = self._separate_heads(k, self.num_heads)
|
||||
v = self._separate_heads(v, self.num_heads)
|
||||
|
||||
# Attention
|
||||
_, _, _, c_per_head = q.shape
|
||||
attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens
|
||||
attn = attn / math.sqrt(c_per_head)
|
||||
attn = torch.softmax(attn, dim=-1)
|
||||
|
||||
# Get output
|
||||
out = attn @ v
|
||||
out = self._recombine_heads(out)
|
||||
return self.out_proj(out)
|
||||
512
ultralytics/models/sam/modules/utils.py
Executable file
512
ultralytics/models/sam/modules/utils.py
Executable file
@@ -0,0 +1,512 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def select_closest_cond_frames(frame_idx: int, cond_frame_outputs: dict[int, Any], max_cond_frame_num: int):
|
||||
"""Select the closest conditioning frames to a given frame index.
|
||||
|
||||
Args:
|
||||
frame_idx (int): Current frame index.
|
||||
cond_frame_outputs (dict[int, Any]): Dictionary of conditioning frame outputs keyed by frame indices.
|
||||
max_cond_frame_num (int): Maximum number of conditioning frames to select.
|
||||
|
||||
Returns:
|
||||
selected_outputs (dict[int, Any]): Selected items from cond_frame_outputs.
|
||||
unselected_outputs (dict[int, Any]): Items not selected from cond_frame_outputs.
|
||||
|
||||
Examples:
|
||||
>>> frame_idx = 5
|
||||
>>> cond_frame_outputs = {1: "a", 3: "b", 7: "c", 9: "d"}
|
||||
>>> max_cond_frame_num = 2
|
||||
>>> selected, unselected = select_closest_cond_frames(frame_idx, cond_frame_outputs, max_cond_frame_num)
|
||||
>>> print(selected)
|
||||
{3: 'b', 7: 'c'}
|
||||
>>> print(unselected)
|
||||
{1: 'a', 9: 'd'}
|
||||
"""
|
||||
if max_cond_frame_num == -1 or len(cond_frame_outputs) <= max_cond_frame_num:
|
||||
selected_outputs = cond_frame_outputs
|
||||
unselected_outputs = {}
|
||||
else:
|
||||
assert max_cond_frame_num >= 2, "we should allow using 2+ conditioning frames"
|
||||
selected_outputs = {}
|
||||
|
||||
# The closest conditioning frame before `frame_idx` (if any)
|
||||
idx_before = max((t for t in cond_frame_outputs if t < frame_idx), default=None)
|
||||
if idx_before is not None:
|
||||
selected_outputs[idx_before] = cond_frame_outputs[idx_before]
|
||||
|
||||
# The closest conditioning frame after `frame_idx` (if any)
|
||||
idx_after = min((t for t in cond_frame_outputs if t >= frame_idx), default=None)
|
||||
if idx_after is not None:
|
||||
selected_outputs[idx_after] = cond_frame_outputs[idx_after]
|
||||
|
||||
# Add other temporally closest conditioning frames until reaching a total
|
||||
# of `max_cond_frame_num` conditioning frames.
|
||||
num_remain = max_cond_frame_num - len(selected_outputs)
|
||||
inds_remain = sorted(
|
||||
(t for t in cond_frame_outputs if t not in selected_outputs),
|
||||
key=lambda x: abs(x - frame_idx),
|
||||
)[:num_remain]
|
||||
selected_outputs.update((t, cond_frame_outputs[t]) for t in inds_remain)
|
||||
unselected_outputs = {t: v for t, v in cond_frame_outputs.items() if t not in selected_outputs}
|
||||
|
||||
return selected_outputs, unselected_outputs
|
||||
|
||||
|
||||
def get_1d_sine_pe(pos_inds: torch.Tensor, dim: int, temperature: float = 10000):
|
||||
"""Generate 1D sinusoidal positional embeddings for given positions and dimensions.
|
||||
|
||||
Args:
|
||||
pos_inds (torch.Tensor): Position indices for which to generate embeddings.
|
||||
dim (int): Dimension of the positional embeddings. Should be an even number.
|
||||
temperature (float, optional): Scaling factor for the frequency of the sinusoidal functions.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Sinusoidal positional embeddings with shape (pos_inds.shape, dim).
|
||||
|
||||
Examples:
|
||||
>>> pos = torch.tensor([0, 1, 2, 3])
|
||||
>>> embeddings = get_1d_sine_pe(pos, 128)
|
||||
>>> embeddings.shape
|
||||
torch.Size([4, 128])
|
||||
"""
|
||||
pe_dim = dim // 2
|
||||
dim_t = torch.arange(pe_dim, dtype=pos_inds.dtype, device=pos_inds.device)
|
||||
dim_t = temperature ** (2 * (dim_t // 2) / pe_dim)
|
||||
|
||||
pos_embed = pos_inds.unsqueeze(-1) / dim_t
|
||||
pos_embed = torch.cat([pos_embed.sin(), pos_embed.cos()], dim=-1)
|
||||
return pos_embed
|
||||
|
||||
|
||||
def init_t_xy(end_x: int, end_y: int, scale: float = 1.0, offset: int = 0):
|
||||
"""Initialize 1D and 2D coordinate tensors for a grid of specified dimensions.
|
||||
|
||||
This function creates coordinate tensors for a grid with dimensions end_x × end_y. It generates a linear index
|
||||
tensor and corresponding x and y coordinate tensors.
|
||||
|
||||
Args:
|
||||
end_x (int): Width of the grid (number of columns).
|
||||
end_y (int): Height of the grid (number of rows).
|
||||
scale (float): Scaling factor to apply to the coordinates.
|
||||
offset (int): Offset to add to the coordinates.
|
||||
|
||||
Returns:
|
||||
t_x (torch.Tensor): X-coordinates for each position, with shape (end_x * end_y).
|
||||
t_y (torch.Tensor): Y-coordinates for each position, with shape (end_x * end_y).
|
||||
|
||||
Examples:
|
||||
>>> t_x, t_y = init_t_xy(3, 2)
|
||||
>>> print(t_x)
|
||||
tensor([0., 1., 2., 0., 1., 2.])
|
||||
>>> print(t_y)
|
||||
tensor([0., 0., 0., 1., 1., 1.])
|
||||
"""
|
||||
t = torch.arange(end_x * end_y, dtype=torch.float32)
|
||||
t_x = (t % end_x).float()
|
||||
t_y = torch.div(t, end_x, rounding_mode="floor").float()
|
||||
return t_x * scale + offset, t_y * scale + offset
|
||||
|
||||
|
||||
def compute_axial_cis(dim: int, end_x: int, end_y: int, theta: float = 10000.0, scale_pos: float = 1.0):
|
||||
"""Compute axial complex exponential positional encodings for 2D spatial positions in a grid.
|
||||
|
||||
This function generates complex exponential positional encodings for a 2D grid of spatial positions, using separate
|
||||
frequency components for the x and y dimensions.
|
||||
|
||||
Args:
|
||||
dim (int): Dimension of the positional encoding.
|
||||
end_x (int): Width of the 2D grid.
|
||||
end_y (int): Height of the 2D grid.
|
||||
theta (float, optional): Scaling factor for frequency computation.
|
||||
scale_pos (float, optional): Scaling factor for position coordinates.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Complex exponential positional encodings with shape (end_x*end_y, dim//2).
|
||||
|
||||
Examples:
|
||||
>>> dim, end_x, end_y = 128, 8, 8
|
||||
>>> freqs_cis = compute_axial_cis(dim, end_x, end_y)
|
||||
>>> freqs_cis.shape
|
||||
torch.Size([64, 64])
|
||||
"""
|
||||
freqs_x = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))
|
||||
freqs_y = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim))
|
||||
|
||||
t_x, t_y = init_t_xy(end_x, end_y, scale=scale_pos)
|
||||
freqs_x = torch.outer(t_x, freqs_x)
|
||||
freqs_y = torch.outer(t_y, freqs_y)
|
||||
freqs_cis_x = torch.polar(torch.ones_like(freqs_x), freqs_x)
|
||||
freqs_cis_y = torch.polar(torch.ones_like(freqs_y), freqs_y)
|
||||
return torch.cat([freqs_cis_x, freqs_cis_y], dim=-1)
|
||||
|
||||
|
||||
def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
|
||||
"""Reshape frequency tensor for broadcasting with input tensor.
|
||||
|
||||
Reshapes a frequency tensor to ensure dimensional compatibility for broadcasting with an input tensor. This function
|
||||
is typically used in positional encoding operations.
|
||||
|
||||
Args:
|
||||
freqs_cis (torch.Tensor): Frequency tensor with shape matching the last two dimensions of x.
|
||||
x (torch.Tensor): Input tensor to broadcast with.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Reshaped frequency tensor ready for broadcasting with the input tensor.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the shape of freqs_cis doesn't match the last two dimensions of x.
|
||||
"""
|
||||
ndim = x.ndim
|
||||
assert ndim >= 2
|
||||
assert freqs_cis.shape == (x.shape[-2], x.shape[-1])
|
||||
shape = [d if i >= ndim - 2 else 1 for i, d in enumerate(x.shape)]
|
||||
return freqs_cis.view(*shape)
|
||||
|
||||
|
||||
def apply_rotary_enc(
|
||||
xq: torch.Tensor,
|
||||
xk: torch.Tensor,
|
||||
freqs_cis: torch.Tensor,
|
||||
repeat_freqs_k: bool = False,
|
||||
):
|
||||
"""Apply rotary positional encoding to query and key tensors.
|
||||
|
||||
This function applies rotary positional encoding (RoPE) to query and key tensors using complex-valued frequency
|
||||
components. RoPE is a technique that injects relative position information into self-attention mechanisms.
|
||||
|
||||
Args:
|
||||
xq (torch.Tensor): Query tensor to encode with positional information.
|
||||
xk (torch.Tensor): Key tensor to encode with positional information.
|
||||
freqs_cis (torch.Tensor): Complex-valued frequency components for rotary encoding with shape matching the last
|
||||
two dimensions of xq.
|
||||
repeat_freqs_k (bool, optional): Whether to repeat frequency components along sequence length dimension to match
|
||||
key sequence length.
|
||||
|
||||
Returns:
|
||||
xq_out (torch.Tensor): Query tensor with rotary positional encoding applied.
|
||||
xk_out (torch.Tensor): Key tensor with rotary positional encoding applied, or original xk if xk is empty.
|
||||
|
||||
Examples:
|
||||
>>> import torch
|
||||
>>> xq = torch.randn(2, 8, 16, 64) # [batch, heads, seq_len, dim]
|
||||
>>> xk = torch.randn(2, 8, 16, 64)
|
||||
>>> freqs_cis = compute_axial_cis(64, 4, 4) # For a 4x4 spatial grid with dim=64
|
||||
>>> q_encoded, k_encoded = apply_rotary_enc(xq, xk, freqs_cis)
|
||||
"""
|
||||
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
|
||||
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) if xk.shape[-2] != 0 else None
|
||||
freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
|
||||
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
|
||||
if xk_ is None:
|
||||
# No keys to rotate, due to dropout
|
||||
return xq_out.type_as(xq).to(xq.device), xk
|
||||
# Repeat freqs along seq_len dim to match k seq_len
|
||||
if repeat_freqs_k and (r := xk_.shape[-2] // xq_.shape[-2]) > 1:
|
||||
# MPS doesn't support repeat on complex tensors, decompose to real representation
|
||||
if freqs_cis.device.type == "mps":
|
||||
freqs_cis = torch.view_as_real(freqs_cis)
|
||||
freqs_cis = freqs_cis.repeat(*([1] * (freqs_cis.ndim - 3)), r, 1, 1)
|
||||
freqs_cis = torch.view_as_complex(freqs_cis.contiguous())
|
||||
else:
|
||||
freqs_cis = freqs_cis.repeat(*([1] * (freqs_cis.ndim - 2)), r, 1)
|
||||
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
|
||||
return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device)
|
||||
|
||||
|
||||
def window_partition(x: torch.Tensor, window_size: int):
|
||||
"""Partition input tensor into non-overlapping windows with padding if needed.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Input tensor with shape (B, H, W, C).
|
||||
window_size (int): Size of each window.
|
||||
|
||||
Returns:
|
||||
windows (torch.Tensor): Partitioned windows with shape (B * num_windows, window_size, window_size, C).
|
||||
padded_h_w (tuple[int, int]): Padded height and width before partition.
|
||||
|
||||
Examples:
|
||||
>>> x = torch.randn(1, 16, 16, 3)
|
||||
>>> windows, (Hp, Wp) = window_partition(x, window_size=4)
|
||||
>>> print(windows.shape, Hp, Wp)
|
||||
torch.Size([16, 4, 4, 3]) 16 16
|
||||
"""
|
||||
B, H, W, C = x.shape
|
||||
|
||||
pad_h = (window_size - H % window_size) % window_size
|
||||
pad_w = (window_size - W % window_size) % window_size
|
||||
if pad_h > 0 or pad_w > 0:
|
||||
x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h))
|
||||
Hp, Wp = H + pad_h, W + pad_w
|
||||
|
||||
x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C)
|
||||
windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
|
||||
return windows, (Hp, Wp)
|
||||
|
||||
|
||||
def window_unpartition(windows: torch.Tensor, window_size: int, pad_hw: tuple[int, int], hw: tuple[int, int]):
|
||||
"""Unpartition windowed sequences into original sequences and remove padding.
|
||||
|
||||
This function reverses the windowing process, reconstructing the original input from windowed segments and removing
|
||||
any padding that was added during the windowing process.
|
||||
|
||||
Args:
|
||||
windows (torch.Tensor): Input tensor of windowed sequences with shape (B * num_windows, window_size,
|
||||
window_size, C), where B is the batch size, num_windows is the number of windows, window_size is the size of
|
||||
each window, and C is the number of channels.
|
||||
window_size (int): Size of each window.
|
||||
pad_hw (tuple[int, int]): Padded height and width (Hp, Wp) of the input before windowing.
|
||||
hw (tuple[int, int]): Original height and width (H, W) of the input before padding and windowing.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Unpartitioned sequences with shape (B, H, W, C), where B is the batch size, H and W are the
|
||||
original height and width, and C is the number of channels.
|
||||
|
||||
Examples:
|
||||
>>> windows = torch.rand(32, 8, 8, 64) # 32 windows of size 8x8 with 64 channels
|
||||
>>> pad_hw = (16, 16) # Padded height and width
|
||||
>>> hw = (15, 14) # Original height and width
|
||||
>>> x = window_unpartition(windows, window_size=8, pad_hw=pad_hw, hw=hw)
|
||||
>>> print(x.shape)
|
||||
torch.Size([1, 15, 14, 64])
|
||||
"""
|
||||
Hp, Wp = pad_hw
|
||||
H, W = hw
|
||||
B = windows.shape[0] // (Hp * Wp // window_size // window_size)
|
||||
x = windows.view(B, Hp // window_size, Wp // window_size, window_size, window_size, -1)
|
||||
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1)
|
||||
|
||||
if Hp > H or Wp > W:
|
||||
x = x[:, :H, :W, :].contiguous()
|
||||
return x
|
||||
|
||||
|
||||
def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor:
|
||||
"""Extract relative positional embeddings based on query and key sizes.
|
||||
|
||||
Args:
|
||||
q_size (int): Size of the query.
|
||||
k_size (int): Size of the key.
|
||||
rel_pos (torch.Tensor): Relative position embeddings with shape (L, C), where L is the maximum relative distance
|
||||
and C is the embedding dimension.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Extracted positional embeddings according to relative positions, with shape (q_size, k_size, C).
|
||||
|
||||
Examples:
|
||||
>>> q_size, k_size = 8, 16
|
||||
>>> rel_pos = torch.randn(31, 64) # 31 = 2 * max(8, 16) - 1
|
||||
>>> extracted_pos = get_rel_pos(q_size, k_size, rel_pos)
|
||||
>>> print(extracted_pos.shape)
|
||||
torch.Size([8, 16, 64])
|
||||
"""
|
||||
max_rel_dist = int(2 * max(q_size, k_size) - 1)
|
||||
# Interpolate rel pos if needed.
|
||||
if rel_pos.shape[0] != max_rel_dist:
|
||||
# Interpolate rel pos.
|
||||
rel_pos_resized = F.interpolate(
|
||||
rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1),
|
||||
size=max_rel_dist,
|
||||
mode="linear",
|
||||
)
|
||||
rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0)
|
||||
else:
|
||||
rel_pos_resized = rel_pos
|
||||
|
||||
# Scale the coords with short length if shapes for q and k are different.
|
||||
q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0)
|
||||
k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0)
|
||||
relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0)
|
||||
|
||||
return rel_pos_resized[relative_coords.long()]
|
||||
|
||||
|
||||
def add_decomposed_rel_pos(
|
||||
attn: torch.Tensor,
|
||||
q: torch.Tensor,
|
||||
rel_pos_h: torch.Tensor,
|
||||
rel_pos_w: torch.Tensor,
|
||||
q_size: tuple[int, int],
|
||||
k_size: tuple[int, int],
|
||||
) -> torch.Tensor:
|
||||
"""Add decomposed Relative Positional Embeddings to the attention map.
|
||||
|
||||
This function calculates and applies decomposed Relative Positional Embeddings as described in the MVITv2
|
||||
paper. It enhances the attention mechanism by incorporating spatial relationships between query and key
|
||||
positions.
|
||||
|
||||
Args:
|
||||
attn (torch.Tensor): Attention map with shape (B, q_h * q_w, k_h * k_w).
|
||||
q (torch.Tensor): Query tensor in the attention layer with shape (B, q_h * q_w, C).
|
||||
rel_pos_h (torch.Tensor): Relative position embeddings for height axis with shape (Lh, C).
|
||||
rel_pos_w (torch.Tensor): Relative position embeddings for width axis with shape (Lw, C).
|
||||
q_size (tuple[int, int]): Spatial sequence size of query q as (q_h, q_w).
|
||||
k_size (tuple[int, int]): Spatial sequence size of key k as (k_h, k_w).
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Updated attention map with added relative positional embeddings, shape (B, q_h * q_w, k_h *
|
||||
k_w).
|
||||
|
||||
Examples:
|
||||
>>> B, C, q_h, q_w, k_h, k_w = 1, 64, 8, 8, 8, 8
|
||||
>>> attn = torch.rand(B, q_h * q_w, k_h * k_w)
|
||||
>>> q = torch.rand(B, q_h * q_w, C)
|
||||
>>> rel_pos_h = torch.rand(2 * max(q_h, k_h) - 1, C)
|
||||
>>> rel_pos_w = torch.rand(2 * max(q_w, k_w) - 1, C)
|
||||
>>> q_size, k_size = (q_h, q_w), (k_h, k_w)
|
||||
>>> updated_attn = add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size)
|
||||
>>> print(updated_attn.shape)
|
||||
torch.Size([1, 64, 64])
|
||||
|
||||
References:
|
||||
https://github.com/facebookresearch/mvit/blob/main/mvit/models/attention.py
|
||||
"""
|
||||
q_h, q_w = q_size
|
||||
k_h, k_w = k_size
|
||||
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
|
||||
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
|
||||
|
||||
B, _, dim = q.shape
|
||||
r_q = q.reshape(B, q_h, q_w, dim)
|
||||
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh)
|
||||
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw)
|
||||
|
||||
attn = (attn.view(B, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :]).view(
|
||||
B, q_h * q_w, k_h * k_w
|
||||
)
|
||||
|
||||
return attn
|
||||
|
||||
|
||||
def get_abs_pos(
|
||||
abs_pos: torch.Tensor,
|
||||
has_cls_token: bool,
|
||||
hw: tuple[int, int],
|
||||
retain_cls_token: bool = False,
|
||||
tiling: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Calculate absolute positional embeddings. If needed, resize embeddings and remove cls_token dimension for the
|
||||
original embeddings.
|
||||
|
||||
Args:
|
||||
abs_pos (torch.Tensor): Absolute positional embeddings with shape (1, num_position, C).
|
||||
has_cls_token (bool): If true, has 1 embedding in abs_pos for cls token.
|
||||
hw (tuple[int, int]): Size of input image tokens.
|
||||
retain_cls_token (bool): Whether to retain the cls_token.
|
||||
tiling (bool): Whether to tile the embeddings, *instead* of interpolation (a la abs_win).
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Absolute positional embeddings after processing with shape (1, H, W, C) if retain_cls_token is
|
||||
False, otherwise (1, 1+H*W, C).
|
||||
"""
|
||||
if retain_cls_token:
|
||||
assert has_cls_token
|
||||
|
||||
h, w = hw
|
||||
if has_cls_token:
|
||||
cls_pos = abs_pos[:, :1]
|
||||
abs_pos = abs_pos[:, 1:]
|
||||
|
||||
xy_num = abs_pos.shape[1]
|
||||
size = int(math.sqrt(xy_num))
|
||||
assert size * size == xy_num
|
||||
|
||||
if size != h or size != w:
|
||||
new_abs_pos = abs_pos.reshape(1, size, size, -1).permute(0, 3, 1, 2)
|
||||
if tiling:
|
||||
new_abs_pos = new_abs_pos.tile([1, 1] + [x // y + 1 for x, y in zip((h, w), new_abs_pos.shape[2:])])[
|
||||
:, :, :h, :w
|
||||
]
|
||||
else:
|
||||
new_abs_pos = F.interpolate(
|
||||
new_abs_pos,
|
||||
size=(h, w),
|
||||
mode="bicubic",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
if not retain_cls_token:
|
||||
return new_abs_pos.permute(0, 2, 3, 1)
|
||||
else:
|
||||
# add cls_token back, flatten spatial dims
|
||||
assert has_cls_token
|
||||
return torch.cat(
|
||||
[cls_pos, new_abs_pos.permute(0, 2, 3, 1).reshape(1, h * w, -1)],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
else:
|
||||
if not retain_cls_token:
|
||||
return abs_pos.reshape(1, h, w, -1)
|
||||
else:
|
||||
assert has_cls_token
|
||||
return torch.cat([cls_pos, abs_pos], dim=1)
|
||||
|
||||
|
||||
def concat_rel_pos(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_hw: tuple[int, int],
|
||||
k_hw: tuple[int, int],
|
||||
rel_pos_h: torch.Tensor,
|
||||
rel_pos_w: torch.Tensor,
|
||||
rescale: bool = False,
|
||||
relative_coords: torch.Tensor = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Concatenate rel pos coeffs to the q & k tensors, so that qk^T is now effectively including rel pos biases.
|
||||
|
||||
Args:
|
||||
q (torch.Tensor): Query tensor with shape (B, L_q, C).
|
||||
k (torch.Tensor): Key tensor with shape (B, L_k, C).
|
||||
q_hw (tuple[int, int]): Spatial size of query tensors as (height, width).
|
||||
k_hw (tuple[int, int]): Spatial size of key tensors as (height, width).
|
||||
rel_pos_h (torch.Tensor): Relative positional embeddings for the height axis.
|
||||
rel_pos_w (torch.Tensor): Relative positional embeddings for the width axis.
|
||||
rescale (bool): Whether to rescale for use with SDPA, which would scale by the wrong factor due to the concat.
|
||||
relative_coords (torch.Tensor | None): Precomputed relative coords index tensor.
|
||||
|
||||
Returns:
|
||||
q (torch.Tensor): Query tensor padded so that qk^T accounts for relative position biases.
|
||||
k (torch.Tensor): Key tensor padded so that qk^T accounts for relative position biases.
|
||||
"""
|
||||
q_h, q_w = q_hw
|
||||
k_h, k_w = k_hw
|
||||
|
||||
assert (q_h == q_w) and (k_h == k_w), "only square inputs supported"
|
||||
|
||||
if relative_coords is not None:
|
||||
Rh = rel_pos_h[relative_coords]
|
||||
Rw = rel_pos_w[relative_coords]
|
||||
else:
|
||||
Rh = get_rel_pos(q_h, k_h, rel_pos_h)
|
||||
Rw = get_rel_pos(q_w, k_w, rel_pos_w)
|
||||
|
||||
B, _, dim = q.shape
|
||||
r_q = q.reshape(B, q_h, q_w, dim)
|
||||
|
||||
old_scale = dim**0.5
|
||||
new_scale = (dim + k_h + k_w) ** 0.5 if rescale else old_scale # for sdpa
|
||||
# attn will be divided by new_scale, but we want to divide q by old_scale
|
||||
scale_ratio = new_scale / old_scale
|
||||
|
||||
rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh) * new_scale # (B, q_h, q_w, k_h)
|
||||
rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw) * new_scale # (B, q_h, q_w, k_w)
|
||||
|
||||
eye_h = torch.eye(k_h, dtype=q.dtype, device=q.device)
|
||||
eye_w = torch.eye(k_w, dtype=q.dtype, device=q.device)
|
||||
|
||||
eye_h = eye_h.view(1, k_h, 1, k_h).expand([B, k_h, k_w, k_h])
|
||||
eye_w = eye_w.view(1, 1, k_w, k_w).expand([B, k_h, k_w, k_w])
|
||||
|
||||
q = torch.cat([r_q * scale_ratio, rel_h, rel_w], dim=-1).view(B, q_h * q_w, -1)
|
||||
k = torch.cat([k.view(B, k_h, k_w, -1), eye_h, eye_w], dim=-1).view(B, k_h * k_w, -1)
|
||||
|
||||
return q, k
|
||||
3952
ultralytics/models/sam/predict.py
Executable file
3952
ultralytics/models/sam/predict.py
Executable file
File diff suppressed because it is too large
Load Diff
3
ultralytics/models/sam/sam3/__init__.py
Executable file
3
ultralytics/models/sam/sam3/__init__.py
Executable file
@@ -0,0 +1,3 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
546
ultralytics/models/sam/sam3/decoder.py
Executable file
546
ultralytics/models/sam/sam3/decoder.py
Executable file
@@ -0,0 +1,546 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
"""
|
||||
Transformer decoder.
|
||||
Inspired from Pytorch's version, adds the pre-norm variant.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
from torchvision.ops.roi_align import RoIAlign
|
||||
|
||||
from ultralytics.nn.modules.transformer import MLP
|
||||
from ultralytics.nn.modules.utils import _get_clones, inverse_sigmoid
|
||||
from ultralytics.utils.ops import xywh2xyxy
|
||||
|
||||
from .model_misc import gen_sineembed_for_position
|
||||
|
||||
|
||||
class TransformerDecoderLayer(nn.Module):
|
||||
"""TransformerDecoderLayer is made up of self-attn, cross-attn, and feedforward network (FFN)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
dim_feedforward: int,
|
||||
dropout: float,
|
||||
cross_attention: nn.Module,
|
||||
n_heads: int,
|
||||
use_text_cross_attention: bool = False,
|
||||
):
|
||||
"""Initialize the TransformerDecoderLayer."""
|
||||
super().__init__()
|
||||
# cross attention
|
||||
self.cross_attn = cross_attention
|
||||
self.dropout1 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.norm1 = nn.LayerNorm(d_model)
|
||||
|
||||
# cross attention text
|
||||
self.use_text_cross_attention = use_text_cross_attention
|
||||
if use_text_cross_attention:
|
||||
self.ca_text = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
|
||||
self.catext_dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.catext_norm = nn.LayerNorm(d_model)
|
||||
|
||||
# self attention
|
||||
self.self_attn = nn.MultiheadAttention(d_model, n_heads, dropout=dropout)
|
||||
self.dropout2 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.norm2 = nn.LayerNorm(d_model)
|
||||
|
||||
# ffn
|
||||
self.linear1 = nn.Linear(d_model, dim_feedforward)
|
||||
self.activation = nn.ReLU()
|
||||
self.dropout3 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.linear2 = nn.Linear(dim_feedforward, d_model)
|
||||
self.dropout4 = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
||||
self.norm3 = nn.LayerNorm(d_model)
|
||||
|
||||
@staticmethod
|
||||
def with_pos_embed(tensor, pos):
|
||||
"""Add positional embedding to the tensor."""
|
||||
return tensor if pos is None else tensor + pos
|
||||
|
||||
def forward_ffn(self, tgt):
|
||||
"""Feedforward network forward pass."""
|
||||
tgt2 = self.linear2(self.dropout3(self.activation(self.linear1(tgt))))
|
||||
tgt = tgt + self.dropout4(tgt2)
|
||||
tgt = self.norm3(tgt)
|
||||
return tgt
|
||||
|
||||
def forward(
|
||||
self,
|
||||
# for tgt
|
||||
tgt: torch.Tensor, # nq, bs, d_model
|
||||
tgt_query_pos: torch.Tensor = None, # pos for query. MLP(Sine(pos))
|
||||
memory_text: torch.Tensor = None, # num_token, bs, d_model
|
||||
text_attention_mask: torch.Tensor = None, # bs, num_token
|
||||
# for memory
|
||||
memory: torch.Tensor = None, # hw, bs, d_model
|
||||
memory_key_padding_mask: torch.Tensor = None,
|
||||
memory_pos: torch.Tensor = None, # pos for memory
|
||||
# sa
|
||||
self_attn_mask: torch.Tensor = None, # mask used for self-attention
|
||||
cross_attn_mask: torch.Tensor = None, # mask used for cross-attention
|
||||
# dac
|
||||
dac=False,
|
||||
dac_use_selfatt_ln=True,
|
||||
presence_token=None,
|
||||
# skip inside deformable attn
|
||||
**kwargs, # additional kwargs for compatibility
|
||||
):
|
||||
"""Forward pass of the TransformerDecoderLayer."""
|
||||
# self attention
|
||||
tgt, tgt_query_pos = self._apply_self_attention(
|
||||
tgt, tgt_query_pos, dac, dac_use_selfatt_ln, presence_token, self_attn_mask
|
||||
)
|
||||
|
||||
if self.use_text_cross_attention:
|
||||
tgt2 = self.ca_text(
|
||||
self.with_pos_embed(tgt, tgt_query_pos),
|
||||
memory_text.to(tgt.dtype),
|
||||
memory_text.to(tgt.dtype),
|
||||
key_padding_mask=text_attention_mask,
|
||||
)[0]
|
||||
tgt = tgt + self.catext_dropout(tgt2)
|
||||
tgt = self.catext_norm(tgt)
|
||||
|
||||
if presence_token is not None:
|
||||
presence_token_mask = torch.zeros_like(cross_attn_mask[:, :1, :])
|
||||
cross_attn_mask = torch.cat([presence_token_mask, cross_attn_mask], dim=1) # (bs*nheads, 1+nq, hw)
|
||||
|
||||
# Cross attention to image
|
||||
tgt2 = self.cross_attn(
|
||||
query=self.with_pos_embed(tgt, tgt_query_pos),
|
||||
key=self.with_pos_embed(memory, memory_pos),
|
||||
value=memory,
|
||||
attn_mask=cross_attn_mask,
|
||||
key_padding_mask=(memory_key_padding_mask.transpose(0, 1) if memory_key_padding_mask is not None else None),
|
||||
need_weights=False,
|
||||
)[0]
|
||||
|
||||
tgt = tgt + self.dropout1(tgt2)
|
||||
tgt = self.norm1(tgt)
|
||||
|
||||
# ffn
|
||||
tgt = self.forward_ffn(tgt.to(memory.dtype))
|
||||
|
||||
presence_token_out = None
|
||||
if presence_token is not None:
|
||||
presence_token_out = tgt[:1]
|
||||
tgt = tgt[1:]
|
||||
|
||||
return tgt, presence_token_out
|
||||
|
||||
def _apply_self_attention(self, tgt, tgt_query_pos, dac, dac_use_selfatt_ln, presence_token, self_attn_mask):
|
||||
"""Apply self-attention with optional DAC splitting."""
|
||||
if self.self_attn is None:
|
||||
return tgt
|
||||
|
||||
if dac:
|
||||
# Split queries for DAC (detect-and-classify)
|
||||
assert tgt.shape[0] % 2 == 0, "DAC requires even number of queries"
|
||||
num_o2o_queries = tgt.shape[0] // 2
|
||||
tgt_o2o = tgt[:num_o2o_queries]
|
||||
tgt_query_pos_o2o = tgt_query_pos[:num_o2o_queries]
|
||||
tgt_o2m = tgt[num_o2o_queries:]
|
||||
else:
|
||||
tgt_o2o = tgt
|
||||
tgt_query_pos_o2o = tgt_query_pos
|
||||
|
||||
# Handle presence token
|
||||
if presence_token is not None:
|
||||
tgt_o2o = torch.cat([presence_token, tgt_o2o], dim=0)
|
||||
tgt_query_pos_o2o = torch.cat([torch.zeros_like(presence_token), tgt_query_pos_o2o], dim=0).to(
|
||||
tgt_o2o.dtype
|
||||
)
|
||||
tgt_query_pos = torch.cat([torch.zeros_like(presence_token), tgt_query_pos], dim=0)
|
||||
|
||||
# Self-attention
|
||||
q = k = self.with_pos_embed(tgt_o2o, tgt_query_pos_o2o)
|
||||
tgt2 = self.self_attn(q, k, tgt_o2o, attn_mask=self_attn_mask)[0].to(tgt.dtype)
|
||||
tgt_o2o = tgt_o2o + self.dropout2(tgt2)
|
||||
|
||||
# Recombine and normalize
|
||||
if dac:
|
||||
if not dac_use_selfatt_ln:
|
||||
tgt_o2o = self.norm2(tgt_o2o)
|
||||
tgt = torch.cat((tgt_o2o, tgt_o2m), dim=0)
|
||||
if dac_use_selfatt_ln:
|
||||
tgt = self.norm2(tgt)
|
||||
else:
|
||||
tgt = tgt_o2o
|
||||
tgt = self.norm2(tgt)
|
||||
|
||||
return tgt, tgt_query_pos
|
||||
|
||||
|
||||
class TransformerDecoder(nn.Module):
|
||||
"""Transformer Decoder consisting of multiple layers."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
frozen: bool,
|
||||
interaction_layer,
|
||||
layer,
|
||||
num_layers: int,
|
||||
num_queries: int,
|
||||
return_intermediate: bool,
|
||||
box_refine: bool = False,
|
||||
num_o2m_queries: int = 0,
|
||||
dac: bool = False,
|
||||
boxRPB: str = "none",
|
||||
# Experimental: An object query for SAM 2 tasks
|
||||
instance_query: bool = False,
|
||||
# Defines the number of additional instance queries,
|
||||
# 1 or 4 are the most likely for single vs multi mask support
|
||||
num_instances: int = 1, # Irrelevant if instance_query is False
|
||||
dac_use_selfatt_ln: bool = True,
|
||||
use_act_checkpoint: bool = False,
|
||||
compile_mode=None,
|
||||
presence_token: bool = False,
|
||||
clamp_presence_logits: bool = True,
|
||||
clamp_presence_logit_max_val: float = 10.0,
|
||||
use_normed_output_consistently: bool = True,
|
||||
separate_box_head_instance: bool = False,
|
||||
separate_norm_instance: bool = False,
|
||||
):
|
||||
"""Initialize the TransformerDecoder."""
|
||||
super().__init__()
|
||||
self.d_model = d_model
|
||||
self.layers = _get_clones(layer, num_layers)
|
||||
self.fine_layers = (
|
||||
_get_clones(interaction_layer, num_layers) if interaction_layer is not None else [None] * num_layers
|
||||
)
|
||||
self.num_layers = num_layers
|
||||
self.num_queries = num_queries
|
||||
self.dac = dac
|
||||
if dac:
|
||||
self.num_o2m_queries = num_queries
|
||||
tot_num_queries = num_queries
|
||||
else:
|
||||
self.num_o2m_queries = num_o2m_queries
|
||||
tot_num_queries = num_queries + num_o2m_queries
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
self.return_intermediate = return_intermediate
|
||||
self.bbox_embed = MLP(d_model, d_model, 4, 3)
|
||||
self.query_embed = nn.Embedding(tot_num_queries, d_model)
|
||||
self.instance_query_embed = None
|
||||
self.instance_query_reference_points = None
|
||||
self.use_instance_query = instance_query
|
||||
self.num_instances = num_instances
|
||||
self.use_normed_output_consistently = use_normed_output_consistently
|
||||
|
||||
self.instance_norm = nn.LayerNorm(d_model) if separate_norm_instance else None
|
||||
self.instance_bbox_embed = None
|
||||
if separate_box_head_instance:
|
||||
self.instance_bbox_embed = MLP(d_model, d_model, 4, 3)
|
||||
if instance_query:
|
||||
self.instance_query_embed = nn.Embedding(num_instances, d_model)
|
||||
self.box_refine = box_refine
|
||||
if box_refine:
|
||||
nn.init.constant_(self.bbox_embed.layers[-1].weight.data, 0)
|
||||
nn.init.constant_(self.bbox_embed.layers[-1].bias.data, 0)
|
||||
|
||||
self.reference_points = nn.Embedding(num_queries, 4)
|
||||
if instance_query:
|
||||
self.instance_reference_points = nn.Embedding(num_instances, 4)
|
||||
|
||||
assert boxRPB in ["none", "log", "linear", "both"]
|
||||
self.boxRPB = boxRPB
|
||||
if boxRPB != "none":
|
||||
try:
|
||||
nheads = self.layers[0].cross_attn_image.num_heads
|
||||
except AttributeError:
|
||||
nheads = self.layers[0].cross_attn.num_heads
|
||||
|
||||
n_input = 4 if boxRPB == "both" else 2
|
||||
self.boxRPB_embed_x = MLP(n_input, d_model, nheads, 2)
|
||||
self.boxRPB_embed_y = MLP(n_input, d_model, nheads, 2)
|
||||
self.compilable_cord_cache = None
|
||||
self.compilable_stored_size = None
|
||||
self.coord_cache = {}
|
||||
|
||||
self.roi_pooler = (
|
||||
RoIAlign(output_size=7, spatial_scale=1, sampling_ratio=-1, aligned=True)
|
||||
if interaction_layer is not None
|
||||
else None
|
||||
)
|
||||
if frozen:
|
||||
for p in self.parameters():
|
||||
p.requires_grad_(False)
|
||||
|
||||
self.presence_token = None
|
||||
self.clamp_presence_logits = clamp_presence_logits
|
||||
self.clamp_presence_logit_max_val = clamp_presence_logit_max_val
|
||||
if presence_token:
|
||||
self.presence_token = nn.Embedding(1, d_model)
|
||||
self.presence_token_head = MLP(d_model, d_model, 1, 3)
|
||||
self.presence_token_out_norm = nn.LayerNorm(d_model)
|
||||
|
||||
self.ref_point_head = MLP(2 * self.d_model, self.d_model, self.d_model, 2)
|
||||
self.dac_use_selfatt_ln = dac_use_selfatt_ln
|
||||
self.use_act_checkpoint = use_act_checkpoint
|
||||
|
||||
nn.init.normal_(self.query_embed.weight.data)
|
||||
if self.instance_query_embed is not None:
|
||||
nn.init.normal_(self.instance_query_embed.weight.data)
|
||||
|
||||
assert self.roi_pooler is None
|
||||
assert self.return_intermediate, "support return_intermediate only"
|
||||
assert self.box_refine, "support box refine only"
|
||||
|
||||
self.compile_mode = compile_mode
|
||||
self.compiled = False
|
||||
# We defer compilation till after the first forward, to first warm-up the boxRPB cache
|
||||
|
||||
# assign layer index to each layer so that some layers can decide what to do
|
||||
# based on which layer index they are (e.g. cross attention to memory bank only
|
||||
# in selected layers)
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
layer.layer_idx = layer_idx
|
||||
|
||||
@staticmethod
|
||||
def _get_coords(H, W, device, dtype):
|
||||
"""Get normalized coordinates for height and width."""
|
||||
coords_h = torch.arange(0, H, dtype=dtype, device=device) / H
|
||||
coords_w = torch.arange(0, W, dtype=dtype, device=device) / W
|
||||
return coords_h, coords_w
|
||||
|
||||
def _get_rpb_matrix(self, reference_boxes, feat_size):
|
||||
"""Get the relative position bias (RPB) matrix for box-relative position bias."""
|
||||
H, W = feat_size
|
||||
boxes_xyxy = xywh2xyxy(reference_boxes).transpose(0, 1)
|
||||
bs, num_queries, _ = boxes_xyxy.shape
|
||||
if self.compilable_cord_cache is None:
|
||||
self.compilable_cord_cache = self._get_coords(H, W, reference_boxes.device, reference_boxes.dtype)
|
||||
self.compilable_stored_size = (H, W)
|
||||
|
||||
if torch.compiler.is_dynamo_compiling() or self.compilable_stored_size == (
|
||||
H,
|
||||
W,
|
||||
):
|
||||
# good, hitting the cache, will be compilable
|
||||
coords_h, coords_w = self.compilable_cord_cache
|
||||
else:
|
||||
# cache miss, will create compilation issue
|
||||
# In case we're not compiling, we'll still rely on the dict-based cache
|
||||
if feat_size not in self.coord_cache:
|
||||
self.coord_cache[feat_size] = self._get_coords(H, W, reference_boxes.device)
|
||||
coords_h, coords_w = self.coord_cache[feat_size]
|
||||
|
||||
assert coords_h.shape == (H,)
|
||||
assert coords_w.shape == (W,)
|
||||
|
||||
deltas_y = coords_h.view(1, -1, 1) - boxes_xyxy.reshape(-1, 1, 4)[:, :, 1:4:2]
|
||||
deltas_y = deltas_y.view(bs, num_queries, -1, 2)
|
||||
deltas_x = coords_w.view(1, -1, 1) - boxes_xyxy.reshape(-1, 1, 4)[:, :, 0:3:2]
|
||||
deltas_x = deltas_x.view(bs, num_queries, -1, 2)
|
||||
|
||||
if self.boxRPB in ["log", "both"]:
|
||||
deltas_x_log = deltas_x * 8 # normalize to -8, 8
|
||||
deltas_x_log = torch.sign(deltas_x_log) * torch.log2(torch.abs(deltas_x_log) + 1.0) / np.log2(8)
|
||||
|
||||
deltas_y_log = deltas_y * 8 # normalize to -8, 8
|
||||
deltas_y_log = torch.sign(deltas_y_log) * torch.log2(torch.abs(deltas_y_log) + 1.0) / np.log2(8)
|
||||
if self.boxRPB == "log":
|
||||
deltas_x = deltas_x_log
|
||||
deltas_y = deltas_y_log
|
||||
else:
|
||||
deltas_x = torch.cat([deltas_x, deltas_x_log], dim=-1)
|
||||
deltas_y = torch.cat([deltas_y, deltas_y_log], dim=-1)
|
||||
|
||||
if self.training:
|
||||
assert self.use_act_checkpoint, "activation ckpt not enabled in decoder"
|
||||
deltas_x = self.boxRPB_embed_x(x=deltas_x) # bs, num_queries, W, n_heads
|
||||
deltas_y = self.boxRPB_embed_y(x=deltas_y) # bs, num_queries, H, n_heads
|
||||
|
||||
if not torch.compiler.is_dynamo_compiling():
|
||||
assert deltas_x.shape[:3] == (bs, num_queries, W)
|
||||
assert deltas_y.shape[:3] == (bs, num_queries, H)
|
||||
|
||||
B = deltas_y.unsqueeze(3) + deltas_x.unsqueeze(2) # bs, num_queries, H, W, n_heads
|
||||
if not torch.compiler.is_dynamo_compiling():
|
||||
assert B.shape[:4] == (bs, num_queries, H, W)
|
||||
B = B.flatten(2, 3) # bs, num_queries, H*W, n_heads
|
||||
B = B.permute(0, 3, 1, 2) # bs, n_heads, num_queries, H*W
|
||||
B = B.contiguous() # memeff attn likes ordered strides
|
||||
if not torch.compiler.is_dynamo_compiling():
|
||||
assert B.shape[2:] == (num_queries, H * W)
|
||||
return B
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tgt,
|
||||
memory,
|
||||
tgt_mask: torch.Tensor = None,
|
||||
memory_mask: torch.Tensor = None,
|
||||
memory_key_padding_mask: torch.Tensor = None,
|
||||
pos: torch.Tensor = None,
|
||||
reference_boxes: torch.Tensor = None, # num_queries, bs, 4
|
||||
# for memory
|
||||
spatial_shapes: torch.Tensor = None, # bs, num_levels, 2
|
||||
valid_ratios: torch.Tensor = None,
|
||||
# for text
|
||||
memory_text: torch.Tensor = None,
|
||||
text_attention_mask: torch.Tensor = None,
|
||||
# if `apply_dac` is None, it will default to `self.dac`
|
||||
apply_dac: bool | None = None,
|
||||
is_instance_prompt=False,
|
||||
decoder_extra_kwargs: dict | None = None,
|
||||
# ROI memory bank
|
||||
obj_roi_memory_feat=None,
|
||||
obj_roi_memory_mask=None,
|
||||
box_head_trk=None,
|
||||
):
|
||||
"""Forward pass of the TransformerDecoder."""
|
||||
if memory_mask is not None:
|
||||
assert self.boxRPB == "none", (
|
||||
"inputting a memory_mask in the presence of boxRPB is unexpected/not implemented"
|
||||
)
|
||||
|
||||
apply_dac = apply_dac if apply_dac is not None else self.dac
|
||||
if apply_dac:
|
||||
assert (tgt.shape[0] == self.num_queries) or (
|
||||
self.use_instance_query and (tgt.shape[0] == self.instance_query_embed.num_embeddings)
|
||||
)
|
||||
|
||||
tgt = tgt.repeat(2, 1, 1)
|
||||
# note that we don't tile tgt_mask, since DAC doesn't
|
||||
# use self-attention in o2m queries
|
||||
if reference_boxes is not None:
|
||||
assert (reference_boxes.shape[0] == self.num_queries) or (
|
||||
self.use_instance_query and (reference_boxes.shape[0] == self.instance_query_embed.num_embeddings)
|
||||
)
|
||||
reference_boxes = reference_boxes.repeat(2, 1, 1)
|
||||
|
||||
bs = tgt.shape[1]
|
||||
intermediate = []
|
||||
intermediate_presence_logits = []
|
||||
presence_feats = None
|
||||
|
||||
if self.box_refine:
|
||||
if reference_boxes is None:
|
||||
# In this case, we're in a one-stage model, so we generate the reference boxes
|
||||
reference_boxes = self.reference_points.weight.unsqueeze(1)
|
||||
reference_boxes = reference_boxes.repeat(2, bs, 1) if apply_dac else reference_boxes.repeat(1, bs, 1)
|
||||
reference_boxes = reference_boxes.sigmoid()
|
||||
intermediate_ref_boxes = [reference_boxes]
|
||||
else:
|
||||
reference_boxes = None
|
||||
intermediate_ref_boxes = None
|
||||
|
||||
output = tgt
|
||||
presence_out = None
|
||||
if self.presence_token is not None and is_instance_prompt is False:
|
||||
# expand to batch dim
|
||||
presence_out = self.presence_token.weight[None].expand(1, bs, -1)
|
||||
|
||||
box_head = self.bbox_embed
|
||||
if is_instance_prompt and self.instance_bbox_embed is not None:
|
||||
box_head = self.instance_bbox_embed
|
||||
|
||||
out_norm = self.norm
|
||||
if is_instance_prompt and self.instance_norm is not None:
|
||||
out_norm = self.instance_norm
|
||||
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
reference_points_input = (
|
||||
reference_boxes[:, :, None] * torch.cat([valid_ratios, valid_ratios], -1)[None, :]
|
||||
) # nq, bs, nlevel, 4
|
||||
|
||||
query_sine_embed = gen_sineembed_for_position(
|
||||
reference_points_input[:, :, 0, :], self.d_model
|
||||
) # nq, bs, d_model*2
|
||||
|
||||
# conditional query
|
||||
query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model
|
||||
|
||||
if self.boxRPB != "none" and reference_boxes is not None:
|
||||
assert spatial_shapes.shape[0] == 1, "only single scale support implemented"
|
||||
memory_mask = self._get_rpb_matrix(
|
||||
reference_boxes,
|
||||
(spatial_shapes[0, 0], spatial_shapes[0, 1]),
|
||||
)
|
||||
memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W)
|
||||
if self.training:
|
||||
assert self.use_act_checkpoint, "Activation checkpointing not enabled in the decoder"
|
||||
output, presence_out = layer(
|
||||
tgt=output,
|
||||
tgt_query_pos=query_pos,
|
||||
memory_text=memory_text,
|
||||
text_attention_mask=text_attention_mask,
|
||||
memory=memory,
|
||||
memory_key_padding_mask=memory_key_padding_mask,
|
||||
memory_pos=pos,
|
||||
self_attn_mask=tgt_mask,
|
||||
cross_attn_mask=memory_mask,
|
||||
dac=apply_dac,
|
||||
dac_use_selfatt_ln=self.dac_use_selfatt_ln,
|
||||
presence_token=presence_out,
|
||||
**(decoder_extra_kwargs or {}),
|
||||
# ROI memory bank
|
||||
obj_roi_memory_feat=obj_roi_memory_feat,
|
||||
obj_roi_memory_mask=obj_roi_memory_mask,
|
||||
)
|
||||
|
||||
# iter update
|
||||
if self.box_refine:
|
||||
reference_before_sigmoid = inverse_sigmoid(reference_boxes)
|
||||
if box_head_trk is None:
|
||||
# delta_unsig = self.bbox_embed(output)
|
||||
if not self.use_normed_output_consistently:
|
||||
delta_unsig = box_head(output)
|
||||
else:
|
||||
delta_unsig = box_head(out_norm(output))
|
||||
else:
|
||||
# box_head_trk use a separate box head for tracking queries
|
||||
Q_det = decoder_extra_kwargs["Q_det"]
|
||||
assert output.size(0) >= Q_det
|
||||
delta_unsig_det = self.bbox_embed(output[:Q_det])
|
||||
delta_unsig_trk = box_head_trk(output[Q_det:])
|
||||
delta_unsig = torch.cat([delta_unsig_det, delta_unsig_trk], dim=0)
|
||||
outputs_unsig = delta_unsig + reference_before_sigmoid
|
||||
new_reference_points = outputs_unsig.sigmoid()
|
||||
|
||||
reference_boxes = new_reference_points.detach()
|
||||
if layer_idx != self.num_layers - 1:
|
||||
intermediate_ref_boxes.append(new_reference_points)
|
||||
else:
|
||||
raise NotImplementedError("not implemented yet")
|
||||
|
||||
intermediate.append(out_norm(output))
|
||||
if self.presence_token is not None and is_instance_prompt is False:
|
||||
# norm, mlp head
|
||||
intermediate_layer_presence_logits = self.presence_token_head(
|
||||
self.presence_token_out_norm(presence_out)
|
||||
).squeeze(-1)
|
||||
|
||||
# clamp to mitigate numerical issues
|
||||
if self.clamp_presence_logits:
|
||||
intermediate_layer_presence_logits.clamp(
|
||||
min=-self.clamp_presence_logit_max_val,
|
||||
max=self.clamp_presence_logit_max_val,
|
||||
)
|
||||
|
||||
intermediate_presence_logits.append(intermediate_layer_presence_logits)
|
||||
presence_feats = presence_out.clone()
|
||||
|
||||
if not self.compiled and self.compile_mode is not None:
|
||||
self.forward = torch.compile(self.forward, mode=self.compile_mode, fullgraph=True)
|
||||
self.compiled = True
|
||||
|
||||
return (
|
||||
torch.stack(intermediate),
|
||||
torch.stack(intermediate_ref_boxes),
|
||||
(
|
||||
torch.stack(intermediate_presence_logits)
|
||||
if self.presence_token is not None and is_instance_prompt is False
|
||||
else None
|
||||
),
|
||||
presence_feats,
|
||||
)
|
||||
529
ultralytics/models/sam/sam3/encoder.py
Executable file
529
ultralytics/models/sam/sam3/encoder.py
Executable file
@@ -0,0 +1,529 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
# Based on https://github.com/IDEA-Research/GroundingDINO
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from ultralytics.nn.modules.utils import _get_clones
|
||||
|
||||
from .model_misc import get_valid_ratio
|
||||
|
||||
|
||||
class TransformerEncoderLayer(nn.Module):
|
||||
"""Transformer encoder layer that performs self-attention followed by cross-attention.
|
||||
|
||||
This layer was previously called TransformerDecoderLayer but was renamed to better reflect its role in the
|
||||
architecture. It processes input sequences through self-attention and then cross-attention with another input
|
||||
(typically image features).
|
||||
|
||||
The layer supports both pre-norm and post-norm configurations, as well as positional encoding at different stages of
|
||||
the attention mechanism.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
dim_feedforward: int,
|
||||
dropout: float,
|
||||
pos_enc_at_attn: bool,
|
||||
pos_enc_at_cross_attn_keys: bool,
|
||||
pos_enc_at_cross_attn_queries: bool,
|
||||
pre_norm: bool,
|
||||
self_attention: nn.Module = None,
|
||||
cross_attention: nn.Module = None,
|
||||
):
|
||||
"""Initialize a transformer encoder layer.
|
||||
|
||||
Args:
|
||||
d_model: Model dimension/hidden size
|
||||
dim_feedforward: Dimension of the feedforward network
|
||||
dropout: Dropout probability
|
||||
pos_enc_at_attn: Whether to add positional encodings at self-attention
|
||||
pos_enc_at_cross_attn_keys: Whether to add positional encodings to keys in cross-attention
|
||||
pos_enc_at_cross_attn_queries: Whether to add positional encodings to queries in cross-attention
|
||||
pre_norm: Whether to use pre-norm (True) or post-norm (False) architecture
|
||||
self_attention: Self-attention module
|
||||
cross_attention: Cross-attention module for attending to image features
|
||||
"""
|
||||
super().__init__()
|
||||
self.d_model = d_model
|
||||
self.dim_feedforward = dim_feedforward
|
||||
self.dropout_value = dropout
|
||||
self.self_attn = self_attention or nn.MultiheadAttention(num_heads=8, dropout=0.1, embed_dim=256)
|
||||
self.cross_attn_image = cross_attention or nn.MultiheadAttention(num_heads=8, dropout=0.1, embed_dim=256)
|
||||
|
||||
# Implementation of Feedforward model
|
||||
self.linear1 = nn.Linear(d_model, dim_feedforward)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.linear2 = nn.Linear(dim_feedforward, d_model)
|
||||
|
||||
self.norm1 = nn.LayerNorm(d_model)
|
||||
self.norm2 = nn.LayerNorm(d_model)
|
||||
self.norm3 = nn.LayerNorm(d_model)
|
||||
self.dropout1 = nn.Dropout(dropout)
|
||||
self.dropout2 = nn.Dropout(dropout)
|
||||
self.dropout3 = nn.Dropout(dropout)
|
||||
|
||||
self.activation = nn.ReLU()
|
||||
self.pre_norm = pre_norm
|
||||
|
||||
self.pos_enc_at_attn = pos_enc_at_attn
|
||||
self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries
|
||||
self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys
|
||||
|
||||
self.layer_idx = None
|
||||
|
||||
def forward_post(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
tgt_mask: torch.Tensor = None,
|
||||
memory_mask: torch.Tensor = None,
|
||||
tgt_key_padding_mask: torch.Tensor = None,
|
||||
memory_key_padding_mask: torch.Tensor = None,
|
||||
pos: torch.Tensor = None,
|
||||
query_pos: torch.Tensor = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass for post-norm architecture.
|
||||
|
||||
In post-norm architecture, normalization is applied after attention and feedforward operations.
|
||||
|
||||
Args:
|
||||
tgt: Input tensor to be processed
|
||||
memory: Memory tensor for cross-attention
|
||||
tgt_mask: Mask for self-attention
|
||||
memory_mask: Mask for cross-attention
|
||||
tgt_key_padding_mask: Key padding mask for self-attention
|
||||
memory_key_padding_mask: Key padding mask for cross-attention
|
||||
pos: Positional encoding for memory
|
||||
query_pos: Positional encoding for query
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
Processed tensor
|
||||
"""
|
||||
q = k = tgt + query_pos if self.pos_enc_at_attn else tgt
|
||||
|
||||
# Self attention
|
||||
tgt2 = self.self_attn(
|
||||
q, k, value=tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask, need_weights=False
|
||||
)[0]
|
||||
tgt = tgt + self.dropout1(tgt2)
|
||||
tgt = self.norm1(tgt)
|
||||
|
||||
# Cross attention to image
|
||||
tgt2 = self.cross_attn_image(
|
||||
query=tgt + query_pos if self.pos_enc_at_cross_attn_queries else tgt,
|
||||
key=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
|
||||
value=memory,
|
||||
attn_mask=memory_mask,
|
||||
key_padding_mask=memory_key_padding_mask,
|
||||
need_weights=False,
|
||||
)[0]
|
||||
tgt = tgt + self.dropout2(tgt2)
|
||||
tgt = self.norm2(tgt)
|
||||
|
||||
# FFN
|
||||
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
|
||||
tgt = tgt + self.dropout3(tgt2)
|
||||
tgt = self.norm3(tgt)
|
||||
return tgt
|
||||
|
||||
def forward_pre(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
dac: bool = False,
|
||||
tgt_mask: torch.Tensor = None,
|
||||
memory_mask: torch.Tensor = None,
|
||||
tgt_key_padding_mask: torch.Tensor = None,
|
||||
memory_key_padding_mask: torch.Tensor = None,
|
||||
pos: torch.Tensor = None,
|
||||
query_pos: torch.Tensor = None,
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass for pre-norm architecture.
|
||||
|
||||
In pre-norm architecture, normalization is applied before attention and feedforward operations.
|
||||
|
||||
Args:
|
||||
tgt: Input tensor to be processed
|
||||
memory: Memory tensor for cross-attention
|
||||
dac: Whether to use Divide-and-Conquer attention
|
||||
tgt_mask: Mask for self-attention
|
||||
memory_mask: Mask for cross-attention
|
||||
tgt_key_padding_mask: Key padding mask for self-attention
|
||||
memory_key_padding_mask: Key padding mask for cross-attention
|
||||
pos: Positional encoding for memory
|
||||
query_pos: Positional encoding for query
|
||||
|
||||
Returns:
|
||||
Processed tensor
|
||||
"""
|
||||
if dac:
|
||||
# we only apply self attention to the first half of the queries
|
||||
assert tgt.shape[0] % 2 == 0
|
||||
other_tgt = tgt[tgt.shape[0] // 2 :]
|
||||
tgt = tgt[: tgt.shape[0] // 2]
|
||||
tgt2 = self.norm1(tgt).contiguous()
|
||||
q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2
|
||||
tgt2 = self.self_attn(q, k, value=tgt2, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0]
|
||||
tgt = tgt + self.dropout1(tgt2)
|
||||
if dac:
|
||||
# Recombine
|
||||
tgt = torch.cat((tgt, other_tgt), dim=0)
|
||||
tgt2 = self.norm2(tgt)
|
||||
memory = memory.to(tgt2.dtype).contiguous()
|
||||
tgt2 = self.cross_attn_image(
|
||||
query=tgt2 + query_pos if self.pos_enc_at_cross_attn_queries else tgt2,
|
||||
key=memory + pos if self.pos_enc_at_cross_attn_keys else memory,
|
||||
value=memory,
|
||||
attn_mask=memory_mask,
|
||||
key_padding_mask=memory_key_padding_mask,
|
||||
)[0]
|
||||
tgt = tgt + self.dropout2(tgt2)
|
||||
tgt2 = self.norm3(tgt)
|
||||
tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2))))
|
||||
tgt = tgt + self.dropout3(tgt2)
|
||||
return tgt
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
dac: bool = False,
|
||||
tgt_mask: torch.Tensor = None,
|
||||
memory_mask: torch.Tensor = None,
|
||||
tgt_key_padding_mask: torch.Tensor = None,
|
||||
memory_key_padding_mask: torch.Tensor = None,
|
||||
pos: torch.Tensor = None,
|
||||
query_pos: torch.Tensor = None,
|
||||
) -> torch.Tensor:
|
||||
"""Forward pass for the transformer encoder layer.
|
||||
|
||||
Args:
|
||||
tgt: Input tensor to be processed
|
||||
memory: Memory tensor (e.g., image features) for cross-attention
|
||||
dac: Whether to use Divide-and-Conquer attention (only apply self-attention to first half)
|
||||
tgt_mask: Mask for self-attention
|
||||
memory_mask: Mask for cross-attention
|
||||
tgt_key_padding_mask: Key padding mask for self-attention
|
||||
memory_key_padding_mask: Key padding mask for cross-attention
|
||||
pos: Positional encoding for memory
|
||||
query_pos: Positional encoding for query
|
||||
|
||||
Returns:
|
||||
Processed tensor after self-attention, cross-attention, and feedforward network
|
||||
"""
|
||||
fwd_fn = self.forward_pre if self.pre_norm else self.forward_post
|
||||
return fwd_fn(
|
||||
tgt,
|
||||
memory,
|
||||
dac=dac,
|
||||
tgt_mask=tgt_mask,
|
||||
memory_mask=memory_mask,
|
||||
tgt_key_padding_mask=tgt_key_padding_mask,
|
||||
memory_key_padding_mask=memory_key_padding_mask,
|
||||
pos=pos,
|
||||
query_pos=query_pos,
|
||||
# attn_bias=attn_bias,
|
||||
# **kwds,
|
||||
)
|
||||
|
||||
|
||||
class TransformerEncoder(nn.Module):
|
||||
"""Transformer encoder that processes multi-level features.
|
||||
|
||||
This encoder takes multi-level features (e.g., from a backbone network) and processes them through a stack of
|
||||
transformer encoder layers. It supports features from multiple levels (e.g., different resolutions) and can apply
|
||||
activation checkpointing for memory efficiency during training.
|
||||
|
||||
Args:
|
||||
layer: The encoder layer to be stacked multiple times
|
||||
num_layers: Number of encoder layers to stack
|
||||
d_model: Model dimension/hidden size
|
||||
num_feature_levels: Number of feature levels to process
|
||||
frozen: Whether to freeze the parameters of this module
|
||||
use_act_checkpoint: Whether to use activation checkpointing during training
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
layer: nn.Module,
|
||||
num_layers: int,
|
||||
d_model: int,
|
||||
num_feature_levels: int,
|
||||
frozen: bool = False,
|
||||
use_act_checkpoint: bool = False,
|
||||
):
|
||||
"""Initialize the transformer encoder."""
|
||||
super().__init__()
|
||||
self.layers = _get_clones(layer, num_layers)
|
||||
self.num_layers = num_layers
|
||||
|
||||
self.num_feature_levels = num_feature_levels
|
||||
self.level_embed = None
|
||||
if num_feature_levels > 1:
|
||||
self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model))
|
||||
|
||||
if frozen:
|
||||
for p in self.parameters():
|
||||
p.requires_grad_(False)
|
||||
|
||||
self.use_act_checkpoint = use_act_checkpoint
|
||||
|
||||
# assign layer index to each layer so that some layers can decide what to do
|
||||
# based on which layer index they are (e.g. cross attention to memory bank only
|
||||
# in selected layers)
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
layer.layer_idx = layer_idx
|
||||
|
||||
def _prepare_multilevel_features(self, srcs, masks, pos_embeds):
|
||||
"""Prepare multi-level features for transformer encoder."""
|
||||
assert len(srcs) == self.num_feature_levels, "mismatch between expected and received # of feature levels"
|
||||
|
||||
src_flatten = []
|
||||
mask_flatten = []
|
||||
lvl_pos_embed_flatten = []
|
||||
spatial_shapes = []
|
||||
has_mask = masks is not None and masks[0] is not None
|
||||
for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)):
|
||||
_, _, h, w = src.shape
|
||||
spatial_shape = (h, w)
|
||||
spatial_shapes.append(spatial_shape)
|
||||
|
||||
src = src.flatten(2).transpose(1, 2) # bs, hw, c
|
||||
if has_mask:
|
||||
mask = mask.flatten(1)
|
||||
pos_embed = pos_embed.flatten(2).transpose(1, 2) # bs, hw, c
|
||||
if self.level_embed is not None:
|
||||
lvl_pos_embed = pos_embed + self.level_embed[lvl].view(1, 1, -1)
|
||||
else:
|
||||
lvl_pos_embed = pos_embed
|
||||
lvl_pos_embed_flatten.append(lvl_pos_embed)
|
||||
src_flatten.append(src)
|
||||
if has_mask:
|
||||
mask_flatten.append(mask)
|
||||
src_flatten = torch.cat(src_flatten, 1) # bs, \sum{hxw}, c
|
||||
mask_flatten = torch.cat(mask_flatten, 1) if has_mask else None # bs, \sum{hxw}
|
||||
lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1) # bs, \sum{hxw}, c
|
||||
spatial_shapes = torch.tensor(spatial_shapes, dtype=torch.long, device=src_flatten.device)
|
||||
level_start_index = torch.cat(
|
||||
(
|
||||
spatial_shapes.new_zeros((1,)),
|
||||
spatial_shapes.prod(1).cumsum(0)[:-1],
|
||||
)
|
||||
)
|
||||
if has_mask:
|
||||
valid_ratios = torch.stack([get_valid_ratio(m) for m in masks], 1)
|
||||
else:
|
||||
valid_ratios = torch.ones(
|
||||
(src_flatten.shape[0], self.num_feature_levels, 2),
|
||||
device=src_flatten.device,
|
||||
dtype=src_flatten.dtype,
|
||||
)
|
||||
|
||||
return (
|
||||
src_flatten,
|
||||
mask_flatten,
|
||||
lvl_pos_embed_flatten,
|
||||
level_start_index,
|
||||
valid_ratios,
|
||||
spatial_shapes,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
src: list[torch.Tensor],
|
||||
src_key_padding_masks: list[torch.Tensor] | None = None,
|
||||
pos: list[torch.Tensor] | None = None,
|
||||
prompt: torch.Tensor = None,
|
||||
prompt_key_padding_mask: torch.Tensor = None,
|
||||
encoder_extra_kwargs: dict | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Process multi-level features through the transformer encoder.
|
||||
|
||||
Args:
|
||||
src: List of multi-level features, each with shape (batch_size, channels, height, width)
|
||||
src_key_padding_masks: List of padding masks for each feature level, each with shape (batch_size, height,
|
||||
width)
|
||||
pos: List of positional embeddings for each feature level, each with shape (batch_size, channels, height,
|
||||
width)
|
||||
prompt: Optional text/prompt features to attend to, with shape (seq_len, batch_size, d_model)
|
||||
prompt_key_padding_mask: Optional padding mask for prompt, with shape (batch_size, seq_len)
|
||||
encoder_extra_kwargs: Optional additional arguments to pass to each encoder layer
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
- output: Processed features with shape (seq_len, batch_size, d_model)
|
||||
- key_padding_masks_flatten: Flattened padding masks
|
||||
- lvl_pos_embed_flatten: Flattened positional embeddings
|
||||
- level_start_index: Starting indices for each feature level
|
||||
- spatial_shapes: Spatial dimensions of each feature level
|
||||
- valid_ratios: Valid ratios for each feature level
|
||||
"""
|
||||
assert len(src) == self.num_feature_levels, "must be equal to num_feature_levels"
|
||||
if src_key_padding_masks is not None:
|
||||
assert len(src_key_padding_masks) == self.num_feature_levels
|
||||
if pos is not None:
|
||||
assert len(pos) == self.num_feature_levels
|
||||
# Flatten multilevel feats and add level pos embeds
|
||||
(
|
||||
src_flatten,
|
||||
key_padding_masks_flatten,
|
||||
lvl_pos_embed_flatten,
|
||||
level_start_index,
|
||||
valid_ratios,
|
||||
spatial_shapes,
|
||||
) = self._prepare_multilevel_features(src, src_key_padding_masks, pos)
|
||||
|
||||
output = src_flatten
|
||||
for layer in self.layers:
|
||||
layer_kwargs = {}
|
||||
|
||||
assert isinstance(layer, TransformerEncoderLayer)
|
||||
layer_kwargs["memory"] = prompt
|
||||
layer_kwargs["memory_key_padding_mask"] = prompt_key_padding_mask
|
||||
layer_kwargs["query_pos"] = lvl_pos_embed_flatten
|
||||
layer_kwargs["tgt"] = output
|
||||
layer_kwargs["tgt_key_padding_mask"] = key_padding_masks_flatten
|
||||
|
||||
if self.training:
|
||||
assert self.use_act_checkpoint, "activation ckpt not enabled in encoder"
|
||||
if encoder_extra_kwargs is not None:
|
||||
layer_kwargs.update(encoder_extra_kwargs)
|
||||
output = layer(**layer_kwargs)
|
||||
# return as seq first
|
||||
return (
|
||||
output.transpose(0, 1),
|
||||
(key_padding_masks_flatten.transpose(0, 1) if key_padding_masks_flatten is not None else None),
|
||||
lvl_pos_embed_flatten.transpose(0, 1),
|
||||
level_start_index,
|
||||
spatial_shapes,
|
||||
valid_ratios,
|
||||
)
|
||||
|
||||
|
||||
class TransformerEncoderFusion(TransformerEncoder):
|
||||
"""Transformer encoder that fuses text and image features.
|
||||
|
||||
This encoder extends TransformerEncoder to handle both text and image features, with the ability to add pooled text
|
||||
features to image features for better cross-modal fusion. It supports torch.compile for performance optimization.
|
||||
|
||||
Args:
|
||||
layer: The encoder layer to be stacked multiple times
|
||||
num_layers: Number of encoder layers to stack
|
||||
d_model: Model dimension/hidden size
|
||||
num_feature_levels: Number of feature levels to process
|
||||
add_pooled_text_to_img_feat: Whether to add pooled text features to image features
|
||||
pool_text_with_mask: Whether to use the mask when pooling text features
|
||||
compile_mode: Mode for torch.compile, or None to disable compilation
|
||||
**kwargs: Additional arguments to pass to the parent class
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
layer: nn.Module,
|
||||
num_layers: int,
|
||||
d_model: int,
|
||||
num_feature_levels: int,
|
||||
add_pooled_text_to_img_feat: bool = True,
|
||||
pool_text_with_mask: bool = False,
|
||||
compile_mode: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the transformer encoder with text-image fusion."""
|
||||
super().__init__(
|
||||
layer,
|
||||
num_layers,
|
||||
d_model,
|
||||
num_feature_levels,
|
||||
**kwargs,
|
||||
)
|
||||
self.add_pooled_text_to_img_feat = add_pooled_text_to_img_feat
|
||||
if self.add_pooled_text_to_img_feat:
|
||||
self.text_pooling_proj = nn.Linear(d_model, d_model)
|
||||
self.pool_text_with_mask = pool_text_with_mask
|
||||
if compile_mode is not None:
|
||||
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
src: list[torch.Tensor],
|
||||
prompt: torch.Tensor,
|
||||
src_key_padding_mask: list[torch.Tensor] | None = None,
|
||||
src_pos: list[torch.Tensor] | None = None,
|
||||
prompt_key_padding_mask: torch.Tensor = None,
|
||||
feat_sizes: list[int] | None = None,
|
||||
encoder_extra_kwargs: dict | None = None,
|
||||
):
|
||||
"""Forward pass for the transformer encoder with text-image fusion."""
|
||||
# Restore spatial shapes of vision
|
||||
bs = src[0].shape[1] # seq first
|
||||
if feat_sizes is not None:
|
||||
assert len(feat_sizes) == len(src)
|
||||
if src_key_padding_mask is None:
|
||||
src_key_padding_mask = [None] * len(src)
|
||||
for i, (h, w) in enumerate(feat_sizes):
|
||||
src[i] = src[i].reshape(h, w, bs, -1).permute(2, 3, 0, 1)
|
||||
src_pos[i] = src_pos[i].reshape(h, w, bs, -1).permute(2, 3, 0, 1)
|
||||
src_key_padding_mask[i] = (
|
||||
src_key_padding_mask[i].reshape(h, w, bs).permute(2, 0, 1)
|
||||
if src_key_padding_mask[i] is not None
|
||||
else None
|
||||
)
|
||||
else:
|
||||
assert all(x.dim == 4 for x in src), "expected list of (bs, c, h, w) tensors"
|
||||
|
||||
if self.add_pooled_text_to_img_feat:
|
||||
# Fusion: Add mean pooled text to image features
|
||||
pooled_text = pool_text_feat(prompt, prompt_key_padding_mask, self.pool_text_with_mask)
|
||||
pooled_text = self.text_pooling_proj(pooled_text)[..., None, None] # prompt is seq first
|
||||
src = [x.add_(pooled_text) for x in src]
|
||||
|
||||
(
|
||||
out,
|
||||
key_padding_masks_flatten,
|
||||
lvl_pos_embed_flatten,
|
||||
level_start_index,
|
||||
spatial_shapes,
|
||||
valid_ratios,
|
||||
) = super().forward(
|
||||
src,
|
||||
src_key_padding_masks=src_key_padding_mask,
|
||||
pos=src_pos,
|
||||
prompt=prompt.transpose(0, 1),
|
||||
prompt_key_padding_mask=prompt_key_padding_mask,
|
||||
encoder_extra_kwargs=encoder_extra_kwargs,
|
||||
)
|
||||
|
||||
return {
|
||||
"memory": out,
|
||||
"padding_mask": key_padding_masks_flatten,
|
||||
"pos_embed": lvl_pos_embed_flatten,
|
||||
"memory_text": prompt,
|
||||
"level_start_index": level_start_index,
|
||||
"spatial_shapes": spatial_shapes,
|
||||
"valid_ratios": valid_ratios,
|
||||
}
|
||||
|
||||
|
||||
def pool_text_feat(prompt, prompt_mask, pool_with_mask):
|
||||
"""Mean-pool the prompt embeddings over the valid tokens only."""
|
||||
# prompt has shape (seq, bs, dim)
|
||||
if not pool_with_mask:
|
||||
return prompt.mean(dim=0)
|
||||
|
||||
# prompt_mask has shape (bs, seq), where False is valid and True is padding
|
||||
assert prompt_mask.dim() == 2
|
||||
# is_valid has shape (seq, bs, 1), where 1 is valid and 0 is padding
|
||||
is_valid = (~prompt_mask).float().permute(1, 0)[..., None]
|
||||
# num_valid has shape (bs, 1)
|
||||
num_valid = torch.clamp(torch.sum(is_valid, dim=0), min=1.0)
|
||||
|
||||
# mean pool over all the valid tokens
|
||||
pooled_text = (prompt * is_valid).sum(dim=0) / num_valid
|
||||
return pooled_text
|
||||
415
ultralytics/models/sam/sam3/geometry_encoders.py
Executable file
415
ultralytics/models/sam/sam3/geometry_encoders.py
Executable file
@@ -0,0 +1,415 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torchvision
|
||||
|
||||
from ultralytics.nn.modules.utils import _get_clones
|
||||
from ultralytics.utils.ops import xywh2xyxy
|
||||
|
||||
|
||||
def is_right_padded(mask: torch.Tensor):
|
||||
"""Given a padding mask (following pytorch convention, 1s for padded values), returns whether the padding is on the
|
||||
right or not.
|
||||
"""
|
||||
return (mask.long() == torch.sort(mask.long(), dim=-1)[0]).all()
|
||||
|
||||
|
||||
def concat_padded_sequences(seq1, mask1, seq2, mask2, return_index: bool = False):
|
||||
"""
|
||||
Concatenates two right-padded sequences, such that the resulting sequence
|
||||
is contiguous and also right-padded.
|
||||
|
||||
Following pytorch's convention, tensors are sequence first, and the mask are
|
||||
batch first, with 1s for padded values.
|
||||
|
||||
:param seq1: A tensor of shape (seq1_length, batch_size, hidden_size).
|
||||
:param mask1: A tensor of shape (batch_size, seq1_length).
|
||||
:param seq2: A tensor of shape (seq2_length, batch_size, hidden_size).
|
||||
:param mask2: A tensor of shape (batch_size, seq2_length).
|
||||
:param return_index: If True, also returns the index of the ids of the element of seq2
|
||||
in the concatenated sequence. This can be used to retrieve the elements of seq2
|
||||
:return: A tuple (concatenated_sequence, concatenated_mask) if return_index is False,
|
||||
otherwise (concatenated_sequence, concatenated_mask, index).
|
||||
"""
|
||||
seq1_length, batch_size, hidden_size = seq1.shape
|
||||
seq2_length, batch_size, hidden_size = seq2.shape
|
||||
|
||||
assert batch_size == seq1.size(1) == seq2.size(1) == mask1.size(0) == mask2.size(0)
|
||||
assert hidden_size == seq1.size(2) == seq2.size(2)
|
||||
assert seq1_length == mask1.size(1)
|
||||
assert seq2_length == mask2.size(1)
|
||||
|
||||
torch._assert(is_right_padded(mask1), "Mask is not right padded")
|
||||
torch._assert(is_right_padded(mask2), "Mask is not right padded")
|
||||
|
||||
actual_seq1_lengths = (~mask1).sum(dim=-1)
|
||||
actual_seq2_lengths = (~mask2).sum(dim=-1)
|
||||
|
||||
final_lengths = actual_seq1_lengths + actual_seq2_lengths
|
||||
max_length = seq1_length + seq2_length
|
||||
concatenated_mask = (
|
||||
torch.arange(max_length, device=seq2.device)[None].repeat(batch_size, 1) >= final_lengths[:, None]
|
||||
)
|
||||
|
||||
# (max_len, batch_size, hidden_size)
|
||||
concatenated_sequence = torch.zeros((max_length, batch_size, hidden_size), device=seq2.device, dtype=seq2.dtype)
|
||||
concatenated_sequence[:seq1_length, :, :] = seq1
|
||||
|
||||
# At this point, the element of seq1 are in the right place
|
||||
# We just need to shift the elements of seq2
|
||||
|
||||
index = torch.arange(seq2_length, device=seq2.device)[:, None].repeat(1, batch_size)
|
||||
index = index + actual_seq1_lengths[None]
|
||||
|
||||
concatenated_sequence = concatenated_sequence.scatter(0, index[:, :, None].expand(-1, -1, hidden_size), seq2)
|
||||
|
||||
if return_index:
|
||||
return concatenated_sequence, concatenated_mask, index
|
||||
|
||||
return concatenated_sequence, concatenated_mask
|
||||
|
||||
|
||||
class Prompt:
|
||||
"""Utility class to manipulate geometric prompts.
|
||||
|
||||
We expect the sequences in pytorch convention, that is sequence first, batch second The dimensions are expected as
|
||||
follows: box_embeddings shape: N_boxes x B x C_box box_mask shape: B x N_boxes. Can be None if nothing is masked out
|
||||
point_embeddings shape: N_points x B x C_point point_mask shape: B x N_points. Can be None if nothing is masked out
|
||||
mask_embeddings shape: N_masks x B x 1 x H_mask x W_mask mask_mask shape: B x N_masks. Can be None if nothing is
|
||||
masked out
|
||||
|
||||
We also store positive/negative labels. These tensors are also stored batch-first If they are None, we'll assume
|
||||
positive labels everywhere box_labels: long tensor of shape N_boxes x B point_labels: long tensor of shape N_points
|
||||
x B mask_labels: long tensor of shape N_masks x B
|
||||
"""
|
||||
|
||||
def __init__(self, box_embeddings=None, box_mask=None, box_labels=None):
|
||||
"""Initialize the Prompt object."""
|
||||
# Check for null prompt
|
||||
# Check for null prompt
|
||||
if box_embeddings is None:
|
||||
self.box_embeddings = None
|
||||
self.box_labels = None
|
||||
self.box_mask = None
|
||||
return
|
||||
|
||||
# Get sequence length, batch size, and device
|
||||
box_seq_len = box_embeddings.shape[0]
|
||||
bs = box_embeddings.shape[1]
|
||||
device = box_embeddings.device
|
||||
|
||||
# Initialize labels and attention mask if not provided
|
||||
if box_labels is None:
|
||||
box_labels = torch.ones(box_seq_len, bs, device=device, dtype=torch.long)
|
||||
if box_mask is None:
|
||||
box_mask = torch.zeros(bs, box_seq_len, device=device, dtype=torch.bool)
|
||||
|
||||
# Dimension checks
|
||||
assert list(box_embeddings.shape[:2]) == [box_seq_len, bs], (
|
||||
f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}"
|
||||
)
|
||||
assert box_embeddings.shape[-1] == 4, (
|
||||
f"Expected box embeddings to have 4 coordinates, got {box_embeddings.shape[-1]}"
|
||||
)
|
||||
assert list(box_mask.shape) == [bs, box_seq_len], (
|
||||
f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}"
|
||||
)
|
||||
assert list(box_labels.shape) == [box_seq_len, bs], (
|
||||
f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}"
|
||||
)
|
||||
|
||||
# Device checks
|
||||
assert box_embeddings.device == device, (
|
||||
f"Expected box embeddings to be on device {device}, got {box_embeddings.device}"
|
||||
)
|
||||
assert box_mask.device == device, f"Expected box mask to be on device {device}, got {box_mask.device}"
|
||||
assert box_labels.device == device, f"Expected box labels to be on device {device}, got {box_labels.device}"
|
||||
|
||||
self.box_embeddings = box_embeddings
|
||||
self.box_mask = box_mask
|
||||
self.box_labels = box_labels
|
||||
|
||||
def append_boxes(self, boxes, labels=None, mask=None):
|
||||
"""Append box prompts to existing prompts.
|
||||
|
||||
Args:
|
||||
boxes: Tensor of shape (N_new_boxes, B, 4) with normalized box coordinates
|
||||
labels: Optional tensor of shape (N_new_boxes, B) with positive/negative labels
|
||||
mask: Optional tensor of shape (B, N_new_boxes) for attention mask
|
||||
"""
|
||||
if self.box_embeddings is None:
|
||||
# First boxes - initialize
|
||||
self.box_embeddings = boxes
|
||||
bs = boxes.shape[1]
|
||||
box_seq_len = boxes.shape[0]
|
||||
|
||||
if labels is None:
|
||||
labels = torch.ones(box_seq_len, bs, device=boxes.device, dtype=torch.long)
|
||||
if mask is None:
|
||||
mask = torch.zeros(bs, box_seq_len, device=boxes.device, dtype=torch.bool)
|
||||
|
||||
self.box_labels = labels
|
||||
self.box_mask = mask
|
||||
return
|
||||
|
||||
# Append to existing boxes
|
||||
bs = self.box_embeddings.shape[1]
|
||||
assert boxes.shape[1] == bs, f"Batch size mismatch: expected {bs}, got {boxes.shape[1]}"
|
||||
|
||||
if labels is None:
|
||||
labels = torch.ones(boxes.shape[0], bs, device=boxes.device, dtype=torch.long)
|
||||
if mask is None:
|
||||
mask = torch.zeros(bs, boxes.shape[0], dtype=torch.bool, device=boxes.device)
|
||||
|
||||
assert list(boxes.shape[:2]) == list(labels.shape[:2]), (
|
||||
f"Shape mismatch between boxes {boxes.shape} and labels {labels.shape}"
|
||||
)
|
||||
|
||||
# Concatenate using the helper function
|
||||
self.box_labels, _ = concat_padded_sequences(
|
||||
self.box_labels.unsqueeze(-1), self.box_mask, labels.unsqueeze(-1), mask
|
||||
)
|
||||
self.box_labels = self.box_labels.squeeze(-1)
|
||||
self.box_embeddings, self.box_mask = concat_padded_sequences(self.box_embeddings, self.box_mask, boxes, mask)
|
||||
|
||||
|
||||
class SequenceGeometryEncoder(nn.Module):
|
||||
"""Encoder for geometric box prompts. Assumes boxes are passed in the "normalized CxCyWH" format.
|
||||
|
||||
Boxes can be encoded with any of the three possibilities:
|
||||
- direct projection: linear projection from coordinate space to d_model
|
||||
- pooling: RoI align features from the backbone
|
||||
- pos encoder: position encoding of the box center
|
||||
|
||||
These three options are mutually compatible and will be summed if multiple are selected.
|
||||
|
||||
As an alternative, boxes can be encoded as two corner points (top-left and bottom-right).
|
||||
|
||||
The encoded sequence can be further processed with a transformer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encode_boxes_as_points: bool,
|
||||
boxes_direct_project: bool,
|
||||
boxes_pool: bool,
|
||||
boxes_pos_enc: bool,
|
||||
d_model: int,
|
||||
pos_enc,
|
||||
num_layers: int,
|
||||
layer: nn.Module,
|
||||
roi_size: int = 7,
|
||||
add_cls: bool = True,
|
||||
add_post_encode_proj: bool = True,
|
||||
use_act_ckpt: bool = False,
|
||||
):
|
||||
"""Initialize the SequenceGeometryEncoder."""
|
||||
super().__init__()
|
||||
|
||||
self.d_model = d_model
|
||||
self.pos_enc = pos_enc
|
||||
self.encode_boxes_as_points = encode_boxes_as_points
|
||||
self.roi_size = roi_size
|
||||
|
||||
# Label embeddings: 2 labels if encoding as boxes (pos/neg)
|
||||
# 6 labels if encoding as points (regular pos/neg, top-left pos/neg, bottom-right pos/neg)
|
||||
num_labels = 6 if self.encode_boxes_as_points else 2
|
||||
self.label_embed = torch.nn.Embedding(num_labels, self.d_model)
|
||||
|
||||
# CLS token for pooling
|
||||
self.cls_embed = None
|
||||
if add_cls:
|
||||
self.cls_embed = torch.nn.Embedding(1, self.d_model)
|
||||
|
||||
# Point encoding (used when encode_boxes_as_points is True)
|
||||
if encode_boxes_as_points:
|
||||
self.points_direct_project = nn.Linear(2, self.d_model)
|
||||
self.points_pool_project = None
|
||||
self.points_pos_enc_project = None
|
||||
else:
|
||||
# Box encoding modules
|
||||
assert boxes_direct_project or boxes_pos_enc or boxes_pool, "Error: need at least one way to encode boxes"
|
||||
self.points_direct_project = None
|
||||
self.points_pool_project = None
|
||||
self.points_pos_enc_project = None
|
||||
|
||||
self.boxes_direct_project = None
|
||||
self.boxes_pool_project = None
|
||||
self.boxes_pos_enc_project = None
|
||||
|
||||
if boxes_direct_project:
|
||||
self.boxes_direct_project = nn.Linear(4, self.d_model)
|
||||
if boxes_pool:
|
||||
self.boxes_pool_project = nn.Conv2d(self.d_model, self.d_model, self.roi_size)
|
||||
if boxes_pos_enc:
|
||||
self.boxes_pos_enc_project = nn.Linear(self.d_model + 2, self.d_model)
|
||||
|
||||
self.final_proj = None
|
||||
if add_post_encode_proj:
|
||||
self.final_proj = nn.Linear(self.d_model, self.d_model)
|
||||
self.norm = nn.LayerNorm(self.d_model)
|
||||
|
||||
self.img_pre_norm = nn.Identity()
|
||||
if self.points_pool_project is not None or self.boxes_pool_project is not None:
|
||||
self.img_pre_norm = nn.LayerNorm(self.d_model)
|
||||
|
||||
self.encode = None
|
||||
if num_layers > 0:
|
||||
assert add_cls, "It's currently highly recommended to add a CLS when using a transformer"
|
||||
self.encode = _get_clones(layer, num_layers)
|
||||
self.encode_norm = nn.LayerNorm(self.d_model)
|
||||
|
||||
self.use_act_ckpt = use_act_ckpt
|
||||
|
||||
def _encode_points(self, points, points_mask, points_labels, img_feats):
|
||||
"""Encode points (used when boxes are converted to corner points)."""
|
||||
# Direct projection of coordinates
|
||||
points_embed = self.points_direct_project(points.to(img_feats.dtype))
|
||||
|
||||
# Add label embeddings
|
||||
type_embed = self.label_embed(points_labels.long())
|
||||
return type_embed + points_embed, points_mask
|
||||
|
||||
def _encode_boxes(self, boxes, boxes_mask, boxes_labels, img_feats: torch.Tensor):
|
||||
"""Encode boxes using configured encoding methods."""
|
||||
boxes_embed = None
|
||||
n_boxes, bs = boxes.shape[:2]
|
||||
|
||||
if self.boxes_direct_project is not None:
|
||||
proj = self.boxes_direct_project(boxes.to(img_feats.dtype))
|
||||
boxes_embed = proj
|
||||
|
||||
if self.boxes_pool_project is not None:
|
||||
H, W = img_feats.shape[-2:]
|
||||
|
||||
# Convert boxes to xyxy format and denormalize
|
||||
boxes_xyxy = xywh2xyxy(boxes.to(img_feats.dtype))
|
||||
scale = torch.tensor([W, H, W, H], dtype=boxes_xyxy.dtype)
|
||||
scale = scale.to(device=boxes_xyxy.device, non_blocking=True)
|
||||
scale = scale.view(1, 1, 4)
|
||||
boxes_xyxy = boxes_xyxy * scale
|
||||
|
||||
# RoI align
|
||||
sampled = torchvision.ops.roi_align(img_feats, boxes_xyxy.transpose(0, 1).unbind(0), self.roi_size)
|
||||
assert list(sampled.shape) == [
|
||||
bs * n_boxes,
|
||||
self.d_model,
|
||||
self.roi_size,
|
||||
self.roi_size,
|
||||
]
|
||||
proj = self.boxes_pool_project(sampled)
|
||||
proj = proj.view(bs, n_boxes, self.d_model).transpose(0, 1)
|
||||
|
||||
if boxes_embed is None:
|
||||
boxes_embed = proj
|
||||
else:
|
||||
boxes_embed = boxes_embed + proj
|
||||
|
||||
if self.boxes_pos_enc_project is not None:
|
||||
cx, cy, w, h = boxes.unbind(-1)
|
||||
enc = self.pos_enc.encode_boxes(cx.flatten(), cy.flatten(), w.flatten(), h.flatten())
|
||||
enc = enc.view(boxes.shape[0], boxes.shape[1], enc.shape[-1])
|
||||
|
||||
proj = self.boxes_pos_enc_project(enc.to(img_feats.dtype))
|
||||
if boxes_embed is None:
|
||||
boxes_embed = proj
|
||||
else:
|
||||
boxes_embed = boxes_embed + proj
|
||||
|
||||
# Add label embeddings
|
||||
type_embed = self.label_embed(boxes_labels.long())
|
||||
return type_embed + boxes_embed, boxes_mask
|
||||
|
||||
def forward(self, geo_prompt: Prompt, img_feats, img_sizes, img_pos_embeds=None):
|
||||
"""Encode geometric box prompts.
|
||||
|
||||
Args:
|
||||
geo_prompt: Prompt object containing box embeddings, masks, and labels
|
||||
img_feats: List of image features from backbone
|
||||
img_sizes: List of (H, W) tuples for each feature level
|
||||
img_pos_embeds: Optional position embeddings for image features
|
||||
|
||||
Returns:
|
||||
Tuple of (encoded_embeddings, attention_mask)
|
||||
"""
|
||||
boxes = geo_prompt.box_embeddings
|
||||
boxes_mask = geo_prompt.box_mask
|
||||
boxes_labels = geo_prompt.box_labels
|
||||
|
||||
seq_first_img_feats = img_feats[-1] # [H*W, B, C]
|
||||
seq_first_img_pos_embeds = (
|
||||
img_pos_embeds[-1] if img_pos_embeds is not None else torch.zeros_like(seq_first_img_feats)
|
||||
)
|
||||
|
||||
# Prepare image features for pooling if needed
|
||||
if self.points_pool_project or self.boxes_pool_project:
|
||||
assert len(img_feats) == len(img_sizes)
|
||||
cur_img_feat = img_feats[-1]
|
||||
cur_img_feat = self.img_pre_norm(cur_img_feat)
|
||||
H, W = img_sizes[-1]
|
||||
assert cur_img_feat.shape[0] == H * W
|
||||
N, C = cur_img_feat.shape[-2:]
|
||||
# Reshape to NxCxHxW
|
||||
cur_img_feat = cur_img_feat.permute(1, 2, 0)
|
||||
cur_img_feat = cur_img_feat.view(N, C, H, W)
|
||||
img_feats = cur_img_feat
|
||||
|
||||
if self.encode_boxes_as_points:
|
||||
# Convert boxes to corner points
|
||||
assert boxes is not None and boxes.shape[-1] == 4
|
||||
|
||||
boxes_xyxy = xywh2xyxy(boxes)
|
||||
top_left, bottom_right = boxes_xyxy.split(split_size=2, dim=-1)
|
||||
|
||||
# Adjust labels for corner points (offset by 2 and 4)
|
||||
labels_tl = boxes_labels + 2
|
||||
labels_br = boxes_labels + 4
|
||||
|
||||
# Concatenate top-left and bottom-right points
|
||||
points = torch.cat([top_left, bottom_right], dim=0)
|
||||
points_labels = torch.cat([labels_tl, labels_br], dim=0)
|
||||
points_mask = torch.cat([boxes_mask, boxes_mask], dim=1)
|
||||
|
||||
final_embeds, final_mask = self._encode_points(
|
||||
points=points,
|
||||
points_mask=points_mask,
|
||||
points_labels=points_labels,
|
||||
img_feats=img_feats,
|
||||
)
|
||||
else:
|
||||
# Encode boxes directly
|
||||
final_embeds, final_mask = self._encode_boxes(
|
||||
boxes=boxes,
|
||||
boxes_mask=boxes_mask,
|
||||
boxes_labels=boxes_labels,
|
||||
img_feats=img_feats,
|
||||
)
|
||||
|
||||
bs = final_embeds.shape[1]
|
||||
assert final_mask.shape[0] == bs
|
||||
|
||||
# Add CLS token if configured
|
||||
if self.cls_embed is not None:
|
||||
cls = self.cls_embed.weight.view(1, 1, self.d_model).repeat(1, bs, 1)
|
||||
cls_mask = torch.zeros(bs, 1, dtype=final_mask.dtype, device=final_mask.device)
|
||||
final_embeds, final_mask = concat_padded_sequences(final_embeds, final_mask, cls, cls_mask)
|
||||
|
||||
# Final projection
|
||||
if self.final_proj is not None:
|
||||
final_embeds = self.norm(self.final_proj(final_embeds))
|
||||
|
||||
# Transformer encoding layers
|
||||
if self.encode is not None:
|
||||
for lay in self.encode:
|
||||
final_embeds = lay(
|
||||
tgt=final_embeds,
|
||||
memory=seq_first_img_feats,
|
||||
tgt_key_padding_mask=final_mask,
|
||||
pos=seq_first_img_pos_embeds,
|
||||
)
|
||||
final_embeds = self.encode_norm(final_embeds)
|
||||
|
||||
return final_embeds, final_mask
|
||||
286
ultralytics/models/sam/sam3/maskformer_segmentation.py
Executable file
286
ultralytics/models/sam/sam3/maskformer_segmentation.py
Executable file
@@ -0,0 +1,286 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.checkpoint as checkpoint
|
||||
|
||||
from ultralytics.nn.modules.transformer import MLP
|
||||
|
||||
|
||||
class LinearPresenceHead(nn.Sequential):
|
||||
"""Linear presence head for predicting the presence of classes in an image."""
|
||||
|
||||
def __init__(self, d_model):
|
||||
"""Initializes the LinearPresenceHead."""
|
||||
# a hack to make `LinearPresenceHead` compatible with old checkpoints
|
||||
super().__init__(nn.Identity(), nn.Identity(), nn.Linear(d_model, 1))
|
||||
|
||||
def forward(self, hs, prompt, prompt_mask):
|
||||
"""Forward pass of the presence head."""
|
||||
return super().forward(hs)
|
||||
|
||||
|
||||
class MaskPredictor(nn.Module):
|
||||
"""Predicts masks from object queries and pixel embeddings."""
|
||||
|
||||
def __init__(self, hidden_dim, mask_dim):
|
||||
"""Initializes the MaskPredictor."""
|
||||
super().__init__()
|
||||
self.mask_embed = MLP(hidden_dim, hidden_dim, mask_dim, 3)
|
||||
|
||||
def forward(self, obj_queries, pixel_embed):
|
||||
"""Predicts masks from object queries and pixel embeddings."""
|
||||
if len(obj_queries.shape) == 3:
|
||||
if pixel_embed.ndim == 3:
|
||||
# batch size was omitted
|
||||
mask_preds = torch.einsum("bqc,chw->bqhw", self.mask_embed(obj_queries), pixel_embed)
|
||||
else:
|
||||
mask_preds = torch.einsum("bqc,bchw->bqhw", self.mask_embed(obj_queries), pixel_embed)
|
||||
else:
|
||||
# Assumed to have aux masks
|
||||
if pixel_embed.ndim == 3:
|
||||
# batch size was omitted
|
||||
mask_preds = torch.einsum("lbqc,chw->lbqhw", self.mask_embed(obj_queries), pixel_embed)
|
||||
else:
|
||||
mask_preds = torch.einsum("lbqc,bchw->lbqhw", self.mask_embed(obj_queries), pixel_embed)
|
||||
|
||||
return mask_preds
|
||||
|
||||
|
||||
class SegmentationHead(nn.Module):
|
||||
"""Segmentation head that predicts masks from backbone features and object queries."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_dim,
|
||||
upsampling_stages,
|
||||
use_encoder_inputs=False,
|
||||
aux_masks=False,
|
||||
no_dec=False,
|
||||
pixel_decoder=None,
|
||||
act_ckpt=False,
|
||||
shared_conv=False,
|
||||
compile_mode_pixel_decoder=None,
|
||||
):
|
||||
"""Initializes the SegmentationHead."""
|
||||
super().__init__()
|
||||
self.use_encoder_inputs = use_encoder_inputs
|
||||
self.aux_masks = aux_masks
|
||||
if pixel_decoder is not None:
|
||||
self.pixel_decoder = pixel_decoder
|
||||
else:
|
||||
self.pixel_decoder = PixelDecoder(
|
||||
hidden_dim,
|
||||
upsampling_stages,
|
||||
shared_conv=shared_conv,
|
||||
compile_mode=compile_mode_pixel_decoder,
|
||||
)
|
||||
self.no_dec = no_dec
|
||||
if no_dec:
|
||||
self.mask_predictor = nn.Conv2d(hidden_dim, 1, kernel_size=3, stride=1, padding=1)
|
||||
else:
|
||||
self.mask_predictor = MaskPredictor(hidden_dim, mask_dim=hidden_dim)
|
||||
|
||||
self.act_ckpt = act_ckpt
|
||||
|
||||
# used to update the output dictionary
|
||||
self.instance_keys = ["pred_masks"]
|
||||
|
||||
def _embed_pixels(self, backbone_feats: list[torch.Tensor], encoder_hidden_states) -> torch.Tensor:
|
||||
"""Embeds pixels using the pixel decoder."""
|
||||
if self.use_encoder_inputs:
|
||||
backbone_visual_feats = [bb_feat.clone() for bb_feat in backbone_feats]
|
||||
# Extract visual embeddings
|
||||
encoder_hidden_states = encoder_hidden_states.permute(1, 2, 0)
|
||||
spatial_dim = math.prod(backbone_feats[-1].shape[-2:])
|
||||
encoder_visual_embed = encoder_hidden_states[..., :spatial_dim].reshape(-1, *backbone_feats[-1].shape[1:])
|
||||
|
||||
backbone_visual_feats[-1] = encoder_visual_embed
|
||||
if self.act_ckpt:
|
||||
pixel_embed = checkpoint.checkpoint(self.pixel_decoder, backbone_visual_feats, use_reentrant=False)
|
||||
else:
|
||||
pixel_embed = self.pixel_decoder(backbone_visual_feats)
|
||||
else:
|
||||
backbone_feats = [x for x in backbone_feats]
|
||||
pixel_embed = self.pixel_decoder(backbone_feats)
|
||||
if pixel_embed.shape[0] == 1:
|
||||
# For batch_size=1 training, we can avoid the indexing to save memory
|
||||
pixel_embed = pixel_embed.squeeze(0)
|
||||
else:
|
||||
pixel_embed = pixel_embed[[0], ...]
|
||||
return pixel_embed
|
||||
|
||||
def forward(
|
||||
self,
|
||||
backbone_feats: list[torch.Tensor],
|
||||
obj_queries: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor = None,
|
||||
**kwargs,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Forward pass of the SegmentationHead."""
|
||||
if self.use_encoder_inputs:
|
||||
assert encoder_hidden_states is not None
|
||||
|
||||
pixel_embed = self._embed_pixels(backbone_feats=backbone_feats, encoder_hidden_states=encoder_hidden_states)
|
||||
|
||||
if self.no_dec:
|
||||
mask_pred = self.mask_predictor(pixel_embed)
|
||||
elif self.aux_masks:
|
||||
mask_pred = self.mask_predictor(obj_queries, pixel_embed)
|
||||
else:
|
||||
mask_pred = self.mask_predictor(obj_queries[-1], pixel_embed)
|
||||
|
||||
return {"pred_masks": mask_pred}
|
||||
|
||||
|
||||
class PixelDecoder(nn.Module):
|
||||
"""Pixel decoder module that upsamples backbone features."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_dim,
|
||||
num_upsampling_stages,
|
||||
interpolation_mode="nearest",
|
||||
shared_conv=False,
|
||||
compile_mode=None,
|
||||
):
|
||||
"""Initializes the PixelDecoder."""
|
||||
super().__init__()
|
||||
self.hidden_dim = hidden_dim
|
||||
self.num_upsampling_stages = num_upsampling_stages
|
||||
self.interpolation_mode = interpolation_mode
|
||||
conv_layers = []
|
||||
norms = []
|
||||
num_convs = 1 if shared_conv else num_upsampling_stages
|
||||
for _ in range(num_convs):
|
||||
conv_layers.append(nn.Conv2d(self.hidden_dim, self.hidden_dim, 3, 1, 1))
|
||||
norms.append(nn.GroupNorm(8, self.hidden_dim))
|
||||
|
||||
self.conv_layers = nn.ModuleList(conv_layers)
|
||||
self.norms = nn.ModuleList(norms)
|
||||
self.shared_conv = shared_conv
|
||||
self.out_dim = self.conv_layers[-1].out_channels
|
||||
if compile_mode is not None:
|
||||
self.forward = torch.compile(self.forward, mode=compile_mode, dynamic=True, fullgraph=True)
|
||||
# Needed to make checkpointing happy. But we don't know if the module is checkpointed, so we disable it by default.
|
||||
torch._dynamo.config.optimize_ddp = False
|
||||
|
||||
def forward(self, backbone_feats: list[torch.Tensor]):
|
||||
"""Forward pass of the PixelDecoder."""
|
||||
prev_fpn = backbone_feats[-1]
|
||||
fpn_feats = backbone_feats[:-1]
|
||||
for layer_idx, bb_feat in enumerate(fpn_feats[::-1]):
|
||||
curr_fpn = bb_feat
|
||||
prev_fpn = curr_fpn + F.interpolate(prev_fpn, size=curr_fpn.shape[-2:], mode=self.interpolation_mode)
|
||||
if self.shared_conv:
|
||||
# only one conv layer
|
||||
layer_idx = 0
|
||||
prev_fpn = self.conv_layers[layer_idx](prev_fpn)
|
||||
prev_fpn = F.relu(self.norms[layer_idx](prev_fpn))
|
||||
|
||||
return prev_fpn
|
||||
|
||||
|
||||
class UniversalSegmentationHead(SegmentationHead):
|
||||
"""This module handles semantic+instance segmentation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_dim,
|
||||
upsampling_stages,
|
||||
pixel_decoder,
|
||||
aux_masks=False,
|
||||
no_dec=False,
|
||||
act_ckpt=False,
|
||||
presence_head: bool = False,
|
||||
dot_product_scorer=None,
|
||||
cross_attend_prompt=None,
|
||||
):
|
||||
"""Initializes the UniversalSegmentationHead."""
|
||||
super().__init__(
|
||||
hidden_dim=hidden_dim,
|
||||
upsampling_stages=upsampling_stages,
|
||||
use_encoder_inputs=True,
|
||||
aux_masks=aux_masks,
|
||||
no_dec=no_dec,
|
||||
pixel_decoder=pixel_decoder,
|
||||
act_ckpt=act_ckpt,
|
||||
)
|
||||
self.d_model = hidden_dim
|
||||
|
||||
if dot_product_scorer is not None:
|
||||
assert presence_head, "Specifying a dot product scorer without a presence head is likely a mistake"
|
||||
|
||||
self.presence_head = None
|
||||
if presence_head:
|
||||
self.presence_head = (
|
||||
dot_product_scorer if dot_product_scorer is not None else LinearPresenceHead(self.d_model)
|
||||
)
|
||||
|
||||
self.cross_attend_prompt = cross_attend_prompt
|
||||
if self.cross_attend_prompt is not None:
|
||||
self.cross_attn_norm = nn.LayerNorm(self.d_model)
|
||||
|
||||
self.semantic_seg_head = nn.Conv2d(self.pixel_decoder.out_dim, 1, kernel_size=1)
|
||||
self.instance_seg_head = nn.Conv2d(self.pixel_decoder.out_dim, self.d_model, kernel_size=1)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
backbone_feats: list[torch.Tensor],
|
||||
obj_queries: torch.Tensor,
|
||||
encoder_hidden_states: torch.Tensor = None,
|
||||
prompt: torch.Tensor = None,
|
||||
prompt_mask: torch.Tensor = None,
|
||||
**kwargs,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Forward pass of the UniversalSegmentationHead."""
|
||||
assert encoder_hidden_states is not None
|
||||
bs = encoder_hidden_states.shape[1]
|
||||
|
||||
if self.cross_attend_prompt is not None:
|
||||
tgt2 = self.cross_attn_norm(encoder_hidden_states)
|
||||
tgt2 = self.cross_attend_prompt(
|
||||
query=tgt2,
|
||||
key=prompt.to(tgt2.dtype),
|
||||
value=prompt.to(tgt2.dtype),
|
||||
key_padding_mask=prompt_mask,
|
||||
need_weights=False,
|
||||
)[0]
|
||||
encoder_hidden_states = tgt2 + encoder_hidden_states
|
||||
|
||||
presence_logit = None
|
||||
if self.presence_head is not None:
|
||||
pooled_enc = encoder_hidden_states.mean(0)
|
||||
presence_logit = (
|
||||
self.presence_head(
|
||||
pooled_enc.view(1, bs, 1, self.d_model),
|
||||
prompt=prompt,
|
||||
prompt_mask=prompt_mask,
|
||||
)
|
||||
.squeeze(0)
|
||||
.squeeze(1)
|
||||
)
|
||||
|
||||
pixel_embed = self._embed_pixels(backbone_feats=backbone_feats, encoder_hidden_states=encoder_hidden_states)
|
||||
|
||||
instance_embeds = self.instance_seg_head(pixel_embed)
|
||||
|
||||
if self.no_dec:
|
||||
mask_pred = self.mask_predictor(instance_embeds)
|
||||
elif self.aux_masks:
|
||||
mask_pred = self.mask_predictor(obj_queries, instance_embeds)
|
||||
else:
|
||||
mask_pred = self.mask_predictor(obj_queries[-1], instance_embeds)
|
||||
|
||||
return {
|
||||
"pred_masks": mask_pred,
|
||||
"semantic_seg": self.semantic_seg_head(pixel_embed),
|
||||
"presence_logit": presence_logit,
|
||||
}
|
||||
199
ultralytics/models/sam/sam3/model_misc.py
Executable file
199
ultralytics/models/sam/sam3/model_misc.py
Executable file
@@ -0,0 +1,199 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
"""Various utility models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
|
||||
|
||||
class DotProductScoring(torch.nn.Module):
|
||||
"""A module that computes dot-product scores between query features and pooled prompt embeddings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model,
|
||||
d_proj,
|
||||
prompt_mlp=None,
|
||||
clamp_logits=True,
|
||||
clamp_max_val=12.0,
|
||||
):
|
||||
"""Initialize the DotProductScoring module."""
|
||||
super().__init__()
|
||||
self.d_proj = d_proj
|
||||
assert isinstance(prompt_mlp, torch.nn.Module) or prompt_mlp is None
|
||||
self.prompt_mlp = prompt_mlp # an optional MLP projection for prompt
|
||||
self.prompt_proj = torch.nn.Linear(d_model, d_proj)
|
||||
self.hs_proj = torch.nn.Linear(d_model, d_proj)
|
||||
self.scale = float(1.0 / np.sqrt(d_proj))
|
||||
self.clamp_logits = clamp_logits
|
||||
if self.clamp_logits:
|
||||
self.clamp_max_val = clamp_max_val
|
||||
|
||||
@staticmethod
|
||||
def mean_pool_text(prompt, prompt_mask):
|
||||
"""Mean-pool the prompt embeddings over the valid tokens only."""
|
||||
# is_valid has shape (seq, bs, 1), where 1 is valid and 0 is padding
|
||||
is_valid = (~prompt_mask).to(prompt.dtype).permute(1, 0)[..., None]
|
||||
# num_valid has shape (bs, 1)
|
||||
num_valid = torch.clamp(torch.sum(is_valid, dim=0), min=1.0)
|
||||
# mean pool over all the valid tokens -- pooled_prompt has shape (bs, proj_dim)
|
||||
pooled_prompt = (prompt * is_valid).sum(dim=0) / num_valid
|
||||
return pooled_prompt
|
||||
|
||||
def forward(self, hs, prompt, prompt_mask):
|
||||
"""Compute dot-product scores between hs and prompt."""
|
||||
# hs has shape (num_layer, bs, num_query, d_model)
|
||||
# prompt has shape (seq, bs, d_model)
|
||||
# prompt_mask has shape (bs, seq), where 1 is valid and 0 is padding
|
||||
assert hs.dim() == 4 and prompt.dim() == 3 and prompt_mask.dim() == 2
|
||||
|
||||
# apply MLP on prompt if specified
|
||||
if self.prompt_mlp is not None:
|
||||
prompt = self.prompt_mlp(prompt.to(hs.dtype))
|
||||
|
||||
# first, get the mean-pooled version of the prompt
|
||||
pooled_prompt = self.mean_pool_text(prompt, prompt_mask)
|
||||
|
||||
# then, project pooled_prompt and hs to d_proj dimensions
|
||||
proj_pooled_prompt = self.prompt_proj(pooled_prompt) # (bs, d_proj)
|
||||
proj_hs = self.hs_proj(hs) # (num_layer, bs, num_query, d_proj)
|
||||
|
||||
# finally, get dot-product scores of shape (num_layer, bs, num_query, 1)
|
||||
scores = torch.matmul(proj_hs, proj_pooled_prompt.unsqueeze(-1))
|
||||
scores *= self.scale
|
||||
|
||||
# clamp scores to a max value to avoid numerical issues in loss or matcher
|
||||
if self.clamp_logits:
|
||||
scores.clamp_(min=-self.clamp_max_val, max=self.clamp_max_val)
|
||||
|
||||
return scores
|
||||
|
||||
|
||||
class LayerScale(nn.Module):
|
||||
"""LayerScale module for per-channel scaling of layer outputs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
init_values: float | Tensor = 1e-5,
|
||||
inplace: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the LayerScale module."""
|
||||
super().__init__()
|
||||
self.inplace = inplace
|
||||
self.gamma = nn.Parameter(init_values * torch.ones(dim))
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Apply LayerScale to the input tensor."""
|
||||
return x.mul_(self.gamma) if self.inplace else x * self.gamma
|
||||
|
||||
|
||||
class TransformerWrapper(nn.Module):
|
||||
"""A wrapper for the transformer consisting of an encoder and a decoder."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
encoder,
|
||||
decoder,
|
||||
d_model: int,
|
||||
two_stage_type="none", # ["none"] only for now
|
||||
pos_enc_at_input_dec=True,
|
||||
):
|
||||
"""Initialize the TransformerWrapper."""
|
||||
super().__init__()
|
||||
self.encoder = encoder
|
||||
self.decoder = decoder
|
||||
self.num_queries = decoder.num_queries if decoder is not None else None
|
||||
self.pos_enc_at_input_dec = pos_enc_at_input_dec
|
||||
|
||||
# for two stage
|
||||
assert two_stage_type in ["none"], f"unknown param {two_stage_type} of two_stage_type"
|
||||
self.two_stage_type = two_stage_type
|
||||
|
||||
self._reset_parameters()
|
||||
self.d_model = d_model
|
||||
|
||||
def _reset_parameters(self):
|
||||
"""Initialize the parameters of the model."""
|
||||
for n, p in self.named_parameters():
|
||||
if p.dim() > 1:
|
||||
if "box_embed" not in n and "query_embed" not in n and "reference_points" not in n:
|
||||
nn.init.xavier_uniform_(p)
|
||||
|
||||
|
||||
def get_valid_ratio(mask):
|
||||
"""Compute the valid ratio of height and width from the mask."""
|
||||
_, H, W = mask.shape
|
||||
valid_H = torch.sum(~mask[:, :, 0], 1)
|
||||
valid_W = torch.sum(~mask[:, 0, :], 1)
|
||||
valid_ratio_h = valid_H.float() / H
|
||||
valid_ratio_w = valid_W.float() / W
|
||||
valid_ratio = torch.stack([valid_ratio_w, valid_ratio_h], -1)
|
||||
return valid_ratio
|
||||
|
||||
|
||||
def gen_sineembed_for_position(pos_tensor: torch.Tensor, num_feats: int = 256):
|
||||
"""Generate sinusoidal position embeddings for 2D or 4D coordinate tensors.
|
||||
|
||||
This function creates sinusoidal embeddings using sine and cosine functions at different frequencies, similar to the
|
||||
positional encoding used in Transformer models. It supports both 2D position tensors (x, y) and 4D tensors (x, y, w,
|
||||
h) for bounding box coordinates.
|
||||
|
||||
Args:
|
||||
pos_tensor (torch.Tensor): Input position tensor of shape (n_query, bs, 2) for 2D coordinates or (n_query, bs,
|
||||
4) for 4D coordinates (bounding boxes).
|
||||
num_feats (int): Number of feature dimensions for the output embedding. Must be even. Defaults to 256.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Sinusoidal position embeddings of shape (n_query, bs, num_feats) for 2D input or (n_query, bs,
|
||||
num_feats * 2) for 4D input.
|
||||
|
||||
Raises:
|
||||
AssertionError: If num_feats is not even.
|
||||
ValueError: If pos_tensor.size(-1) is not 2 or 4.
|
||||
|
||||
Examples:
|
||||
>>> pos_2d = torch.rand(100, 8, 2) # 100 queries, batch size 8, 2D coordinates
|
||||
>>> embeddings_2d = gen_sineembed_for_position(pos_2d, num_feats=256)
|
||||
>>> embeddings_2d.shape
|
||||
torch.Size([100, 8, 256])
|
||||
>>> pos_4d = torch.rand(50, 4, 4) # 50 queries, batch size 4, 4D coordinates
|
||||
>>> embeddings_4d = gen_sineembed_for_position(pos_4d, num_feats=128)
|
||||
>>> embeddings_4d.shape
|
||||
torch.Size([50, 4, 256])
|
||||
"""
|
||||
assert num_feats % 2 == 0
|
||||
num_feats = num_feats // 2
|
||||
# n_query, bs, _ = pos_tensor.size()
|
||||
# sineembed_tensor = torch.zeros(n_query, bs, 256)
|
||||
scale = 2 * math.pi
|
||||
dim_t = torch.arange(num_feats, dtype=pos_tensor.dtype, device=pos_tensor.device)
|
||||
dim_t = 10000 ** (2 * (torch.div(dim_t, 2, rounding_mode="floor")) / num_feats)
|
||||
x_embed = pos_tensor[:, :, 0] * scale
|
||||
y_embed = pos_tensor[:, :, 1] * scale
|
||||
pos_x = x_embed[:, :, None] / dim_t
|
||||
pos_y = y_embed[:, :, None] / dim_t
|
||||
pos_x = torch.stack((pos_x[:, :, 0::2].sin(), pos_x[:, :, 1::2].cos()), dim=3).flatten(2)
|
||||
pos_y = torch.stack((pos_y[:, :, 0::2].sin(), pos_y[:, :, 1::2].cos()), dim=3).flatten(2)
|
||||
if pos_tensor.size(-1) == 2:
|
||||
pos = torch.cat((pos_y, pos_x), dim=2)
|
||||
elif pos_tensor.size(-1) == 4:
|
||||
w_embed = pos_tensor[:, :, 2] * scale
|
||||
pos_w = w_embed[:, :, None] / dim_t
|
||||
pos_w = torch.stack((pos_w[:, :, 0::2].sin(), pos_w[:, :, 1::2].cos()), dim=3).flatten(2)
|
||||
|
||||
h_embed = pos_tensor[:, :, 3] * scale
|
||||
pos_h = h_embed[:, :, None] / dim_t
|
||||
pos_h = torch.stack((pos_h[:, :, 0::2].sin(), pos_h[:, :, 1::2].cos()), dim=3).flatten(2)
|
||||
|
||||
pos = torch.cat((pos_y, pos_x, pos_w, pos_h), dim=2)
|
||||
else:
|
||||
raise ValueError(f"Unknown pos_tensor shape(-1):{pos_tensor.size(-1)}")
|
||||
return pos
|
||||
131
ultralytics/models/sam/sam3/necks.py
Executable file
131
ultralytics/models/sam/sam3/necks.py
Executable file
@@ -0,0 +1,131 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
"""Necks are the interface between a vision backbone and the rest of the detection model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class Sam3DualViTDetNeck(nn.Module):
|
||||
"""A neck that implements a simple FPN as in ViTDet, with support for dual necks (for SAM3 and SAM2)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
trunk: nn.Module,
|
||||
position_encoding: nn.Module,
|
||||
d_model: int,
|
||||
scale_factors=(4.0, 2.0, 1.0, 0.5),
|
||||
add_sam2_neck: bool = False,
|
||||
):
|
||||
"""
|
||||
SimpleFPN neck a la ViTDet
|
||||
(From detectron2, very lightly adapted)
|
||||
It supports a "dual neck" setting, where we have two identical necks (for SAM3 and SAM2), with different weights.
|
||||
|
||||
:param trunk: the backbone
|
||||
:param position_encoding: the positional encoding to use
|
||||
:param d_model: the dimension of the model
|
||||
:param scale_factors: tuple of scale factors for each FPN level
|
||||
:param add_sam2_neck: whether to add a second neck for SAM2
|
||||
"""
|
||||
super().__init__()
|
||||
self.trunk = trunk
|
||||
self.position_encoding = position_encoding
|
||||
self.convs = nn.ModuleList()
|
||||
|
||||
self.scale_factors = scale_factors
|
||||
use_bias = True
|
||||
dim: int = self.trunk.channel_list[-1]
|
||||
|
||||
for _, scale in enumerate(scale_factors):
|
||||
current = nn.Sequential()
|
||||
|
||||
if scale == 4.0:
|
||||
current.add_module(
|
||||
"dconv_2x2_0",
|
||||
nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2),
|
||||
)
|
||||
current.add_module(
|
||||
"gelu",
|
||||
nn.GELU(),
|
||||
)
|
||||
current.add_module(
|
||||
"dconv_2x2_1",
|
||||
nn.ConvTranspose2d(dim // 2, dim // 4, kernel_size=2, stride=2),
|
||||
)
|
||||
out_dim = dim // 4
|
||||
elif scale == 2.0:
|
||||
current.add_module(
|
||||
"dconv_2x2",
|
||||
nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2),
|
||||
)
|
||||
out_dim = dim // 2
|
||||
elif scale == 1.0:
|
||||
out_dim = dim
|
||||
elif scale == 0.5:
|
||||
current.add_module(
|
||||
"maxpool_2x2",
|
||||
nn.MaxPool2d(kernel_size=2, stride=2),
|
||||
)
|
||||
out_dim = dim
|
||||
else:
|
||||
raise NotImplementedError(f"scale_factor={scale} is not supported yet.")
|
||||
|
||||
current.add_module(
|
||||
"conv_1x1",
|
||||
nn.Conv2d(
|
||||
in_channels=out_dim,
|
||||
out_channels=d_model,
|
||||
kernel_size=1,
|
||||
bias=use_bias,
|
||||
),
|
||||
)
|
||||
current.add_module(
|
||||
"conv_3x3",
|
||||
nn.Conv2d(
|
||||
in_channels=d_model,
|
||||
out_channels=d_model,
|
||||
kernel_size=3,
|
||||
padding=1,
|
||||
bias=use_bias,
|
||||
),
|
||||
)
|
||||
self.convs.append(current)
|
||||
|
||||
self.sam2_convs = None
|
||||
if add_sam2_neck:
|
||||
# Assumes sam2 neck is just a clone of the original neck
|
||||
self.sam2_convs = deepcopy(self.convs)
|
||||
|
||||
def forward(
|
||||
self, tensor_list: list[torch.Tensor]
|
||||
) -> tuple[list[torch.Tensor], list[torch.Tensor], list[torch.Tensor] | None, list[torch.Tensor] | None]:
|
||||
"""Get feature maps and positional encodings from the neck."""
|
||||
xs = self.trunk(tensor_list)
|
||||
x = xs[-1] # simpleFPN
|
||||
sam3_out, sam3_pos = self.sam_forward_feature_levels(x, self.convs)
|
||||
if self.sam2_convs is None:
|
||||
return sam3_out, sam3_pos, None, None
|
||||
sam2_out, sam2_pos = self.sam_forward_feature_levels(x, self.sam2_convs)
|
||||
return sam3_out, sam3_pos, sam2_out, sam2_pos
|
||||
|
||||
def sam_forward_feature_levels(
|
||||
self, x: torch.Tensor, convs: nn.ModuleList
|
||||
) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
|
||||
"""Run neck convolutions and compute positional encodings for each feature level."""
|
||||
outs, poss = [], []
|
||||
for conv in convs:
|
||||
feat = conv(x)
|
||||
outs.append(feat)
|
||||
poss.append(self.position_encoding(feat).to(feat.dtype))
|
||||
return outs, poss
|
||||
|
||||
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
|
||||
"""Set the image size for the trunk backbone."""
|
||||
self.trunk.set_imgsz(imgsz)
|
||||
339
ultralytics/models/sam/sam3/sam3_image.py
Executable file
339
ultralytics/models/sam/sam3/sam3_image.py
Executable file
@@ -0,0 +1,339 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.nn.modules.utils import inverse_sigmoid
|
||||
from ultralytics.utils.ops import xywh2xyxy
|
||||
|
||||
from ..modules.sam import SAM2Model
|
||||
from .geometry_encoders import Prompt
|
||||
from .vl_combiner import SAM3VLBackbone
|
||||
|
||||
|
||||
def _update_out(out, out_name, out_value, auxiliary=True, update_aux=True):
|
||||
"""Helper function to update output dictionary with main and auxiliary outputs."""
|
||||
out[out_name] = out_value[-1] if auxiliary else out_value
|
||||
if auxiliary and update_aux:
|
||||
if "aux_outputs" not in out:
|
||||
out["aux_outputs"] = [{} for _ in range(len(out_value) - 1)]
|
||||
assert len(out["aux_outputs"]) == len(out_value) - 1
|
||||
for aux_output, aux_value in zip(out["aux_outputs"], out_value[:-1]):
|
||||
aux_output[out_name] = aux_value
|
||||
|
||||
|
||||
class SAM3SemanticModel(torch.nn.Module):
|
||||
"""SAM3 model for semantic segmentation with vision-language backbone."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
backbone: SAM3VLBackbone,
|
||||
transformer,
|
||||
input_geometry_encoder,
|
||||
segmentation_head=None,
|
||||
num_feature_levels=1,
|
||||
o2m_mask_predict=True,
|
||||
dot_prod_scoring=None,
|
||||
use_instance_query: bool = True,
|
||||
multimask_output: bool = True,
|
||||
use_act_checkpoint_seg_head: bool = True,
|
||||
matcher=None,
|
||||
use_dot_prod_scoring=True,
|
||||
supervise_joint_box_scores: bool = False, # only relevant if using presence token/score
|
||||
detach_presence_in_joint_score: bool = False, # only relevant if using presence token/score
|
||||
separate_scorer_for_instance: bool = False,
|
||||
num_interactive_steps_val: int = 0,
|
||||
):
|
||||
"""Initialize the SAM3SemanticModel."""
|
||||
super().__init__()
|
||||
self.backbone = backbone
|
||||
self.geometry_encoder = input_geometry_encoder
|
||||
self.transformer = transformer
|
||||
self.hidden_dim = transformer.d_model
|
||||
self.num_feature_levels = num_feature_levels
|
||||
self.segmentation_head = segmentation_head
|
||||
|
||||
self.o2m_mask_predict = o2m_mask_predict
|
||||
|
||||
self.dot_prod_scoring = dot_prod_scoring
|
||||
self.use_act_checkpoint_seg_head = use_act_checkpoint_seg_head
|
||||
self.matcher = matcher
|
||||
|
||||
self.num_interactive_steps_val = num_interactive_steps_val
|
||||
self.use_dot_prod_scoring = use_dot_prod_scoring
|
||||
|
||||
if self.use_dot_prod_scoring:
|
||||
assert dot_prod_scoring is not None
|
||||
self.dot_prod_scoring = dot_prod_scoring
|
||||
self.instance_dot_prod_scoring = None
|
||||
if separate_scorer_for_instance:
|
||||
self.instance_dot_prod_scoring = deepcopy(dot_prod_scoring)
|
||||
else:
|
||||
self.class_embed = torch.nn.Linear(self.hidden_dim, 1)
|
||||
self.instance_class_embed = None
|
||||
if separate_scorer_for_instance:
|
||||
self.instance_class_embed = deepcopy(self.class_embed)
|
||||
|
||||
self.supervise_joint_box_scores = supervise_joint_box_scores
|
||||
self.detach_presence_in_joint_score = detach_presence_in_joint_score
|
||||
|
||||
# verify the number of queries for O2O and O2M
|
||||
num_o2o_static = self.transformer.decoder.num_queries
|
||||
num_o2m_static = self.transformer.decoder.num_o2m_queries
|
||||
assert num_o2m_static == (num_o2o_static if self.transformer.decoder.dac else 0)
|
||||
self.dac = self.transformer.decoder.dac
|
||||
|
||||
self.use_instance_query = use_instance_query
|
||||
self.multimask_output = multimask_output
|
||||
|
||||
self.text_embeddings = {}
|
||||
self.names = []
|
||||
|
||||
def _encode_prompt(
|
||||
self,
|
||||
img_feats,
|
||||
img_pos_embeds,
|
||||
vis_feat_sizes,
|
||||
geometric_prompt,
|
||||
visual_prompt_embed=None,
|
||||
visual_prompt_mask=None,
|
||||
prev_mask_pred=None,
|
||||
):
|
||||
"""Encode the geometric and visual prompts."""
|
||||
if prev_mask_pred is not None:
|
||||
img_feats = [img_feats[-1] + prev_mask_pred]
|
||||
# Encode geometry
|
||||
geo_feats, geo_masks = self.geometry_encoder(
|
||||
geo_prompt=geometric_prompt,
|
||||
img_feats=img_feats,
|
||||
img_sizes=vis_feat_sizes,
|
||||
img_pos_embeds=img_pos_embeds,
|
||||
)
|
||||
if visual_prompt_embed is None:
|
||||
visual_prompt_embed = torch.zeros((0, *geo_feats.shape[1:]), device=geo_feats.device)
|
||||
visual_prompt_mask = torch.zeros(
|
||||
(*geo_masks.shape[:-1], 0),
|
||||
device=geo_masks.device,
|
||||
dtype=geo_masks.dtype,
|
||||
)
|
||||
prompt = torch.cat([geo_feats, visual_prompt_embed], dim=0)
|
||||
prompt_mask = torch.cat([geo_masks, visual_prompt_mask], dim=1)
|
||||
return prompt, prompt_mask
|
||||
|
||||
def _run_encoder(
|
||||
self,
|
||||
img_feats,
|
||||
img_pos_embeds,
|
||||
vis_feat_sizes,
|
||||
prompt,
|
||||
prompt_mask,
|
||||
encoder_extra_kwargs: dict | None = None,
|
||||
):
|
||||
"""Run the transformer encoder."""
|
||||
# Run the encoder
|
||||
# make a copy of the image feature lists since the encoder may modify these lists in-place
|
||||
memory = self.transformer.encoder(
|
||||
src=img_feats.copy(),
|
||||
src_key_padding_mask=None,
|
||||
src_pos=img_pos_embeds.copy(),
|
||||
prompt=prompt,
|
||||
prompt_key_padding_mask=prompt_mask,
|
||||
feat_sizes=vis_feat_sizes,
|
||||
encoder_extra_kwargs=encoder_extra_kwargs,
|
||||
)
|
||||
encoder_out = {
|
||||
# encoded image features
|
||||
"encoder_hidden_states": memory["memory"],
|
||||
"pos_embed": memory["pos_embed"],
|
||||
"padding_mask": memory["padding_mask"],
|
||||
"spatial_shapes": memory["spatial_shapes"],
|
||||
"valid_ratios": memory["valid_ratios"],
|
||||
"vis_feat_sizes": vis_feat_sizes,
|
||||
# encoded text features (or other prompts)
|
||||
"prompt_before_enc": prompt,
|
||||
"prompt_after_enc": memory.get("memory_text", prompt),
|
||||
"prompt_mask": prompt_mask,
|
||||
}
|
||||
return encoder_out
|
||||
|
||||
def _run_decoder(
|
||||
self,
|
||||
pos_embed,
|
||||
memory,
|
||||
src_mask,
|
||||
out,
|
||||
prompt,
|
||||
prompt_mask,
|
||||
encoder_out,
|
||||
):
|
||||
"""Run the transformer decoder."""
|
||||
bs = memory.shape[1]
|
||||
query_embed = self.transformer.decoder.query_embed.weight
|
||||
tgt = query_embed.unsqueeze(1).repeat(1, bs, 1)
|
||||
|
||||
hs, reference_boxes, dec_presence_out, _ = self.transformer.decoder(
|
||||
tgt=tgt,
|
||||
memory=memory,
|
||||
memory_key_padding_mask=src_mask,
|
||||
pos=pos_embed,
|
||||
reference_boxes=None,
|
||||
spatial_shapes=encoder_out["spatial_shapes"],
|
||||
valid_ratios=encoder_out["valid_ratios"],
|
||||
tgt_mask=None,
|
||||
memory_text=prompt,
|
||||
text_attention_mask=prompt_mask,
|
||||
apply_dac=False,
|
||||
)
|
||||
hs = hs.transpose(1, 2) # seq-first to batch-first
|
||||
reference_boxes = reference_boxes.transpose(1, 2) # seq-first to batch-first
|
||||
if dec_presence_out is not None:
|
||||
# seq-first to batch-first
|
||||
dec_presence_out = dec_presence_out.transpose(1, 2)
|
||||
self._update_scores_and_boxes(
|
||||
out,
|
||||
hs,
|
||||
reference_boxes,
|
||||
prompt,
|
||||
prompt_mask,
|
||||
dec_presence_out=dec_presence_out,
|
||||
)
|
||||
return out, hs
|
||||
|
||||
def _update_scores_and_boxes(
|
||||
self,
|
||||
out,
|
||||
hs,
|
||||
reference_boxes,
|
||||
prompt,
|
||||
prompt_mask,
|
||||
dec_presence_out=None,
|
||||
is_instance_prompt=False,
|
||||
):
|
||||
"""Update output dict with class scores and box predictions."""
|
||||
num_o2o = hs.size(2)
|
||||
# score prediction
|
||||
if self.use_dot_prod_scoring:
|
||||
dot_prod_scoring_head = self.dot_prod_scoring
|
||||
if is_instance_prompt and self.instance_dot_prod_scoring is not None:
|
||||
dot_prod_scoring_head = self.instance_dot_prod_scoring
|
||||
outputs_class = dot_prod_scoring_head(hs, prompt, prompt_mask)
|
||||
else:
|
||||
class_embed_head = self.class_embed
|
||||
if is_instance_prompt and self.instance_class_embed is not None:
|
||||
class_embed_head = self.instance_class_embed
|
||||
outputs_class = class_embed_head(hs)
|
||||
|
||||
# box prediction
|
||||
box_head = self.transformer.decoder.bbox_embed
|
||||
if is_instance_prompt and self.transformer.decoder.instance_bbox_embed is not None:
|
||||
box_head = self.transformer.decoder.instance_bbox_embed
|
||||
anchor_box_offsets = box_head(hs)
|
||||
reference_boxes_inv_sig = inverse_sigmoid(reference_boxes)
|
||||
outputs_coord = (reference_boxes_inv_sig + anchor_box_offsets).sigmoid()
|
||||
outputs_boxes_xyxy = xywh2xyxy(outputs_coord)
|
||||
|
||||
if dec_presence_out is not None:
|
||||
_update_out(out, "presence_logit_dec", dec_presence_out, update_aux=False)
|
||||
|
||||
if self.supervise_joint_box_scores:
|
||||
assert dec_presence_out is not None
|
||||
prob_dec_presence_out = dec_presence_out.clone().sigmoid()
|
||||
if self.detach_presence_in_joint_score:
|
||||
prob_dec_presence_out = prob_dec_presence_out.detach()
|
||||
|
||||
outputs_class = inverse_sigmoid(outputs_class.sigmoid() * prob_dec_presence_out.unsqueeze(2)).clamp(
|
||||
min=-10.0, max=10.0
|
||||
)
|
||||
|
||||
_update_out(out, "pred_logits", outputs_class[:, :, :num_o2o], update_aux=False)
|
||||
_update_out(out, "pred_boxes", outputs_coord[:, :, :num_o2o], update_aux=False)
|
||||
_update_out(out, "pred_boxes_xyxy", outputs_boxes_xyxy[:, :, :num_o2o], update_aux=False)
|
||||
|
||||
def _run_segmentation_heads(
|
||||
self,
|
||||
out,
|
||||
backbone_out,
|
||||
encoder_hidden_states,
|
||||
prompt,
|
||||
prompt_mask,
|
||||
hs,
|
||||
):
|
||||
"""Run segmentation heads and get masks."""
|
||||
if self.segmentation_head is not None:
|
||||
num_o2o = hs.size(2)
|
||||
obj_queries = hs if self.o2m_mask_predict else hs[:, :, :num_o2o]
|
||||
seg_head_outputs = self.segmentation_head(
|
||||
backbone_feats=backbone_out["backbone_fpn"],
|
||||
obj_queries=obj_queries,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
prompt=prompt,
|
||||
prompt_mask=prompt_mask,
|
||||
)
|
||||
for k, v in seg_head_outputs.items():
|
||||
if k in self.segmentation_head.instance_keys:
|
||||
_update_out(out, k, v[:, :num_o2o], auxiliary=False)
|
||||
else:
|
||||
out[k] = v
|
||||
else:
|
||||
backbone_out.pop("backbone_fpn", None)
|
||||
|
||||
def forward_grounding(
|
||||
self, backbone_out: dict[str, torch.Tensor], text_ids: torch.Tensor, geometric_prompt: Prompt = None
|
||||
):
|
||||
"""Forward pass for grounding (detection + segmentation) given input images and text."""
|
||||
backbone_out, img_feats, img_pos_embeds, vis_feat_sizes = SAM2Model._prepare_backbone_features(
|
||||
self, backbone_out, batch=len(text_ids)
|
||||
)
|
||||
backbone_out.update({k: v for k, v in self.text_embeddings.items()})
|
||||
with torch.profiler.record_function("SAM3Image._encode_prompt"):
|
||||
prompt, prompt_mask = self._encode_prompt(img_feats, img_pos_embeds, vis_feat_sizes, geometric_prompt)
|
||||
# index text features (note that regardless of early or late fusion, the batch size of
|
||||
# `txt_feats` is always the number of *prompts* in the encoder)
|
||||
txt_feats = backbone_out["language_features"][:, text_ids]
|
||||
txt_masks = backbone_out["language_mask"][text_ids]
|
||||
# encode text
|
||||
prompt = torch.cat([txt_feats, prompt], dim=0)
|
||||
prompt_mask = torch.cat([txt_masks, prompt_mask], dim=1)
|
||||
|
||||
# Run the encoder
|
||||
with torch.profiler.record_function("SAM3Image._run_encoder"):
|
||||
encoder_out = self._run_encoder(img_feats, img_pos_embeds, vis_feat_sizes, prompt, prompt_mask)
|
||||
out = {"backbone_out": backbone_out}
|
||||
|
||||
# Run the decoder
|
||||
with torch.profiler.record_function("SAM3Image._run_decoder"):
|
||||
out, hs = self._run_decoder(
|
||||
memory=encoder_out["encoder_hidden_states"],
|
||||
pos_embed=encoder_out["pos_embed"],
|
||||
src_mask=encoder_out["padding_mask"],
|
||||
out=out,
|
||||
prompt=prompt,
|
||||
prompt_mask=prompt_mask,
|
||||
encoder_out=encoder_out,
|
||||
)
|
||||
|
||||
# Run segmentation heads
|
||||
with torch.profiler.record_function("SAM3Image._run_segmentation_heads"):
|
||||
self._run_segmentation_heads(
|
||||
out=out,
|
||||
backbone_out=backbone_out,
|
||||
encoder_hidden_states=encoder_out["encoder_hidden_states"],
|
||||
prompt=prompt,
|
||||
prompt_mask=prompt_mask,
|
||||
hs=hs,
|
||||
)
|
||||
return out
|
||||
|
||||
def set_classes(self, text: list[str]):
|
||||
"""Set the text embeddings for the given class names."""
|
||||
self.text_embeddings = self.backbone.forward_text(text)
|
||||
self.names = text
|
||||
|
||||
def set_imgsz(self, imgsz: tuple[int, int]):
|
||||
"""Set the image size for the model."""
|
||||
self.backbone.set_imgsz(imgsz)
|
||||
307
ultralytics/models/sam/sam3/text_encoder_ve.py
Executable file
307
ultralytics/models/sam/sam3/text_encoder_ve.py
Executable file
@@ -0,0 +1,307 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Callable
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.checkpoint import checkpoint
|
||||
|
||||
from .model_misc import LayerScale
|
||||
|
||||
|
||||
class ResidualAttentionBlock(nn.Module):
|
||||
"""Transformer block with multi-head attention, layer normalization, and MLP feed-forward network."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
n_head: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
ls_init_value: float | None = None,
|
||||
act_layer: Callable[[], nn.Module] = nn.GELU,
|
||||
norm_layer: Callable[[int], nn.Module] = nn.LayerNorm,
|
||||
):
|
||||
"""Initialize residual attention block with configurable dimensions and normalization."""
|
||||
super().__init__()
|
||||
# Attention
|
||||
self.attn = nn.MultiheadAttention(d_model, n_head, batch_first=True)
|
||||
|
||||
# LayerNorm, LayerScale
|
||||
self.ln_1 = norm_layer(d_model)
|
||||
self.ln_2 = norm_layer(d_model)
|
||||
|
||||
self.ls_1 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
|
||||
self.ls_2 = LayerScale(d_model, ls_init_value) if ls_init_value is not None else nn.Identity()
|
||||
|
||||
# MLP
|
||||
mlp_width = int(d_model * mlp_ratio)
|
||||
self.mlp = nn.Sequential(
|
||||
OrderedDict(
|
||||
[
|
||||
("c_fc", nn.Linear(d_model, mlp_width)),
|
||||
("gelu", act_layer()),
|
||||
("c_proj", nn.Linear(mlp_width, d_model)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def attention(
|
||||
self, q_x: torch.Tensor, k_x: torch.Tensor = None, v_x: torch.Tensor = None, attn_mask: torch.Tensor = None
|
||||
) -> torch.Tensor:
|
||||
"""Compute multi-head attention with optional cross-attention support and masking."""
|
||||
k_x = k_x if k_x is not None else q_x
|
||||
v_x = v_x if v_x is not None else q_x
|
||||
if attn_mask is not None:
|
||||
# Leave boolean masks as is
|
||||
if not attn_mask.dtype == torch.bool:
|
||||
attn_mask = attn_mask.to(q_x.dtype)
|
||||
|
||||
return self.attn(q_x, k_x, v_x, need_weights=False, attn_mask=attn_mask)[0]
|
||||
|
||||
def forward(
|
||||
self, q_x: torch.Tensor, k_x: torch.Tensor = None, v_x: torch.Tensor = None, attn_mask: torch.Tensor = None
|
||||
) -> torch.Tensor:
|
||||
"""Apply residual attention with layer normalization and MLP, supporting optional cross-attention."""
|
||||
k_x = self.ln_1_kv(k_x) if hasattr(self, "ln_1_kv") and k_x is not None else None
|
||||
v_x = self.ln_1_kv(v_x) if hasattr(self, "ln_1_kv") and v_x is not None else None
|
||||
x = q_x + self.ls_1(self.attention(q_x=self.ln_1(q_x), k_x=k_x, v_x=v_x, attn_mask=attn_mask))
|
||||
x = x + self.ls_2(self.mlp(self.ln_2(x)))
|
||||
return x
|
||||
|
||||
|
||||
class Transformer(nn.Module):
|
||||
"""Stack of residual attention blocks forming a transformer encoder with optional gradient checkpointing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int,
|
||||
layers: int,
|
||||
heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
ls_init_value: float | None = None,
|
||||
act_layer: Callable[[], nn.Module] = nn.GELU,
|
||||
norm_layer: Callable[[int], nn.Module] = nn.LayerNorm,
|
||||
compile_mode: str | None = None,
|
||||
use_act_checkpoint: bool = False,
|
||||
):
|
||||
"""Initialize transformer with configurable depth, width, and optional compilation/checkpointing."""
|
||||
super().__init__()
|
||||
self.width = width
|
||||
self.layers = layers
|
||||
self.grad_checkpointing = use_act_checkpoint
|
||||
self.resblocks = nn.ModuleList(
|
||||
[
|
||||
ResidualAttentionBlock(
|
||||
width,
|
||||
heads,
|
||||
mlp_ratio,
|
||||
ls_init_value=ls_init_value,
|
||||
act_layer=act_layer,
|
||||
norm_layer=norm_layer,
|
||||
)
|
||||
for _ in range(layers)
|
||||
]
|
||||
)
|
||||
|
||||
if compile_mode is not None:
|
||||
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
|
||||
if self.grad_checkpointing:
|
||||
torch._dynamo.config.optimize_ddp = False
|
||||
|
||||
def forward(self, x: torch.Tensor, attn_mask: torch.Tensor = None) -> torch.Tensor:
|
||||
"""Process input through all transformer blocks with optional gradient checkpointing during training."""
|
||||
for _, r in enumerate(self.resblocks):
|
||||
if self.grad_checkpointing and not torch.jit.is_scripting() and self.training:
|
||||
x = checkpoint(r, x, None, None, attn_mask, use_reentrant=False)
|
||||
else:
|
||||
x = r(x, attn_mask=attn_mask)
|
||||
return x
|
||||
|
||||
|
||||
def text_global_pool(
|
||||
x: torch.Tensor, text: torch.Tensor = None, pool_type: str = "argmax"
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Extract pooled representation and tokens from text embeddings using specified pooling strategy
|
||||
(first/last/argmax/none).
|
||||
"""
|
||||
if pool_type == "first":
|
||||
pooled, tokens = x[:, 0], x[:, 1:]
|
||||
elif pool_type == "last":
|
||||
pooled, tokens = x[:, -1], x[:, :-1]
|
||||
elif pool_type == "argmax":
|
||||
# take features from the eot embedding (eot_token is the highest number in each sequence)
|
||||
assert text is not None
|
||||
pooled, tokens = x[torch.arange(x.shape[0]), text.argmax(dim=-1)], x
|
||||
else:
|
||||
pooled = tokens = x
|
||||
return pooled, tokens
|
||||
|
||||
|
||||
class TextTransformer(nn.Module):
|
||||
"""Text transformer encoder with causal masking and flexible pooling strategies."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context_length: int = 77,
|
||||
vocab_size: int = 49408,
|
||||
width: int = 512,
|
||||
heads: int = 8,
|
||||
layers: int = 12,
|
||||
mlp_ratio: float = 4.0,
|
||||
ls_init_value: float | None = None,
|
||||
output_dim: int = 512,
|
||||
no_causal_mask: bool = False,
|
||||
pool_type: str = "none", # no pooling
|
||||
proj_bias: bool = False,
|
||||
act_layer: Callable = nn.GELU,
|
||||
norm_layer: Callable = nn.LayerNorm,
|
||||
output_tokens: bool = False,
|
||||
use_ln_post: bool = True,
|
||||
compile_mode: str | None = None,
|
||||
use_act_checkpoint: bool = False,
|
||||
):
|
||||
"""Initialize text transformer with embedding layers, transformer blocks, and pooling options."""
|
||||
super().__init__()
|
||||
assert pool_type in ("first", "last", "argmax", "none")
|
||||
self.output_tokens = output_tokens
|
||||
self.num_pos = self.context_length = context_length
|
||||
self.vocab_size = vocab_size
|
||||
self.width = width
|
||||
self.output_dim = output_dim
|
||||
self.heads = heads
|
||||
self.pool_type = pool_type
|
||||
|
||||
self.token_embedding = nn.Embedding(self.vocab_size, width)
|
||||
self.positional_embedding = nn.Parameter(torch.empty(self.num_pos, width))
|
||||
self.transformer = Transformer(
|
||||
width=width,
|
||||
layers=layers,
|
||||
heads=heads,
|
||||
mlp_ratio=mlp_ratio,
|
||||
ls_init_value=ls_init_value,
|
||||
act_layer=act_layer,
|
||||
norm_layer=norm_layer,
|
||||
compile_mode=compile_mode,
|
||||
use_act_checkpoint=use_act_checkpoint,
|
||||
)
|
||||
self.ln_final = norm_layer(width) if use_ln_post else nn.Identity()
|
||||
if no_causal_mask:
|
||||
self.attn_mask = None
|
||||
else:
|
||||
self.register_buffer("attn_mask", self.build_causal_mask(), persistent=False)
|
||||
if proj_bias:
|
||||
self.text_projection = nn.Linear(width, output_dim)
|
||||
else:
|
||||
self.text_projection = nn.Parameter(torch.empty(width, output_dim))
|
||||
|
||||
def build_causal_mask(self) -> torch.Tensor:
|
||||
"""Create a causal attention mask to prevent attention to future tokens."""
|
||||
# lazily create causal attention mask, with full attention between the tokens
|
||||
# pytorch uses additive attention mask; fill with -inf
|
||||
mask = torch.empty(self.num_pos, self.num_pos)
|
||||
mask.fill_(float("-inf"))
|
||||
mask.triu_(1) # zero out the lower diagonal
|
||||
return mask
|
||||
|
||||
def forward(self, text: torch.Tensor) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Forward pass through the text transformer, returning pooled output and optionally token embeddings."""
|
||||
seq_len = text.shape[1]
|
||||
x = self.token_embedding(text) # [batch_size, n_ctx, d_model]
|
||||
|
||||
attn_mask = self.attn_mask
|
||||
if attn_mask is not None:
|
||||
attn_mask = attn_mask[:seq_len, :seq_len]
|
||||
|
||||
x = x + self.positional_embedding[:seq_len]
|
||||
x = self.transformer(x, attn_mask=attn_mask)
|
||||
|
||||
x = self.ln_final(x)
|
||||
pooled, tokens = text_global_pool(x, text, pool_type=self.pool_type)
|
||||
if self.text_projection is not None:
|
||||
if isinstance(self.text_projection, nn.Linear):
|
||||
pooled = self.text_projection(pooled)
|
||||
else:
|
||||
pooled = pooled @ self.text_projection
|
||||
if self.output_tokens:
|
||||
return pooled, tokens
|
||||
return pooled
|
||||
|
||||
|
||||
class VETextEncoder(nn.Module):
|
||||
"""Text encoder for Vision Encoder (VE) models, combining a text transformer and a linear resizer."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int,
|
||||
tokenizer: Callable,
|
||||
width: int = 1024,
|
||||
heads: int = 16,
|
||||
layers: int = 24,
|
||||
context_length: int = 32,
|
||||
vocab_size: int = 49408,
|
||||
use_ln_post: bool = True,
|
||||
compile_mode: str | None = None,
|
||||
use_act_checkpoint: bool = True,
|
||||
):
|
||||
"""Initialize VE text encoder with a text transformer and a linear resizer to match decoder dimensions."""
|
||||
super().__init__()
|
||||
self.context_length = context_length
|
||||
self.use_ln_post = use_ln_post
|
||||
self.tokenizer = tokenizer
|
||||
|
||||
self.encoder = TextTransformer(
|
||||
context_length=self.context_length,
|
||||
vocab_size=vocab_size,
|
||||
width=width,
|
||||
heads=heads,
|
||||
layers=layers,
|
||||
# we want the tokens, not just the pooled output
|
||||
output_tokens=True,
|
||||
use_ln_post=use_ln_post,
|
||||
compile_mode=compile_mode,
|
||||
use_act_checkpoint=use_act_checkpoint,
|
||||
)
|
||||
self.resizer = nn.Linear(self.encoder.width, d_model)
|
||||
|
||||
def forward(
|
||||
self, text: list[str] | tuple[torch.Tensor, torch.Tensor, dict], input_boxes: list | None = None
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Encode text input, either raw strings or pre-encoded tensors, and resize to match decoder dimensions."""
|
||||
if isinstance(text[0], str):
|
||||
# no use case for this
|
||||
assert input_boxes is None or len(input_boxes) == 0, "not supported"
|
||||
|
||||
# Encode the text
|
||||
tokenized = self.tokenizer(text, context_length=self.context_length).to(
|
||||
self.resizer.weight.device
|
||||
) # [b, seq_len]
|
||||
text_attention_mask = (tokenized != 0).bool()
|
||||
|
||||
# manually embed the tokens
|
||||
inputs_embeds = self.encoder.token_embedding(tokenized) # [b, seq_len, d=1024]
|
||||
_, text_memory = self.encoder(tokenized) # [b, seq_len, d=1024]
|
||||
|
||||
assert text_memory.shape[1] == inputs_embeds.shape[1]
|
||||
# Invert attention mask because its the opposite in pytorch transformer
|
||||
text_attention_mask = text_attention_mask.ne(1)
|
||||
# Transpose memory because pytorch's attention expects sequence first
|
||||
text_memory = text_memory.transpose(0, 1)
|
||||
# Resize the encoder hidden states to be of the same d_model as the decoder
|
||||
text_memory_resized = self.resizer(text_memory)
|
||||
else:
|
||||
# The text is already encoded, use as is.
|
||||
text_attention_mask, text_memory_resized, tokenized = text
|
||||
inputs_embeds = tokenized["inputs_embeds"]
|
||||
assert input_boxes is None or len(input_boxes) == 0, "Can't replace boxes in text if it's already encoded"
|
||||
|
||||
# Note that the input_embeds are returned in pytorch's convention (sequence first)
|
||||
return (
|
||||
text_attention_mask,
|
||||
text_memory_resized,
|
||||
inputs_embeds.transpose(0, 1),
|
||||
)
|
||||
549
ultralytics/models/sam/sam3/vitdet.py
Executable file
549
ultralytics/models/sam/sam3/vitdet.py
Executable file
@@ -0,0 +1,549 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
"""
|
||||
ViTDet backbone adapted from Detectron2.
|
||||
This module implements Vision Transformer (ViT) backbone for object detection.
|
||||
|
||||
Rope embedding code adopted from:
|
||||
1. https://github.com/meta-llama/codellama/blob/main/llama/model.py
|
||||
2. https://github.com/naver-ai/rope-vit
|
||||
3. https://github.com/lucidrains/rotary-embedding-torch
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.checkpoint as checkpoint
|
||||
from torch import Tensor
|
||||
|
||||
from ultralytics.models.sam.modules.blocks import PatchEmbed
|
||||
from ultralytics.models.sam.modules.utils import (
|
||||
apply_rotary_enc,
|
||||
compute_axial_cis,
|
||||
concat_rel_pos,
|
||||
get_abs_pos,
|
||||
window_partition,
|
||||
window_unpartition,
|
||||
)
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
|
||||
from .model_misc import LayerScale
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
"""Multi-head Attention block with relative position embeddings and 2d-rope."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int = 8,
|
||||
qkv_bias: bool = True,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
input_size: tuple[int, int] | None = None,
|
||||
cls_token: bool = False,
|
||||
use_rope: bool = False,
|
||||
rope_theta: float = 10000.0,
|
||||
rope_pt_size: tuple[int, int] | None = None,
|
||||
rope_interp: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
input_size (tuple[int, int] or None): Input resolution for calculating the relative positional parameter
|
||||
size or rope size.
|
||||
cls_token (bool): Whether a cls_token is present.
|
||||
use_rope (bool): Whether to use rope 2d (independent of use_rel_pos, as it can be used together).
|
||||
rope_theta (float): Control frequencies of rope.
|
||||
rope_pt_size (tuple[int, int] or None): Size of rope in previous stage of training, needed for interpolation
|
||||
or tiling.
|
||||
rope_interp (bool): Whether to interpolate (or extrapolate) rope to match input size.
|
||||
"""
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = dim // num_heads
|
||||
self.scale = self.head_dim**-0.5
|
||||
self.cls_token = cls_token
|
||||
|
||||
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
||||
self.proj = nn.Linear(dim, dim)
|
||||
|
||||
# rel_pos embeddings and rope
|
||||
self.use_rel_pos = use_rel_pos
|
||||
self.input_size = input_size
|
||||
|
||||
self.use_rope = use_rope
|
||||
self.rope_theta = rope_theta
|
||||
self.rope_pt_size = rope_pt_size
|
||||
self.rope_interp = rope_interp
|
||||
|
||||
# init rel_pos embeddings and rope
|
||||
self._setup_rel_pos(rel_pos_zero_init, input_size)
|
||||
self._setup_rope_freqs(input_size)
|
||||
|
||||
def _setup_rel_pos(self, rel_pos_zero_init: bool = True, input_size: tuple[int, int] | None = None) -> None:
|
||||
"""Setup relative positional embeddings."""
|
||||
if not self.use_rel_pos:
|
||||
self.rel_pos_h = None
|
||||
self.rel_pos_w = None
|
||||
return
|
||||
|
||||
assert input_size is not None
|
||||
assert self.cls_token is False, "not supported"
|
||||
# initialize relative positional embeddings
|
||||
self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, self.head_dim))
|
||||
self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, self.head_dim))
|
||||
|
||||
if not rel_pos_zero_init:
|
||||
nn.init.trunc_normal_(self.rel_pos_h, std=0.02)
|
||||
nn.init.trunc_normal_(self.rel_pos_w, std=0.02)
|
||||
|
||||
# Precompute the relative coords
|
||||
H, W = input_size
|
||||
q_coords = torch.arange(H)[:, None]
|
||||
k_coords = torch.arange(W)[None, :]
|
||||
relative_coords = (q_coords - k_coords) + (H - 1)
|
||||
self.relative_coords = relative_coords.long()
|
||||
|
||||
def _setup_rope_freqs(self, input_size: tuple[int, int] | None = None) -> None:
|
||||
"""Setup 2d-rope frequencies."""
|
||||
if not self.use_rope:
|
||||
self.freqs_cis = None
|
||||
return
|
||||
|
||||
assert input_size is not None
|
||||
# determine rope input size
|
||||
if self.rope_pt_size is None:
|
||||
self.rope_pt_size = input_size
|
||||
|
||||
# initialize 2d rope freqs
|
||||
self.compute_cis = partial(
|
||||
compute_axial_cis,
|
||||
dim=self.head_dim,
|
||||
theta=self.rope_theta,
|
||||
)
|
||||
|
||||
# interpolate rope
|
||||
scale_pos = 1.0
|
||||
if self.rope_interp:
|
||||
scale_pos = self.rope_pt_size[0] / input_size[0]
|
||||
# get scaled freqs_cis
|
||||
freqs_cis = self.compute_cis(
|
||||
end_x=input_size[0],
|
||||
end_y=input_size[1],
|
||||
scale_pos=scale_pos,
|
||||
)
|
||||
if self.cls_token:
|
||||
t = torch.zeros(
|
||||
self.head_dim // 2,
|
||||
dtype=torch.float32,
|
||||
device=freqs_cis.device,
|
||||
)
|
||||
cls_freqs_cis = torch.polar(torch.ones_like(t), t)[None, :]
|
||||
freqs_cis = torch.cat([cls_freqs_cis, freqs_cis], dim=0)
|
||||
|
||||
self.freqs_cis = freqs_cis
|
||||
|
||||
def _apply_rope(self, q, k) -> tuple[Tensor, Tensor]:
|
||||
"""Apply 2d-rope to q and k."""
|
||||
if not self.use_rope:
|
||||
return q, k
|
||||
|
||||
assert self.freqs_cis is not None
|
||||
return apply_rotary_enc(q, k, freqs_cis=self.freqs_cis.to(q.device))
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Forward pass of attention block."""
|
||||
s = 1 if self.cls_token else 0 # used to exclude cls_token
|
||||
if x.ndim == 4:
|
||||
B, H, W, _ = x.shape
|
||||
assert s == 0 # no cls_token
|
||||
L = H * W
|
||||
ndim = 4
|
||||
else:
|
||||
assert x.ndim == 3
|
||||
B, L, _ = x.shape
|
||||
ndim = 3
|
||||
H = W = math.sqrt(L - s)
|
||||
|
||||
# qkv with shape (3, B, nHead, L, C)
|
||||
qkv = self.qkv(x).reshape(B, L, 3, self.num_heads, -1)
|
||||
# q, k, v with shape (B, nHead, L, C)
|
||||
q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0)
|
||||
|
||||
# handle rope and rel pos embeddings
|
||||
q, k = self._apply_rope(q, k)
|
||||
if self.use_rel_pos:
|
||||
q, k = concat_rel_pos(
|
||||
q.flatten(0, 1),
|
||||
k.flatten(0, 1),
|
||||
(H, W),
|
||||
x.shape[1:3],
|
||||
self.rel_pos_h,
|
||||
self.rel_pos_w,
|
||||
rescale=True,
|
||||
relative_coords=self.relative_coords,
|
||||
)
|
||||
|
||||
# sdpa expects [B, nheads, H*W, C] so we transpose back
|
||||
q = q.reshape(B, self.num_heads, H * W, -1)
|
||||
k = k.reshape(B, self.num_heads, H * W, -1)
|
||||
|
||||
x = F.scaled_dot_product_attention(q, k, v)
|
||||
|
||||
if ndim == 4:
|
||||
x = x.view(B, self.num_heads, H, W, -1).permute(0, 2, 3, 1, 4).reshape(B, H, W, -1)
|
||||
else:
|
||||
x = x.view(B, self.num_heads, L, -1).permute(0, 2, 1, 3).reshape(B, L, -1)
|
||||
|
||||
x = self.proj(x)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Block(nn.Module):
|
||||
"""Transformer blocks with support of window attention."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
qkv_bias: bool = True,
|
||||
drop_path: float = 0.0,
|
||||
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
|
||||
act_layer: Callable[..., nn.Module] = nn.GELU,
|
||||
use_rel_pos: bool = False,
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 0,
|
||||
input_size: tuple[int, int] | None = None,
|
||||
use_rope: bool = False,
|
||||
rope_pt_size: tuple[int, int] | None = None,
|
||||
rope_interp: bool = False,
|
||||
cls_token: bool = False,
|
||||
dropout: float = 0.0,
|
||||
init_values: float | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
drop_path (float): Stochastic depth rate.
|
||||
norm_layer (Callable): Normalization layer constructor.
|
||||
act_layer (Callable): Activation layer constructor.
|
||||
use_rel_pos (bool): If True, add relative positional embeddings to the attention map.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks. If it equals 0, then not use window attention.
|
||||
input_size (tuple[int, int] | None): Input resolution for calculating the relative positional parameter
|
||||
size.
|
||||
use_rope (bool): Whether to use rope 2d (independent of use_rel_pos, as it can be used together).
|
||||
rope_pt_size (tuple[int, int] | None): Size of rope in previous stage of training, needed for interpolation
|
||||
or tiling.
|
||||
rope_interp (bool): Whether to interpolate (or extrapolate) rope to match target input size, expected to
|
||||
specify source size as rope_pt_size.
|
||||
cls_token (bool): Whether a cls_token is present.
|
||||
dropout (float): Dropout rate.
|
||||
init_values (float | None): Layer scale init, None for no layer scale.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
check_requirements("timm")
|
||||
from timm.layers import DropPath, Mlp
|
||||
|
||||
self.norm1 = norm_layer(dim)
|
||||
self.attn = Attention(
|
||||
dim,
|
||||
num_heads=num_heads,
|
||||
qkv_bias=qkv_bias,
|
||||
use_rel_pos=use_rel_pos,
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
input_size=input_size if window_size == 0 else (window_size, window_size),
|
||||
use_rope=use_rope,
|
||||
rope_pt_size=rope_pt_size,
|
||||
rope_interp=rope_interp,
|
||||
cls_token=cls_token,
|
||||
)
|
||||
self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
||||
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
||||
|
||||
self.norm2 = norm_layer(dim)
|
||||
self.mlp = Mlp(
|
||||
in_features=dim,
|
||||
hidden_features=int(dim * mlp_ratio),
|
||||
act_layer=act_layer,
|
||||
drop=(dropout, 0.0),
|
||||
)
|
||||
self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.window_size = window_size
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
"""Forward pass of the transformer block."""
|
||||
shortcut = x
|
||||
x = self.norm1(x)
|
||||
# Window partition
|
||||
if self.window_size > 0:
|
||||
H, W = x.shape[1], x.shape[2]
|
||||
x, pad_hw = window_partition(x, self.window_size)
|
||||
|
||||
x = self.ls1(self.attn(x))
|
||||
# Reverse window partition
|
||||
if self.window_size > 0:
|
||||
x = window_unpartition(x, self.window_size, pad_hw, (H, W))
|
||||
|
||||
x = shortcut + self.dropout(self.drop_path(x))
|
||||
x = x + self.dropout(self.drop_path(self.ls2(self.mlp(self.norm2(x)))))
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class ViT(nn.Module):
|
||||
"""This module implements Vision Transformer (ViT) backbone in :paper:`vitdet`. "Exploring Plain Vision Transformer
|
||||
Backbones for Object Detection", https://arxiv.org/abs/2203.16527.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
img_size: int = 1024,
|
||||
patch_size: int = 16,
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
depth: int = 12,
|
||||
num_heads: int = 12,
|
||||
mlp_ratio: float = 4.0,
|
||||
qkv_bias: bool = True,
|
||||
drop_path_rate: float = 0.0,
|
||||
norm_layer: Callable[..., nn.Module] | str = "LayerNorm",
|
||||
act_layer: Callable[..., nn.Module] = nn.GELU,
|
||||
use_abs_pos: bool = True,
|
||||
tile_abs_pos: bool = True,
|
||||
rel_pos_blocks: tuple[int, ...] | bool = (2, 5, 8, 11),
|
||||
rel_pos_zero_init: bool = True,
|
||||
window_size: int = 14,
|
||||
global_att_blocks: tuple[int, ...] = (2, 5, 8, 11),
|
||||
use_rope: bool = False,
|
||||
rope_pt_size: int | None = None,
|
||||
use_interp_rope: bool = False,
|
||||
pretrain_img_size: int = 224,
|
||||
pretrain_use_cls_token: bool = True,
|
||||
retain_cls_token: bool = True,
|
||||
dropout: float = 0.0,
|
||||
return_interm_layers: bool = False,
|
||||
init_values: float | None = None, # for layerscale
|
||||
ln_pre: bool = False,
|
||||
ln_post: bool = False,
|
||||
bias_patch_embed: bool = True,
|
||||
compile_mode: str | None = None,
|
||||
use_act_checkpoint: bool = True,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
img_size (int): Input image size. Only relevant for rel pos or rope.
|
||||
patch_size (int): Patch size.
|
||||
in_chans (int): Number of input image channels.
|
||||
embed_dim (int): Patch embedding dimension.
|
||||
depth (int): Depth of ViT.
|
||||
num_heads (int): Number of attention heads in each ViT block.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
||||
qkv_bias (bool): If True, add a learnable bias to query, key, value.
|
||||
drop_path_rate (float): Stochastic depth rate.
|
||||
norm_layer (Callable or str): Normalization layer constructor or name.
|
||||
act_layer (Callable): Activation layer constructor.
|
||||
use_abs_pos (bool): If True, use absolute positional embeddings.
|
||||
tile_abs_pos (bool): If True, tile absolute positional embeddings instead of interpolation.
|
||||
rel_pos_blocks (tuple[int, ...] | bool): Blocks which have rel pos embeddings.
|
||||
rel_pos_zero_init (bool): If True, zero initialize relative positional parameters.
|
||||
window_size (int): Window size for window attention blocks.
|
||||
global_att_blocks (tuple[int, ...]): Indexes for blocks using global attention (other blocks use window
|
||||
attention).
|
||||
use_rope (bool): Whether to use rope 2d (independent of rel_pos_blocks, as it can be used together).
|
||||
rope_pt_size (int | None): Size of rope in previous stage of training, needed for interpolation or tiling.
|
||||
use_interp_rope (bool): Whether to interpolate (or extrapolate) rope to match target input size, expected to
|
||||
specify source size as rope_pt_size.
|
||||
pretrain_img_size (int): Input image size for pretraining models.
|
||||
pretrain_use_cls_token (bool): If True, pretraining models use class token.
|
||||
retain_cls_token (bool): Whether cls_token should be retained.
|
||||
dropout (float): Dropout rate. Applied in residual blocks of attn, mlp and inside the mlp.
|
||||
return_interm_layers (bool): Whether to return intermediate layers (all global attention blocks).
|
||||
init_values (float | None): Layer scale init, None for no layer scale.
|
||||
ln_pre (bool): If True, apply layer norm before transformer blocks.
|
||||
ln_post (bool): If True, apply layer norm after transformer blocks.
|
||||
bias_patch_embed (bool): If True, use bias in conv for patch embed.
|
||||
compile_mode (str | None): Mode to compile the forward, or None to disable.
|
||||
use_act_checkpoint (bool): If True, use activation checkpointing.
|
||||
"""
|
||||
super().__init__()
|
||||
self.pretrain_use_cls_token = pretrain_use_cls_token
|
||||
|
||||
window_block_indexes = [i for i in range(depth) if i not in global_att_blocks]
|
||||
self.full_attn_ids = list(global_att_blocks)
|
||||
self.rel_pos_blocks = [False] * depth
|
||||
if isinstance(rel_pos_blocks, bool) and rel_pos_blocks:
|
||||
self.rel_pos_blocks = [True] * depth
|
||||
else:
|
||||
for i in rel_pos_blocks:
|
||||
self.rel_pos_blocks[i] = True
|
||||
|
||||
self.retain_cls_token = retain_cls_token
|
||||
if self.retain_cls_token:
|
||||
assert pretrain_use_cls_token
|
||||
assert len(window_block_indexes) == 0, "windowing not supported with cls token"
|
||||
|
||||
assert sum(self.rel_pos_blocks) == 0, "rel pos not supported with cls token"
|
||||
|
||||
scale = embed_dim**-0.5
|
||||
self.class_embedding = nn.Parameter(scale * torch.randn(1, 1, embed_dim))
|
||||
|
||||
if isinstance(norm_layer, str):
|
||||
norm_layer = partial(getattr(nn, norm_layer), eps=1e-5)
|
||||
|
||||
self.patch_embed = PatchEmbed(
|
||||
kernel_size=(patch_size, patch_size),
|
||||
stride=(patch_size, patch_size),
|
||||
in_chans=in_chans,
|
||||
embed_dim=embed_dim,
|
||||
bias=bias_patch_embed,
|
||||
)
|
||||
|
||||
# Handle absolute positional embedding
|
||||
self.tile_abs_pos = tile_abs_pos
|
||||
self.use_abs_pos = use_abs_pos
|
||||
if self.tile_abs_pos:
|
||||
assert self.use_abs_pos
|
||||
|
||||
if self.use_abs_pos:
|
||||
# Initialize absolute positional embedding with pretrain image size.
|
||||
num_patches = (pretrain_img_size // patch_size) * (pretrain_img_size // patch_size)
|
||||
num_positions = (num_patches + 1) if pretrain_use_cls_token else num_patches
|
||||
self.pos_embed = nn.Parameter(torch.zeros(1, num_positions, embed_dim))
|
||||
else:
|
||||
self.pos_embed = None
|
||||
|
||||
# stochastic depth decay rule
|
||||
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
|
||||
|
||||
self.patch_size = patch_size
|
||||
self.window_size = window_size
|
||||
self.blocks = nn.ModuleList()
|
||||
cur_stage = 1
|
||||
for i in range(depth):
|
||||
block = Block(
|
||||
dim=embed_dim,
|
||||
num_heads=num_heads,
|
||||
mlp_ratio=mlp_ratio,
|
||||
qkv_bias=qkv_bias,
|
||||
drop_path=dpr[i],
|
||||
norm_layer=norm_layer,
|
||||
act_layer=act_layer,
|
||||
use_rel_pos=self.rel_pos_blocks[i],
|
||||
rel_pos_zero_init=rel_pos_zero_init,
|
||||
window_size=window_size if i in window_block_indexes else 0,
|
||||
input_size=(img_size // patch_size, img_size // patch_size),
|
||||
use_rope=use_rope,
|
||||
rope_pt_size=((window_size, window_size) if rope_pt_size is None else (rope_pt_size, rope_pt_size)),
|
||||
rope_interp=use_interp_rope,
|
||||
cls_token=self.retain_cls_token,
|
||||
dropout=dropout,
|
||||
init_values=init_values,
|
||||
)
|
||||
|
||||
if i not in window_block_indexes:
|
||||
cur_stage += 1
|
||||
|
||||
self.use_act_checkpoint = use_act_checkpoint
|
||||
|
||||
self.blocks.append(block)
|
||||
|
||||
self.return_interm_layers = return_interm_layers
|
||||
self.channel_list = [embed_dim] * len(self.full_attn_ids) if return_interm_layers else [embed_dim]
|
||||
|
||||
if self.pos_embed is not None:
|
||||
nn.init.trunc_normal_(self.pos_embed, std=0.02)
|
||||
|
||||
self.ln_pre = norm_layer(embed_dim) if ln_pre else nn.Identity()
|
||||
self.ln_post = norm_layer(embed_dim) if ln_post else nn.Identity()
|
||||
|
||||
self.apply(self._init_weights)
|
||||
|
||||
if compile_mode is not None:
|
||||
self.forward = torch.compile(self.forward, mode=compile_mode, fullgraph=True)
|
||||
if self.use_act_checkpoint and self.training:
|
||||
torch._dynamo.config.optimize_ddp = False
|
||||
|
||||
@staticmethod
|
||||
def _init_weights(m: nn.Module) -> None:
|
||||
"""Initialize the weights."""
|
||||
if isinstance(m, nn.Linear):
|
||||
nn.init.trunc_normal_(m.weight, std=0.02)
|
||||
if isinstance(m, nn.Linear) and m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
elif isinstance(m, nn.LayerNorm):
|
||||
nn.init.constant_(m.bias, 0)
|
||||
nn.init.constant_(m.weight, 1.0)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> list[torch.Tensor]:
|
||||
"""Vit forward path and get feature maps."""
|
||||
x = self.patch_embed(x)
|
||||
h, w = x.shape[1], x.shape[2]
|
||||
|
||||
s = 0
|
||||
if self.retain_cls_token:
|
||||
# If cls_token is retained, we don't
|
||||
# maintain spatial shape
|
||||
x = torch.cat([self.class_embedding, x.flatten(1, 2)], dim=1)
|
||||
s = 1
|
||||
|
||||
if self.pos_embed is not None:
|
||||
x = x + get_abs_pos(
|
||||
self.pos_embed,
|
||||
self.pretrain_use_cls_token,
|
||||
(h, w),
|
||||
self.retain_cls_token,
|
||||
tiling=self.tile_abs_pos,
|
||||
)
|
||||
|
||||
x = self.ln_pre(x)
|
||||
|
||||
outputs = []
|
||||
for i, blk in enumerate(self.blocks):
|
||||
if self.use_act_checkpoint and self.training:
|
||||
x = checkpoint.checkpoint(blk, x, use_reentrant=False)
|
||||
else:
|
||||
x = blk(x)
|
||||
if (i == self.full_attn_ids[-1]) or (self.return_interm_layers and i in self.full_attn_ids):
|
||||
if i == self.full_attn_ids[-1]:
|
||||
x = self.ln_post(x)
|
||||
|
||||
feats = x[:, s:]
|
||||
if feats.ndim == 4:
|
||||
feats = feats.permute(0, 3, 1, 2)
|
||||
else:
|
||||
assert feats.ndim == 3
|
||||
h = w = math.sqrt(feats.shape[1])
|
||||
feats = feats.reshape(feats.shape[0], h, w, feats.shape[-1]).permute(0, 3, 1, 2)
|
||||
|
||||
outputs.append(feats)
|
||||
|
||||
return outputs
|
||||
|
||||
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
|
||||
"""Setup rel pos embeddings and rope freqs for a new input image size."""
|
||||
for block in self.blocks:
|
||||
if block.window_size != 0:
|
||||
continue
|
||||
block.attn._setup_rel_pos(input_size=(imgsz[0] // self.patch_size, imgsz[1] // self.patch_size))
|
||||
block.attn._setup_rope_freqs(input_size=(imgsz[0] // self.patch_size, imgsz[1] // self.patch_size))
|
||||
160
ultralytics/models/sam/sam3/vl_combiner.py
Executable file
160
ultralytics/models/sam/sam3/vl_combiner.py
Executable file
@@ -0,0 +1,160 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
|
||||
|
||||
"""Provides utility to combine a vision backbone with a language backbone."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn.attention import SDPBackend, sdpa_kernel
|
||||
|
||||
from .necks import Sam3DualViTDetNeck
|
||||
|
||||
|
||||
class SAM3VLBackbone(nn.Module):
|
||||
"""This backbone combines a vision backbone and a language backbone without fusion. As such it is more of a
|
||||
convenience wrapper to handle the two backbones together.
|
||||
|
||||
It adds support for activation checkpointing and compilation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
visual: Sam3DualViTDetNeck,
|
||||
text,
|
||||
compile_visual: bool = False,
|
||||
act_ckpt_whole_vision_backbone: bool = False,
|
||||
act_ckpt_whole_language_backbone: bool = False,
|
||||
scalp=0,
|
||||
):
|
||||
"""Initialize the backbone combiner.
|
||||
|
||||
:param visual: The vision backbone to use
|
||||
:param text: The text encoder to use
|
||||
"""
|
||||
super().__init__()
|
||||
self.vision_backbone: Sam3DualViTDetNeck = torch.compile(visual) if compile_visual else visual
|
||||
self.language_backbone = text
|
||||
self.scalp = scalp
|
||||
# allow running activation checkpointing on the entire vision and language backbones
|
||||
self.act_ckpt_whole_vision_backbone = act_ckpt_whole_vision_backbone
|
||||
self.act_ckpt_whole_language_backbone = act_ckpt_whole_language_backbone
|
||||
|
||||
def forward(
|
||||
self,
|
||||
samples: torch.Tensor,
|
||||
captions: list[str],
|
||||
input_boxes: torch.Tensor = None,
|
||||
additional_text: list[str] | None = None,
|
||||
):
|
||||
"""Forward pass of the backbone combiner.
|
||||
|
||||
:param samples: The input images
|
||||
:param captions: The input captions
|
||||
:param input_boxes: If the text contains place-holders for boxes, this
|
||||
parameter contains the tensor containing their spatial features
|
||||
:param additional_text: This can be used to encode some additional text
|
||||
(different from the captions) in the same forward of the backbone
|
||||
:return: Output dictionary with the following keys:
|
||||
- vision_features: The output of the vision backbone
|
||||
- language_features: The output of the language backbone
|
||||
- language_mask: The attention mask of the language backbone
|
||||
- vision_pos_enc: The positional encoding of the vision backbone
|
||||
- (optional) additional_text_features: The output of the language
|
||||
backbone for the additional text
|
||||
- (optional) additional_text_mask: The attention mask of the
|
||||
language backbone for the additional text
|
||||
"""
|
||||
output = self.forward_image(samples)
|
||||
output.update(self.forward_text(captions, input_boxes, additional_text))
|
||||
return output
|
||||
|
||||
def forward_image(self, samples: torch.Tensor):
|
||||
"""Forward pass of the vision backbone and get both SAM3 and SAM2 features."""
|
||||
# Forward through backbone
|
||||
sam3_features, sam3_pos, sam2_features, sam2_pos = self.vision_backbone.forward(samples)
|
||||
if self.scalp > 0:
|
||||
# Discard the lowest resolution features
|
||||
sam3_features, sam3_pos = (
|
||||
sam3_features[: -self.scalp],
|
||||
sam3_pos[: -self.scalp],
|
||||
)
|
||||
if sam2_features is not None and sam2_pos is not None:
|
||||
sam2_features, sam2_pos = (
|
||||
sam2_features[: -self.scalp],
|
||||
sam2_pos[: -self.scalp],
|
||||
)
|
||||
|
||||
sam2_output = None
|
||||
|
||||
if sam2_features is not None and sam2_pos is not None:
|
||||
sam2_src = sam2_features[-1]
|
||||
sam2_output = {
|
||||
"vision_features": sam2_src,
|
||||
"vision_pos_enc": sam2_pos,
|
||||
"backbone_fpn": sam2_features,
|
||||
}
|
||||
|
||||
sam3_src = sam3_features[-1]
|
||||
return {
|
||||
"vision_features": sam3_src,
|
||||
"vision_pos_enc": sam3_pos,
|
||||
"backbone_fpn": sam3_features,
|
||||
"sam2_backbone_out": sam2_output,
|
||||
}
|
||||
|
||||
def forward_image_sam2(self, samples: torch.Tensor):
|
||||
"""Forward pass of the vision backbone to get SAM2 features only."""
|
||||
xs = self.vision_backbone.trunk(samples)
|
||||
x = xs[-1] # simpleFPN
|
||||
|
||||
assert self.vision_backbone.sam2_convs is not None, "SAM2 neck is not available."
|
||||
sam2_features, sam2_pos = self.vision_backbone.sam_forward_feature_levels(x, self.vision_backbone.sam2_convs)
|
||||
|
||||
if self.scalp > 0:
|
||||
# Discard the lowest resolution features
|
||||
sam2_features, sam2_pos = (
|
||||
sam2_features[: -self.scalp],
|
||||
sam2_pos[: -self.scalp],
|
||||
)
|
||||
|
||||
return {
|
||||
"vision_features": sam2_features[-1],
|
||||
"vision_pos_enc": sam2_pos,
|
||||
"backbone_fpn": sam2_features,
|
||||
}
|
||||
|
||||
def forward_text(self, captions, input_boxes=None, additional_text=None):
|
||||
"""Forward pass of the text encoder."""
|
||||
output = {}
|
||||
|
||||
# Forward through text_encoder
|
||||
text_to_encode = copy(captions)
|
||||
if additional_text is not None:
|
||||
# if there are additional_text, we piggy-back them into this forward.
|
||||
# They'll be used later for output alignment
|
||||
text_to_encode += additional_text
|
||||
|
||||
with sdpa_kernel([SDPBackend.MATH, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.FLASH_ATTENTION]):
|
||||
text_attention_mask, text_memory, text_embeds = self.language_backbone(text_to_encode, input_boxes)
|
||||
|
||||
if additional_text is not None:
|
||||
output["additional_text_features"] = text_memory[:, -len(additional_text) :]
|
||||
output["additional_text_mask"] = text_attention_mask[-len(additional_text) :]
|
||||
|
||||
text_memory = text_memory[:, : len(captions)]
|
||||
text_attention_mask = text_attention_mask[: len(captions)]
|
||||
text_embeds = text_embeds[:, : len(captions)]
|
||||
output["language_features"] = text_memory
|
||||
output["language_mask"] = text_attention_mask
|
||||
output["language_embeds"] = text_embeds # Text embeddings before forward to the encoder
|
||||
|
||||
return output
|
||||
|
||||
def set_imgsz(self, imgsz: list[int] = [1008, 1008]):
|
||||
"""Set the image size for the vision backbone."""
|
||||
self.vision_backbone.set_imgsz(imgsz)
|
||||
1
ultralytics/models/utils/__init__.py
Executable file
1
ultralytics/models/utils/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
466
ultralytics/models/utils/loss.py
Executable file
466
ultralytics/models/utils/loss.py
Executable file
@@ -0,0 +1,466 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ultralytics.utils.loss import FocalLoss, VarifocalLoss
|
||||
from ultralytics.utils.metrics import bbox_iou
|
||||
|
||||
from .ops import HungarianMatcher
|
||||
|
||||
|
||||
class DETRLoss(nn.Module):
|
||||
"""DETR (DEtection TRansformer) Loss class for calculating various loss components.
|
||||
|
||||
This class computes classification loss, bounding box loss, GIoU loss, and optionally auxiliary losses for the DETR
|
||||
object detection model.
|
||||
|
||||
Attributes:
|
||||
nc (int): Number of classes.
|
||||
loss_gain (dict[str, float]): Coefficients for different loss components.
|
||||
aux_loss (bool): Whether to compute auxiliary losses.
|
||||
use_fl (bool): Whether to use FocalLoss.
|
||||
use_vfl (bool): Whether to use VarifocalLoss.
|
||||
use_uni_match (bool): Whether to use a fixed layer for auxiliary branch label assignment.
|
||||
uni_match_ind (int): Index of fixed layer to use if use_uni_match is True.
|
||||
matcher (HungarianMatcher): Object to compute matching cost and indices.
|
||||
fl (FocalLoss | None): Focal Loss object if use_fl is True, otherwise None.
|
||||
vfl (VarifocalLoss | None): Varifocal Loss object if use_vfl is True, otherwise None.
|
||||
device (torch.device): Device on which tensors are stored.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nc: int = 80,
|
||||
loss_gain: dict[str, float] | None = None,
|
||||
aux_loss: bool = True,
|
||||
use_fl: bool = True,
|
||||
use_vfl: bool = False,
|
||||
use_uni_match: bool = False,
|
||||
uni_match_ind: int = 0,
|
||||
gamma: float = 1.5,
|
||||
alpha: float = 0.25,
|
||||
):
|
||||
"""Initialize DETR loss function with customizable components and gains.
|
||||
|
||||
Uses default loss_gain if not provided. Initializes HungarianMatcher with preset cost gains. Supports auxiliary
|
||||
losses and various loss types.
|
||||
|
||||
Args:
|
||||
nc (int): Number of classes.
|
||||
loss_gain (dict[str, float], optional): Coefficients for different loss components.
|
||||
aux_loss (bool): Whether to use auxiliary losses from each decoder layer.
|
||||
use_fl (bool): Whether to use FocalLoss.
|
||||
use_vfl (bool): Whether to use VarifocalLoss.
|
||||
use_uni_match (bool): Whether to use fixed layer for auxiliary branch label assignment.
|
||||
uni_match_ind (int): Index of fixed layer for uni_match.
|
||||
gamma (float): The focusing parameter that controls how much the loss focuses on hard-to-classify examples.
|
||||
alpha (float): The balancing factor used to address class imbalance.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
if loss_gain is None:
|
||||
loss_gain = {"class": 1, "bbox": 5, "giou": 2, "no_object": 0.1, "mask": 1, "dice": 1}
|
||||
self.nc = nc
|
||||
self.matcher = HungarianMatcher(cost_gain={"class": 2, "bbox": 5, "giou": 2})
|
||||
self.loss_gain = loss_gain
|
||||
self.aux_loss = aux_loss
|
||||
self.fl = FocalLoss(gamma, alpha) if use_fl else None
|
||||
self.vfl = VarifocalLoss(gamma, alpha) if use_vfl else None
|
||||
|
||||
self.use_uni_match = use_uni_match
|
||||
self.uni_match_ind = uni_match_ind
|
||||
self.device = None
|
||||
|
||||
def _get_loss_class(
|
||||
self, pred_scores: torch.Tensor, targets: torch.Tensor, gt_scores: torch.Tensor, num_gts: int, postfix: str = ""
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Compute classification loss based on predictions, target values, and ground truth scores.
|
||||
|
||||
Args:
|
||||
pred_scores (torch.Tensor): Predicted class scores with shape (B, N, C).
|
||||
targets (torch.Tensor): Target class indices with shape (B, N).
|
||||
gt_scores (torch.Tensor): Ground truth confidence scores with shape (B, N).
|
||||
num_gts (int): Number of ground truth objects.
|
||||
postfix (str, optional): String to append to the loss name for identification in multi-loss scenarios.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary containing classification loss value.
|
||||
|
||||
Notes:
|
||||
The function supports different classification loss types:
|
||||
- Varifocal Loss (if self.vfl is not None and num_gts > 0)
|
||||
- Focal Loss (if self.fl is not None)
|
||||
- BCE Loss (default fallback)
|
||||
"""
|
||||
# Logits: [b, query, num_classes], gt_class: list[[n, 1]]
|
||||
name_class = f"loss_class{postfix}"
|
||||
bs, nq = pred_scores.shape[:2]
|
||||
# one_hot = F.one_hot(targets, self.nc + 1)[..., :-1] # (bs, num_queries, num_classes)
|
||||
one_hot = torch.zeros((bs, nq, self.nc + 1), dtype=torch.int64, device=targets.device)
|
||||
one_hot.scatter_(2, targets.unsqueeze(-1), 1)
|
||||
one_hot = one_hot[..., :-1]
|
||||
gt_scores = gt_scores.view(bs, nq, 1) * one_hot
|
||||
|
||||
if self.fl:
|
||||
if num_gts and self.vfl:
|
||||
loss_cls = self.vfl(pred_scores, gt_scores, one_hot)
|
||||
else:
|
||||
loss_cls = self.fl(pred_scores, one_hot.float())
|
||||
loss_cls /= max(num_gts, 1) / nq
|
||||
else:
|
||||
loss_cls = nn.BCEWithLogitsLoss(reduction="none")(pred_scores, gt_scores).mean(1).sum() # YOLO CLS loss
|
||||
|
||||
return {name_class: loss_cls.squeeze() * self.loss_gain["class"]}
|
||||
|
||||
def _get_loss_bbox(
|
||||
self, pred_bboxes: torch.Tensor, gt_bboxes: torch.Tensor, postfix: str = ""
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Compute bounding box and GIoU losses for predicted and ground truth bounding boxes.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes with shape (N, 4).
|
||||
gt_bboxes (torch.Tensor): Ground truth bounding boxes with shape (N, 4).
|
||||
postfix (str, optional): String to append to the loss names for identification in multi-loss scenarios.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary containing:
|
||||
- loss_bbox{postfix}: L1 loss between predicted and ground truth boxes, scaled by the bbox loss gain.
|
||||
- loss_giou{postfix}: GIoU loss between predicted and ground truth boxes, scaled by the giou loss gain.
|
||||
|
||||
Notes:
|
||||
If no ground truth boxes are provided (empty list), zero-valued tensors are returned for both losses.
|
||||
"""
|
||||
# Boxes: [b, query, 4], gt_bbox: list[[n, 4]]
|
||||
name_bbox = f"loss_bbox{postfix}"
|
||||
name_giou = f"loss_giou{postfix}"
|
||||
|
||||
loss = {}
|
||||
if len(gt_bboxes) == 0:
|
||||
loss[name_bbox] = torch.tensor(0.0, device=self.device)
|
||||
loss[name_giou] = torch.tensor(0.0, device=self.device)
|
||||
return loss
|
||||
|
||||
loss[name_bbox] = self.loss_gain["bbox"] * F.l1_loss(pred_bboxes, gt_bboxes, reduction="sum") / len(gt_bboxes)
|
||||
loss[name_giou] = 1.0 - bbox_iou(pred_bboxes, gt_bboxes, xywh=True, GIoU=True)
|
||||
loss[name_giou] = loss[name_giou].sum() / len(gt_bboxes)
|
||||
loss[name_giou] = self.loss_gain["giou"] * loss[name_giou]
|
||||
return {k: v.squeeze() for k, v in loss.items()}
|
||||
|
||||
# This function is for future RT-DETR Segment models
|
||||
# def _get_loss_mask(self, masks, gt_mask, match_indices, postfix=''):
|
||||
# # masks: [b, query, h, w], gt_mask: list[[n, H, W]]
|
||||
# name_mask = f'loss_mask{postfix}'
|
||||
# name_dice = f'loss_dice{postfix}'
|
||||
#
|
||||
# loss = {}
|
||||
# if sum(len(a) for a in gt_mask) == 0:
|
||||
# loss[name_mask] = torch.tensor(0., device=self.device)
|
||||
# loss[name_dice] = torch.tensor(0., device=self.device)
|
||||
# return loss
|
||||
#
|
||||
# num_gts = len(gt_mask)
|
||||
# src_masks, target_masks = self._get_assigned_bboxes(masks, gt_mask, match_indices)
|
||||
# src_masks = F.interpolate(src_masks.unsqueeze(0), size=target_masks.shape[-2:], mode='bilinear')[0]
|
||||
# # TODO: torch does not have `sigmoid_focal_loss`, but it's not urgent since we don't use mask branch for now.
|
||||
# loss[name_mask] = self.loss_gain['mask'] * F.sigmoid_focal_loss(src_masks, target_masks,
|
||||
# torch.tensor([num_gts], dtype=torch.float32))
|
||||
# loss[name_dice] = self.loss_gain['dice'] * self._dice_loss(src_masks, target_masks, num_gts)
|
||||
# return loss
|
||||
|
||||
# This function is for future RT-DETR Segment models
|
||||
# @staticmethod
|
||||
# def _dice_loss(inputs, targets, num_gts):
|
||||
# inputs = F.sigmoid(inputs).flatten(1)
|
||||
# targets = targets.flatten(1)
|
||||
# numerator = 2 * (inputs * targets).sum(1)
|
||||
# denominator = inputs.sum(-1) + targets.sum(-1)
|
||||
# loss = 1 - (numerator + 1) / (denominator + 1)
|
||||
# return loss.sum() / num_gts
|
||||
|
||||
def _get_loss_aux(
|
||||
self,
|
||||
pred_bboxes: torch.Tensor,
|
||||
pred_scores: torch.Tensor,
|
||||
gt_bboxes: torch.Tensor,
|
||||
gt_cls: torch.Tensor,
|
||||
gt_groups: list[int],
|
||||
match_indices: list[tuple] | None = None,
|
||||
postfix: str = "",
|
||||
masks: torch.Tensor | None = None,
|
||||
gt_mask: torch.Tensor | None = None,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Get auxiliary losses for intermediate decoder layers.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes from auxiliary layers.
|
||||
pred_scores (torch.Tensor): Predicted scores from auxiliary layers.
|
||||
gt_bboxes (torch.Tensor): Ground truth bounding boxes.
|
||||
gt_cls (torch.Tensor): Ground truth classes.
|
||||
gt_groups (list[int]): Number of ground truths per image.
|
||||
match_indices (list[tuple], optional): Pre-computed matching indices.
|
||||
postfix (str, optional): String to append to loss names.
|
||||
masks (torch.Tensor, optional): Predicted masks if using segmentation.
|
||||
gt_mask (torch.Tensor, optional): Ground truth masks if using segmentation.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary of auxiliary losses.
|
||||
"""
|
||||
# NOTE: loss class, bbox, giou, mask, dice
|
||||
loss = torch.zeros(5 if masks is not None else 3, device=pred_bboxes.device)
|
||||
if match_indices is None and self.use_uni_match:
|
||||
match_indices = self.matcher(
|
||||
pred_bboxes[self.uni_match_ind],
|
||||
pred_scores[self.uni_match_ind],
|
||||
gt_bboxes,
|
||||
gt_cls,
|
||||
gt_groups,
|
||||
masks=masks[self.uni_match_ind] if masks is not None else None,
|
||||
gt_mask=gt_mask,
|
||||
)
|
||||
for i, (aux_bboxes, aux_scores) in enumerate(zip(pred_bboxes, pred_scores)):
|
||||
aux_masks = masks[i] if masks is not None else None
|
||||
loss_ = self._get_loss(
|
||||
aux_bboxes,
|
||||
aux_scores,
|
||||
gt_bboxes,
|
||||
gt_cls,
|
||||
gt_groups,
|
||||
masks=aux_masks,
|
||||
gt_mask=gt_mask,
|
||||
postfix=postfix,
|
||||
match_indices=match_indices,
|
||||
)
|
||||
loss[0] += loss_[f"loss_class{postfix}"]
|
||||
loss[1] += loss_[f"loss_bbox{postfix}"]
|
||||
loss[2] += loss_[f"loss_giou{postfix}"]
|
||||
# if masks is not None and gt_mask is not None:
|
||||
# loss_ = self._get_loss_mask(aux_masks, gt_mask, match_indices, postfix)
|
||||
# loss[3] += loss_[f'loss_mask{postfix}']
|
||||
# loss[4] += loss_[f'loss_dice{postfix}']
|
||||
|
||||
loss = {
|
||||
f"loss_class_aux{postfix}": loss[0],
|
||||
f"loss_bbox_aux{postfix}": loss[1],
|
||||
f"loss_giou_aux{postfix}": loss[2],
|
||||
}
|
||||
# if masks is not None and gt_mask is not None:
|
||||
# loss[f'loss_mask_aux{postfix}'] = loss[3]
|
||||
# loss[f'loss_dice_aux{postfix}'] = loss[4]
|
||||
return loss
|
||||
|
||||
@staticmethod
|
||||
def _get_index(match_indices: list[tuple]) -> tuple[tuple[torch.Tensor, torch.Tensor], torch.Tensor]:
|
||||
"""Extract batch indices, source indices, and destination indices from match indices.
|
||||
|
||||
Args:
|
||||
match_indices (list[tuple]): List of tuples containing matched indices.
|
||||
|
||||
Returns:
|
||||
batch_idx (tuple[torch.Tensor, torch.Tensor]): Tuple containing (batch_idx, src_idx).
|
||||
dst_idx (torch.Tensor): Destination indices.
|
||||
"""
|
||||
batch_idx = torch.cat([torch.full_like(src, i) for i, (src, _) in enumerate(match_indices)])
|
||||
src_idx = torch.cat([src for (src, _) in match_indices])
|
||||
dst_idx = torch.cat([dst for (_, dst) in match_indices])
|
||||
return (batch_idx, src_idx), dst_idx
|
||||
|
||||
def _get_assigned_bboxes(
|
||||
self, pred_bboxes: torch.Tensor, gt_bboxes: torch.Tensor, match_indices: list[tuple]
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Assign predicted bounding boxes to ground truth bounding boxes based on match indices.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes.
|
||||
gt_bboxes (torch.Tensor): Ground truth bounding boxes.
|
||||
match_indices (list[tuple]): List of tuples containing matched indices.
|
||||
|
||||
Returns:
|
||||
pred_assigned (torch.Tensor): Assigned predicted bounding boxes.
|
||||
gt_assigned (torch.Tensor): Assigned ground truth bounding boxes.
|
||||
"""
|
||||
pred_assigned = torch.cat(
|
||||
[
|
||||
t[i] if len(i) > 0 else torch.zeros(0, t.shape[-1], device=self.device)
|
||||
for t, (i, _) in zip(pred_bboxes, match_indices)
|
||||
]
|
||||
)
|
||||
gt_assigned = torch.cat(
|
||||
[
|
||||
t[j] if len(j) > 0 else torch.zeros(0, t.shape[-1], device=self.device)
|
||||
for t, (_, j) in zip(gt_bboxes, match_indices)
|
||||
]
|
||||
)
|
||||
return pred_assigned, gt_assigned
|
||||
|
||||
def _get_loss(
|
||||
self,
|
||||
pred_bboxes: torch.Tensor,
|
||||
pred_scores: torch.Tensor,
|
||||
gt_bboxes: torch.Tensor,
|
||||
gt_cls: torch.Tensor,
|
||||
gt_groups: list[int],
|
||||
masks: torch.Tensor | None = None,
|
||||
gt_mask: torch.Tensor | None = None,
|
||||
postfix: str = "",
|
||||
match_indices: list[tuple] | None = None,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Calculate losses for a single prediction layer.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes.
|
||||
pred_scores (torch.Tensor): Predicted class scores.
|
||||
gt_bboxes (torch.Tensor): Ground truth bounding boxes.
|
||||
gt_cls (torch.Tensor): Ground truth classes.
|
||||
gt_groups (list[int]): Number of ground truths per image.
|
||||
masks (torch.Tensor, optional): Predicted masks if using segmentation.
|
||||
gt_mask (torch.Tensor, optional): Ground truth masks if using segmentation.
|
||||
postfix (str, optional): String to append to loss names.
|
||||
match_indices (list[tuple], optional): Pre-computed matching indices.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary of losses.
|
||||
"""
|
||||
if match_indices is None:
|
||||
match_indices = self.matcher(
|
||||
pred_bboxes, pred_scores, gt_bboxes, gt_cls, gt_groups, masks=masks, gt_mask=gt_mask
|
||||
)
|
||||
|
||||
idx, gt_idx = self._get_index(match_indices)
|
||||
pred_bboxes, gt_bboxes = pred_bboxes[idx], gt_bboxes[gt_idx]
|
||||
|
||||
bs, nq = pred_scores.shape[:2]
|
||||
targets = torch.full((bs, nq), self.nc, device=pred_scores.device, dtype=gt_cls.dtype)
|
||||
targets[idx] = gt_cls[gt_idx]
|
||||
|
||||
gt_scores = torch.zeros([bs, nq], device=pred_scores.device)
|
||||
if len(gt_bboxes):
|
||||
gt_scores[idx] = bbox_iou(pred_bboxes.detach(), gt_bboxes, xywh=True).squeeze(-1)
|
||||
|
||||
return {
|
||||
**self._get_loss_class(pred_scores, targets, gt_scores, len(gt_bboxes), postfix),
|
||||
**self._get_loss_bbox(pred_bboxes, gt_bboxes, postfix),
|
||||
# **(self._get_loss_mask(masks, gt_mask, match_indices, postfix) if masks is not None and gt_mask is not None else {})
|
||||
}
|
||||
|
||||
def forward(
|
||||
self,
|
||||
pred_bboxes: torch.Tensor,
|
||||
pred_scores: torch.Tensor,
|
||||
batch: dict[str, Any],
|
||||
postfix: str = "",
|
||||
**kwargs: Any,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Calculate loss for predicted bounding boxes and scores.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes, shape (L, B, N, 4).
|
||||
pred_scores (torch.Tensor): Predicted class scores, shape (L, B, N, C).
|
||||
batch (dict[str, Any]): Batch information containing cls, bboxes, and gt_groups.
|
||||
postfix (str, optional): Postfix for loss names.
|
||||
**kwargs (Any): Additional arguments, may include 'match_indices'.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Computed losses, including main and auxiliary (if enabled).
|
||||
|
||||
Notes:
|
||||
Uses last elements of pred_bboxes and pred_scores for main loss, and the rest for auxiliary losses if
|
||||
self.aux_loss is True.
|
||||
"""
|
||||
self.device = pred_bboxes.device
|
||||
match_indices = kwargs.get("match_indices", None)
|
||||
gt_cls, gt_bboxes, gt_groups = batch["cls"], batch["bboxes"], batch["gt_groups"]
|
||||
|
||||
total_loss = self._get_loss(
|
||||
pred_bboxes[-1], pred_scores[-1], gt_bboxes, gt_cls, gt_groups, postfix=postfix, match_indices=match_indices
|
||||
)
|
||||
|
||||
if self.aux_loss:
|
||||
total_loss.update(
|
||||
self._get_loss_aux(
|
||||
pred_bboxes[:-1], pred_scores[:-1], gt_bboxes, gt_cls, gt_groups, match_indices, postfix
|
||||
)
|
||||
)
|
||||
|
||||
return total_loss
|
||||
|
||||
|
||||
class RTDETRDetectionLoss(DETRLoss):
|
||||
"""Real-Time DEtection TRansformer (RT-DETR) Detection Loss class that extends the DETRLoss.
|
||||
|
||||
This class computes the detection loss for the RT-DETR model, which includes the standard detection loss as well as
|
||||
an additional denoising training loss when provided with denoising metadata.
|
||||
"""
|
||||
|
||||
def forward(
|
||||
self,
|
||||
preds: tuple[torch.Tensor, torch.Tensor],
|
||||
batch: dict[str, Any],
|
||||
dn_bboxes: torch.Tensor | None = None,
|
||||
dn_scores: torch.Tensor | None = None,
|
||||
dn_meta: dict[str, Any] | None = None,
|
||||
) -> dict[str, torch.Tensor]:
|
||||
"""Forward pass to compute detection loss with optional denoising loss.
|
||||
|
||||
Args:
|
||||
preds (tuple[torch.Tensor, torch.Tensor]): Tuple containing predicted bounding boxes and scores.
|
||||
batch (dict[str, Any]): Batch data containing ground truth information.
|
||||
dn_bboxes (torch.Tensor, optional): Denoising bounding boxes.
|
||||
dn_scores (torch.Tensor, optional): Denoising scores.
|
||||
dn_meta (dict[str, Any], optional): Metadata for denoising.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary containing total loss and denoising loss if applicable.
|
||||
"""
|
||||
pred_bboxes, pred_scores = preds
|
||||
total_loss = super().forward(pred_bboxes, pred_scores, batch)
|
||||
|
||||
# Check for denoising metadata to compute denoising training loss
|
||||
if dn_meta is not None:
|
||||
dn_pos_idx, dn_num_group = dn_meta["dn_pos_idx"], dn_meta["dn_num_group"]
|
||||
assert len(batch["gt_groups"]) == len(dn_pos_idx)
|
||||
|
||||
# Get the match indices for denoising
|
||||
match_indices = self.get_dn_match_indices(dn_pos_idx, dn_num_group, batch["gt_groups"])
|
||||
|
||||
# Compute the denoising training loss
|
||||
dn_loss = super().forward(dn_bboxes, dn_scores, batch, postfix="_dn", match_indices=match_indices)
|
||||
total_loss.update(dn_loss)
|
||||
else:
|
||||
# If no denoising metadata is provided, set denoising loss to zero
|
||||
total_loss.update({f"{k}_dn": torch.tensor(0.0, device=self.device) for k in total_loss.keys()})
|
||||
|
||||
return total_loss
|
||||
|
||||
@staticmethod
|
||||
def get_dn_match_indices(
|
||||
dn_pos_idx: list[torch.Tensor], dn_num_group: int, gt_groups: list[int]
|
||||
) -> list[tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Get match indices for denoising.
|
||||
|
||||
Args:
|
||||
dn_pos_idx (list[torch.Tensor]): List of tensors containing positive indices for denoising.
|
||||
dn_num_group (int): Number of denoising groups.
|
||||
gt_groups (list[int]): List of integers representing number of ground truths per image.
|
||||
|
||||
Returns:
|
||||
(list[tuple[torch.Tensor, torch.Tensor]]): List of tuples containing matched indices for denoising.
|
||||
"""
|
||||
dn_match_indices = []
|
||||
idx_groups = torch.as_tensor([0, *gt_groups[:-1]]).cumsum_(0)
|
||||
for i, num_gt in enumerate(gt_groups):
|
||||
if num_gt > 0:
|
||||
gt_idx = torch.arange(end=num_gt, dtype=torch.long) + idx_groups[i]
|
||||
gt_idx = gt_idx.repeat(dn_num_group)
|
||||
assert len(dn_pos_idx[i]) == len(gt_idx), (
|
||||
f"Expected the same length, but got {len(dn_pos_idx[i])} and {len(gt_idx)} respectively."
|
||||
)
|
||||
dn_match_indices.append((dn_pos_idx[i], gt_idx))
|
||||
else:
|
||||
dn_match_indices.append((torch.zeros([0], dtype=torch.long), torch.zeros([0], dtype=torch.long)))
|
||||
return dn_match_indices
|
||||
315
ultralytics/models/utils/ops.py
Executable file
315
ultralytics/models/utils/ops.py
Executable file
@@ -0,0 +1,315 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
from ultralytics.utils.metrics import bbox_iou
|
||||
from ultralytics.utils.ops import xywh2xyxy, xyxy2xywh
|
||||
|
||||
|
||||
class HungarianMatcher(nn.Module):
|
||||
"""A module implementing the HungarianMatcher for optimal assignment between predictions and ground truth.
|
||||
|
||||
HungarianMatcher performs optimal bipartite assignment over predicted and ground truth bounding boxes using a cost
|
||||
function that considers classification scores, bounding box coordinates, and optionally mask predictions. This is
|
||||
used in end-to-end object detection models like DETR.
|
||||
|
||||
Attributes:
|
||||
cost_gain (dict[str, float]): Dictionary of cost coefficients for 'class', 'bbox', 'giou', 'mask', and 'dice'
|
||||
components.
|
||||
use_fl (bool): Whether to use Focal Loss for classification cost calculation.
|
||||
with_mask (bool): Whether the model makes mask predictions.
|
||||
num_sample_points (int): Number of sample points used in mask cost calculation.
|
||||
alpha (float): Alpha factor in Focal Loss calculation.
|
||||
gamma (float): Gamma factor in Focal Loss calculation.
|
||||
|
||||
Methods:
|
||||
forward: Compute optimal assignment between predictions and ground truths for a batch.
|
||||
_cost_mask: Compute mask cost and dice cost if masks are predicted.
|
||||
|
||||
Examples:
|
||||
Initialize a HungarianMatcher with custom cost gains
|
||||
>>> matcher = HungarianMatcher(cost_gain={"class": 2, "bbox": 5, "giou": 2})
|
||||
|
||||
Perform matching between predictions and ground truth
|
||||
>>> pred_boxes = torch.rand(2, 100, 4) # batch_size=2, num_queries=100
|
||||
>>> pred_scores = torch.rand(2, 100, 80) # 80 classes
|
||||
>>> gt_boxes = torch.rand(10, 4) # 10 ground truth boxes
|
||||
>>> gt_classes = torch.randint(0, 80, (10,))
|
||||
>>> gt_groups = [5, 5] # 5 GT boxes per image
|
||||
>>> indices = matcher(pred_boxes, pred_scores, gt_boxes, gt_classes, gt_groups)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cost_gain: dict[str, float] | None = None,
|
||||
use_fl: bool = True,
|
||||
with_mask: bool = False,
|
||||
num_sample_points: int = 12544,
|
||||
alpha: float = 0.25,
|
||||
gamma: float = 2.0,
|
||||
):
|
||||
"""Initialize HungarianMatcher for optimal assignment of predicted and ground truth bounding boxes.
|
||||
|
||||
Args:
|
||||
cost_gain (dict[str, float], optional): Dictionary of cost coefficients for different matching cost
|
||||
components. Should contain keys 'class', 'bbox', 'giou', 'mask', and 'dice'.
|
||||
use_fl (bool): Whether to use Focal Loss for classification cost calculation.
|
||||
with_mask (bool): Whether the model makes mask predictions.
|
||||
num_sample_points (int): Number of sample points used in mask cost calculation.
|
||||
alpha (float): Alpha factor in Focal Loss calculation.
|
||||
gamma (float): Gamma factor in Focal Loss calculation.
|
||||
"""
|
||||
super().__init__()
|
||||
if cost_gain is None:
|
||||
cost_gain = {"class": 1, "bbox": 5, "giou": 2, "mask": 1, "dice": 1}
|
||||
self.cost_gain = cost_gain
|
||||
self.use_fl = use_fl
|
||||
self.with_mask = with_mask
|
||||
self.num_sample_points = num_sample_points
|
||||
self.alpha = alpha
|
||||
self.gamma = gamma
|
||||
|
||||
def forward(
|
||||
self,
|
||||
pred_bboxes: torch.Tensor,
|
||||
pred_scores: torch.Tensor,
|
||||
gt_bboxes: torch.Tensor,
|
||||
gt_cls: torch.Tensor,
|
||||
gt_groups: list[int],
|
||||
masks: torch.Tensor | None = None,
|
||||
gt_mask: list[torch.Tensor] | None = None,
|
||||
) -> list[tuple[torch.Tensor, torch.Tensor]]:
|
||||
"""Compute optimal assignment between predictions and ground truth using Hungarian algorithm.
|
||||
|
||||
This method calculates matching costs based on classification scores, bounding box coordinates, and optionally
|
||||
mask predictions, then finds the optimal bipartite assignment between predictions and ground truth.
|
||||
|
||||
Args:
|
||||
pred_bboxes (torch.Tensor): Predicted bounding boxes with shape (batch_size, num_queries, 4).
|
||||
pred_scores (torch.Tensor): Predicted classification scores with shape (batch_size, num_queries,
|
||||
num_classes).
|
||||
gt_bboxes (torch.Tensor): Ground truth bounding boxes with shape (num_gts, 4).
|
||||
gt_cls (torch.Tensor): Ground truth class labels with shape (num_gts,).
|
||||
gt_groups (list[int]): Number of ground truth boxes for each image in the batch.
|
||||
masks (torch.Tensor, optional): Predicted masks with shape (batch_size, num_queries, height, width).
|
||||
gt_mask (list[torch.Tensor], optional): Ground truth masks, each with shape (num_masks, Height, Width).
|
||||
|
||||
Returns:
|
||||
(list[tuple[torch.Tensor, torch.Tensor]]): A list of size batch_size, each element is a tuple (index_i,
|
||||
index_j), where index_i is the tensor of indices of the selected predictions (in order) and index_j is
|
||||
the tensor of indices of the corresponding selected ground truth targets (in order).
|
||||
For each batch element, it holds: len(index_i) = len(index_j) = min(num_queries, num_target_boxes).
|
||||
"""
|
||||
bs, nq, nc = pred_scores.shape
|
||||
|
||||
if sum(gt_groups) == 0:
|
||||
return [(torch.tensor([], dtype=torch.long), torch.tensor([], dtype=torch.long)) for _ in range(bs)]
|
||||
|
||||
# Flatten to compute cost matrices in batch format
|
||||
pred_scores = pred_scores.detach().view(-1, nc)
|
||||
pred_scores = F.sigmoid(pred_scores) if self.use_fl else F.softmax(pred_scores, dim=-1)
|
||||
pred_bboxes = pred_bboxes.detach().view(-1, 4)
|
||||
|
||||
# Compute classification cost
|
||||
pred_scores = pred_scores[:, gt_cls]
|
||||
if self.use_fl:
|
||||
neg_cost_class = (1 - self.alpha) * (pred_scores**self.gamma) * (-(1 - pred_scores + 1e-8).log())
|
||||
pos_cost_class = self.alpha * ((1 - pred_scores) ** self.gamma) * (-(pred_scores + 1e-8).log())
|
||||
cost_class = pos_cost_class - neg_cost_class
|
||||
else:
|
||||
cost_class = -pred_scores
|
||||
|
||||
# Compute L1 cost between boxes
|
||||
cost_bbox = (pred_bboxes.unsqueeze(1) - gt_bboxes.unsqueeze(0)).abs().sum(-1) # (bs*num_queries, num_gt)
|
||||
|
||||
# Compute GIoU cost between boxes, (bs*num_queries, num_gt)
|
||||
cost_giou = 1.0 - bbox_iou(pred_bboxes.unsqueeze(1), gt_bboxes.unsqueeze(0), xywh=True, GIoU=True).squeeze(-1)
|
||||
|
||||
# Combine costs into final cost matrix
|
||||
C = (
|
||||
self.cost_gain["class"] * cost_class
|
||||
+ self.cost_gain["bbox"] * cost_bbox
|
||||
+ self.cost_gain["giou"] * cost_giou
|
||||
)
|
||||
|
||||
# Add mask costs if available
|
||||
if self.with_mask:
|
||||
C += self._cost_mask(bs, gt_groups, masks, gt_mask)
|
||||
|
||||
# Set invalid values (NaNs and infinities) to 0
|
||||
C[C.isnan() | C.isinf()] = 0.0
|
||||
|
||||
C = C.view(bs, nq, -1).cpu()
|
||||
indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(gt_groups, -1))]
|
||||
gt_groups = torch.as_tensor([0, *gt_groups[:-1]]).cumsum_(0) # (idx for queries, idx for gt)
|
||||
return [
|
||||
(torch.tensor(i, dtype=torch.long), torch.tensor(j, dtype=torch.long) + gt_groups[k])
|
||||
for k, (i, j) in enumerate(indices)
|
||||
]
|
||||
|
||||
# This function is for future RT-DETR Segment models
|
||||
# def _cost_mask(self, bs, num_gts, masks=None, gt_mask=None):
|
||||
# assert masks is not None and gt_mask is not None, 'Make sure the input has `mask` and `gt_mask`'
|
||||
# # all masks share the same set of points for efficient matching
|
||||
# sample_points = torch.rand([bs, 1, self.num_sample_points, 2])
|
||||
# sample_points = 2.0 * sample_points - 1.0
|
||||
#
|
||||
# out_mask = F.grid_sample(masks.detach(), sample_points, align_corners=False).squeeze(-2)
|
||||
# out_mask = out_mask.flatten(0, 1)
|
||||
#
|
||||
# tgt_mask = torch.cat(gt_mask).unsqueeze(1)
|
||||
# sample_points = torch.cat([a.repeat(b, 1, 1, 1) for a, b in zip(sample_points, num_gts) if b > 0])
|
||||
# tgt_mask = F.grid_sample(tgt_mask, sample_points, align_corners=False).squeeze([1, 2])
|
||||
#
|
||||
# with torch.amp.autocast("cuda", enabled=False):
|
||||
# # binary cross entropy cost
|
||||
# pos_cost_mask = F.binary_cross_entropy_with_logits(out_mask, torch.ones_like(out_mask), reduction='none')
|
||||
# neg_cost_mask = F.binary_cross_entropy_with_logits(out_mask, torch.zeros_like(out_mask), reduction='none')
|
||||
# cost_mask = torch.matmul(pos_cost_mask, tgt_mask.T) + torch.matmul(neg_cost_mask, 1 - tgt_mask.T)
|
||||
# cost_mask /= self.num_sample_points
|
||||
#
|
||||
# # dice cost
|
||||
# out_mask = F.sigmoid(out_mask)
|
||||
# numerator = 2 * torch.matmul(out_mask, tgt_mask.T)
|
||||
# denominator = out_mask.sum(-1, keepdim=True) + tgt_mask.sum(-1).unsqueeze(0)
|
||||
# cost_dice = 1 - (numerator + 1) / (denominator + 1)
|
||||
#
|
||||
# C = self.cost_gain['mask'] * cost_mask + self.cost_gain['dice'] * cost_dice
|
||||
# return C
|
||||
|
||||
|
||||
def get_cdn_group(
|
||||
batch: dict[str, Any],
|
||||
num_classes: int,
|
||||
num_queries: int,
|
||||
class_embed: torch.Tensor,
|
||||
num_dn: int = 100,
|
||||
cls_noise_ratio: float = 0.5,
|
||||
box_noise_scale: float = 1.0,
|
||||
training: bool = False,
|
||||
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None, dict[str, Any] | None]:
|
||||
"""Generate contrastive denoising training group with positive and negative samples from ground truths.
|
||||
|
||||
This function creates denoising queries for contrastive denoising training by adding noise to ground truth bounding
|
||||
boxes and class labels. It generates both positive and negative samples to improve model robustness.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch dictionary containing 'cls' (torch.Tensor with shape (num_gts,)), 'bboxes'
|
||||
(torch.Tensor with shape (num_gts, 4)), 'batch_idx' (torch.Tensor), and 'gt_groups' (list[int]) indicating
|
||||
number of ground truths per image.
|
||||
num_classes (int): Total number of object classes.
|
||||
num_queries (int): Number of object queries.
|
||||
class_embed (torch.Tensor): Class embedding weights to map labels to embedding space.
|
||||
num_dn (int): Number of denoising queries to generate.
|
||||
cls_noise_ratio (float): Noise ratio for class labels.
|
||||
box_noise_scale (float): Noise scale for bounding box coordinates.
|
||||
training (bool): Whether model is in training mode.
|
||||
|
||||
Returns:
|
||||
padding_cls (torch.Tensor | None): Modified class embeddings for denoising with shape (bs, num_dn, embed_dim).
|
||||
padding_bbox (torch.Tensor | None): Modified bounding boxes for denoising with shape (bs, num_dn, 4).
|
||||
attn_mask (torch.Tensor | None): Attention mask for denoising with shape (tgt_size, tgt_size).
|
||||
dn_meta (dict[str, Any] | None): Meta information dictionary containing denoising parameters.
|
||||
|
||||
Examples:
|
||||
Generate denoising group for training
|
||||
>>> batch = {
|
||||
... "cls": torch.tensor([0, 1, 2]),
|
||||
... "bboxes": torch.rand(3, 4),
|
||||
... "batch_idx": torch.tensor([0, 0, 1]),
|
||||
... "gt_groups": [2, 1],
|
||||
... }
|
||||
>>> class_embed = torch.rand(80, 256) # 80 classes, 256 embedding dim
|
||||
>>> cdn_outputs = get_cdn_group(batch, 80, 100, class_embed, training=True)
|
||||
"""
|
||||
if (not training) or num_dn <= 0 or batch is None:
|
||||
return None, None, None, None
|
||||
gt_groups = batch["gt_groups"]
|
||||
total_num = sum(gt_groups)
|
||||
max_nums = max(gt_groups)
|
||||
if max_nums == 0:
|
||||
return None, None, None, None
|
||||
|
||||
num_group = num_dn // max_nums
|
||||
num_group = 1 if num_group == 0 else num_group
|
||||
# Pad gt to max_num of a batch
|
||||
bs = len(gt_groups)
|
||||
gt_cls = batch["cls"] # (bs*num, )
|
||||
gt_bbox = batch["bboxes"] # bs*num, 4
|
||||
b_idx = batch["batch_idx"]
|
||||
|
||||
# Each group has positive and negative queries
|
||||
dn_cls = gt_cls.repeat(2 * num_group) # (2*num_group*bs*num, )
|
||||
dn_bbox = gt_bbox.repeat(2 * num_group, 1) # 2*num_group*bs*num, 4
|
||||
dn_b_idx = b_idx.repeat(2 * num_group).view(-1) # (2*num_group*bs*num, )
|
||||
|
||||
# Positive and negative mask
|
||||
# (bs*num*num_group, ), the second total_num*num_group part as negative samples
|
||||
neg_idx = torch.arange(total_num * num_group, dtype=torch.long, device=gt_bbox.device) + num_group * total_num
|
||||
|
||||
if cls_noise_ratio > 0:
|
||||
# Apply class label noise to half of the samples
|
||||
mask = torch.rand(dn_cls.shape) < (cls_noise_ratio * 0.5)
|
||||
idx = torch.nonzero(mask).squeeze(-1)
|
||||
# Randomly assign new class labels
|
||||
new_label = torch.randint_like(idx, 0, num_classes, dtype=dn_cls.dtype, device=dn_cls.device)
|
||||
dn_cls[idx] = new_label
|
||||
|
||||
if box_noise_scale > 0:
|
||||
known_bbox = xywh2xyxy(dn_bbox)
|
||||
|
||||
diff = (dn_bbox[..., 2:] * 0.5).repeat(1, 2) * box_noise_scale # 2*num_group*bs*num, 4
|
||||
|
||||
rand_sign = torch.randint_like(dn_bbox, 0, 2) * 2.0 - 1.0
|
||||
rand_part = torch.rand_like(dn_bbox)
|
||||
rand_part[neg_idx] += 1.0
|
||||
rand_part *= rand_sign
|
||||
known_bbox += rand_part * diff
|
||||
known_bbox.clip_(min=0.0, max=1.0)
|
||||
dn_bbox = xyxy2xywh(known_bbox)
|
||||
dn_bbox = torch.logit(dn_bbox, eps=1e-6) # inverse sigmoid
|
||||
|
||||
num_dn = int(max_nums * 2 * num_group) # total denoising queries
|
||||
dn_cls_embed = class_embed[dn_cls] # bs*num * 2 * num_group, 256
|
||||
padding_cls = torch.zeros(bs, num_dn, dn_cls_embed.shape[-1], device=gt_cls.device)
|
||||
padding_bbox = torch.zeros(bs, num_dn, 4, device=gt_bbox.device)
|
||||
|
||||
map_indices = torch.cat([torch.tensor(range(num), dtype=torch.long) for num in gt_groups])
|
||||
pos_idx = torch.stack([map_indices + max_nums * i for i in range(num_group)], dim=0)
|
||||
|
||||
map_indices = torch.cat([map_indices + max_nums * i for i in range(2 * num_group)])
|
||||
padding_cls[(dn_b_idx, map_indices)] = dn_cls_embed
|
||||
padding_bbox[(dn_b_idx, map_indices)] = dn_bbox
|
||||
|
||||
tgt_size = num_dn + num_queries
|
||||
attn_mask = torch.zeros([tgt_size, tgt_size], dtype=torch.bool)
|
||||
# Match query cannot see the reconstruct
|
||||
attn_mask[num_dn:, :num_dn] = True
|
||||
# Reconstruct cannot see each other
|
||||
for i in range(num_group):
|
||||
if i == 0:
|
||||
attn_mask[max_nums * 2 * i : max_nums * 2 * (i + 1), max_nums * 2 * (i + 1) : num_dn] = True
|
||||
if i == num_group - 1:
|
||||
attn_mask[max_nums * 2 * i : max_nums * 2 * (i + 1), : max_nums * i * 2] = True
|
||||
else:
|
||||
attn_mask[max_nums * 2 * i : max_nums * 2 * (i + 1), max_nums * 2 * (i + 1) : num_dn] = True
|
||||
attn_mask[max_nums * 2 * i : max_nums * 2 * (i + 1), : max_nums * 2 * i] = True
|
||||
dn_meta = {
|
||||
"dn_pos_idx": [p.reshape(-1) for p in pos_idx.cpu().split(list(gt_groups), dim=1)],
|
||||
"dn_num_group": num_group,
|
||||
"dn_num_split": [num_dn, num_queries],
|
||||
}
|
||||
|
||||
return (
|
||||
padding_cls.to(class_embed.device),
|
||||
padding_bbox.to(class_embed.device),
|
||||
attn_mask.to(class_embed.device),
|
||||
dn_meta,
|
||||
)
|
||||
7
ultralytics/models/yolo/__init__.py
Executable file
7
ultralytics/models/yolo/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.models.yolo import classify, detect, obb, pose, segment, world, yoloe
|
||||
|
||||
from .model import YOLO, YOLOE, YOLOWorld
|
||||
|
||||
__all__ = "YOLO", "YOLOE", "YOLOWorld", "classify", "detect", "obb", "pose", "segment", "world", "yoloe"
|
||||
7
ultralytics/models/yolo/classify/__init__.py
Executable file
7
ultralytics/models/yolo/classify/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.models.yolo.classify.predict import ClassificationPredictor
|
||||
from ultralytics.models.yolo.classify.train import ClassificationTrainer
|
||||
from ultralytics.models.yolo.classify.val import ClassificationValidator
|
||||
|
||||
__all__ = "ClassificationPredictor", "ClassificationTrainer", "ClassificationValidator"
|
||||
90
ultralytics/models/yolo/classify/predict.py
Executable file
90
ultralytics/models/yolo/classify/predict.py
Executable file
@@ -0,0 +1,90 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from ultralytics.data.augment import classify_transforms
|
||||
from ultralytics.engine.predictor import BasePredictor
|
||||
from ultralytics.engine.results import Results
|
||||
from ultralytics.utils import DEFAULT_CFG, ops
|
||||
|
||||
|
||||
class ClassificationPredictor(BasePredictor):
|
||||
"""A class extending the BasePredictor class for prediction based on a classification model.
|
||||
|
||||
This predictor handles the specific requirements of classification models, including preprocessing images and
|
||||
postprocessing predictions to generate classification results.
|
||||
|
||||
Attributes:
|
||||
args (dict): Configuration arguments for the predictor.
|
||||
|
||||
Methods:
|
||||
preprocess: Convert input images to model-compatible format.
|
||||
postprocess: Process model predictions into Results objects.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.yolo.classify import ClassificationPredictor
|
||||
>>> args = dict(model="yolo26n-cls.pt", source=ASSETS)
|
||||
>>> predictor = ClassificationPredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
|
||||
Notes:
|
||||
- Torchvision classification models can also be passed to the 'model' argument, i.e. model='resnet18'.
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize the ClassificationPredictor with the specified configuration and set task to 'classify'.
|
||||
|
||||
This constructor initializes a ClassificationPredictor instance, which extends BasePredictor for classification
|
||||
tasks. It ensures the task is set to 'classify' regardless of input configuration.
|
||||
|
||||
Args:
|
||||
cfg (dict): Default configuration dictionary containing prediction settings.
|
||||
overrides (dict, optional): Configuration overrides that take precedence over cfg.
|
||||
_callbacks (list, optional): List of callback functions to be executed during prediction.
|
||||
"""
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.args.task = "classify"
|
||||
|
||||
def setup_source(self, source):
|
||||
"""Set up source and inference mode and classify transforms."""
|
||||
super().setup_source(source)
|
||||
updated = (
|
||||
self.model.model.transforms.transforms[0].size != max(self.imgsz)
|
||||
if hasattr(self.model.model, "transforms") and hasattr(self.model.model.transforms.transforms[0], "size")
|
||||
else False
|
||||
)
|
||||
self.transforms = (
|
||||
classify_transforms(self.imgsz) if updated or not self.model.pt else self.model.model.transforms
|
||||
)
|
||||
|
||||
def preprocess(self, img):
|
||||
"""Convert input images to model-compatible tensor format with appropriate normalization."""
|
||||
if not isinstance(img, torch.Tensor):
|
||||
img = torch.stack(
|
||||
[self.transforms(Image.fromarray(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))) for im in img], dim=0
|
||||
)
|
||||
img = (img if isinstance(img, torch.Tensor) else torch.from_numpy(img)).to(self.model.device)
|
||||
return img.half() if self.model.fp16 else img.float() # Convert uint8 to fp16/32
|
||||
|
||||
def postprocess(self, preds, img, orig_imgs):
|
||||
"""Process predictions to return Results objects with classification probabilities.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Raw predictions from the model.
|
||||
img (torch.Tensor): Input images after preprocessing.
|
||||
orig_imgs (list[np.ndarray] | torch.Tensor): Original images before preprocessing.
|
||||
|
||||
Returns:
|
||||
(list[Results]): List of Results objects containing classification results for each image.
|
||||
"""
|
||||
if not isinstance(orig_imgs, list): # Input images are a torch.Tensor, not a list
|
||||
orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)[..., ::-1]
|
||||
|
||||
preds = preds[0] if isinstance(preds, (list, tuple)) else preds
|
||||
return [
|
||||
Results(orig_img, path=img_path, names=self.model.names, probs=pred)
|
||||
for pred, orig_img, img_path in zip(preds, orig_imgs, self.batch[0])
|
||||
]
|
||||
214
ultralytics/models/yolo/classify/train.py
Executable file
214
ultralytics/models/yolo/classify/train.py
Executable file
@@ -0,0 +1,214 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data import ClassificationDataset, build_dataloader
|
||||
from ultralytics.engine.trainer import BaseTrainer
|
||||
from ultralytics.models import yolo
|
||||
from ultralytics.nn.tasks import ClassificationModel
|
||||
from ultralytics.utils import DEFAULT_CFG, LOGGER, RANK
|
||||
from ultralytics.utils.plotting import plot_images
|
||||
from ultralytics.utils.torch_utils import is_parallel, torch_distributed_zero_first
|
||||
|
||||
|
||||
class ClassificationTrainer(BaseTrainer):
|
||||
"""A trainer class extending BaseTrainer for training image classification models.
|
||||
|
||||
This trainer handles the training process for image classification tasks, supporting both YOLO classification models
|
||||
and torchvision models with comprehensive dataset handling and validation.
|
||||
|
||||
Attributes:
|
||||
model (ClassificationModel): The classification model to be trained.
|
||||
data (dict[str, Any]): Dictionary containing dataset information including class names and number of classes.
|
||||
loss_names (list[str]): Names of the loss functions used during training.
|
||||
validator (ClassificationValidator): Validator instance for model evaluation.
|
||||
|
||||
Methods:
|
||||
set_model_attributes: Set the model's class names from the loaded dataset.
|
||||
get_model: Return a modified PyTorch model configured for training.
|
||||
setup_model: Load, create or download model for classification.
|
||||
build_dataset: Create a ClassificationDataset instance.
|
||||
get_dataloader: Return PyTorch DataLoader with transforms for image preprocessing.
|
||||
preprocess_batch: Preprocess a batch of images and classes.
|
||||
progress_string: Return a formatted string showing training progress.
|
||||
get_validator: Return an instance of ClassificationValidator.
|
||||
label_loss_items: Return a loss dict with labeled training loss items.
|
||||
final_eval: Evaluate trained model and save validation results.
|
||||
plot_training_samples: Plot training samples with their annotations.
|
||||
|
||||
Examples:
|
||||
Initialize and train a classification model
|
||||
>>> from ultralytics.models.yolo.classify import ClassificationTrainer
|
||||
>>> args = dict(model="yolo26n-cls.pt", data="imagenet10", epochs=3)
|
||||
>>> trainer = ClassificationTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict[str, Any] | None = None, _callbacks=None):
|
||||
"""Initialize a ClassificationTrainer object.
|
||||
|
||||
Args:
|
||||
cfg (dict[str, Any], optional): Default configuration dictionary containing training parameters.
|
||||
overrides (dict[str, Any], optional): Dictionary of parameter overrides for the default configuration.
|
||||
_callbacks (list[Any], optional): List of callback functions to be executed during training.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
overrides["task"] = "classify"
|
||||
if overrides.get("imgsz") is None:
|
||||
overrides["imgsz"] = 224
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def set_model_attributes(self):
|
||||
"""Set the YOLO model's class names from the loaded dataset."""
|
||||
self.model.names = self.data["names"]
|
||||
|
||||
def get_model(self, cfg=None, weights=None, verbose: bool = True):
|
||||
"""Return a modified PyTorch model configured for training YOLO classification.
|
||||
|
||||
Args:
|
||||
cfg (Any, optional): Model configuration.
|
||||
weights (Any, optional): Pre-trained model weights.
|
||||
verbose (bool, optional): Whether to display model information.
|
||||
|
||||
Returns:
|
||||
(ClassificationModel): Configured PyTorch model for classification.
|
||||
"""
|
||||
model = ClassificationModel(cfg, nc=self.data["nc"], ch=self.data["channels"], verbose=verbose and RANK == -1)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
for m in model.modules():
|
||||
if not self.args.pretrained and hasattr(m, "reset_parameters"):
|
||||
m.reset_parameters()
|
||||
if isinstance(m, torch.nn.Dropout) and self.args.dropout:
|
||||
m.p = self.args.dropout # set dropout
|
||||
for p in model.parameters():
|
||||
p.requires_grad = True # for training
|
||||
return model
|
||||
|
||||
def setup_model(self):
|
||||
"""Load, create or download model for classification tasks.
|
||||
|
||||
Returns:
|
||||
(Any): Model checkpoint if applicable, otherwise None.
|
||||
"""
|
||||
import torchvision # scope for faster 'import ultralytics'
|
||||
|
||||
if str(self.model) in torchvision.models.__dict__:
|
||||
self.model = torchvision.models.__dict__[self.model](
|
||||
weights="IMAGENET1K_V1" if self.args.pretrained else None
|
||||
)
|
||||
ckpt = None
|
||||
else:
|
||||
ckpt = super().setup_model()
|
||||
ClassificationModel.reshape_outputs(self.model, self.data["nc"])
|
||||
return ckpt
|
||||
|
||||
def build_dataset(self, img_path: str, mode: str = "train", batch=None):
|
||||
"""Create a ClassificationDataset instance given an image path and mode.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the dataset images.
|
||||
mode (str, optional): Dataset mode ('train', 'val', or 'test').
|
||||
batch (Any, optional): Batch information (unused in this implementation).
|
||||
|
||||
Returns:
|
||||
(ClassificationDataset): Dataset for the specified mode.
|
||||
"""
|
||||
return ClassificationDataset(root=img_path, args=self.args, augment=mode == "train", prefix=mode)
|
||||
|
||||
def get_dataloader(self, dataset_path: str, batch_size: int = 16, rank: int = 0, mode: str = "train"):
|
||||
"""Return PyTorch DataLoader with transforms to preprocess images.
|
||||
|
||||
Args:
|
||||
dataset_path (str): Path to the dataset.
|
||||
batch_size (int, optional): Number of images per batch.
|
||||
rank (int, optional): Process rank for distributed training.
|
||||
mode (str, optional): 'train', 'val', or 'test' mode.
|
||||
|
||||
Returns:
|
||||
(torch.utils.data.DataLoader): DataLoader for the specified dataset and mode.
|
||||
"""
|
||||
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
|
||||
dataset = self.build_dataset(dataset_path, mode)
|
||||
|
||||
# Filter out samples with class indices >= nc (prevents CUDA assertion errors)
|
||||
nc = self.data.get("nc", 0)
|
||||
dataset_nc = len(dataset.base.classes)
|
||||
if nc and dataset_nc > nc:
|
||||
extra_classes = dataset.base.classes[nc:]
|
||||
original_count = len(dataset.samples)
|
||||
dataset.samples = [s for s in dataset.samples if s[1] < nc]
|
||||
skipped = original_count - len(dataset.samples)
|
||||
LOGGER.warning(
|
||||
f"{mode} split has {dataset_nc} classes but model expects {nc}. "
|
||||
f"Skipping {skipped} samples from extra classes: {extra_classes}"
|
||||
)
|
||||
|
||||
loader = build_dataloader(dataset, batch_size, self.args.workers, rank=rank, drop_last=self.args.compile)
|
||||
# Attach inference transforms
|
||||
if mode != "train":
|
||||
if is_parallel(self.model):
|
||||
self.model.module.transforms = loader.dataset.torch_transforms
|
||||
else:
|
||||
self.model.transforms = loader.dataset.torch_transforms
|
||||
return loader
|
||||
|
||||
def preprocess_batch(self, batch: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
||||
"""Preprocess a batch of images and classes."""
|
||||
batch["img"] = batch["img"].to(self.device, non_blocking=self.device.type == "cuda")
|
||||
batch["cls"] = batch["cls"].to(self.device, non_blocking=self.device.type == "cuda")
|
||||
return batch
|
||||
|
||||
def progress_string(self) -> str:
|
||||
"""Return a formatted string showing training progress."""
|
||||
return ("\n" + "%11s" * (4 + len(self.loss_names))) % (
|
||||
"Epoch",
|
||||
"GPU_mem",
|
||||
*self.loss_names,
|
||||
"Instances",
|
||||
"Size",
|
||||
)
|
||||
|
||||
def get_validator(self):
|
||||
"""Return an instance of ClassificationValidator for validation."""
|
||||
self.loss_names = ["loss"]
|
||||
return yolo.classify.ClassificationValidator(
|
||||
self.test_loader, self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
|
||||
def label_loss_items(self, loss_items: torch.Tensor | None = None, prefix: str = "train"):
|
||||
"""Return a loss dict with labeled training loss items tensor.
|
||||
|
||||
Args:
|
||||
loss_items (torch.Tensor, optional): Loss tensor items.
|
||||
prefix (str, optional): Prefix to prepend to loss names.
|
||||
|
||||
Returns:
|
||||
(dict | list): Dictionary of labeled loss items if loss_items is provided, otherwise list of keys.
|
||||
"""
|
||||
keys = [f"{prefix}/{x}" for x in self.loss_names]
|
||||
if loss_items is None:
|
||||
return keys
|
||||
loss_items = [round(float(loss_items), 5)]
|
||||
return dict(zip(keys, loss_items))
|
||||
|
||||
def plot_training_samples(self, batch: dict[str, torch.Tensor], ni: int):
|
||||
"""Plot training samples with their annotations.
|
||||
|
||||
Args:
|
||||
batch (dict[str, torch.Tensor]): Batch containing images and class labels.
|
||||
ni (int): Batch index used for naming the output file.
|
||||
"""
|
||||
batch["batch_idx"] = torch.arange(batch["img"].shape[0]) # add batch index for plotting
|
||||
plot_images(
|
||||
labels=batch,
|
||||
fname=self.save_dir / f"train_batch{ni}.jpg",
|
||||
on_plot=self.on_plot,
|
||||
)
|
||||
216
ultralytics/models/yolo/classify/val.py
Executable file
216
ultralytics/models/yolo/classify/val.py
Executable file
@@ -0,0 +1,216 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from ultralytics.data import ClassificationDataset, build_dataloader
|
||||
from ultralytics.engine.validator import BaseValidator
|
||||
from ultralytics.utils import LOGGER, RANK
|
||||
from ultralytics.utils.metrics import ClassifyMetrics, ConfusionMatrix
|
||||
from ultralytics.utils.plotting import plot_images
|
||||
|
||||
|
||||
class ClassificationValidator(BaseValidator):
|
||||
"""A class extending the BaseValidator class for validation based on a classification model.
|
||||
|
||||
This validator handles the validation process for classification models, including metrics calculation, confusion
|
||||
matrix generation, and visualization of results.
|
||||
|
||||
Attributes:
|
||||
targets (list[torch.Tensor]): Ground truth class labels.
|
||||
pred (list[torch.Tensor]): Model predictions.
|
||||
metrics (ClassifyMetrics): Object to calculate and store classification metrics.
|
||||
names (dict): Mapping of class indices to class names.
|
||||
nc (int): Number of classes.
|
||||
confusion_matrix (ConfusionMatrix): Matrix to evaluate model performance across classes.
|
||||
|
||||
Methods:
|
||||
get_desc: Return a formatted string summarizing classification metrics.
|
||||
init_metrics: Initialize confusion matrix, class names, and tracking containers.
|
||||
preprocess: Preprocess input batch by moving data to device.
|
||||
update_metrics: Update running metrics with model predictions and batch targets.
|
||||
finalize_metrics: Finalize metrics including confusion matrix and processing speed.
|
||||
postprocess: Extract the primary prediction from model output.
|
||||
get_stats: Calculate and return a dictionary of metrics.
|
||||
build_dataset: Create a ClassificationDataset instance for validation.
|
||||
get_dataloader: Build and return a data loader for classification validation.
|
||||
print_results: Print evaluation metrics for the classification model.
|
||||
plot_val_samples: Plot validation image samples with their ground truth labels.
|
||||
plot_predictions: Plot images with their predicted class labels.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.classify import ClassificationValidator
|
||||
>>> args = dict(model="yolo26n-cls.pt", data="imagenet10")
|
||||
>>> validator = ClassificationValidator(args=args)
|
||||
>>> validator()
|
||||
|
||||
Notes:
|
||||
Torchvision classification models can also be passed to the 'model' argument, i.e. model='resnet18'.
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
|
||||
"""Initialize ClassificationValidator with dataloader, save directory, and other parameters.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to use for validation.
|
||||
save_dir (str | Path, optional): Directory to save results.
|
||||
args (dict, optional): Arguments containing model and validation configuration.
|
||||
_callbacks (list, optional): List of callback functions to be called during validation.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.targets = None
|
||||
self.pred = None
|
||||
self.args.task = "classify"
|
||||
self.metrics = ClassifyMetrics()
|
||||
|
||||
def get_desc(self) -> str:
|
||||
"""Return a formatted string summarizing classification metrics."""
|
||||
return ("%22s" + "%11s" * 2) % ("classes", "top1_acc", "top5_acc")
|
||||
|
||||
def init_metrics(self, model: torch.nn.Module) -> None:
|
||||
"""Initialize confusion matrix, class names, and tracking containers for predictions and targets."""
|
||||
self.names = model.names
|
||||
self.nc = len(model.names)
|
||||
self.pred = []
|
||||
self.targets = []
|
||||
self.confusion_matrix = ConfusionMatrix(names=model.names)
|
||||
|
||||
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Preprocess input batch by moving data to device and converting to appropriate dtype."""
|
||||
batch["img"] = batch["img"].to(self.device, non_blocking=self.device.type == "cuda")
|
||||
batch["img"] = batch["img"].half() if self.args.half else batch["img"].float()
|
||||
batch["cls"] = batch["cls"].to(self.device, non_blocking=self.device.type == "cuda")
|
||||
return batch
|
||||
|
||||
def update_metrics(self, preds: torch.Tensor, batch: dict[str, Any]) -> None:
|
||||
"""Update running metrics with model predictions and batch targets.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Model predictions, typically logits or probabilities for each class.
|
||||
batch (dict): Batch data containing images and class labels.
|
||||
|
||||
Notes:
|
||||
This method appends the top-N predictions (sorted by confidence in descending order) to the
|
||||
prediction list for later evaluation. N is limited to the minimum of 5 and the number of classes.
|
||||
"""
|
||||
n5 = min(len(self.names), 5)
|
||||
self.pred.append(preds.argsort(1, descending=True)[:, :n5].type(torch.int32).cpu())
|
||||
self.targets.append(batch["cls"].type(torch.int32).cpu())
|
||||
|
||||
def finalize_metrics(self) -> None:
|
||||
"""Finalize metrics including confusion matrix and processing speed.
|
||||
|
||||
Examples:
|
||||
>>> validator = ClassificationValidator()
|
||||
>>> validator.pred = [torch.tensor([[0, 1, 2]])] # Top-3 predictions for one sample
|
||||
>>> validator.targets = [torch.tensor([0])] # Ground truth class
|
||||
>>> validator.finalize_metrics()
|
||||
>>> print(validator.metrics.confusion_matrix) # Access the confusion matrix
|
||||
|
||||
Notes:
|
||||
This method processes the accumulated predictions and targets to generate the confusion matrix,
|
||||
optionally plots it, and updates the metrics object with speed information.
|
||||
"""
|
||||
self.confusion_matrix.process_cls_preds(self.pred, self.targets)
|
||||
if self.args.plots:
|
||||
for normalize in True, False:
|
||||
self.confusion_matrix.plot(save_dir=self.save_dir, normalize=normalize, on_plot=self.on_plot)
|
||||
self.metrics.speed = self.speed
|
||||
self.metrics.save_dir = self.save_dir
|
||||
self.metrics.confusion_matrix = self.confusion_matrix
|
||||
|
||||
def postprocess(self, preds: torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor]) -> torch.Tensor:
|
||||
"""Extract the primary prediction from model output if it's in a list or tuple format."""
|
||||
return preds[0] if isinstance(preds, (list, tuple)) else preds
|
||||
|
||||
def get_stats(self) -> dict[str, float]:
|
||||
"""Calculate and return a dictionary of metrics by processing targets and predictions."""
|
||||
self.metrics.process(self.targets, self.pred)
|
||||
return self.metrics.results_dict
|
||||
|
||||
def gather_stats(self) -> None:
|
||||
"""Gather stats from all GPUs."""
|
||||
if RANK == 0:
|
||||
gathered_preds = [None] * dist.get_world_size()
|
||||
gathered_targets = [None] * dist.get_world_size()
|
||||
dist.gather_object(self.pred, gathered_preds, dst=0)
|
||||
dist.gather_object(self.targets, gathered_targets, dst=0)
|
||||
self.pred = [pred for rank in gathered_preds for pred in rank]
|
||||
self.targets = [targets for rank in gathered_targets for targets in rank]
|
||||
elif RANK > 0:
|
||||
dist.gather_object(self.pred, None, dst=0)
|
||||
dist.gather_object(self.targets, None, dst=0)
|
||||
|
||||
def build_dataset(self, img_path: str) -> ClassificationDataset:
|
||||
"""Create a ClassificationDataset instance for validation."""
|
||||
return ClassificationDataset(root=img_path, args=self.args, augment=False, prefix=self.args.split)
|
||||
|
||||
def get_dataloader(self, dataset_path: Path | str, batch_size: int) -> torch.utils.data.DataLoader:
|
||||
"""Build and return a data loader for classification validation.
|
||||
|
||||
Args:
|
||||
dataset_path (str | Path): Path to the dataset directory.
|
||||
batch_size (int): Number of samples per batch.
|
||||
|
||||
Returns:
|
||||
(torch.utils.data.DataLoader): DataLoader object for the classification validation dataset.
|
||||
"""
|
||||
dataset = self.build_dataset(dataset_path)
|
||||
return build_dataloader(dataset, batch_size, self.args.workers, rank=-1)
|
||||
|
||||
def print_results(self) -> None:
|
||||
"""Print evaluation metrics for the classification model."""
|
||||
pf = "%22s" + "%11.3g" * len(self.metrics.keys) # print format
|
||||
LOGGER.info(pf % ("all", self.metrics.top1, self.metrics.top5))
|
||||
|
||||
def plot_val_samples(self, batch: dict[str, Any], ni: int) -> None:
|
||||
"""Plot validation image samples with their ground truth labels.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Dictionary containing batch data with 'img' (images) and 'cls' (class labels).
|
||||
ni (int): Batch index used for naming the output file.
|
||||
|
||||
Examples:
|
||||
>>> validator = ClassificationValidator()
|
||||
>>> batch = {"img": torch.rand(16, 3, 224, 224), "cls": torch.randint(0, 10, (16,))}
|
||||
>>> validator.plot_val_samples(batch, 0)
|
||||
"""
|
||||
batch["batch_idx"] = torch.arange(batch["img"].shape[0]) # add batch index for plotting
|
||||
plot_images(
|
||||
labels=batch,
|
||||
fname=self.save_dir / f"val_batch{ni}_labels.jpg",
|
||||
names=self.names,
|
||||
on_plot=self.on_plot,
|
||||
)
|
||||
|
||||
def plot_predictions(self, batch: dict[str, Any], preds: torch.Tensor, ni: int) -> None:
|
||||
"""Plot images with their predicted class labels and save the visualization.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch data containing images and other information.
|
||||
preds (torch.Tensor): Model predictions with shape (batch_size, num_classes).
|
||||
ni (int): Batch index used for naming the output file.
|
||||
|
||||
Examples:
|
||||
>>> validator = ClassificationValidator()
|
||||
>>> batch = {"img": torch.rand(16, 3, 224, 224)}
|
||||
>>> preds = torch.rand(16, 10) # 16 images, 10 classes
|
||||
>>> validator.plot_predictions(batch, preds, 0)
|
||||
"""
|
||||
batched_preds = dict(
|
||||
img=batch["img"],
|
||||
batch_idx=torch.arange(batch["img"].shape[0]),
|
||||
cls=torch.argmax(preds, dim=1),
|
||||
conf=torch.amax(preds, dim=1),
|
||||
)
|
||||
plot_images(
|
||||
batched_preds,
|
||||
fname=self.save_dir / f"val_batch{ni}_pred.jpg",
|
||||
names=self.names,
|
||||
on_plot=self.on_plot,
|
||||
) # pred
|
||||
7
ultralytics/models/yolo/detect/__init__.py
Executable file
7
ultralytics/models/yolo/detect/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .predict import DetectionPredictor
|
||||
from .train import DetectionTrainer, GroundDetectionTrainer
|
||||
from .val import DetectionValidator
|
||||
|
||||
__all__ = "DetectionPredictor", "DetectionTrainer", "DetectionValidator", "GroundDetectionTrainer"
|
||||
122
ultralytics/models/yolo/detect/predict.py
Executable file
122
ultralytics/models/yolo/detect/predict.py
Executable file
@@ -0,0 +1,122 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.engine.predictor import BasePredictor
|
||||
from ultralytics.engine.results import Results
|
||||
from ultralytics.utils import nms, ops
|
||||
|
||||
|
||||
class DetectionPredictor(BasePredictor):
|
||||
"""A class extending the BasePredictor class for prediction based on a detection model.
|
||||
|
||||
This predictor specializes in object detection tasks, processing model outputs into meaningful detection results
|
||||
with bounding boxes and class predictions.
|
||||
|
||||
Attributes:
|
||||
args (namespace): Configuration arguments for the predictor.
|
||||
model (nn.Module): The detection model used for inference.
|
||||
batch (list): Batch of images and metadata for processing.
|
||||
|
||||
Methods:
|
||||
postprocess: Process raw model predictions into detection results.
|
||||
construct_results: Build Results objects from processed predictions.
|
||||
construct_result: Create a single Result object from a prediction.
|
||||
get_obj_feats: Extract object features from the feature maps.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.yolo.detect import DetectionPredictor
|
||||
>>> args = dict(model="yolo26n.pt", source=ASSETS)
|
||||
>>> predictor = DetectionPredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
"""
|
||||
|
||||
def postprocess(self, preds, img, orig_imgs, **kwargs):
|
||||
"""Post-process predictions and return a list of Results objects.
|
||||
|
||||
This method applies non-maximum suppression to raw model predictions and prepares them for visualization and
|
||||
further analysis.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Raw predictions from the model.
|
||||
img (torch.Tensor): Processed input image tensor in model input format.
|
||||
orig_imgs (torch.Tensor | list): Original input images before preprocessing.
|
||||
**kwargs (Any): Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
(list): List of Results objects containing the post-processed predictions.
|
||||
|
||||
Examples:
|
||||
>>> predictor = DetectionPredictor(overrides=dict(model="yolo26n.pt"))
|
||||
>>> results = predictor.predict("path/to/image.jpg")
|
||||
>>> processed_results = predictor.postprocess(preds, img, orig_imgs)
|
||||
"""
|
||||
save_feats = getattr(self, "_feats", None) is not None
|
||||
preds = nms.non_max_suppression(
|
||||
preds,
|
||||
self.args.conf,
|
||||
self.args.iou,
|
||||
self.args.classes,
|
||||
self.args.agnostic_nms,
|
||||
max_det=self.args.max_det,
|
||||
nc=0 if self.args.task == "detect" else len(self.model.names),
|
||||
end2end=getattr(self.model, "end2end", False),
|
||||
rotated=self.args.task == "obb",
|
||||
return_idxs=save_feats,
|
||||
)
|
||||
|
||||
if not isinstance(orig_imgs, list): # input images are a torch.Tensor, not a list
|
||||
orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)[..., ::-1]
|
||||
|
||||
if save_feats:
|
||||
obj_feats = self.get_obj_feats(self._feats, preds[1])
|
||||
preds = preds[0]
|
||||
|
||||
results = self.construct_results(preds, img, orig_imgs, **kwargs)
|
||||
|
||||
if save_feats:
|
||||
for r, f in zip(results, obj_feats):
|
||||
r.feats = f # add object features to results
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_obj_feats(feat_maps, idxs):
|
||||
"""Extract object features from the feature maps."""
|
||||
import torch
|
||||
|
||||
s = min(x.shape[1] for x in feat_maps) # find shortest vector length
|
||||
obj_feats = torch.cat(
|
||||
[x.permute(0, 2, 3, 1).reshape(x.shape[0], -1, s, x.shape[1] // s).mean(dim=-1) for x in feat_maps], dim=1
|
||||
) # mean reduce all vectors to same length
|
||||
return [feats[idx] if idx.shape[0] else [] for feats, idx in zip(obj_feats, idxs)] # for each img in batch
|
||||
|
||||
def construct_results(self, preds, img, orig_imgs):
|
||||
"""Construct a list of Results objects from model predictions.
|
||||
|
||||
Args:
|
||||
preds (list[torch.Tensor]): List of predicted bounding boxes and scores for each image.
|
||||
img (torch.Tensor): Batch of preprocessed images used for inference.
|
||||
orig_imgs (list[np.ndarray]): List of original images before preprocessing.
|
||||
|
||||
Returns:
|
||||
(list[Results]): List of Results objects containing detection information for each image.
|
||||
"""
|
||||
return [
|
||||
self.construct_result(pred, img, orig_img, img_path)
|
||||
for pred, orig_img, img_path in zip(preds, orig_imgs, self.batch[0])
|
||||
]
|
||||
|
||||
def construct_result(self, pred, img, orig_img, img_path):
|
||||
"""Construct a single Results object from one image prediction.
|
||||
|
||||
Args:
|
||||
pred (torch.Tensor): Predicted boxes and scores with shape (N, 6) where N is the number of detections.
|
||||
img (torch.Tensor): Preprocessed image tensor used for inference.
|
||||
orig_img (np.ndarray): Original image before preprocessing.
|
||||
img_path (str): Path to the original image file.
|
||||
|
||||
Returns:
|
||||
(Results): Results object containing the original image, image path, class names, and scaled bounding boxes.
|
||||
"""
|
||||
pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)
|
||||
return Results(orig_img, path=img_path, names=self.model.names, boxes=pred[:, :6])
|
||||
1288
ultralytics/models/yolo/detect/train.py
Executable file
1288
ultralytics/models/yolo/detect/train.py
Executable file
File diff suppressed because it is too large
Load Diff
515
ultralytics/models/yolo/detect/val.py
Executable file
515
ultralytics/models/yolo/detect/val.py
Executable file
@@ -0,0 +1,515 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from ultralytics.data import build_dataloader, build_yolo_dataset, converter
|
||||
from ultralytics.engine.validator import BaseValidator
|
||||
from ultralytics.utils import LOGGER, RANK, nms, ops
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
from ultralytics.utils.metrics import ConfusionMatrix, DetMetrics, box_iou
|
||||
from ultralytics.utils.plotting import plot_images
|
||||
|
||||
|
||||
class DetectionValidator(BaseValidator):
|
||||
"""A class extending the BaseValidator class for validation based on a detection model.
|
||||
|
||||
This class implements validation functionality specific to object detection tasks, including metrics calculation,
|
||||
prediction processing, and visualization of results.
|
||||
|
||||
Attributes:
|
||||
is_coco (bool): Whether the dataset is COCO.
|
||||
is_lvis (bool): Whether the dataset is LVIS.
|
||||
class_map (list[int]): Mapping from model class indices to dataset class indices.
|
||||
metrics (DetMetrics): Object detection metrics calculator.
|
||||
iouv (torch.Tensor): IoU thresholds for mAP calculation.
|
||||
niou (int): Number of IoU thresholds.
|
||||
lb (list[Any]): List for storing ground truth labels for hybrid saving.
|
||||
jdict (list[dict[str, Any]]): List for storing JSON detection results.
|
||||
stats (dict[str, list[torch.Tensor]]): Dictionary for storing statistics during validation.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.detect import DetectionValidator
|
||||
>>> args = dict(model="yolo26n.pt", data="coco8.yaml")
|
||||
>>> validator = DetectionValidator(args=args)
|
||||
>>> validator()
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
|
||||
"""Initialize detection validator with necessary variables and settings.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to use for validation.
|
||||
save_dir (Path, optional): Directory to save results.
|
||||
args (dict[str, Any], optional): Arguments for the validator.
|
||||
_callbacks (list[Any], optional): List of callback functions.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.is_coco = False
|
||||
self.is_lvis = False
|
||||
self.class_map = None
|
||||
self.args.task = "detect"
|
||||
self.iouv = torch.linspace(0.5, 0.95, 10) # IoU vector for mAP@0.5:0.95
|
||||
self.niou = self.iouv.numel()
|
||||
self.metrics = DetMetrics()
|
||||
|
||||
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Preprocess batch of images for YOLO validation.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch containing images and annotations.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Preprocessed batch.
|
||||
"""
|
||||
for k, v in batch.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
batch[k] = v.to(self.device, non_blocking=self.device.type == "cuda")
|
||||
batch["img"] = (batch["img"].half() if self.args.half else batch["img"].float()) / 255
|
||||
return batch
|
||||
|
||||
def init_metrics(self, model: torch.nn.Module) -> None:
|
||||
"""Initialize evaluation metrics for YOLO detection validation.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Model to validate.
|
||||
"""
|
||||
val = self.data.get(self.args.split, "") # validation path
|
||||
self.is_coco = (
|
||||
isinstance(val, str)
|
||||
and "coco" in val
|
||||
and (val.endswith(f"{os.sep}val2017.txt") or val.endswith(f"{os.sep}test-dev2017.txt"))
|
||||
) # is COCO
|
||||
self.is_lvis = isinstance(val, str) and "lvis" in val and not self.is_coco # is LVIS
|
||||
self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1, len(model.names) + 1))
|
||||
self.args.save_json |= self.args.val and (self.is_coco or self.is_lvis) and not self.training # run final val
|
||||
self.names = model.names
|
||||
self.nc = len(model.names)
|
||||
self.end2end = getattr(model, "end2end", False)
|
||||
self.seen = 0
|
||||
self.jdict = []
|
||||
self.metrics.names = model.names
|
||||
self.confusion_matrix = ConfusionMatrix(names=model.names, save_matches=self.args.plots and self.args.visualize)
|
||||
|
||||
def get_desc(self) -> str:
|
||||
"""Return a formatted string summarizing class metrics of YOLO model."""
|
||||
return ("%22s" + "%11s" * 6) % ("Class", "Images", "Instances", "Box(P", "R", "mAP50", "mAP50-95)")
|
||||
|
||||
def postprocess(self, preds: torch.Tensor) -> list[dict[str, torch.Tensor]]:
|
||||
"""Apply Non-maximum suppression to prediction outputs.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Raw predictions from the model.
|
||||
|
||||
Returns:
|
||||
(list[dict[str, torch.Tensor]]): Processed predictions after NMS, where each dict contains 'bboxes', 'conf',
|
||||
'cls', and 'extra' tensors.
|
||||
"""
|
||||
outputs = nms.non_max_suppression(
|
||||
preds,
|
||||
self.args.conf,
|
||||
self.args.iou,
|
||||
nc=0 if self.args.task == "detect" else self.nc,
|
||||
multi_label=True,
|
||||
agnostic=self.args.single_cls or self.args.agnostic_nms,
|
||||
max_det=self.args.max_det,
|
||||
end2end=self.end2end,
|
||||
rotated=self.args.task == "obb",
|
||||
)
|
||||
return [{"bboxes": x[:, :4], "conf": x[:, 4], "cls": x[:, 5], "extra": x[:, 6:]} for x in outputs]
|
||||
|
||||
def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare a batch of images and annotations for validation.
|
||||
|
||||
Args:
|
||||
si (int): Sample index within the batch.
|
||||
batch (dict[str, Any]): Batch data containing images and annotations.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Prepared batch with processed annotations.
|
||||
"""
|
||||
idx = batch["batch_idx"] == si
|
||||
cls = batch["cls"][idx].squeeze(-1)
|
||||
bbox = batch["bboxes"][idx]
|
||||
ori_shape = batch["ori_shape"][si]
|
||||
imgsz = batch["img"].shape[2:]
|
||||
ratio_pad = batch["ratio_pad"][si]
|
||||
if cls.shape[0]:
|
||||
bbox = ops.xywh2xyxy(bbox) * torch.tensor(imgsz, device=self.device)[[1, 0, 1, 0]] # target boxes
|
||||
return {
|
||||
"cls": cls,
|
||||
"bboxes": bbox,
|
||||
"ori_shape": ori_shape,
|
||||
"imgsz": imgsz,
|
||||
"ratio_pad": ratio_pad,
|
||||
"im_file": batch["im_file"][si],
|
||||
}
|
||||
|
||||
def _prepare_pred(self, pred: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
||||
"""Prepare predictions for evaluation against ground truth.
|
||||
|
||||
Args:
|
||||
pred (dict[str, torch.Tensor]): Post-processed predictions from the model.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Prepared predictions in native space.
|
||||
"""
|
||||
if self.args.single_cls:
|
||||
pred["cls"] *= 0
|
||||
return pred
|
||||
|
||||
def update_metrics(self, preds: list[dict[str, torch.Tensor]], batch: dict[str, Any]) -> None:
|
||||
"""Update metrics with new predictions and ground truth.
|
||||
|
||||
Args:
|
||||
preds (list[dict[str, torch.Tensor]]): List of predictions from the model.
|
||||
batch (dict[str, Any]): Batch data containing ground truth.
|
||||
"""
|
||||
for si, pred in enumerate(preds):
|
||||
self.seen += 1
|
||||
pbatch = self._prepare_batch(si, batch)
|
||||
predn = self._prepare_pred(pred)
|
||||
|
||||
cls = pbatch["cls"].cpu().numpy()
|
||||
no_pred = predn["cls"].shape[0] == 0
|
||||
self.metrics.update_stats(
|
||||
{
|
||||
**self._process_batch(predn, pbatch),
|
||||
"target_cls": cls,
|
||||
"target_img": np.unique(cls),
|
||||
"conf": np.zeros(0) if no_pred else predn["conf"].cpu().numpy(),
|
||||
"pred_cls": np.zeros(0) if no_pred else predn["cls"].cpu().numpy(),
|
||||
}
|
||||
)
|
||||
# Evaluate
|
||||
if self.args.plots:
|
||||
self.confusion_matrix.process_batch(predn, pbatch, conf=self.args.conf)
|
||||
if self.args.visualize:
|
||||
self.confusion_matrix.plot_matches(batch["img"][si], pbatch["im_file"], self.save_dir)
|
||||
|
||||
if no_pred:
|
||||
continue
|
||||
|
||||
# Save
|
||||
if self.args.save_json or self.args.save_txt:
|
||||
predn_scaled = self.scale_preds(predn, pbatch)
|
||||
if self.args.save_json:
|
||||
self.pred_to_json(predn_scaled, pbatch)
|
||||
if self.args.save_txt:
|
||||
self.save_one_txt(
|
||||
predn_scaled,
|
||||
self.args.save_conf,
|
||||
pbatch["ori_shape"],
|
||||
self.save_dir / "labels" / f"{Path(pbatch['im_file']).stem}.txt",
|
||||
)
|
||||
|
||||
def finalize_metrics(self) -> None:
|
||||
"""Set final values for metrics speed and confusion matrix."""
|
||||
if self.args.plots:
|
||||
for normalize in True, False:
|
||||
self.confusion_matrix.plot(save_dir=self.save_dir, normalize=normalize, on_plot=self.on_plot)
|
||||
self.metrics.speed = self.speed
|
||||
self.metrics.confusion_matrix = self.confusion_matrix
|
||||
self.metrics.save_dir = self.save_dir
|
||||
|
||||
def gather_stats(self) -> None:
|
||||
"""Gather stats from all GPUs."""
|
||||
if getattr(self, "_single_rank_eval", False) or not (dist.is_available() and dist.is_initialized()):
|
||||
return
|
||||
if RANK == 0:
|
||||
gathered_stats = [None] * dist.get_world_size()
|
||||
dist.gather_object(self.metrics.stats, gathered_stats, dst=0)
|
||||
merged_stats = {key: [] for key in self.metrics.stats.keys()}
|
||||
for stats_dict in gathered_stats:
|
||||
for key in merged_stats:
|
||||
merged_stats[key].extend(stats_dict[key])
|
||||
gathered_jdict = [None] * dist.get_world_size()
|
||||
dist.gather_object(self.jdict, gathered_jdict, dst=0)
|
||||
self.jdict = []
|
||||
for jdict in gathered_jdict:
|
||||
self.jdict.extend(jdict)
|
||||
self.metrics.stats = merged_stats
|
||||
self.seen = len(self.dataloader.dataset) # total image count from dataset
|
||||
elif RANK > 0:
|
||||
dist.gather_object(self.metrics.stats, None, dst=0)
|
||||
dist.gather_object(self.jdict, None, dst=0)
|
||||
self.jdict = []
|
||||
self.metrics.clear_stats()
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
"""Calculate and return metrics statistics.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Dictionary containing metrics results.
|
||||
"""
|
||||
self.metrics.process(save_dir=self.save_dir, plot=self.args.plots, on_plot=self.on_plot)
|
||||
self.metrics.clear_stats()
|
||||
return self.metrics.results_dict
|
||||
|
||||
def print_results(self) -> None:
|
||||
"""Print training/validation set metrics per class."""
|
||||
pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys) # print format
|
||||
LOGGER.info(pf % ("all", self.seen, self.metrics.nt_per_class.sum(), *self.metrics.mean_results()))
|
||||
if self.metrics.nt_per_class.sum() == 0:
|
||||
LOGGER.warning(f"no labels found in {self.args.task} set, cannot compute metrics without labels")
|
||||
|
||||
# Print results per class
|
||||
if self.args.verbose and not self.training and self.nc > 1 and len(self.metrics.stats):
|
||||
for i, c in enumerate(self.metrics.ap_class_index):
|
||||
LOGGER.info(
|
||||
pf
|
||||
% (
|
||||
self.names[c],
|
||||
self.metrics.nt_per_image[c],
|
||||
self.metrics.nt_per_class[c],
|
||||
*self.metrics.class_result(i),
|
||||
)
|
||||
)
|
||||
|
||||
def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, Any]) -> dict[str, np.ndarray]:
|
||||
"""Return correct prediction matrix.
|
||||
|
||||
Args:
|
||||
preds (dict[str, torch.Tensor]): Dictionary containing prediction data with 'bboxes' and 'cls' keys.
|
||||
batch (dict[str, Any]): Batch dictionary containing ground truth data with 'bboxes' and 'cls' keys.
|
||||
|
||||
Returns:
|
||||
(dict[str, np.ndarray]): Dictionary containing 'tp' key with correct prediction matrix of shape (N, 10) for
|
||||
10 IoU levels.
|
||||
"""
|
||||
if batch["cls"].shape[0] == 0 or preds["cls"].shape[0] == 0:
|
||||
return {"tp": np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)}
|
||||
iou = box_iou(batch["bboxes"], preds["bboxes"])
|
||||
return {"tp": self.match_predictions(preds["cls"], batch["cls"], iou).cpu().numpy()}
|
||||
|
||||
def build_dataset(self, img_path: str, mode: str = "val", batch: int | None = None) -> torch.utils.data.Dataset:
|
||||
"""Build YOLO Dataset.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the folder containing images.
|
||||
mode (str): `train` mode or `val` mode, users are able to customize different augmentations for each mode.
|
||||
batch (int, optional): Size of batches, this is for `rect`.
|
||||
|
||||
Returns:
|
||||
(Dataset): YOLO dataset.
|
||||
"""
|
||||
return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, stride=self.stride)
|
||||
|
||||
def get_dataloader(self, dataset_path: str, batch_size: int) -> torch.utils.data.DataLoader:
|
||||
"""Construct and return dataloader.
|
||||
|
||||
Args:
|
||||
dataset_path (str): Path to the dataset.
|
||||
batch_size (int): Size of each batch.
|
||||
|
||||
Returns:
|
||||
(torch.utils.data.DataLoader): DataLoader for validation.
|
||||
"""
|
||||
dataset = self.build_dataset(dataset_path, batch=batch_size, mode="val")
|
||||
return build_dataloader(
|
||||
dataset,
|
||||
batch_size,
|
||||
self.args.workers,
|
||||
shuffle=False,
|
||||
rank=-1,
|
||||
drop_last=self.args.compile,
|
||||
pin_memory=self.training,
|
||||
)
|
||||
|
||||
def plot_val_samples(self, batch: dict[str, Any], ni: int) -> None:
|
||||
"""Plot validation image samples.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch containing images and annotations.
|
||||
ni (int): Batch index.
|
||||
"""
|
||||
plot_images(
|
||||
labels=batch,
|
||||
paths=batch["im_file"],
|
||||
fname=self.save_dir / f"val_batch{ni}_labels.jpg",
|
||||
names=self.names,
|
||||
on_plot=self.on_plot,
|
||||
)
|
||||
|
||||
def plot_predictions(
|
||||
self, batch: dict[str, Any], preds: list[dict[str, torch.Tensor]], ni: int, max_det: int | None = None
|
||||
) -> None:
|
||||
"""Plot predicted bounding boxes on input images and save the result.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch containing images and annotations.
|
||||
preds (list[dict[str, torch.Tensor]]): List of predictions from the model.
|
||||
ni (int): Batch index.
|
||||
max_det (int | None): Maximum number of detections to plot.
|
||||
"""
|
||||
if not preds:
|
||||
return
|
||||
for i, pred in enumerate(preds):
|
||||
pred["batch_idx"] = torch.ones_like(pred["conf"]) * i # add batch index to predictions
|
||||
keys = preds[0].keys()
|
||||
max_det = max_det or self.args.max_det
|
||||
batched_preds = {k: torch.cat([x[k][:max_det] for x in preds], dim=0) for k in keys}
|
||||
batched_preds["bboxes"] = ops.xyxy2xywh(batched_preds["bboxes"]) # convert to xywh format
|
||||
plot_images(
|
||||
images=batch["img"],
|
||||
labels=batched_preds,
|
||||
paths=batch["im_file"],
|
||||
fname=self.save_dir / f"val_batch{ni}_pred.jpg",
|
||||
names=self.names,
|
||||
on_plot=self.on_plot,
|
||||
) # pred
|
||||
|
||||
def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
|
||||
"""Save YOLO detections to a txt file in normalized coordinates in a specific format.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Dictionary containing predictions with keys 'bboxes', 'conf', and 'cls'.
|
||||
save_conf (bool): Whether to save confidence scores.
|
||||
shape (tuple[int, int]): Shape of the original image (height, width).
|
||||
file (Path): File path to save the detections.
|
||||
"""
|
||||
from ultralytics.engine.results import Results
|
||||
|
||||
Results(
|
||||
np.zeros((shape[0], shape[1]), dtype=np.uint8),
|
||||
path=None,
|
||||
names=self.names,
|
||||
boxes=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
|
||||
).save_txt(file, save_conf=save_conf)
|
||||
|
||||
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
|
||||
"""Serialize YOLO predictions to COCO json format.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Predictions dictionary containing 'bboxes', 'conf', and 'cls' keys with
|
||||
bounding box coordinates, confidence scores, and class predictions.
|
||||
pbatch (dict[str, Any]): Batch dictionary containing 'imgsz', 'ori_shape', 'ratio_pad', and 'im_file'.
|
||||
|
||||
Examples:
|
||||
>>> result = {
|
||||
... "image_id": 42,
|
||||
... "file_name": "42.jpg",
|
||||
... "category_id": 18,
|
||||
... "bbox": [258.15, 41.29, 348.26, 243.78],
|
||||
... "score": 0.236,
|
||||
... }
|
||||
"""
|
||||
path = Path(pbatch["im_file"])
|
||||
stem = path.stem
|
||||
image_id = int(stem) if stem.isnumeric() else stem
|
||||
box = ops.xyxy2xywh(predn["bboxes"]) # xywh
|
||||
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
||||
for b, s, c in zip(box.tolist(), predn["conf"].tolist(), predn["cls"].tolist()):
|
||||
self.jdict.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"file_name": path.name,
|
||||
"category_id": self.class_map[int(c)],
|
||||
"bbox": [round(x, 3) for x in b],
|
||||
"score": round(s, 5),
|
||||
}
|
||||
)
|
||||
|
||||
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Scales predictions to the original image size."""
|
||||
return {
|
||||
**predn,
|
||||
"bboxes": ops.scale_boxes(
|
||||
pbatch["imgsz"],
|
||||
predn["bboxes"].clone(),
|
||||
pbatch["ori_shape"],
|
||||
ratio_pad=pbatch["ratio_pad"],
|
||||
),
|
||||
}
|
||||
|
||||
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Evaluate YOLO output in JSON format and return performance statistics.
|
||||
|
||||
Args:
|
||||
stats (dict[str, Any]): Current statistics dictionary.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Updated statistics dictionary with COCO/LVIS evaluation results.
|
||||
"""
|
||||
pred_json = self.save_dir / "predictions.json" # predictions
|
||||
anno_json = (
|
||||
self.data["path"]
|
||||
/ "annotations"
|
||||
/ ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
|
||||
) # annotations
|
||||
return self.coco_evaluate(stats, pred_json, anno_json)
|
||||
|
||||
def coco_evaluate(
|
||||
self,
|
||||
stats: dict[str, Any],
|
||||
pred_json: str,
|
||||
anno_json: str,
|
||||
iou_types: str | list[str] = "bbox",
|
||||
suffix: str | list[str] = "Box",
|
||||
) -> dict[str, Any]:
|
||||
"""Evaluate COCO/LVIS metrics using faster-coco-eval library.
|
||||
|
||||
Performs evaluation using the faster-coco-eval library to compute mAP metrics for object detection. Updates the
|
||||
provided stats dictionary with computed metrics including mAP50, mAP50-95, and LVIS-specific metrics if
|
||||
applicable.
|
||||
|
||||
Args:
|
||||
stats (dict[str, Any]): Dictionary to store computed metrics and statistics.
|
||||
pred_json (str | Path): Path to JSON file containing predictions in COCO format.
|
||||
anno_json (str | Path): Path to JSON file containing ground truth annotations in COCO format.
|
||||
iou_types (str | list[str]): IoU type(s) for evaluation. Can be single string or list of strings. Common
|
||||
values include "bbox", "segm", "keypoints". Defaults to "bbox".
|
||||
suffix (str | list[str]): Suffix to append to metric names in stats dictionary. Should correspond to
|
||||
iou_types if multiple types provided. Defaults to "Box".
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Updated stats dictionary containing the computed COCO/LVIS evaluation metrics.
|
||||
"""
|
||||
if self.args.save_json and (self.is_coco or self.is_lvis) and len(self.jdict):
|
||||
LOGGER.info(f"\nEvaluating faster-coco-eval mAP using {pred_json} and {anno_json}...")
|
||||
try:
|
||||
for x in pred_json, anno_json:
|
||||
assert x.is_file(), f"{x} file not found"
|
||||
iou_types = [iou_types] if isinstance(iou_types, str) else iou_types
|
||||
suffix = [suffix] if isinstance(suffix, str) else suffix
|
||||
check_requirements("faster-coco-eval>=1.6.7")
|
||||
from faster_coco_eval import COCO, COCOeval_faster
|
||||
|
||||
anno = COCO(anno_json)
|
||||
pred = anno.loadRes(pred_json)
|
||||
for i, iou_type in enumerate(iou_types):
|
||||
val = COCOeval_faster(
|
||||
anno, pred, iouType=iou_type, lvis_style=self.is_lvis, print_function=LOGGER.info
|
||||
)
|
||||
val.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files] # images to eval
|
||||
val.evaluate()
|
||||
val.accumulate()
|
||||
val.summarize()
|
||||
|
||||
# update mAP50-95 and mAP50
|
||||
stats[f"metrics/mAP50({suffix[i][0]})"] = val.stats_as_dict["AP_50"]
|
||||
stats[f"metrics/mAP50-95({suffix[i][0]})"] = val.stats_as_dict["AP_all"]
|
||||
# record mAP for small, medium, large objects as well
|
||||
stats["metrics/mAP_small(B)"] = val.stats_as_dict["AP_small"]
|
||||
stats["metrics/mAP_medium(B)"] = val.stats_as_dict["AP_medium"]
|
||||
stats["metrics/mAP_large(B)"] = val.stats_as_dict["AP_large"]
|
||||
# update fitness
|
||||
stats["fitness"] = 0.9 * val.stats_as_dict["AP_all"] + 0.1 * val.stats_as_dict["AP_50"]
|
||||
|
||||
if self.is_lvis:
|
||||
stats[f"metrics/APr({suffix[i][0]})"] = val.stats_as_dict["APr"]
|
||||
stats[f"metrics/APc({suffix[i][0]})"] = val.stats_as_dict["APc"]
|
||||
stats[f"metrics/APf({suffix[i][0]})"] = val.stats_as_dict["APf"]
|
||||
|
||||
if self.is_lvis:
|
||||
stats["fitness"] = stats["metrics/mAP50-95(B)"] # always use box mAP50-95 for fitness
|
||||
except Exception as e:
|
||||
LOGGER.warning(f"faster-coco-eval unable to run: {e}")
|
||||
return stats
|
||||
431
ultralytics/models/yolo/model.py
Executable file
431
ultralytics/models/yolo/model.py
Executable file
@@ -0,0 +1,431 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data.build import load_inference_source
|
||||
from ultralytics.engine.model import Model
|
||||
from ultralytics.models import yolo
|
||||
from ultralytics.nn.tasks import (
|
||||
ClassificationModel,
|
||||
DetectionModel,
|
||||
OBBModel,
|
||||
PoseModel,
|
||||
SegmentationModel,
|
||||
WorldModel,
|
||||
YOLOEModel,
|
||||
YOLOESegModel,
|
||||
)
|
||||
from ultralytics.utils import ROOT, YAML
|
||||
|
||||
|
||||
class YOLO(Model):
|
||||
"""YOLO (You Only Look Once) object detection model.
|
||||
|
||||
This class provides a unified interface for YOLO models, automatically switching to specialized model types
|
||||
(YOLOWorld or YOLOE) based on the model filename. It supports various computer vision tasks including object
|
||||
detection, segmentation, classification, pose estimation, and oriented bounding box detection.
|
||||
|
||||
Attributes:
|
||||
model: The loaded YOLO model instance.
|
||||
task: The task type (detect, segment, classify, pose, obb).
|
||||
overrides: Configuration overrides for the model.
|
||||
|
||||
Methods:
|
||||
__init__: Initialize a YOLO model with automatic type detection.
|
||||
task_map: Map tasks to their corresponding model, trainer, validator, and predictor classes.
|
||||
|
||||
Examples:
|
||||
Load a pretrained YOLO26n detection model
|
||||
>>> model = YOLO("yolo26n.pt")
|
||||
|
||||
Load a pretrained YOLO26n segmentation model
|
||||
>>> model = YOLO("yolo26n-seg.pt")
|
||||
|
||||
Initialize from a YAML configuration
|
||||
>>> model = YOLO("yolo26n.yaml")
|
||||
"""
|
||||
|
||||
def __init__(self, model: str | Path = "yolo26n.pt", task: str | None = None, verbose: bool = False):
|
||||
"""Initialize a YOLO model.
|
||||
|
||||
This constructor initializes a YOLO model, automatically switching to specialized model types (YOLOWorld or
|
||||
YOLOE) based on the model filename.
|
||||
|
||||
Args:
|
||||
model (str | Path): Model name or path to model file, i.e. 'yolo26n.pt', 'yolo26n.yaml'.
|
||||
task (str, optional): YOLO task specification, i.e. 'detect', 'segment', 'classify', 'pose', 'obb'. Defaults
|
||||
to auto-detection based on model.
|
||||
verbose (bool): Display model info on load.
|
||||
"""
|
||||
path = Path(model if isinstance(model, (str, Path)) else "")
|
||||
if "-world" in path.stem and path.suffix in {".pt", ".yaml", ".yml"}: # if YOLOWorld PyTorch model
|
||||
new_instance = YOLOWorld(path, verbose=verbose)
|
||||
self.__class__ = type(new_instance)
|
||||
self.__dict__ = new_instance.__dict__
|
||||
elif "yoloe" in path.stem and path.suffix in {".pt", ".yaml", ".yml"}: # if YOLOE PyTorch model
|
||||
new_instance = YOLOE(path, task=task, verbose=verbose)
|
||||
self.__class__ = type(new_instance)
|
||||
self.__dict__ = new_instance.__dict__
|
||||
else:
|
||||
# Continue with default YOLO initialization
|
||||
super().__init__(model=model, task=task, verbose=verbose)
|
||||
if hasattr(self.model, "model") and "RTDETR" in self.model.model[-1]._get_name(): # if RTDETR head
|
||||
from ultralytics import RTDETR
|
||||
|
||||
new_instance = RTDETR(self)
|
||||
self.__class__ = type(new_instance)
|
||||
self.__dict__ = new_instance.__dict__
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Map head to model, trainer, validator, and predictor classes."""
|
||||
return {
|
||||
"classify": {
|
||||
"model": ClassificationModel,
|
||||
"trainer": yolo.classify.ClassificationTrainer,
|
||||
"validator": yolo.classify.ClassificationValidator,
|
||||
"predictor": yolo.classify.ClassificationPredictor,
|
||||
},
|
||||
"detect": {
|
||||
"model": DetectionModel,
|
||||
"trainer": yolo.detect.DetectionTrainer,
|
||||
"validator": yolo.detect.DetectionValidator,
|
||||
"predictor": yolo.detect.DetectionPredictor,
|
||||
},
|
||||
"segment": {
|
||||
"model": SegmentationModel,
|
||||
"trainer": yolo.segment.SegmentationTrainer,
|
||||
"validator": yolo.segment.SegmentationValidator,
|
||||
"predictor": yolo.segment.SegmentationPredictor,
|
||||
},
|
||||
"pose": {
|
||||
"model": PoseModel,
|
||||
"trainer": yolo.pose.PoseTrainer,
|
||||
"validator": yolo.pose.PoseValidator,
|
||||
"predictor": yolo.pose.PosePredictor,
|
||||
},
|
||||
"obb": {
|
||||
"model": OBBModel,
|
||||
"trainer": yolo.obb.OBBTrainer,
|
||||
"validator": yolo.obb.OBBValidator,
|
||||
"predictor": yolo.obb.OBBPredictor,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class YOLOWorld(Model):
|
||||
"""YOLO-World object detection model.
|
||||
|
||||
YOLO-World is an open-vocabulary object detection model that can detect objects based on text descriptions without
|
||||
requiring training on specific classes. It extends the YOLO architecture to support real-time open-vocabulary
|
||||
detection.
|
||||
|
||||
Attributes:
|
||||
model: The loaded YOLO-World model instance.
|
||||
task: Always set to 'detect' for object detection.
|
||||
overrides: Configuration overrides for the model.
|
||||
|
||||
Methods:
|
||||
__init__: Initialize YOLOv8-World model with a pre-trained model file.
|
||||
task_map: Map tasks to their corresponding model, trainer, validator, and predictor classes.
|
||||
set_classes: Set the model's class names for detection.
|
||||
|
||||
Examples:
|
||||
Load a YOLOv8-World model
|
||||
>>> model = YOLOWorld("yolov8s-world.pt")
|
||||
|
||||
Set custom classes for detection
|
||||
>>> model.set_classes(["person", "car", "bicycle"])
|
||||
"""
|
||||
|
||||
def __init__(self, model: str | Path = "yolov8s-world.pt", verbose: bool = False) -> None:
|
||||
"""Initialize YOLOv8-World model with a pre-trained model file.
|
||||
|
||||
Loads a YOLOv8-World model for object detection. If no custom class names are provided, it assigns default COCO
|
||||
class names.
|
||||
|
||||
Args:
|
||||
model (str | Path): Path to the pre-trained model file. Supports *.pt and *.yaml formats.
|
||||
verbose (bool): If True, prints additional information during initialization.
|
||||
"""
|
||||
super().__init__(model=model, task="detect", verbose=verbose)
|
||||
|
||||
# Assign default COCO class names when there are no custom names
|
||||
if not hasattr(self.model, "names"):
|
||||
self.model.names = YAML.load(ROOT / "cfg/datasets/coco8.yaml").get("names")
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Map head to model, trainer, validator, and predictor classes."""
|
||||
return {
|
||||
"detect": {
|
||||
"model": WorldModel,
|
||||
"validator": yolo.detect.DetectionValidator,
|
||||
"predictor": yolo.detect.DetectionPredictor,
|
||||
"trainer": yolo.world.WorldTrainer,
|
||||
}
|
||||
}
|
||||
|
||||
def set_classes(self, classes: list[str]) -> None:
|
||||
"""Set the model's class names for detection.
|
||||
|
||||
Args:
|
||||
classes (list[str]): A list of categories i.e. ["person"].
|
||||
"""
|
||||
self.model.set_classes(classes)
|
||||
# Remove background if it's given
|
||||
background = " "
|
||||
if background in classes:
|
||||
classes.remove(background)
|
||||
self.model.names = classes
|
||||
|
||||
# Reset method class names
|
||||
if self.predictor:
|
||||
self.predictor.model.names = classes
|
||||
|
||||
|
||||
class YOLOE(Model):
|
||||
"""YOLOE object detection and segmentation model.
|
||||
|
||||
YOLOE is an enhanced YOLO model that supports both object detection and instance segmentation tasks with improved
|
||||
performance and additional features like visual and text positional embeddings.
|
||||
|
||||
Attributes:
|
||||
model: The loaded YOLOE model instance.
|
||||
task: The task type (detect or segment).
|
||||
overrides: Configuration overrides for the model.
|
||||
|
||||
Methods:
|
||||
__init__: Initialize YOLOE model with a pre-trained model file.
|
||||
task_map: Map tasks to their corresponding model, trainer, validator, and predictor classes.
|
||||
get_text_pe: Get text positional embeddings for the given texts.
|
||||
get_visual_pe: Get visual positional embeddings for the given image and visual features.
|
||||
set_vocab: Set vocabulary and class names for the YOLOE model.
|
||||
get_vocab: Get vocabulary for the given class names.
|
||||
set_classes: Set the model's class names and embeddings for detection.
|
||||
val: Validate the model using text or visual prompts.
|
||||
predict: Run prediction on images, videos, directories, streams, etc.
|
||||
|
||||
Examples:
|
||||
Load a YOLOE detection model
|
||||
>>> model = YOLOE("yoloe-11s-seg.pt")
|
||||
|
||||
Set vocabulary and class names
|
||||
>>> model.set_vocab(["person", "car", "dog"], ["person", "car", "dog"])
|
||||
|
||||
Predict with visual prompts
|
||||
>>> prompts = {"bboxes": [[10, 20, 100, 200]], "cls": ["person"]}
|
||||
>>> results = model.predict("image.jpg", visual_prompts=prompts)
|
||||
"""
|
||||
|
||||
def __init__(self, model: str | Path = "yoloe-11s-seg.pt", task: str | None = None, verbose: bool = False) -> None:
|
||||
"""Initialize YOLOE model with a pre-trained model file.
|
||||
|
||||
Args:
|
||||
model (str | Path): Path to the pre-trained model file. Supports *.pt and *.yaml formats.
|
||||
task (str, optional): Task type for the model. Auto-detected if None.
|
||||
verbose (bool): If True, prints additional information during initialization.
|
||||
"""
|
||||
super().__init__(model=model, task=task, verbose=verbose)
|
||||
|
||||
@property
|
||||
def task_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Map head to model, trainer, validator, and predictor classes."""
|
||||
return {
|
||||
"detect": {
|
||||
"model": YOLOEModel,
|
||||
"validator": yolo.yoloe.YOLOEDetectValidator,
|
||||
"predictor": yolo.detect.DetectionPredictor,
|
||||
"trainer": yolo.yoloe.YOLOETrainer,
|
||||
},
|
||||
"segment": {
|
||||
"model": YOLOESegModel,
|
||||
"validator": yolo.yoloe.YOLOESegValidator,
|
||||
"predictor": yolo.segment.SegmentationPredictor,
|
||||
"trainer": yolo.yoloe.YOLOESegTrainer,
|
||||
},
|
||||
}
|
||||
|
||||
def get_text_pe(self, texts):
|
||||
"""Get text positional embeddings for the given texts."""
|
||||
assert isinstance(self.model, YOLOEModel)
|
||||
return self.model.get_text_pe(texts)
|
||||
|
||||
def get_visual_pe(self, img, visual):
|
||||
"""Get visual positional embeddings for the given image and visual features.
|
||||
|
||||
This method extracts positional embeddings from visual features based on the input image. It requires that the
|
||||
model is an instance of YOLOEModel.
|
||||
|
||||
Args:
|
||||
img (torch.Tensor): Input image tensor.
|
||||
visual (torch.Tensor): Visual features extracted from the image.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Visual positional embeddings.
|
||||
|
||||
Examples:
|
||||
>>> model = YOLOE("yoloe-11s-seg.pt")
|
||||
>>> img = torch.rand(1, 3, 640, 640)
|
||||
>>> visual_features = torch.rand(1, 1, 80, 80)
|
||||
>>> pe = model.get_visual_pe(img, visual_features)
|
||||
"""
|
||||
assert isinstance(self.model, YOLOEModel)
|
||||
return self.model.get_visual_pe(img, visual)
|
||||
|
||||
def set_vocab(self, vocab: list[str], names: list[str]) -> None:
|
||||
"""Set vocabulary and class names for the YOLOE model.
|
||||
|
||||
This method configures the vocabulary and class names used by the model for text processing and classification
|
||||
tasks. The model must be an instance of YOLOEModel.
|
||||
|
||||
Args:
|
||||
vocab (list[str]): Vocabulary list containing tokens or words used by the model for text processing.
|
||||
names (list[str]): List of class names that the model can detect or classify.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the model is not an instance of YOLOEModel.
|
||||
|
||||
Examples:
|
||||
>>> model = YOLOE("yoloe-11s-seg.pt")
|
||||
>>> model.set_vocab(["person", "car", "dog"], ["person", "car", "dog"])
|
||||
"""
|
||||
assert isinstance(self.model, YOLOEModel)
|
||||
self.model.set_vocab(vocab, names=names)
|
||||
|
||||
def get_vocab(self, names):
|
||||
"""Get vocabulary for the given class names."""
|
||||
assert isinstance(self.model, YOLOEModel)
|
||||
return self.model.get_vocab(names)
|
||||
|
||||
def set_classes(self, classes: list[str], embeddings: torch.Tensor | None = None) -> None:
|
||||
"""Set the model's class names and embeddings for detection.
|
||||
|
||||
Args:
|
||||
classes (list[str]): A list of categories i.e. ["person"].
|
||||
embeddings (torch.Tensor, optional): Embeddings corresponding to the classes.
|
||||
"""
|
||||
# Verify no background class is present
|
||||
assert " " not in classes
|
||||
assert isinstance(self.model, YOLOEModel)
|
||||
if sorted(list(self.model.names.values())) != sorted(classes):
|
||||
if embeddings is None:
|
||||
embeddings = self.get_text_pe(classes) # generate text embeddings if not provided
|
||||
self.model.set_classes(classes, embeddings)
|
||||
|
||||
# Reset method class names
|
||||
if self.predictor:
|
||||
self.predictor.model.names = self.model.names
|
||||
|
||||
def val(
|
||||
self,
|
||||
validator=None,
|
||||
load_vp: bool = False,
|
||||
refer_data: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Validate the model using text or visual prompts.
|
||||
|
||||
Args:
|
||||
validator (callable, optional): A callable validator function. If None, a default validator is loaded.
|
||||
load_vp (bool): Whether to load visual prompts. If False, text prompts are used.
|
||||
refer_data (str, optional): Path to the reference data for visual prompts.
|
||||
**kwargs (Any): Additional keyword arguments to override default settings.
|
||||
|
||||
Returns:
|
||||
(dict): Validation statistics containing metrics computed during validation.
|
||||
"""
|
||||
custom = {"rect": not load_vp} # method defaults
|
||||
args = {**self.overrides, **custom, **kwargs, "mode": "val"} # highest priority args on the right
|
||||
|
||||
validator = (validator or self._smart_load("validator"))(args=args, _callbacks=self.callbacks)
|
||||
validator(model=self.model, load_vp=load_vp, refer_data=refer_data)
|
||||
self.metrics = validator.metrics
|
||||
return validator.metrics
|
||||
|
||||
def predict(
|
||||
self,
|
||||
source=None,
|
||||
stream: bool = False,
|
||||
visual_prompts: dict[str, list] = {},
|
||||
refer_image=None,
|
||||
predictor=yolo.yoloe.YOLOEVPDetectPredictor,
|
||||
**kwargs,
|
||||
):
|
||||
"""Run prediction on images, videos, directories, streams, etc.
|
||||
|
||||
Args:
|
||||
source (str | int | PIL.Image | np.ndarray, optional): Source for prediction. Accepts image paths, directory
|
||||
paths, URL/YouTube streams, PIL images, numpy arrays, or webcam indices.
|
||||
stream (bool): Whether to stream the prediction results. If True, results are yielded as a generator as they
|
||||
are computed.
|
||||
visual_prompts (dict[str, list]): Dictionary containing visual prompts for the model. Must include 'bboxes'
|
||||
and 'cls' keys when non-empty.
|
||||
refer_image (str | PIL.Image | np.ndarray, optional): Reference image for visual prompts.
|
||||
predictor (callable): Custom predictor class for visual prompt predictions. Defaults to
|
||||
YOLOEVPDetectPredictor.
|
||||
**kwargs (Any): Additional keyword arguments passed to the predictor.
|
||||
|
||||
Returns:
|
||||
(list | generator): List of Results objects or generator of Results objects if stream=True.
|
||||
|
||||
Examples:
|
||||
>>> model = YOLOE("yoloe-11s-seg.pt")
|
||||
>>> results = model.predict("path/to/image.jpg")
|
||||
>>> # With visual prompts
|
||||
>>> prompts = {"bboxes": [[10, 20, 100, 200]], "cls": ["person"]}
|
||||
>>> results = model.predict("path/to/image.jpg", visual_prompts=prompts)
|
||||
"""
|
||||
if len(visual_prompts):
|
||||
assert "bboxes" in visual_prompts and "cls" in visual_prompts, (
|
||||
f"Expected 'bboxes' and 'cls' in visual prompts, but got {visual_prompts.keys()}"
|
||||
)
|
||||
assert len(visual_prompts["bboxes"]) == len(visual_prompts["cls"]), (
|
||||
f"Expected equal number of bounding boxes and classes, but got {len(visual_prompts['bboxes'])} and "
|
||||
f"{len(visual_prompts['cls'])} respectively"
|
||||
)
|
||||
if type(self.predictor) is not predictor:
|
||||
self.predictor = predictor(
|
||||
overrides={
|
||||
"task": self.model.task,
|
||||
"mode": "predict",
|
||||
"save": False,
|
||||
"verbose": refer_image is None,
|
||||
"batch": 1,
|
||||
"device": kwargs.get("device", None),
|
||||
"half": kwargs.get("half", False),
|
||||
"imgsz": kwargs.get("imgsz", self.overrides.get("imgsz", 640)),
|
||||
},
|
||||
_callbacks=self.callbacks,
|
||||
)
|
||||
|
||||
num_cls = (
|
||||
max(len(set(c)) for c in visual_prompts["cls"])
|
||||
if isinstance(source, list) and refer_image is None # means multiple images
|
||||
else len(set(visual_prompts["cls"]))
|
||||
)
|
||||
self.model.model[-1].nc = num_cls
|
||||
self.model.names = [f"object{i}" for i in range(num_cls)]
|
||||
self.predictor.set_prompts(visual_prompts.copy())
|
||||
self.predictor.setup_model(model=self.model)
|
||||
|
||||
if refer_image is None and source is not None:
|
||||
dataset = load_inference_source(source)
|
||||
if dataset.mode in {"video", "stream"}:
|
||||
# NOTE: set the first frame as refer image for videos/streams inference
|
||||
refer_image = next(iter(dataset))[1][0]
|
||||
if refer_image is not None:
|
||||
vpe = self.predictor.get_vpe(refer_image)
|
||||
self.model.set_classes(self.model.names, vpe)
|
||||
self.task = "segment" if isinstance(self.predictor, yolo.segment.SegmentationPredictor) else "detect"
|
||||
self.predictor = None # reset predictor
|
||||
elif isinstance(self.predictor, yolo.yoloe.YOLOEVPDetectPredictor):
|
||||
self.predictor = None # reset predictor if no visual prompts
|
||||
self.overrides["agnostic_nms"] = True # use agnostic nms for YOLOE default
|
||||
|
||||
return super().predict(source, stream, **kwargs)
|
||||
7
ultralytics/models/yolo/obb/__init__.py
Executable file
7
ultralytics/models/yolo/obb/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .predict import OBBPredictor
|
||||
from .train import OBBTrainer
|
||||
from .val import OBBValidator
|
||||
|
||||
__all__ = "OBBPredictor", "OBBTrainer", "OBBValidator"
|
||||
56
ultralytics/models/yolo/obb/predict.py
Executable file
56
ultralytics/models/yolo/obb/predict.py
Executable file
@@ -0,0 +1,56 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.engine.results import Results
|
||||
from ultralytics.models.yolo.detect.predict import DetectionPredictor
|
||||
from ultralytics.utils import DEFAULT_CFG, ops
|
||||
|
||||
|
||||
class OBBPredictor(DetectionPredictor):
|
||||
"""A class extending the DetectionPredictor class for prediction based on an Oriented Bounding Box (OBB) model.
|
||||
|
||||
This predictor handles oriented bounding box detection tasks, processing images and returning results with rotated
|
||||
bounding boxes.
|
||||
|
||||
Attributes:
|
||||
args (namespace): Configuration arguments for the predictor.
|
||||
model (torch.nn.Module): The loaded YOLO OBB model.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.yolo.obb import OBBPredictor
|
||||
>>> args = dict(model="yolo26n-obb.pt", source=ASSETS)
|
||||
>>> predictor = OBBPredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize OBBPredictor with optional model and data configuration overrides.
|
||||
|
||||
Args:
|
||||
cfg (dict, optional): Default configuration for the predictor.
|
||||
overrides (dict, optional): Configuration overrides that take precedence over the default config.
|
||||
_callbacks (list, optional): List of callback functions to be invoked during prediction.
|
||||
"""
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.args.task = "obb"
|
||||
|
||||
def construct_result(self, pred, img, orig_img, img_path):
|
||||
"""Construct the result object from the prediction.
|
||||
|
||||
Args:
|
||||
pred (torch.Tensor): The predicted bounding boxes, scores, and rotation angles with shape (N, 7) where the
|
||||
last dimension contains [x, y, w, h, confidence, class_id, angle].
|
||||
img (torch.Tensor): The image after preprocessing with shape (B, C, H, W).
|
||||
orig_img (np.ndarray): The original image before preprocessing.
|
||||
img_path (str): The path to the original image.
|
||||
|
||||
Returns:
|
||||
(Results): The result object containing the original image, image path, class names, and oriented bounding
|
||||
boxes.
|
||||
"""
|
||||
rboxes = torch.cat([pred[:, :4], pred[:, -1:]], dim=-1)
|
||||
rboxes[:, :4] = ops.scale_boxes(img.shape[2:], rboxes[:, :4], orig_img.shape, xywh=True)
|
||||
obb = torch.cat([rboxes, pred[:, 4:6]], dim=-1)
|
||||
return Results(orig_img, path=img_path, names=self.model.names, obb=obb)
|
||||
79
ultralytics/models/yolo/obb/train.py
Executable file
79
ultralytics/models/yolo/obb/train.py
Executable file
@@ -0,0 +1,79 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ultralytics.models import yolo
|
||||
from ultralytics.nn.tasks import OBBModel
|
||||
from ultralytics.utils import DEFAULT_CFG, RANK
|
||||
|
||||
|
||||
class OBBTrainer(yolo.detect.DetectionTrainer):
|
||||
"""A class extending the DetectionTrainer class for training based on an Oriented Bounding Box (OBB) model.
|
||||
|
||||
This trainer specializes in training YOLO models that detect oriented bounding boxes, which are useful for detecting
|
||||
objects at arbitrary angles rather than just axis-aligned rectangles.
|
||||
|
||||
Attributes:
|
||||
loss_names (tuple): Names of the loss components used during training including box_loss, cls_loss, dfl_loss,
|
||||
and angle_loss.
|
||||
|
||||
Methods:
|
||||
get_model: Return OBBModel initialized with specified config and weights.
|
||||
get_validator: Return an instance of OBBValidator for validation of YOLO model.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.obb import OBBTrainer
|
||||
>>> args = dict(model="yolo26n-obb.pt", data="dota8.yaml", epochs=3)
|
||||
>>> trainer = OBBTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict | None = None, _callbacks: list[Any] | None = None):
|
||||
"""Initialize an OBBTrainer object for training Oriented Bounding Box (OBB) models.
|
||||
|
||||
Args:
|
||||
cfg (dict, optional): Configuration dictionary for the trainer. Contains training parameters and model
|
||||
configuration.
|
||||
overrides (dict, optional): Dictionary of parameter overrides for the configuration. Any values here will
|
||||
take precedence over those in cfg.
|
||||
_callbacks (list[Any], optional): List of callback functions to be invoked during training.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
overrides["task"] = "obb"
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def get_model(
|
||||
self, cfg: str | dict | None = None, weights: str | Path | None = None, verbose: bool = True
|
||||
) -> OBBModel:
|
||||
"""Return OBBModel initialized with specified config and weights.
|
||||
|
||||
Args:
|
||||
cfg (str | dict, optional): Model configuration. Can be a path to a YAML config file, a dictionary
|
||||
containing configuration parameters, or None to use default configuration.
|
||||
weights (str | Path, optional): Path to pretrained weights file. If None, random initialization is used.
|
||||
verbose (bool): Whether to display model information during initialization.
|
||||
|
||||
Returns:
|
||||
(OBBModel): Initialized OBBModel with the specified configuration and weights.
|
||||
|
||||
Examples:
|
||||
>>> trainer = OBBTrainer()
|
||||
>>> model = trainer.get_model(cfg="yolo26n-obb.yaml", weights="yolo26n-obb.pt")
|
||||
"""
|
||||
model = OBBModel(cfg, nc=self.data["nc"], ch=self.data["channels"], verbose=verbose and RANK == -1)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
return model
|
||||
|
||||
def get_validator(self):
|
||||
"""Return an instance of OBBValidator for validation of YOLO model."""
|
||||
self.loss_names = "box_loss", "cls_loss", "dfl_loss", "angle_loss"
|
||||
return yolo.obb.OBBValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
305
ultralytics/models/yolo/obb/val.py
Executable file
305
ultralytics/models/yolo/obb/val.py
Executable file
@@ -0,0 +1,305 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.utils import LOGGER, ops
|
||||
from ultralytics.utils.metrics import OBBMetrics, batch_probiou
|
||||
from ultralytics.utils.nms import TorchNMS
|
||||
from ultralytics.utils.plotting import plot_images
|
||||
|
||||
|
||||
class OBBValidator(DetectionValidator):
|
||||
"""A class extending the DetectionValidator class for validation based on an Oriented Bounding Box (OBB) model.
|
||||
|
||||
This validator specializes in evaluating models that predict rotated bounding boxes, commonly used for aerial and
|
||||
satellite imagery where objects can appear at various orientations.
|
||||
|
||||
Attributes:
|
||||
args (dict): Configuration arguments for the validator.
|
||||
metrics (OBBMetrics): Metrics object for evaluating OBB model performance.
|
||||
is_dota (bool): Flag indicating whether the validation dataset is in DOTA format.
|
||||
|
||||
Methods:
|
||||
init_metrics: Initialize evaluation metrics for YOLO.
|
||||
_process_batch: Process batch of detections and ground truth boxes to compute IoU matrix.
|
||||
_prepare_batch: Prepare batch data for OBB validation.
|
||||
_prepare_pred: Prepare predictions for evaluation against ground truth.
|
||||
plot_predictions: Plot predicted bounding boxes on input images.
|
||||
pred_to_json: Serialize YOLO predictions to COCO json format.
|
||||
save_one_txt: Save YOLO detections to a txt file in normalized coordinates.
|
||||
eval_json: Evaluate YOLO output in JSON format and return performance statistics.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.obb import OBBValidator
|
||||
>>> args = dict(model="yolo26n-obb.pt", data="dota8.yaml")
|
||||
>>> validator = OBBValidator(args=args)
|
||||
>>> validator(model=args["model"])
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
|
||||
"""Initialize OBBValidator and set task to 'obb', metrics to OBBMetrics.
|
||||
|
||||
This constructor initializes an OBBValidator instance for validating Oriented Bounding Box (OBB) models. It
|
||||
extends the DetectionValidator class and configures it specifically for the OBB task.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to be used for validation.
|
||||
save_dir (str | Path, optional): Directory to save results.
|
||||
args (dict, optional): Arguments containing validation parameters.
|
||||
_callbacks (list, optional): List of callback functions to be called during validation.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.args.task = "obb"
|
||||
self.metrics = OBBMetrics()
|
||||
|
||||
def init_metrics(self, model: torch.nn.Module) -> None:
|
||||
"""Initialize evaluation metrics for YOLO obb validation.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Model to validate.
|
||||
"""
|
||||
super().init_metrics(model)
|
||||
val = self.data.get(self.args.split, "") # validation path
|
||||
self.is_dota = isinstance(val, str) and "DOTA" in val # check if dataset is DOTA format
|
||||
self.confusion_matrix.task = "obb" # set confusion matrix task to 'obb'
|
||||
|
||||
def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, torch.Tensor]) -> dict[str, np.ndarray]:
|
||||
"""Compute the correct prediction matrix for a batch of detections and ground truth bounding boxes.
|
||||
|
||||
Args:
|
||||
preds (dict[str, torch.Tensor]): Prediction dictionary containing 'cls' and 'bboxes' keys with detected
|
||||
class labels and bounding boxes.
|
||||
batch (dict[str, torch.Tensor]): Batch dictionary containing 'cls' and 'bboxes' keys with ground truth class
|
||||
labels and bounding boxes.
|
||||
|
||||
Returns:
|
||||
(dict[str, np.ndarray]): Dictionary containing 'tp' key with the correct prediction matrix as a numpy array
|
||||
with shape (N, 10), which includes 10 IoU levels for each detection, indicating the accuracy of
|
||||
predictions compared to the ground truth.
|
||||
|
||||
Examples:
|
||||
>>> preds = {"cls": torch.randint(0, 5, (100,)), "bboxes": torch.rand(100, 5)}
|
||||
>>> batch = {"cls": torch.randint(0, 5, (50,)), "bboxes": torch.rand(50, 5)}
|
||||
>>> correct_matrix = validator._process_batch(preds, batch)
|
||||
"""
|
||||
if batch["cls"].shape[0] == 0 or preds["cls"].shape[0] == 0:
|
||||
return {"tp": np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)}
|
||||
iou = batch_probiou(batch["bboxes"], preds["bboxes"])
|
||||
return {"tp": self.match_predictions(preds["cls"], batch["cls"], iou).cpu().numpy()}
|
||||
|
||||
def postprocess(self, preds: torch.Tensor) -> list[dict[str, torch.Tensor]]:
|
||||
"""Postprocess OBB predictions.
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Raw predictions from the model.
|
||||
|
||||
Returns:
|
||||
(list[dict[str, torch.Tensor]]): Processed predictions with angle information concatenated to bboxes.
|
||||
"""
|
||||
preds = super().postprocess(preds)
|
||||
for pred in preds:
|
||||
pred["bboxes"] = torch.cat([pred["bboxes"], pred.pop("extra")], dim=-1) # concatenate angle
|
||||
return preds
|
||||
|
||||
def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare batch data for OBB validation with proper scaling and formatting.
|
||||
|
||||
Args:
|
||||
si (int): Sample index within the batch.
|
||||
batch (dict[str, Any]): Dictionary containing batch data with keys:
|
||||
- batch_idx: Tensor of batch indices
|
||||
- cls: Tensor of class labels
|
||||
- bboxes: Tensor of bounding boxes
|
||||
- ori_shape: Original image shapes
|
||||
- img: Batch of images
|
||||
- ratio_pad: Ratio and padding information
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Prepared batch data with scaled bounding boxes and metadata.
|
||||
"""
|
||||
idx = batch["batch_idx"] == si
|
||||
cls = batch["cls"][idx].squeeze(-1)
|
||||
bbox = batch["bboxes"][idx]
|
||||
ori_shape = batch["ori_shape"][si]
|
||||
imgsz = batch["img"].shape[2:]
|
||||
ratio_pad = batch["ratio_pad"][si]
|
||||
if cls.shape[0]:
|
||||
bbox[..., :4].mul_(torch.tensor(imgsz, device=self.device)[[1, 0, 1, 0]]) # target boxes
|
||||
return {
|
||||
"cls": cls,
|
||||
"bboxes": bbox,
|
||||
"ori_shape": ori_shape,
|
||||
"imgsz": imgsz,
|
||||
"ratio_pad": ratio_pad,
|
||||
"im_file": batch["im_file"][si],
|
||||
}
|
||||
|
||||
def plot_predictions(self, batch: dict[str, Any], preds: list[dict[str, torch.Tensor]], ni: int) -> None:
|
||||
"""Plot predicted bounding boxes on input images and save the result.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch data containing images, file paths, and other metadata.
|
||||
preds (list[dict[str, torch.Tensor]]): List of prediction dictionaries for each image in the batch.
|
||||
ni (int): Batch index used for naming the output file.
|
||||
|
||||
Examples:
|
||||
>>> validator = OBBValidator()
|
||||
>>> batch = {"img": images, "im_file": paths}
|
||||
>>> preds = [{"bboxes": torch.rand(10, 5), "cls": torch.zeros(10), "conf": torch.rand(10)}]
|
||||
>>> validator.plot_predictions(batch, preds, 0)
|
||||
"""
|
||||
if not preds:
|
||||
return
|
||||
for i, pred in enumerate(preds):
|
||||
pred["batch_idx"] = torch.ones_like(pred["conf"]) * i
|
||||
keys = preds[0].keys()
|
||||
batched_preds = {k: torch.cat([x[k] for x in preds], dim=0) for k in keys}
|
||||
plot_images(
|
||||
images=batch["img"],
|
||||
labels=batched_preds,
|
||||
paths=batch["im_file"],
|
||||
fname=self.save_dir / f"val_batch{ni}_pred.jpg",
|
||||
names=self.names,
|
||||
on_plot=self.on_plot,
|
||||
)
|
||||
|
||||
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
|
||||
"""Convert YOLO predictions to COCO JSON format with rotated bounding box information.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Prediction dictionary containing 'bboxes', 'conf', and 'cls' keys with
|
||||
bounding box coordinates, confidence scores, and class predictions.
|
||||
pbatch (dict[str, Any]): Batch dictionary containing 'imgsz', 'ori_shape', 'ratio_pad', and 'im_file'.
|
||||
|
||||
Notes:
|
||||
This method processes rotated bounding box predictions and converts them to both rbox format
|
||||
(x, y, w, h, angle) and polygon format (x1, y1, x2, y2, x3, y3, x4, y4) before adding them
|
||||
to the JSON dictionary.
|
||||
"""
|
||||
path = Path(pbatch["im_file"])
|
||||
stem = path.stem
|
||||
image_id = int(stem) if stem.isnumeric() else stem
|
||||
rbox = predn["bboxes"]
|
||||
poly = ops.xywhr2xyxyxyxy(rbox).view(-1, 8)
|
||||
for r, b, s, c in zip(rbox.tolist(), poly.tolist(), predn["conf"].tolist(), predn["cls"].tolist()):
|
||||
self.jdict.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"file_name": path.name,
|
||||
"category_id": self.class_map[int(c)],
|
||||
"score": round(s, 5),
|
||||
"rbox": [round(x, 3) for x in r],
|
||||
"poly": [round(x, 3) for x in b],
|
||||
}
|
||||
)
|
||||
|
||||
def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
|
||||
"""Save YOLO OBB detections to a text file in normalized coordinates.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Prediction dictionary containing 'bboxes', 'conf', and 'cls' keys with
|
||||
bounding box coordinates (including angle), confidence scores, and class predictions.
|
||||
save_conf (bool): Whether to save confidence scores in the text file.
|
||||
shape (tuple[int, int]): Original image shape in format (height, width).
|
||||
file (Path): Output file path to save detections.
|
||||
|
||||
Examples:
|
||||
>>> validator = OBBValidator()
|
||||
>>> predn = {
|
||||
... "bboxes": torch.tensor([[100, 100, 50, 30, 45]]),
|
||||
... "conf": torch.tensor([0.9]),
|
||||
... "cls": torch.tensor([0]),
|
||||
... }
|
||||
>>> validator.save_one_txt(predn, True, (640, 480), Path("detection.txt"))
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from ultralytics.engine.results import Results
|
||||
|
||||
Results(
|
||||
np.zeros((shape[0], shape[1]), dtype=np.uint8),
|
||||
path=None,
|
||||
names=self.names,
|
||||
obb=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
|
||||
).save_txt(file, save_conf=save_conf)
|
||||
|
||||
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Scales predictions to the original image size."""
|
||||
return {
|
||||
**predn,
|
||||
"bboxes": ops.scale_boxes(
|
||||
pbatch["imgsz"], predn["bboxes"].clone(), pbatch["ori_shape"], ratio_pad=pbatch["ratio_pad"], xywh=True
|
||||
),
|
||||
}
|
||||
|
||||
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Evaluate YOLO output in JSON format and save predictions in DOTA format.
|
||||
|
||||
Args:
|
||||
stats (dict[str, Any]): Performance statistics dictionary.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Updated performance statistics.
|
||||
"""
|
||||
if self.args.save_json and self.is_dota and len(self.jdict):
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
pred_json = self.save_dir / "predictions.json" # predictions
|
||||
pred_txt = self.save_dir / "predictions_txt" # predictions
|
||||
pred_txt.mkdir(parents=True, exist_ok=True)
|
||||
data = json.load(open(pred_json))
|
||||
# Save split results
|
||||
LOGGER.info(f"Saving predictions with DOTA format to {pred_txt}...")
|
||||
for d in data:
|
||||
image_id = d["image_id"]
|
||||
score = d["score"]
|
||||
classname = self.names[d["category_id"] - 1].replace(" ", "-")
|
||||
p = d["poly"]
|
||||
|
||||
with open(f"{pred_txt / f'Task1_{classname}'}.txt", "a", encoding="utf-8") as f:
|
||||
f.writelines(f"{image_id} {score} {p[0]} {p[1]} {p[2]} {p[3]} {p[4]} {p[5]} {p[6]} {p[7]}\n")
|
||||
# Save merged results, this could result slightly lower map than using official merging script,
|
||||
# because of the probiou calculation.
|
||||
pred_merged_txt = self.save_dir / "predictions_merged_txt" # predictions
|
||||
pred_merged_txt.mkdir(parents=True, exist_ok=True)
|
||||
merged_results = defaultdict(list)
|
||||
LOGGER.info(f"Saving merged predictions with DOTA format to {pred_merged_txt}...")
|
||||
for d in data:
|
||||
image_id = d["image_id"].split("__", 1)[0]
|
||||
pattern = re.compile(r"\d+___\d+")
|
||||
x, y = (int(c) for c in re.findall(pattern, d["image_id"])[0].split("___"))
|
||||
bbox, score, cls = d["rbox"], d["score"], d["category_id"] - 1
|
||||
bbox[0] += x
|
||||
bbox[1] += y
|
||||
bbox.extend([score, cls])
|
||||
merged_results[image_id].append(bbox)
|
||||
for image_id, bbox in merged_results.items():
|
||||
bbox = torch.tensor(bbox)
|
||||
max_wh = torch.max(bbox[:, :2]).item() * 2
|
||||
c = bbox[:, 6:7] * max_wh # classes
|
||||
scores = bbox[:, 5] # scores
|
||||
b = bbox[:, :5].clone()
|
||||
b[:, :2] += c
|
||||
# 0.3 could get results close to the ones from official merging script, even slightly better.
|
||||
i = TorchNMS.fast_nms(b, scores, 0.3, iou_func=batch_probiou)
|
||||
bbox = bbox[i]
|
||||
|
||||
b = ops.xywhr2xyxyxyxy(bbox[:, :5]).view(-1, 8)
|
||||
for x in torch.cat([b, bbox[:, 5:7]], dim=-1).tolist():
|
||||
classname = self.names[int(x[-1])].replace(" ", "-")
|
||||
p = [round(i, 3) for i in x[:-2]] # poly
|
||||
score = round(x[-2], 3)
|
||||
|
||||
with open(f"{pred_merged_txt / f'Task1_{classname}'}.txt", "a", encoding="utf-8") as f:
|
||||
f.writelines(f"{image_id} {score} {p[0]} {p[1]} {p[2]} {p[3]} {p[4]} {p[5]} {p[6]} {p[7]}\n")
|
||||
|
||||
return stats
|
||||
7
ultralytics/models/yolo/pose/__init__.py
Executable file
7
ultralytics/models/yolo/pose/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .predict import PosePredictor
|
||||
from .train import PoseTrainer
|
||||
from .val import PoseValidator
|
||||
|
||||
__all__ = "PosePredictor", "PoseTrainer", "PoseValidator"
|
||||
65
ultralytics/models/yolo/pose/predict.py
Executable file
65
ultralytics/models/yolo/pose/predict.py
Executable file
@@ -0,0 +1,65 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.models.yolo.detect.predict import DetectionPredictor
|
||||
from ultralytics.utils import DEFAULT_CFG, ops
|
||||
|
||||
|
||||
class PosePredictor(DetectionPredictor):
|
||||
"""A class extending the DetectionPredictor class for prediction based on a pose model.
|
||||
|
||||
This class specializes in pose estimation, handling keypoints detection alongside standard object detection
|
||||
capabilities inherited from DetectionPredictor.
|
||||
|
||||
Attributes:
|
||||
args (namespace): Configuration arguments for the predictor.
|
||||
model (torch.nn.Module): The loaded YOLO pose model with keypoint detection capabilities.
|
||||
|
||||
Methods:
|
||||
construct_result: Construct the result object from the prediction, including keypoints.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.yolo.pose import PosePredictor
|
||||
>>> args = dict(model="yolo26n-pose.pt", source=ASSETS)
|
||||
>>> predictor = PosePredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize PosePredictor for pose estimation tasks.
|
||||
|
||||
Sets up a PosePredictor instance, configuring it for pose detection tasks and handling device-specific warnings
|
||||
for Apple MPS.
|
||||
|
||||
Args:
|
||||
cfg (Any): Configuration for the predictor.
|
||||
overrides (dict, optional): Configuration overrides that take precedence over cfg.
|
||||
_callbacks (list, optional): List of callback functions to be invoked during prediction.
|
||||
"""
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.args.task = "pose"
|
||||
|
||||
def construct_result(self, pred, img, orig_img, img_path):
|
||||
"""Construct the result object from the prediction, including keypoints.
|
||||
|
||||
Extends the parent class implementation by extracting keypoint data from predictions and adding them to the
|
||||
result object.
|
||||
|
||||
Args:
|
||||
pred (torch.Tensor): The predicted bounding boxes, scores, and keypoints with shape (N, 6+K*D) where N is
|
||||
the number of detections, K is the number of keypoints, and D is the keypoint dimension.
|
||||
img (torch.Tensor): The processed input image tensor with shape (B, C, H, W).
|
||||
orig_img (np.ndarray): The original unprocessed image as a numpy array.
|
||||
img_path (str): The path to the original image file.
|
||||
|
||||
Returns:
|
||||
(Results): The result object containing the original image, image path, class names, bounding boxes, and
|
||||
keypoints.
|
||||
"""
|
||||
result = super().construct_result(pred, img, orig_img, img_path)
|
||||
# Extract keypoints from prediction and reshape according to model's keypoint shape
|
||||
pred_kpts = pred[:, 6:].view(pred.shape[0], *self.model.kpt_shape)
|
||||
# Scale keypoints coordinates to match the original image dimensions
|
||||
pred_kpts = ops.scale_coords(img.shape[2:], pred_kpts, orig_img.shape)
|
||||
result.update(keypoints=pred_kpts)
|
||||
return result
|
||||
113
ultralytics/models/yolo/pose/train.py
Executable file
113
ultralytics/models/yolo/pose/train.py
Executable file
@@ -0,0 +1,113 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ultralytics.models import yolo
|
||||
from ultralytics.nn.tasks import PoseModel
|
||||
from ultralytics.utils import DEFAULT_CFG
|
||||
from ultralytics.utils.torch_utils import unwrap_model
|
||||
|
||||
|
||||
class PoseTrainer(yolo.detect.DetectionTrainer):
|
||||
"""A class extending the DetectionTrainer class for training YOLO pose estimation models.
|
||||
|
||||
This trainer specializes in handling pose estimation tasks, managing model training, validation, and visualization
|
||||
of pose keypoints alongside bounding boxes.
|
||||
|
||||
Attributes:
|
||||
args (dict): Configuration arguments for training.
|
||||
model (PoseModel): The pose estimation model being trained.
|
||||
data (dict): Dataset configuration including keypoint shape information.
|
||||
loss_names (tuple): Names of the loss components used in training.
|
||||
|
||||
Methods:
|
||||
get_model: Retrieve a pose estimation model with specified configuration.
|
||||
set_model_attributes: Set keypoints shape attribute on the model.
|
||||
get_validator: Create a validator instance for model evaluation.
|
||||
plot_training_samples: Visualize training samples with keypoints.
|
||||
get_dataset: Retrieve the dataset and ensure it contains required kpt_shape key.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.pose import PoseTrainer
|
||||
>>> args = dict(model="yolo26n-pose.pt", data="coco8-pose.yaml", epochs=3)
|
||||
>>> trainer = PoseTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict[str, Any] | None = None, _callbacks=None):
|
||||
"""Initialize a PoseTrainer object for training YOLO pose estimation models.
|
||||
|
||||
Args:
|
||||
cfg (dict, optional): Default configuration dictionary containing training parameters.
|
||||
overrides (dict, optional): Dictionary of parameter overrides for the default configuration.
|
||||
_callbacks (list, optional): List of callback functions to be executed during training.
|
||||
|
||||
Notes:
|
||||
This trainer will automatically set the task to 'pose' regardless of what is provided in overrides.
|
||||
A warning is issued when using Apple MPS device due to known bugs with pose models.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
overrides["task"] = "pose"
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def get_model(
|
||||
self,
|
||||
cfg: str | Path | dict[str, Any] | None = None,
|
||||
weights: str | Path | None = None,
|
||||
verbose: bool = True,
|
||||
) -> PoseModel:
|
||||
"""Get pose estimation model with specified configuration and weights.
|
||||
|
||||
Args:
|
||||
cfg (str | Path | dict, optional): Model configuration file path or dictionary.
|
||||
weights (str | Path, optional): Path to the model weights file.
|
||||
verbose (bool): Whether to display model information.
|
||||
|
||||
Returns:
|
||||
(PoseModel): Initialized pose estimation model.
|
||||
"""
|
||||
model = PoseModel(
|
||||
cfg, nc=self.data["nc"], ch=self.data["channels"], data_kpt_shape=self.data["kpt_shape"], verbose=verbose
|
||||
)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
return model
|
||||
|
||||
def set_model_attributes(self):
|
||||
"""Set keypoints shape attribute of PoseModel."""
|
||||
super().set_model_attributes()
|
||||
self.model.kpt_shape = self.data["kpt_shape"]
|
||||
kpt_names = self.data.get("kpt_names")
|
||||
if not kpt_names:
|
||||
names = list(map(str, range(self.model.kpt_shape[0])))
|
||||
kpt_names = {i: names for i in range(self.model.nc)}
|
||||
self.model.kpt_names = kpt_names
|
||||
|
||||
def get_validator(self):
|
||||
"""Return an instance of the PoseValidator class for validation."""
|
||||
self.loss_names = "box_loss", "pose_loss", "kobj_loss", "cls_loss", "dfl_loss"
|
||||
if getattr(unwrap_model(self.model).model[-1], "flow_model", None) is not None:
|
||||
self.loss_names += ("rle_loss",)
|
||||
return yolo.pose.PoseValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
|
||||
def get_dataset(self) -> dict[str, Any]:
|
||||
"""Retrieve the dataset and ensure it contains the required `kpt_shape` key.
|
||||
|
||||
Returns:
|
||||
(dict): A dictionary containing the training/validation/test dataset and category names.
|
||||
|
||||
Raises:
|
||||
KeyError: If the `kpt_shape` key is not present in the dataset.
|
||||
"""
|
||||
data = super().get_dataset()
|
||||
if "kpt_shape" not in data:
|
||||
raise KeyError(f"No `kpt_shape` in the {self.args.data}. See https://docs.ultralytics.com/datasets/pose/")
|
||||
return data
|
||||
248
ultralytics/models/yolo/pose/val.py
Executable file
248
ultralytics/models/yolo/pose/val.py
Executable file
@@ -0,0 +1,248 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.utils import ops
|
||||
from ultralytics.utils.metrics import OKS_SIGMA, PoseMetrics, kpt_iou
|
||||
|
||||
|
||||
class PoseValidator(DetectionValidator):
|
||||
"""A class extending the DetectionValidator class for validation based on a pose model.
|
||||
|
||||
This validator is specifically designed for pose estimation tasks, handling keypoints and implementing specialized
|
||||
metrics for pose evaluation.
|
||||
|
||||
Attributes:
|
||||
sigma (np.ndarray): Sigma values for OKS calculation, either OKS_SIGMA or ones divided by number of keypoints.
|
||||
kpt_shape (list[int]): Shape of the keypoints, typically [17, 3] for COCO format.
|
||||
args (dict): Arguments for the validator including task set to "pose".
|
||||
metrics (PoseMetrics): Metrics object for pose evaluation.
|
||||
|
||||
Methods:
|
||||
preprocess: Preprocess batch by converting keypoints data to float and moving it to the device.
|
||||
get_desc: Return description of evaluation metrics in string format.
|
||||
init_metrics: Initialize pose estimation metrics for YOLO model.
|
||||
_prepare_batch: Prepare a batch for processing by converting keypoints to float and scaling to original
|
||||
dimensions.
|
||||
_prepare_pred: Prepare and scale keypoints in predictions for pose processing.
|
||||
_process_batch: Return correct prediction matrix by computing Intersection over Union (IoU) between detections
|
||||
and ground truth.
|
||||
plot_val_samples: Plot and save validation set samples with ground truth bounding boxes and keypoints.
|
||||
plot_predictions: Plot and save model predictions with bounding boxes and keypoints.
|
||||
save_one_txt: Save YOLO pose detections to a text file in normalized coordinates.
|
||||
pred_to_json: Convert YOLO predictions to COCO JSON format.
|
||||
eval_json: Evaluate object detection model using COCO JSON format.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.pose import PoseValidator
|
||||
>>> args = dict(model="yolo26n-pose.pt", data="coco8-pose.yaml")
|
||||
>>> validator = PoseValidator(args=args)
|
||||
>>> validator()
|
||||
|
||||
Notes:
|
||||
This class extends DetectionValidator with pose-specific functionality. It initializes with sigma values
|
||||
for OKS calculation and sets up PoseMetrics for evaluation. A warning is displayed when using Apple MPS
|
||||
due to a known bug with pose models.
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
|
||||
"""Initialize a PoseValidator object for pose estimation validation.
|
||||
|
||||
This validator is specifically designed for pose estimation tasks, handling keypoints and implementing
|
||||
specialized metrics for pose evaluation.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to be used for validation.
|
||||
save_dir (Path | str, optional): Directory to save results.
|
||||
args (dict, optional): Arguments for the validator including task set to "pose".
|
||||
_callbacks (list, optional): List of callback functions to be executed during validation.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.sigma = None
|
||||
self.kpt_shape = None
|
||||
self.args.task = "pose"
|
||||
self.metrics = PoseMetrics()
|
||||
|
||||
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Preprocess batch by converting keypoints data to float and moving it to the device."""
|
||||
batch = super().preprocess(batch)
|
||||
batch["keypoints"] = batch["keypoints"].float()
|
||||
return batch
|
||||
|
||||
def get_desc(self) -> str:
|
||||
"""Return description of evaluation metrics in string format."""
|
||||
return ("%22s" + "%11s" * 10) % (
|
||||
"Class",
|
||||
"Images",
|
||||
"Instances",
|
||||
"Box(P",
|
||||
"R",
|
||||
"mAP50",
|
||||
"mAP50-95)",
|
||||
"Pose(P",
|
||||
"R",
|
||||
"mAP50",
|
||||
"mAP50-95)",
|
||||
)
|
||||
|
||||
def init_metrics(self, model: torch.nn.Module) -> None:
|
||||
"""Initialize evaluation metrics for YOLO pose validation.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Model to validate.
|
||||
"""
|
||||
super().init_metrics(model)
|
||||
self.kpt_shape = self.data["kpt_shape"]
|
||||
is_pose = self.kpt_shape == [17, 3]
|
||||
nkpt = self.kpt_shape[0]
|
||||
self.sigma = OKS_SIGMA if is_pose else np.ones(nkpt) / nkpt
|
||||
|
||||
def postprocess(self, preds: torch.Tensor) -> list[dict[str, torch.Tensor]]:
|
||||
"""Postprocess YOLO predictions to extract and reshape keypoints for pose estimation.
|
||||
|
||||
This method extends the parent class postprocessing by extracting keypoints from the 'extra' field of
|
||||
predictions and reshaping them according to the keypoint shape configuration. The keypoints are reshaped from a
|
||||
flattened format to the proper dimensional structure (typically [N, 17, 3] for COCO pose format).
|
||||
|
||||
Args:
|
||||
preds (torch.Tensor): Raw prediction tensor from the YOLO pose model containing bounding boxes, confidence
|
||||
scores, class predictions, and keypoint data.
|
||||
|
||||
Returns:
|
||||
(list[dict[str, torch.Tensor]]): List of processed prediction dictionaries, each containing:
|
||||
- 'bboxes': Bounding box coordinates
|
||||
- 'conf': Confidence scores
|
||||
- 'cls': Class predictions
|
||||
- 'keypoints': Reshaped keypoint coordinates with shape (-1, *self.kpt_shape)
|
||||
|
||||
Notes:
|
||||
If no keypoints are present in a prediction (empty keypoints), that prediction is skipped and continues
|
||||
to the next one. The keypoints are extracted from the 'extra' field which contains additional
|
||||
task-specific data beyond basic detection.
|
||||
"""
|
||||
preds = super().postprocess(preds)
|
||||
for pred in preds:
|
||||
pred["keypoints"] = pred.pop("extra").view(-1, *self.kpt_shape) # remove extra if exists
|
||||
return preds
|
||||
|
||||
def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare a batch for processing by converting keypoints to float and scaling to original dimensions.
|
||||
|
||||
Args:
|
||||
si (int): Sample index within the batch.
|
||||
batch (dict[str, Any]): Dictionary containing batch data with keys like 'keypoints', 'batch_idx', etc.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Prepared batch with keypoints scaled to original image dimensions.
|
||||
|
||||
Notes:
|
||||
This method extends the parent class's _prepare_batch method by adding keypoint processing.
|
||||
Keypoints are scaled from normalized coordinates to original image dimensions.
|
||||
"""
|
||||
pbatch = super()._prepare_batch(si, batch)
|
||||
kpts = batch["keypoints"][batch["batch_idx"] == si]
|
||||
h, w = pbatch["imgsz"]
|
||||
kpts = kpts.clone()
|
||||
kpts[..., 0] *= w
|
||||
kpts[..., 1] *= h
|
||||
pbatch["keypoints"] = kpts
|
||||
return pbatch
|
||||
|
||||
def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, Any]) -> dict[str, np.ndarray]:
|
||||
"""Return correct prediction matrix by computing Intersection over Union (IoU) between detections and ground
|
||||
truth.
|
||||
|
||||
Args:
|
||||
preds (dict[str, torch.Tensor]): Dictionary containing prediction data with keys 'cls' for class predictions
|
||||
and 'keypoints' for keypoint predictions.
|
||||
batch (dict[str, Any]): Dictionary containing ground truth data with keys 'cls' for class labels, 'bboxes'
|
||||
for bounding boxes, and 'keypoints' for keypoint annotations.
|
||||
|
||||
Returns:
|
||||
(dict[str, np.ndarray]): Dictionary containing the correct prediction matrix including 'tp_p' for pose true
|
||||
positives across 10 IoU levels.
|
||||
|
||||
Notes:
|
||||
`0.53` scale factor used in area computation is referenced from
|
||||
https://github.com/jin-s13/xtcocoapi/blob/master/xtcocotools/cocoeval.py#L384.
|
||||
"""
|
||||
tp = super()._process_batch(preds, batch)
|
||||
gt_cls = batch["cls"]
|
||||
if gt_cls.shape[0] == 0 or preds["cls"].shape[0] == 0:
|
||||
tp_p = np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)
|
||||
else:
|
||||
# `0.53` is from https://github.com/jin-s13/xtcocoapi/blob/master/xtcocotools/cocoeval.py#L384
|
||||
area = ops.xyxy2xywh(batch["bboxes"])[:, 2:].prod(1) * 0.53
|
||||
iou = kpt_iou(batch["keypoints"], preds["keypoints"], sigma=self.sigma, area=area)
|
||||
tp_p = self.match_predictions(preds["cls"], gt_cls, iou).cpu().numpy()
|
||||
tp.update({"tp_p": tp_p}) # update tp with kpts IoU
|
||||
return tp
|
||||
|
||||
def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
|
||||
"""Save YOLO pose detections to a text file in normalized coordinates.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Prediction dict with keys 'bboxes', 'conf', 'cls', and 'keypoints'.
|
||||
save_conf (bool): Whether to save confidence scores.
|
||||
shape (tuple[int, int]): Shape of the original image (height, width).
|
||||
file (Path): Output file path to save detections.
|
||||
|
||||
Notes:
|
||||
The output format is: class_id x_center y_center width height confidence keypoints where keypoints are
|
||||
normalized (x, y, visibility) values for each point.
|
||||
"""
|
||||
from ultralytics.engine.results import Results
|
||||
|
||||
Results(
|
||||
np.zeros((shape[0], shape[1]), dtype=np.uint8),
|
||||
path=None,
|
||||
names=self.names,
|
||||
boxes=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
|
||||
keypoints=predn["keypoints"],
|
||||
).save_txt(file, save_conf=save_conf)
|
||||
|
||||
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
|
||||
"""Convert YOLO predictions to COCO JSON format.
|
||||
|
||||
This method takes prediction tensors and batch data, converts the bounding boxes from YOLO format to COCO
|
||||
format, and appends the results with keypoints to the internal JSON dictionary (self.jdict).
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Prediction dictionary containing 'bboxes', 'conf', 'cls', and 'kpts'
|
||||
tensors.
|
||||
pbatch (dict[str, Any]): Batch dictionary containing 'imgsz', 'ori_shape', 'ratio_pad', and 'im_file'.
|
||||
|
||||
Notes:
|
||||
The method extracts the image ID from the filename stem (either as an integer if numeric, or as a string),
|
||||
converts bounding boxes from xyxy to xywh format, and adjusts coordinates from center to top-left corner
|
||||
before saving to the JSON dictionary.
|
||||
"""
|
||||
super().pred_to_json(predn, pbatch)
|
||||
kpts = predn["kpts"]
|
||||
for i, k in enumerate(kpts.flatten(1, 2).tolist()):
|
||||
self.jdict[-len(kpts) + i]["keypoints"] = k # keypoints
|
||||
|
||||
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Scales predictions to the original image size."""
|
||||
return {
|
||||
**super().scale_preds(predn, pbatch),
|
||||
"kpts": ops.scale_coords(
|
||||
pbatch["imgsz"],
|
||||
predn["keypoints"].clone(),
|
||||
pbatch["ori_shape"],
|
||||
ratio_pad=pbatch["ratio_pad"],
|
||||
),
|
||||
}
|
||||
|
||||
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Evaluate object detection model using COCO JSON format."""
|
||||
anno_json = self.data["path"] / "annotations/person_keypoints_val2017.json" # annotations
|
||||
pred_json = self.save_dir / "predictions.json" # predictions
|
||||
return super().coco_evaluate(stats, pred_json, anno_json, ["bbox", "keypoints"], suffix=["Box", "Pose"])
|
||||
7
ultralytics/models/yolo/segment/__init__.py
Executable file
7
ultralytics/models/yolo/segment/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .predict import SegmentationPredictor
|
||||
from .train import SegmentationTrainer
|
||||
from .val import SegmentationValidator
|
||||
|
||||
__all__ = "SegmentationPredictor", "SegmentationTrainer", "SegmentationValidator"
|
||||
109
ultralytics/models/yolo/segment/predict.py
Executable file
109
ultralytics/models/yolo/segment/predict.py
Executable file
@@ -0,0 +1,109 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from ultralytics.engine.results import Results
|
||||
from ultralytics.models.yolo.detect.predict import DetectionPredictor
|
||||
from ultralytics.utils import DEFAULT_CFG, ops
|
||||
|
||||
|
||||
class SegmentationPredictor(DetectionPredictor):
|
||||
"""A class extending the DetectionPredictor class for prediction based on a segmentation model.
|
||||
|
||||
This class specializes in processing segmentation model outputs, handling both bounding boxes and masks in the
|
||||
prediction results.
|
||||
|
||||
Attributes:
|
||||
args (dict): Configuration arguments for the predictor.
|
||||
model (torch.nn.Module): The loaded YOLO segmentation model.
|
||||
batch (list): Current batch of images being processed.
|
||||
|
||||
Methods:
|
||||
postprocess: Apply non-max suppression and process segmentation detections.
|
||||
construct_results: Construct a list of result objects from predictions.
|
||||
construct_result: Construct a single result object from a prediction.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.utils import ASSETS
|
||||
>>> from ultralytics.models.yolo.segment import SegmentationPredictor
|
||||
>>> args = dict(model="yolo26n-seg.pt", source=ASSETS)
|
||||
>>> predictor = SegmentationPredictor(overrides=args)
|
||||
>>> predictor.predict_cli()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize the SegmentationPredictor with configuration, overrides, and callbacks.
|
||||
|
||||
This class specializes in processing segmentation model outputs, handling both bounding boxes and masks in the
|
||||
prediction results.
|
||||
|
||||
Args:
|
||||
cfg (dict): Configuration for the predictor.
|
||||
overrides (dict, optional): Configuration overrides that take precedence over cfg.
|
||||
_callbacks (list, optional): List of callback functions to be invoked during prediction.
|
||||
"""
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.args.task = "segment"
|
||||
|
||||
def postprocess(self, preds, img, orig_imgs):
|
||||
"""Apply non-max suppression and process segmentation detections for each image in the input batch.
|
||||
|
||||
Args:
|
||||
preds (tuple): Model predictions, containing bounding boxes, scores, classes, and mask coefficients.
|
||||
img (torch.Tensor): Input image tensor in model format, with shape (B, C, H, W).
|
||||
orig_imgs (list | torch.Tensor | np.ndarray): Original image or batch of images.
|
||||
|
||||
Returns:
|
||||
(list): List of Results objects containing the segmentation predictions for each image in the batch. Each
|
||||
Results object includes both bounding boxes and segmentation masks.
|
||||
|
||||
Examples:
|
||||
>>> predictor = SegmentationPredictor(overrides=dict(model="yolo26n-seg.pt"))
|
||||
>>> results = predictor.postprocess(preds, img, orig_img)
|
||||
"""
|
||||
# Extract protos - tuple if PyTorch model or array if exported
|
||||
protos = preds[0][1] if isinstance(preds[0], tuple) else preds[1]
|
||||
return super().postprocess(preds[0], img, orig_imgs, protos=protos)
|
||||
|
||||
def construct_results(self, preds, img, orig_imgs, protos):
|
||||
"""Construct a list of result objects from the predictions.
|
||||
|
||||
Args:
|
||||
preds (list[torch.Tensor]): List of predicted bounding boxes, scores, and masks.
|
||||
img (torch.Tensor): The image after preprocessing.
|
||||
orig_imgs (list[np.ndarray]): List of original images before preprocessing.
|
||||
protos (torch.Tensor): Prototype masks tensor with shape (B, C, H, W).
|
||||
|
||||
Returns:
|
||||
(list[Results]): List of result objects containing the original images, image paths, class names, bounding
|
||||
boxes, and masks.
|
||||
"""
|
||||
return [
|
||||
self.construct_result(pred, img, orig_img, img_path, proto)
|
||||
for pred, orig_img, img_path, proto in zip(preds, orig_imgs, self.batch[0], protos)
|
||||
]
|
||||
|
||||
def construct_result(self, pred, img, orig_img, img_path, proto):
|
||||
"""Construct a single result object from the prediction.
|
||||
|
||||
Args:
|
||||
pred (torch.Tensor): The predicted bounding boxes, scores, and masks.
|
||||
img (torch.Tensor): The image after preprocessing.
|
||||
orig_img (np.ndarray): The original image before preprocessing.
|
||||
img_path (str): The path to the original image.
|
||||
proto (torch.Tensor): The prototype masks.
|
||||
|
||||
Returns:
|
||||
(Results): Result object containing the original image, image path, class names, bounding boxes, and masks.
|
||||
"""
|
||||
if pred.shape[0] == 0: # save empty boxes
|
||||
masks = None
|
||||
elif self.args.retina_masks:
|
||||
pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)
|
||||
masks = ops.process_mask_native(proto, pred[:, 6:], pred[:, :4], orig_img.shape[:2]) # NHW
|
||||
else:
|
||||
masks = ops.process_mask(proto, pred[:, 6:], pred[:, :4], img.shape[2:], upsample=True) # NHW
|
||||
pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)
|
||||
if masks is not None:
|
||||
keep = masks.amax((-2, -1)) > 0 # only keep predictions with masks
|
||||
if not all(keep): # most predictions have masks
|
||||
pred, masks = pred[keep], masks[keep] # indexing is slow
|
||||
return Results(orig_img, path=img_path, names=self.model.names, boxes=pred[:, :6], masks=masks)
|
||||
69
ultralytics/models/yolo/segment/train.py
Executable file
69
ultralytics/models/yolo/segment/train.py
Executable file
@@ -0,0 +1,69 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.models import yolo
|
||||
from ultralytics.nn.tasks import SegmentationModel
|
||||
from ultralytics.utils import DEFAULT_CFG, RANK
|
||||
|
||||
|
||||
class SegmentationTrainer(yolo.detect.DetectionTrainer):
|
||||
"""A class extending the DetectionTrainer class for training based on a segmentation model.
|
||||
|
||||
This trainer specializes in handling segmentation tasks, extending the detection trainer with segmentation-specific
|
||||
functionality including model initialization, validation, and visualization.
|
||||
|
||||
Attributes:
|
||||
loss_names (tuple[str]): Names of the loss components used during training.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.segment import SegmentationTrainer
|
||||
>>> args = dict(model="yolo26n-seg.pt", data="coco8-seg.yaml", epochs=3)
|
||||
>>> trainer = SegmentationTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict | None = None, _callbacks=None):
|
||||
"""Initialize a SegmentationTrainer object.
|
||||
|
||||
Args:
|
||||
cfg (dict): Configuration dictionary with default training settings.
|
||||
overrides (dict, optional): Dictionary of parameter overrides for the default configuration.
|
||||
_callbacks (list, optional): List of callback functions to be executed during training.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
overrides["task"] = "segment"
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def get_model(self, cfg: dict | str | None = None, weights: str | Path | None = None, verbose: bool = True):
|
||||
"""Initialize and return a SegmentationModel with specified configuration and weights.
|
||||
|
||||
Args:
|
||||
cfg (dict | str, optional): Model configuration. Can be a dictionary, a path to a YAML file, or None.
|
||||
weights (str | Path, optional): Path to pretrained weights file.
|
||||
verbose (bool): Whether to display model information during initialization.
|
||||
|
||||
Returns:
|
||||
(SegmentationModel): Initialized segmentation model with loaded weights if specified.
|
||||
|
||||
Examples:
|
||||
>>> trainer = SegmentationTrainer()
|
||||
>>> model = trainer.get_model(cfg="yolo26n-seg.yaml")
|
||||
>>> model = trainer.get_model(weights="yolo26n-seg.pt", verbose=False)
|
||||
"""
|
||||
model = SegmentationModel(cfg, nc=self.data["nc"], ch=self.data["channels"], verbose=verbose and RANK == -1)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
return model
|
||||
|
||||
def get_validator(self):
|
||||
"""Return an instance of SegmentationValidator for validation of YOLO model."""
|
||||
self.loss_names = "box_loss", "seg_loss", "cls_loss", "dfl_loss", "sem_loss"
|
||||
return yolo.segment.SegmentationValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
307
ultralytics/models/yolo/segment/val.py
Executable file
307
ultralytics/models/yolo/segment/val.py
Executable file
@@ -0,0 +1,307 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.utils import LOGGER, ops
|
||||
from ultralytics.utils.checks import check_requirements
|
||||
from ultralytics.utils.metrics import SegmentMetrics, mask_iou
|
||||
|
||||
|
||||
class SegmentationValidator(DetectionValidator):
|
||||
"""A class extending the DetectionValidator class for validation based on a segmentation model.
|
||||
|
||||
This validator handles the evaluation of segmentation models, processing both bounding box and mask predictions to
|
||||
compute metrics such as mAP for both detection and segmentation tasks.
|
||||
|
||||
Attributes:
|
||||
plot_masks (list): List to store masks for plotting.
|
||||
process (callable): Function to process masks based on save_json and save_txt flags.
|
||||
args (SimpleNamespace): Arguments for the validator.
|
||||
metrics (SegmentMetrics): Metrics calculator for segmentation tasks.
|
||||
stats (dict): Dictionary to store statistics during validation.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.segment import SegmentationValidator
|
||||
>>> args = dict(model="yolo26n-seg.pt", data="coco8-seg.yaml")
|
||||
>>> validator = SegmentationValidator(args=args)
|
||||
>>> validator()
|
||||
"""
|
||||
|
||||
def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None) -> None:
|
||||
"""Initialize SegmentationValidator and set task to 'segment', metrics to SegmentMetrics.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader, optional): DataLoader to use for validation.
|
||||
save_dir (Path, optional): Directory to save results.
|
||||
args (dict, optional): Arguments for the validator.
|
||||
_callbacks (list, optional): List of callback functions.
|
||||
"""
|
||||
super().__init__(dataloader, save_dir, args, _callbacks)
|
||||
self.process = None
|
||||
self.args.task = "segment"
|
||||
self.metrics = SegmentMetrics()
|
||||
|
||||
def preprocess(self, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Preprocess batch of images for YOLO segmentation validation.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch containing images and annotations.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Preprocessed batch.
|
||||
"""
|
||||
batch = super().preprocess(batch)
|
||||
batch["masks"] = batch["masks"].float()
|
||||
return batch
|
||||
|
||||
def init_metrics(self, model: torch.nn.Module) -> None:
|
||||
"""Initialize metrics and select mask processing function based on save_json flag.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Model to validate.
|
||||
"""
|
||||
super().init_metrics(model)
|
||||
if self.args.save_json:
|
||||
check_requirements("faster-coco-eval>=1.6.7")
|
||||
# More accurate vs faster
|
||||
self.process = ops.process_mask_native if self.args.save_json or self.args.save_txt else ops.process_mask
|
||||
|
||||
def get_desc(self) -> str:
|
||||
"""Return a formatted description of evaluation metrics."""
|
||||
return ("%22s" + "%11s" * 10) % (
|
||||
"Class",
|
||||
"Images",
|
||||
"Instances",
|
||||
"Box(P",
|
||||
"R",
|
||||
"mAP50",
|
||||
"mAP50-95)",
|
||||
"Mask(P",
|
||||
"R",
|
||||
"mAP50",
|
||||
"mAP50-95)",
|
||||
)
|
||||
|
||||
def postprocess(self, preds: list[torch.Tensor]) -> list[dict[str, torch.Tensor]]:
|
||||
"""Post-process YOLO predictions and return output detections with proto.
|
||||
|
||||
Args:
|
||||
preds (list[torch.Tensor]): Raw predictions from the model.
|
||||
|
||||
Returns:
|
||||
(list[dict[str, torch.Tensor]]): Processed detection predictions with masks.
|
||||
"""
|
||||
proto = preds[0][1] if isinstance(preds[0], tuple) else preds[1]
|
||||
preds = super().postprocess(preds[0])
|
||||
imgsz = [4 * x for x in proto.shape[2:]] # get image size from proto
|
||||
for i, pred in enumerate(preds):
|
||||
coefficient = pred.pop("extra")
|
||||
pred["masks"] = (
|
||||
self.process(proto[i], coefficient, pred["bboxes"], shape=imgsz)
|
||||
if coefficient.shape[0]
|
||||
else torch.zeros(
|
||||
(0, *(imgsz if self.process is ops.process_mask_native else proto.shape[2:])),
|
||||
dtype=torch.uint8,
|
||||
device=pred["bboxes"].device,
|
||||
)
|
||||
)
|
||||
return preds
|
||||
|
||||
def _prepare_batch(self, si: int, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare a batch for validation by processing images and targets.
|
||||
|
||||
Args:
|
||||
si (int): Sample index within the batch.
|
||||
batch (dict[str, Any]): Batch data containing images and annotations.
|
||||
|
||||
Returns:
|
||||
(dict[str, Any]): Prepared batch with processed annotations.
|
||||
"""
|
||||
prepared_batch = super()._prepare_batch(si, batch)
|
||||
nl = prepared_batch["cls"].shape[0]
|
||||
if self.args.overlap_mask:
|
||||
masks = batch["masks"][si]
|
||||
index = torch.arange(1, nl + 1, device=masks.device).view(nl, 1, 1)
|
||||
masks = (masks == index).float()
|
||||
else:
|
||||
masks = batch["masks"][batch["batch_idx"] == si]
|
||||
if nl:
|
||||
mask_size = [s if self.process is ops.process_mask_native else s // 4 for s in prepared_batch["imgsz"]]
|
||||
if masks.shape[1:] != mask_size:
|
||||
masks = F.interpolate(masks[None], mask_size, mode="bilinear", align_corners=False)[0]
|
||||
masks = masks.gt_(0.5)
|
||||
prepared_batch["masks"] = masks
|
||||
return prepared_batch
|
||||
|
||||
def _process_batch(self, preds: dict[str, torch.Tensor], batch: dict[str, Any]) -> dict[str, np.ndarray]:
|
||||
"""Compute correct prediction matrix for a batch based on bounding boxes and optional masks.
|
||||
|
||||
Args:
|
||||
preds (dict[str, torch.Tensor]): Dictionary containing predictions with keys like 'cls' and 'masks'.
|
||||
batch (dict[str, Any]): Dictionary containing batch data with keys like 'cls' and 'masks'.
|
||||
|
||||
Returns:
|
||||
(dict[str, np.ndarray]): A dictionary containing correct prediction matrices including 'tp_m' for mask IoU.
|
||||
|
||||
Examples:
|
||||
>>> preds = {"cls": torch.tensor([1, 0]), "masks": torch.rand(2, 640, 640), "bboxes": torch.rand(2, 4)}
|
||||
>>> batch = {"cls": torch.tensor([1, 0]), "masks": torch.rand(2, 640, 640), "bboxes": torch.rand(2, 4)}
|
||||
>>> correct_preds = validator._process_batch(preds, batch)
|
||||
|
||||
Notes:
|
||||
- This method computes IoU between predicted and ground truth masks.
|
||||
- Overlapping masks are handled based on the overlap_mask argument setting.
|
||||
"""
|
||||
tp = super()._process_batch(preds, batch)
|
||||
gt_cls = batch["cls"]
|
||||
if gt_cls.shape[0] == 0 or preds["cls"].shape[0] == 0:
|
||||
tp_m = np.zeros((preds["cls"].shape[0], self.niou), dtype=bool)
|
||||
else:
|
||||
iou = mask_iou(batch["masks"].flatten(1), preds["masks"].flatten(1).float()) # float, uint8
|
||||
tp_m = self.match_predictions(preds["cls"], gt_cls, iou).cpu().numpy()
|
||||
tp.update({"tp_m": tp_m}) # update tp with mask IoU
|
||||
return tp
|
||||
|
||||
def plot_predictions(self, batch: dict[str, Any], preds: list[dict[str, torch.Tensor]], ni: int) -> None:
|
||||
"""Plot batch predictions with masks and bounding boxes.
|
||||
|
||||
Args:
|
||||
batch (dict[str, Any]): Batch containing images and annotations.
|
||||
preds (list[dict[str, torch.Tensor]]): List of predictions from the model.
|
||||
ni (int): Batch index.
|
||||
"""
|
||||
for p in preds:
|
||||
masks = p["masks"]
|
||||
if masks.shape[0] > self.args.max_det:
|
||||
LOGGER.warning(f"Limiting validation plots to 'max_det={self.args.max_det}' items.")
|
||||
p["masks"] = torch.as_tensor(masks[: self.args.max_det], dtype=torch.uint8).cpu()
|
||||
super().plot_predictions(batch, preds, ni, max_det=self.args.max_det) # plot bboxes
|
||||
|
||||
def save_one_txt(self, predn: dict[str, torch.Tensor], save_conf: bool, shape: tuple[int, int], file: Path) -> None:
|
||||
"""Save YOLO detections to a txt file in normalized coordinates in a specific format.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Prediction dictionary containing 'bboxes', 'conf', 'cls', and 'masks' keys.
|
||||
save_conf (bool): Whether to save confidence scores.
|
||||
shape (tuple[int, int]): Shape of the original image.
|
||||
file (Path): File path to save the detections.
|
||||
"""
|
||||
from ultralytics.engine.results import Results
|
||||
|
||||
Results(
|
||||
np.zeros((shape[0], shape[1]), dtype=np.uint8),
|
||||
path=None,
|
||||
names=self.names,
|
||||
boxes=torch.cat([predn["bboxes"], predn["conf"].unsqueeze(-1), predn["cls"].unsqueeze(-1)], dim=1),
|
||||
masks=torch.as_tensor(predn["masks"], dtype=torch.uint8),
|
||||
).save_txt(file, save_conf=save_conf)
|
||||
|
||||
def pred_to_json(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> None:
|
||||
"""Save one JSON result for COCO evaluation.
|
||||
|
||||
Args:
|
||||
predn (dict[str, torch.Tensor]): Predictions containing bboxes, masks, confidence scores, and classes.
|
||||
pbatch (dict[str, Any]): Batch dictionary containing 'imgsz', 'ori_shape', 'ratio_pad', and 'im_file'.
|
||||
"""
|
||||
|
||||
def to_string(counts: list[int]) -> str:
|
||||
"""Converts the RLE object into a compact string representation. Each count is delta-encoded and
|
||||
variable-length encoded as a string.
|
||||
|
||||
Args:
|
||||
counts (list[int]): List of RLE counts.
|
||||
"""
|
||||
result = []
|
||||
|
||||
for i in range(len(counts)):
|
||||
x = int(counts[i])
|
||||
|
||||
# Apply delta encoding for all counts after the second entry
|
||||
if i > 2:
|
||||
x -= int(counts[i - 2])
|
||||
|
||||
# Variable-length encode the value
|
||||
while True:
|
||||
c = x & 0x1F # Take 5 bits
|
||||
x >>= 5
|
||||
|
||||
# If the sign bit (0x10) is set, continue if x != -1;
|
||||
# otherwise, continue if x != 0
|
||||
more = (x != -1) if (c & 0x10) else (x != 0)
|
||||
if more:
|
||||
c |= 0x20 # Set continuation bit
|
||||
c += 48 # Shift to ASCII
|
||||
result.append(chr(c))
|
||||
if not more:
|
||||
break
|
||||
|
||||
return "".join(result)
|
||||
|
||||
def multi_encode(pixels: torch.Tensor) -> list[int]:
|
||||
"""Convert multiple binary masks using Run-Length Encoding (RLE).
|
||||
|
||||
Args:
|
||||
pixels (torch.Tensor): A 2D tensor where each row represents a flattened binary mask with shape [N,
|
||||
H*W].
|
||||
|
||||
Returns:
|
||||
(list[list[int]]): A list of RLE counts for each mask.
|
||||
"""
|
||||
transitions = pixels[:, 1:] != pixels[:, :-1]
|
||||
row_idx, col_idx = torch.where(transitions)
|
||||
col_idx = col_idx + 1
|
||||
|
||||
# Compute run lengths
|
||||
counts = []
|
||||
for i in range(pixels.shape[0]):
|
||||
positions = col_idx[row_idx == i]
|
||||
if len(positions):
|
||||
count = torch.diff(positions).tolist()
|
||||
count.insert(0, positions[0].item())
|
||||
count.append(len(pixels[i]) - positions[-1].item())
|
||||
else:
|
||||
count = [len(pixels[i])]
|
||||
|
||||
# Ensure starting with background (0) count
|
||||
if pixels[i][0].item() == 1:
|
||||
count = [0, *count]
|
||||
counts.append(count)
|
||||
|
||||
return counts
|
||||
|
||||
pred_masks = predn["masks"].transpose(2, 1).contiguous().view(len(predn["masks"]), -1) # N, H*W
|
||||
h, w = predn["masks"].shape[1:3]
|
||||
counts = multi_encode(pred_masks)
|
||||
rles = []
|
||||
for c in counts:
|
||||
rles.append({"size": [h, w], "counts": to_string(c)})
|
||||
super().pred_to_json(predn, pbatch)
|
||||
for i, r in enumerate(rles):
|
||||
self.jdict[-len(rles) + i]["segmentation"] = r # segmentation
|
||||
|
||||
def scale_preds(self, predn: dict[str, torch.Tensor], pbatch: dict[str, Any]) -> dict[str, torch.Tensor]:
|
||||
"""Scales predictions to the original image size."""
|
||||
return {
|
||||
**super().scale_preds(predn, pbatch),
|
||||
"masks": ops.scale_masks(predn["masks"][None], pbatch["ori_shape"], ratio_pad=pbatch["ratio_pad"])[
|
||||
0
|
||||
].byte(),
|
||||
}
|
||||
|
||||
def eval_json(self, stats: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return COCO-style instance segmentation evaluation metrics."""
|
||||
pred_json = self.save_dir / "predictions.json" # predictions
|
||||
anno_json = (
|
||||
self.data["path"]
|
||||
/ "annotations"
|
||||
/ ("instances_val2017.json" if self.is_coco else f"lvis_v1_{self.args.split}.json")
|
||||
) # annotations
|
||||
return super().coco_evaluate(stats, pred_json, anno_json, ["bbox", "segm"], suffix=["Box", "Mask"])
|
||||
5
ultralytics/models/yolo/world/__init__.py
Executable file
5
ultralytics/models/yolo/world/__init__.py
Executable file
@@ -0,0 +1,5 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .train import WorldTrainer
|
||||
|
||||
__all__ = ["WorldTrainer"]
|
||||
173
ultralytics/models/yolo/world/train.py
Executable file
173
ultralytics/models/yolo/world/train.py
Executable file
@@ -0,0 +1,173 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data import build_yolo_dataset
|
||||
from ultralytics.models.yolo.detect import DetectionTrainer
|
||||
from ultralytics.nn.tasks import WorldModel
|
||||
from ultralytics.utils import DEFAULT_CFG, LOGGER, RANK
|
||||
from ultralytics.utils.torch_utils import unwrap_model
|
||||
|
||||
|
||||
def on_pretrain_routine_end(trainer) -> None:
|
||||
"""Set up model classes and text encoder at the end of the pretrain routine."""
|
||||
if RANK in {-1, 0}:
|
||||
# Set class names for evaluation
|
||||
names = [name.split("/", 1)[0] for name in list(trainer.test_loader.dataset.data["names"].values())]
|
||||
unwrap_model(trainer.ema.ema).set_classes(names, cache_clip_model=False)
|
||||
|
||||
|
||||
class WorldTrainer(DetectionTrainer):
|
||||
"""A trainer class for fine-tuning YOLO World models on close-set datasets.
|
||||
|
||||
This trainer extends the DetectionTrainer to support training YOLO World models, which combine visual and textual
|
||||
features for improved object detection and understanding. It handles text embedding generation and caching to
|
||||
accelerate training with multi-modal data.
|
||||
|
||||
Attributes:
|
||||
text_embeddings (dict[str, torch.Tensor] | None): Cached text embeddings for category names to accelerate
|
||||
training.
|
||||
model (WorldModel): The YOLO World model being trained.
|
||||
data (dict[str, Any]): Dataset configuration containing class information.
|
||||
args (Any): Training arguments and configuration.
|
||||
|
||||
Methods:
|
||||
get_model: Return WorldModel initialized with specified config and weights.
|
||||
build_dataset: Build YOLO Dataset for training or validation.
|
||||
set_text_embeddings: Set text embeddings for datasets to accelerate training.
|
||||
generate_text_embeddings: Generate text embeddings for a list of text samples.
|
||||
preprocess_batch: Preprocess a batch of images and text for YOLOWorld training.
|
||||
|
||||
Examples:
|
||||
Initialize and train a YOLO World model
|
||||
>>> from ultralytics.models.yolo.world import WorldTrainer
|
||||
>>> args = dict(model="yolov8s-world.pt", data="coco8.yaml", epochs=3)
|
||||
>>> trainer = WorldTrainer(overrides=args)
|
||||
>>> trainer.train()
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict[str, Any] | None = None, _callbacks=None):
|
||||
"""Initialize a WorldTrainer object with given arguments.
|
||||
|
||||
Args:
|
||||
cfg (dict[str, Any]): Configuration for the trainer.
|
||||
overrides (dict[str, Any], optional): Configuration overrides.
|
||||
_callbacks (list[Any], optional): List of callback functions.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
assert not overrides.get("compile"), f"Training with 'model={overrides['model']}' requires 'compile=False'"
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
self.text_embeddings = None
|
||||
|
||||
def get_model(self, cfg=None, weights: str | None = None, verbose: bool = True) -> WorldModel:
|
||||
"""Return WorldModel initialized with specified config and weights.
|
||||
|
||||
Args:
|
||||
cfg (dict[str, Any] | str, optional): Model configuration.
|
||||
weights (str, optional): Path to pretrained weights.
|
||||
verbose (bool): Whether to display model info.
|
||||
|
||||
Returns:
|
||||
(WorldModel): Initialized WorldModel.
|
||||
"""
|
||||
# NOTE: This `nc` here is the max number of different text samples in one image, rather than the actual `nc`.
|
||||
# NOTE: Following the official config, nc hard-coded to 80 for now.
|
||||
model = WorldModel(
|
||||
cfg["yaml_file"] if isinstance(cfg, dict) else cfg,
|
||||
ch=self.data["channels"],
|
||||
nc=min(self.data["nc"], 80),
|
||||
verbose=verbose and RANK == -1,
|
||||
)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
self.add_callback("on_pretrain_routine_end", on_pretrain_routine_end)
|
||||
|
||||
return model
|
||||
|
||||
def build_dataset(self, img_path: str, mode: str = "train", batch: int | None = None):
|
||||
"""Build YOLO Dataset for training or validation.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the folder containing images.
|
||||
mode (str): `train` mode or `val` mode, users are able to customize different augmentations for each mode.
|
||||
batch (int, optional): Size of batches, this is for `rect`.
|
||||
|
||||
Returns:
|
||||
(Any): YOLO dataset configured for training or validation.
|
||||
"""
|
||||
gs = max(int(unwrap_model(self.model).stride.max() if self.model else 0), 32)
|
||||
dataset = build_yolo_dataset(
|
||||
self.args, img_path, batch, self.data, mode=mode, rect=mode == "val", stride=gs, multi_modal=mode == "train"
|
||||
)
|
||||
if mode == "train":
|
||||
self.set_text_embeddings([dataset], batch) # cache text embeddings to accelerate training
|
||||
return dataset
|
||||
|
||||
def set_text_embeddings(self, datasets: list[Any], batch: int | None) -> None:
|
||||
"""Set text embeddings for datasets to accelerate training by caching category names.
|
||||
|
||||
This method collects unique category names from all datasets, then generates and caches text embeddings for
|
||||
these categories to improve training efficiency.
|
||||
|
||||
Args:
|
||||
datasets (list[Any]): List of datasets from which to extract category names.
|
||||
batch (int | None): Batch size used for processing.
|
||||
|
||||
Notes:
|
||||
This method collects category names from datasets that have the 'category_names' attribute,
|
||||
then uses the first dataset's image path to determine where to cache the generated text embeddings.
|
||||
"""
|
||||
text_embeddings = {}
|
||||
for dataset in datasets:
|
||||
if not hasattr(dataset, "category_names"):
|
||||
continue
|
||||
text_embeddings.update(
|
||||
self.generate_text_embeddings(
|
||||
list(dataset.category_names), batch, cache_dir=Path(dataset.img_path).parent
|
||||
)
|
||||
)
|
||||
self.text_embeddings = text_embeddings
|
||||
|
||||
def generate_text_embeddings(self, texts: list[str], batch: int, cache_dir: Path) -> dict[str, torch.Tensor]:
|
||||
"""Generate text embeddings for a list of text samples.
|
||||
|
||||
Args:
|
||||
texts (list[str]): List of text samples to encode.
|
||||
batch (int): Batch size for processing.
|
||||
cache_dir (Path): Directory to save/load cached embeddings.
|
||||
|
||||
Returns:
|
||||
(dict[str, torch.Tensor]): Dictionary mapping text samples to their embeddings.
|
||||
"""
|
||||
model = "clip:ViT-B/32"
|
||||
cache_path = cache_dir / f"text_embeddings_{model.replace(':', '_').replace('/', '_')}.pt"
|
||||
if cache_path.exists():
|
||||
LOGGER.info(f"Reading existed cache from '{cache_path}'")
|
||||
txt_map = torch.load(cache_path, map_location=self.device)
|
||||
if sorted(txt_map.keys()) == sorted(texts):
|
||||
return txt_map
|
||||
LOGGER.info(f"Caching text embeddings to '{cache_path}'")
|
||||
assert self.model is not None
|
||||
txt_feats = unwrap_model(self.model).get_text_pe(texts, batch, cache_clip_model=False)
|
||||
txt_map = dict(zip(texts, txt_feats.squeeze(0)))
|
||||
torch.save(txt_map, cache_path)
|
||||
return txt_map
|
||||
|
||||
def preprocess_batch(self, batch: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Preprocess a batch of images and text for YOLOWorld training."""
|
||||
batch = DetectionTrainer.preprocess_batch(self, batch)
|
||||
|
||||
# Add text features
|
||||
texts = list(itertools.chain(*batch["texts"]))
|
||||
txt_feats = torch.stack([self.text_embeddings[text] for text in texts]).to(
|
||||
self.device, non_blocking=self.device.type == "cuda"
|
||||
)
|
||||
batch["txt_feats"] = txt_feats.reshape(len(batch["texts"]), -1, txt_feats.shape[-1])
|
||||
return batch
|
||||
197
ultralytics/models/yolo/world/train_world.py
Executable file
197
ultralytics/models/yolo/world/train_world.py
Executable file
@@ -0,0 +1,197 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ultralytics.data import YOLOConcatDataset, build_grounding, build_yolo_dataset
|
||||
from ultralytics.data.utils import check_det_dataset
|
||||
from ultralytics.models.yolo.world import WorldTrainer
|
||||
from ultralytics.utils import DATASETS_DIR, DEFAULT_CFG, LOGGER
|
||||
from ultralytics.utils.checks import check_file
|
||||
from ultralytics.utils.torch_utils import unwrap_model
|
||||
|
||||
|
||||
class WorldTrainerFromScratch(WorldTrainer):
|
||||
"""A class extending the WorldTrainer for training a world model from scratch on open-set datasets.
|
||||
|
||||
This trainer specializes in handling mixed datasets including both object detection and grounding datasets,
|
||||
supporting training YOLO-World models with combined vision-language capabilities.
|
||||
|
||||
Attributes:
|
||||
cfg (dict): Configuration dictionary with default parameters for model training.
|
||||
overrides (dict): Dictionary of parameter overrides to customize the configuration.
|
||||
_callbacks (list): List of callback functions to be executed during different stages of training.
|
||||
data (dict): Final processed data configuration containing train/val paths and metadata.
|
||||
training_data (dict): Dictionary mapping training dataset paths to their configurations.
|
||||
|
||||
Methods:
|
||||
build_dataset: Build YOLO Dataset for training or validation with mixed dataset support.
|
||||
get_dataset: Get train and validation paths from data dictionary.
|
||||
plot_training_labels: Skip label plotting for YOLO-World training.
|
||||
final_eval: Perform final evaluation and validation for the YOLO-World model.
|
||||
|
||||
Examples:
|
||||
>>> from ultralytics.models.yolo.world.train_world import WorldTrainerFromScratch
|
||||
>>> from ultralytics import YOLOWorld
|
||||
>>> data = dict(
|
||||
... train=dict(
|
||||
... yolo_data=["Objects365.yaml"],
|
||||
... grounding_data=[
|
||||
... dict(
|
||||
... img_path="flickr30k/images",
|
||||
... json_file="flickr30k/final_flickr_separateGT_train.json",
|
||||
... ),
|
||||
... dict(
|
||||
... img_path="GQA/images",
|
||||
... json_file="GQA/final_mixed_train_no_coco.json",
|
||||
... ),
|
||||
... ],
|
||||
... ),
|
||||
... val=dict(yolo_data=["lvis.yaml"]),
|
||||
... )
|
||||
>>> model = YOLOWorld("yolov8s-worldv2.yaml")
|
||||
>>> model.train(data=data, trainer=WorldTrainerFromScratch)
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
||||
"""Initialize a WorldTrainerFromScratch object.
|
||||
|
||||
This initializes a trainer for YOLO-World models from scratch, supporting mixed datasets including both object
|
||||
detection and grounding datasets for vision-language capabilities.
|
||||
|
||||
Args:
|
||||
cfg (dict): Configuration dictionary with default parameters for model training.
|
||||
overrides (dict, optional): Dictionary of parameter overrides to customize the configuration.
|
||||
_callbacks (list, optional): List of callback functions to be executed during different stages of training.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def build_dataset(self, img_path, mode="train", batch=None):
|
||||
"""Build YOLO Dataset for training or validation.
|
||||
|
||||
This method constructs appropriate datasets based on the mode and input paths, handling both standard YOLO
|
||||
datasets and grounding datasets with different formats.
|
||||
|
||||
Args:
|
||||
img_path (list[str] | str): Path to the folder containing images or list of paths.
|
||||
mode (str): 'train' mode or 'val' mode, allowing customized augmentations for each mode.
|
||||
batch (int, optional): Size of batches, used for rectangular training/validation.
|
||||
|
||||
Returns:
|
||||
(YOLOConcatDataset | Dataset): The constructed dataset for training or validation.
|
||||
"""
|
||||
gs = max(int(unwrap_model(self.model).stride.max() if self.model else 0), 32)
|
||||
if mode != "train":
|
||||
return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=False, stride=gs)
|
||||
datasets = [
|
||||
build_yolo_dataset(self.args, im_path, batch, self.training_data[im_path], stride=gs, multi_modal=True)
|
||||
if isinstance(im_path, str)
|
||||
else build_grounding(
|
||||
# assign `nc` from validation set to max number of text samples for training consistency
|
||||
self.args,
|
||||
im_path["img_path"],
|
||||
im_path["json_file"],
|
||||
batch,
|
||||
stride=gs,
|
||||
max_samples=self.data["nc"],
|
||||
)
|
||||
for im_path in img_path
|
||||
]
|
||||
self.set_text_embeddings(datasets, batch) # cache text embeddings to accelerate training
|
||||
return YOLOConcatDataset(datasets) if len(datasets) > 1 else datasets[0]
|
||||
|
||||
@staticmethod
|
||||
def check_data_config(data: dict | str | Path) -> dict:
|
||||
"""Check and load the data configuration from a YAML file or dictionary.
|
||||
|
||||
Args:
|
||||
data (dict | str | Path): Data configuration as a dictionary or path to a YAML file.
|
||||
|
||||
Returns:
|
||||
(dict): Data configuration dictionary loaded from YAML file or passed directly.
|
||||
"""
|
||||
# If string, load from YAML file
|
||||
if not isinstance(data, dict):
|
||||
from ultralytics.utils import YAML
|
||||
|
||||
return YAML.load(check_file(data))
|
||||
return data
|
||||
|
||||
def get_dataset(self):
|
||||
"""Get train and validation paths from data dictionary.
|
||||
|
||||
Processes the data configuration to extract paths for training and validation datasets, handling both YOLO
|
||||
detection datasets and grounding datasets.
|
||||
|
||||
Returns:
|
||||
(dict): Final processed data configuration containing train/val paths and metadata.
|
||||
|
||||
Raises:
|
||||
AssertionError: If train or validation datasets are not found, or if validation has multiple datasets.
|
||||
"""
|
||||
final_data = {}
|
||||
self.args.data = data_yaml = self.check_data_config(self.args.data)
|
||||
assert data_yaml.get("train", False), "train dataset not found" # object365.yaml
|
||||
assert data_yaml.get("val", False), "validation dataset not found" # lvis.yaml
|
||||
data = {k: [check_det_dataset(d) for d in v.get("yolo_data", [])] for k, v in data_yaml.items()}
|
||||
assert len(data["val"]) == 1, f"Only support validating on 1 dataset for now, but got {len(data['val'])}."
|
||||
val_split = "minival" if "lvis" in data["val"][0]["val"] else "val"
|
||||
for d in data["val"]:
|
||||
if d.get("minival") is None: # for lvis dataset
|
||||
continue
|
||||
d["minival"] = str(d["path"] / d["minival"])
|
||||
for s in {"train", "val"}:
|
||||
final_data[s] = [d["train" if s == "train" else val_split] for d in data[s]]
|
||||
# save grounding data if there's one
|
||||
grounding_data = data_yaml[s].get("grounding_data")
|
||||
if grounding_data is None:
|
||||
continue
|
||||
grounding_data = grounding_data if isinstance(grounding_data, list) else [grounding_data]
|
||||
for g in grounding_data:
|
||||
assert isinstance(g, dict), f"Grounding data should be provided in dict format, but got {type(g)}"
|
||||
for k in {"img_path", "json_file"}:
|
||||
path = Path(g[k])
|
||||
if not path.exists() and not path.is_absolute():
|
||||
g[k] = str((DATASETS_DIR / g[k]).resolve()) # path relative to DATASETS_DIR
|
||||
final_data[s] += grounding_data
|
||||
# assign the first val dataset as currently only one validation set is supported
|
||||
data["val"] = data["val"][0]
|
||||
final_data["val"] = final_data["val"][0]
|
||||
# NOTE: to make training work properly, set `nc` and `names`
|
||||
final_data["nc"] = data["val"]["nc"]
|
||||
final_data["names"] = data["val"]["names"]
|
||||
# NOTE: add path with lvis path
|
||||
final_data["path"] = data["val"]["path"]
|
||||
final_data["channels"] = data["val"]["channels"]
|
||||
self.data = final_data
|
||||
if self.args.single_cls: # consistent with base trainer
|
||||
LOGGER.info("Overriding class names with single class.")
|
||||
self.data["names"] = {0: "object"}
|
||||
self.data["nc"] = 1
|
||||
self.training_data = {}
|
||||
for d in data["train"]:
|
||||
if self.args.single_cls:
|
||||
d["names"] = {0: "object"}
|
||||
d["nc"] = 1
|
||||
self.training_data[d["train"]] = d
|
||||
return final_data
|
||||
|
||||
def plot_training_labels(self):
|
||||
"""Skip label plotting for YOLO-World training."""
|
||||
pass
|
||||
|
||||
def final_eval(self):
|
||||
"""Perform final evaluation and validation for the YOLO-World model.
|
||||
|
||||
Configures the validator with appropriate dataset and split information before running evaluation.
|
||||
|
||||
Returns:
|
||||
(dict): Dictionary containing evaluation metrics and results.
|
||||
"""
|
||||
val = self.args.data["val"]["yolo_data"][0]
|
||||
self.validator.args.data = val
|
||||
self.validator.args.split = "minival" if isinstance(val, str) and "lvis" in val else "val"
|
||||
return super().final_eval()
|
||||
22
ultralytics/models/yolo/yoloe/__init__.py
Executable file
22
ultralytics/models/yolo/yoloe/__init__.py
Executable file
@@ -0,0 +1,22 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from .predict import YOLOEVPDetectPredictor, YOLOEVPSegPredictor
|
||||
from .train import YOLOEPEFreeTrainer, YOLOEPETrainer, YOLOETrainer, YOLOETrainerFromScratch, YOLOEVPTrainer
|
||||
from .train_seg import YOLOEPESegTrainer, YOLOESegTrainer, YOLOESegTrainerFromScratch, YOLOESegVPTrainer
|
||||
from .val import YOLOEDetectValidator, YOLOESegValidator
|
||||
|
||||
__all__ = [
|
||||
"YOLOEDetectValidator",
|
||||
"YOLOEPEFreeTrainer",
|
||||
"YOLOEPESegTrainer",
|
||||
"YOLOEPETrainer",
|
||||
"YOLOESegTrainer",
|
||||
"YOLOESegTrainerFromScratch",
|
||||
"YOLOESegVPTrainer",
|
||||
"YOLOESegValidator",
|
||||
"YOLOETrainer",
|
||||
"YOLOETrainerFromScratch",
|
||||
"YOLOEVPDetectPredictor",
|
||||
"YOLOEVPSegPredictor",
|
||||
"YOLOEVPTrainer",
|
||||
]
|
||||
162
ultralytics/models/yolo/yoloe/predict.py
Executable file
162
ultralytics/models/yolo/yoloe/predict.py
Executable file
@@ -0,0 +1,162 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ultralytics.data.augment import LoadVisualPrompt
|
||||
from ultralytics.models.yolo.detect import DetectionPredictor
|
||||
from ultralytics.models.yolo.segment import SegmentationPredictor
|
||||
|
||||
|
||||
class YOLOEVPDetectPredictor(DetectionPredictor):
|
||||
"""A class extending DetectionPredictor for YOLO-EVP (Enhanced Visual Prompting) predictions.
|
||||
|
||||
This class provides common functionality for YOLO models that use visual prompting, including model setup, prompt
|
||||
handling, and preprocessing transformations.
|
||||
|
||||
Attributes:
|
||||
model (torch.nn.Module): The YOLO model for inference.
|
||||
device (torch.device): Device to run the model on (CPU or CUDA).
|
||||
prompts (dict | torch.Tensor): Visual prompts containing class indices and bounding boxes or masks.
|
||||
|
||||
Methods:
|
||||
setup_model: Initialize the YOLO model and set it to evaluation mode.
|
||||
set_prompts: Set the visual prompts for the model.
|
||||
pre_transform: Preprocess images and prompts before inference.
|
||||
inference: Run inference with visual prompts.
|
||||
get_vpe: Process source to get visual prompt embeddings.
|
||||
"""
|
||||
|
||||
def setup_model(self, model, verbose: bool = True):
|
||||
"""Set up the model for prediction.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Model to load or use.
|
||||
verbose (bool, optional): If True, provides detailed logging.
|
||||
"""
|
||||
super().setup_model(model, verbose=verbose)
|
||||
self.done_warmup = True
|
||||
|
||||
def set_prompts(self, prompts):
|
||||
"""Set the visual prompts for the model.
|
||||
|
||||
Args:
|
||||
prompts (dict): Dictionary containing class indices and bounding boxes or masks. Must include a 'cls' key
|
||||
with class indices.
|
||||
"""
|
||||
self.prompts = prompts
|
||||
|
||||
def pre_transform(self, im):
|
||||
"""Preprocess images and prompts before inference.
|
||||
|
||||
This method applies letterboxing to the input image and transforms the visual prompts (bounding boxes or masks)
|
||||
accordingly.
|
||||
|
||||
Args:
|
||||
im (list): List of input images.
|
||||
|
||||
Returns:
|
||||
(list): Preprocessed images ready for model inference.
|
||||
|
||||
Raises:
|
||||
ValueError: If neither valid bounding boxes nor masks are provided in the prompts.
|
||||
"""
|
||||
img = super().pre_transform(im)
|
||||
bboxes = self.prompts.pop("bboxes", None)
|
||||
masks = self.prompts.pop("masks", None)
|
||||
category = self.prompts["cls"]
|
||||
if len(img) == 1:
|
||||
visuals = self._process_single_image(img[0].shape[:2], im[0].shape[:2], category, bboxes, masks)
|
||||
prompts = visuals.unsqueeze(0).to(self.device) # (1, N, H, W)
|
||||
else:
|
||||
# NOTE: only supports bboxes as prompts for now
|
||||
assert bboxes is not None, f"Expected bboxes, but got {bboxes}!"
|
||||
# NOTE: needs list[np.ndarray]
|
||||
assert isinstance(bboxes, list) and all(isinstance(b, np.ndarray) for b in bboxes), (
|
||||
f"Expected list[np.ndarray], but got {bboxes}!"
|
||||
)
|
||||
assert isinstance(category, list) and all(isinstance(b, np.ndarray) for b in category), (
|
||||
f"Expected list[np.ndarray], but got {category}!"
|
||||
)
|
||||
assert len(im) == len(category) == len(bboxes), (
|
||||
f"Expected same length for all inputs, but got {len(im)}vs{len(category)}vs{len(bboxes)}!"
|
||||
)
|
||||
visuals = [
|
||||
self._process_single_image(img[i].shape[:2], im[i].shape[:2], category[i], bboxes[i])
|
||||
for i in range(len(img))
|
||||
]
|
||||
prompts = torch.nn.utils.rnn.pad_sequence(visuals, batch_first=True).to(self.device) # (B, N, H, W)
|
||||
self.prompts = prompts.half() if self.model.fp16 else prompts.float()
|
||||
return img
|
||||
|
||||
def _process_single_image(self, dst_shape, src_shape, category, bboxes=None, masks=None):
|
||||
"""Process a single image by resizing bounding boxes or masks and generating visuals.
|
||||
|
||||
Args:
|
||||
dst_shape (tuple): The target shape (height, width) of the image.
|
||||
src_shape (tuple): The original shape (height, width) of the image.
|
||||
category (list | np.ndarray): The category indices for visual prompts.
|
||||
bboxes (list | np.ndarray, optional): A list of bounding boxes in the format [x1, y1, x2, y2].
|
||||
masks (np.ndarray, optional): A list of masks corresponding to the image.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): The processed visuals for the image.
|
||||
|
||||
Raises:
|
||||
ValueError: If neither `bboxes` nor `masks` are provided.
|
||||
"""
|
||||
if bboxes is not None and len(bboxes):
|
||||
bboxes = np.array(bboxes, dtype=np.float32)
|
||||
if bboxes.ndim == 1:
|
||||
bboxes = bboxes[None, :]
|
||||
# Calculate scaling factor and adjust bounding boxes
|
||||
gain = min(dst_shape[0] / src_shape[0], dst_shape[1] / src_shape[1]) # gain = old / new
|
||||
bboxes *= gain
|
||||
bboxes[..., 0::2] += round((dst_shape[1] - src_shape[1] * gain) / 2 - 0.1)
|
||||
bboxes[..., 1::2] += round((dst_shape[0] - src_shape[0] * gain) / 2 - 0.1)
|
||||
elif masks is not None:
|
||||
# Resize and process masks
|
||||
resized_masks = super().pre_transform(masks)
|
||||
masks = np.stack(resized_masks) # (N, H, W)
|
||||
masks[masks == 114] = 0 # Reset padding values to 0
|
||||
else:
|
||||
raise ValueError("Please provide valid bboxes or masks")
|
||||
|
||||
# Generate visuals using the visual prompt loader
|
||||
return LoadVisualPrompt().get_visuals(category, dst_shape, bboxes, masks)
|
||||
|
||||
def inference(self, im, *args, **kwargs):
|
||||
"""Run inference with visual prompts.
|
||||
|
||||
Args:
|
||||
im (torch.Tensor): Input image tensor.
|
||||
*args (Any): Variable length argument list.
|
||||
**kwargs (Any): Arbitrary keyword arguments.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Model prediction results.
|
||||
"""
|
||||
return super().inference(im, vpe=self.prompts, *args, **kwargs)
|
||||
|
||||
def get_vpe(self, source):
|
||||
"""Process the source to get the visual prompt embeddings (VPE).
|
||||
|
||||
Args:
|
||||
source (str | Path | int | PIL.Image | np.ndarray | torch.Tensor | list | tuple): The source of the image to
|
||||
make predictions on. Accepts various types including file paths, URLs, PIL images, numpy arrays, and
|
||||
torch tensors.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): The visual prompt embeddings (VPE) from the model.
|
||||
"""
|
||||
self.setup_source(source)
|
||||
assert len(self.dataset) == 1, "get_vpe only supports one image!"
|
||||
for _, im0s, _ in self.dataset:
|
||||
im = self.preprocess(im0s)
|
||||
return self.model(im, vpe=self.prompts, return_vpe=True)
|
||||
|
||||
|
||||
class YOLOEVPSegPredictor(YOLOEVPDetectPredictor, SegmentationPredictor):
|
||||
"""Predictor for YOLO-EVP segmentation tasks combining detection and segmentation capabilities."""
|
||||
|
||||
pass
|
||||
283
ultralytics/models/yolo/yoloe/train.py
Executable file
283
ultralytics/models/yolo/yoloe/train.py
Executable file
@@ -0,0 +1,283 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy, deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics.data import YOLOConcatDataset, build_yolo_dataset
|
||||
from ultralytics.data.augment import LoadVisualPrompt
|
||||
from ultralytics.models.yolo.detect import DetectionTrainer, DetectionValidator
|
||||
from ultralytics.nn.tasks import YOLOEModel
|
||||
from ultralytics.utils import DEFAULT_CFG, LOGGER, RANK
|
||||
from ultralytics.utils.torch_utils import unwrap_model
|
||||
|
||||
from ..world.train_world import WorldTrainerFromScratch
|
||||
from .val import YOLOEDetectValidator
|
||||
|
||||
|
||||
class YOLOETrainer(DetectionTrainer):
|
||||
"""A trainer class for YOLOE object detection models.
|
||||
|
||||
This class extends DetectionTrainer to provide specialized training functionality for YOLOE models, including custom
|
||||
model initialization, validation, and dataset building with multi-modal support.
|
||||
|
||||
Attributes:
|
||||
loss_names (tuple): Names of loss components used during training.
|
||||
|
||||
Methods:
|
||||
get_model: Initialize and return a YOLOEModel with specified configuration.
|
||||
get_validator: Return a YOLOEDetectValidator for model validation.
|
||||
build_dataset: Build YOLO dataset with multi-modal support for training.
|
||||
"""
|
||||
|
||||
def __init__(self, cfg=DEFAULT_CFG, overrides: dict | None = None, _callbacks=None):
|
||||
"""Initialize the YOLOE Trainer with specified configurations.
|
||||
|
||||
Args:
|
||||
cfg (dict): Configuration dictionary with default training settings from DEFAULT_CFG.
|
||||
overrides (dict, optional): Dictionary of parameter overrides for the default configuration.
|
||||
_callbacks (list, optional): List of callback functions to be applied during training.
|
||||
"""
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
assert not overrides.get("compile"), f"Training with 'model={overrides['model']}' requires 'compile=False'"
|
||||
overrides["overlap_mask"] = False
|
||||
super().__init__(cfg, overrides, _callbacks)
|
||||
|
||||
def get_model(self, cfg=None, weights=None, verbose: bool = True):
|
||||
"""Return a YOLOEModel initialized with the specified configuration and weights.
|
||||
|
||||
Args:
|
||||
cfg (dict | str, optional): Model configuration. Can be a dictionary containing a 'yaml_file' key, a direct
|
||||
path to a YAML file, or None to use default configuration.
|
||||
weights (str | Path, optional): Path to pretrained weights file to load into the model.
|
||||
verbose (bool): Whether to display model information during initialization.
|
||||
|
||||
Returns:
|
||||
(YOLOEModel): The initialized YOLOE model.
|
||||
|
||||
Notes:
|
||||
- The number of classes (nc) is hard-coded to a maximum of 80 following the official configuration.
|
||||
- The nc parameter here represents the maximum number of different text samples in one image,
|
||||
rather than the actual number of classes.
|
||||
"""
|
||||
# NOTE: This `nc` here is the max number of different text samples in one image, rather than the actual `nc`.
|
||||
# NOTE: Following the official config, nc hard-coded to 80 for now.
|
||||
model = YOLOEModel(
|
||||
cfg["yaml_file"] if isinstance(cfg, dict) else cfg,
|
||||
ch=self.data["channels"],
|
||||
nc=min(self.data["nc"], 80),
|
||||
verbose=verbose and RANK == -1,
|
||||
)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
return model
|
||||
|
||||
def get_validator(self):
|
||||
"""Return a YOLOEDetectValidator for YOLOE model validation."""
|
||||
self.loss_names = "box", "cls", "dfl"
|
||||
return YOLOEDetectValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
|
||||
def build_dataset(self, img_path: str, mode: str = "train", batch: int | None = None):
|
||||
"""Build YOLO Dataset.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the folder containing images.
|
||||
mode (str): 'train' mode or 'val' mode, users are able to customize different augmentations for each mode.
|
||||
batch (int, optional): Size of batches, this is for rectangular training.
|
||||
|
||||
Returns:
|
||||
(Dataset): YOLO dataset configured for training or validation.
|
||||
"""
|
||||
gs = max(int(unwrap_model(self.model).stride.max() if self.model else 0), 32)
|
||||
return build_yolo_dataset(
|
||||
self.args, img_path, batch, self.data, mode=mode, rect=mode == "val", stride=gs, multi_modal=mode == "train"
|
||||
)
|
||||
|
||||
|
||||
class YOLOEPETrainer(DetectionTrainer):
|
||||
"""Fine-tune YOLOE model using linear probing approach.
|
||||
|
||||
This trainer freezes most model layers and only trains specific projection layers for efficient fine-tuning on new
|
||||
datasets while preserving pretrained features.
|
||||
|
||||
Methods:
|
||||
get_model: Initialize YOLOEModel with frozen layers except projection layers.
|
||||
"""
|
||||
|
||||
def get_model(self, cfg=None, weights=None, verbose: bool = True):
|
||||
"""Return YOLOEModel initialized with specified config and weights.
|
||||
|
||||
Args:
|
||||
cfg (dict | str, optional): Model configuration.
|
||||
weights (str, optional): Path to pretrained weights.
|
||||
verbose (bool): Whether to display model information.
|
||||
|
||||
Returns:
|
||||
(YOLOEModel): Initialized model with frozen layers except for specific projection layers.
|
||||
"""
|
||||
# NOTE: This `nc` here is the max number of different text samples in one image, rather than the actual `nc`.
|
||||
# NOTE: Following the official config, nc hard-coded to 80 for now.
|
||||
model = YOLOEModel(
|
||||
cfg["yaml_file"] if isinstance(cfg, dict) else cfg,
|
||||
ch=self.data["channels"],
|
||||
nc=self.data["nc"],
|
||||
verbose=verbose and RANK == -1,
|
||||
)
|
||||
|
||||
del model.model[-1].savpe
|
||||
|
||||
assert weights is not None, "Pretrained weights must be provided for linear probing."
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
model.eval()
|
||||
names = list(self.data["names"].values())
|
||||
# NOTE: `get_text_pe` related to text model and YOLOEDetect.reprta,
|
||||
# it'd get correct results as long as loading proper pretrained weights.
|
||||
tpe = model.get_text_pe(names)
|
||||
model.set_classes(names, tpe)
|
||||
model.model[-1].fuse(model.pe) # fuse text embeddings to classify head
|
||||
model.model[-1].cv3[0][2] = deepcopy(model.model[-1].cv3[0][2]).requires_grad_(True)
|
||||
model.model[-1].cv3[1][2] = deepcopy(model.model[-1].cv3[1][2]).requires_grad_(True)
|
||||
model.model[-1].cv3[2][2] = deepcopy(model.model[-1].cv3[2][2]).requires_grad_(True)
|
||||
|
||||
if getattr(model.model[-1], "one2one_cv3", None) is not None:
|
||||
model.model[-1].one2one_cv3[0][2] = deepcopy(model.model[-1].cv3[0][2]).requires_grad_(True)
|
||||
model.model[-1].one2one_cv3[1][2] = deepcopy(model.model[-1].cv3[1][2]).requires_grad_(True)
|
||||
model.model[-1].one2one_cv3[2][2] = deepcopy(model.model[-1].cv3[2][2]).requires_grad_(True)
|
||||
|
||||
model.train()
|
||||
|
||||
return model
|
||||
|
||||
|
||||
class YOLOETrainerFromScratch(YOLOETrainer, WorldTrainerFromScratch):
|
||||
"""Train YOLOE models from scratch with text embedding support.
|
||||
|
||||
This trainer combines YOLOE training capabilities with world training features, enabling training from scratch with
|
||||
text embeddings and grounding datasets.
|
||||
|
||||
Methods:
|
||||
build_dataset: Build datasets for training with grounding support.
|
||||
generate_text_embeddings: Generate and cache text embeddings for training.
|
||||
"""
|
||||
|
||||
def build_dataset(self, img_path: list[str] | str, mode: str = "train", batch: int | None = None):
|
||||
"""Build YOLO Dataset for training or validation.
|
||||
|
||||
This method constructs appropriate datasets based on the mode and input paths, handling both standard YOLO
|
||||
datasets and grounding datasets with different formats.
|
||||
|
||||
Args:
|
||||
img_path (list[str] | str): Path to the folder containing images or list of paths.
|
||||
mode (str): 'train' mode or 'val' mode, allowing customized augmentations for each mode.
|
||||
batch (int, optional): Size of batches, used for rectangular training/validation.
|
||||
|
||||
Returns:
|
||||
(YOLOConcatDataset | Dataset): The constructed dataset for training or validation.
|
||||
"""
|
||||
return WorldTrainerFromScratch.build_dataset(self, img_path, mode, batch)
|
||||
|
||||
def generate_text_embeddings(self, texts: list[str], batch: int, cache_dir: Path):
|
||||
"""Generate text embeddings for a list of text samples.
|
||||
|
||||
Args:
|
||||
texts (list[str]): List of text samples to encode.
|
||||
batch (int): Batch size for processing.
|
||||
cache_dir (Path): Directory to save/load cached embeddings.
|
||||
|
||||
Returns:
|
||||
(dict): Dictionary mapping text samples to their embeddings.
|
||||
"""
|
||||
model = unwrap_model(self.model).text_model
|
||||
cache_path = cache_dir / f"text_embeddings_{model.replace(':', '_').replace('/', '_')}.pt"
|
||||
if cache_path.exists():
|
||||
LOGGER.info(f"Reading existed cache from '{cache_path}'")
|
||||
txt_map = torch.load(cache_path, map_location=self.device)
|
||||
if sorted(txt_map.keys()) == sorted(texts):
|
||||
return txt_map
|
||||
LOGGER.info(f"Caching text embeddings to '{cache_path}'")
|
||||
txt_feats = unwrap_model(self.model).get_text_pe(texts, batch, without_reprta=True, cache_clip_model=False)
|
||||
txt_map = dict(zip(texts, txt_feats.squeeze(0)))
|
||||
torch.save(txt_map, cache_path)
|
||||
return txt_map
|
||||
|
||||
|
||||
class YOLOEPEFreeTrainer(YOLOEPETrainer, YOLOETrainerFromScratch):
|
||||
"""Train prompt-free YOLOE model.
|
||||
|
||||
This trainer combines linear probing capabilities with from-scratch training for prompt-free YOLOE models that don't
|
||||
require text prompts during inference.
|
||||
|
||||
Methods:
|
||||
get_validator: Return standard DetectionValidator for validation.
|
||||
preprocess_batch: Preprocess batches without text features.
|
||||
set_text_embeddings: Set text embeddings for datasets (no-op for prompt-free).
|
||||
"""
|
||||
|
||||
def get_validator(self):
|
||||
"""Return a DetectionValidator for YOLO model validation."""
|
||||
self.loss_names = "box", "cls", "dfl"
|
||||
return DetectionValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
|
||||
def preprocess_batch(self, batch):
|
||||
"""Preprocess a batch of images for YOLOE training, adjusting formatting and dimensions as needed."""
|
||||
return DetectionTrainer.preprocess_batch(self, batch)
|
||||
|
||||
def set_text_embeddings(self, datasets, batch: int):
|
||||
"""No-op override for prompt-free training that does not require text embeddings.
|
||||
|
||||
Args:
|
||||
datasets (list[Dataset]): List of datasets containing category names to process.
|
||||
batch (int): Batch size for processing text embeddings.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class YOLOEVPTrainer(YOLOETrainerFromScratch):
|
||||
"""Train YOLOE model with visual prompts.
|
||||
|
||||
This trainer extends YOLOETrainerFromScratch to support visual prompt-based training, where visual cues are provided
|
||||
alongside images to guide the detection process.
|
||||
|
||||
Methods:
|
||||
build_dataset: Build dataset with visual prompt loading transforms.
|
||||
"""
|
||||
|
||||
def build_dataset(self, img_path: list[str] | str, mode: str = "train", batch: int | None = None):
|
||||
"""Build YOLO Dataset for training or validation with visual prompts.
|
||||
|
||||
Args:
|
||||
img_path (list[str] | str): Path to the folder containing images or list of paths.
|
||||
mode (str): 'train' mode or 'val' mode, allowing customized augmentations for each mode.
|
||||
batch (int, optional): Size of batches, used for rectangular training/validation.
|
||||
|
||||
Returns:
|
||||
(YOLOConcatDataset | Dataset): YOLO dataset configured for training or validation, with visual prompts for
|
||||
training mode.
|
||||
"""
|
||||
dataset = super().build_dataset(img_path, mode, batch)
|
||||
if isinstance(dataset, YOLOConcatDataset):
|
||||
for d in dataset.datasets:
|
||||
d.transforms.append(LoadVisualPrompt())
|
||||
else:
|
||||
dataset.transforms.append(LoadVisualPrompt())
|
||||
return dataset
|
||||
|
||||
def _close_dataloader_mosaic(self):
|
||||
"""Close mosaic augmentation and add visual prompt loading to the training dataset."""
|
||||
super()._close_dataloader_mosaic()
|
||||
if isinstance(self.train_loader.dataset, YOLOConcatDataset):
|
||||
for d in self.train_loader.dataset.datasets:
|
||||
d.transforms.append(LoadVisualPrompt())
|
||||
else:
|
||||
self.train_loader.dataset.transforms.append(LoadVisualPrompt())
|
||||
127
ultralytics/models/yolo/yoloe/train_seg.py
Executable file
127
ultralytics/models/yolo/yoloe/train_seg.py
Executable file
@@ -0,0 +1,127 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from copy import copy, deepcopy
|
||||
|
||||
from ultralytics.models.yolo.segment import SegmentationTrainer
|
||||
from ultralytics.nn.tasks import YOLOESegModel
|
||||
from ultralytics.utils import RANK
|
||||
|
||||
from .train import YOLOETrainer, YOLOETrainerFromScratch, YOLOEVPTrainer
|
||||
from .val import YOLOESegValidator
|
||||
|
||||
|
||||
class YOLOESegTrainer(YOLOETrainer, SegmentationTrainer):
|
||||
"""Trainer class for YOLOE segmentation models.
|
||||
|
||||
This class combines YOLOETrainer and SegmentationTrainer to provide training functionality specifically for YOLOE
|
||||
segmentation models, enabling both object detection and instance segmentation capabilities.
|
||||
|
||||
Attributes:
|
||||
cfg (dict): Configuration dictionary with training parameters.
|
||||
overrides (dict): Dictionary with parameter overrides.
|
||||
_callbacks (list): List of callback functions for training events.
|
||||
"""
|
||||
|
||||
def get_model(self, cfg=None, weights=None, verbose=True):
|
||||
"""Return YOLOESegModel initialized with specified config and weights.
|
||||
|
||||
Args:
|
||||
cfg (dict | str, optional): Model configuration dictionary or YAML file path.
|
||||
weights (str, optional): Path to pretrained weights file.
|
||||
verbose (bool): Whether to display model information.
|
||||
|
||||
Returns:
|
||||
(YOLOESegModel): Initialized YOLOE segmentation model.
|
||||
"""
|
||||
# NOTE: This `nc` here is the max number of different text samples in one image, rather than the actual `nc`.
|
||||
# NOTE: Following the official config, nc hard-coded to 80 for now.
|
||||
model = YOLOESegModel(
|
||||
cfg["yaml_file"] if isinstance(cfg, dict) else cfg,
|
||||
ch=self.data["channels"],
|
||||
nc=min(self.data["nc"], 80),
|
||||
verbose=verbose and RANK == -1,
|
||||
)
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
return model
|
||||
|
||||
def get_validator(self):
|
||||
"""Create and return a validator for YOLOE segmentation model evaluation.
|
||||
|
||||
Returns:
|
||||
(YOLOESegValidator): Validator for YOLOE segmentation models.
|
||||
"""
|
||||
self.loss_names = "box", "seg", "cls", "dfl"
|
||||
return YOLOESegValidator(
|
||||
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
||||
)
|
||||
|
||||
|
||||
class YOLOEPESegTrainer(SegmentationTrainer):
|
||||
"""Fine-tune YOLOESeg model in linear probing way.
|
||||
|
||||
This trainer specializes in fine-tuning YOLOESeg models using a linear probing approach, which involves freezing
|
||||
most of the model and only training specific layers for efficient adaptation to new tasks.
|
||||
|
||||
Attributes:
|
||||
data (dict): Dataset configuration containing channels, class names, and number of classes.
|
||||
"""
|
||||
|
||||
def get_model(self, cfg=None, weights=None, verbose=True):
|
||||
"""Return YOLOESegModel initialized with specified config and weights for linear probing.
|
||||
|
||||
Args:
|
||||
cfg (dict | str, optional): Model configuration dictionary or YAML file path.
|
||||
weights (str, optional): Path to pretrained weights file.
|
||||
verbose (bool): Whether to display model information.
|
||||
|
||||
Returns:
|
||||
(YOLOESegModel): Initialized YOLOE segmentation model configured for linear probing.
|
||||
"""
|
||||
# NOTE: This `nc` here is the max number of different text samples in one image, rather than the actual `nc`.
|
||||
# NOTE: Following the official config, nc hard-coded to 80 for now.
|
||||
model = YOLOESegModel(
|
||||
cfg["yaml_file"] if isinstance(cfg, dict) else cfg,
|
||||
ch=self.data["channels"],
|
||||
nc=self.data["nc"],
|
||||
verbose=verbose and RANK == -1,
|
||||
)
|
||||
|
||||
del model.model[-1].savpe
|
||||
|
||||
assert weights is not None, "Pretrained weights must be provided for linear probing."
|
||||
if weights:
|
||||
model.load(weights)
|
||||
|
||||
model.eval()
|
||||
names = list(self.data["names"].values())
|
||||
# NOTE: `get_text_pe` related to text model and YOLOEDetect.reprta,
|
||||
# it'd get correct results as long as loading proper pretrained weights.
|
||||
tpe = model.get_text_pe(names)
|
||||
model.set_classes(names, tpe)
|
||||
model.model[-1].fuse(model.pe)
|
||||
model.model[-1].cv3[0][2] = deepcopy(model.model[-1].cv3[0][2]).requires_grad_(True)
|
||||
model.model[-1].cv3[1][2] = deepcopy(model.model[-1].cv3[1][2]).requires_grad_(True)
|
||||
model.model[-1].cv3[2][2] = deepcopy(model.model[-1].cv3[2][2]).requires_grad_(True)
|
||||
|
||||
if getattr(model.model[-1], "one2one_cv3", None) is not None:
|
||||
model.model[-1].one2one_cv3[0][2] = deepcopy(model.model[-1].cv3[0][2]).requires_grad_(True)
|
||||
model.model[-1].one2one_cv3[1][2] = deepcopy(model.model[-1].cv3[1][2]).requires_grad_(True)
|
||||
model.model[-1].one2one_cv3[2][2] = deepcopy(model.model[-1].cv3[2][2]).requires_grad_(True)
|
||||
|
||||
model.train()
|
||||
|
||||
return model
|
||||
|
||||
|
||||
class YOLOESegTrainerFromScratch(YOLOETrainerFromScratch, YOLOESegTrainer):
|
||||
"""Trainer for YOLOE segmentation models trained from scratch without pretrained weights."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class YOLOESegVPTrainer(YOLOEVPTrainer, YOLOESegTrainerFromScratch):
|
||||
"""Trainer for YOLOE segmentation models with Vision Prompt (VP) capabilities."""
|
||||
|
||||
pass
|
||||
206
ultralytics/models/yolo/yoloe/val.py
Executable file
206
ultralytics/models/yolo/yoloe/val.py
Executable file
@@ -0,0 +1,206 @@
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
|
||||
from ultralytics.data import YOLOConcatDataset, build_dataloader, build_yolo_dataset
|
||||
from ultralytics.data.augment import LoadVisualPrompt
|
||||
from ultralytics.data.utils import check_det_dataset
|
||||
from ultralytics.models.yolo.detect import DetectionValidator
|
||||
from ultralytics.models.yolo.segment import SegmentationValidator
|
||||
from ultralytics.nn.modules.head import YOLOEDetect
|
||||
from ultralytics.nn.tasks import YOLOEModel
|
||||
from ultralytics.utils import LOGGER, TQDM
|
||||
from ultralytics.utils.torch_utils import select_device, smart_inference_mode
|
||||
|
||||
|
||||
class YOLOEDetectValidator(DetectionValidator):
|
||||
"""A validator class for YOLOE detection models that handles both text and visual prompt embeddings.
|
||||
|
||||
This class extends DetectionValidator to provide specialized validation functionality for YOLOE models. It supports
|
||||
validation using either text prompts or visual prompt embeddings extracted from training samples, enabling flexible
|
||||
evaluation strategies for prompt-based object detection.
|
||||
|
||||
Attributes:
|
||||
device (torch.device): The device on which validation is performed.
|
||||
args (namespace): Configuration arguments for validation.
|
||||
dataloader (DataLoader): DataLoader for validation data.
|
||||
|
||||
Methods:
|
||||
get_visual_pe: Extract visual prompt embeddings from training samples.
|
||||
preprocess: Preprocess batch data ensuring visuals are on the same device as images.
|
||||
get_vpe_dataloader: Create a dataloader for LVIS training visual prompt samples.
|
||||
__call__: Run validation using either text or visual prompt embeddings.
|
||||
|
||||
Examples:
|
||||
Validate with text prompts
|
||||
>>> validator = YOLOEDetectValidator()
|
||||
>>> stats = validator(model=model, load_vp=False)
|
||||
|
||||
Validate with visual prompts
|
||||
>>> stats = validator(model=model, refer_data="path/to/data.yaml", load_vp=True)
|
||||
"""
|
||||
|
||||
@smart_inference_mode()
|
||||
def get_visual_pe(self, dataloader: torch.utils.data.DataLoader, model: YOLOEModel) -> torch.Tensor:
|
||||
"""Extract visual prompt embeddings from training samples.
|
||||
|
||||
This method processes a dataloader to compute visual prompt embeddings for each class using a YOLOE model. It
|
||||
normalizes the embeddings and handles cases where no samples exist for a class by setting their embeddings to
|
||||
zero.
|
||||
|
||||
Args:
|
||||
dataloader (torch.utils.data.DataLoader): The dataloader providing training samples.
|
||||
model (YOLOEModel): The YOLOE model from which to extract visual prompt embeddings.
|
||||
|
||||
Returns:
|
||||
(torch.Tensor): Visual prompt embeddings with shape (1, num_classes, embed_dim).
|
||||
"""
|
||||
assert isinstance(model, YOLOEModel)
|
||||
names = [name.split("/", 1)[0] for name in list(dataloader.dataset.data["names"].values())]
|
||||
visual_pe = torch.zeros(len(names), model.model[-1].embed, device=self.device)
|
||||
cls_visual_num = torch.zeros(len(names))
|
||||
|
||||
desc = "Get visual prompt embeddings from samples"
|
||||
|
||||
# Count samples per class
|
||||
for batch in dataloader:
|
||||
cls = batch["cls"].squeeze(-1).to(torch.int).unique()
|
||||
count = torch.bincount(cls, minlength=len(names))
|
||||
cls_visual_num += count
|
||||
|
||||
cls_visual_num = cls_visual_num.to(self.device)
|
||||
|
||||
# Extract visual prompt embeddings
|
||||
pbar = TQDM(dataloader, total=len(dataloader), desc=desc)
|
||||
for batch in pbar:
|
||||
batch = self.preprocess(batch)
|
||||
preds = model.get_visual_pe(batch["img"], visual=batch["visuals"]) # (B, max_n, embed_dim)
|
||||
|
||||
batch_idx = batch["batch_idx"]
|
||||
for i in range(preds.shape[0]):
|
||||
cls = batch["cls"][batch_idx == i].squeeze(-1).to(torch.int).unique(sorted=True)
|
||||
pad_cls = torch.ones(preds.shape[1], device=self.device) * -1
|
||||
pad_cls[: cls.shape[0]] = cls
|
||||
for c in cls:
|
||||
visual_pe[c] += preds[i][pad_cls == c].sum(0) / cls_visual_num[c]
|
||||
|
||||
# Normalize embeddings for classes with samples, set others to zero
|
||||
visual_pe[cls_visual_num != 0] = F.normalize(visual_pe[cls_visual_num != 0], dim=-1, p=2)
|
||||
visual_pe[cls_visual_num == 0] = 0
|
||||
return visual_pe.unsqueeze(0)
|
||||
|
||||
def get_vpe_dataloader(self, data: dict[str, Any]) -> torch.utils.data.DataLoader:
|
||||
"""Create a dataloader for LVIS training visual prompt samples.
|
||||
|
||||
This method prepares a dataloader for visual prompt embeddings (VPE) using the specified dataset. It applies
|
||||
necessary transformations including LoadVisualPrompt and configurations to the dataset for validation purposes.
|
||||
|
||||
Args:
|
||||
data (dict): Dataset configuration dictionary containing paths and settings.
|
||||
|
||||
Returns:
|
||||
(torch.utils.data.DataLoader): The dataloader for visual prompt samples.
|
||||
"""
|
||||
dataset = build_yolo_dataset(
|
||||
self.args,
|
||||
data.get(self.args.split, data.get("val")),
|
||||
self.args.batch,
|
||||
data,
|
||||
mode="val",
|
||||
rect=False,
|
||||
)
|
||||
if isinstance(dataset, YOLOConcatDataset):
|
||||
for d in dataset.datasets:
|
||||
d.transforms.append(LoadVisualPrompt())
|
||||
else:
|
||||
dataset.transforms.append(LoadVisualPrompt())
|
||||
return build_dataloader(
|
||||
dataset,
|
||||
self.args.batch,
|
||||
self.args.workers,
|
||||
shuffle=False,
|
||||
rank=-1,
|
||||
)
|
||||
|
||||
@smart_inference_mode()
|
||||
def __call__(
|
||||
self,
|
||||
trainer: Any | None = None,
|
||||
model: YOLOEModel | str | None = None,
|
||||
refer_data: str | None = None,
|
||||
load_vp: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Run validation on the model using either text or visual prompt embeddings.
|
||||
|
||||
This method validates the model using either text prompts or visual prompts, depending on the load_vp flag. It
|
||||
supports validation during training (using a trainer object) or standalone validation with a provided model. For
|
||||
visual prompts, reference data can be specified to extract embeddings from a different dataset.
|
||||
|
||||
Args:
|
||||
trainer (object, optional): Trainer object containing the model and device.
|
||||
model (YOLOEModel | str, optional): Model to validate. Required if trainer is not provided.
|
||||
refer_data (str, optional): Path to reference data for visual prompts.
|
||||
load_vp (bool): Whether to load visual prompts. If False, text prompts are used.
|
||||
|
||||
Returns:
|
||||
(dict): Validation statistics containing metrics computed during validation.
|
||||
"""
|
||||
if trainer is not None:
|
||||
self.device = trainer.device
|
||||
model = trainer.ema.ema
|
||||
names = [name.split("/", 1)[0] for name in list(self.dataloader.dataset.data["names"].values())]
|
||||
|
||||
if load_vp:
|
||||
LOGGER.info("Validate using the visual prompt.")
|
||||
self.args.half = False
|
||||
# Directly use the same dataloader for visual embeddings extracted during training
|
||||
vpe = self.get_visual_pe(self.dataloader, model)
|
||||
model.set_classes(names, vpe)
|
||||
else:
|
||||
LOGGER.info("Validate using the text prompt.")
|
||||
tpe = model.get_text_pe(names)
|
||||
model.set_classes(names, tpe)
|
||||
stats = super().__call__(trainer, model)
|
||||
else:
|
||||
if refer_data is not None:
|
||||
assert load_vp, "Refer data is only used for visual prompt validation."
|
||||
self.device = select_device(self.args.device, verbose=False)
|
||||
|
||||
if isinstance(model, (str, Path)):
|
||||
from ultralytics.nn.tasks import load_checkpoint
|
||||
|
||||
model, _ = load_checkpoint(model, device=self.device) # model, ckpt
|
||||
model.eval().to(self.device)
|
||||
data = check_det_dataset(refer_data or self.args.data)
|
||||
names = [name.split("/", 1)[0] for name in list(data["names"].values())]
|
||||
|
||||
if load_vp:
|
||||
LOGGER.info("Validate using the visual prompt.")
|
||||
self.args.half = False
|
||||
# TODO: need to check if the names from refer data is consistent with the evaluated dataset
|
||||
# could use same dataset or refer to extract visual prompt embeddings
|
||||
dataloader = self.get_vpe_dataloader(data)
|
||||
vpe = self.get_visual_pe(dataloader, model)
|
||||
model.set_classes(names, vpe)
|
||||
stats = super().__call__(model=deepcopy(model))
|
||||
elif isinstance(model.model[-1], YOLOEDetect) and hasattr(model.model[-1], "lrpc"): # prompt-free
|
||||
return super().__call__(trainer, model)
|
||||
else:
|
||||
LOGGER.info("Validate using the text prompt.")
|
||||
tpe = model.get_text_pe(names)
|
||||
model.set_classes(names, tpe)
|
||||
stats = super().__call__(model=deepcopy(model))
|
||||
return stats
|
||||
|
||||
|
||||
class YOLOESegValidator(YOLOEDetectValidator, SegmentationValidator):
|
||||
"""YOLOE segmentation validator that supports both text and visual prompt embeddings."""
|
||||
|
||||
pass
|
||||
Reference in New Issue
Block a user