BruNet

Weights for BruNet (BruNet: A Cross-Domain Transfer Framework for Bruise Segmentation). BruNet trains only on HAM10000 skin-lesion masks and segments bruises in photographs without bruise labels and without any prompt.

Folder Encoder Input Bruise F1 Bruise IoU
lingbot/ LingBot-Vision ViT-B/16 512 ร— 512 0.8911 0.8251
dinov3/ DINOv3 ViT-B/16 224 ร— 224 0.8081 0.7289

Each folder contains:

  • model.safetensors: the full model state_dict (fp32, 92 M values, 368 MB). The optimizer and RNG state of the training checkpoint are not included.
  • config.yaml: the training config stored in the checkpoint. The two dataset paths are replaced with /path/to/... placeholders.

Model

  • Encoder. ViT-B/16. LingBot-Vision takes a 512 ร— 512 input and yields a 32 ร— 32 token grid; DINOv3 takes 224 ร— 224 and yields 14 ร— 14.
  • Adapter. Projects the token grid to 256 channels, upsamples it bilinearly to 56 ร— 56 and applies two 3 ร— 3 convolution blocks (GroupNorm with 8 groups, GELU). The output has the shape of a SAM ViT-B image embedding, so one decoder serves both encoders.
  • Decoder. The architecture of the SAM ViT-B mask decoder, randomly initialised: no SAM weights are loaded. Its 44 linear layers stay at their random initialisation and receive rank-8 LoRA updates; its normalisation layers, output tokens, prompt embeddings and upscaling convolutions train directly.
  • No prompt. The sparse prompt holds only SAM's padding-point token and the dense prompt its no-mask embedding, the same for every image, in training and at test time.
  • Output. One logit map (low_res_masks, 224 ร— 224). Upsample it to the image size, apply a sigmoid and threshold at 0.5.

Training

  • Data. HAM10000 lesion masks: 7,010 training images, each used as captured and after Multi-Scale Retinex with Chromaticity Preservation (MSRCP), giving 14,020 samples. No bruise image was used for training, checkpoint selection or threshold choice.
  • Recipe. AdamW, bfloat16, Dice plus binary cross-entropy loss. No text encoder and no auxiliary loss.
  • DINOv3. 150 epochs at learning rate 2e-5 on 224 ร— 224 crops, batch 64.
  • LingBot. 10 epochs with the backbone frozen, then 40 epochs unfrozen (backbone 3e-6, adapter 2e-5, LoRA 3e-4) on 512 ร— 512 crops, batch 8 with 8 gradient-accumulation steps.
  • Checkpoint. The lowest-training-loss epoch: epoch 148 of 150 (DINOv3) and epoch 40 of the 40 unfrozen epochs (LingBot).
  • Validation. HAM10000 validation Dice at threshold 0.5 (1,502 images, both views): LingBot 0.9559, DINOv3 0.9579. For both models, 0.5 is the Dice-maximising threshold on this split among ten values from 0.5 to 0.999.

Bruise results

86 bruise photographs (256 ร— 256), each with two expert contours: pixels inside the confident inner contour are bruise, pixels outside the permissive outer contour are background, and the band between them is ignored. Threshold 0.5; each metric is computed per image and averaged over the 86 images. F1 is identical to Dice.

Model Precision Recall F1 IoU Accuracy
BruNet-LingBot (lingbot/) 0.9126 0.9003 0.8911 0.8251 0.9583
BruNet-DINOv3 (dinov3/) 0.7793 0.9202 0.8081 0.7289 0.9181

Usage

Requires the BruNet code (the clipdinosam package). The LingBot model also needs the lingbot-vision package pinned in requirements-lingbot.txt.

import cv2
import torch
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file

from clipdinosam.config import load_config_with_overrides
from clipdinosam.models import build_clipdinosam_from_cfg

repo, variant = "chandlerWang/BruNet", "lingbot"  # or "dinov3"
cfg = load_config_with_overrides(hf_hub_download(repo, f"{variant}/config.yaml"), [])
cfg["model"]["backbone"]["pretrained"] = False  # every weight comes from model.safetensors
model = build_clipdinosam_from_cfg(cfg, torch.device("cpu"))
model.load_state_dict(load_file(hf_hub_download(repo, f"{variant}/model.safetensors")), strict=True)
model.eval()

size = cfg["data"]["crop"]  # 512 for lingbot, 224 for dinov3
img = cv2.cvtColor(cv2.imread("bruise.jpg"), cv2.COLOR_BGR2RGB)
x = torch.from_numpy(cv2.resize(img, (size, size))).permute(2, 0, 1).float() / 255
mean = torch.tensor([0.485, 0.456, 0.406])[:, None, None]
std = torch.tensor([0.229, 0.224, 0.225])[:, None, None]
with torch.no_grad():
    logits = model(((x - mean) / std)[None])["low_res_masks"][:, :1]
logits = torch.nn.functional.interpolate(logits, size=img.shape[:2], mode="bilinear", align_corners=False)
mask = torch.sigmoid(logits)[0, 0] >= 0.5

scripts/eval_dual_region.py expects a PyTorch checkpoint; wrap the weights first with torch.save({"model": load_file("model.safetensors")}, "best.pt").

Limitations

  • Evaluated on 86 bruise photographs only; per-image skin-tone and age labels were not collected.
  • Research use only; not validated for clinical or forensic use.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support