Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions .github/workflows/ci-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ permissions:

env:
QT_VERSION: '6.8.2'
ORT_VERSION: '1.24.1'

jobs:
build:
Expand All @@ -38,10 +39,16 @@ jobs:
- os: windows-latest
qt_arch: win64_msvc2022_64
artifact_name: YoloLabel-Windows-x64
ort_package: onnxruntime-win-x64
ort_ext: zip
- os: ubuntu-22.04
artifact_name: YoloLabel-Linux-x64
ort_package: onnxruntime-linux-x64
ort_ext: tgz
- os: macos-latest
artifact_name: YoloLabel-macOS
ort_package: onnxruntime-osx-arm64
ort_ext: tgz

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -79,33 +86,81 @@ jobs:
libxcb-render-util0 \
libxcb-shape0

# ── Download ONNX Runtime ─────────────────────────────────
- name: Download ONNX Runtime (Unix)
if: runner.os != 'Windows'
run: |
curl -L -o ort.${{ matrix.ort_ext }} \
"https://github.com/microsoft/onnxruntime/releases/download/v${{ env.ORT_VERSION }}/${{ matrix.ort_package }}-${{ env.ORT_VERSION }}.${{ matrix.ort_ext }}"
tar xzf ort.${{ matrix.ort_ext }}
mv ${{ matrix.ort_package }}-${{ env.ORT_VERSION }} onnxruntime

- name: Download ONNX Runtime (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$pkg = "${{ matrix.ort_package }}-${{ env.ORT_VERSION }}"
Invoke-WebRequest -Uri "https://github.com/microsoft/onnxruntime/releases/download/v${{ env.ORT_VERSION }}/${pkg}.zip" -OutFile ort.zip
Expand-Archive -Path ort.zip -DestinationPath .
Rename-Item -Path $pkg -NewName onnxruntime

# ── Build ─────────────────────────────────────────────────
- name: Build (Unix)
if: runner.os != 'Windows'
run: |
qmake YoloLabel.pro CONFIG+=release
qmake YoloLabel.pro CONFIG+=release "ONNXRUNTIME_DIR=$PWD/onnxruntime"
make -j$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)

- name: Build (Windows)
if: runner.os == 'Windows'
shell: cmd
run: |
qmake YoloLabel.pro CONFIG+=release
qmake YoloLabel.pro CONFIG+=release "ONNXRUNTIME_DIR=%CD%\onnxruntime"
nmake

# ── Accuracy Test (Linux only) ────────────────────────────
- name: Setup Python for accuracy test
if: runner.os == 'Linux'
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Ultralytics
if: runner.os == 'Linux'
run: pip install ultralytics

- name: Build test binary
if: runner.os == 'Linux'
run: |
cd tests
qmake test_inference.pro "ONNXRUNTIME_DIR=$PWD/../onnxruntime"
make -j$(nproc)

- name: Run accuracy test
if: runner.os == 'Linux'
run: |
python tests/run_accuracy_test.py \
--test-binary tests/test_inference \
--image Samples/images/raccoon_1.jpg \
--conf 0.25

# ── Package: Windows ──────────────────────────────────────
- name: Package (Windows)
if: runner.os == 'Windows'
shell: cmd
run: |
mkdir deploy
copy release\YoloLabel.exe deploy\
copy onnxruntime\lib\onnxruntime.dll deploy\
windeployqt deploy\YoloLabel.exe --release --no-translations

# ── Package: macOS ────────────────────────────────────────
- name: Package (macOS)
if: runner.os == 'macOS'
run: |
mkdir -p YoloLabel.app/Contents/Frameworks
cp onnxruntime/lib/libonnxruntime.*.dylib YoloLabel.app/Contents/Frameworks/
install_name_tool -add_rpath @executable_path/../Frameworks YoloLabel.app/Contents/MacOS/YoloLabel || true
macdeployqt YoloLabel.app
codesign --force --deep --sign - YoloLabel.app
hdiutil create -volname YoloLabel -srcfolder YoloLabel.app -ov -format UDZO "${{ matrix.artifact_name }}.dmg"
Expand All @@ -116,9 +171,11 @@ jobs:
run: |
# Prepare AppDir
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/lib
mkdir -p AppDir/usr/share/applications
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
cp YoloLabel AppDir/usr/bin/
cp onnxruntime/lib/libonnxruntime.so* AppDir/usr/lib/

# Create desktop entry
cat > AppDir/usr/share/applications/yololabel.desktop << 'EOF'
Expand Down Expand Up @@ -175,6 +232,8 @@ jobs:
cp -L "${QT_LIB_DIR}/${lib}.so"* package/YoloLabel/lib/ 2>/dev/null || true
done

cp onnxruntime/lib/libonnxruntime.so* package/YoloLabel/lib/

cp -L "${QT_PLUGIN_DIR}/platforms/libqxcb.so" package/YoloLabel/plugins/platforms/ 2>/dev/null || true

cat > package/YoloLabel/YoloLabel.sh << 'LAUNCHER'
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@ CONTRIBUTING.md
.DS_Store

# Qt MOC predefines
moc_predefs.h
moc_predefs.h

# ONNX Runtime (downloaded pre-built binaries)
onnxruntime/
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,58 @@ Key design: uses a **two-click method** (not drag-and-drop) to define bounding b

- **MainWindow** (`mainwindow.cpp/h`): Application logic, UI controls, file I/O, keyboard shortcuts
- **label_img** (`label_img.cpp/h`): Custom QLabel widget for image display, mouse-based bounding box drawing, contrast adjustment
- **YoloDetector** (`yolo_detector.cpp/h`): ONNX Runtime-based YOLO inference engine for pseudo labeling (see below)
- **Build**: qmake (`YoloLabel.pro`), CI via GitHub Actions (`.github/workflows/ci-release.yml`)
- **Output format**: YOLO annotation `.txt` files with normalized bounding box coordinates

## Pseudo Labeling (Auto-Label)

Local YOLO object detection that auto-generates bounding boxes from an Ultralytics `.onnx` model. Loading a single `.onnx` file is sufficient — class names, input size, and model configuration are all read from the ONNX metadata embedded by Ultralytics.

### Design Principle

This feature targets full compatibility with the [Ultralytics](https://github.com/ultralytics/ultralytics) project's ONNX export. When modifying or extending the pseudo labeling feature:

1. **Always check the upstream Ultralytics source** for the latest supported models, ONNX export format, and metadata schema. Key references:
- Model export: `ultralytics/engine/exporter.py` — how metadata is embedded in ONNX
- ONNX predict: `ultralytics/nn/autobackend.py` — how metadata is read back and used for inference
- Postprocessing: `ultralytics/utils/ops.py` (`non_max_suppression`) and `ultralytics/models/yolo/detect/predict.py`
- Supported models list: https://docs.ultralytics.com/models/
2. **Ultralytics may add new model versions, change ONNX metadata keys, or alter output tensor layouts.** Before making changes, search the Ultralytics repo and docs for the current state rather than relying solely on what is documented here.
3. **The goal is: a user exports any Ultralytics object detection model to `.onnx` and loads it in YOLO-Label — it just works.** No separate class names file, no manual configuration.

### ONNX Metadata

Ultralytics embeds metadata as string key-value pairs in ONNX `metadata_props` during export. The detector reads these via `Ort::Session::GetModelMetadata()` to configure itself automatically. Key fields (check `ultralytics/engine/exporter.py` for the latest schema):

| Key | Example | Usage |
|---|---|---|
| `names` | `"{0: 'person', 1: 'car', ...}"` | Auto-populate class list (Python dict format, parsed in C++) |
| `task` | `"detect"` | Validate model type; reject non-detection models |
| `stride` | `"32"` | Model stride |
| `imgsz` | `"[640, 640]"` | Input resolution for dynamic-shape models |
| `end2end` | `"True"` | Skip NMS when model has NMS baked in |
| `description` | `"Ultralytics YOLOv8n model"` | Detect specific YOLO version |

### Inference Pipeline (`yolo_detector.cpp`)

The C++ inference code references the Ultralytics Python implementation but maximizes use of ONNX metadata to make a single `.onnx` file self-sufficient for detection:

1. **Load**: `Ort::Session` loads the model. Metadata is read via `GetModelMetadata()` → `GetCustomMetadataMapKeysAllocated()` / `LookupCustomMetadataMapAllocated()`.
2. **Version detection**: First checks metadata `description` for specific version, then falls back to output shape heuristic (`dim1 > dim2` → V5-style, else V8-style). End-to-end is a boolean flag (`endToEnd`), not a version. The `YoloVersion` enum and `detectVersionFromMetadata()` may need new entries when Ultralytics releases new model architectures.
3. **Preprocess**: Letterbox resize via `QImage::scaled()`, gray padding (114/255), HWC→CHW, normalize to [0,1].
4. **Postprocess**: Three paths depending on model output tensor layout:
- **V5-style**: `[B, N, C+5]` — has objectness score, row-major layout
- **V8-style**: `[B, C+4, N]` — no objectness, transposed (column-major per detection). Used by V8, V11, V12, V26 and likely future versions that share this layout.
- **End-to-end**: `[1, maxDet, 6]` — `[x1, y1, x2, y2, score, class_id]`, NMS already applied
- **When adding support for a new model**, check whether its ONNX output layout matches an existing path or requires a new `postprocess*()` function.
5. **NMS**: Pure C++ greedy NMS (class-aware, sorted by confidence). Skipped for end-to-end models.
6. **Output**: `DetectionResult` with normalized [0,1] coordinates matching `ObjectLabelingBox.box` format.

### Conditional Build

ONNX Runtime is optional. When `ONNXRUNTIME_DIR` points to a valid installation, `YoloLabel.pro` defines `ONNXRUNTIME_AVAILABLE` and compiles the detector. Without it, the app builds normally without the auto-label UI. See `scripts/download_onnxruntime.sh` for downloading pre-built ONNX Runtime binaries.

# Project Guidelines

## Git Configuration
Expand Down Expand Up @@ -47,5 +96,11 @@ Key design: uses a **two-click method** (not drag-and-drop) to define bounding b
- When upgrading libraries or frameworks (e.g., Qt 5 → Qt 6), **existing functionality must remain identical**. Only replace deprecated APIs with their direct equivalents — do not add, remove, or alter any user-facing behavior.
- **Visual rendering must also remain identical.** Framework upgrades can change how widgets are styled or drawn (e.g., checkbox indicators, selection highlights, frame styles, gradient support). After any upgrade, review all UI elements — especially those with custom stylesheets — and verify they render the same as before. Fix any visual regressions with explicit styles.

## Testing the App on macOS
- When launching the app via `open YoloLabel.app --args ...`, macOS sets the working directory to `/`, **not** the current shell directory. This means **relative paths will not work**. Always use absolute paths for all arguments (dataset dir, class file, ONNX model).
- Wrong: `open YoloLabel.app --args Samples/images Samples/coco_names.txt yolov8n.onnx`
- Correct: `open YoloLabel.app --args /full/path/to/Samples/images /full/path/to/coco_names.txt /full/path/to/yolov8n.onnx`
- Alternatively, run the binary directly to use relative paths: `./YoloLabel.app/Contents/MacOS/YoloLabel Samples/images ...`

## Language
- All commit messages, PR descriptions, code comments, and documentation must be written in **English only**
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,15 @@ To minimize wrist strain when labeling, I adopted the method **"twice left butto

## USAGE AND OPTIONS
```
./YoloLabel [dataset dir] [class file names]
# Example
./YoloLabel [dataset dir] [class file] [model.onnx]
# Examples
./YoloLabel ../project/dataset/objects/frames ../project/dataset/objects/obj_names.txt

./YoloLabel ../project/dataset/objects/frames ../project/dataset/objects/obj_names.txt yolov8n.onnx
./YoloLabel ../project/dataset/objects/frames yolov8n.onnx
```

Arguments are detected by file extension — `.onnx` files are loaded as YOLO models, all other files are loaded as class name lists. When a model with embedded class names is loaded without a class file, class names are populated from the model automatically.


## SHORTCUTS

Expand Down
11 changes: 11 additions & 0 deletions YoloLabel.pro
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ HEADERS += \
FORMS += \
mainwindow.ui

# --- ONNX Runtime integration (optional) ---
isEmpty(ONNXRUNTIME_DIR): ONNXRUNTIME_DIR = $$PWD/onnxruntime
exists($$ONNXRUNTIME_DIR/include) {
DEFINES += ONNXRUNTIME_AVAILABLE
INCLUDEPATH += $$ONNXRUNTIME_DIR/include
LIBS += -L$$ONNXRUNTIME_DIR/lib -lonnxruntime
unix: QMAKE_RPATHDIR += $$ONNXRUNTIME_DIR/lib
SOURCES += yolo_detector.cpp
HEADERS += yolo_detector.h
}

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
Expand Down
5 changes: 5 additions & 0 deletions label_img.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,11 @@ QImage label_img::crop(QRect rect)
return m_inputImg.copy(rect);
}

QImage label_img::getInputImage() const
{
return m_inputImg;
}

void label_img::drawCrossLine(QPainter& painter, QColor color, int thickWidth)
{
if(m_relative_mouse_pos_in_ui == QPointF(0., 0.)) return;
Expand Down
1 change: 1 addition & 0 deletions label_img.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class label_img : public QLabel
void moveBox(int boxIdx, double dx, double dy);
int findBoxUnderCursor(QPointF point) const;
QImage crop(QRect);
QImage getInputImage() const;

void zoomIn(QPoint widgetPos);
void zoomOut(QPoint widgetPos);
Expand Down
Loading