ibsocr1 commited on
Commit
1902473
·
verified ·
1 Parent(s): d2591a9

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +115 -183
  2. app.py +490 -0
  3. requirements.txt +1 -3
README.md CHANGED
@@ -1,252 +1,184 @@
1
  ---
2
- title: Ice Cream Counter
3
  emoji: 🍦
4
- colorFrom: blue
5
  colorTo: red
6
- sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- # Ice Cream Counter
11
 
12
- RT-DETR-based ice cream detection and counting API.
13
-
14
- ## Features
15
-
16
- - Upload training freezer photos once
17
- - Save the dataset
18
- - Annotate individual ice creams
19
- - Train an RT-DETR model
20
- - Save the trained model
21
- - Upload new images one by one
22
- - Detect and count every ice cream by type
23
- - No YOLO
24
-
25
- ## Main workflow
26
-
27
- 1. Define your ice cream classes.
28
- 2. Upload training images.
29
- 3. Annotate each ice cream with a bounding box and product type.
30
- 4. Train the RT-DETR model.
31
- 5. The trained model is saved.
32
- 6. Upload a new freezer image.
33
- 7. Get the total count and count for every product type.
34
-
35
- ## API
36
-
37
- - `GET /health`
38
- - `POST /dataset/images`
39
- - `GET /dataset`
40
- - `POST /dataset/annotations`
41
- - `POST /train`
42
- - `GET /training/status`
43
- - `GET /model`
44
- - `POST /count`
45
-
46
- ## Local development
47
-
48
- ```bash
49
- pip install -r requirements.txt
50
- uvicorn app.main:app --host 0.0.0.0 --port 7860
51
 
 
52
 
 
 
 
 
 
 
53
 
54
- # Ice Cream Freezer Counter — RT-DETR + Hugging Face
55
 
56
- A complete object-detection project for counting individual ice creams in freezer photos and identifying each product type.
57
 
58
- **No YOLO is used.** The detector is Hugging Face Transformers' RT-DETR (`PekingU/rtdetr_r50vd`), fine-tuned on your own annotated freezer images.
59
 
60
- ## What it does
61
 
62
- Upload a freezer photo:
63
 
64
- - detects every visible ice cream
65
- - classifies each detected item (for example `cornetto`, `magnum`, `cone`, `cup`)
66
- - counts items per class
67
- - returns bounding boxes and confidence scores
68
- - optionally returns an annotated image
69
 
70
- Example response:
71
 
72
- ```json
73
- {
74
- "total": 17,
75
- "counts": {
76
- "cornetto": 7,
77
- "magnum": 5,
78
- "cone": 3,
79
- "cup": 2
80
- },
81
- "detections": [
82
- {
83
- "class": "cornetto",
84
- "confidence": 0.96,
85
- "box": [120, 85, 210, 310]
86
- }
87
- ]
88
- }
89
  ```
90
 
91
- ## 1. Define your classes
92
-
93
- Edit `training/classes.txt`.
94
-
95
- Example:
96
 
97
  ```text
98
- cornetto
99
- magnum
100
- cone
101
- cup
102
- sandwich
103
- stick
104
- other
105
  ```
106
 
107
- 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.
108
-
109
- ## 2. Annotate your images
110
-
111
- Use an annotation tool such as CVAT, Label Studio, Roboflow, or another COCO-compatible tool.
112
-
113
- Annotate **each individual ice cream**, not just the freezer shelf.
114
 
115
- Export in **COCO object-detection format**.
116
-
117
- Expected layout:
118
 
119
  ```text
120
- data/
121
- train/
122
- images/
123
- 0001.jpg
124
- 0002.jpg
125
- annotations.json
126
- val/
127
- images/
128
- 1001.jpg
129
- 1002.jpg
130
- annotations.json
131
  ```
132
 
133
- The `annotations.json` files must be COCO detection JSON.
134
 
135
- Important:
136
- - draw one bounding box around each visible product
137
- - assign the correct product class
138
- - include partially visible products if you want them counted
139
- - keep train and validation images separate
140
- - avoid putting near-duplicate photos in both sets
141
 
142
- A useful starting target is 500–2,000+ annotated images. More variation is usually more valuable than many almost-identical photos.
143
 
144
- ## 3. Install
 
 
145
 
146
- Python 3.10+ is recommended.
147
 
148
- ```bash
149
- pip install -r requirements.txt
150
- ```
151
 
152
- For NVIDIA GPU training, install the appropriate PyTorch build for your CUDA version first, following the official PyTorch instructions.
153
 
154
- ## 4. Train
155
 
156
- Make sure the dataset is in the structure above, then:
157
 
158
- ```bash
159
- python training/train.py \
160
- --train-dir data/train \
161
- --val-dir data/val \
162
- --output-dir model \
163
- --epochs 30 \
164
- --batch-size 2 \
165
- --learning-rate 1e-5
166
- ```
167
 
168
- For a small GPU, use `--batch-size 1`.
 
 
 
 
 
 
 
 
 
169
 
170
- The script starts from `PekingU/rtdetr_r50vd` and replaces the classification head with your product classes.
171
 
172
- At the end, `model/` contains the fine-tuned Hugging Face model and processor.
173
 
174
- ## 5. Test locally
175
 
176
- ```bash
177
- python -m uvicorn app.main:app --host 0.0.0.0 --port 7860
178
- ```
179
 
180
- Open:
181
 
182
  ```text
183
- http://localhost:7860/docs
 
 
184
  ```
185
 
186
- Use `POST /predict` to upload an image.
187
 
188
- Or from Python:
189
 
190
- ```python
191
- import requests
192
 
193
- with open("freezer.jpg", "rb") as f:
194
- r = requests.post(
195
- "http://localhost:7860/predict",
196
- files={"file": ("freezer.jpg", f, "image/jpeg")}
197
- )
198
 
199
- print(r.json())
200
- ```
201
 
202
- ## 6. Environment variables
203
-
204
- The API supports:
205
-
206
- ```text
207
- MODEL_ID=./model
208
- CONFIDENCE_THRESHOLD=0.35
209
- MAX_IMAGE_MB=15
 
 
210
  ```
211
 
212
- If the model is stored on the Hugging Face Hub instead of the local filesystem:
 
 
213
 
214
  ```text
215
- MODEL_ID=your-username/your-icecream-rtdetr
216
- HF_TOKEN=hf_...
 
 
 
 
 
 
217
  ```
218
 
219
- ## 7. Deploy to Hugging Face
220
-
221
- Create a Hugging Face **Docker Space** and upload this repository.
222
 
223
- The included `Dockerfile` listens on port `7860`.
224
 
225
- For a private model, add a Space secret named:
226
-
227
- ```text
228
- HF_TOKEN
229
  ```
230
 
231
- and set:
232
 
233
  ```text
234
- MODEL_ID=your-username/your-icecream-rtdetr
235
  ```
236
 
237
- If the model is public, `HF_TOKEN` is not required.
238
-
239
- ## Accuracy notes
240
-
241
- 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.
242
-
243
- Start with a confidence threshold around `0.35–0.50` and tune it on validation photos.
244
-
245
- 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.
246
 
247
- ## Why RT-DETR?
248
 
249
- 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.
 
 
 
 
 
 
 
 
250
 
251
- Hugging Face reference:
252
- https://huggingface.co/docs/transformers/model_doc/rt_detr
 
1
  ---
2
+ title: Ice Cream Dataset + Counter
3
  emoji: 🍦
4
+ colorFrom: yellow
5
  colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 6.5.1
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # 🍦 Ice Cream Dataset + Counter — Gradio Space
13
 
14
+ This is the **Gradio version** of the uploaded Ice Cream Counter project. It does **not** use Docker, FastAPI, Uvicorn, or a custom HTML frontend.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ ## What it does
17
 
18
+ 1. Upload training freezer photos.
19
+ 2. Define your product classes.
20
+ 3. Annotate each ice cream with bounding boxes.
21
+ 4. Train an RT-DETR object detector.
22
+ 5. Upload one new freezer image.
23
+ 6. Get the total count, per-product counts, confidence scores, and an annotated result image.
24
 
25
+ The model is still:
26
 
27
+ `PekingU/rtdetr_r50vd`
28
 
29
+ No YOLO is used.
30
 
31
+ ## Create the Hugging Face Space
32
 
33
+ Create a new Space and choose:
34
 
35
+ - **SDK:** Gradio
36
+ - **Hardware:** GPU is strongly recommended for training
 
 
 
37
 
38
+ Then upload these files/folders:
39
 
40
+ ```text
41
+ app.py
42
+ requirements.txt
43
+ README.md
44
+ training/
 
 
 
 
 
 
 
 
 
 
 
 
45
  ```
46
 
47
+ You do **not** need:
 
 
 
 
48
 
49
  ```text
50
+ Dockerfile
51
+ FastAPI
52
+ Uvicorn
53
+ static/index.html
54
+ app/main.py
 
 
55
  ```
56
 
57
+ ## Persistent dataset and model
 
 
 
 
 
 
58
 
59
+ The app stores:
 
 
60
 
61
  ```text
62
+ images/
63
+ dataset.json
64
+ model/
65
+ generated_dataset/
 
 
 
 
 
 
 
66
  ```
67
 
68
+ When `/data` is available and writable, the app automatically uses:
69
 
70
+ ```text
71
+ /data/icecream_counter/
72
+ ```
 
 
 
73
 
74
+ You can also explicitly set:
75
 
76
+ ```text
77
+ DATA_DIR=/data/icecream_counter
78
+ ```
79
 
80
+ in the Space variables.
81
 
82
+ ### Important Hugging Face storage point
 
 
83
 
84
+ A normal Space filesystem is not permanent storage across every rebuild/restart. If you need the dataset and trained model to survive Space restarts/rebuilds, attach **persistent storage** to the Space or move the data/model to an external persistent service.
85
 
86
+ The Gradio conversion itself does not change this storage rule.
87
 
88
+ ## Annotation workflow
89
 
90
+ In the **Annotate** tab:
 
 
 
 
 
 
 
 
91
 
92
+ 1. Select a training image.
93
+ 2. Select the class.
94
+ 3. Enter the bounding box in the original image's pixel coordinates:
95
+ - X
96
+ - Y
97
+ - Width
98
+ - Height
99
+ 4. Click **Add Box**.
100
+ 5. Repeat for every ice cream.
101
+ 6. Use **Delete Box** or **Clear All Boxes** when needed.
102
 
103
+ The preview displays the saved boxes.
104
 
105
+ This coordinate-based annotation UI is intentionally implemented entirely in Gradio/Python so it does not depend on a custom JavaScript/FastAPI frontend.
106
 
107
+ ## Training
108
 
109
+ The app creates an 80/20 COCO train/validation split from the annotated images and starts the existing RT-DETR training script.
 
 
110
 
111
+ Default settings:
112
 
113
  ```text
114
+ Epochs: 30
115
+ Batch size: 2
116
+ Learning rate: 1e-5
117
  ```
118
 
119
+ For an initial test on a small dataset, use fewer epochs such as 2–5. Once everything works, increase the epochs.
120
 
121
+ A GPU Space is strongly recommended.
122
 
123
+ ## Counting
 
124
 
125
+ After training, open the **Count** tab and upload one image.
 
 
 
 
126
 
127
+ The result contains:
 
128
 
129
+ ```json
130
+ {
131
+ "total": 31,
132
+ "counts": {
133
+ "cone": 6,
134
+ "correto": 7,
135
+ "cornetto": 12,
136
+ "magnum": 6
137
+ }
138
+ }
139
  ```
140
 
141
+ The result image also shows the detected bounding boxes and confidence values.
142
+
143
+ ## Default classes
144
 
145
  ```text
146
+ cornetto
147
+ magnum
148
+ correto
149
+ cone
150
+ cup
151
+ sandwich
152
+ stick
153
+ other
154
  ```
155
 
156
+ You can change them from the Dataset tab.
 
 
157
 
158
+ ## Local test
159
 
160
+ ```bash
161
+ pip install -r requirements.txt
162
+ python app.py
 
163
  ```
164
 
165
+ Then open:
166
 
167
  ```text
168
+ http://localhost:7860
169
  ```
170
 
171
+ ## Recommended Space setup
 
 
 
 
 
 
 
 
172
 
173
+ For the first deployment:
174
 
175
+ 1. Create the Space as **Gradio**.
176
+ 2. Upload `app.py`, `requirements.txt`, `README.md`, and `training/`.
177
+ 3. Wait for dependencies to install.
178
+ 4. Open the Dataset tab.
179
+ 5. Upload 2+ training images.
180
+ 6. Annotate them.
181
+ 7. Start with 2–5 epochs to verify training.
182
+ 8. After the model finishes, test the Count tab.
183
+ 9. For serious training, attach a GPU and persistent storage.
184
 
 
 
app.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import threading
8
+ from collections import Counter
9
+ from pathlib import Path
10
+
11
+ import gradio as gr
12
+ import torch
13
+ from PIL import Image, ImageDraw
14
+ from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
15
+
16
+ # Hugging Face Spaces can mount persistent storage at /data.
17
+ # DATA_DIR can be overridden in Space Settings -> Variables.
18
+ if os.getenv("DATA_DIR"):
19
+ BASE = Path(os.environ["DATA_DIR"])
20
+ elif Path("/data").exists() and os.access("/data", os.W_OK):
21
+ BASE = Path("/data") / "icecream_counter"
22
+ else:
23
+ BASE = Path("./data")
24
+
25
+ ROOT = Path(__file__).resolve().parent
26
+ IMAGE_DIR = BASE / "images"
27
+ DATASET_FILE = BASE / "dataset.json"
28
+ MODEL_DIR = BASE / "model"
29
+ GENERATED_DIR = BASE / "generated_dataset"
30
+ CLASSES_FILE = ROOT / "training" / "classes.txt"
31
+
32
+ IMAGE_DIR.mkdir(parents=True, exist_ok=True)
33
+ BASE.mkdir(parents=True, exist_ok=True)
34
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
35
+
36
+ CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.35"))
37
+ MAX_IMAGE_MB = int(os.getenv("MAX_IMAGE_MB", "15"))
38
+
39
+ _training = {"running": False, "message": "not started", "error": None}
40
+ _model = None
41
+ _processor = None
42
+ _model_lock = threading.Lock()
43
+ _annotation_click = None
44
+
45
+
46
+ def load_dataset():
47
+ if not DATASET_FILE.exists():
48
+ return {"images": [], "classes": read_classes()}
49
+ try:
50
+ data = json.loads(DATASET_FILE.read_text(encoding="utf-8"))
51
+ data.setdefault("images", [])
52
+ data["classes"] = read_classes()
53
+ return data
54
+ except Exception:
55
+ return {"images": [], "classes": read_classes()}
56
+
57
+
58
+ def save_dataset(data):
59
+ data["classes"] = read_classes()
60
+ tmp = DATASET_FILE.with_suffix(".tmp")
61
+ tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
62
+ tmp.replace(DATASET_FILE)
63
+
64
+
65
+ def read_classes():
66
+ if not CLASSES_FILE.exists():
67
+ return []
68
+ return [x.strip() for x in CLASSES_FILE.read_text(encoding="utf-8").splitlines() if x.strip()]
69
+
70
+
71
+ def image_path(image_id):
72
+ return IMAGE_DIR / f"{image_id}.jpg"
73
+
74
+
75
+ def model_ready():
76
+ return (MODEL_DIR / "config.json").exists()
77
+
78
+
79
+ def load_model():
80
+ global _model, _processor
81
+ if not model_ready():
82
+ raise RuntimeError("No trained model yet. Train the model first.")
83
+ with _model_lock:
84
+ if _model is None:
85
+ _processor = RTDetrImageProcessor.from_pretrained(str(MODEL_DIR))
86
+ _model = RTDetrForObjectDetection.from_pretrained(str(MODEL_DIR))
87
+ _model.to("cuda" if torch.cuda.is_available() else "cpu")
88
+ _model.eval()
89
+ return _processor, _model
90
+
91
+
92
+ def dataset_status():
93
+ data = load_dataset()
94
+ annotated = sum(bool(x.get("annotations")) for x in data["images"])
95
+ return (
96
+ f"**Dataset:** {len(data['images'])} images | "
97
+ f"**Annotated:** {annotated} | "
98
+ f"**Classes:** {len(read_classes())} | "
99
+ f"**Model:** {'READY' if model_ready() else 'NOT TRAINED'} | "
100
+ f"**Storage:** `{BASE}`"
101
+ )
102
+
103
+
104
+ def image_choices():
105
+ data = load_dataset()
106
+ return [(x["filename"], x["id"]) for x in data["images"]]
107
+
108
+
109
+ def upload_training_images(files):
110
+ if not files:
111
+ return dataset_status(), gr.update(choices=image_choices()), "No files selected."
112
+
113
+ data = load_dataset()
114
+ saved = 0
115
+ skipped = []
116
+ for f in files:
117
+ path = Path(getattr(f, "path", getattr(f, "name", f)))
118
+ try:
119
+ raw = path.read_bytes()
120
+ if len(raw) > MAX_IMAGE_MB * 1024 * 1024:
121
+ skipped.append(f"{path.name}: over {MAX_IMAGE_MB} MB")
122
+ continue
123
+ im = Image.open(io.BytesIO(raw)).convert("RGB")
124
+ image_id = __import__("uuid").uuid4().hex
125
+ out = image_path(image_id)
126
+ im.save(out, "JPEG", quality=95)
127
+ data["images"].append({
128
+ "id": image_id,
129
+ "filename": path.name,
130
+ "width": im.width,
131
+ "height": im.height,
132
+ "annotations": [],
133
+ })
134
+ saved += 1
135
+ except Exception as e:
136
+ skipped.append(f"{path.name}: {e}")
137
+
138
+ save_dataset(data)
139
+ msg = f"Saved {saved} image(s)."
140
+ if skipped:
141
+ msg += "\nSkipped:\n- " + "\n- ".join(skipped)
142
+ return dataset_status(), gr.update(choices=image_choices()), msg
143
+
144
+
145
+ def load_editor(image_id):
146
+ if not image_id:
147
+ return None, "Select an image.", [], None, None
148
+ data = load_dataset()
149
+ item = next((x for x in data["images"] if x["id"] == image_id), None)
150
+ if not item:
151
+ return None, "Image not found.", [], None, None
152
+ p = image_path(image_id)
153
+ return (
154
+ str(p),
155
+ f"**{item['filename']}** — {item['width']} × {item['height']} px",
156
+ item.get("annotations", []),
157
+ item["width"],
158
+ item["height"],
159
+ )
160
+
161
+
162
+ def draw_annotations(image_id):
163
+ if not image_id:
164
+ return None
165
+ p = image_path(image_id)
166
+ if not p.exists():
167
+ return None
168
+ image = Image.open(p).convert("RGB")
169
+ data = load_dataset()
170
+ item = next((x for x in data["images"] if x["id"] == image_id), None)
171
+ if not item:
172
+ return image
173
+ draw = ImageDraw.Draw(image)
174
+ for i, a in enumerate(item.get("annotations", []), 1):
175
+ x, y, w, h = a["box"]
176
+ color = "red"
177
+ draw.rectangle([x, y, x+w, y+h], outline=color, width=4)
178
+ label = f"{i}. {a['class']}"
179
+ draw.rectangle([x, max(0, y-22), x+max(100, len(label)*8), y], fill=color)
180
+ draw.text((x+3, max(0, y-20)), label, fill="white")
181
+ return image
182
+
183
+
184
+ def save_classes(text):
185
+ classes = [x.strip() for x in (text or "").splitlines() if x.strip()]
186
+ if not classes:
187
+ return "At least one class is required.", gr.update(choices=[]), dataset_status()
188
+ if len(set(classes)) != len(classes):
189
+ return "Classes must be unique.", gr.update(choices=classes), dataset_status()
190
+ CLASSES_FILE.write_text("\n".join(classes) + "\n", encoding="utf-8")
191
+ data = load_dataset()
192
+ save_dataset(data)
193
+ return f"Saved {len(classes)} classes.", gr.update(choices=classes, value=classes[0]), dataset_status()
194
+
195
+
196
+ def add_annotation(image_id, cls, x, y, w, h):
197
+ if not image_id:
198
+ return None, "Select an image first.", [], []
199
+ if not cls:
200
+ return None, "Select a class.", [], []
201
+ try:
202
+ x, y, w, h = map(float, [x, y, w, h])
203
+ except Exception:
204
+ return None, "Coordinates must be numbers.", [], []
205
+ if w <= 0 or h <= 0:
206
+ return None, "Width and height must be greater than zero.", [], []
207
+ data = load_dataset()
208
+ item = next((z for z in data["images"] if z["id"] == image_id), None)
209
+ if not item:
210
+ return None, "Image not found.", [], []
211
+ x = max(0, min(x, item["width"] - 1))
212
+ y = max(0, min(y, item["height"] - 1))
213
+ w = min(w, item["width"] - x)
214
+ h = min(h, item["height"] - y)
215
+ item.setdefault("annotations", []).append({"class": cls, "box": [x, y, w, h]})
216
+ save_dataset(data)
217
+ anns = item["annotations"]
218
+ return draw_annotations(image_id), f"Added {cls}: [{x:.0f}, {y:.0f}, {w:.0f}, {h:.0f}]", anns, anns
219
+
220
+
221
+ def remove_annotation(image_id, index):
222
+ if not image_id:
223
+ return None, "Select an image first.", []
224
+ data = load_dataset()
225
+ item = next((z for z in data["images"] if z["id"] == image_id), None)
226
+ if not item:
227
+ return None, "Image not found.", []
228
+ try:
229
+ idx = int(index) - 1
230
+ except Exception:
231
+ return None, "Enter the annotation number to delete.", item.get("annotations", [])
232
+ anns = item.get("annotations", [])
233
+ if idx < 0 or idx >= len(anns):
234
+ return None, "Annotation number not found.", anns
235
+ deleted = anns.pop(idx)
236
+ save_dataset(data)
237
+ return draw_annotations(image_id), f"Deleted annotation {index}: {deleted['class']}", anns
238
+
239
+
240
+ def clear_annotations(image_id):
241
+ if not image_id:
242
+ return None, "Select an image first.", []
243
+ data = load_dataset()
244
+ item = next((z for z in data["images"] if z["id"] == image_id), None)
245
+ if not item:
246
+ return None, "Image not found.", []
247
+ item["annotations"] = []
248
+ save_dataset(data)
249
+ return draw_annotations(image_id), "Annotations cleared.", []
250
+
251
+
252
+ def build_coco():
253
+ data = load_dataset()
254
+ classes = read_classes()
255
+ if not classes:
256
+ raise RuntimeError("No classes configured.")
257
+ items = [x for x in data["images"] if x.get("annotations")]
258
+ if len(items) < 2:
259
+ raise RuntimeError("Annotate at least 2 images before training.")
260
+
261
+ # Deterministic split; upload a reasonably shuffled dataset.
262
+ split = max(1, int(len(items) * 0.8))
263
+ if split == len(items):
264
+ split -= 1
265
+ train_items, val_items = items[:split], items[split:]
266
+ category_id = {name: i + 1 for i, name in enumerate(classes)}
267
+
268
+ def make_coco(selected):
269
+ images, annotations = [], []
270
+ ann_id = 1
271
+ for item in selected:
272
+ images.append({
273
+ "id": item["id"],
274
+ "file_name": item["id"] + ".jpg",
275
+ "width": item["width"],
276
+ "height": item["height"],
277
+ })
278
+ for ann in item["annotations"]:
279
+ x, y, w, h = ann["box"]
280
+ annotations.append({
281
+ "id": ann_id,
282
+ "image_id": item["id"],
283
+ "category_id": category_id[ann["class"]],
284
+ "bbox": [x, y, w, h],
285
+ "area": w*h,
286
+ "iscrowd": 0,
287
+ })
288
+ ann_id += 1
289
+ return {
290
+ "images": images,
291
+ "annotations": annotations,
292
+ "categories": [{"id": i+1, "name": n} for i, n in enumerate(classes)]
293
+ }
294
+
295
+ if GENERATED_DIR.exists():
296
+ shutil.rmtree(GENERATED_DIR)
297
+ for name, selected in [("train", train_items), ("val", val_items)]:
298
+ d = GENERATED_DIR / name
299
+ (d / "images").mkdir(parents=True, exist_ok=True)
300
+ for item in selected:
301
+ shutil.copy2(image_path(item["id"]), d / "images" / f"{item['id']}.jpg")
302
+ (d / "annotations.json").write_text(
303
+ json.dumps(make_coco(selected), indent=2), encoding="utf-8"
304
+ )
305
+
306
+
307
+ def run_training(epochs, batch_size, learning_rate):
308
+ global _training, _model
309
+ try:
310
+ _training = {"running": True, "message": "building COCO dataset", "error": None}
311
+ build_coco()
312
+ _training["message"] = "training RT-DETR"
313
+ cmd = [
314
+ sys.executable, str(ROOT / "training" / "train.py"),
315
+ "--train-dir", str(GENERATED_DIR / "train"),
316
+ "--val-dir", str(GENERATED_DIR / "val"),
317
+ "--classes", str(CLASSES_FILE),
318
+ "--output-dir", str(MODEL_DIR),
319
+ "--epochs", str(int(epochs)),
320
+ "--batch-size", str(int(batch_size)),
321
+ "--learning-rate", str(float(learning_rate)),
322
+ ]
323
+ result = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
324
+ if result.returncode != 0:
325
+ raise RuntimeError((result.stderr or result.stdout)[-8000:])
326
+ _model = None
327
+ _training = {"running": False, "message": "training complete", "error": None}
328
+ except Exception as e:
329
+ _training = {"running": False, "message": "training failed", "error": str(e)}
330
+
331
+
332
+ def start_training(epochs, batch_size, learning_rate):
333
+ if _training["running"]:
334
+ return "Training is already running."
335
+ threading.Thread(
336
+ target=run_training,
337
+ args=(epochs, batch_size, learning_rate),
338
+ daemon=True,
339
+ ).start()
340
+ return "Training started in the background. Use Refresh Training Status."
341
+
342
+
343
+ def training_status():
344
+ return json.dumps(_training, indent=2)
345
+
346
+
347
+ def count_image(image):
348
+ if image is None:
349
+ return None, "Upload an image first.", {}
350
+ if not model_ready():
351
+ return None, "Model is not trained yet. Go to Training.", {}
352
+ try:
353
+ proc, detector = load_model()
354
+ image = image.convert("RGB") if isinstance(image, Image.Image) else Image.fromarray(image).convert("RGB")
355
+ device = next(detector.parameters()).device
356
+ inputs = proc(images=image, return_tensors="pt")
357
+ inputs = {k: v.to(device) if torch.is_tensor(v) else v for k, v in inputs.items()}
358
+ with torch.inference_mode():
359
+ outputs = detector(**inputs)
360
+ target_sizes = torch.tensor([[image.height, image.width]], device=device)
361
+ result = proc.post_process_object_detection(
362
+ outputs, threshold=CONFIDENCE_THRESHOLD, target_sizes=target_sizes
363
+ )[0]
364
+
365
+ detections = []
366
+ counts = Counter()
367
+ for score, label, box in zip(result["scores"], result["labels"], result["boxes"]):
368
+ s = float(score.item())
369
+ cls = detector.config.id2label[int(label.item())]
370
+ coords = [round(float(v), 2) for v in box.tolist()]
371
+ detections.append({"class": cls, "confidence": round(s, 4), "box": coords})
372
+ counts[cls] += 1
373
+
374
+ out = image.copy()
375
+ draw = ImageDraw.Draw(out)
376
+ for d in detections:
377
+ x1, y1, x2, y2 = d["box"]
378
+ draw.rectangle([x1, y1, x2, y2], outline="red", width=4)
379
+ label = f"{d['class']} {d['confidence']:.2f}"
380
+ draw.rectangle([x1, max(0, y1-22), x1+max(120, len(label)*8), y1], fill="red")
381
+ draw.text((x1+3, max(0, y1-20)), label, fill="white")
382
+
383
+ response = {
384
+ "total": len(detections),
385
+ "counts": dict(sorted(counts.items())),
386
+ "detections": detections,
387
+ }
388
+ return out, json.dumps(response, indent=2), response["counts"]
389
+ except Exception as e:
390
+ return None, f"Counting failed: {e}", {}
391
+
392
+
393
+ # ----- Gradio UI -----
394
+ CSS = """
395
+ .gradio-container { max-width: 1250px !important; }
396
+ h1 { margin-bottom: 0.2rem !important; }
397
+ .status { padding: 10px 14px; border-radius: 10px; }
398
+ """
399
+
400
+ with gr.Blocks(title="Ice Cream Dataset + Counter", css=CSS) as demo:
401
+ gr.Markdown("# 🍦 Ice Cream Dataset + Counter\nUpload and annotate training images, train RT-DETR, then count ice creams in new images.")
402
+ status = gr.Markdown(dataset_status(), elem_classes="status")
403
+
404
+ with gr.Tab("1 · Dataset"):
405
+ gr.Markdown("### Upload training images")
406
+ files = gr.Files(file_count="multiple", file_types=["image"], label="Images")
407
+ upload_btn = gr.Button("Save Images", variant="primary")
408
+ upload_msg = gr.Markdown()
409
+ # Dataset selector
410
+ image_select = gr.Dropdown(choices=image_choices(), label="Training image", interactive=True)
411
+ refresh_btn = gr.Button("Refresh Dataset")
412
+ refresh_btn.click(lambda: (dataset_status(), gr.update(choices=image_choices())), None, [status, image_select])
413
+
414
+ gr.Markdown("### Classes")
415
+ class_text = gr.Textbox(value="\n".join(read_classes()), lines=8, label="One class per line")
416
+ save_class_btn = gr.Button("Save Classes")
417
+ class_msg = gr.Markdown()
418
+ # Class dropdown is updated after the Annotate tab creates it.
419
+
420
+ with gr.Tab("2 · Annotate"):
421
+ gr.Markdown(
422
+ "Select an image. To create a box, enter **X, Y, Width, Height** in original image pixels. "
423
+ "The preview shows saved boxes. This is deliberately simple and works reliably inside Gradio Spaces."
424
+ )
425
+ with gr.Row():
426
+ with gr.Column(scale=2):
427
+ editor_image = gr.Image(label="Training image", type="pil", interactive=False)
428
+ editor_info = gr.Markdown()
429
+ with gr.Column(scale=1):
430
+ ann_class = gr.Dropdown(choices=read_classes(), value=(read_classes()[0] if read_classes() else None), label="Class")
431
+ save_class_btn.click(save_classes, class_text, [class_msg, ann_class, status], preprocess=False)
432
+ with gr.Row():
433
+ x = gr.Number(label="X", value=0)
434
+ y = gr.Number(label="Y", value=0)
435
+ with gr.Row():
436
+ w = gr.Number(label="Width", value=100)
437
+ h = gr.Number(label="Height", value=100)
438
+ add_btn = gr.Button("➕ Add Box", variant="primary")
439
+ delete_index = gr.Number(label="Annotation # to delete", value=1, precision=0)
440
+ delete_btn = gr.Button("Delete Box")
441
+ clear_btn = gr.Button("Clear All Boxes")
442
+ annotations = gr.JSON(label="Saved annotations")
443
+ ann_msg = gr.Markdown()
444
+
445
+ def refresh_editor(image_id):
446
+ img, info, anns, _, _ = load_editor(image_id)
447
+ return draw_annotations(image_id), info, anns
448
+
449
+ image_select.change(refresh_editor, image_select, [editor_image, editor_info, annotations])
450
+ add_btn.click(add_annotation, [image_select, ann_class, x, y, w, h], [editor_image, ann_msg, annotations, annotations])
451
+ delete_btn.click(remove_annotation, [image_select, delete_index], [editor_image, ann_msg, annotations])
452
+ clear_btn.click(clear_annotations, image_select, [editor_image, ann_msg, annotations])
453
+
454
+ with gr.Tab("3 · Training"):
455
+ gr.Markdown("### Train RT-DETR")
456
+ gr.Markdown("Training runs in the Space process. A GPU Space is strongly recommended for practical training speed.")
457
+ with gr.Row():
458
+ epochs = gr.Number(value=int(os.getenv("EPOCHS", "30")), label="Epochs", precision=0)
459
+ batch = gr.Number(value=int(os.getenv("BATCH_SIZE", "2")), label="Batch size", precision=0)
460
+ lr = gr.Number(value=float(os.getenv("LEARNING_RATE", "1e-5")), label="Learning rate")
461
+ train_btn = gr.Button("🚀 Start Training", variant="primary")
462
+ refresh_train = gr.Button("Refresh Training Status")
463
+ train_out = gr.Code(value=training_status, language="json", label="Training status")
464
+ train_btn.click(start_training, [epochs, batch, lr], train_out)
465
+ refresh_train.click(training_status, None, train_out)
466
+
467
+ with gr.Tab("4 · Count"):
468
+ gr.Markdown("### Count ice creams")
469
+ count_in = gr.Image(type="pil", sources=["upload", "clipboard"], label="Image to count")
470
+ count_btn = gr.Button("🍦 Count", variant="primary")
471
+ count_out = gr.Image(label="Detections")
472
+ count_json = gr.Code(language="json", label="Detection details")
473
+ count_table = gr.JSON(label="Counts by class")
474
+ count_btn.click(count_image, count_in, [count_out, count_json, count_table])
475
+
476
+ # Correct the upload event now that image_select exists.
477
+ upload_btn.click(
478
+ upload_training_images,
479
+ files,
480
+ [status, image_select, upload_msg],
481
+ preprocess=False,
482
+ queue=False,
483
+ )
484
+ # The earlier placeholder event is harmlessly superseded by this real event.
485
+
486
+ demo.load(lambda: (dataset_status(), gr.update(choices=image_choices()), gr.update(choices=read_classes())),
487
+ None, [status, image_select, ann_class])
488
+
489
+ if __name__ == "__main__":
490
+ demo.queue().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
requirements.txt CHANGED
@@ -1,11 +1,9 @@
 
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
 
1
+ gradio>=6.5,<7
2
  torch>=2.3
3
  torchvision>=0.18
4
  transformers>=4.50
5
  huggingface_hub>=0.25
6
  Pillow>=10.0
 
 
 
7
  numpy>=1.26
8
  tqdm>=4.66
9
  pycocotools>=2.0.8