Initial commit: Wood knot detection model and GUI

This commit is contained in:
2025-12-22 14:11:39 -07:00
commit aed092f09c
20307 changed files with 785367 additions and 0 deletions

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
*.egg
# Virtual Environment
.venv/
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Model checkpoints and weights
*.pt
*.pth
*.onnx
*.blob
*.xml
*.bin
checkpoints/
weights/
runs/
# Dataset (large files)
IMAGE/
images/
*.jpg
*.jpeg
*.png
*.gif
bbox_coco_dataset.json
# Training outputs
train_output/
results/
*.log
# Jupyter
.ipynb_checkpoints/
*.ipynb
# OS
.DS_Store
Thumbs.db

148
GUI_README.md Normal file
View File

@ -0,0 +1,148 @@
# Custom Annotation GUI
A simple, **fully customizable** annotation tool built with Gradio (pure Python).
## Features
**Auto-labeling** with your trained RF-DETR model
**Manual annotation** by entering box coordinates
**Edit/delete** annotations easily
**Navigation** between images
**Export** to COCO JSON format
**100% Python** - easy to modify and extend
## Quick Start
### 1. Install dependencies
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python -m pip install gradio>=4.0.0
```
### 2. Run the GUI
**With auto-labeling (requires trained model):**
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python annotation_gui.py \
--images-dir /path/to/images \
--model-weights runs/knot_rfdetr_medium/checkpoint_best_total.pth
```
**Manual annotation only:**
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python annotation_gui.py \
--images-dir /path/to/images
```
### 3. Open in browser
Opens automatically at http://localhost:7860
## Usage
1. **Auto-Label**: Click "🤖 Auto-Label" to detect knots with your model
2. **Adjust threshold**: Lower = more detections, Higher = only confident ones
3. **Manual boxes**: Enter coordinates (x1, y1, x2, y2) and click " Add Box"
4. **Delete mistakes**: Click "🗑️ Delete Last" to remove last box
5. **Navigate**: Use "Previous" / "Next" buttons
6. **Export**: Click "💾 Export COCO" when done
## Customization Examples
### Add keyboard shortcuts
```python
# In create_ui(), add:
image_display.keyboard_shortcuts = {
"d": delete_btn.click, # Press 'd' to delete
"n": next_btn.click, # Press 'n' for next
}
```
### Add interactive drawing
```python
# Replace manual coordinates with image annotator:
from gradio_image_annotation import image_annotator
annotator = image_annotator(
label="Draw boxes",
type="numpy"
)
```
### Change box colors by confidence
```python
# In draw_boxes_on_image():
color = "green" if conf > 0.8 else "yellow" if conf > 0.5 else "red"
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
```
### Add multiple label classes
```python
# Add a dropdown:
label_choice = gr.Dropdown(
choices=["knot", "crack", "hole"],
value="knot",
label="Label Type"
)
# Update box dict:
box = {
"bbox": [x1, y1, x2, y2],
"label": label_choice_value, # from the dropdown
"confidence": 1.0
}
```
### Save checkpoints automatically
```python
# In _save_annotations(), add:
import shutil
backup_path = self.ann_file.with_suffix('.backup.json')
shutil.copy(self.ann_file, backup_path)
```
### Add image filters/preprocessing
```python
# Add before annotation:
def preprocess_image(img: Image.Image) -> Image.Image:
from PIL import ImageEnhance
enhancer = ImageEnhance.Contrast(img)
return enhancer.enhance(1.5) # Increase contrast
```
## File Structure
```
annotation_gui.py
├── AnnotationApp # Main logic (easy to extend)
│ ├── auto_label_current() # Modify for different models
│ ├── add_box_manual() # Customize annotation format
│ ├── export_to_coco() # Change export format
│ └── draw_boxes_on_image() # Customize visualization
└── create_ui() # Gradio interface (add components)
```
## Advantages vs Label Studio
| Feature | Custom GUI | Label Studio |
|---------|-----------|--------------|
| **Modify code** | ✅ Easy (pure Python) | ❌ Complex (React + Python) |
| **Add features** | ✅ ~10-50 lines | ❌ Hundreds of lines |
| **Custom models** | ✅ Direct integration | ⚠️ Need ONNX export |
| **Learning curve** | ✅ Simple Gradio | ⚠️ Larger codebase |
| **Setup** | ✅ pip install | ⚠️ Docker/complex |
## Troubleshooting
**Port already in use:**
```bash
python annotation_gui.py --images-dir /path --port 7861
```
**Model not loading:**
- Check the weights path exists
- Verify it's a valid checkpoint file
- Try without `--model-weights` for manual-only mode
**Need more features?**
- Check Gradio docs: https://www.gradio.app/docs/
- Add custom components easily
- Fork and modify the code freely!

244
LABELING_GUIDE.md Normal file
View File

@ -0,0 +1,244 @@
# Image Labeling Guide for Knot Detection
## Quick Start: Label Studio (Recommended)
### 1. Install and Launch
```bash
# Install (outside your project venv is fine)
pip install label-studio
# Start the server
label-studio start
```
Open http://localhost:8080 in your browser.
### 2. Create Your Project
1. Click "Create Project"
2. Project Name: "Wood Knot Detection"
3. Data Import: Click "Upload Files" → select your wood images
4. Labeling Setup:
- Template: "Object Detection with Bounding Boxes"
- Add label: `knot` (or multiple types: `sound_knot`, `dead_knot`, etc.)
### 3. Label Images
**Keyboard Shortcuts (speeds up 3-5x):**
- `Alt + Click` = Create bounding box
- `Alt + R` = Select rectangle tool
- `Ctrl + Enter` = Submit and move to next
- `Ctrl + Z` = Undo
**Best Practices:**
- Draw boxes tight around each knot
- Include partial knots at image edges
- Label consistently (all knots, or only specific types)
- Take breaks every 30-50 images to maintain quality
### 4. Export to COCO Format
1. Click project name → **Export**
2. Format: **"COCO"**
3. Download the zip file
4. Extract and organize for RF-DETR:
```bash
# After extracting export.zip:
unzip export.zip -d exported/
# Organize into RF-DETR format
mkdir -p dataset/train dataset/valid dataset/test
# Move images and rename JSON
mv exported/images/* dataset/train/
mv exported/result.json dataset/train/_annotations.coco.json
# Split 80/10/10 manually or use a script
# Move ~10% of images + their annotations to valid/
# Move ~10% to test/
```
**Tip**: Label Studio keeps all annotations in one JSON. You'll need to split it into train/valid/test. The `validate_coco_dataset.py` script can help verify the structure.
---
## Alternative: CVAT (More Powerful)
### Setup with Docker
```bash
git clone https://github.com/opencv/cvat
cd cvat
docker compose up -d
```
Open http://localhost:8080 (default login: admin/admin)
### Features
- **Keyboard shortcuts**: `N` (new box), `Shift+arrows` (adjust box)
- **Interpolation**: Auto-label between frames (for video)
- **Team mode**: Multiple annotators on same project
- **Quality control**: Review mode for double-checking labels
### Export
1. Actions → Export task dataset
2. Format: "COCO 1.0"
3. Restructure files to match RF-DETR's expected format
---
## Alternative: labelImg (Desktop App)
### Quick Setup
```bash
pip install labelImg
labelImg /path/to/images
```
**Pros:**
- No web server needed
- Works offline
- Very simple interface
**Cons:**
- Exports Pascal VOC by default (not COCO)
- Need to convert format:
```bash
# Use roboflow or pylabel library to convert VOC → COCO
```
---
## Model-Assisted Labeling Workflow
After you have a trained model, speed up labeling 10-20x:
### Step 1: Auto-label new images
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python auto_label_images.py \
--weights runs/knot_rfdetr_medium/checkpoint_best_total.pth \
--images-dir unlabeled_images/ \
--output-json predictions.json \
--threshold 0.3
```
**Use low threshold (0.3)** to capture more candidates - easier to delete false positives than add missed knots.
### Step 2: Convert to Label Studio format
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python convert_to_label_studio.py \
--coco-json predictions.json \
--images-dir unlabeled_images/ \
--output-json label_studio_tasks.json
```
### Step 3: Import predictions into Label Studio
1. In Label Studio, open your project
2. **Settings → Storage → Add Source Storage → Local files**:
- Storage Type: Local files
- Absolute local path: `/full/path/to/unlabeled_images`
- Click "Add Storage" then "Sync Storage"
3. **Import → Upload Files** → select `label_studio_tasks.json`
4. Each image now loads with **pre-drawn boxes from your model**
5. Click through images, fixing mistakes (much faster than labeling from scratch!)
### Step 4: Active Learning Loop with Label Studio
1. **Initial labeling**: Label 50-100 images manually in Label Studio
2. **Export & prepare**:
```bash
# Export from Label Studio as COCO format
# Split into train/valid/test folders
```
3. **Train RF-DETR**: Run for just 10 epochs (faster iteration)
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python train_rfdetr.py \
--dataset-dir dataset/ \
--output-dir runs/iteration_1 \
--model medium \
--epochs 10
```
4. **Auto-label new batch**: Get predictions on 500 unlabeled images
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python auto_label_images.py \
--weights runs/iteration_1/checkpoint_best_total.pth \
--images-dir batch_2_images/ \
--output-json batch_2_predictions.json \
--threshold 0.3
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python convert_to_label_studio.py \
--coco-json batch_2_predictions.json \
--images-dir batch_2_images/ \
--output-json batch_2_ls_tasks.json
```
5. **Review in Label Studio**: Import `batch_2_ls_tasks.json` → review/correct (10x faster than from scratch)
6. **Export & retrain**: Add corrected labels to dataset, retrain for 20-50 epochs
7. **Repeat**: Continue with batch 3, 4, etc.
This iterative approach typically achieves **95%+ accuracy with 5-10x less manual effort**.
---
## Tips for High-Quality Labels
### Consistency is Key
- **Same criteria every time**: Decide upfront if you label tiny knots, damaged areas, etc.
- **Box boundaries**: Tight around knot, or include some margin? Pick one and stick to it.
- **Occlusions**: Label partially visible knots? Document your decision.
### Speed vs. Quality
- **First 100 images**: Take your time, establish consistency
- **After 100**: Speed up - model will help catch inconsistencies later
- **Every 500 images**: Audit 20-30 random labels to check quality
### Common Mistakes
1. ❌ Inconsistent box sizes (sometimes tight, sometimes loose)
2. ❌ Missing small knots in some images but labeling them in others
3. ❌ Labeling knot-like wood grain patterns
4. ❌ Fatigue errors after 2+ hours - take breaks!
### Dataset Size Guidelines
- **Minimum**: 200 labeled images (split: 150 train, 30 valid, 20 test)
- **Good**: 500-1000 images
- **Excellent**: 2000+ images
- **With active learning**: Start with 100, grow to 500+ iteratively
---
## Converting Other Formats to COCO
If you have labels in another format:
### From YOLO format:
```python
from pylabel import importer
dataset = importer.ImportYoloV5(
path="yolo_labels/",
img_path="images/",
cat_names=['knot']
)
dataset.export.ExportToCoco(output_path="coco_format/")
```
### From Pascal VOC:
```python
from pylabel import importer
dataset = importer.ImportVOC(path="voc_annotations/")
dataset.export.ExportToCoco()
```
---
## Troubleshooting
**Label Studio won't start:**
- Try: `label-studio reset` then `label-studio start`
**CVAT Docker issues:**
- Check: `docker compose logs`
- Ensure ports 8080, 8070 are free
**Export format doesn't match RF-DETR:**
- See [validate_coco_dataset.py](validate_coco_dataset.py) to check your format
- Your JSON needs: `images`, `annotations`, `categories` keys
**Need help?**
- Label Studio docs: https://labelstud.io/guide/
- CVAT docs: https://opencv.github.io/cvat/docs/

92
MODEL_COMPARISON.md Normal file
View File

@ -0,0 +1,92 @@
# Model Framework Comparison
## License Comparison
| Framework | License | Commercial Use | OAK-D Support |
|-----------|---------|----------------|---------------|
| **RT-DETR** | Apache 2.0 | ✅ Free | ⭐ Excellent |
| **YOLOv6** | MIT | ✅ Free | ⭐ Excellent |
| **YOLOX** | MIT | ✅ Free | ⭐ Excellent |
| RF-DETR | Check repo | ⚠️ Unknown | ⚠️ May need conversion |
| YOLOv8/v11 | AGPL-3.0 | ❌ Paid ($1k-5k/yr) | Excellent |
## Performance on OAK-D 4 Pro (48 TOPS INT8)
| Model | Size | Speed (FPS) | Accuracy | Training Time |
|-------|------|-------------|----------|---------------|
| RT-DETR r18 | ~15MB | 30-40 | Good | Fast |
| RT-DETR r34 | ~30MB | 20-30 | Better | Medium |
| YOLOv6n | ~10MB | 40-50 | Good | Fast |
| YOLOv6s | ~20MB | 30-40 | Better | Medium |
| YOLOX nano | ~6MB | 50-60 | Good | Fast |
| YOLOX-s | ~18MB | 35-45 | Better | Medium |
## Which to Choose?
### For Maximum Speed (50-60 FPS):
**YOLOX nano** - Smallest, fastest, proven
### For Best Balance (30-40 FPS):
**RT-DETR r18** or **YOLOv6n** - Modern, accurate
### For Best Accuracy (20-30 FPS):
**RT-DETR r34** or **YOLOv6s** - Larger models
### Recommended Starting Point:
**YOLOv6n** - Great balance, proven OAK compatibility, MIT license
## Training Commands
All models use the same workflow in the GUI, or from command line:
### RT-DETR
```bash
.venv/bin/python train_rtdetr.py \
--dataset-dir dataset_prepared \
--model rtdetr-r18 \
--epochs 100
```
### YOLOv6
```bash
.venv/bin/python train_yolov6.py \
--dataset-dir dataset_prepared \
--model yolov6n \
--epochs 100
```
### YOLOX (YOLOv8 equivalent)
```bash
.venv/bin/python train_yolox.py \
--dataset-dir dataset_prepared \
--model yolox-nano \
--epochs 100
```
## Export for OAK-D
All models export to OpenVINO format for OAK deployment:
```bash
# RT-DETR
.venv/bin/python export_rtdetr_oak.py --weights runs/rtdetr_training/training/weights/best.pt
# YOLOv6/YOLOX use Ultralytics export
.venv/bin/python -c "
from ultralytics import YOLO
model = YOLO('runs/yolov6_training/training/weights/best.pt')
model.export(format='openvino', imgsz=640, half=False)
"
```
Then convert to blob:
- Online: https://blobconverter.luxonis.com/
- CLI: `blobconverter --openvino-xml model.xml`
## Tips
1. **Start with nano/r18 models** for fast iteration
2. **Train for 100-200 epochs** - use early stopping
3. **Collect 200+ images** for good accuracy
4. **Test on OAK-D** before collecting more data
5. **Use INT8 quantization** for full 48 TOPS speed

153
README.md Normal file
View File

@ -0,0 +1,153 @@
# Saw Mill Knot Detection (RF-DETR)
This repo contains a minimal training pipeline to fine-tune **RF-DETR** to detect knots in wood.
**Dataset Source**: The wood defect images and annotations used in this project come from [Kaggle Wood Surface Defects Dataset](https://www.kaggle.com/datasets/kirs0816/wood-surface-defects?resource=download).
## 1) Dataset format (required)
RF-DETR expects **COCO format**, split into `train/`, `valid/`, `test/`, each with its own `_annotations.coco.json`.
Example:
```
dataset/
├── train/
│ ├── _annotations.coco.json
│ ├── 0001.jpg
│ └── ...
├── valid/
│ ├── _annotations.coco.json
│ ├── 0101.jpg
│ └── ...
└── test/
├── _annotations.coco.json
├── 0201.jpg
└── ...
```
Your COCO JSON should include a `categories` entry for your class(es), e.g. `knot`.
## 2) Setup
Create venv (already created if you used the VS Code prompt) and install deps:
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python -m pip install -U pip
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python -m pip install -r requirements.txt
```
## 3) Validate dataset
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python validate_coco_dataset.py --dataset-dir /path/to/dataset
```
## 4) Train
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python train_rfdetr.py \
--dataset-dir /path/to/dataset \
--output-dir runs/knot_rfdetr_medium \
--model medium \
--epochs 50 \
--batch-size 4 \
--grad-accum-steps 4 \
--lr 1e-4
```
Notes:
- Keep **effective batch size** near 16: `batch_size * grad_accum_steps * num_gpus ≈ 16`.
- Checkpoints are written into `--output-dir` (including `checkpoint_best_total.pth`).
## 5) Auto-label new images (automatic)
Use your trained model to generate annotations on unlabeled images:
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python auto_label_images.py \
--weights runs/knot_rfdetr_medium/checkpoint_best_total.pth \
--images-dir /path/to/new_images \
--output-json auto_labeled.json \
--threshold 0.4
```
This outputs a COCO JSON with predicted bounding boxes. You can then review/correct them manually.
## 6) Manual labeling (recommended tools)
**Don't build your own GUI** - use these proven open-source tools instead:
### Option A: Label Studio (Recommended - Easiest)
**Best for**: Quick setup, modern UI, ML-assisted labeling
```bash
# Install Label Studio
pip install label-studio
# Start the server
label-studio start
```
Then open http://localhost:8080 in your browser:
1. Create a new project for "Object Detection with Bounding Boxes"
2. Import your images
3. Start labeling manually OR:
- Use the auto-label script to generate initial annotations:
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python auto_label_images.py \
--weights runs/knot_rfdetr_medium/checkpoint_best_total.pth \
--images-dir /path/to/images \
--output-json predictions.json \
--threshold 0.3
```
- Import the predictions into Label Studio
- Review and correct them
4. Export in COCO format when done
### Option B: CVAT (Most Powerful)
**Best for**: Large-scale projects, team collaboration
```bash
# Using Docker (easiest)
git clone https://github.com/opencv/cvat
cd cvat
docker compose up -d
```
Open http://localhost:8080:
- Create project → upload images → annotate
- Supports keyboard shortcuts, interpolation, and advanced features
- Export directly to COCO JSON
[CVAT Documentation](https://opencv.github.io/cvat/docs/)
### Option C: labelImg (Simplest Desktop App)
**Best for**: Offline labeling, no server needed
```bash
pip install labelImg
labelImg
```
- Simple desktop app with no web server
- Exports to Pascal VOC (needs conversion to COCO)
- Good for small datasets
### Workflow with Model Assistance:
1. **Initial batch**: Manually label 50-100 images
2. **Train RF-DETR**: Use your training script
3. **Auto-label**: Run `auto_label_images.py` on remaining images
4. **Review**: Import predictions into Label Studio/CVAT
5. **Correct**: Fix any mistakes (much faster than labeling from scratch)
6. **Iterate**: Retrain with corrected labels, repeat
This semi-supervised approach is **10-20x faster** than manual labeling alone.
## 7) Quick inference sanity check
```bash
/home/dillon/_code/saw_mill_knot_detection/.venv/bin/python predict_rfdetr.py \
--weights runs/knot_rfdetr_medium/checkpoint_best_total.pth \
--image /path/to/example.jpg \
--threshold 0.4
```

188
RTDETR_README.md Normal file
View File

@ -0,0 +1,188 @@
# RT-DETR Training for OAK-D Camera Deployment
RT-DETR (Real-Time Detection Transformer) is Apache 2.0 licensed - **free for commercial use**. It's designed for real-time detection and works great on edge devices like the OAK-D 4 Pro.
## Why RT-DETR?
-**Apache 2.0 license** - truly free for commercial use
-**Excellent OAK camera compatibility** - exports cleanly to OpenVINO
-**Real-time performance** - 30-60 FPS on OAK-D 4 Pro
-**Modern transformer architecture** - competitive accuracy with YOLO
-**Easy deployment** - direct export to OpenVINO format
## Quick Start
### 1. Annotate Images
Use the annotation GUI:
```bash
.venv/bin/python annotation_gui.py
```
- Load your images from Settings
- Annotate knots manually or use auto-labeling
- Aim for 100+ annotated images for good results
### 2. Train Model
From the GUI:
1. Go to **Training** tab
2. Click "Prepare Dataset" (creates train/valid/test splits)
3. Select **RT-DETR** framework
4. Choose model size:
- `nano` (r18): Fastest, 30-40 FPS on OAK
- `small` (r34): Balanced
- `medium` (r50): More accurate
- `base` (l): Best accuracy, slower
5. Click "Start Training"
Or from command line:
```bash
.venv/bin/python train_rtdetr.py \
--dataset-dir dataset_prepared \
--model rtdetr-r18 \
--epochs 100 \
--batch-size 8
```
### 3. Test Model
```bash
.venv/bin/python predict_rtdetr.py \
--weights runs/rtdetr_training/training/weights/best.pt \
--image test_image.jpg
```
### 4. Export for OAK-D
Export to OpenVINO format:
```bash
.venv/bin/python export_rtdetr_oak.py \
--weights runs/rtdetr_training/training/weights/best.pt \
--img-size 640
```
This creates:
- `best_openvino_model/` - OpenVINO IR format (.xml + .bin files)
- `best.onnx` - ONNX format (intermediate)
### 5. Convert to Blob for OAK
**Option A: Online converter** (easiest)
1. Go to https://blobconverter.luxonis.com/
2. Upload `best_openvino_model/model.xml`
3. Select "OAK-D 4 Pro"
4. Download `.blob` file
**Option B: Command line**
```bash
pip install blobconverter
blobconverter --openvino-xml best_openvino_model/model.xml \
--shaves 6
```
### 6. Deploy to OAK-D Camera
Example DepthAI script:
```python
import depthai as dai
import cv2
# Create pipeline
pipeline = dai.Pipeline()
# Camera
cam = pipeline.createColorCamera()
cam.setPreviewSize(640, 640)
cam.setInterleaved(False)
# Neural network
nn = pipeline.createNeuralNetwork()
nn.setBlobPath("best.blob")
cam.preview.link(nn.input)
# Output
xout = pipeline.createXLinkOut()
xout.setStreamName("detections")
nn.out.link(xout.input)
# Run
with dai.Device(pipeline) as device:
queue = device.getOutputQueue("detections")
while True:
detections = queue.get()
# Process detections...
```
## Model Comparison
| Model | Size | Speed (OAK-D) | Accuracy | License |
|-------|------|---------------|----------|---------|
| RT-DETR r18 | ~15MB | 30-40 FPS | Good | Apache 2.0 ✅ |
| RT-DETR r34 | ~30MB | 20-30 FPS | Better | Apache 2.0 ✅ |
| YOLOv11n | ~6MB | 50-60 FPS | Good | AGPL ❌ |
| YOLOv6n | ~10MB | 40-50 FPS | Good | MIT ✅ |
| RF-DETR nano | ~15MB | 10-20 FPS* | Good | Check repo |
*May have compatibility issues with OpenVINO
## Training Tips
1. **Dataset size**:
- Minimum: 50 images
- Good: 200+ images
- Excellent: 1000+ images
2. **Data diversity**:
- Different wood types
- Various lighting conditions
- Multiple knot sizes/types
- Different angles
3. **Training settings**:
- Start with `rtdetr-r18` for fastest iteration
- Use `batch-size=8` if you have 8GB+ GPU
- Train for 100-200 epochs
- Use early stopping (patience=20)
4. **Data augmentation** (automatic):
- Flips, rotations
- Color adjustments
- Crops and scales
## Troubleshooting
**Training is slow:**
- Reduce batch size
- Use smaller model (r18)
- Check GPU usage with `nvidia-smi`
**Low accuracy:**
- Add more training data
- Train longer (more epochs)
- Use larger model (r34 or r50)
- Check your annotations for errors
**OAK deployment fails:**
- Ensure OpenVINO export succeeded
- Check blob size (<200MB for OAK-D)
- Verify input size matches training (640x640)
- Try FP16 instead of FP32 to reduce size
## Resources
- [RT-DETR Paper](https://arxiv.org/abs/2304.08069)
- [Ultralytics RT-DETR Docs](https://docs.ultralytics.com/models/rtdetr/)
- [OAK-D Docs](https://docs.luxonis.com/)
- [DepthAI Examples](https://github.com/luxonis/depthai-experiments)
## License
RT-DETR is Apache 2.0 licensed - you can use it for:
- Personal projects
- Commercial products
- Internal business tools
- Proprietary software
No restrictions, no paid licenses required!

824
annotation_gui.py Normal file
View File

@ -0,0 +1,824 @@
"""
Simple customizable annotation GUI with auto-labeling support.
Built with Gradio - easy to modify and extend.
Run: python annotation_gui.py
To set default paths, edit config.py
"""
from __future__ import annotations
import argparse
import json
import subprocess
import threading
from pathlib import Path
from typing import Any
import gradio as gr
from PIL import Image, ImageDraw
# Try to load config, use fallbacks if not available
try:
from config import (
DEFAULT_IMAGES_DIR, DEFAULT_MODEL_WEIGHTS, DEFAULT_PORT,
DEFAULT_DETECTION_THRESHOLD, DEFAULT_TRAIN_EPOCHS,
DEFAULT_BATCH_SIZE, DEFAULT_LEARNING_RATE, DEFAULT_MODEL_SIZE
)
except ImportError:
DEFAULT_IMAGES_DIR = None
DEFAULT_MODEL_WEIGHTS = None
DEFAULT_PORT = 7860
DEFAULT_DETECTION_THRESHOLD = 0.5
DEFAULT_TRAIN_EPOCHS = 20
DEFAULT_BATCH_SIZE = 4
DEFAULT_LEARNING_RATE = 1e-4
DEFAULT_MODEL_SIZE = "small"
class AnnotationApp:
def __init__(self, images_dir: Path | None = None, model_weights: Path | None = None):
self.images_dir = images_dir if images_dir else Path.cwd()
self.current_model_path = model_weights
self.image_paths = []
self.current_idx = 0
self.annotations = {} # image_name -> list of boxes
self.model = None
self.training_process = None
self.training_thread = None
self.training_status = "Not training"
# Load images if directory provided
if images_dir and images_dir.exists():
self._load_images(images_dir)
if model_weights and model_weights.exists():
self._load_model(model_weights)
def _load_images(self, images_dir: Path):
"""Load images from directory."""
self.images_dir = images_dir
self.image_paths = sorted(
list(images_dir.glob("*.jpg")) + list(images_dir.glob("*.png"))
)
self.current_idx = 0
# Load existing annotations if present
self.ann_file = images_dir / "annotations.json"
if self.ann_file.exists():
with self.ann_file.open("r") as f:
self.annotations = json.load(f)
else:
self.annotations = {}
return f"✓ Loaded {len(self.image_paths)} images from {images_dir}"
def _load_model(self, weights_path: Path):
"""Load YOLO/YOLOX model for auto-labeling (Ultralytics format)."""
try:
from ultralytics import YOLO
print(f"Loading model from {weights_path}...")
self.model = YOLO(str(weights_path))
self.current_model_path = weights_path
print("✓ Model loaded")
return f"✓ Model loaded from {weights_path.name}"
except Exception as e:
error_msg = f"⚠ Could not load model: {e}"
print(error_msg)
self.model = None
return error_msg
def load_new_model(self, weights_path: str) -> str:
"""Load a new model from the GUI."""
path = Path(weights_path)
if not path.exists():
return f"❌ File not found: {weights_path}"
return self._load_model(path)
def load_new_images_dir(self, images_dir: str) -> tuple[Image.Image | None, str, str]:
"""Load a new images directory from the GUI."""
path = Path(images_dir)
if not path.exists():
return None, "", f"❌ Directory not found: {images_dir}"
if not path.is_dir():
return None, "", f"❌ Not a directory: {images_dir}"
result = self._load_images(path)
# Load first image
if self.image_paths:
img, filename = self.get_current_image()
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"{result}\nImage 1/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
else:
return None, "", f"{result}\n⚠️ No .jpg or .png images found in directory"
def get_current_model_info(self) -> str:
"""Get info about currently loaded model."""
if self.model and self.current_model_path:
return f"📦 Loaded: {self.current_model_path}"
elif self.model:
return "📦 Model loaded (pretrained)"
else:
return "⚠️ No model loaded"
def get_current_dir_info(self) -> str:
"""Get info about current images directory."""
return f"📁 {self.images_dir} ({len(self.image_paths)} images)"
def get_current_image(self) -> tuple[Image.Image, str]:
"""Get current image and filename."""
if not self.image_paths:
return None, ""
path = self.image_paths[self.current_idx]
img = Image.open(path).convert("RGB")
return img, path.name
def draw_boxes_on_image(self, img: Image.Image, boxes: list[dict]) -> Image.Image:
"""Draw bounding boxes on image."""
img_draw = img.copy()
draw = ImageDraw.Draw(img_draw)
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
label = box.get("label", "knot")
conf = box.get("confidence", 1.0)
# Draw box
draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
# Draw label
text = f"{label} {conf:.2f}" if conf < 1.0 else label
draw.text((x1, y1 - 20), text, fill="red")
return img_draw
def auto_label_current(self, threshold: float = 0.5) -> tuple[Image.Image, str, str]:
"""Auto-label current image with model."""
if not self.model:
img, filename = self.get_current_image()
info = f"⚠ No model loaded | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img, "", info
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Run inference with Ultralytics YOLO
results = self.model.predict(img, conf=threshold, verbose=False)
# Convert to our format
boxes = []
if len(results) > 0:
result = results[0] # First image result
if result.boxes is not None and len(result.boxes) > 0:
for box in result.boxes:
xyxy = box.xyxy[0].cpu().numpy().tolist() # [x1, y1, x2, y2]
conf = float(box.conf[0].cpu().numpy())
cls = int(box.cls[0].cpu().numpy())
# Get class name if available
label = result.names.get(cls, f"class_{cls}") if hasattr(result, 'names') else f"class_{cls}"
boxes.append({
"bbox": xyxy,
"label": label,
"confidence": conf,
"source": "auto"
})
# Save
self.annotations[filename] = boxes
self._save_annotations()
# Draw boxes on image
img_with_boxes = self.draw_boxes_on_image(img, boxes)
# Info with image index
info = f"✓ Auto-labeled: {len(boxes)} boxes detected | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
boxes_text = self._format_boxes_text(boxes)
return img_with_boxes, boxes_text, info
def _format_boxes_text(self, boxes: list[dict]) -> str:
"""Format boxes for display."""
if not boxes:
return "No annotations"
lines = []
for i, box in enumerate(boxes):
x1, y1, x2, y2 = box["bbox"]
conf = box.get("confidence", 1.0)
source = box.get("source", "manual")
lines.append(f"{i}: [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}] conf={conf:.2f} ({source})")
return "\n".join(lines)
def load_image(self, direction: str = "current") -> tuple[Image.Image, str, str]:
"""Load image (current/next/prev)."""
if direction == "next":
self.current_idx = min(self.current_idx + 1, len(self.image_paths) - 1)
elif direction == "prev":
self.current_idx = max(self.current_idx - 1, 0)
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Load existing annotations
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
def add_box_manual(self, x1: int, y1: int, x2: int, y2: int) -> tuple[Image.Image, str, str]:
"""Manually add a bounding box."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Add box
box = {
"bbox": [float(x1), float(y1), float(x2), float(y2)],
"label": "knot",
"confidence": 1.0,
"source": "manual"
}
if filename not in self.annotations:
self.annotations[filename] = []
self.annotations[filename].append(box)
self._save_annotations()
# Redraw
boxes = self.annotations[filename]
img_with_boxes = self.draw_boxes_on_image(img, boxes)
boxes_text = self._format_boxes_text(boxes)
info = f"✓ Added box: {len(boxes)} total | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
def delete_last_box(self) -> tuple[Image.Image, str, str]:
"""Delete the last box from current image."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
if filename in self.annotations and self.annotations[filename]:
self.annotations[filename].pop()
self._save_annotations()
# Redraw
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"✓ Deleted last box: {len(boxes)} remaining | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
def clear_boxes(self) -> tuple[Image.Image, str, str]:
"""Clear all boxes from current image."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
self.annotations[filename] = []
self._save_annotations()
boxes_text = "No annotations"
info = f"✓ Cleared all boxes | Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img, boxes_text, info
def _save_annotations(self):
"""Save annotations to JSON file."""
with self.ann_file.open("w") as f:
json.dump(self.annotations, f, indent=2)
def export_to_coco(self, output_path: Path):
"""Export annotations to COCO format."""
coco_data = {
"images": [],
"annotations": [],
"categories": [{"id": 0, "name": "knot", "supercategory": "defect"}]
}
ann_id = 0
for img_id, img_path in enumerate(self.image_paths):
filename = img_path.name
img = Image.open(img_path)
width, height = img.size
coco_data["images"].append({
"id": img_id,
"file_name": filename,
"width": width,
"height": height
})
# Add annotations
boxes = self.annotations.get(filename, [])
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
w = x2 - x1
h = y2 - y1
coco_data["annotations"].append({
"id": ann_id,
"image_id": img_id,
"category_id": 0,
"bbox": [x1, y1, w, h],
"area": w * h,
"iscrowd": 0,
"score": box.get("confidence", 1.0)
})
ann_id += 1
with output_path.open("w") as f:
json.dump(coco_data, f, indent=2)
return f"✓ Exported {len(coco_data['annotations'])} annotations to {output_path}"
def prepare_training_dataset(self, output_dir: Path, train_split: float = 0.8, valid_split: float = 0.1):
"""Prepare dataset in RF-DETR format (train/valid/test splits)."""
output_dir.mkdir(parents=True, exist_ok=True)
# Create splits
import random
annotated_images = [img for img in self.image_paths if img.name in self.annotations and self.annotations[img.name]]
if len(annotated_images) < 10:
return f"⚠️ Need at least 10 annotated images, have {len(annotated_images)}"
random.shuffle(annotated_images)
n = len(annotated_images)
train_n = int(n * train_split)
valid_n = int(n * valid_split)
splits = {
"train": annotated_images[:train_n],
"valid": annotated_images[train_n:train_n + valid_n],
"test": annotated_images[train_n + valid_n:]
}
# Create directories and copy images
import shutil
for split_name, split_images in splits.items():
split_dir = output_dir / split_name
split_dir.mkdir(exist_ok=True)
# Prepare COCO JSON for this split
coco_data = {
"images": [],
"annotations": [],
"categories": [{"id": 0, "name": "knot", "supercategory": "defect"}]
}
ann_id = 0
for img_id, img_path in enumerate(split_images):
# Copy image
dest = split_dir / img_path.name
shutil.copy2(img_path, dest)
# Add to COCO
img = Image.open(img_path)
width, height = img.size
coco_data["images"].append({
"id": img_id,
"file_name": img_path.name,
"width": width,
"height": height
})
# Add annotations
boxes = self.annotations.get(img_path.name, [])
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
w = x2 - x1
h = y2 - y1
coco_data["annotations"].append({
"id": ann_id,
"image_id": img_id,
"category_id": 0,
"bbox": [x1, y1, w, h],
"area": w * h,
"iscrowd": 0
})
ann_id += 1
# Save COCO JSON
with (split_dir / "_annotations.coco.json").open("w") as f:
json.dump(coco_data, f, indent=2)
return f"✓ Dataset prepared: {len(splits['train'])} train, {len(splits['valid'])} valid, {len(splits['test'])} test"
def start_training(self, framework: str, dataset_dir: str, output_dir: str, model_size: str,
epochs: int, batch_size: int, lr: float, progress=gr.Progress()):
"""Start training in background."""
dataset_path = Path(dataset_dir)
output_path = Path(output_dir)
if not dataset_path.exists():
return "❌ Dataset directory not found"
if self.training_process and self.training_process.poll() is None:
return "⚠️ Training already in progress"
output_path.mkdir(parents=True, exist_ok=True)
# Build training command based on framework
venv_python = Path(__file__).parent / ".venv/bin/python"
if framework == "RT-DETR":
train_script = Path(__file__).parent / "train_rtdetr.py"
# Map sizes: nano->r18, small->r34, medium->r50, base->l
size_map = {"nano": "rtdetr-r18", "small": "rtdetr-r34", "medium": "rtdetr-r50", "base": "rtdetr-l"}
model_arg = size_map.get(model_size, "rtdetr-r18")
cmd = [
str(venv_python),
str(train_script),
"--dataset-dir", str(dataset_path),
"--output-dir", str(output_path),
"--model", model_arg,
"--epochs", str(epochs),
"--batch-size", str(batch_size),
"--lr", str(lr)
]
elif framework == "YOLOv6":
train_script = Path(__file__).parent / "train_yolov6.py"
# Map sizes: nano->n, small->s, medium->m, base->l
size_map = {"nano": "yolov6n", "small": "yolov6s", "medium": "yolov6m", "base": "yolov6l"}
model_arg = size_map.get(model_size, "yolov6n")
cmd = [
str(venv_python),
str(train_script),
"--dataset-dir", str(dataset_path),
"--output-dir", str(output_path),
"--model", model_arg,
"--epochs", str(epochs),
"--batch-size", str(batch_size),
"--lr", str(lr)
]
elif framework == "YOLOX":
train_script = Path(__file__).parent / "train_yolox.py"
# Map sizes: nano->nano, small->s, medium->m, base->l
size_map = {"nano": "yolox-nano", "small": "yolox-s", "medium": "yolox-m", "base": "yolox-l"}
model_arg = size_map.get(model_size, "yolox-nano")
cmd = [
str(venv_python),
str(train_script),
"--dataset-dir", str(dataset_path),
"--output-dir", str(output_path),
"--model", model_arg,
"--epochs", str(epochs),
"--batch-size", str(batch_size),
"--lr", str(lr)
]
else:
return f"❌ Unknown framework: {framework}"
# Start training process
log_file = output_path / "training.log"
self.training_status = f"🚀 Starting {framework} training..."
def run_training():
try:
with log_file.open("w") as f:
self.training_process = subprocess.Popen(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
text=True
)
self.training_status = f"⏳ Training in progress (PID: {self.training_process.pid})"
self.training_process.wait()
if self.training_process.returncode == 0:
self.training_status = "✅ Training completed successfully!"
# Reload model with new weights
best_weights = output_path / "checkpoint_best_total.pth"
if best_weights.exists():
self._load_model(best_weights)
else:
self.training_status = f"❌ Training failed (exit code {self.training_process.returncode})"
except Exception as e:
self.training_status = f"❌ Error: {e}"
self.training_thread = threading.Thread(target=run_training, daemon=True)
self.training_thread.start()
return f"✓ Training started! Check {log_file} for progress"
def get_training_status(self):
"""Get current training status."""
return self.training_status
def stop_training(self):
"""Stop the training process."""
if self.training_process and self.training_process.poll() is None:
self.training_process.terminate()
self.training_status = "⏹️ Training stopped by user"
return "✓ Training process terminated"
return "⚠️ No training in progress"
def create_ui(app: AnnotationApp) -> gr.Blocks:
"""Create Gradio UI."""
with gr.Blocks(title="Knot Annotation Tool") as demo:
gr.Markdown("""
# 🪵 Wood Knot Annotation Tool
**Label → Train → Auto-Label → Repeat**
- Manually annotate images or use **Auto-Label** with your trained model
- Export and prepare dataset for training
- Train **RT-DETR, YOLOv6, or YOLOX** (all free for commercial use!)
- Optimized for OAK-D camera deployment
- Use trained model to auto-label more images
""")
# Settings section at the top
with gr.Accordion("⚙️ Settings", open=False):
with gr.Row():
with gr.Column():
images_dir_input = gr.Textbox(
label="Images Directory",
value=str(app.images_dir),
placeholder="/path/to/images"
)
load_images_btn = gr.Button("📁 Load Images Directory")
dir_info = gr.Textbox(label="Current Directory", value=app.get_current_dir_info(), interactive=False)
with gr.Column():
model_weights_input = gr.Textbox(
label="Model Weights Path",
value=str(app.current_model_path) if app.current_model_path else "",
placeholder="runs/training/checkpoint_best_total.pth"
)
load_model_btn = gr.Button("🤖 Load Model Weights")
model_info = gr.Textbox(label="Current Model", value=app.get_current_model_info(), interactive=False)
with gr.Row():
with gr.Column(scale=3):
image_display = gr.Image(label="Current Image", type="pil")
with gr.Row():
prev_btn = gr.Button("⬅️ Previous")
next_btn = gr.Button("Next ➡️")
auto_label_btn = gr.Button("🤖 Auto-Label", variant="primary")
with gr.Row():
threshold_slider = gr.Slider(0.1, 0.9, DEFAULT_DETECTION_THRESHOLD, label="Detection Threshold")
with gr.Column(scale=1):
info_text = gr.Textbox(label="Status", lines=2)
boxes_text = gr.Textbox(label="Annotations", lines=10)
gr.Markdown("### Manual Annotation")
with gr.Row():
x1_input = gr.Number(label="x1", value=100)
y1_input = gr.Number(label="y1", value=100)
with gr.Row():
x2_input = gr.Number(label="x2", value=200)
y2_input = gr.Number(label="y2", value=200)
add_box_btn = gr.Button(" Add Box")
delete_btn = gr.Button("🗑️ Delete Last")
clear_btn = gr.Button("❌ Clear All")
gr.Markdown("### Export & Training")
export_path = gr.Textbox(
label="Export Path",
value="annotations_coco.json"
)
export_btn = gr.Button("💾 Export COCO")
export_result = gr.Textbox(label="Export Result", lines=1)
# Training tab
with gr.Tab("🎯 Training"):
gr.Markdown("""
### Train Object Detection Model
**Choose your framework:**
- **RT-DETR** (Apache 2.0): Modern transformer, great accuracy
- **YOLOv6** (MIT): Fast, proven on OAK cameras
- **YOLOX** (MIT): Similar to YOLOv6, slight differences
**All MIT/Apache 2.0 licensed - free for commercial use!** ✅
**Steps:**
1. Annotate at least 50-100 images in the Annotation tab
2. Click "Prepare Dataset" to create train/valid/test splits
3. Select your framework and configure training parameters
4. Click "Start Training" (runs in background)
5. After training, export for OAK-D deployment
""")
with gr.Row():
with gr.Column():
dataset_prep_dir = gr.Textbox(
label="Dataset Output Directory",
value="dataset_prepared"
)
train_split = gr.Slider(0.5, 0.9, 0.8, label="Train Split Ratio")
valid_split = gr.Slider(0.05, 0.3, 0.1, label="Valid Split Ratio")
prep_btn = gr.Button("📦 Prepare Dataset", variant="secondary")
prep_result = gr.Textbox(label="Preparation Result", lines=2)
with gr.Column():
gr.Markdown("### Training Configuration")
model_framework = gr.Dropdown(
choices=["RT-DETR", "YOLOv6", "YOLOX"],
value="RT-DETR",
label="Model Framework",
info="All MIT/Apache 2.0 licensed - free for commercial use. Optimized for OAK cameras."
)
train_dataset_dir = gr.Textbox(
label="Dataset Directory",
value="dataset_prepared"
)
train_output_dir = gr.Textbox(
label="Output Directory",
value="runs/gui_training"
)
model_size = gr.Dropdown(
choices=["nano", "small", "medium", "base"],
value=DEFAULT_MODEL_SIZE,
label="Model Size"
)
epochs = gr.Slider(5, 100, DEFAULT_TRAIN_EPOCHS, step=5, label="Epochs")
batch_size = gr.Slider(1, 16, DEFAULT_BATCH_SIZE, step=1, label="Batch Size")
learning_rate = gr.Number(value=DEFAULT_LEARNING_RATE, label="Learning Rate")
with gr.Row():
start_train_btn = gr.Button("🚀 Start Training", variant="primary")
stop_train_btn = gr.Button("⏹️ Stop Training", variant="stop")
refresh_status_btn = gr.Button("🔄 Refresh Status")
training_status = gr.Textbox(
label="Training Status",
value="Not training",
lines=3
)
gr.Markdown("""
**Note**: Training runs in the background. You can continue annotating while training.
Check the training log file for detailed progress.
""")
# Event handlers
def on_load():
return app.load_image("current")
# Settings handlers
load_images_btn.click(
app.load_new_images_dir,
inputs=[images_dir_input],
outputs=[image_display, boxes_text, info_text]
).then(
lambda: (app.get_current_dir_info(), app.get_current_model_info()),
outputs=[dir_info, model_info]
)
load_model_btn.click(
app.load_new_model,
inputs=[model_weights_input],
outputs=[model_info]
)
prev_btn.click(
lambda: app.load_image("prev"),
outputs=[image_display, boxes_text, info_text]
)
next_btn.click(
lambda: app.load_image("next"),
outputs=[image_display, boxes_text, info_text]
)
auto_label_btn.click(
lambda t: app.auto_label_current(t),
inputs=[threshold_slider],
outputs=[image_display, boxes_text, info_text]
)
add_box_btn.click(
app.add_box_manual,
inputs=[x1_input, y1_input, x2_input, y2_input],
outputs=[image_display, boxes_text, info_text]
)
delete_btn.click(
app.delete_last_box,
outputs=[image_display, boxes_text, info_text]
)
clear_btn.click(
app.clear_boxes,
outputs=[image_display, boxes_text, info_text]
)
export_btn.click(
lambda path: app.export_to_coco(Path(path)),
inputs=[export_path],
outputs=[export_result]
)
# Training handlers
prep_btn.click(
lambda out, train, valid: app.prepare_training_dataset(Path(out), train, valid),
inputs=[dataset_prep_dir, train_split, valid_split],
outputs=[prep_result]
)
start_train_btn.click(
app.start_training,
inputs=[model_framework, train_dataset_dir, train_output_dir, model_size, epochs, batch_size, learning_rate],
outputs=[training_status]
)
stop_train_btn.click(
app.stop_training,
outputs=[training_status]
)
refresh_status_btn.click(
app.get_training_status,
outputs=[training_status]
)
# Load first image on start
demo.load(on_load, outputs=[image_display, boxes_text, info_text])
return demo
def main():
parser = argparse.ArgumentParser(description="Simple annotation GUI with auto-labeling")
parser.add_argument(
"--images-dir",
type=Path,
default=Path(DEFAULT_IMAGES_DIR) if DEFAULT_IMAGES_DIR else None,
help="Default directory with images (can be changed in GUI)"
)
parser.add_argument(
"--model-weights",
type=Path,
default=Path(DEFAULT_MODEL_WEIGHTS) if DEFAULT_MODEL_WEIGHTS else None,
help="Default trained model for auto-labeling (can be changed in GUI)"
)
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port for web interface")
args = parser.parse_args()
# Validate paths if provided
if args.images_dir and not args.images_dir.exists():
print(f"⚠️ Warning: Images directory not found: {args.images_dir}")
print("You can load a different directory from the GUI Settings")
args.images_dir = None
if args.model_weights and not args.model_weights.exists():
print(f"⚠️ Warning: Model weights not found: {args.model_weights}")
print("You can load different weights from the GUI Settings")
args.model_weights = None
# Create app
app = AnnotationApp(args.images_dir, args.model_weights)
# Create and launch UI
demo = create_ui(app)
print(f"\n{'='*60}")
print(f"🚀 Starting annotation tool...")
if args.images_dir:
print(f"📁 Default images: {args.images_dir} ({len(app.image_paths)} images)")
else:
print(f"📁 No default images - load directory from Settings")
if app.model:
print(f"🤖 Model: Loaded from {args.model_weights}")
else:
print(f"⚠️ No model loaded - load from Settings or train one")
print(f"💡 You can change images directory and model weights from the Settings panel")
print(f"{'='*60}\n")
demo.launch(
server_name="0.0.0.0",
server_port=args.port,
share=False
)
if __name__ == "__main__":
main()

708
annotation_gui.py.broken Normal file
View File

@ -0,0 +1,708 @@
"""
Simple customizable annotation GUI with auto-labeling support.
Built with Gradio - easy to modify and extend.
Run: python annotation_gui.py --images-dir /path/to/images
To set default paths, edit config.py
"""
from __future__ import annotations
import argparse
import json
import subprocess
import threading
from pathlib import Path
from typing import Any
import gradio as gr
import numpy as np
from PIL import Image, ImageDraw
# Try to load config, use fallbacks if not available
try:
from config import (
DEFAULT_IMAGES_DIR, DEFAULT_MODEL_WEIGHTS, DEFAULT_PORT,
DEFAULT_DETECTION_THRESHOLD, DEFAULT_TRAIN_EPOCHS,
DEFAULT_BATCH_SIZE, DEFAULT_LEARNING_RATE, DEFAULT_MODEL_SIZE
)
except ImportError:
DEFAULT_IMAGES_DIR = None
DEFAULT_MODEL_WEIGHTS = None
DEFAULT_PORT = 7860
DEFAULT_DETECTION_THRESHOLD = 0.5
DEFAULT_TRAIN_EPOCHS = 20
DEFAULT_BATCH_SIZE = 4
DEFAULT_LEARNING_RATE = 1e-4
DEFAULT_MODEL_SIZE = "small"
class AnnotationApp:
def __init__(self, images_dir: Path | None = None, model_weights: Path | None = None):
self.images_dir = images_dir if images_dir else Path.cwd()
self.current_model_path = model_weights
self.image_paths = []
self.current_idx = 0
self.annotations = {} # image_name -> list of boxes
self.model = None
self.training_process = None
self.training_thread = None
self.training_status = "Not training"
# Load images if directory provided
if images_dir and images_dir.exists():
self._load_images(images_dir)
if model_weights and model_weights.exists():
self._load_model(model_weights)
def _load_images(self, images_dir: Path):
"""Load images from directory."""
self.images_dir = images_dir
self.image_paths = sorted(
list(images_dir.glob("*.jpg")) + list(images_dir.glob("*.png"))
)
self.current_idx = 0
# Load existing annotations if present
self.ann_file = images_dir / "annotations.json"
if self.ann_file.exists():
with self.ann_file.open("r") as f:
self.annotations = json.load(f)
else:
self.annotations = {}
return f"✓ Loaded {len(self.image_paths)} images from {images_dir}"
def _load_model(self, weights_path: Path):
"""Load RF-DETR model for auto-labeling."""
try:
from rfdetr import RFDETRBase
print(f"Loading model from {weights_path}...")
self.model = RFDETRBase(pretrain_weights=str(weights_path))
self.current_model_path = weights_path
print("✓ Model loaded")
return f"✓ Model loaded from {weights_path.name}"
except Exception as e:
error_msg = f"⚠ Could not load model: {e}"
print(error_msg)
self.model = None
return error_msg
def load_new_model(self, weights_path: str) -> str:
"""Load a new model from the GUI."""
path = Path(weights_path)
if not path.exists():
return f"❌ File not found: {weights_path}"
return self._load_model(path)
def load_new_images_dir(self, images_dir: str) -> tuple[Image.Image | None, str, str]:
"""Load a new images directory from the GUI."""
path = Path(images_dir)
if not path.exists():
return None, "", f"❌ Directory not found: {images_dir}"
if not path.is_dir():
return None, "", f"❌ Not a directory: {images_dir}"
result = self._load_images(path)
# Load first image
if self.image_paths:
img, filename = self.get_current_image()
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"{result}\nImage 1/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
else:
return None, "", f"{result}\n⚠ No .jpg or .png images found in directory"
def get_current_model_info(self) -> str:
"""Get info about currently loaded model."""
if self.model and self.current_model_path:
return f"📦 Loaded: {self.current_model_path}"
elif self.model:
return "📦 Model loaded (pretrained)"
else:
return "⚠️ No model loaded"
def get_current_dir_info(self) -> str:
"""Get info about current images directory."""
return f"📁 {self.images_dir} ({len(self.image_paths)} images)"
def get_current_image(self) -> tuple[Image.Image, str]:
"""Get current image and filename."""
if not self.image_paths:
return None, ""
path = self.image_paths[self.current_idx]
img = Image.open(path).convert("RGB")
return img, path.name
def draw_boxes_on_image(self, img: Image.Image, boxes: list[dict]) -> Image.Image:
"""Draw bounding boxes on image."""
img_draw = img.copy()
draw = ImageDraw.Draw(img_draw)
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
label = box.get("label", "knot")
conf = box.get("confidence", 1.0)
# Draw box
draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
# Draw label
text = f"{label} {conf:.2f}" if conf < 1.0 else label
draw.text((x1, y1 - 20), text, fill="red")
return img_draw
def auto_label_current(self, threshold: float = 0.5) -> tuple[Image.Image, str, str]:
"""Auto-label current image with model."""
if not self.model:
return self.get_current_image()[0], "", "⚠ No model loaded"
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Run inference
detections = self.model.predict(img, threshold=threshold)
# Convert to our format
boxes = []
for i in range(len(detections)):
xyxy = detections.xyxy[i].tolist()
conf = float(detections.confidence[i]) if detections.confidence is not None else 1.0
boxes.append({
"bbox": xyxy,
"label": "knot",
"confidence": conf,
"source": "auto"
})
# Save
self.annotations[filename] = boxes
self._save_annotations()
# Draw
img_with_boxes = self.draw_boxes_on_image(img, boxes)
info = f"✓ Auto-labeled: {len(boxes)} boxes detected"
boxes_text = self._format_boxes_text(boxes)
return img_with_boxes, boxes_text, info
def _format_boxes_text(self, boxes: list[dict]) -> str:
"""Format boxes for display."""
if not boxes:
return "No annotations"
lines = []
for i, box in enumerate(boxes):
x1, y1, x2, y2 = box["bbox"]
conf = box.get("confidence", 1.0)
source = box.get("source", "manual")
lines.append(f"{i}: [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}] conf={conf:.2f} ({source})")
return "\n".join(lines)
def load_image(self, direction: str = "current") -> tuple[Image.Image, str, str]:
"""Load image (current/next/prev)."""
if direction == "next":
self.current_idx = min(self.current_idx + 1, len(self.image_paths) - 1)
elif direction == "prev":
self.current_idx = max(self.current_idx - 1, 0)
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Load existing annotations
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"Image {self.current_idx + 1}/{len(self.image_paths)}: {filename}"
return img_with_boxes, boxes_text, info
def add_box_manual(self, x1: int, y1: int, x2: int, y2: int) -> tuple[Image.Image, str, str]:
"""Manually add a bounding box."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
# Add box
box = {
"bbox": [float(x1), float(y1), float(x2), float(y2)],
"label": "knot",
"confidence": 1.0,
"source": "manual"
}
if filename not in self.annotations:
self.annotations[filename] = []
self.annotations[filename].append(box)
self._save_annotations()
# Redraw
boxes = self.annotations[filename]
img_with_boxes = self.draw_boxes_on_image(img, boxes)
boxes_text = self._format_boxes_text(boxes)
info = f"✓ Added box: {len(boxes)} total"
return img_with_boxes, boxes_text, info
def delete_last_box(self) -> tuple[Image.Image, str, str]:
"""Delete the last box from current image."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
if filename in self.annotations and self.annotations[filename]:
self.annotations[filename].pop()
self._save_annotations()
# Redraw
boxes = self.annotations.get(filename, [])
img_with_boxes = self.draw_boxes_on_image(img, boxes) if boxes else img
boxes_text = self._format_boxes_text(boxes)
info = f"✓ Deleted last box: {len(boxes)} remaining"
return img_with_boxes, boxes_text, info
def clear_boxes(self) -> tuple[Image.Image, str, str]:
"""Clear all boxes from current image."""
img, filename = self.get_current_image()
if not img:
return None, "", "No images"
self.annotations[filename] = []
self._save_annotations()
boxes_text = "No annotations"
info = "✓ Cleared all boxes"
return img, boxes_text, info
def _save_annotations(self):
"""Save annotations to JSON file."""
with self.ann_file.open("w") as f:
json.dump(self.annotations, f, indent=2)
def export_to_coco(self, output_path: Path):
"""Export annotations to COCO format."""
coco_data = {
"images": [],
"annotations": [],
"categories": [{"id": 0, "name": "knot", "supercategory": "defect"}]
}
ann_id = 0
for img_id, img_path in enumerate(self.image_paths):
filename = img_path.name
img = Image.open(img_path)
width, height = img.size
coco_data["images"].append({
"id": img_id,
"file_name": filename,
"width": width,
"height": height
})
# Add annotations
boxes = self.annotations.get(filename, [])
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
w = x2 - x1
h = y2 - y1
coco_data["annotations"].append({
"id": ann_id,
"image_id": img_id,
"category_id": 0,
"bbox": [x1, y1, w, h],
"area": w * h,
"iscrowd": 0,
"score": box.get("confidence", 1.0)
})
ann_id += 1
with output_path.open("w") as f:
def prepare_training_dataset(self, output_dir: Path, train_split: float = 0.8, valid_split: float = 0.1):
"""Prepare dataset in RF-DETR format (train/valid/test splits)."""
output_dir.mkdir(parents=True, exist_ok=True)
# Create splits
import random
annotated_images = [img for img in self.image_paths if img.name in self.annotations and self.annotations[img.name]]
if len(annotated_images) < 10:
return f"⚠️ Need at least 10 annotated images, have {len(annotated_images)}"
random.shuffle(annotated_images)
n = len(annotated_images)
train_n = int(n * train_split)
valid_n = int(n * valid_split)
splits = {
"train": annotated_images[:train_n],
"valid": annotated_images[train_n:train_n + valid_n],
"test": annotated_images[train_n + valid_n:]
}
# Create directories and copy images
import shutil
for split_name, split_images in splits.items():
split_dir = output_dir / split_name
split_dir.mkdir(exist_ok=True)
# Prepare COCO JSON for this split
coco_data = {
"images": [],
"annotations": [],
"categories": [{"id": 0, "name": "knot", "supercategory": "defect"}]
}
ann_id = 0
for img_id, img_path in enumerate(split_images):
# Copy image
dest = split_dir / img_path.name
shutil.copy2(img_path, dest)
# Add to COCO
img = Image.open(img_path)
width, height = img.size
coco_data["images"].append({
"id": img_id,
"file_name": img_path.name,
"width": width,
"height": height
})
# Add annotations
boxes = self.annotations.get(img_path.name, [])
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
w = x2 - x1
h = y2 - y1
coco_data["annotations"].append({
"id": ann_id,
"image_id": img_id,
"category_id": 0,
"bbox": [x1, y1, w, h],
"area": w * h,
"iscrowd": 0
})
ann_id += 1
# Save COCO JSON
with (split_dir / "_annotations.coco.json").open("w") as f:
json.dump(coco_data, f, indent=2)
return f"✓ Dataset prepared: {len(splits['train'])} train, {len(splits['valid'])} valid, {len(splits['test'])} test"
with gr.Row():
start_train_btn = gr.Button("🚀 Start Training", variant="primary")
stop_train_btn = gr.Button("⏹️ Stop Training", variant="stop")
refresh_status_btn = gr.Button("🔄 Refresh Status")
training_status = gr.Textbox(
label="Training Status",
value="Not training",
lines=3
)
gr.Markdown("""
**Note**: Training runs in the background. You can continue annotating while training.
Check the training log file for detailed progress.
""
boxes = self.annotations.get(img_path.name, [])
for box in boxes:
x1, y1, x2, y2 = box["bbox"]
w = x2 - x1
h = y2 - y1
coco_data["annotations"].append({
"id": ann_id,
"image_id": img_id,
"category_id": 0,
"bbox": [x1, y1, w, h],
"area": w * h,
"iscrowd": 0
})
ann_id += 1
# Save COCO JSON
with (split_dir / "_annotations.coco.json").open("w") as f:
json.dump(coco_data, f, indent=2)
return f"✓ Dataset prepared: {len(splits['train'])} train, {len(splits['valid'])} valid, {len(splits['test'])} test"
def start_training(self, dataset_dir: str, output_dir: str, model_size: str,
epochs: int, batch_size: int, lr: float, progress=gr.Progress()):
"""Start training in background."""
dataset_path = Path(dataset_dir)
output_path = Path(output_dir)
if not dataset_path.exists():
return "❌ Dataset directory not found"
if self.training_process and self.training_process.poll() is None:
return "⚠️ Training already in progress"
output_path.mkdir(parents=True, exist_ok=True)
# Build training command
venv_python = Path(__file__).parent / ".venv/bin/python"
train_script = Path(__file__).parent / "train_rfdetr.py"
cmd = [
str(venv_python),
str(train_script),
"--dataset-dir", str(dataset_path),
"--output-dir", str(output_path),
"--model", model_size,
"--epochs", str(epochs),
"--batch-size", str(batch_size),
"--grad-accum-steps", str(max(1, 16 // batch_size)),
"--lr", str(lr)
]
# Start training process
log_file = output_path / "training.log"
self.training_status = f"🚀 Starting training..."
def run_training():
try:
with log_file.open("w") as f:
self.training_process = subprocess.Popen(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
text=True
)
self.training_status = f"⏳ Training in progress (PID: {self.training_process.pid})"
self.training_process.wait()
if self.training_process.returncode == 0:
self.training_status = "✅ Training completed successfully!"
# Reload model with new weights
best_weights = output_path / "checkpoint_best_total.pth"
if best_weights.exists():
self._load_model(best_weights)
else:
self.training_status = f"❌ Training failed (exit code {self.training_process.returncode})"
except Exception as e:
self.training_status = f"❌ Error: {e}"
self.training_thread = threading.Thread(target=run_training, daemon=True)
self.training_thread.start()
return f"✓ Training started! Check {log_file} for progress"
def get_training_status(self):
"""Get current training status."""
return self.training_status
def stop_training(self):
"""Stop the training process."""
if self.training_process and self.training_process.poll() is None:
self.training_process.terminate()
self.training_status = "⏹️ Training stopped by user"
return "✓ Training process terminated"
return "⚠️ No training in progress"
json.dump(coco_data, f, indent=2)
return f"✓ Exported {len(coco_data['annotations'])} annotations to {output_path}"
def create_ui(app: AnnotationApp) -> gr.Blocks:
"""Create Gradio UI."""
with gr.Blocks(title="Knot Annotation Tool") as demo:
gr.Markdown("""
# 🪵 Wood Knot Annotation Tool
**Label -> Train -> Auto-Label -> Repeat**
- Manually annotate images or use **Auto-Label** with your trained model
- Export and prepare dataset for training
- Train RF-DETR directly from this GUI
- Use trained model to auto-label more images
""")
with gr.Row():
with gr.Column(scale=3):
image_display = gr.Image(label="Current Image", type="pil")
with gr.Row():
prev_btn = gr.Button("⬅️ Previous")
next_btn = gr.Button("Next ➡️")
auto_label_btn = gr.Button("🤖 Auto-Label", variant="primary")
with gr.Row():
threshold_slider = gr.Slider(0.1, 0.9, DEFAULT_DETECTION_THRESHOLD, label="Detection Threshold")
with gr.Column(scale=1):
info_text = gr.Textbox(label="Status", lines=2)
boxes_text = gr.Textbox(label="Annotations", lines=10)
gr.Markdown("### Manual Annotation")
with gr.Row():
x1_input = gr.Number(label="x1", value=100)
y1_input = gr.Number(label="y1", value=100)
with gr.Row():
Training handlers
prep_btn.click(
lambda out, train, valid: app.prepare_training_dataset(Path(out), train, valid),
inputs=[dataset_prep_dir, train_split, valid_split],
outputs=[prep_result]
)
start_train_btn.click(
app.start_training,
inputs=[train_dataset_dir, train_output_dir, model_size, epochs, batch_size, learning_rate],
outputs=[training_status]
)
stop_train_btn.click(
app.stop_training,
outputs=[training_status]
)
refresh_status_btn.click(
app.get_training_status,
outputs=[training_status]
)
# x2_input = gr.Number(label="x2", value=200)
y2_input = gr.Number(label="y2", value=200)
add_box_btn = gr.Button(" Add Box")
delete_btn = gr.Button("🗑️ Delete Last")
clear_btn = gr.Button("❌ Clear All")
gr.Markdown("### Export")
export_path = gr.Textbox(
label="Output Path",
value="annotations_coco.json"
)
export_btn = gr.Button("💾 Export COCO")
export_result = gr.Textbox(label="Export Result")
# Event handlers
def on_load():
return app.load_image("current")
# Settings handlers
load_images_btn.click(
app.load_new_images_dir,
inputs=[images_dir_input],
outputs=[image_display, boxes_text, info_text]
).then(
lambda: (app.get_current_dir_info(), app.get_current_model_info()),
outputs=[dir_info, model_info]
)
load_model_btn.click(
app.load_new_model,
inputs=[model_weights_input],
outputs=[model_info]
)
prev_btn.click(
lambda: app.load_image("prev"),
outputs=[image_display, boxes_text, info_text]
)
next_btn.click(
lambda: app.load_image("next"),
outputs=[image_display, boxes_text, info_text]
)
auto_label_btn.click(
lambda t: app.auto_label_current(t),
inputs=[threshold_slider],
outputs=[image_display, boxes_text, info_text]
)
add_box_btn.click(
app.add_box_manual,
inputs=[x1_input, y1_input, x2_input, y2_input],
outputs=[image_display, boxes_text, info_text]
)
delete_btn.click(
app.delete_last_box,
outputs=[image_display, boxes_text, info_text]
)
clear_btn.click(
app.clear_boxes,
outputs=[image_display, boxes_text, info_text]
)help="Default directory with images (can be changed in GUI)")
parser.add_argument("--model-weights", type=Path, help="Default trained model for auto-labeling (can be changed in GUI)")
parser.add_argument("--port", type=int, default=7860, help="Port for web interface")
args = parser.parse_args()
# Validate paths if provided
if args.images_dir and not args.images_dir.exists():
print(f"⚠️ Warning: Images directory not found: {args.images_dir}")
print("You can load a different directory from the GUI Settings")
args.images_dir = None
if args.model_weights and not args.model_weights.exists():
print(f"⚠️ Warning: Model weights not found: {args.model_weights}")
print("You can load different weights from the GUI Settings")
args.model_weights = None
# Load first image on start
demo.load(on_load, outputs=[image_display, boxes_text, info_text])
return demo
def main():
parser = argparse.ArgumentParser(description="Simple annotation GUI with auto-labeling")
parser.add_argument("--images-dir", type=Path, required=True, help="Directory with images")
parser.add_argument("--model-weights", type=Path, help="Optional: trained model for auto-labeling")
parser.add_argument("--port", type=int, default=7860, help="Port for web interface")
args = parser.parse_args()
if not args.images_dir.exists():
raise SystemExit(f"Images directory not found: {args.images_dir}")
if args.images_dir:
print(f"📁 Default images: {args.images_dir} ({len(app.image_paths)} images)")
else:
print(f"📁 No default images - load directory from Settings")
if app.model:
print(f"🤖 Model: Loaded from {args.model_weights}")
else:
print(f"⚠️ No model loaded - load from Settings or train one")
print(f"💡 You can change images directory and model weights from the Settings panel
print(f"\n{'='*60}")
print(f"🚀 Starting annotation tool...")
print(f"📁 Images: {args.images_dir} ({len(app.image_paths)} images)")
if app.model:
print(f"🤖 Model: Loaded from {args.model_weights}")
else:
print(f"⚠️ No model loaded (manual annotation only)")
print(f"{'='*60}\n")
demo.launch(
server_name="0.0.0.0",
server_port=args.port,
share=False
)
if __name__ == "__main__":
main()

104
auto_label_images.py Normal file
View File

@ -0,0 +1,104 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from PIL import Image
def _detections_to_coco(
detections: Any, # supervision.Detections
image_path: Path,
image_id: int,
category_id: int = 0, # Assuming single class 'knot'
) -> dict[str, Any]:
"""Convert supervision.Detections to COCO annotation format for one image."""
annotations = []
for i in range(len(detections)):
xyxy = detections.xyxy[i].tolist()
conf = float(detections.confidence[i]) if detections.confidence is not None else 1.0
# COCO bbox: [x_min, y_min, width, height]
x_min, y_min, x_max, y_max = xyxy
width = x_max - x_min
height = y_max - y_min
bbox = [x_min, y_min, width, height]
ann = {
"id": image_id * 1000 + i, # Simple ID scheme
"image_id": image_id,
"category_id": category_id,
"bbox": bbox,
"area": width * height,
"iscrowd": 0,
"score": conf, # Optional, for model confidence
}
annotations.append(ann)
return {"annotations": annotations}
def main() -> int:
parser = argparse.ArgumentParser(
description="Auto-label new images with trained RF-DETR, output COCO JSON."
)
parser.add_argument("--weights", type=Path, required=True, help="Path to trained checkpoint")
parser.add_argument("--images-dir", type=Path, required=True, help="Directory with new images")
parser.add_argument("--output-json", type=Path, required=True, help="Output COCO JSON file")
parser.add_argument("--threshold", type=float, default=0.5)
parser.add_argument("--category-name", default="knot", help="Class name for annotations")
args = parser.parse_args()
if not args.weights.exists():
raise SystemExit(f"Weights not found: {args.weights}")
if not args.images_dir.exists():
raise SystemExit(f"Images dir not found: {args.images_dir}")
from rfdetr import RFDETRBase
model = RFDETRBase(pretrain_weights=str(args.weights))
# Collect all images
image_paths = list(args.images_dir.glob("*.jpg")) + list(args.images_dir.glob("*.png"))
if not image_paths:
raise SystemExit(f"No .jpg or .png images found in {args.images_dir}")
coco_data = {
"images": [],
"annotations": [],
"categories": [{"id": 0, "name": args.category_name, "supercategory": "none"}],
}
ann_id_counter = 0
for img_id, img_path in enumerate(sorted(image_paths)):
image = Image.open(img_path).convert("RGB")
detections = model.predict(image, threshold=args.threshold)
# Add image entry
width, height = image.size
coco_data["images"].append({
"id": img_id,
"file_name": img_path.name,
"width": width,
"height": height,
})
# Convert detections to annotations
ann_data = _detections_to_coco(detections, img_path, img_id, category_id=0)
for ann in ann_data["annotations"]:
ann["id"] = ann_id_counter
ann_id_counter += 1
coco_data["annotations"].append(ann)
# Write COCO JSON
with args.output_json.open("w", encoding="utf-8") as f:
json.dump(coco_data, f, indent=2)
print(f"Auto-labeled {len(image_paths)} images -> {args.output_json}")
print(f"Total annotations: {len(coco_data['annotations'])}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

28
config.py Normal file
View File

@ -0,0 +1,28 @@
# config.py - Default settings for annotation GUI
# Default directories (edit these to your preferences)
DEFAULT_IMAGES_DIR = "IMAGE/" # Directory containing wood defect images
DEFAULT_MODEL_WEIGHTS = "runs/yolox_training/training/weights/best.pt" # Trained YOLOX model
# Training defaults
DEFAULT_TRAIN_EPOCHS = 20
DEFAULT_BATCH_SIZE = 4
DEFAULT_LEARNING_RATE = 1e-4
DEFAULT_MODEL_SIZE = "small" # nano, small, medium, base
# Dataset split ratios
DEFAULT_TRAIN_SPLIT = 0.8
DEFAULT_VALID_SPLIT = 0.1
# test split = 1 - train - valid
# GUI settings
DEFAULT_PORT = 7860
DEFAULT_DETECTION_THRESHOLD = 0.5
# Annotation categories (add more if needed)
ANNOTATION_CATEGORIES = [
"knot",
# "crack",
# "hole",
# "discoloration"
]

128
convert_to_label_studio.py Normal file
View File

@ -0,0 +1,128 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from PIL import Image
def coco_to_label_studio(
image_paths: list[Path],
coco_predictions_path: Path,
category_name: str = "knot",
) -> list[dict]:
"""Convert COCO predictions to Label Studio import format with pre-annotations."""
# Load COCO predictions
with coco_predictions_path.open("r", encoding="utf-8") as f:
coco_data = json.load(f)
# Build image_id -> annotations mapping
annotations_by_image = {}
for ann in coco_data.get("annotations", []):
img_id = ann["image_id"]
if img_id not in annotations_by_image:
annotations_by_image[img_id] = []
annotations_by_image[img_id].append(ann)
# Build image_id -> image info mapping
image_info = {img["id"]: img for img in coco_data.get("images", [])}
# Convert to Label Studio format
ls_tasks = []
for img_id, img_data in image_info.items():
file_name = img_data["file_name"]
width = img_data["width"]
height = img_data["height"]
# Find the actual file path
matching_paths = [p for p in image_paths if p.name == file_name]
if not matching_paths:
continue
img_path = matching_paths[0]
# Build predictions (pre-annotations)
predictions = []
for ann in annotations_by_image.get(img_id, []):
# COCO bbox: [x_min, y_min, width, height]
x, y, w, h = ann["bbox"]
# Label Studio uses percentages
x_percent = (x / width) * 100
y_percent = (y / height) * 100
w_percent = (w / width) * 100
h_percent = (h / height) * 100
predictions.append({
"value": {
"x": x_percent,
"y": y_percent,
"width": w_percent,
"height": h_percent,
"rectanglelabels": [category_name]
},
"from_name": "label",
"to_name": "image",
"type": "rectanglelabels",
"score": ann.get("score", 1.0)
})
# Create Label Studio task with predictions
task = {
"data": {
"image": f"/data/local-files/?d={img_path.absolute()}"
},
"predictions": [{
"result": predictions,
"model_version": "rfdetr-auto-label"
}]
}
ls_tasks.append(task)
return ls_tasks
def main() -> int:
parser = argparse.ArgumentParser(
description="Convert COCO predictions to Label Studio format for review."
)
parser.add_argument("--coco-json", type=Path, required=True, help="COCO predictions JSON from auto_label_images.py")
parser.add_argument("--images-dir", type=Path, required=True, help="Directory with the images")
parser.add_argument("--output-json", type=Path, required=True, help="Output Label Studio tasks JSON")
parser.add_argument("--category-name", default="knot", help="Label name in Label Studio")
args = parser.parse_args()
if not args.coco_json.exists():
raise SystemExit(f"COCO JSON not found: {args.coco_json}")
if not args.images_dir.exists():
raise SystemExit(f"Images dir not found: {args.images_dir}")
# Collect all images
image_paths = list(args.images_dir.glob("*.jpg")) + list(args.images_dir.glob("*.png"))
if not image_paths:
raise SystemExit(f"No images found in {args.images_dir}")
# Convert
ls_tasks = coco_to_label_studio(image_paths, args.coco_json, args.category_name)
# Write Label Studio import format
with args.output_json.open("w", encoding="utf-8") as f:
json.dump(ls_tasks, f, indent=2)
print(f"✓ Converted {len(ls_tasks)} tasks with predictions")
print(f"✓ Output: {args.output_json}")
print(f"\nImport into Label Studio:")
print(f" 1. Open your project")
print(f" 2. Settings → Storage → Add Local Storage")
print(f" Absolute path: {args.images_dir.absolute()}")
print(f" 3. Import → Upload Files → select {args.output_json.name}")
print(f" 4. Start reviewing predictions!")
return 0
if __name__ == "__main__":
raise SystemExit(main())

18
dataset_split/data.yaml Normal file
View File

@ -0,0 +1,18 @@
# YOLO dataset configuration
path: /home/dillon/_code/saw_mill_knot_detection/dataset_split # dataset root dir
train: train/images # train images (relative to 'path')
val: valid/images # val images (relative to 'path')
test: test/images # test images (relative to 'path')
# Classes
names:
0: Live knot
1: Dead knot
2: Knot with crack
3: Crack
4: Resin
5: Marrow
6: Quartzity
7: Knot missing
8: Blue stain
9: Overgrown

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
4 0.573214 0.049805 0.041429 0.099609
6 0.414464 0.500000 0.061786 1.000000
0 0.173214 0.163575 0.236429 0.254883

View File

@ -0,0 +1 @@
6 0.416607 0.500000 0.097500 1.000000

View File

@ -0,0 +1,8 @@
2 0.751786 0.627930 0.003571 0.003906
2 0.745178 0.622559 0.000357 0.000977
2 0.744465 0.621582 0.000357 0.000976
2 0.802500 0.523438 0.002858 0.003907
0 0.400536 0.761231 0.000357 0.000977
0 0.406071 0.756347 0.010000 0.008789
0 0.361071 0.671875 0.088571 0.117188
0 0.793572 0.578125 0.112143 0.103516

View File

View File

@ -0,0 +1,4 @@
1 0.320863 0.982422 0.062590 0.035156
1 0.496583 0.424316 0.000360 0.000977
1 0.458993 0.372071 0.055396 0.078125
1 0.536331 0.191895 0.030216 0.047851

View File

@ -0,0 +1,2 @@
2 0.373393 0.126953 0.203214 0.253906
1 0.854464 0.477051 0.031071 0.071289

View File

@ -0,0 +1 @@
6 0.418929 0.535644 0.047143 0.928711

View File

@ -0,0 +1,3 @@
4 0.724642 0.071777 0.027857 0.143555
0 0.648035 0.861328 0.063929 0.060547
0 0.412322 0.842773 0.068929 0.080078

View File

View File

@ -0,0 +1,2 @@
2 0.443393 0.656250 0.106072 0.152344
0 0.867857 0.756347 0.129286 0.186523

View File

@ -0,0 +1,3 @@
1 0.296964 0.062500 0.067500 0.111328
0 0.279286 0.132325 0.002143 0.004883
0 0.517500 0.172851 0.060000 0.105469

View File

@ -0,0 +1,4 @@
0 0.386964 0.723144 0.003214 0.002929
0 0.785000 0.709960 0.203572 0.171875
0 0.350893 0.595215 0.000357 0.000976
0 0.389285 0.641114 0.087143 0.125977

View File

@ -0,0 +1,2 @@
0 0.354465 0.247070 0.058929 0.101563
0 0.505536 0.100098 0.081071 0.118164

View File

@ -0,0 +1,2 @@
1 0.153929 0.525879 0.026429 0.067383
0 0.728929 0.367675 0.035000 0.061523

View File

@ -0,0 +1 @@
1 0.527321 0.804688 0.036785 0.080079

View File

@ -0,0 +1,2 @@
1 0.880178 0.479981 0.095357 0.133789
1 0.277143 0.068360 0.069286 0.099609

View File

@ -0,0 +1,2 @@
1 0.307678 0.921386 0.110357 0.157227
1 0.677143 0.748535 0.099286 0.143554

View File

View File

@ -0,0 +1 @@
1 0.359822 0.544922 0.063929 0.087890

View File

View File

@ -0,0 +1,4 @@
2 0.425179 0.242676 0.002500 0.006836
1 0.073036 0.072265 0.026786 0.064453
0 0.913214 0.668457 0.050000 0.170898
0 0.395357 0.323242 0.088572 0.113281

View File

View File

@ -0,0 +1,5 @@
0 0.689464 0.869628 0.003214 0.008789
0 0.630357 0.918945 0.094286 0.121094
0 0.601965 0.839356 0.000357 0.000977
0 0.603036 0.837403 0.000357 0.000977
0 0.449107 0.505859 0.059643 0.093750

View File

@ -0,0 +1,4 @@
1 0.465715 0.573730 0.038571 0.073243
0 0.447857 0.499512 0.000714 0.002930
0 0.666964 0.128418 0.062500 0.088868
0 0.298929 0.104492 0.035000 0.068360

View File

@ -0,0 +1,2 @@
0 0.428036 0.971680 0.054643 0.056641
0 0.664107 0.894531 0.041786 0.062500

View File

@ -0,0 +1,3 @@
1 0.430357 0.400879 0.047143 0.057617
1 0.589465 0.264160 0.023929 0.045898
0 0.521965 0.224121 0.046071 0.075196

View File

@ -0,0 +1,2 @@
1 0.771607 0.750000 0.111786 0.117188
1 0.144286 0.482910 0.102143 0.094726

View File

@ -0,0 +1 @@
0 0.619822 0.447265 0.026071 0.064453

View File

@ -0,0 +1,2 @@
4 0.514107 0.680664 0.031072 0.294922
0 0.447322 0.783203 0.036071 0.062500

View File

@ -0,0 +1,3 @@
0 0.604822 0.949707 0.050357 0.100586
0 0.382321 0.697266 0.047500 0.093750
0 0.663215 0.396484 0.036429 0.089844

View File

@ -0,0 +1,2 @@
1 0.431428 0.983399 0.022857 0.033203
0 0.460715 0.291016 0.022857 0.062500

View File

@ -0,0 +1,2 @@
4 0.859286 0.355957 0.025714 0.213868
1 0.105179 0.257325 0.099643 0.182617

View File

View File

View File

@ -0,0 +1,2 @@
2 0.198392 0.945801 0.000357 0.000977
1 0.161608 0.958008 0.079643 0.083984

View File

@ -0,0 +1 @@
4 0.792500 0.042968 0.017858 0.085937

View File

@ -0,0 +1,2 @@
1 0.473572 0.983886 0.032857 0.032227
1 0.524285 0.400879 0.042143 0.073242

View File

@ -0,0 +1,2 @@
1 0.322857 0.973633 0.030714 0.052734
1 0.664643 0.942871 0.035000 0.059570

View File

@ -0,0 +1 @@
1 0.406250 0.060547 0.040358 0.070312

View File

@ -0,0 +1 @@
1 0.382857 0.045899 0.146428 0.091797

View File

View File

@ -0,0 +1,6 @@
6 0.546964 0.818360 0.043214 0.363281
3 0.550536 0.393555 0.016786 0.244141
3 0.534822 0.169433 0.013929 0.147461
3 0.528036 0.044922 0.011786 0.087890
1 0.522857 0.419922 0.027857 0.062500
0 0.574107 0.428223 0.028928 0.065429

View File

@ -0,0 +1 @@
0 0.495000 0.114258 0.055000 0.093750

View File

@ -0,0 +1,2 @@
1 0.912143 0.694336 0.035714 0.128906
1 0.466071 0.215820 0.091429 0.128906

View File

View File

@ -0,0 +1,2 @@
0 0.679643 0.148438 0.000714 0.001953
0 0.609822 0.164551 0.128929 0.163086

View File

@ -0,0 +1,2 @@
2 0.656964 0.482422 0.121071 0.115234
0 0.116965 0.410157 0.119643 0.279297

View File

@ -0,0 +1,2 @@
0 0.676250 0.802734 0.051786 0.111328
0 0.374108 0.373535 0.025357 0.065430

View File

@ -0,0 +1,2 @@
1 0.584108 0.029785 0.125357 0.059570
0 0.116965 0.085449 0.126071 0.170898

View File

@ -0,0 +1,3 @@
1 0.249643 0.038574 0.034286 0.047852
1 0.921964 0.028809 0.028214 0.057617
0 0.549821 0.395019 0.043929 0.071289

View File

@ -0,0 +1,4 @@
2 0.353036 0.932617 0.117500 0.134766
1 0.636429 0.952149 0.022143 0.060547
1 0.905893 0.414062 0.062500 0.052735
1 0.183929 0.396973 0.050000 0.051758

View File

View File

@ -0,0 +1,2 @@
0 0.760893 0.416992 0.046786 0.066406
0 0.435179 0.371582 0.058215 0.071290

View File

@ -0,0 +1,5 @@
4 0.208750 0.357422 0.066786 0.294922
4 0.239285 0.103515 0.032857 0.207031
0 0.444643 0.780761 0.046428 0.061523
0 0.858214 0.702636 0.077143 0.102539
0 0.520536 0.262695 0.042500 0.076172

View File

@ -0,0 +1,2 @@
6 0.502321 0.297364 0.098929 0.375977
3 0.562143 0.061524 0.035000 0.123047

View File

View File

View File

@ -0,0 +1,8 @@
1 0.328928 0.947266 0.021429 0.058593
1 0.098572 0.852051 0.027143 0.065430
1 0.253215 0.820312 0.021429 0.058593
1 0.321429 0.815430 0.021429 0.058594
1 0.151965 0.710449 0.023929 0.063476
1 0.516965 0.692383 0.050357 0.068359
1 0.913393 0.649414 0.039643 0.054688
1 0.886964 0.523926 0.043929 0.063477

View File

View File

@ -0,0 +1,3 @@
1 0.723215 0.432617 0.021429 0.058594
0 0.674821 0.928711 0.001071 0.011718
0 0.553571 0.897461 0.200715 0.205078

View File

View File

@ -0,0 +1 @@
4 0.175000 0.082519 0.107858 0.165039

View File

@ -0,0 +1,4 @@
0 0.500000 0.591797 0.021428 0.058594
0 0.450000 0.583008 0.021428 0.058594
0 0.433393 0.451660 0.068214 0.075196
0 0.559107 0.427246 0.063214 0.075196

View File

@ -0,0 +1,4 @@
6 0.493571 0.765136 0.030000 0.469727
0 0.435179 0.221680 0.102500 0.111328
0 0.520714 0.170899 0.020000 0.054687
0 0.620000 0.124023 0.145000 0.107422

View File

@ -0,0 +1 @@
6 0.465000 0.391601 0.061428 0.783203

View File

@ -0,0 +1,2 @@
4 0.682321 0.874511 0.027500 0.250977
1 0.583214 0.771972 0.029286 0.057617

View File

@ -0,0 +1,2 @@
3 0.570893 0.500000 0.023214 1.000000
3 0.481965 0.500000 0.035357 1.000000

View File

@ -0,0 +1,4 @@
4 0.751607 0.197753 0.039643 0.227539
0 0.382858 0.973633 0.047143 0.052734
0 0.740714 0.816894 0.068571 0.094727
0 0.500892 0.218750 0.044643 0.082032

View File

@ -0,0 +1 @@
0 0.713036 0.125489 0.103214 0.112305

View File

@ -0,0 +1,3 @@
6 0.554107 0.318848 0.043928 0.325195
1 0.576964 0.888184 0.054643 0.038086
1 0.493214 0.828125 0.039286 0.078125

View File

@ -0,0 +1 @@
1 0.920893 0.944824 0.058214 0.110352

View File

@ -0,0 +1,4 @@
4 0.157143 0.059082 0.030000 0.118164
3 0.622678 0.818847 0.038215 0.362305
3 0.668214 0.258789 0.031429 0.429688
0 0.900893 0.540528 0.073214 0.206055

View File

@ -0,0 +1,2 @@
1 0.739822 0.935059 0.021071 0.059571
0 0.728392 0.897949 0.000357 0.000976

View File

@ -0,0 +1,4 @@
3 0.512500 0.646972 0.024286 0.258789
0 0.240714 0.894531 0.252857 0.210938
0 0.485179 0.741699 0.029643 0.067383
0 0.545714 0.745605 0.038571 0.081055

View File

@ -0,0 +1,2 @@
0 0.522321 0.174316 0.001785 0.004883
0 0.542500 0.194824 0.032858 0.055664

View File

@ -0,0 +1 @@
0 0.225357 0.400391 0.143572 0.076172

View File

@ -0,0 +1,2 @@
1 0.721250 0.740234 0.035358 0.062500
1 0.390714 0.244629 0.019286 0.051758

View File

View File

@ -0,0 +1,2 @@
1 0.098393 0.546875 0.088928 0.103516
0 0.603393 0.514648 0.088928 0.136719

View File

@ -0,0 +1 @@
1 0.367321 0.337891 0.023929 0.048828

View File

@ -0,0 +1 @@
0 0.527321 0.319825 0.028929 0.057617

View File

@ -0,0 +1,6 @@
3 0.489107 0.425781 0.048214 0.851562
7 0.058750 0.922364 0.000358 0.000977
7 0.060000 0.920899 0.001428 0.001953
1 0.594107 0.984864 0.043928 0.030273
1 0.068214 0.960938 0.017143 0.046875
1 0.181071 0.030762 0.168571 0.061523

View File

@ -0,0 +1 @@
1 0.618392 0.967774 0.069643 0.064453

View File

View File

View File

@ -0,0 +1 @@
5 0.474822 0.500000 0.041785 1.000000

View File

@ -0,0 +1 @@
5 0.467500 0.478515 0.041428 0.957031

Some files were not shown because too many files have changed in this diff Show More