项目文件夹

文件
wehub-resource-sync 3a2c66702c
Tests on CPU (scheduled) / check-skip (push) Has been cancelled
Tests on CPU (scheduled) / pre-tests (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-ubuntu (float32) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-ubuntu (float64) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float64, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float64, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.13, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.13, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.11, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.11, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.12, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.12, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.13, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / coverage (push) Has been cancelled
Tests on CPU (scheduled) / typing (push) Has been cancelled
Tests on CPU (scheduled) / tutorials (push) Has been cancelled
Tests on CPU (scheduled) / docs (push) Has been cancelled
Lint / TOML Format (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:27 +08:00

410 行
16 KiB
Python

# LICENSE HEADER MANAGED BY add-license-header
#
# Copyright 2018 Kornia Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Based from the original code from Meta Platforms, Inc. and affiliates.
https://github.com/facebookresearch/segment-
anything/blob/3518c86b78b3bc9cf4fbe3d18e682fad1c79dc51/segment_anything/build_sam.py
https://github.com/facebookresearch/segment-
anything/blob/3518c86b78b3bc9cf4fbe3d18e682fad1c79dc51/segment_anything/modeling/sam.py
"""
from __future__ import annotations
import warnings
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional
import torch
from kornia.core.check import KORNIA_CHECK, KORNIA_CHECK_SHAPE
from kornia.core.mixin.onnx import ONNXExportMixin
from kornia.models.base import ModelBase
from kornia.models.sam.architecture.common import LayerNorm
from kornia.models.sam.architecture.image_encoder import ImageEncoderViT
from kornia.models.sam.architecture.mask_decoder import MaskDecoder
from kornia.models.sam.architecture.prompt_encoder import PromptEncoder
from kornia.models.sam.architecture.transformer import TwoWayTransformer
from kornia.models.structures import SegmentationResults
from kornia.models.tiny_vit import TinyViT
class SamModelType(Enum):
"""Map the SAM model types."""
vit_h = 0
vit_l = 1
vit_b = 2
mobile_sam = 3
@dataclass
class SamConfig:
"""Encapsulate the Config to build a SAM model.
Args:
model_type: the available models are:
- 0, 'vit_h' or :func:`kornia.contrib.sam.SamModelType.vit_h`
- 1, 'vit_l' or :func:`kornia.contrib.sam.SamModelType.vit_l`
- 2, 'vit_b' or :func:`kornia.contrib.sam.SamModelType.vit_b`
- 3, 'mobile_sam', or :func:`kornia.contrib.sam.SamModelType.mobile_sam`
checkpoint: URL or a path for a file with the weights of the model
encoder_embed_dim: Patch embedding dimension.
encoder_depth: Depth of ViT.
encoder_num_heads: Number of attention heads in each ViT block.
encoder_global_attn_indexes: Encoder indexes for blocks using global attention.
"""
model_type: Optional[str | int | SamModelType] = None
checkpoint: Optional[str] = None
pretrained: bool = False
encoder_embed_dim: Optional[int] = None
encoder_depth: Optional[int] = None
encoder_num_heads: Optional[int] = None
encoder_global_attn_indexes: Optional[tuple[int, ...]] = None
class Sam(ONNXExportMixin, ModelBase[SamConfig]):
"""Implement the Segment Anything Model (SAM) wrapper.
This class coordinates the image encoder, prompt encoder, and mask decoder.
"""
mask_threshold: float = 0.0
def __init__(
self, image_encoder: ImageEncoderViT | TinyViT, prompt_encoder: PromptEncoder, mask_decoder: MaskDecoder
) -> None:
"""SAM predicts object masks from an image and input prompts.
Args:
image_encoder: The backbone used to encode the image into image embeddings that allow for efficient mask
prediction.
prompt_encoder: Encodes various types of input prompts.
mask_decoder: Predicts masks from the image embeddings and encoded prompts.
"""
super().__init__()
self.image_encoder = image_encoder
self.prompt_encoder = prompt_encoder
self.mask_decoder = mask_decoder
@staticmethod
def from_name(name: str) -> Sam:
"""Build/load the SAM model based on it's name.
Args:
name: The name of the SAM model. Valid names are:
- 'vit_b'
- 'vit_l'
- 'vit_h'
- 'mobile_sam'
Returns:
The respective SAM model
"""
if name in ["vit_b", "vit_l", "vit_h", "mobile_sam"]:
return Sam.from_config(SamConfig(name))
else:
raise ValueError(f"Invalid SAM model name: {name}")
@staticmethod
def from_config(config: SamConfig) -> Sam:
"""Build/load the SAM model based on it's config.
Args:
config: The SamConfig data structure. If the model_type is available, build from it, otherwise will use
the parameters set.
Returns:
The respective SAM model
Example:
>>> from kornia.models.sam import SamConfig
>>> sam_model = Sam.from_config(SamConfig('vit_b'))
"""
model_type = config.model_type
if isinstance(model_type, int):
model_type = SamModelType(model_type)
elif isinstance(model_type, str):
_map_sam_type = {
"vit_h": SamModelType.vit_h,
"vit_l": SamModelType.vit_l,
"vit_b": SamModelType.vit_b,
"mobile_sam": SamModelType.mobile_sam,
}
model_type = _map_sam_type[model_type]
if model_type == SamModelType.vit_b:
model = _build_sam(
encoder_embed_dim=768, encoder_depth=12, encoder_num_heads=12, encoder_global_attn_indexes=(2, 5, 8, 11)
)
elif model_type == SamModelType.vit_l:
model = _build_sam(
encoder_embed_dim=1024,
encoder_depth=24,
encoder_num_heads=16,
encoder_global_attn_indexes=(5, 11, 17, 23),
)
elif model_type == SamModelType.vit_h:
model = _build_sam(
encoder_embed_dim=1280,
encoder_depth=32,
encoder_num_heads=16,
encoder_global_attn_indexes=(7, 15, 23, 31),
)
elif model_type == SamModelType.mobile_sam:
# TODO: merge this with _build_sam()
prompt_embed_dim = 256
image_size = 1024
vit_patch_size = 16
image_embedding_size = image_size // vit_patch_size
model = Sam(
image_encoder=TinyViT.from_config("5m", img_size=image_size, mobile_sam=True),
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],
)
elif (
isinstance(config.encoder_embed_dim, int)
and isinstance(config.encoder_depth, int)
and isinstance(config.encoder_num_heads, int)
and isinstance(config.encoder_global_attn_indexes, int)
):
model = _build_sam(
encoder_embed_dim=config.encoder_embed_dim,
encoder_depth=config.encoder_depth,
encoder_num_heads=config.encoder_num_heads,
encoder_global_attn_indexes=config.encoder_global_attn_indexes,
)
else:
raise NotImplementedError("Unexpected config. The model_type should be provide or the encoder configs.")
checkpoint = config.checkpoint
if config.pretrained:
if checkpoint is None:
checkpoint = {
SamModelType.vit_b: "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
SamModelType.vit_l: "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
SamModelType.vit_h: "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
SamModelType.mobile_sam: "https://github.com/ChaoningZhang/MobileSAM/raw/a509aac54fdd7af59f843135f2f7cee307283c88/weights/mobile_sam.pt",
}[model_type]
else:
warnings.warn("checkpoint is not None. pretrained=True is ignored", stacklevel=1)
if checkpoint:
model.load_checkpoint(checkpoint)
return model
def to_onnx(
self,
onnx_name: Optional[str] = None,
pseudo_shape: Optional[list[int]] = None,
save: bool = True,
**kwargs: Any,
) -> Any:
"""Export SAM's image encoder to ONNX.
SAM's full :meth:`forward` signature accepts non-tensor inputs
(``batched_prompts: list[dict]``, ``multimask_output: bool``) and returns
a Python list of :class:`~kornia.models.structures.SegmentationResults`,
which cannot be directly exported via :func:`torch.onnx.export`.
This override exports only the **image encoder** subgraph
(``self.image_encoder``), which is a pure ``(B, 3, H, W) -> (B, C, H', W')``
tensor-to-tensor module and fully ONNX-compatible. The encoder embeddings
can then be fed into a separate prompt-encoder / mask-decoder pipeline.
Args:
onnx_name: Path for the saved ``.onnx`` file. Defaults to
``"Kornia-Sam-ImageEncoder.onnx"``.
pseudo_shape: Concrete input shape used to trace the encoder, e.g.
``[1, 3, 1024, 1024]``. Defaults to ``[1, 3, 1024, 1024]``.
save: Whether to write the model to disk. Default ``True``.
**kwargs: Additional keyword arguments forwarded to
:func:`torch.onnx.export`.
Returns:
``onnx.ModelProto`` of the exported image encoder.
"""
if onnx_name is None:
onnx_name = "Kornia-Sam-ImageEncoder.onnx"
if pseudo_shape is None:
pseudo_shape = [1, 3, 1024, 1024]
kwargs.setdefault("output_names", ["image_embeddings"])
kwargs.setdefault(
"dynamic_axes",
{
"input": {0: "batch"},
"image_embeddings": {0: "batch"},
},
)
return super().to_onnx(
onnx_name=onnx_name,
input_shape=[-1, 3, -1, -1],
pseudo_shape=pseudo_shape,
model=self.image_encoder,
save=save,
**kwargs,
)
@torch.no_grad()
def forward(
self, images: torch.Tensor, batched_prompts: list[dict[str, Any]], multimask_output: bool
) -> list[SegmentationResults]:
"""Predicts masks end-to-end from provided images and prompts.
This method expects that the images have already been pre-processed, at least been normalized, resized and
padded to be compatible with the `self.image_encoder`.
.. note:: For each image :math:`(3, H, W)`, it is possible to input a batch (:math:`K`) of :math:`N` prompts,
the results are batched by the number of prompts batch. So given a prompt with :math:`K=5`, and
:math:`N=10`, the results will look like :math:`5xCxHxW` where :math:`C` is determined by
multimask_output. And within each of these masks :math:`(5xC)`, it should be possible to find
:math:`N` instances if the model succeed.
Args:
images: The image as a torch tensor in :math:`(B, 3, H, W)` format, already transformed for input to the
model.
batched_prompts: A list over the batch of images (list length should be :math:`B`), each a dictionary with
the following keys. If it does not have the respective prompt, it should not be included
in this dictionary. The options are:
- "points": tuple of (torch.Tensor, torch.Tensor) within the coordinate keypoints
and their respective labels. The tuple should look like (keypoints, labels), where the keypoints
(a tensor) are a batched point prompts for this image, with shape :math:`(K, N, 2)`. Already
transformed to the input frame of the model. The labels (a tensor) are a batched labels for point
prompts, with shape :math:`(K, N)`. Where 1 indicates a foreground point and 0 indicates a background
point.
- "boxes": (torch.Tensor) Batched box inputs, with shape :math:`(K, 4)`.
Already transformed to the input frame of the model.
- "mask_inputs": (torch.Tensor) Batched mask inputs to the model, in the form :math:`(K, 1, H, W)`.
multimask_output: Whether the model should predict multiple disambiguating masks, or return a single mask.
Returns:
A list over input images, where each element is as SegmentationResults the following:
- logits: Low resolution logits with shape :math:`(K, C, H, W)`. Can be passed as mask input to
subsequent iterations of prediction. Where :math:`K` is the number of input prompts,
:math:`C` is determined by multimask_output, and :math:`H=W=256` are the model output size.
- scores: The model's predictions of mask quality (iou prediction), in shape BxC.
"""
KORNIA_CHECK_SHAPE(images, ["B", "3", "H", "W"])
KORNIA_CHECK(
images.shape[0] == len(batched_prompts),
"The number of images (`B`) should match with the length of prompts!",
)
image_embeddings = self.image_encoder(images)
outputs = []
for prompt_record, curr_embedding in zip(batched_prompts, image_embeddings):
# Embed prompts
sparse_embeddings, dense_embeddings = self.prompt_encoder(
points=prompt_record.get("points", None),
boxes=prompt_record.get("boxes", None),
masks=prompt_record.get("mask_inputs", None),
)
# Predict masks
low_res_logits, iou_predictions = self.mask_decoder(
image_embeddings=curr_embedding[None, ...],
image_pe=self.prompt_encoder.get_dense_pe(),
sparse_prompt_embeddings=sparse_embeddings,
dense_prompt_embeddings=dense_embeddings,
multimask_output=multimask_output,
)
# Save results
outputs.append(SegmentationResults(low_res_logits, iou_predictions, self.mask_threshold))
return outputs
def _build_sam(
encoder_embed_dim: int, encoder_depth: int, encoder_num_heads: int, encoder_global_attn_indexes: tuple[int, ...]
) -> Sam:
prompt_embed_dim = 256
image_size = 1024
vit_patch_size = 16
image_embedding_size = image_size // vit_patch_size
return Sam(
image_encoder=ImageEncoderViT(
depth=encoder_depth,
embed_dim=encoder_embed_dim,
img_size=image_size,
mlp_ratio=4,
norm_layer=LayerNorm,
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,
),
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],
)