kornia--kornia
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
410 行
16 KiB
Python
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],
|
|
)
|