ibsocr1 commited on
Commit
47f2ebf
·
verified ·
1 Parent(s): 77370e4

Upload 13 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .git
3
+ .venv
4
+ venv
5
+ data
6
+ training
7
+ outputs
8
+ checkpoints
9
+ *.pyc
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ model/
7
+ outputs/
8
+ checkpoints/
9
+ data/*/images/*
10
+ data/*/annotations.json
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV PIP_NO_CACHE_DIR=1
5
+ ENV PORT=7860
6
+
7
+ WORKDIR /app
8
+
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ libglib2.0-0 \
11
+ libgl1 \
12
+ libgomp1 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ COPY requirements.txt .
16
+ RUN pip install --upgrade pip && pip install -r requirements.txt
17
+
18
+ COPY app ./app
19
+ COPY model ./model
20
+
21
+ EXPOSE 7860
22
+
23
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,13 +1,199 @@
1
- ---
2
- title: IcecreamDetect
3
- emoji: 📚
4
- colorFrom: indigo
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.25.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ice Cream Freezer Counter — RT-DETR + Hugging Face
2
+
3
+ A complete object-detection project for counting individual ice creams in freezer photos and identifying each product type.
4
+
5
+ **No YOLO is used.** The detector is Hugging Face Transformers' RT-DETR (`PekingU/rtdetr_r50vd`), fine-tuned on your own annotated freezer images.
6
+
7
+ ## What it does
8
+
9
+ Upload a freezer photo:
10
+
11
+ - detects every visible ice cream
12
+ - classifies each detected item (for example `cornetto`, `magnum`, `cone`, `cup`)
13
+ - counts items per class
14
+ - returns bounding boxes and confidence scores
15
+ - optionally returns an annotated image
16
+
17
+ Example response:
18
+
19
+ ```json
20
+ {
21
+ "total": 17,
22
+ "counts": {
23
+ "cornetto": 7,
24
+ "magnum": 5,
25
+ "cone": 3,
26
+ "cup": 2
27
+ },
28
+ "detections": [
29
+ {
30
+ "class": "cornetto",
31
+ "confidence": 0.96,
32
+ "box": [120, 85, 210, 310]
33
+ }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ ## 1. Define your classes
39
+
40
+ Edit `training/classes.txt`.
41
+
42
+ Example:
43
+
44
+ ```text
45
+ cornetto
46
+ magnum
47
+ cone
48
+ cup
49
+ sandwich
50
+ stick
51
+ other
52
+ ```
53
+
54
+ Use the exact product names you want returned by the API. If "correto" is your actual product spelling, use `correto`; otherwise use the correct brand/product name.
55
+
56
+ ## 2. Annotate your images
57
+
58
+ Use an annotation tool such as CVAT, Label Studio, Roboflow, or another COCO-compatible tool.
59
+
60
+ Annotate **each individual ice cream**, not just the freezer shelf.
61
+
62
+ Export in **COCO object-detection format**.
63
+
64
+ Expected layout:
65
+
66
+ ```text
67
+ data/
68
+ train/
69
+ images/
70
+ 0001.jpg
71
+ 0002.jpg
72
+ annotations.json
73
+ val/
74
+ images/
75
+ 1001.jpg
76
+ 1002.jpg
77
+ annotations.json
78
+ ```
79
+
80
+ The `annotations.json` files must be COCO detection JSON.
81
+
82
+ Important:
83
+ - draw one bounding box around each visible product
84
+ - assign the correct product class
85
+ - include partially visible products if you want them counted
86
+ - keep train and validation images separate
87
+ - avoid putting near-duplicate photos in both sets
88
+
89
+ A useful starting target is 500–2,000+ annotated images. More variation is usually more valuable than many almost-identical photos.
90
+
91
+ ## 3. Install
92
+
93
+ Python 3.10+ is recommended.
94
+
95
+ ```bash
96
+ pip install -r requirements.txt
97
+ ```
98
+
99
+ For NVIDIA GPU training, install the appropriate PyTorch build for your CUDA version first, following the official PyTorch instructions.
100
+
101
+ ## 4. Train
102
+
103
+ Make sure the dataset is in the structure above, then:
104
+
105
+ ```bash
106
+ python training/train.py \
107
+ --train-dir data/train \
108
+ --val-dir data/val \
109
+ --output-dir model \
110
+ --epochs 30 \
111
+ --batch-size 2 \
112
+ --learning-rate 1e-5
113
+ ```
114
+
115
+ For a small GPU, use `--batch-size 1`.
116
+
117
+ The script starts from `PekingU/rtdetr_r50vd` and replaces the classification head with your product classes.
118
+
119
+ At the end, `model/` contains the fine-tuned Hugging Face model and processor.
120
+
121
+ ## 5. Test locally
122
+
123
+ ```bash
124
+ python -m uvicorn app.main:app --host 0.0.0.0 --port 7860
125
+ ```
126
+
127
+ Open:
128
+
129
+ ```text
130
+ http://localhost:7860/docs
131
+ ```
132
+
133
+ Use `POST /predict` to upload an image.
134
+
135
+ Or from Python:
136
+
137
+ ```python
138
+ import requests
139
+
140
+ with open("freezer.jpg", "rb") as f:
141
+ r = requests.post(
142
+ "http://localhost:7860/predict",
143
+ files={"file": ("freezer.jpg", f, "image/jpeg")}
144
+ )
145
+
146
+ print(r.json())
147
+ ```
148
+
149
+ ## 6. Environment variables
150
+
151
+ The API supports:
152
+
153
+ ```text
154
+ MODEL_ID=./model
155
+ CONFIDENCE_THRESHOLD=0.35
156
+ MAX_IMAGE_MB=15
157
+ ```
158
+
159
+ If the model is stored on the Hugging Face Hub instead of the local filesystem:
160
+
161
+ ```text
162
+ MODEL_ID=your-username/your-icecream-rtdetr
163
+ HF_TOKEN=hf_...
164
+ ```
165
+
166
+ ## 7. Deploy to Hugging Face
167
+
168
+ Create a Hugging Face **Docker Space** and upload this repository.
169
+
170
+ The included `Dockerfile` listens on port `7860`.
171
+
172
+ For a private model, add a Space secret named:
173
+
174
+ ```text
175
+ HF_TOKEN
176
+ ```
177
+
178
+ and set:
179
+
180
+ ```text
181
+ MODEL_ID=your-username/your-icecream-rtdetr
182
+ ```
183
+
184
+ If the model is public, `HF_TOKEN` is not required.
185
+
186
+ ## Accuracy notes
187
+
188
+ Counting accuracy depends heavily on the training data. Freezer images have difficult cases: occlusion, reflections, small objects, similar packaging, tilted products, and products stacked behind each other.
189
+
190
+ Start with a confidence threshold around `0.35–0.50` and tune it on validation photos.
191
+
192
+ If two products are touching or one is heavily hidden, the detector can still miss or merge them. Add examples of those exact situations to the training set.
193
+
194
+ ## Why RT-DETR?
195
+
196
+ RT-DETR is a transformer-based object detector available in Hugging Face Transformers. It produces object boxes and class labels and is suitable for instance counting. This project deliberately does not use YOLO.
197
+
198
+ Hugging Face reference:
199
+ https://huggingface.co/docs/transformers/model_doc/rt_detr
README_HF_SPACE.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Space configuration
2
+
3
+ Create a **Docker Space** and upload the project files.
4
+
5
+ The Space should contain:
6
+
7
+ ```text
8
+ Dockerfile
9
+ requirements.txt
10
+ app/
11
+ model/
12
+ ```
13
+
14
+ For a model stored on the Hugging Face Hub, you can instead omit the local `model/` directory and set the Space variables:
15
+
16
+ ```text
17
+ MODEL_ID=YOUR_USERNAME/YOUR_MODEL_REPO
18
+ HF_TOKEN=hf_your_token_if_private
19
+ ```
20
+
21
+ The API starts on port 7860.
22
+
23
+ Endpoints:
24
+
25
+ - `GET /health`
26
+ - `POST /predict`
27
+ - `GET /docs`
28
+
29
+ Example:
30
+
31
+ ```bash
32
+ curl -X POST \
33
+ -F "file=@freezer.jpg" \
34
+ "https://YOUR-SPACE-NAME.hf.space/predict?return_image=true"
35
+ ```
36
+
37
+ The `annotated_image_base64` field contains the annotated result image.
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import os
4
+ from collections import Counter
5
+
6
+ import torch
7
+ from fastapi import FastAPI, File, HTTPException, UploadFile
8
+ from fastapi.responses import JSONResponse
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
11
+
12
+
13
+ MODEL_ID = os.getenv("MODEL_ID", "./model")
14
+ CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.35"))
15
+ MAX_IMAGE_MB = int(os.getenv("MAX_IMAGE_MB", "15"))
16
+
17
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+
19
+ processor = RTDetrImageProcessor.from_pretrained(MODEL_ID)
20
+ model = RTDetrForObjectDetection.from_pretrained(MODEL_ID)
21
+ model.to(DEVICE)
22
+ model.eval()
23
+
24
+ app = FastAPI(
25
+ title="Ice Cream Counter API",
26
+ version="1.0.0",
27
+ description="RT-DETR ice cream product detector and counter. No YOLO.",
28
+ )
29
+
30
+
31
+ @app.get("/health")
32
+ def health():
33
+ return {
34
+ "status": "ok",
35
+ "model": MODEL_ID,
36
+ "device": str(DEVICE),
37
+ "threshold": CONFIDENCE_THRESHOLD,
38
+ }
39
+
40
+
41
+ def annotate(image, detections):
42
+ image = image.copy()
43
+ draw = ImageDraw.Draw(image)
44
+
45
+ try:
46
+ font = ImageFont.truetype("DejaVuSans.ttf", 18)
47
+ except Exception:
48
+ font = ImageFont.load_default()
49
+
50
+ for d in detections:
51
+ x1, y1, x2, y2 = d["box"]
52
+ label = f'{d["class"]} {d["confidence"]:.2f}'
53
+ draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
54
+
55
+ bbox = draw.textbbox((x1, y1), label, font=font)
56
+ draw.rectangle(bbox, fill="red")
57
+ draw.text((x1, y1), label, fill="white", font=font)
58
+
59
+ return image
60
+
61
+
62
+ @app.post("/predict")
63
+ async def predict(
64
+ file: UploadFile = File(...),
65
+ return_image: bool = True,
66
+ ):
67
+ if not file.content_type or not file.content_type.startswith("image/"):
68
+ raise HTTPException(400, "Upload a JPG, PNG, WEBP, or other image file.")
69
+
70
+ raw = await file.read()
71
+
72
+ if len(raw) > MAX_IMAGE_MB * 1024 * 1024:
73
+ raise HTTPException(
74
+ 413,
75
+ f"Image is too large. Maximum is {MAX_IMAGE_MB} MB.",
76
+ )
77
+
78
+ try:
79
+ image = Image.open(io.BytesIO(raw)).convert("RGB")
80
+ except Exception as exc:
81
+ raise HTTPException(400, f"Could not read image: {exc}")
82
+
83
+ inputs = processor(images=image, return_tensors="pt")
84
+ inputs = {
85
+ k: v.to(DEVICE) if torch.is_tensor(v) else v
86
+ for k, v in inputs.items()
87
+ }
88
+
89
+ with torch.inference_mode():
90
+ outputs = model(**inputs)
91
+
92
+ target_sizes = torch.tensor(
93
+ [[image.height, image.width]],
94
+ device=DEVICE,
95
+ )
96
+
97
+ result = processor.post_process_object_detection(
98
+ outputs,
99
+ threshold=CONFIDENCE_THRESHOLD,
100
+ target_sizes=target_sizes,
101
+ )[0]
102
+
103
+ detections = []
104
+ counts = Counter()
105
+
106
+ for score, label, box in zip(
107
+ result["scores"],
108
+ result["labels"],
109
+ result["boxes"],
110
+ ):
111
+ score_value = float(score.item())
112
+ label_id = int(label.item())
113
+ class_name = model.config.id2label[label_id]
114
+
115
+ coords = [round(float(x), 2) for x in box.tolist()]
116
+
117
+ detections.append(
118
+ {
119
+ "class": class_name,
120
+ "confidence": round(score_value, 4),
121
+ "box": coords,
122
+ }
123
+ )
124
+ counts[class_name] += 1
125
+
126
+ response = {
127
+ "total": len(detections),
128
+ "counts": dict(sorted(counts.items())),
129
+ "detections": detections,
130
+ }
131
+
132
+ if return_image:
133
+ annotated = annotate(image, detections)
134
+ buf = io.BytesIO()
135
+ annotated.save(buf, format="JPEG", quality=90)
136
+ response["annotated_image_base64"] = base64.b64encode(
137
+ buf.getvalue()
138
+ ).decode("ascii")
139
+
140
+ return JSONResponse(response)
data/train/PUT_IMAGES_HERE.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Put training images in this directory's images/ subdirectory and COCO annotations in annotations.json.
data/val/PUT_IMAGES_HERE.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Put validation images in this directory's images/ subdirectory and COCO annotations in annotations.json.
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.3
2
+ torchvision>=0.18
3
+ transformers>=4.50
4
+ huggingface_hub>=0.25
5
+ Pillow>=10.0
6
+ fastapi>=0.115
7
+ uvicorn[standard]>=0.30
8
+ python-multipart>=0.0.9
9
+ numpy>=1.26
10
+ tqdm>=4.66
11
+ pycocotools>=2.0.8
training/classes.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Carnavalita
2
+ Kimo-COno
3
+ Squizz
4
+ Oreo
5
+ Moro
6
+ Dulce
7
+ KitKat
8
+ Cadbury
9
+ Mega
10
+ other
training/convert_coco_categories.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Optional helper.
3
+
4
+ Your COCO exporter should already contain the same categories in train and val.
5
+ This script prints the category IDs/names so you can compare them with classes.txt.
6
+
7
+ Usage:
8
+ python training/convert_coco_categories.py data/train/annotations.json
9
+ """
10
+
11
+ import json
12
+ import sys
13
+
14
+ if len(sys.argv) != 2:
15
+ raise SystemExit("Usage: python training/convert_coco_categories.py annotations.json")
16
+
17
+ data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
18
+ for cat in sorted(data["categories"], key=lambda x: x["id"]):
19
+ print(f'{cat["id"]}: {cat["name"]}')
training/train.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import torch
6
+ from PIL import Image
7
+ from torch.utils.data import Dataset, DataLoader
8
+ from tqdm import tqdm
9
+ from transformers import RTDetrImageProcessor, RTDetrForObjectDetection
10
+
11
+
12
+ BASE_MODEL = "PekingU/rtdetr_r50vd"
13
+
14
+
15
+ def load_classes(path):
16
+ classes = [x.strip() for x in Path(path).read_text().splitlines() if x.strip()]
17
+ if not classes:
18
+ raise ValueError("classes.txt is empty")
19
+ return classes
20
+
21
+
22
+ class COCODetectionDataset(Dataset):
23
+ def __init__(self, image_dir, annotation_file, processor):
24
+ self.image_dir = Path(image_dir)
25
+ self.processor = processor
26
+
27
+ coco = json.loads(Path(annotation_file).read_text())
28
+ self.images = {x["id"]: x for x in coco["images"]}
29
+
30
+ # The training model needs contiguous labels 0..N-1.
31
+ # categories.json is converted here from the COCO category IDs.
32
+ categories = sorted(coco["categories"], key=lambda x: x["id"])
33
+ self.category_id_to_label = {
34
+ cat["id"]: i for i, cat in enumerate(categories)
35
+ }
36
+
37
+ anns_by_image = {}
38
+ for ann in coco["annotations"]:
39
+ if ann.get("iscrowd", 0):
40
+ continue
41
+ anns_by_image.setdefault(ann["image_id"], []).append(ann)
42
+
43
+ self.records = []
44
+ for image_id, info in self.images.items():
45
+ self.records.append(
46
+ {
47
+ "image_id": image_id,
48
+ "file_name": info["file_name"],
49
+ "width": info["width"],
50
+ "height": info["height"],
51
+ "annotations": anns_by_image.get(image_id, []),
52
+ }
53
+ )
54
+
55
+ def __len__(self):
56
+ return len(self.records)
57
+
58
+ def __getitem__(self, idx):
59
+ record = self.records[idx]
60
+ image_path = self.image_dir / record["file_name"]
61
+ image = Image.open(image_path).convert("RGB")
62
+
63
+ annotations = []
64
+ for ann in record["annotations"]:
65
+ x, y, w, h = ann["bbox"]
66
+ if w <= 0 or h <= 0:
67
+ continue
68
+
69
+ label = self.category_id_to_label[ann["category_id"]]
70
+ annotations.append(
71
+ {
72
+ "id": ann["id"],
73
+ "image_id": record["image_id"],
74
+ "category_id": label,
75
+ "bbox": [x, y, w, h],
76
+ "area": float(ann.get("area", w * h)),
77
+ "iscrowd": 0,
78
+ }
79
+ )
80
+
81
+ target = {
82
+ "image_id": record["image_id"],
83
+ "annotations": annotations,
84
+ }
85
+
86
+ encoded = self.processor(
87
+ images=image,
88
+ annotations=target,
89
+ return_tensors="pt",
90
+ )
91
+
92
+ # Remove batch dimension. DataLoader will create it.
93
+ encoded["pixel_values"] = encoded["pixel_values"].squeeze(0)
94
+ if "pixel_mask" in encoded:
95
+ encoded["pixel_mask"] = encoded["pixel_mask"].squeeze(0)
96
+ encoded["labels"] = encoded["labels"][0]
97
+ return encoded
98
+
99
+
100
+ def collate_fn(batch):
101
+ pixel_values = torch.stack([x["pixel_values"] for x in batch])
102
+ pixel_mask = None
103
+ if "pixel_mask" in batch[0]:
104
+ pixel_mask = torch.stack([x["pixel_mask"] for x in batch])
105
+
106
+ labels = [x["labels"] for x in batch]
107
+ result = {"pixel_values": pixel_values, "labels": labels}
108
+ if pixel_mask is not None:
109
+ result["pixel_mask"] = pixel_mask
110
+ return result
111
+
112
+
113
+ def evaluate(model, loader, device):
114
+ model.eval()
115
+ total = 0.0
116
+ count = 0
117
+ with torch.no_grad():
118
+ for batch in loader:
119
+ batch = {
120
+ k: (v.to(device) if torch.is_tensor(v) else v)
121
+ for k, v in batch.items()
122
+ }
123
+ out = model(**batch)
124
+ total += float(out.loss.item())
125
+ count += 1
126
+ model.train()
127
+ return total / max(count, 1)
128
+
129
+
130
+ def main():
131
+ parser = argparse.ArgumentParser()
132
+ parser.add_argument("--train-dir", default="data/train")
133
+ parser.add_argument("--val-dir", default="data/val")
134
+ parser.add_argument("--classes", default="training/classes.txt")
135
+ parser.add_argument("--output-dir", default="model")
136
+ parser.add_argument("--epochs", type=int, default=30)
137
+ parser.add_argument("--batch-size", type=int, default=2)
138
+ parser.add_argument("--learning-rate", type=float, default=1e-5)
139
+ parser.add_argument("--weight-decay", type=float, default=1e-4)
140
+ parser.add_argument("--num-workers", type=int, default=2)
141
+ parser.add_argument("--grad-accumulation", type=int, default=1)
142
+ parser.add_argument("--resume", default=None)
143
+ args = parser.parse_args()
144
+
145
+ classes = load_classes(args.classes)
146
+ label2id = {name: i for i, name in enumerate(classes)}
147
+ id2label = {i: name for i, name in enumerate(classes)}
148
+
149
+ train_ann = Path(args.train_dir) / "annotations.json"
150
+ val_ann = Path(args.val_dir) / "annotations.json"
151
+
152
+ processor = RTDetrImageProcessor.from_pretrained(BASE_MODEL)
153
+
154
+ train_ds = COCODetectionDataset(
155
+ Path(args.train_dir) / "images",
156
+ train_ann,
157
+ processor,
158
+ )
159
+ val_ds = COCODetectionDataset(
160
+ Path(args.val_dir) / "images",
161
+ val_ann,
162
+ processor,
163
+ )
164
+
165
+ # Make sure both datasets use exactly the class list supplied by the user.
166
+ train_categories = len(train_ds.category_id_to_label)
167
+ val_categories = len(val_ds.category_id_to_label)
168
+ if train_categories != len(classes) or val_categories != len(classes):
169
+ raise ValueError(
170
+ "The number of COCO categories does not match classes.txt. "
171
+ f"classes.txt={len(classes)}, train={train_categories}, val={val_categories}"
172
+ )
173
+
174
+ model = RTDetrForObjectDetection.from_pretrained(
175
+ BASE_MODEL,
176
+ num_labels=len(classes),
177
+ id2label=id2label,
178
+ label2id=label2id,
179
+ ignore_mismatched_sizes=True,
180
+ )
181
+
182
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
183
+ model.to(device)
184
+
185
+ train_loader = DataLoader(
186
+ train_ds,
187
+ batch_size=args.batch_size,
188
+ shuffle=True,
189
+ num_workers=args.num_workers,
190
+ collate_fn=collate_fn,
191
+ pin_memory=torch.cuda.is_available(),
192
+ )
193
+ val_loader = DataLoader(
194
+ val_ds,
195
+ batch_size=args.batch_size,
196
+ shuffle=False,
197
+ num_workers=args.num_workers,
198
+ collate_fn=collate_fn,
199
+ pin_memory=torch.cuda.is_available(),
200
+ )
201
+
202
+ optimizer = torch.optim.AdamW(
203
+ model.parameters(),
204
+ lr=args.learning_rate,
205
+ weight_decay=args.weight_decay,
206
+ )
207
+
208
+ scaler = torch.amp.GradScaler("cuda", enabled=torch.cuda.is_available())
209
+ start_epoch = 0
210
+
211
+ if args.resume:
212
+ checkpoint = torch.load(args.resume, map_location=device)
213
+ model.load_state_dict(checkpoint["model"])
214
+ optimizer.load_state_dict(checkpoint["optimizer"])
215
+ start_epoch = checkpoint["epoch"] + 1
216
+
217
+ output_dir = Path(args.output_dir)
218
+ output_dir.mkdir(parents=True, exist_ok=True)
219
+
220
+ best_val = float("inf")
221
+
222
+ for epoch in range(start_epoch, args.epochs):
223
+ model.train()
224
+ optimizer.zero_grad(set_to_none=True)
225
+ running = 0.0
226
+
227
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{args.epochs}")
228
+ for step, batch in enumerate(pbar):
229
+ batch = {
230
+ k: (v.to(device) if torch.is_tensor(v) else v)
231
+ for k, v in batch.items()
232
+ }
233
+
234
+ with torch.amp.autocast(
235
+ device_type="cuda",
236
+ enabled=torch.cuda.is_available(),
237
+ ):
238
+ out = model(**batch)
239
+ loss = out.loss / args.grad_accumulation
240
+
241
+ scaler.scale(loss).backward()
242
+
243
+ if (step + 1) % args.grad_accumulation == 0:
244
+ scaler.step(optimizer)
245
+ scaler.update()
246
+ optimizer.zero_grad(set_to_none=True)
247
+
248
+ running += float(loss.item()) * args.grad_accumulation
249
+ pbar.set_postfix(loss=f"{running / (step + 1):.4f}")
250
+
251
+ val_loss = evaluate(model, val_loader, device)
252
+ print(f"epoch={epoch + 1} validation_loss={val_loss:.4f}")
253
+
254
+ checkpoint = {
255
+ "epoch": epoch,
256
+ "model": model.state_dict(),
257
+ "optimizer": optimizer.state_dict(),
258
+ }
259
+ torch.save(checkpoint, output_dir / "last_checkpoint.pt")
260
+
261
+ if val_loss < best_val:
262
+ best_val = val_loss
263
+ model.save_pretrained(output_dir)
264
+ processor.save_pretrained(output_dir)
265
+ (output_dir / "classes.json").write_text(
266
+ json.dumps(
267
+ {"id2label": id2label, "label2id": label2id},
268
+ indent=2,
269
+ )
270
+ )
271
+ print(f"Saved best model to {output_dir}")
272
+
273
+ # Always save the final model as well.
274
+ model.save_pretrained(output_dir)
275
+ processor.save_pretrained(output_dir)
276
+ print(f"Final model saved to {output_dir}")
277
+
278
+
279
+ if __name__ == "__main__":
280
+ main()