MedSAM Inference Code Analysis

Code Overview

This script performs segmentation inference using the MedSAM model. It processes 2D medical images in .npz format, applies a trained segmentation model, and saves the resulting masks along with optional overlay visualizations.

Imports and Configuration

The script begins with necessary imports and configuration settings:


import torch
import numpy as np
import cv2
from segment_anything import sam_model_registry
from segment_anything.modeling import MaskDecoder, PromptEncoder, TwoWayTransformer
from tiny_vit_sam import TinyViT
import argparse
from tqdm import tqdm
from time import time
import os
import matplotlib.pyplot as plt

Argument Parsing

Command-line arguments are defined using argparse:


parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_dir', type=str, default='./data/Viral Pneumonia/dd_dd/npz/')
parser.add_argument('-o', '--output_dir', type=str, default='./data/Viral Pneumonia/dd_dd/new_sam_segs/')
parser.add_argument("-chk", "--checkpoint", type=str, default="./train/work_dir/medsam_model_best.pth")
parser.add_argument('-lite_medsam_checkpoint_path', type=str, default="work_dir/LiteMedSAM/lite_medsam.pth")
parser.add_argument('-device', type=str, default="cpu")
parser.add_argument('--save_overlay', action='store_true', default=True)
parser.add_argument('-png_save_dir', type=str, default='./overlay/new_sam_overlay')
args = parser.parse_args()

Image Porcessing Functions

Two utility functions handle image resizing and padding:


def resize_longest_side(image, target_length=256):
    oldh, oldw = image.shape[:2]
    scale = target_length / max(oldh, oldw)
    new_size = (int(oldw * scale + 0.5), int(oldh * scale + 0.5))
    return cv2.resize(image, new_size, interpolation=cv2.INTER_AREA)

def pad_image(image, target_size=256):
    h, w = image.shape[:2]
    padh = target_size - h
    padw = target_size - w
    if len(image.shape) == 3:
        return np.pad(image, ((0, padh), (0, padw), (0, 0)))
    return np.pad(image, ((0, padh), (0, padw)))

Model Definition

The MedSAM Lite model combines an image encoder, mask decoder, and prompt encoder:


class MedSAM_Lite(nn.Module):
    def __init__(self, image_encoder, mask_decoder, prompt_encoder):
        super().__init__()
        self.image_encoder = image_encoder
        self.mask_decoder = mask_decoder
        self.prompt_encoder = prompt_encoder

    def forward(self, image, box_np):
        image_embedding = self.image_encoder(image)
        with torch.no_grad():
            box_torch = torch.as_tensor(box_np, dtype=torch.float32, device=image.device)
            if len(box_torch.shape) == 2:
                box_torch = box_torch[:, None, :]
        sparse_embeddings, dense_embeddings = self.prompt_encoder(
            points=None, boxes=box_np, masks=None
        )
        low_res_masks, _ = self.mask_decoder(
            image_embeddings=image_embedding,
            image_pe=self.prompt_encoder.get_dense_pe(),
            sparse_prompt_embeddings=sparse_embeddings,
            dense_prompt_embeddings=dense_embeddings,
            multimask_output=False
        )
        return low_res_masks

Postprocessing

The postprocessing function resizes masks to match original image dimensions:


@torch.no_grad()
def postprocess_masks(self, masks, new_size, original_size):
    masks = masks[..., :new_size[0], :new_size[1]]
    return F.interpolate(masks, size=original_size, mode="bilinear", align_corners=False)

Visualization Functions

Utility functions for mask and bounding box visualization:


def show_mask(mask, ax, mask_color=None, alpha=0.5):
    if mask_color is not None:
        color = np.concatenate([mask_color, [alpha]])
    else:
        color = [251/255, 252/255, 30/255, alpha]
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)

def show_box(box, ax, edgecolor='blue'):
    x0, y0 = box[:2]
    w, h = box[2] - x0, box[3] - y0
    ax.add_patch(plt.Rectangle((x0, y0), w, h, edgecolor=edgecolor, facecolor=(0,0,0,0), lw=2))

Bounding Box Calculation

Generates bounding boxes from ground truth masks:


def get_bbox256(mask_256, bbox_shift=3):
    y_indices, x_indices = np.where(mask_256 > 0)
    x_min, x_max = min(x_indices), max(x_indices)
    y_min, y_max = min(y_indices), max(y_indices)
    H, W = mask_256.shape
    x_min = max(0, x_min - bbox_shift)
    x_max = min(W, x_max + bbox_shift)
    y_min = max(0, y_min - bbox_shift)
    y_max = min(H, y_max + bbox_shift)
    return np.array([x_min, y_min, x_max, y_max])

Resizing Bounding Boxes

Adjusts bounding box coordinates to match target image size:


def resize_box_to_256(box, original_size):
    new_box = np.zeros_like(box)
    ratio = 256 / max(original_size)
    for i in range(len(box)):
        new_box[i] = int(box[i] * ratio)
    return new_box

Inference Function

Performs segmentation inference using MedSAM model:


@torch.no_grad()
def medsam_inference(medsam_model, img_embed, box_1024, H, W):
    box_torch = torch.as_tensor(box_1024, dtype=torch.float, device=img_embed.device)
    if len(box_torch.shape) == 2:
        box_torch = box_torch[:, None, :]
    sparse_embeddings, dense_embeddings = medsam_model.prompt_encoder(
        points=None, boxes=box_torch, masks=None
    )
    low_res_logits, _ = medsam_model.mask_decoder(
        image_embeddings=img_embed,
        image_pe=medsam_model.prompt_encoder.get_dense_pe(),
        sparse_prompt_embeddings=sparse_embeddings,
        dense_prompt_embeddings=dense_embeddings,
        multimask_output=False
    )
    low_res_pred = torch.sigmoid(low_res_logits)
    low_res_pred = F.interpolate(low_res_pred, size=(H, W), mode="bilinear", align_corners=False)
    return (low_res_pred.squeeze().cpu().numpy() > 0.5).astype(np.uint8)

Main Inference Pipeline

The core function processes a single .npz file:


def MedSAM_infer_npz_2D(img_npz_file):
    npz_name = os.path.basename(img_npz_file)
    npz_data = np.load(img_npz_file, allow_pickle=True)
    img_3c = npz_data['imgs']
    H, W = 256, 256
    img_256 = cv2.resize(img_3c, (H, W), interpolation=cv2.INTER_NEAREST)
    img_1024 = cv2.resize(img_3c, (1024, 1024), interpolation=cv2.INTER_NEAREST)
    
    # Process ground truth
    gt = npz_data['gts']
    if gt.shape != (H, W):
        gt = cv2.resize(gt.astype(np.uint8), (W, H), interpolation=cv2.INTER_NEAREST)
    gt = pad_image(gt)
    label_ids = np.unique(gt)[1:]
    
    # Generate random label and bounding box
    import random
    gt2D = np.uint8(gt == random.choice(label_ids.tolist()))
    gt2D = np.uint8(gt2D > 0)
    y_indices, x_indices = np.where(gt2D > 0)
    x_min, x_max = min(x_indices), max(x_indices)
    y_min, y_max = min(y_indices), max(y_indices)
    
    # Add random shift to bounding box
    bbox_shift = 5
    x_min = max(0, x_min - random.randint(0, bbox_shift))
    x_max = min(W, x_max + random.randint(0, bbox_shift))
    y_min = max(0, y_min - random.randint(0, bbox_shift))
    y_max = min(H, y_max + random.randint(0, bbox_shift))
    boxes = np.array([[x_min, y_min, x_max, y_max]])
    
    # Process image for inference
    img_1024 = resize_longest_side(img_1024, 1024)
    newh, neww = img_1024.shape[:2]
    img_1024_norm = (img_1024 - img_1024.min()) / max(1e-8, img_1024.max() - img_1024.min())
    img_1024_padded = pad_image(img_1024_norm, 1024)
    img_1024_tensor = torch.tensor(img_1024_padded).float().permute(2, 0, 1).unsqueeze(0).to(device)
    
    # Perform inference
    with torch.no_grad():
        image_embedding = medsam_model.image_encoder(img_1024_tensor)
    
    # Process each bounding box
    segs = np.zeros((H, W), dtype=np.uint8)
    for idx, box in enumerate(boxes, start=1):
        box_1024 = box / np.array([W, H, W, H]) * 1024
        box_1024 = box_1024[None, ...]
        medsam_mask, _ = medsam_inference(medsam_model, image_embedding, box_1024, H, W)
        segs[medsam_mask > 0] = idx
    
    # Save results
    np.savez_compressed(os.path.join(pred_save_dir, npz_name), segs=segs)
    
    # Visualization
    if save_overlay:
        fig, ax = plt.subplots(1, 2, figsize=(10, 5))
        ax[0].imshow(img_256)
        ax[1].imshow(img_256)
        ax[0].set_title("Image")
        ax[1].set_title("SAM Segmentation")
        ax[0].axis('off')
        ax[1].axis('off')
        for i, box in enumerate(boxes):
            color = np.random.rand(3)
            show_box(box, ax[1], edgecolor=color)
            show_mask((segs == i + 1).astype(np.uint8), ax[1], mask_color=color)
        plt.tight_layout()
        plt.savefig(os.path.join(png_save_dir, os.path.splitext(npz_name)[0] + '.png'), dpi=300)
        plt.close()

Execution

The main block processes all .npz files in the input directory:


if __name__ == '__main__':
    img_npz_files = sorted(glob(os.path.join(data_root, '*.npz'), recursive=True))
    efficiency = {'case': [], 'time': []}
    
    for img_npz_file in tqdm(img_npz_files[:-1]):
        start_time = time()
        if os.path.basename(img_npz_file).startswith('3D'):
            MedSAM_infer_npz_3D(img_npz_file)
        else:
            MedSAM_infer_npz_2D(img_npz_file)
        end_time = time()
        
        efficiency['case'].append(os.path.basename(img_npz_file))
        efficiency['time'].append(end_time - start_time)
        print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {os.path.basename(img_npz_file)}: {end_time - start_time:.4f}s")
    
    pd.DataFrame(efficiency).to_csv(os.path.join(pred_save_dir, 'efficiency.csv'), index=False)

Tags: medical-image-segmentation MedSAM model-inference image-processing pytorch

Posted on Thu, 10 Sep 2026 16:05:15 +0000 by xt3mp0r~