Spaces:
Running on Zero
Running on Zero
Upload 3 files
Browse files
README.md
CHANGED
|
@@ -89,22 +89,23 @@ The Gradio conversion itself does not change this storage rule.
|
|
| 89 |
|
| 90 |
## Annotation workflow
|
| 91 |
|
| 92 |
-
In the **Annotate** tab
|
| 93 |
|
| 94 |
1. Select a training image.
|
| 95 |
-
2.
|
| 96 |
-
3.
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
- Width
|
| 100 |
- Height
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
This coordinate-based annotation UI is intentionally implemented entirely in Gradio/Python so it does not depend on a custom JavaScript/FastAPI frontend.
|
| 108 |
|
| 109 |
## Training
|
| 110 |
|
|
@@ -145,13 +146,15 @@ The result image also shows the detected bounding boxes and confidence values.
|
|
| 145 |
## Default classes
|
| 146 |
|
| 147 |
```text
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
other
|
| 156 |
```
|
| 157 |
|
|
|
|
| 89 |
|
| 90 |
## Annotation workflow
|
| 91 |
|
| 92 |
+
In the **Annotate** tab the actual uploaded training image is displayed directly using Gradio's Image component. This avoids browser canvas/JavaScript issues that can make the preview appear black.
|
| 93 |
|
| 94 |
1. Select a training image.
|
| 95 |
+
2. The real image appears in the preview.
|
| 96 |
+
3. Read the image dimensions shown below it.
|
| 97 |
+
4. Select the product class.
|
| 98 |
+
5. Enter the bounding box in original-image pixel coordinates:
|
| 99 |
+
- X (left)
|
| 100 |
+
- Y (top)
|
| 101 |
- Width
|
| 102 |
- Height
|
| 103 |
+
6. Click **Save Box**.
|
| 104 |
+
7. Saved boxes are drawn in red on the real image.
|
| 105 |
+
8. Repeat for every ice cream.
|
| 106 |
+
9. Use **Delete Box** or **Clear All Boxes** when needed.
|
| 107 |
|
| 108 |
+
This version prioritizes a reliable visible image over the previous JavaScript canvas approach.
|
|
|
|
|
|
|
| 109 |
|
| 110 |
## Training
|
| 111 |
|
|
|
|
| 146 |
## Default classes
|
| 147 |
|
| 148 |
```text
|
| 149 |
+
Carnavalita
|
| 150 |
+
Kimo-COno
|
| 151 |
+
Squizz
|
| 152 |
+
Oreo
|
| 153 |
+
Moro
|
| 154 |
+
Dulce
|
| 155 |
+
KitKat
|
| 156 |
+
Cadbury
|
| 157 |
+
Mega
|
| 158 |
other
|
| 159 |
```
|
| 160 |
|
app.py
CHANGED
|
@@ -186,48 +186,91 @@ img.onload=fit;img.src=imgSrc;window.addEventListener('resize',fit);
|
|
| 186 |
})();</script>""" % (json.dumps(src), int(item["width"]), int(item["height"]), boxes)
|
| 187 |
|
| 188 |
|
| 189 |
-
|
|
|
|
|
|
|
| 190 |
if not image_id:
|
| 191 |
-
return
|
| 192 |
-
|
| 193 |
-
if not
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def draw_annotations(image_id):
|
| 198 |
"""Render the annotation canvas for the selected image."""
|
| 199 |
return annotation_canvas_html(image_id)
|
| 200 |
|
| 201 |
|
| 202 |
def add_annotation(image_id, cls, x, y, w, h):
|
| 203 |
-
if not image_id:return
|
| 204 |
-
if not cls:return
|
| 205 |
try:x,y,w,h=map(float,[x,y,w,h])
|
| 206 |
-
except:return
|
| 207 |
-
if w<=0 or h<=0:return
|
| 208 |
-
data=load_dataset()
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
|
| 215 |
def remove_annotation(image_id,index):
|
| 216 |
-
if not image_id:return
|
| 217 |
-
data=load_dataset()
|
| 218 |
-
|
|
|
|
| 219 |
try:idx=int(index)-1
|
| 220 |
-
except:return
|
| 221 |
anns=item.get("annotations",[])
|
| 222 |
-
if idx<0 or idx>=len(anns):return
|
| 223 |
-
deleted=anns.pop(idx);save_dataset(data)
|
|
|
|
| 224 |
|
| 225 |
|
| 226 |
def clear_annotations(image_id):
|
| 227 |
-
if not image_id:return
|
| 228 |
-
data=load_dataset()
|
| 229 |
-
|
| 230 |
-
item
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
def save_classes(text):
|
| 233 |
classes = [x.strip() for x in (text or "").splitlines() if x.strip()]
|
|
@@ -413,30 +456,29 @@ with gr.Blocks(title="Ice Cream Dataset + Counter") as demo:
|
|
| 413 |
# Class dropdown is updated after the Annotate tab creates it.
|
| 414 |
|
| 415 |
with gr.Tab("2 · Annotate"):
|
| 416 |
-
gr.Markdown("###
|
| 417 |
-
gr.Markdown("
|
| 418 |
with gr.Row():
|
| 419 |
with gr.Column(scale=3):
|
| 420 |
-
|
| 421 |
editor_info = gr.Markdown()
|
| 422 |
with gr.Column(scale=1):
|
| 423 |
ann_class = gr.Dropdown(choices=read_classes(), value=(read_classes()[0] if read_classes() else None), label="Class")
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
h = gr.Number(label="Height", value=0, elem_id="anno-h")
|
| 429 |
add_btn = gr.Button("💾 Save Box", variant="primary")
|
| 430 |
-
gr.Markdown(
|
| 431 |
delete_index = gr.Number(label="Annotation # to delete", value=1, precision=0)
|
| 432 |
delete_btn = gr.Button("Delete Box")
|
| 433 |
clear_btn = gr.Button("Clear All Boxes")
|
| 434 |
annotations = gr.JSON(label="Saved annotations")
|
| 435 |
ann_msg = gr.Markdown()
|
| 436 |
-
image_select.change(refresh_editor, image_select, [
|
| 437 |
-
add_btn.click(add_annotation, [image_select, ann_class, x, y, w, h], [
|
| 438 |
-
delete_btn.click(remove_annotation, [image_select, delete_index], [
|
| 439 |
-
clear_btn.click(clear_annotations, image_select, [
|
| 440 |
|
| 441 |
with gr.Tab("3 · Training"):
|
| 442 |
gr.Markdown("### Train RT-DETR")
|
|
|
|
| 186 |
})();</script>""" % (json.dumps(src), int(item["width"]), int(item["height"]), boxes)
|
| 187 |
|
| 188 |
|
| 189 |
+
|
| 190 |
+
def annotation_preview_image(image_id):
|
| 191 |
+
"""Return the real PIL image for Gradio's Image component."""
|
| 192 |
if not image_id:
|
| 193 |
+
return None
|
| 194 |
+
p = image_path(image_id)
|
| 195 |
+
if not p.exists():
|
| 196 |
+
return None
|
| 197 |
+
try:
|
| 198 |
+
return Image.open(p).convert("RGB")
|
| 199 |
+
except Exception:
|
| 200 |
+
return None
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def annotation_preview_with_boxes(image_id):
|
| 204 |
+
image = annotation_preview_image(image_id)
|
| 205 |
+
if image is None:
|
| 206 |
+
return None
|
| 207 |
+
data = load_dataset()
|
| 208 |
+
item = next((x for x in data["images"] if x["id"] == image_id), None)
|
| 209 |
+
if not item:
|
| 210 |
+
return image
|
| 211 |
+
out = image.copy()
|
| 212 |
+
draw = ImageDraw.Draw(out)
|
| 213 |
+
for i, a in enumerate(item.get("annotations", []), 1):
|
| 214 |
+
x, y, w, h = a["box"]
|
| 215 |
+
draw.rectangle([x, y, x+w, y+h], outline="red", width=5)
|
| 216 |
+
label = f"{i}. {a['class']}"
|
| 217 |
+
y0 = max(0, y-24)
|
| 218 |
+
draw.rectangle([x, y0, x+max(130, len(label)*9), y0+24], fill="red")
|
| 219 |
+
draw.text((x+4, y0+4), label, fill="white")
|
| 220 |
+
return out
|
| 221 |
|
| 222 |
|
| 223 |
+
def refresh_editor(image_id):
|
| 224 |
+
if not image_id:
|
| 225 |
+
return None, "Select an image.", []
|
| 226 |
+
data=load_dataset()
|
| 227 |
+
item=next((x for x in data["images"] if x["id"]==image_id),None)
|
| 228 |
+
if not item:
|
| 229 |
+
return None,"Image not found.",[]
|
| 230 |
+
return annotation_preview_with_boxes(image_id), f"**{item['filename']}** — {item['width']} × {item['height']} px", item.get("annotations",[])
|
| 231 |
+
|
| 232 |
def draw_annotations(image_id):
|
| 233 |
"""Render the annotation canvas for the selected image."""
|
| 234 |
return annotation_canvas_html(image_id)
|
| 235 |
|
| 236 |
|
| 237 |
def add_annotation(image_id, cls, x, y, w, h):
|
| 238 |
+
if not image_id:return None,"Select an image first.",[]
|
| 239 |
+
if not cls:return annotation_preview_with_boxes(image_id),"Select a class first.",[]
|
| 240 |
try:x,y,w,h=map(float,[x,y,w,h])
|
| 241 |
+
except:return annotation_preview_with_boxes(image_id),"Enter box coordinates first.",[]
|
| 242 |
+
if w<=0 or h<=0:return annotation_preview_with_boxes(image_id),"Box must have a width and height.",[]
|
| 243 |
+
data=load_dataset()
|
| 244 |
+
item=next((z for z in data["images"] if z["id"]==image_id),None)
|
| 245 |
+
if not item:return None,"Image not found.",[]
|
| 246 |
+
x=max(0,min(x,item["width"]-1)); y=max(0,min(y,item["height"]-1))
|
| 247 |
+
w=min(w,item["width"]-x); h=min(h,item["height"]-y)
|
| 248 |
+
item.setdefault("annotations",[]).append({"class":cls,"box":[x,y,w,h]})
|
| 249 |
+
save_dataset(data)
|
| 250 |
+
return annotation_preview_with_boxes(image_id),f"Saved {cls}: [{x:.0f}, {y:.0f}, {w:.0f}, {h:.0f}]",item["annotations"]
|
| 251 |
|
| 252 |
|
| 253 |
def remove_annotation(image_id,index):
|
| 254 |
+
if not image_id:return None,"Select an image first.",[]
|
| 255 |
+
data=load_dataset()
|
| 256 |
+
item=next((z for z in data["images"] if z["id"]==image_id),None)
|
| 257 |
+
if not item:return None,"Image not found.",[]
|
| 258 |
try:idx=int(index)-1
|
| 259 |
+
except:return annotation_preview_with_boxes(image_id),"Enter an annotation number.",item.get("annotations",[])
|
| 260 |
anns=item.get("annotations",[])
|
| 261 |
+
if idx<0 or idx>=len(anns):return annotation_preview_with_boxes(image_id),"Annotation number not found.",anns
|
| 262 |
+
deleted=anns.pop(idx);save_dataset(data)
|
| 263 |
+
return annotation_preview_with_boxes(image_id),f"Deleted annotation {index}: {deleted['class']}",anns
|
| 264 |
|
| 265 |
|
| 266 |
def clear_annotations(image_id):
|
| 267 |
+
if not image_id:return None,"Select an image first.",[]
|
| 268 |
+
data=load_dataset()
|
| 269 |
+
item=next((z for z in data["images"] if z["id"]==image_id),None)
|
| 270 |
+
if not item:return None,"Image not found.",[]
|
| 271 |
+
item["annotations"]=[];save_dataset(data)
|
| 272 |
+
return annotation_preview_image(image_id),"Annotations cleared.",[]
|
| 273 |
+
|
| 274 |
|
| 275 |
def save_classes(text):
|
| 276 |
classes = [x.strip() for x in (text or "").splitlines() if x.strip()]
|
|
|
|
| 456 |
# Class dropdown is updated after the Annotate tab creates it.
|
| 457 |
|
| 458 |
with gr.Tab("2 · Annotate"):
|
| 459 |
+
gr.Markdown("### See the real training image and annotate it")
|
| 460 |
+
gr.Markdown("The actual uploaded photo is displayed below. Select a class and enter the box coordinates in the original image pixels, then click **Save Box**. Saved boxes appear in red.")
|
| 461 |
with gr.Row():
|
| 462 |
with gr.Column(scale=3):
|
| 463 |
+
annotation_preview = gr.Image(label="Training image", type="pil", interactive=False, height=650)
|
| 464 |
editor_info = gr.Markdown()
|
| 465 |
with gr.Column(scale=1):
|
| 466 |
ann_class = gr.Dropdown(choices=read_classes(), value=(read_classes()[0] if read_classes() else None), label="Class")
|
| 467 |
+
x = gr.Number(label="X (left)", value=0, precision=0)
|
| 468 |
+
y = gr.Number(label="Y (top)", value=0, precision=0)
|
| 469 |
+
w = gr.Number(label="Width", value=0, precision=0)
|
| 470 |
+
h = gr.Number(label="Height", value=0, precision=0)
|
|
|
|
| 471 |
add_btn = gr.Button("💾 Save Box", variant="primary")
|
| 472 |
+
gr.Markdown("Example: X=120, Y=80, Width=180, Height=300.")
|
| 473 |
delete_index = gr.Number(label="Annotation # to delete", value=1, precision=0)
|
| 474 |
delete_btn = gr.Button("Delete Box")
|
| 475 |
clear_btn = gr.Button("Clear All Boxes")
|
| 476 |
annotations = gr.JSON(label="Saved annotations")
|
| 477 |
ann_msg = gr.Markdown()
|
| 478 |
+
image_select.change(refresh_editor, image_select, [annotation_preview, editor_info, annotations])
|
| 479 |
+
add_btn.click(add_annotation, [image_select, ann_class, x, y, w, h], [annotation_preview, ann_msg, annotations])
|
| 480 |
+
delete_btn.click(remove_annotation, [image_select, delete_index], [annotation_preview, ann_msg, annotations])
|
| 481 |
+
clear_btn.click(clear_annotations, image_select, [annotation_preview, ann_msg, annotations])
|
| 482 |
|
| 483 |
with gr.Tab("3 · Training"):
|
| 484 |
gr.Markdown("### Train RT-DETR")
|