Initial commit: Wood knot detection model and GUI
This commit is contained in:
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal 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
148
GUI_README.md
Normal 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
244
LABELING_GUIDE.md
Normal 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
92
MODEL_COMPARISON.md
Normal 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
153
README.md
Normal 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
188
RTDETR_README.md
Normal 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
824
annotation_gui.py
Normal 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
708
annotation_gui.py.broken
Normal 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
104
auto_label_images.py
Normal 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
28
config.py
Normal 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
128
convert_to_label_studio.py
Normal 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
18
dataset_split/data.yaml
Normal 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
|
||||
74508
dataset_split/test/_annotations.coco.json
Normal file
74508
dataset_split/test/_annotations.coco.json
Normal file
File diff suppressed because it is too large
Load Diff
3
dataset_split/test/labels/100000013.txt
Normal file
3
dataset_split/test/labels/100000013.txt
Normal 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
|
||||
1
dataset_split/test/labels/100000018.txt
Normal file
1
dataset_split/test/labels/100000018.txt
Normal file
@ -0,0 +1 @@
|
||||
6 0.416607 0.500000 0.097500 1.000000
|
||||
8
dataset_split/test/labels/100000044.txt
Normal file
8
dataset_split/test/labels/100000044.txt
Normal 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
|
||||
0
dataset_split/test/labels/100000058.txt
Normal file
0
dataset_split/test/labels/100000058.txt
Normal file
4
dataset_split/test/labels/100000062.txt
Normal file
4
dataset_split/test/labels/100000062.txt
Normal 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
|
||||
2
dataset_split/test/labels/100000084.txt
Normal file
2
dataset_split/test/labels/100000084.txt
Normal file
@ -0,0 +1,2 @@
|
||||
2 0.373393 0.126953 0.203214 0.253906
|
||||
1 0.854464 0.477051 0.031071 0.071289
|
||||
1
dataset_split/test/labels/100100013.txt
Normal file
1
dataset_split/test/labels/100100013.txt
Normal file
@ -0,0 +1 @@
|
||||
6 0.418929 0.535644 0.047143 0.928711
|
||||
3
dataset_split/test/labels/100100023.txt
Normal file
3
dataset_split/test/labels/100100023.txt
Normal 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
|
||||
0
dataset_split/test/labels/100100028.txt
Normal file
0
dataset_split/test/labels/100100028.txt
Normal file
2
dataset_split/test/labels/100100043.txt
Normal file
2
dataset_split/test/labels/100100043.txt
Normal file
@ -0,0 +1,2 @@
|
||||
2 0.443393 0.656250 0.106072 0.152344
|
||||
0 0.867857 0.756347 0.129286 0.186523
|
||||
3
dataset_split/test/labels/100100053.txt
Normal file
3
dataset_split/test/labels/100100053.txt
Normal 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
|
||||
4
dataset_split/test/labels/100400027.txt
Normal file
4
dataset_split/test/labels/100400027.txt
Normal 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
|
||||
2
dataset_split/test/labels/100400036.txt
Normal file
2
dataset_split/test/labels/100400036.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.354465 0.247070 0.058929 0.101563
|
||||
0 0.505536 0.100098 0.081071 0.118164
|
||||
2
dataset_split/test/labels/100400057.txt
Normal file
2
dataset_split/test/labels/100400057.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.153929 0.525879 0.026429 0.067383
|
||||
0 0.728929 0.367675 0.035000 0.061523
|
||||
1
dataset_split/test/labels/100400064.txt
Normal file
1
dataset_split/test/labels/100400064.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.527321 0.804688 0.036785 0.080079
|
||||
2
dataset_split/test/labels/100400076.txt
Normal file
2
dataset_split/test/labels/100400076.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.880178 0.479981 0.095357 0.133789
|
||||
1 0.277143 0.068360 0.069286 0.099609
|
||||
2
dataset_split/test/labels/100500024.txt
Normal file
2
dataset_split/test/labels/100500024.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.307678 0.921386 0.110357 0.157227
|
||||
1 0.677143 0.748535 0.099286 0.143554
|
||||
0
dataset_split/test/labels/100500040.txt
Normal file
0
dataset_split/test/labels/100500040.txt
Normal file
1
dataset_split/test/labels/100500042.txt
Normal file
1
dataset_split/test/labels/100500042.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.359822 0.544922 0.063929 0.087890
|
||||
0
dataset_split/test/labels/100500045.txt
Normal file
0
dataset_split/test/labels/100500045.txt
Normal file
4
dataset_split/test/labels/100500078.txt
Normal file
4
dataset_split/test/labels/100500078.txt
Normal 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
|
||||
0
dataset_split/test/labels/100500080.txt
Normal file
0
dataset_split/test/labels/100500080.txt
Normal file
5
dataset_split/test/labels/100700040.txt
Normal file
5
dataset_split/test/labels/100700040.txt
Normal 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
|
||||
4
dataset_split/test/labels/100700042.txt
Normal file
4
dataset_split/test/labels/100700042.txt
Normal 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
|
||||
2
dataset_split/test/labels/100700051.txt
Normal file
2
dataset_split/test/labels/100700051.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.428036 0.971680 0.054643 0.056641
|
||||
0 0.664107 0.894531 0.041786 0.062500
|
||||
3
dataset_split/test/labels/100700081.txt
Normal file
3
dataset_split/test/labels/100700081.txt
Normal 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
|
||||
2
dataset_split/test/labels/101100048.txt
Normal file
2
dataset_split/test/labels/101100048.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.771607 0.750000 0.111786 0.117188
|
||||
1 0.144286 0.482910 0.102143 0.094726
|
||||
1
dataset_split/test/labels/101100079.txt
Normal file
1
dataset_split/test/labels/101100079.txt
Normal file
@ -0,0 +1 @@
|
||||
0 0.619822 0.447265 0.026071 0.064453
|
||||
2
dataset_split/test/labels/101500007.txt
Normal file
2
dataset_split/test/labels/101500007.txt
Normal file
@ -0,0 +1,2 @@
|
||||
4 0.514107 0.680664 0.031072 0.294922
|
||||
0 0.447322 0.783203 0.036071 0.062500
|
||||
3
dataset_split/test/labels/101500050.txt
Normal file
3
dataset_split/test/labels/101500050.txt
Normal 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
|
||||
2
dataset_split/test/labels/101500058.txt
Normal file
2
dataset_split/test/labels/101500058.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.431428 0.983399 0.022857 0.033203
|
||||
0 0.460715 0.291016 0.022857 0.062500
|
||||
2
dataset_split/test/labels/101500065.txt
Normal file
2
dataset_split/test/labels/101500065.txt
Normal file
@ -0,0 +1,2 @@
|
||||
4 0.859286 0.355957 0.025714 0.213868
|
||||
1 0.105179 0.257325 0.099643 0.182617
|
||||
0
dataset_split/test/labels/101600008.txt
Normal file
0
dataset_split/test/labels/101600008.txt
Normal file
0
dataset_split/test/labels/101600011.txt
Normal file
0
dataset_split/test/labels/101600011.txt
Normal file
2
dataset_split/test/labels/101600027.txt
Normal file
2
dataset_split/test/labels/101600027.txt
Normal file
@ -0,0 +1,2 @@
|
||||
2 0.198392 0.945801 0.000357 0.000977
|
||||
1 0.161608 0.958008 0.079643 0.083984
|
||||
1
dataset_split/test/labels/101600068.txt
Normal file
1
dataset_split/test/labels/101600068.txt
Normal file
@ -0,0 +1 @@
|
||||
4 0.792500 0.042968 0.017858 0.085937
|
||||
2
dataset_split/test/labels/101600083.txt
Normal file
2
dataset_split/test/labels/101600083.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.473572 0.983886 0.032857 0.032227
|
||||
1 0.524285 0.400879 0.042143 0.073242
|
||||
2
dataset_split/test/labels/101700018.txt
Normal file
2
dataset_split/test/labels/101700018.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.322857 0.973633 0.030714 0.052734
|
||||
1 0.664643 0.942871 0.035000 0.059570
|
||||
1
dataset_split/test/labels/101700026.txt
Normal file
1
dataset_split/test/labels/101700026.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.406250 0.060547 0.040358 0.070312
|
||||
1
dataset_split/test/labels/101700046.txt
Normal file
1
dataset_split/test/labels/101700046.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.382857 0.045899 0.146428 0.091797
|
||||
0
dataset_split/test/labels/101800008.txt
Normal file
0
dataset_split/test/labels/101800008.txt
Normal file
6
dataset_split/test/labels/101900014.txt
Normal file
6
dataset_split/test/labels/101900014.txt
Normal 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
|
||||
1
dataset_split/test/labels/101900045.txt
Normal file
1
dataset_split/test/labels/101900045.txt
Normal file
@ -0,0 +1 @@
|
||||
0 0.495000 0.114258 0.055000 0.093750
|
||||
2
dataset_split/test/labels/101900058.txt
Normal file
2
dataset_split/test/labels/101900058.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.912143 0.694336 0.035714 0.128906
|
||||
1 0.466071 0.215820 0.091429 0.128906
|
||||
0
dataset_split/test/labels/101900061.txt
Normal file
0
dataset_split/test/labels/101900061.txt
Normal file
2
dataset_split/test/labels/101900063.txt
Normal file
2
dataset_split/test/labels/101900063.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.679643 0.148438 0.000714 0.001953
|
||||
0 0.609822 0.164551 0.128929 0.163086
|
||||
2
dataset_split/test/labels/102100016.txt
Normal file
2
dataset_split/test/labels/102100016.txt
Normal file
@ -0,0 +1,2 @@
|
||||
2 0.656964 0.482422 0.121071 0.115234
|
||||
0 0.116965 0.410157 0.119643 0.279297
|
||||
2
dataset_split/test/labels/102100017.txt
Normal file
2
dataset_split/test/labels/102100017.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.676250 0.802734 0.051786 0.111328
|
||||
0 0.374108 0.373535 0.025357 0.065430
|
||||
2
dataset_split/test/labels/102100029.txt
Normal file
2
dataset_split/test/labels/102100029.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.584108 0.029785 0.125357 0.059570
|
||||
0 0.116965 0.085449 0.126071 0.170898
|
||||
3
dataset_split/test/labels/103000000.txt
Normal file
3
dataset_split/test/labels/103000000.txt
Normal 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
|
||||
4
dataset_split/test/labels/103000006.txt
Normal file
4
dataset_split/test/labels/103000006.txt
Normal 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
|
||||
0
dataset_split/test/labels/103000025.txt
Normal file
0
dataset_split/test/labels/103000025.txt
Normal file
2
dataset_split/test/labels/103000045.txt
Normal file
2
dataset_split/test/labels/103000045.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.760893 0.416992 0.046786 0.066406
|
||||
0 0.435179 0.371582 0.058215 0.071290
|
||||
5
dataset_split/test/labels/103200041.txt
Normal file
5
dataset_split/test/labels/103200041.txt
Normal 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
|
||||
2
dataset_split/test/labels/103200057.txt
Normal file
2
dataset_split/test/labels/103200057.txt
Normal file
@ -0,0 +1,2 @@
|
||||
6 0.502321 0.297364 0.098929 0.375977
|
||||
3 0.562143 0.061524 0.035000 0.123047
|
||||
0
dataset_split/test/labels/103300014.txt
Normal file
0
dataset_split/test/labels/103300014.txt
Normal file
0
dataset_split/test/labels/103300026.txt
Normal file
0
dataset_split/test/labels/103300026.txt
Normal file
8
dataset_split/test/labels/103300033.txt
Normal file
8
dataset_split/test/labels/103300033.txt
Normal 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
|
||||
0
dataset_split/test/labels/103300034.txt
Normal file
0
dataset_split/test/labels/103300034.txt
Normal file
3
dataset_split/test/labels/103400046.txt
Normal file
3
dataset_split/test/labels/103400046.txt
Normal 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
|
||||
0
dataset_split/test/labels/103400049.txt
Normal file
0
dataset_split/test/labels/103400049.txt
Normal file
1
dataset_split/test/labels/103400052.txt
Normal file
1
dataset_split/test/labels/103400052.txt
Normal file
@ -0,0 +1 @@
|
||||
4 0.175000 0.082519 0.107858 0.165039
|
||||
4
dataset_split/test/labels/103400054.txt
Normal file
4
dataset_split/test/labels/103400054.txt
Normal 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
|
||||
4
dataset_split/test/labels/103400075.txt
Normal file
4
dataset_split/test/labels/103400075.txt
Normal 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
|
||||
1
dataset_split/test/labels/103400076.txt
Normal file
1
dataset_split/test/labels/103400076.txt
Normal file
@ -0,0 +1 @@
|
||||
6 0.465000 0.391601 0.061428 0.783203
|
||||
2
dataset_split/test/labels/103400084.txt
Normal file
2
dataset_split/test/labels/103400084.txt
Normal file
@ -0,0 +1,2 @@
|
||||
4 0.682321 0.874511 0.027500 0.250977
|
||||
1 0.583214 0.771972 0.029286 0.057617
|
||||
2
dataset_split/test/labels/103500003.txt
Normal file
2
dataset_split/test/labels/103500003.txt
Normal file
@ -0,0 +1,2 @@
|
||||
3 0.570893 0.500000 0.023214 1.000000
|
||||
3 0.481965 0.500000 0.035357 1.000000
|
||||
4
dataset_split/test/labels/103500044.txt
Normal file
4
dataset_split/test/labels/103500044.txt
Normal 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
|
||||
1
dataset_split/test/labels/103500049.txt
Normal file
1
dataset_split/test/labels/103500049.txt
Normal file
@ -0,0 +1 @@
|
||||
0 0.713036 0.125489 0.103214 0.112305
|
||||
3
dataset_split/test/labels/103500065.txt
Normal file
3
dataset_split/test/labels/103500065.txt
Normal 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
|
||||
1
dataset_split/test/labels/103700027.txt
Normal file
1
dataset_split/test/labels/103700027.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.920893 0.944824 0.058214 0.110352
|
||||
4
dataset_split/test/labels/103700033.txt
Normal file
4
dataset_split/test/labels/103700033.txt
Normal 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
|
||||
2
dataset_split/test/labels/103700044.txt
Normal file
2
dataset_split/test/labels/103700044.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.739822 0.935059 0.021071 0.059571
|
||||
0 0.728392 0.897949 0.000357 0.000976
|
||||
4
dataset_split/test/labels/103900000.txt
Normal file
4
dataset_split/test/labels/103900000.txt
Normal 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
|
||||
2
dataset_split/test/labels/103900003.txt
Normal file
2
dataset_split/test/labels/103900003.txt
Normal file
@ -0,0 +1,2 @@
|
||||
0 0.522321 0.174316 0.001785 0.004883
|
||||
0 0.542500 0.194824 0.032858 0.055664
|
||||
1
dataset_split/test/labels/103900006.txt
Normal file
1
dataset_split/test/labels/103900006.txt
Normal file
@ -0,0 +1 @@
|
||||
0 0.225357 0.400391 0.143572 0.076172
|
||||
2
dataset_split/test/labels/103900044.txt
Normal file
2
dataset_split/test/labels/103900044.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.721250 0.740234 0.035358 0.062500
|
||||
1 0.390714 0.244629 0.019286 0.051758
|
||||
0
dataset_split/test/labels/103900060.txt
Normal file
0
dataset_split/test/labels/103900060.txt
Normal file
2
dataset_split/test/labels/104500080.txt
Normal file
2
dataset_split/test/labels/104500080.txt
Normal file
@ -0,0 +1,2 @@
|
||||
1 0.098393 0.546875 0.088928 0.103516
|
||||
0 0.603393 0.514648 0.088928 0.136719
|
||||
1
dataset_split/test/labels/104600009.txt
Normal file
1
dataset_split/test/labels/104600009.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.367321 0.337891 0.023929 0.048828
|
||||
1
dataset_split/test/labels/104600013.txt
Normal file
1
dataset_split/test/labels/104600013.txt
Normal file
@ -0,0 +1 @@
|
||||
0 0.527321 0.319825 0.028929 0.057617
|
||||
6
dataset_split/test/labels/104600023.txt
Normal file
6
dataset_split/test/labels/104600023.txt
Normal 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
|
||||
1
dataset_split/test/labels/104600050.txt
Normal file
1
dataset_split/test/labels/104600050.txt
Normal file
@ -0,0 +1 @@
|
||||
1 0.618392 0.967774 0.069643 0.064453
|
||||
0
dataset_split/test/labels/104600062.txt
Normal file
0
dataset_split/test/labels/104600062.txt
Normal file
0
dataset_split/test/labels/104600081.txt
Normal file
0
dataset_split/test/labels/104600081.txt
Normal file
1
dataset_split/test/labels/104700023.txt
Normal file
1
dataset_split/test/labels/104700023.txt
Normal file
@ -0,0 +1 @@
|
||||
5 0.474822 0.500000 0.041785 1.000000
|
||||
1
dataset_split/test/labels/104700026.txt
Normal file
1
dataset_split/test/labels/104700026.txt
Normal 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
Reference in New Issue
Block a user