Overview
Maritime monitoring has significant real-world applications, from tracking illegal fishing and smuggling to port logistics and naval operations. Historically this kind of surveillance required significant human effort to review satellite imagery manually. Modern object detection models offer a way to automate this process at scale. This project involved training a YOLOv8 model to detect ships in satellite imagery, using the Airbus Ship Detection dataset which is a competition dataset containing tens of thousands of labelled overhead images of the world's oceans, ports, and coastlines.
Build Process
The Airbus dataset presented an immediate preprocessing challenge. Annotations were stored as run-length encoded (RLE) pixel masks, a compact format that needed to be converted into bounding boxes before YOLO could use them. Using some of the code from the competition's public kernels as a starting point, I added a conversion script that decoded each mask, computed the bounding box extents, and normalised the coordinates relative to the image dimensions. This produced the plain text label files that Ultralytics YOLO expects.
Rather than training on the full 32GB dataset, I sampled 10,000 images - 5,000 containing ships and 5,000 empty ocean scenes, to keep training time manageable while maintaining a balanced dataset. The images were split 80/20 into training and validation sets.
I used YOLOv8n, the nano variant of Ultralytics' YOLOv8 architecture, initialised with pretrained COCO weights rather than training from scratch. Transfer learning meant the model already had a strong understanding of edges, shapes, and spatial features before seeing a single satellite image, which significantly accelerated convergence.
Training ran for 20 epochs on my old desktop GPU. After each epoch, Ultralytics logged precision, recall, and mAP scores against the held-out validation set, making it straightforward to monitor for overfitting and assess when the model had converged.
Results
The final model achieved a precision of 0.756, a recall of 0.582, and a mAP@50 of 0.655 on the validation set. In practical terms, when the model identified something as a ship it was correct roughly three quarters of the time, and it successfully located just over half of all ships present in the validation images. The gap between precision and recall suggests the model errs on the side of caution by only flagging detections it is fairly confident about, at the cost of missing some smaller or partially obscured vessels. This is a reasonable tradeoff for a lightweight model trained on a subset of the available data.
What I Learned
This project was an introduction to the full object detection pipeline, from raw data and annotation conversion through to training, evaluation, and inference. Working with a real competition dataset meant dealing with practical challenges like annotation format conversion, class imbalance between ship and empty-ocean images, and managing dataset scale on consumer hardware. Having a full data set made this dramatically easier than my attempts to do further my Judo project for throw detection which after 20 hours of competition footage watched on 2x speed I had only obtained 26 of the throws I wanted to detect.
Part 2: From Boxes to Masks
Bounding boxes tell you a ship is somewhere in a region of pixels, but they don't tell you its shape, heading, or true extent. A box drawn around a vessel moored diagonally across a harbour wastes most of its area on open water, and two overlapping boxes give no sense of which pixels belong to which hull. The natural next step was instance segmentation: training a model to output a pixel-level mask for every ship rather than just a rectangle around it.
The Airbus dataset was actually built for this from the start. Its RLE-encoded annotations are pixel masks, and Part 1 had thrown away most of that precision by collapsing each mask down to a bounding box. Going back to the raw masks meant I could reuse the same underlying data and most of the same preprocessing pipeline, just without the lossy box-conversion step.
Build Process
I trained YOLOv8n-seg, the segmentation variant of the same nano architecture used in Part 1, again starting from pretrained COCO weights rather than from scratch. The dataset, split, and training setup were kept as close to Part 1 as possible so the results would be a fair comparison, with the RLE masks decoded directly into YOLO's polygon label format instead of being reduced to boxes.
Training ran on the same GTX 970, a card with only 4GB of VRAM. Segmentation models are noticeably more memory-hungry than detection-only models, since they carry an extra mask prediction head and the loss has to be computed per-pixel rather than per-box. In practice this meant a smaller batch size and lower input resolution than I'd have liked, just to keep training inside the available memory without the process falling over.
Results
The final model reached a mask mAP@50 of 0.696, ahead of the 0.655 box mAP@50 from Part 1 despite the tighter memory budget it trained under. The masks tracked hull outlines well, including on ships moored at odd angles where a bounding box would have been mostly empty water.
What I'd Still Change
In an attempt to improve the performance I increase the image resolution and increased the batch size, in an attempt to capture more of the small ships and average gradient direction over more images to get a more stable result. This caused issues on my older graphics card which has a lower available VRAM of 4GB, so these changes were reverted. So the model here is the best fit I could get inside 4GB, not the best fit the dataset supports.
If I had a GPU with more headroom, the refinement step I'd have run next is straightforward: step up to YOLOv8s-seg or m-seg at full resolution, extend training past 20 epochs now that overfitting could be monitored properly at a larger batch size, and re-check the precision/recall balance to see whether the smaller model's caution was a genuine property of the data or just a symptom of the constrained training regime.