YOLO Fish Detection and Counting Using Python: Build a Real-Time Fish Monitoring System

Fish detection and counting is an interesting application of computer vision that can be used to automatically identify and count fish in images and videos.
Traditional fish counting often requires people to manually inspect underwater footage or count fish in tanks, ponds, rivers, and aquaculture farms. This can be time-consuming, especially when there are hundreds or thousands of fish.
With YOLO object detection and object tracking, developers can build an automated system that detects fish, assigns tracking IDs, and counts them as they move through a camera view.
In this tutorial, we will build a Fish Detection and Counting System using Python, YOLO, OpenCV, and object tracking.
We will cover:
Fish detection from images
Fish detection from videos
Real-time webcam detection
Fish counting
Object tracking
Line-crossing counting
Custom YOLO training
Fish monitoring applications
Model evaluation and accuracy improvements
What Is Fish Detection?
Fish detection is the process of using computer vision to identify fish in images or video frames.
For example, an underwater camera may capture an image containing several fish:
Fish 1
Fish 2
Fish 3
Fish 4
Fish 5
A YOLO model can identify each fish and draw a bounding box around it.
The detection result may look like:
Fish → 0.94 confidence
Fish → 0.91 confidence
Fish → 0.89 confidence
Fish → 0.87 confidence
The confidence score represents how confident the model is about the prediction.
What Is Fish Counting?
Fish counting goes one step further.
Instead of simply detecting fish, the application determines how many fish are present or how many fish pass through a specific area.
For example:
Current Fish: 24
Or:
Fish Entered: 120
Fish Exited: 85
Fish counting can be useful in:
Aquaculture
Fish farms
Aquarium monitoring
Marine research
Fisheries research
Underwater surveys
Wildlife monitoring
Why Use YOLO?
YOLO is designed for real-time object detection.
For every detected fish, it can provide:
Class name
Bounding box
Confidence score
For example:
Class: fish
Confidence: 0.93
Bounding Box:
x1 = 120
y1 = 80
x2 = 280
y2 = 250
YOLO can process individual images as well as continuous video frames.
However, detection alone is not enough for reliable video counting.
Why Tracking Is Important
Imagine one fish swimming across a camera.
The camera captures:
Frame 1 → Fish detected
Frame 2 → Fish detected
Frame 3 → Fish detected
Frame 4 → Fish detected
Frame 5 → Fish detected
If we count every detection:
Fish Count = 5
But there is actually only one fish.
Tracking solves this problem by assigning an ID:
Frame 1 → Fish ID 1
Frame 2 → Fish ID 1
Frame 3 → Fish ID 1
Frame 4 → Fish ID 1
Frame 5 → Fish ID 1
The application understands that these detections belong to the same fish.
This is why YOLO + tracking is a better approach for video-based fish counting.
Technologies Used
We will use:
Python
YOLO
Ultralytics
OpenCV
Object Tracking
Install the required packages:
pip install ultralytics opencv-python
Create a requirements.txt file:
ultralytics
opencv-python
Install them with:
pip install -r requirements.txt
Project Structure
Create a project:
yolo-fish-detection/
│
├── images/
│ └── fish.jpg
│
├── videos/
│ └── fish.mp4
│
├── detect_image.py
├── detect_video.py
├── webcam.py
├── fish_count.py
├── train.py
└── requirements.txt
1. Load the YOLO Model
Create:
detect_image.py
Then:
from ultralytics import YOLO
model = YOLO("best.pt")
print("Fish detection model loaded successfully")
Here:
best.pt
should be your fish detection model.
If you have a pretrained model that already contains a suitable fish class, it can be used for initial experiments.
For reliable fish detection in a specific environment, a custom-trained model is generally recommended.
2. Detect Fish in an Image
Let's start with a simple image detection program.
from ultralytics import YOLO
model = YOLO("best.pt")
results = model("images/fish.jpg")
for result in results:
result.show()
Run:
python detect_image.py
The model will analyze the image and display bounding boxes around detected fish.
3. Get Fish Detection Information
We can access the prediction results directly.
from ultralytics import YOLO
model = YOLO("best.pt")
results = model("images/fish.jpg")
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
confidence = float(box.conf[0])
class_name = result.names[class_id]
print(
f"Class: {class_name} | "
f"Confidence: {confidence:.2f}"
)
Example output:
Class: fish | Confidence: 0.95
Class: fish | Confidence: 0.92
Class: fish | Confidence: 0.89
4. Count Fish in an Image
If we only want to count fish in a single image, we can simply count the detections.
from ultralytics import YOLO
model = YOLO("best.pt")
results = model("images/fish.jpg")
fish_count = 0
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
class_name = result.names[class_id]
if class_name == "fish":
fish_count += 1
print("Total Fish:", fish_count)
Example:
Total Fish: 12
This works well for individual images.
5. Add a Confidence Threshold
We can ignore detections that have low confidence.
from ultralytics import YOLO
model = YOLO("best.pt")
results = model(
"images/fish.jpg",
conf=0.50
)
fish_count = 0
for result in results:
for box in result.boxes:
confidence = float(box.conf[0])
if confidence < 0.50:
continue
class_id = int(box.cls[0])
class_name = result.names[class_id]
if class_name == "fish":
fish_count += 1
print("Fish:", fish_count)
You can experiment with:
0.30
0.40
0.50
0.60
0.70
The correct threshold depends on the model and application.
6. Save the Detection Result
We can save an annotated image:
from ultralytics import YOLO
model = YOLO("best.pt")
results = model("images/fish.jpg")
for result in results:
result.save(
filename="detected_fish.jpg"
)
print("Detection result saved")
The output image will contain bounding boxes around detected fish.
7. Real-Time Fish Detection With Webcam
We can use OpenCV to process a live camera stream.
Create:
webcam.py
Then:
import cv2
from ultralytics import YOLO
model = YOLO("best.pt")
camera = cv2.VideoCapture(0)
while True:
success, frame = camera.read()
if not success:
break
results = model(frame)
annotated_frame = results[0].plot()
cv2.imshow(
"Fish Detection",
annotated_frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
Run:
python webcam.py
The camera will continuously send frames to the YOLO model.
8. Display the Current Fish Count
We can count the fish detected in each frame.
import cv2
from ultralytics import YOLO
model = YOLO("best.pt")
camera = cv2.VideoCapture(0)
while True:
success, frame = camera.read()
if not success:
break
results = model(frame)
fish_count = 0
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
class_name = result.names[class_id]
if class_name == "fish":
fish_count += 1
cv2.putText(
frame,
f"Fish: {fish_count}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2
)
cv2.imshow(
"Fish Counting",
frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
The screen could display:
Fish: 17
This represents the number of fish detected in the current frame.
9. Why Frame-by-Frame Counting Is Not Enough
Suppose one fish remains in front of the camera for 100 frames.
The model might detect:
Frame 1 → Fish
Frame 2 → Fish
Frame 3 → Fish
...
Frame 100 → Fish
If we add one to the total for every detection, we would incorrectly report:
100 Fish
when there is only one.
To count unique fish or fish crossing a particular area, we need object tracking.
10. YOLO Fish Tracking
Ultralytics provides tracking functionality.
from ultralytics import YOLO
model = YOLO("best.pt")
results = model.track(
source="videos/fish.mp4",
tracker="bytetrack.yaml",
show=True
)
The tracker attempts to maintain an ID for each detected fish.
For example:
Fish ID 1
Fish ID 2
Fish ID 3
Fish ID 4
These IDs can remain associated with the fish across multiple frames.
11. Real-Time Fish Tracking
Create:
fish_count.py
Then:
import cv2
from ultralytics import YOLO
model = YOLO("best.pt")
camera = cv2.VideoCapture(0)
while True:
success, frame = camera.read()
if not success:
break
results = model.track(
frame,
persist=True,
tracker="bytetrack.yaml",
conf=0.50
)
annotated_frame = results[0].plot()
cv2.imshow(
"Fish Tracking",
annotated_frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
The tracking system can assign IDs to detected fish.
12. Count Unique Fish IDs
We can store tracking IDs in a Python set.
import cv2
from ultralytics import YOLO
model = YOLO("best.pt")
camera = cv2.VideoCapture(0)
unique_fish = set()
while True:
success, frame = camera.read()
if not success:
break
results = model.track(
frame,
persist=True,
tracker="bytetrack.yaml",
conf=0.50
)
result = results[0]
if result.boxes.id is not None:
track_ids = (
result.boxes.id
.int()
.cpu()
.tolist()
)
for track_id in track_ids:
unique_fish.add(track_id)
annotated_frame = result.plot()
cv2.putText(
annotated_frame,
f"Unique Fish: {len(unique_fish)}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2
)
cv2.imshow(
"Fish Counting",
annotated_frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
Example display:
Unique Fish: 25
This counts the tracking IDs observed during the current session.
However, in real underwater footage, tracking IDs can sometimes be lost due to occlusion, fish overlap, or poor visibility, so this should not be treated as a perfect biological census.
13. Fish Counting With a Virtual Line
A more practical counting method is to count fish when they cross a virtual line.
For example:
Fish Movement
↓
↓
--------------------------------
COUNTING LINE
--------------------------------
↓
↓
When a fish crosses the line:
Fish Count += 1
This can be useful for:
Fish passages
Aquarium entrances
Fish farm channels
Conveyor systems
Controlled underwater cameras
14. Create a Counting Line
Define:
LINE_Y = 300
Draw the line:
cv2.line(
frame,
(0, LINE_Y),
(frame.shape[1], LINE_Y),
(255, 0, 0),
2
)
This creates a horizontal counting line.
15. Detect Fish Crossing the Line
First, calculate the center of each fish bounding box:
x1, y1, x2, y2 = map(
int,
box.xyxy[0]
)
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
Store the previous position:
previous_positions = {}
Then compare the previous and current positions.
If:
Previous Y < LINE_Y
and:
Current Y >= LINE_Y
the fish has crossed the line.
16. Complete Fish Line-Crossing Counter
Here is a basic implementation:
import cv2
from ultralytics import YOLO
model = YOLO("best.pt")
camera = cv2.VideoCapture(0)
LINE_Y = 300
fish_count = 0
previous_positions = {}
counted_ids = set()
while True:
success, frame = camera.read()
if not success:
break
results = model.track(
frame,
persist=True,
tracker="bytetrack.yaml",
conf=0.50
)
result = results[0]
if result.boxes.id is not None:
boxes = (
result.boxes.xyxy
.cpu()
.tolist()
)
track_ids = (
result.boxes.id
.int()
.cpu()
.tolist()
)
for box, track_id in zip(
boxes,
track_ids
):
x1, y1, x2, y2 = map(
int,
box
)
center_x = (
x1 + x2
) // 2
center_y = (
y1 + y2
) // 2
previous_y = (
previous_positions
.get(track_id)
)
if (
previous_y is not None
and previous_y < LINE_Y
and center_y >= LINE_Y
and track_id not in counted_ids
):
fish_count += 1
counted_ids.add(
track_id
)
previous_positions[
track_id
] = center_y
cv2.rectangle(
frame,
(x1, y1),
(x2, y2),
(0, 255, 0),
2
)
cv2.putText(
frame,
f"ID: {track_id}",
(x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 0),
2
)
cv2.line(
frame,
(0, LINE_Y),
(frame.shape[1], LINE_Y),
(255, 0, 0),
2
)
cv2.putText(
frame,
f"Fish Count: {fish_count}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2
)
cv2.imshow(
"Fish Counting",
frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
This example counts a tracked fish when it crosses the configured line.
17. Detect Fish in a Video
A recorded underwater video can be processed using:
from ultralytics import YOLO
model = YOLO("best.pt")
results = model.track(
source="videos/fish.mp4",
tracker="bytetrack.yaml",
save=True,
conf=0.50
)
The resulting video can contain bounding boxes and tracking IDs.
18. Create a Custom Fish Dataset
A custom dataset is usually required for reliable fish detection.
This is especially important when working with:
Specific fish species
Underwater cameras
Turbid water
Low-light environments
Coral reefs
Fish farms
Dense groups of fish
A dataset could contain classes such as:
fish
Or multiple species:
tuna
salmon
trout
tilapia
carp
For a simple fish-counting project, a single fish class is often enough.
19. Dataset Structure
A YOLO dataset can look like:
fish-dataset/
│
├── images/
│ ├── train/
│ └── val/
│
├── labels/
│ ├── train/
│ └── val/
│
└── data.yaml
Each image should have a corresponding annotation file.
For example:
images/train/fish001.jpg
labels/train/fish001.txt
20. Annotate Fish Images
Each visible fish should be annotated with a bounding box.
For example:
Image:
underwater01.jpg
Object:
fish
Bounding Box:
x1 = 120
y1 = 90
x2 = 280
y2 = 240
If an image contains 10 fish, each fish should be annotated separately.
This teaches the model to distinguish individual fish.
21. YOLO Label Format
YOLO labels use:
class_id center_x center_y width height
For example:
0 0.510 0.420 0.200 0.300
If you have only one class:
0: fish
then every fish annotation will use class ID 0.
22. Create data.yaml
Create:
data.yaml
Example:
path: ./fish-dataset
train: images/train
val: images/val
names:
0: fish
For multiple species:
path: ./fish-dataset
train: images/train
val: images/val
names:
0: tuna
1: salmon
2: trout
3: tilapia
23. Train a Custom Fish Detection Model
Create:
train.py
Then:
from ultralytics import YOLO
model = YOLO("yolo11n.pt")
model.train(
data="data.yaml",
epochs=50,
imgsz=640,
batch=16
)
Run:
python train.py
The model will learn visual features associated with fish in your dataset.
24. Train From the Command Line
You can also use:
yolo detect train \
data=data.yaml \
model=yolo11n.pt \
epochs=50 \
imgsz=640
For larger datasets, a GPU is recommended.
25. Test the Trained Model
After training:
from ultralytics import YOLO
model = YOLO(
"runs/detect/train/weights/best.pt"
)
results = model(
"test_fish.jpg",
conf=0.50
)
for result in results:
result.show()
The trained model should identify fish in new images.
26. Fish Species Detection
The same system can be extended to identify multiple species.
For example:
0 → Tuna
1 → Salmon
2 → Trout
3 → Tilapia
Then:
from ultralytics import YOLO
model = YOLO("fish_species.pt")
results = model("fish.jpg")
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
confidence = float(box.conf[0])
species = result.names[class_id]
print(
f"Species: {species} | "
f"Confidence: {confidence:.2f}"
)
Example:
Species: Tuna | Confidence: 0.94
Species: Salmon | Confidence: 0.91
Species: Tuna | Confidence: 0.89
27. Count Fish by Species
We can combine YOLO with Python's Counter.
from collections import Counter
from ultralytics import YOLO
model = YOLO("fish_species.pt")
results = model("fish.jpg")
fish_counts = Counter()
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
species = result.names[class_id]
fish_counts[species] += 1
print(fish_counts)
Example output:
Counter({
'tuna': 12,
'salmon': 8,
'trout': 5
})
This provides a species-level count.
28. Fish Detection in Aquaculture
Fish farms can use computer vision to monitor fish populations.
A camera can be placed in a controlled area and the system can estimate:
Fish Count
Fish Movement
Species
Size
Activity
More advanced models can potentially estimate fish size or detect abnormal behavior, but those tasks require specialized datasets and validation.
29. Fish Counting in Aquariums
Aquariums can use computer vision to monitor fish populations.
For example:
Total Fish: 42
Active Fish: 38
Detected Species: 5
The system could continuously analyze camera footage.
30. Marine Research
Underwater cameras can generate huge amounts of video data.
Researchers can use AI to automatically filter and analyze footage.
Instead of manually reviewing every frame, the system can identify frames containing fish.
This can significantly reduce the amount of footage requiring manual review.
31. Challenges in Fish Detection
Fish detection is often more difficult than detecting objects in normal environments.
Common challenges include:
Water Visibility
Murky or cloudy water can make fish difficult to see.
Lighting
Underwater lighting can change significantly with depth and time.
Fish Overlap
Multiple fish may overlap, making it difficult to distinguish individual animals.
Similar Appearance
Fish of the same species may look extremely similar.
Fast Movement
Fish can move quickly, making tracking more difficult.
Camera Movement
An underwater camera may move with water currents.
32. Improving Fish Detection Accuracy
Several techniques can improve performance.
Collect Diverse Images
Include:
Clear water
Murky water
Bright conditions
Dark conditions
Different depths
Different camera angles
Different fish sizes
Include Crowded Scenes
If the final application contains groups of fish, include crowded scenes in the training dataset.
Annotate Small Fish Carefully
Small fish are difficult to detect, so accurate bounding boxes are important.
Use Data Augmentation
Useful augmentation techniques include:
Rotation
Scaling
Cropping
Flipping
Brightness changes
Contrast changes
Blur
Use a Suitable Model
A larger model may improve detection accuracy but can require more computational resources.
33. Evaluate the Model
Important evaluation metrics include:
Precision
Precision =
True Positives /
(True Positives + False Positives)
Recall
Recall =
True Positives /
(True Positives + False Negatives)
IoU
IoU =
Intersection Area /
Union Area
mAP
Mean Average Precision is commonly used to evaluate object detection models.
For fish counting, you should also compare the automated count against manually verified counts on representative videos.
34. Complete Fish Detection and Counting Example
Here is a compact real-time example using YOLO tracking:
import cv2
from ultralytics import YOLO
MODEL_PATH = "best.pt"
CONFIDENCE = 0.50
model = YOLO(MODEL_PATH)
camera = cv2.VideoCapture(0)
unique_fish = set()
while True:
success, frame = camera.read()
if not success:
break
results = model.track(
frame,
persist=True,
tracker="bytetrack.yaml",
conf=CONFIDENCE
)
result = results[0]
if result.boxes.id is not None:
track_ids = (
result.boxes.id
.int()
.cpu()
.tolist()
)
for track_id in track_ids:
unique_fish.add(track_id)
annotated_frame = result.plot()
cv2.putText(
annotated_frame,
f"Fish IDs Seen: {len(unique_fish)}",
(30, 50),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(0, 255, 0),
2
)
cv2.imshow(
"YOLO Fish Detection and Counting",
annotated_frame
)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
This provides a starting point for a fish tracking and counting application.
35. Complete Fish Detection Architecture
A real-world fish monitoring system could follow this architecture:
UNDERWATER CAMERA
↓
VIDEO FRAME
↓
YOLO MODEL
↓
FISH DETECTION
↓
OBJECT TRACKING
↓
┌────────┴────────┐
↓ ↓
FISH COUNT SPECIES
↓ ↓
└────────┬────────┘
↓
DATA PROCESSING
↓
DATABASE
↓
ANALYTICS DASHBOARD
A dashboard could display:
=================================
FISH MONITORING
=================================
Current Fish: 84
Fish Detected: 1,245
Species:
Tuna: 32
Salmon: 27
Trout: 25
=================================
Conclusion
YOLO can be used to build powerful fish detection and counting systems using Python.
A basic model can detect fish in images, while combining YOLO with object tracking allows developers to build systems capable of following individual fish and counting them as they move through a camera view.
The basic workflow is:
Camera
↓
Video Frame
↓
YOLO Detection
↓
Fish Detection
↓
Object Tracking
↓
Tracking IDs
↓
Counting
↓
Database / Dashboard
For a simple image, fish counting can be performed by counting the detected bounding boxes. For video, tracking is important because the same fish appears in many consecutive frames.
For specialized applications such as aquaculture, marine research, or species identification, a custom dataset is recommended. The dataset should represent the actual underwater conditions in which the system will operate.
With YOLO + Python + OpenCV + Object Tracking, developers can create applications for automated fish monitoring, aquaculture analytics, aquarium management, marine research, and underwater computer vision.
