Back to projects

Collaboration · Supporting a final-year team of four

YOLO Object Detection (YOLO26)

  • Co-authored
  • AI

Supporting a four-person HKMU final-year team on how well vision models find objects underwater, in adverse weather, at small scale and under camouflage: the shared code, built with Claude Code, and the real-data baselines of all four plans.

YOLO26nmAP50 0.584fish 0.878crab 0.000
Poster drawn from the repository

By the numbers

0.584mAP50, underwater baseline at 20 epochs
  • mAP50 on DAWN adverse weather, seed 00.635
  • S-measure on COD10K camouflage, seed 00.853
  • mAP50 on VisDrone small objects, seed 00.291
  • Brackish images re-split by video clip14,674
  • test functions written on main84
§ 01

Overview

A four-person HKMU final-year team is studying how well computer-vision models find objects underwater, in adverse weather, at small scale and under camouflage, in one shared repository with a four-tab demo app. My part, built with Claude Code, was the shared training, evaluation and results code and the first real-data baseline: YOLO26n on Brackish, 20 epochs, one seed, validation mAP50 0.584 and mAP50-95 0.345. Crab AP was 0.000 in that same run, and the cause is still unknown. I then ran the Phase 1 baselines of all four plans through a remote session on a teammate’s faster PC (PR #4, still open), seed 0 only: mAP50 0.635 on DAWN, 0.291 on VisDrone and S-measure 0.853 on COD10K. These are baselines: the team’s attention, distillation and edge methods have not yet been trained on real data.

§ 02

In motion

TRAINVALTESTUNDERWATERWEATHERSMALLCAMOUFLAGE
Each video clip goes into exactly one of train, validation and test, so no clip crosses; then the detector is tested underwater, in bad weather, on small objects and against camouflage.
§ 03

My contributions

  • Shared framework (PR #1, with the run plots added in PR #2), written with Claude Code: a dataset manifest, one train, evaluate and log loop, a results CSV and run plots. Python on main, starter files included, totals 5,404 lines with 84 test functions, counted from the source rather than from a test run.
  • Drafted code for all four plans (PR #1): per-plan training scripts, custom attention and fusion layers, and a four-tab Gradio demo app. The real-data runs that followed are baselines: the custom layers have not been trained on real data, and the demo app was only checked to start and serve its four tabs.
  • Underwater baseline (PR #2): YOLO26n on Brackish, 20 epochs, one seed, validation mAP50 0.584 and mAP50-95 0.345. Crab AP was 0.000 in the same run. The best checkpoint was picked on validation, so both figures are slightly optimistic.
  • Phase 1 baselines of all four plans (PR #4, still open), through a remote session on a teammate’s RTX 5060 Ti PC: a scheduled runner trained five models overnight in about 3 h 40 min. Seed 0 only: Brackish mAP50 0.597 at 50 epochs, DAWN fold 0 student 0.635, VisDrone 0.291 with AP small 0.083, and S-measure 0.853 on COD10K and 0.854 on CAMO. The 50-epoch underwater run is newer but not clearly better: mAP50-95 went from 0.345 to 0.344 and precision from 0.754 to 0.580.
  • Dataset corrections (PR #1), with Claude Code: re-split Brackish by video clip and renumbered DAWN’s classes by name, checked against the PASCAL VOC names of its 1,027 images.
  • Project governance: plans, phase gates, stewards for shared files and the team’s workflow documents.
  • A training driver for all four plans and a guard against reusing a run of another epoch count (PR #3). That pull request was closed without merging, so none of this work reached main.
§ 04

How it works

  1. Dataset manifest

    One JSON file records each dataset’s source, licence notes and download instructions.

  2. Preparation

    Scripts prepare and check Brackish and DAWN, then write fixed split lists, with Brackish split by video clip.

  3. Model definition

    Ultralytics stays pinned at 8.4.175; generated YAML files and a layer map add custom layers without editing the library.

  4. Shared training loop

    Plans A, B and C train through one loop at 640 px with nms=False; Plan D keeps its own plain PyTorch loop.

  5. Results log

    Each run adds one CSV row with its metrics, head, GPU, commit, library versions and timestamp.

  6. Plots

    A Matplotlib figure of each run shows losses, mAP and per-class scores; selected plots are committed beside the results.

§ 05

Exhibits

Bar chart of mAP50 and mAP50-95 per class: crab 0.000 and 0.000, fish 0.878 and 0.552, jellyfish 0.812 and 0.512, shrimp 0.080 and 0.056, small_fish 0.740 and 0.336, starfish 0.995 and 0.615.
Validation scores per class from the 20-epoch run with one seed. Crab is at 0.000 and shrimp at 0.080, while the headline mAP50 of 0.584 averages all six.
Normalised confusion matrix for six classes plus background. The crab column reads 1.00 at background; the correct-class cells read starfish 1.00, small_fish 0.90, fish 0.86, jellyfish 0.53 and shrimp 0.08.
Normalised confusion matrix from the same run. True crabs go to background at 1.00, and 0.54 of true shrimp are lost the same way.
Line chart of validation mAP50 and mAP50-95 over 20 epochs. mAP50 stays between about 0.56 and 0.59 from about epoch 7, and mAP50-95 is marked best at 0.345 on epoch 16.
Validation mAP across the run. Epoch 16 is the best checkpoint at mAP50-95 0.345, and more epochs alone did little after about epoch 7.
§ 06

Engineering notes

Dispatch № 1

The split that leaked whole clips

The problem
The Roboflow copy of Brackish spread all 89 source clips across train, validation and test. Frames from one clip sat on both sides of the split, which flatters validation scores.
The decision
Keep every clip whole in one split. Try 500 clip orders and pick the one whose validation and test sets best match the class mix, without using model results.
The result
The 14,674 images became 11,612 train, 1,575 validation and 1,487 test, with no clip in two splits. Frames from different clips of the same fixed camera can still look alike.

Dispatch № 2

Measuring accuracy on the head that gets timed

The problem
The plans took YOLO26 inference to be NMS-free by default. Reading Ultralytics 8.4.175 showed that validation uses the one-to-many head with external NMS unless told otherwise, while the speed protocol times the one-to-one head.
The decision
Pass nms=False through the shared train, validation and predict code, and write the head used into the results rows.
The result
Both rows on main record head=one2one(nms=False). Speed has not been measured, so only the accuracy half of the pairing exists so far.

Dispatch № 3

The class the model never found

The problem
In the 20-epoch run crab scored AP 0.000, and the normalised confusion matrix sends true crabs to background at 1.00. Scarcity does not explain it: crab has the most training boxes, 9,706.
The decision
Report the failure in PR #2 beside the headline figure, and advise inspecting crab images and labels before comparing any attention variants.
The result
The cause has not been established. The mAP50 of 0.584 is an average over six classes, so it also hides shrimp at 0.080.
§ 07

From the source

Keeping every clip wholeunderwater/prepareBrackish.py
def assignClipsToSplits(imageCountByClip, seed):
    clipNames = sorted(imageCountByClip)
    random.Random(seed).shuffle(clipNames)
    totalImages = sum(imageCountByClip.values())
    assignment, filledImages = {}, {"val": 0, "test": 0}
    for clipName in clipNames:
        openSplit = next((splitName for splitName in ("val", "test") if filledImages[splitName] < holdOutFraction * totalImages), None)
        assignment[clipName] = openSplit or "train"
        if openSplit: filledImages[openSplit] += imageCountByClip[clipName]
    return assignment

Validation and test fill up from a shuffled clip order and everything left is training data, so a clip is only ever assigned as a whole.

One head for accuracy and speedcommon/training.py
# Which YOLO26 head produces the reported accuracy. In Ultralytics 8.4.175, val, predict and the validation inside training use the
# one-to-many head with external NMS unless nms=False is passed (the default nms=None means external NMS). The speed protocol times
# the NMS-free one-to-one head, so accuracy is measured on that same head: every train, val and predict call passes these arguments.
# Models without a one-to-one head (YOLO11, YOLOv8 reference rows) ignore the setting and keep using NMS.
# This is a protocol decision for the whole team: change it only by agreement and record it in plans/decisions.md.
endToEndArguments = {"nms": False}
# Text added to the notes of every results row so the head used for a number is never ambiguous
endToEndNote = "head=one2one(nms=False)"

The setting lives in one place, and its note is written into the results rows, so a logged number says which head produced it.

§ 08

Development timeline

No recorded video for this project.

  1. 23 Oct 2026

    Phase 1 baselines of all four plans recorded (PR #4, open)

    Milestone
  2. 22 Oct 2026

    Underwater baseline merged into main (PR #2)

    Milestone
  3. 22 Oct 2026

    All-plans training driver drafted; PR #3 closed unmerged

    Milestone
  4. 18 Oct 2026

    20-epoch underwater baseline: mAP50 0.584, one seed

    Milestone
  5. 17 Oct 2026

    Results plots and first real training epoch

    Milestone
  6. 17 Oct 2026

    Shared code merged into main (PR #1)

    Milestone
  7. 17 Oct 2026

    Brackish re-split by video clip

    Milestone
  8. 11 Oct 2026

    Four-plan code drafted; PR #1 opened

    Milestone
  9. 11 Oct 2026

    Project governance documents added

    Milestone