This project was developed as part of an Object-Oriented Programming course using Python. It implements a YOLO-based object tracking system that processes a video file, detects and tracks multiple objects using the Ultralytics YOLO model, and produces an annotated output video with tracked object paths. The system draws persistent motion trails for each detected object by combining YOLO's detection capabilities with custom tracking history management.
The program processes each frame of a video, assigns consistent IDs to objects, and draws motion trails. The trails are intentionally limited in length to keep the visualization focused on active detections.
Objects are detected, labeled, and tracked with persistent paths as they move through the scene. Note that prediction inaccuracies may occur.
Ensure that all required dependencies listed below are installed before running the application.
The application requires the following Python libraries:
| Component | Purpose |
|---|---|
cv2 (OpenCV) |
Reading, writing, and drawing on video frames |
numpy |
Numerical operations and array manipulation |
argparse |
Parses command-line arguments |
collections.defaultdict |
Efficiently tracks object history |
ultralytics |
YOLO model for object detection and tracking |
unittest |
Built-in module for writing and running tests |
video |
Required: The video file to be processed |
Scripts |
Required: Contains the main application logic and entry point |
Note: YOLO model weights (e.g., yolov8m.pt) are downloaded automatically by the ultralytics library if not already present locally. No manual download is required.
Install the required dependencies using pip:
pip install opencv-python numpy ultralyticsBefore running the program, navigate to the appropriate directory:
cd yolov8_tracking/Car-Tracking-with-yolov8Run the program with:
python -m Scripts.run <path_to_input_video> --model yolov8m.ptExample:
python -m Scripts.run "path/to/input/video" --model yolov8m.pt- Provide a path to a video file as a command-line argument.
- The application processes the video and generates an output file with
_trackedappended to the filename. - The output video is saved to the same directory as the input file.
This project demonstrates all four pillars of OOP.
Encapsulation involves bundling data with the methods that operate on it, while restricting direct access to internal components. This principle appears throughout the project:
-
The
VideoHandlerclass encapsulates all video I/O logic. Users interact with simple public methods likeread_frame()andwrite_frame(), without needing to understand how video capture or writing is initialized. -
A key example is the
_init_writer()method, which is used internally byVideoHandlerto configure the output video writer. It abstracts the complexity of determining the output filename, format, frame size, and encoding:
def _init_writer(self, input_path: str):
output_path = input_path.rsplit('.', 1)[0] + '_tracked.mp4'
fourcc = cv2.VideoWriter.fourcc(*'mp4v')
fps = self.cap.get(cv2.CAP_PROP_FPS)
frame_size = (
int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
)
return cv2.VideoWriter(output_path, fourcc, fps, frame_size)-
The underscore prefix indicates this method is intended for internal use only, reinforcing encapsulation by convention.
-
Similarly, classes such as
YOLOModelandYOLOTrackerexpose only the methods required by the main application, while hiding lower-level details such as inference parameters or track history management.
Abstraction is achieved by exposing only essential methods and hiding the internal complexity of how tasks are performed.
- The
BaseModelabstract class, defined using Python'sabcmodule, establishes a contract (track()) that all derived model classes must implement. This allows the rest of the application to interact with any model uniformly, without needing to understand the specifics of each implementation:
from abc import ABC, abstractmethod
class BaseModel(ABC):
def __init__(self, model_path: str):
self.model_path = model_path
@abstractmethod
def track(self, frame):
pass-
Two concrete implementations derive from
BaseModel:YOLOModel: A general wrapper for the Ultralytics YOLO model.YOLOv8Model: A version with optional confidence threshold filtering for greater control over detection quality.
class YOLOModel(BaseModel):
def __init__(self, model_path: str):
super().__init__(model_path)
self.model = YOLO(model_path)
def track(self, frame):
return self.model.track(frame, persist=True, verbose=False)class YOLOv8Model(BaseModel):
def __init__(self, model_path: str, confidence_threshold: float = 0.5):
super().__init__(model_path)
self.model = YOLO(model_path)
self.confidence_threshold = confidence_threshold
def track(self, frame):
results = self.model.track(frame, persist=True, verbose=False)
if self.confidence_threshold > 0:
results = self._filter_by_confidence(results)
return results
def _filter_by_confidence(self, results):
for result in results:
boxes = result.boxes
confidences = boxes.conf
mask = confidences >= self.confidence_threshold
result.boxes = boxes[mask]
return results- The
YOLOModelFactoryfurther abstracts model instantiation by selecting the correct model class based on the filename or configuration:
class YOLOModelFactory:
@staticmethod
def create(model_path: str, confidence_threshold: float = 0.5) -> BaseModel:
if "v8" in model_path.lower():
return YOLOv8Model(model_path, confidence_threshold)
else:
return YOLOModel(model_path)- This design ensures that
YOLOTrackingAppdoes not need to change when new model types are introduced, as long as they conform to theBaseModelinterface.
Inheritance allows a class to reuse logic and structure from a parent class, reducing redundancy and improving consistency.
Custom Model Abstraction
- The
BaseModelclass defines a common interface for all YOLO models via thetrack()method. BothYOLOModelandYOLOv8Modelinherit from it:
class YOLOModel(BaseModel):
...class YOLOv8Model(BaseModel):
...This enforces a standard method structure and allows model implementations to be swapped or extended with minimal changes.
Unit Testing
- The
TestYOLOIntegrationclass inherits from Python's built-inunittest.TestCase, gaining access to testing utilities such asassertEqual()andsetUp():
class TestYOLOIntegration(unittest.TestCase):
...Polymorphism is applied via the Factory Design Pattern:
- The
YOLOModelFactoryprovides a unified interface for model creation, allowing different model types to be loaded without changing how they are used:
class YOLOModelFactory:
@staticmethod
def create(model_path: str):
return YOLOModel(model_path)- Regardless of which YOLO model version is loaded (
yolov8m.pt,yolov8n.pt, etc.), the calling code inYOLOTrackingAppdoes not change. It simply callsmodel.track(frame).
The project demonstrates both composition and aggregation:
- Composition is used in
YOLOTrackingApp, where the componentsVideoHandler,YOLOModel, andYOLOTrackerare created and managed internally:
class YOLOTrackingApp:
def __init__(self, video_path: str, model_path: str):
self.video = VideoHandler(video_path)
model = YOLOModelFactory.create(model_path)
self.tracker = YOLOTracker(model)These components have no meaningful existence outside of the application. They are fully owned and controlled by it.
- Aggregation is used in
YOLOTrackerto maintain tracking history using adefaultdict(list). This allows track history objects to exist and be updated over time without tight coupling to the tracker itself.
The project performs both reading from and writing to files:
- Input is handled using OpenCV's
VideoCaptureto read frames from a video file. - Output is written frame-by-frame to a new video file with a
_tracked.mp4suffix, usingVideoWriter.
self.cap = cv2.VideoCapture(input_path)
...
self.out.write(frame)This ensures real-time processing of input video and persistent saving of results.
Testing is implemented with Python's built-in unittest framework. Tests in yolo_unit_test.py verify:
- Model creation (
test_model_creation) - Frame processing logic (
test_process_frame) - Tracker history updates (
test_update_history) - Video file I/O (
test_video_handler)
Example test:
def test_update_history(self):
boxes = np.array([[100, 100, 50, 50]])
track_ids = [1]
self.tracker.update_history(boxes, track_ids)
self.assertIn(1, self.tracker.history)This ensures that individual components work correctly and can be confidently composed into a larger system.
- Objects were successfully tracked frame-to-frame with persistent ID assignment.
- Motion trails were drawn for each object to visualize movement over time.
- Annotated output videos with bounding boxes and tracks were saved correctly.
- All unit tests passed successfully.
- This project demonstrates a practical implementation of object tracking using modern deep learning models and classical computer vision techniques.
- The codebase is modular and testable, with clear abstraction boundaries between components.
- Future work could introduce support for real-time video streams and additional model types.
- Potential applications include surveillance, traffic monitoring, and sports analytics.
- YOLOv8 offers a strong balance of speed and accuracy, making it well-suited for real-time use even on modest hardware.
- Despite strong overall performance, the model occasionally produces false positives or misses small and overlapping objects, suggesting room for improvement through post-processing techniques or hybrid tracking approaches.
- Add a GUI for video input and output selection.
- Support live webcam tracking.
- Export tracking logs to CSV or JSON format.
- Add model benchmarking and FPS measurement utilities.