File size: 8,803 Bytes
1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 38d0309 1c80b88 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | ---
license: mit
library_name: pytorch
tags:
- instance-segmentation
- mask-rcnn
- object-detection
- resnet50
- fpn
- medical
- ocr
- pytorch
- safetensors
pipeline_tag: image-segmentation
---
<p align="center">
<img src="assets/banner.png" alt="MediSeG banner" width="100%">
</p>
# MediSeG — Mask R-CNN for Medicine Box Instance Segmentation
Custom **Mask R-CNN** (ResNet-50 + FPN + RPN + ROI Align, implemented from scratch in PyTorch — no `torchvision.models.detection`) that detects and segments medicine packages (`medicine_box`) in photos.
The model is the first stage of the MediSeG pipeline: **detection + mask → masked crop → OCR (French + Arabic) → LLM identification → web search → annotated summary**.
<p align="center">
<img src="assets/examples/01_multi_box_detections.jpg" alt="Mask R-CNN detections on three medicine boxes" width="49%">
<img src="assets/examples/01_multi_box_annotated.jpg" alt="Full pipeline output: each box identified with name, dosage, form and active ingredient" width="49%">
</p>
<p align="center"><em>Left: Mask R-CNN output (boxes + masks + scores). Right: full pipeline output (OCR + LLM + web search).</em></p>
## Model description
- **Backbone**: ResNet-50 (ImageNet-pretrained), feature maps C2–C5; the top 3 ResNet stages are fine-tuned, BatchNorm stays frozen.
- **Neck**: Feature Pyramid Network (FPN), 5 levels, 256 channels.
- **Region proposal**: RPN, one anchor size per level (32 / 64 / 128 / 256 / 512) × 3 aspect ratios (0.5, 1.0, 2.0); NMS 0.7, 300 proposals kept at test time.
- **ROI heads**: two-layer MLP trunk → classification head + class-specific box regression head (7×7 ROI Align); 4-conv + deconv mask head (14×14 ROI Align → 28×28 masks).
- **Classes**: 2 (`0 = background`, `1 = medicine_box`).
- **Input**: RGB, letterboxed to 640×640 (aspect ratio preserved, black padding, top-left aligned).
- **Weights**: `checkpoints/best_model.safetensors` in this repository.
## Intended use
Locate and segment medicine packages (boxes, tubes, sachets) in photos, typically to crop them for OCR — e.g. pharmacy tooling or assistive apps that read a medicine's label.
**Not** validated for clinical or safety-critical use. The downstream LLM identification can be wrong (see [Limitations](#limitations)); never rely on it to decide what medicine to take.
## Training data
Built from two public sources merged into one COCO instance-segmentation dataset (full report: `data/report.md` in the source repository):
| Split | Images | Instances |
|---|---:|---:|
| train | 540 | 806 |
| val | 68 | 121 |
| test | 68 | 100 |
- Sources: `main_ar_fr` (French/Arabic packages, 4-point polygons) and `medicine_packv2` (polygons). A third source with bounding boxes only was excluded.
- Split 80/10/10 with seed 42, **grouped by source photo** (near-duplicates merged by perceptual hash) to avoid train/test leakage — the original splits leaked 128 images.
- Single class: `medicine_box`.
## Training procedure
- SGD, `lr=0.01`, `momentum=0.9`, `weight_decay=1e-4`, batch size 8, 30 epochs.
- Linear warmup over 200 iterations, LR ×0.1 at epochs 20 and 26, gradient clipping at 10, mixed precision on GPU.
- Losses (equal weights): RPN objectness (BCE) + RPN box (smooth-L1), ROI classification (cross-entropy) + ROI box (smooth-L1), mask (BCE on 28×28).
- Sampling: RPN anchors positive at IoU ≥ 0.7 / negative < 0.3 (256 per image, 50 % positive); ROI proposals positive at IoU ≥ 0.5 (512 per image, 25 % positive).
- Augmentation: random horizontal flip (p = 0.5).
All hyperparameters live in `model_rcnn_scratch/config.py`.
## Evaluation
Dice and IoU on the held-out test split (union of predicted masks with score ≥ 0.5 vs. union of ground-truth masks, per image):
```bash
python -m model_rcnn_scratch.evaluate
```
| Metric | Value |
|---|---|
| Dice | _TBD_ |
| IoU | _TBD_ |
## How to use
### Weights only
```python
import torch
import torchvision.transforms.functional as TF
from huggingface_hub import hf_hub_download
from PIL import Image
from safetensors.torch import load_file
from model_rcnn_scratch.dataset import letterbox
from model_rcnn_scratch.model_architecture.mask_rcnn_full import MaskRCNN
weights = hf_hub_download("ApyHTML19/MediSeg-Mask-RCNN", "checkpoints/best_model.safetensors")
model = MaskRCNN(num_classes=2, pretrained_backbone=False)
model.load_state_dict(load_file(weights, device="cpu"))
model.eval()
image, scale = letterbox(Image.open("photo.jpg").convert("RGB")) # 640x640, aspect ratio kept
with torch.no_grad():
detections = model(TF.to_tensor(image).unsqueeze(0))[0] # {boxes, labels, scores, masks}
keep = detections["scores"] >= 0.5
boxes = detections["boxes"][keep] / scale # back to original image coordinates
```
The model code lives in the source repository (`model_rcnn_scratch/`); `model_rcnn_scratch.hub.load_model()` does the download + loading above in one call.
### Command line
```bash
python -m model_rcnn_scratch.predict path/to/photo.jpg # detection only -> outputs_results/<name>_pred.png
python -m harness.pipeline path/to/photo.jpg # detection + masked crop + OCR, no API key needed
python -m operation.run path/to/photo.jpg # full pipeline (needs OPENAI_API_KEY + TAVILY_API_KEY)
```
Full pipeline outputs, in `outputs_results/pipeline/`: `<name>_detections.jpg`, `<name>_box<i>.png` (masked crop), `<name>_analysis.json`, `<name>_annotated.jpg`.
## Examples
All images below are real outputs of the model and pipeline, stored in [`assets/examples/`](assets/examples/).
### Several boxes, French labels
<p align="center">
<img src="assets/examples/01_multi_box_crop0.png" alt="Masked crop of the Doliprane box" width="35%">
</p>
Masked crop (`<name>_box0.png`) sent to OCR. The three boxes are detected with score ≈ 1.00; the pipeline identifies **Doliprane 1000 mg**, **Ibuprofène Mylan 200 mg** and **Efferalgan 500 mg**, each with its active ingredient (paracetamol / ibuprofen).
### Bilingual French / Arabic tube and box
<p align="center">
<img src="assets/examples/02_bilingual_tube_annotated.jpg" alt="Aureomycine tube and box, French and Arabic labels" width="80%">
</p>
The French tube (#0) and the Arabic box (#1) are both identified as **Auréomycine 1 %** (chlortetracycline, ophthalmic ointment). Box #2 is a duplicate partial detection of the tube's label.
### Arabic-only label
<p align="center">
<img src="assets/examples/03_arabic_box_annotated.jpg" alt="Vermox box with an Arabic label" width="60%">
</p>
Arabic OCR reads **فيرموكسمجم** ("Vermox mg", words merged), Latin OCR reads **500** and **janssen**, and the LLM resolves it to **Vermox 500 mg** (mebendazole).
### Failure cases
<p align="center">
<img src="assets/examples/04_failure_false_positives_annotated.jpg" alt="False positives on a glass and a text block" width="60%">
</p>
Advertising image: the Propalgina Plus box (#0) is correct, but the model also fires on a **block of text** (#1, score 0.78) and a **glass** (#2, score 0.50). The LLM then "identifies" the text block as paracetamol 500 mg from the words it contains.
<p align="center">
<img src="assets/examples/05_failure_llm_misid_detections.jpg" alt="Two overlapping Colocalm boxes, correctly segmented" width="49%">
<img src="assets/examples/05_failure_llm_misid_annotated.jpg" alt="LLM misidentification of one box as Lokelma" width="49%">
</p>
Segmentation of the two overlapping boxes is correct and the OCR reads the Arabic brand name correctly (كولوكالم, Colocalm — mebeverine), but the LLM maps it to **Lokelma**, a different medicine. Box #1 gets the right active ingredient without a name.
## Limitations
- Single class (`medicine_box`); not a general-purpose detector. Any rectangular, label-like region (text panels, product packaging, glasses) can trigger a detection — see the failure cases above. Raise the score threshold (default 0.5) to trade recall for precision.
- Small dataset (540 training images), mostly French/Arabic packages photographed on plain backgrounds — expect weaker results on cluttered scenes, blisters, bottles or other scripts.
- Overlapping boxes may produce duplicate or partial detections.
- The identification stage depends on OCR quality and on an external LLM + web search; it can return a wrong medicine with high confidence. Treat it as a hint, not a diagnosis.
- `Backbone_RES` downloads ImageNet ResNet-50 weights when training from scratch (not needed for inference with `pretrained_backbone=False`).
## Citation
```
@misc{mediseg2026,
title = {MediSeG: Mask R-CNN for Medicine Box Instance Segmentation},
year = {2026},
url = {https://huggingface.co/ApyHTML19/MediSeg-Mask-RCNN}
}
```
|