Repository: gaborvecsei/Color-Tracker
Branch: master
Commit: e9167f4a1ac8
Files: 19
Total size: 35.0 KB
Directory structure:
gitextract_b8g_9ogy/
├── .github/
│ └── workflows/
│ └── publish_to_pypi.yml
├── .gitignore
├── LICENSE
├── README.md
├── color_tracker/
│ ├── __init__.py
│ ├── tracker/
│ │ ├── __init__.py
│ │ └── tracker.py
│ └── utils/
│ ├── __init__.py
│ ├── camera/
│ │ ├── __init__.py
│ │ ├── base_camera.py
│ │ └── web_camera.py
│ ├── color_range_detector.py
│ ├── helpers.py
│ ├── tracker_object.py
│ └── visualize.py
├── examples/
│ ├── hsv_color_detector.py
│ └── tracking.py
├── requirements.txt
└── setup.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/publish_to_pypi.yml
================================================
name: Publish Color-Tracker to PyPI when a release is created
on:
release:
types: [published]
jobs:
build-package-and-publish:
name: Publish Color-Tracker to PyPI
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Build the package
run: |
python -m pip install --user --upgrade setuptools wheel
python setup.py sdist bdist_wheel
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.pypi_password }}
================================================
FILE: .gitignore
================================================
__pycache__
.idea
test.py
video_reader.py
dist
build
*egg-info
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Gábor Vecsei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://www.codacy.com/app/vecseigabor.x/Color-Tracker?utm_source=github.com&utm_medium=referral&utm_content=gaborvecsei/Color-Tracker&utm_campaign=Badge_Grade)
[](https://badge.fury.io/py/color-tracker)
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://zenodo.org/badge/latestdoi/101786270)
# Color Tracker - Multi Object Tracker
Easy to use **multi object tracking** package based on colors :art:
## Install
```
pip install color-tracker
```
```
pip install git+https://github.com/gaborvecsei/Color-Tracker.git
```
## Object Tracker
- Check out the **[examples folder](examples)**, or go straight to the **[sample tracking app](examples/tracking.py)** which is an extended version of the script below.
This script tracks the red-ish objects, if you'd like to track another color, then start with the `hsv_color_detector.py` script
``` python
$ python examples/tracking.py --help
usage: tracking.py [-h] [-low LOW LOW LOW] [-high HIGH HIGH HIGH]
[-c CONTOUR_AREA] [-v]
optional arguments:
-h, --help show this help message and exit
-low LOW LOW LOW, --low LOW LOW LOW
Lower value for the HSV range. Default = 155, 103, 82
-high HIGH HIGH HIGH, --high HIGH HIGH HIGH
Higher value for the HSV range. Default = 178, 255,
255
-c CONTOUR_AREA, --contour-area CONTOUR_AREA
Minimum object contour area. This controls how small
objects should be detected. Default = 2500
-v, --verbose
```
- Simple script:
``` python
import cv2
import color_tracker
def tracker_callback(t: color_tracker.ColorTracker):
cv2.imshow("debug", t.debug_frame)
cv2.waitKey(1)
tracker = color_tracker.ColorTracker(max_nb_of_objects=1, max_nb_of_points=20, debug=True)
tracker.set_tracking_callback(tracker_callback)
with color_tracker.WebCamera() as cam:
# Define your custom Lower and Upper HSV values
tracker.track(cam, [155, 103, 82], [178, 255, 255], max_skipped_frames=24)
```
## Color Range Detection
This is a tool which you can use to easily determine the necessary *HSV* color values and kernel sizes for you app
You can find **[the HSV Color Detector code here](examples/hsv_color_detector.py)**
``` python
python examples/hsv_color_detector.py
```
## Donate :coffee:
If you feel like it is a **useful package** and it **saved you time and effor**, then you can donate a coffe for me, so I can keep on staying awake for days :smiley:
## About
Gábor Vecsei
- [Website](https://gaborvecsei.com)
- [Personal Blog](https://gaborvecsei.com)
- [LinkedIn](https://www.linkedin.com/in/gaborvecsei)
- [Twitter](https://twitter.com/GAwesomeBE)
- [Github](https://github.com/gaborvecsei)
```
@misc{vecsei2018colortracker,
doi = {10.5281/ZENODO.4097717},
howpublished={\url{https://github.com/gaborvecsei/Color-Tracker}},
author = {Gabor Vecsei},
title = {Color Tracker - Multi Object Tracker},
year = {2018},
copyright = {MIT License}
}
```
================================================
FILE: color_tracker/__init__.py
================================================
"""
*****************************************************
* Color Tracker
*
* Gabor Vecsei
* Website: https://gaborvecsei.com
* Blog: https://gaborvecsei.com
* LinkedIn: https://www.linkedin.com/in/gaborvecsei
* Github: https://github.com/gaborvecsei
*
*****************************************************
"""
from .tracker.tracker import ColorTracker
from .utils import HSVColorRangeDetector
from .utils.camera import WebCamera
__author__ = "Gabor Vecsei"
__version__ = "0.1.1"
================================================
FILE: color_tracker/tracker/__init__.py
================================================
from .tracker import ColorTracker
================================================
FILE: color_tracker/tracker/tracker.py
================================================
import warnings
from typing import Union, List, Callable
import cv2
import numpy as np
from color_tracker.utils import helpers, visualize
from color_tracker.utils.camera import Camera
from color_tracker.utils.tracker_object import TrackedObject
class ColorTracker(object):
def __init__(self, max_nb_of_objects: int = None,
max_nb_of_points: int = None, debug: bool = True):
"""
:param max_nb_of_points: Maxmimum number of points for storing. If it is set
to None than it means there is no limit
:param debug: When it's true than we can see the visualization of the captured points etc...
"""
super().__init__()
self._debug = debug
self._max_nb_of_objects = max_nb_of_objects
self._max_nb_of_points = max_nb_of_points
self._debug_colors = visualize.random_colors(max_nb_of_objects)
self._selection_points = None
self._is_running = False
self._frame = None
self._debug_frame = None
self._frame_preprocessor = None
self._tracked_objects = []
self._tracked_object_id_count = 0
self._tracking_callback = None
@property
def tracked_objects(self) -> List[TrackedObject]:
return self._tracked_objects
@property
def frame(self):
return self._frame
@property
def debug_frame(self):
if self._debug:
return self._debug_frame
else:
warnings.warn("Debugging is not enabled so there is no debug frame")
return None
def set_frame_preprocessor(self, preprocessor_func):
self._frame_preprocessor = preprocessor_func
def set_court_points(self, court_points):
"""
Set a set of points that crops out a convex polygon from the image.
So only on the cropped part will be detection
:param court_points (list): list of points
"""
self._selection_points = court_points
def set_tracking_callback(self, tracking_callback: Callable[["ColorTracker"], None]):
self._tracking_callback = tracking_callback
def stop_tracking(self):
"""
Stop the color tracking
"""
self._is_running = False
@staticmethod
def _read_from_camera(camera, horizontal_flip: bool) -> np.ndarray:
ret, frame = camera.read()
if ret:
if horizontal_flip:
frame = cv2.flip(frame, 1)
else:
raise ValueError("There is no camera feed")
return frame
def _init_new_tracked_object(self, obj_center):
tracked_obj = TrackedObject(self._tracked_object_id_count, self._max_nb_of_points)
tracked_obj.add_point(obj_center)
self._tracked_object_id_count += 1
self._tracked_objects.append(tracked_obj)
def track(self, camera: Union[Camera, cv2.VideoCapture], hsv_lower_value: Union[np.ndarray, List[int]],
hsv_upper_value: Union[np.ndarray, List[int]], min_contour_area: Union[float, int] = 0,
kernel: np.ndarray = None, horizontal_flip: bool = True, max_track_point_distance: int = 100,
max_skipped_frames: int = 24):
"""
With this we can start the tracking with the given parameters
:param camera: Camera object which parent is a Camera object (like WebCamera)
:param max_skipped_frames: An object can be hidden for this many frames, after that it will be counted as a new
:param max_track_point_distance: maximum distance between tracking points
:param horizontal_flip: Flip input image horizontally
:param hsv_lower_value: lowest acceptable hsv values
:param hsv_upper_value: highest acceptable hsv values
:param min_contour_area: minimum contour area for the detection. Below that the detection does not count
:param kernel: structuring element to perform morphological operations on the mask image
"""
self._is_running = True
while True:
self._frame = self._read_from_camera(camera, horizontal_flip)
if self._frame_preprocessor is not None:
self._frame = self._frame_preprocessor(self._frame)
if (self._selection_points is not None) and (len(self._selection_points) > 0):
self._frame = helpers.crop_out_polygon_convex(self._frame, self._selection_points)
contours = helpers.find_object_contours(image=self._frame,
hsv_lower_value=hsv_lower_value,
hsv_upper_value=hsv_upper_value,
kernel=kernel)
contours = helpers.filter_contours_by_area(contours, min_contour_area)
contours = helpers.sort_contours_by_area(contours)
if self._max_nb_of_objects is not None and self._max_nb_of_objects > 0:
contours = contours[:self._max_nb_of_objects]
bboxes = helpers.get_bbox_for_contours(contours)
object_centers = helpers.get_contour_centers(contours)
# Init the list of tracked objects if it's empty
if len(self._tracked_objects) == 0:
for obj_center in object_centers:
self._init_new_tracked_object(obj_center)
# Constructing cost matrix (matrix with the distances from points to other points)
cost_mtx = helpers.calculate_distance_mtx(self._tracked_objects, object_centers)
# Solve assignment problem
assignment = helpers.solve_assignment(cost_mtx)
# Refine assignment list and objects's skipped frames
for i in range(len(assignment)):
if assignment[i] != -1:
if cost_mtx[i][assignment[i]] > max_track_point_distance:
assignment[i] = -1
else:
self._tracked_objects[i].skipped_frames += 1
# Remove tracked object if the object skipped to many frames, so it was not detected
helpers.remove_object_if_too_many_frames_skipped(self._tracked_objects, assignment, max_skipped_frames)
# Check for new objects and initialize them
un_assigned_detections = [i for i in range(len(object_centers)) if i not in assignment]
if len(un_assigned_detections) != 0:
if len(self._tracked_objects) < self._max_nb_of_objects:
for i in un_assigned_detections:
self._init_new_tracked_object(object_centers[i])
# Refresh tracked objects (reset "skipped frames" counter and add new object center to the queue)
for i in range(len(assignment)):
if assignment[i] != -1:
self._tracked_objects[i].skipped_frames = 0
self._tracked_objects[i].add_point(object_centers[assignment[i]])
if len(contours) > i:
self._tracked_objects[i].last_object_contour = contours[i]
self._tracked_objects[i].last_bbox = bboxes[i]
if self._debug:
self._debug_frame = self._frame.copy()
for i, tracked_obj in enumerate(self._tracked_objects):
self._debug_frame = visualize.draw_debug_frame_for_object(self._debug_frame,
tracked_obj,
self._debug_colors[i])
if self._tracking_callback is not None:
self._tracking_callback(self)
if not self._is_running:
break
================================================
FILE: color_tracker/utils/__init__.py
================================================
from .color_range_detector import HSVColorRangeDetector
from .helpers import *
================================================
FILE: color_tracker/utils/camera/__init__.py
================================================
from .base_camera import Camera
from .web_camera import WebCamera
================================================
FILE: color_tracker/utils/camera/base_camera.py
================================================
import threading
import cv2
class Camera(object):
"""
Base Camera object
"""
def __init__(self):
self._cam = None
self._frame = None
self._frame_width = None
self._frame_height = None
self._ret = False
self._auto_undistortion = False
self._camera_matrix = None
self._distortion_coefficients = None
self._is_running = False
def _init_camera(self):
"""
This is the first for creating our camera
We should override this!
"""
pass
def start_camera(self):
"""
Start the running of the camera, without this we can't capture frames
Camera runs on a separate thread so we can reach a higher FPS
"""
self._init_camera()
self._is_running = True
threading.Thread(target=self._update_camera, args=()).start()
def _read_from_camera(self):
"""
This method is responsible for grabbing frames from the camera
We should override this!
"""
if self._cam is None:
raise Exception("Camera is not started!")
def _update_camera(self):
"""
Grabs the frames from the camera
"""
while True:
if self._is_running:
self._ret, self._frame = self._read_from_camera()
else:
break
def get_frame_width_and_height(self):
"""
Returns the width and height of the grabbed images
:return (int int): width and height
"""
return self._frame_width, self._frame_height
def read(self):
"""
With this you can grab the last frame from the camera
:return (boolean, np.array): return value and frame
"""
if self._is_running:
return self._ret, self._frame
else:
import warnings
warnings.warn("Camera is not started, you should start it with start_camera()")
return False, None
def release(self):
"""
Stop the camera
"""
self._is_running = False
def is_running(self):
return self._is_running
def set_calibration_matrices(self, camera_matrix, distortion_coefficients):
self._camera_matrix = camera_matrix
self._distortion_coefficients = distortion_coefficients
def activate_auto_undistortion(self):
self._auto_undistortion = True
def deactivate_auto_undistortion(self):
self._auto_undistortion = False
def _undistort_image(self, image):
if self._camera_matrix is None or self._distortion_coefficients is None:
import warnings
warnings.warn("Undistortion has no effect because / is None!")
return image
h, w = image.shape[:2]
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(self._camera_matrix,
self._distortion_coefficients, (w, h),
1,
(w, h))
undistorted = cv2.undistort(image, self._camera_matrix, self._distortion_coefficients, None,
new_camera_matrix)
return undistorted
def __enter__(self):
self.start_camera()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
================================================
FILE: color_tracker/utils/camera/web_camera.py
================================================
import cv2
from color_tracker.utils.camera.base_camera import Camera
class WebCamera(Camera):
"""
Simple Webcamera
"""
def __init__(self, video_src=0, start: bool = False):
"""
:param video_src (int): camera source code. It can be an integer or the name of the video file.
"""
super().__init__()
self._video_src = video_src
if start:
self.start_camera()
def _init_camera(self):
super()._init_camera()
self._cam = cv2.VideoCapture(self._video_src)
self._ret, self._frame = self._cam.read()
if not self._ret:
raise Exception("No camera feed")
self._frame_height, self._frame_width, c = self._frame.shape
return self._ret
def _read_from_camera(self):
super()._read_from_camera()
self._ret, self._frame = self._cam.read()
if self._ret:
if self._auto_undistortion:
self._frame = self._undistort_image(self._frame)
return True, self._frame
else:
return False, None
def release(self):
super().release()
self._cam.release()
================================================
FILE: color_tracker/utils/color_range_detector.py
================================================
import cv2
import numpy as np
from color_tracker.utils import helpers
from color_tracker.utils.camera import Camera
class HSVColorRangeDetector:
"""
Just a helper to determine what kind of lower and upper HSV values you need for the tracking
"""
def __init__(self, camera: Camera):
self._camera = camera
self._trackbars = []
self._main_window_name = "HSV color range detector"
cv2.namedWindow(self._main_window_name)
self._init_trackbars()
def _init_trackbars(self):
trackbars_window_name = "hsv settings"
cv2.namedWindow(trackbars_window_name, cv2.WINDOW_NORMAL)
# HSV Lower Bound
h_min_trackbar = _Trackbar("H min", trackbars_window_name, 0, 255)
s_min_trackbar = _Trackbar("S min", trackbars_window_name, 0, 255)
v_min_trackbar = _Trackbar("V min", trackbars_window_name, 0, 255)
# HSV Upper Bound
h_max_trackbar = _Trackbar("H max", trackbars_window_name, 255, 255)
s_max_trackbar = _Trackbar("S max", trackbars_window_name, 255, 255)
v_max_trackbar = _Trackbar("V max", trackbars_window_name, 255, 255)
# Kernel for morphology
kernel_x = _Trackbar("kernel x", trackbars_window_name, 0, 30)
kernel_y = _Trackbar("kernel y", trackbars_window_name, 0, 30)
self._trackbars = [h_min_trackbar, s_min_trackbar, v_min_trackbar, h_max_trackbar, s_max_trackbar,
v_max_trackbar, kernel_x, kernel_y]
def _get_trackbar_values(self):
values = []
for t in self._trackbars:
value = t.get_value()
values.append(value)
return values
def detect(self):
display_width = 360
display_height = 240
font_color = (0, 255, 255)
font_scale = 0.4
font_org = (5, 10)
while True:
ret, frame = self._camera.read()
if ret:
frame = cv2.flip(frame, 1)
else:
continue
draw_image = frame.copy()
hsv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
values = self._get_trackbar_values()
h_min, s_min, v_min = values[:3]
h_max, s_max, v_max = values[3:6]
kernel_x, kernel_y = values[6:]
if kernel_y < 1:
kernel_y = 1
if kernel_x < 1:
kernel_x = 1
thresh = cv2.inRange(hsv_img, (h_min, s_min, v_min), (h_max, s_max, v_max))
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_x, kernel_y))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=1)
preview = cv2.bitwise_and(draw_image, draw_image, mask=thresh)
# Original image
img_display = helpers.resize_img(draw_image, display_width, display_height)
cv2.putText(img_display, "Original image", font_org, cv2.FONT_HERSHEY_COMPLEX, font_scale, font_color)
# Thresholded image
thresh_display = cv2.cvtColor(helpers.resize_img(thresh, display_width, display_height), cv2.COLOR_GRAY2BGR)
cv2.putText(thresh_display, "Object map", font_org, cv2.FONT_HERSHEY_COMPLEX, font_scale, font_color)
# Preview of masked objects
preview_display = helpers.resize_img(preview, display_width, display_height)
cv2.putText(preview_display, "Object preview", font_org, cv2.FONT_HERSHEY_COMPLEX, font_scale, font_color)
# HSV image
hsv_img_display = helpers.resize_img(hsv_img, display_width, display_height)
cv2.putText(hsv_img_display, "HSV image", font_org, cv2.FONT_HERSHEY_COMPLEX, font_scale, font_color)
# Combine images
display_img_1 = np.concatenate((img_display, thresh_display), axis=1)
display_img_2 = np.concatenate((preview_display, hsv_img_display), axis=1)
display_img = np.concatenate((display_img_1, display_img_2), axis=0)
cv2.imshow(self._main_window_name, display_img)
key = cv2.waitKey(1)
if key == 27:
break
self._camera.release()
cv2.destroyAllWindows()
upper_color = np.array([h_max, s_max, v_max])
lower_color = np.array([h_min, s_min, v_min])
return lower_color, upper_color, kernel
class _Trackbar(object):
def __init__(self, name, parent_window_name, init_value=0, max_value=255):
self.parent_window_name = parent_window_name
self.name = name
self.init_value = init_value
self.max_value = max_value
cv2.createTrackbar(self.name, self.parent_window_name, self.init_value, self.max_value, lambda x: x)
def get_value(self):
value = cv2.getTrackbarPos(self.name, self.parent_window_name)
return value
================================================
FILE: color_tracker/utils/helpers.py
================================================
from typing import List, Tuple, Union
import cv2
import numpy as np
from scipy import optimize
from color_tracker.utils.tracker_object import TrackedObject
def crop_out_polygon_convex(image: np.ndarray, point_array: np.ndarray) -> np.ndarray:
"""
Crops out a convex polygon given from a list of points from an image
:param image: Opencv BGR image
:param point_array: list of points that defines a convex polygon
:return: Cropped out image
"""
point_array = np.reshape(cv2.convexHull(point_array), point_array.shape)
mask = np.zeros(image.shape, dtype=np.uint8)
roi_corners = np.array([point_array], dtype=np.int32)
ignore_mask_color = (255, 255, 255)
cv2.fillConvexPoly(mask, roi_corners, ignore_mask_color)
masked_image = cv2.bitwise_and(image, mask)
return masked_image
def resize_img(image: np.ndarray, min_width: int, min_height: int) -> np.ndarray:
"""
Resize the image with keeping the aspect ratio.
:param image: image
:param min_width: minimum width of the image
:param min_height: minimum height of the image
:return: resized image
"""
h, w = image.shape[:2]
new_w = w
new_h = h
if w > min_width:
new_w = min_width
new_h = int(h * (float(new_w) / w))
h, w = (new_h, new_w)
if h > min_height:
new_h = min_height
new_w = int(w * (float(new_h) / h))
return cv2.resize(image, (new_w, new_h))
def sort_contours_by_area(contours: np.ndarray, descending: bool = True) -> np.ndarray:
if len(contours) > 0:
contours = sorted(contours, key=cv2.contourArea, reverse=descending)
return contours
def filter_contours_by_area(contours: np.ndarray, min_area: float = 0, max_area: float = np.inf) -> np.ndarray:
if len(contours) == 0:
return np.array([])
def _keep_contour(c):
area = cv2.contourArea(c)
if area <= min_area:
return False
if area >= max_area:
return False
return True
return np.array(list(filter(_keep_contour, contours)))
def get_contour_centers(contours: np.ndarray) -> np.ndarray:
"""
Calculate the centers of the contours
:param contours: Contours detected with find_contours
:return: object centers as numpy array
"""
if len(contours) == 0:
return np.array([])
# ((x, y), radius) = cv2.minEnclosingCircle(c)
centers = np.zeros((len(contours), 2), dtype=np.int16)
for i, c in enumerate(contours):
M = cv2.moments(c)
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
centers[i] = center
return centers
def find_object_contours(image: np.ndarray, hsv_lower_value: Union[Tuple[int], List[int]],
hsv_upper_value: Union[Tuple[int], List[int]], kernel: np.ndarray):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, tuple(hsv_lower_value), tuple(hsv_upper_value))
if kernel is not None:
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)
return cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
def get_bbox_for_contours(contours: np.ndarray) -> np.ndarray:
bboxes = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
bboxes.append([x, y, x + w, y + h])
return np.array(bboxes)
def calculate_distance_mtx(tracked_objects: List[TrackedObject], points: np.ndarray) -> np.ndarray:
# (nb_tracked_objects, nb_current_detected_points)
cost_mtx = np.zeros((len(tracked_objects), len(points)))
for i, tracked_obj in enumerate(tracked_objects):
for j, point in enumerate(points):
diff = tracked_obj.last_point - point
distance = np.sqrt(diff[0] ** 2 + diff[1] ** 2)
cost_mtx[i][j] = distance
return cost_mtx
def solve_assignment(cost_mtx: np.ndarray) -> List[int]:
nb_tracked_objects, nb_detected_obj_centers = cost_mtx.shape
assignment = [-1] * nb_tracked_objects
row_index, column_index = optimize.linear_sum_assignment(cost_mtx)
for i in range(len(row_index)):
assignment[row_index[i]] = column_index[i]
return assignment
def remove_object_if_too_many_frames_skipped(tracked_objects: List[TrackedObject], assignment: List[int],
max_skipped_frames: int):
for i, tracked_obj in enumerate(tracked_objects):
if tracked_obj.skipped_frames > max_skipped_frames:
del tracked_objects[i]
del assignment[i]
================================================
FILE: color_tracker/utils/tracker_object.py
================================================
import collections
class TrackedObject:
def __init__(self, id: int, max_nb_of_points: int = None):
self._id = id
self._tracked_points = collections.deque(maxlen=max_nb_of_points)
self._skipped_frames = 0
self._last_object_contour = None
self._last_bounding_box = None
@property
def id(self):
return self._id
@property
def skipped_frames(self):
return self._skipped_frames
@skipped_frames.setter
def skipped_frames(self, value):
self._skipped_frames = value
@property
def tracked_points(self):
return self._tracked_points
@property
def last_point(self):
return self.tracked_points[-1]
@property
def last_object_contour(self):
return self._last_object_contour
@last_object_contour.setter
def last_object_contour(self, value):
self._last_object_contour = value
@property
def last_bbox(self):
return self._last_bounding_box
@last_bbox.setter
def last_bbox(self, value):
self._last_bounding_box = value
def add_point(self, point):
self._tracked_points.append(point)
================================================
FILE: color_tracker/utils/visualize.py
================================================
import colorsys
import random
from typing import Tuple
import cv2
from color_tracker.utils.tracker_object import TrackedObject
def random_colors(nb_of_colors: int, brightness: float = 1.0):
hsv = [(i / nb_of_colors, 1, brightness) for i in range(nb_of_colors)]
colors = list(map(lambda c: colorsys.hsv_to_rgb(*c), hsv))
# note: we need to use list here with values [0, 255] as python built in scalar types,
# because OpenCV functions can't get numpy dtypes for color
colors = [list(map(lambda x: int(x * 255), c)) for c in colors]
random.shuffle(colors)
return colors
def draw_tracker_points(points, debug_image, color: Tuple[int, int, int] = (255, 255, 255)):
for i in range(1, len(points)):
if points[i - 1] is None or points[i] is None:
continue
rectangle_offset = 4
rectangle_pt1 = tuple(x - rectangle_offset for x in points[i])
rectangle_pt2 = tuple(x + rectangle_offset for x in points[i])
cv2.rectangle(debug_image, rectangle_pt1, rectangle_pt2, color, 1)
cv2.line(debug_image, tuple(points[i - 1]), tuple(points[i]), color, 1)
return debug_image
def draw_debug_frame_for_object(debug_frame, tracked_object: TrackedObject, color: Tuple[int, int, int] = (255, 255, 255)):
# contour = tracked_object.last_object_contour
bbox = tracked_object.last_bbox
points = tracked_object.tracked_points
# if contour is not None:
# cv2.drawContours(debug_frame, [contour], -1, (0, 255, 0), cv2.FILLED)
if bbox is not None:
x1, y1, x2, y2 = bbox
cv2.rectangle(debug_frame, (x1, y1), (x2, y2), (255, 255, 255), 1)
cv2.putText(debug_frame, "Id {0}".format(tracked_object.id), (x1, y1 - 5), cv2.FONT_HERSHEY_COMPLEX, 0.5,
(255, 255, 255))
if points is not None and len(points) > 0:
draw_tracker_points(points, debug_frame, color)
cv2.circle(debug_frame, tuple(points[-1]), 3, (0, 0, 255), -1)
return debug_frame
================================================
FILE: examples/hsv_color_detector.py
================================================
import color_tracker
# Init camera
cam = color_tracker.WebCamera(video_src=0)
cam.start_camera()
# Init Range detector
detector = color_tracker.HSVColorRangeDetector(camera=cam)
lower, upper, kernel = detector.detect()
# Print out the selected values
# (best practice is to save as numpy arrays and then you can load it whenever you want it)
print("Lower HSV color is: {0}".format(lower))
print("Upper HSV color is: {0}".format(upper))
print("Kernel shape is:\n{0}".format(kernel.shape))
================================================
FILE: examples/tracking.py
================================================
import argparse
from functools import partial
import cv2
import color_tracker
# You can determine these values with the HSVColorRangeDetector()
HSV_LOWER_VALUE = [155, 103, 82]
HSV_UPPER_VALUE = [178, 255, 255]
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("-low", "--low", nargs=3, type=int, default=HSV_LOWER_VALUE,
help="Lower value for the HSV range. Default = 155, 103, 82")
parser.add_argument("-high", "--high", nargs=3, type=int, default=HSV_UPPER_VALUE,
help="Higher value for the HSV range. Default = 178, 255, 255")
parser.add_argument("-c", "--contour-area", type=float, default=2500,
help="Minimum object contour area. This controls how small objects should be detected. Default = 2500")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
return args
def tracking_callback(tracker: color_tracker.ColorTracker, verbose: bool = True):
# Visualizing the original frame and the debugger frame
cv2.imshow("original frame", tracker.frame)
cv2.imshow("debug frame", tracker.debug_frame)
# Stop the script when we press ESC
key = cv2.waitKey(1)
if key == 27:
tracker.stop_tracking()
if verbose:
for obj in tracker.tracked_objects:
print("Object {0} center {1}".format(obj.id, obj.last_point))
def main():
args = get_args()
# Creating a kernel for the morphology operations
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
# Init the ColorTracker object
tracker = color_tracker.ColorTracker(max_nb_of_objects=5, max_nb_of_points=20, debug=True)
# Setting a callback which is called at every iteration
callback = partial(tracking_callback, verbose=args.verbose)
tracker.set_tracking_callback(tracking_callback=callback)
# Start tracking with a camera
with color_tracker.WebCamera(video_src=0) as webcam:
# Start the actual tracking of the object
tracker.track(webcam,
hsv_lower_value=args.low,
hsv_upper_value=args.high,
min_contour_area=args.contour_area,
kernel=kernel)
if __name__ == "__main__":
main()
================================================
FILE: requirements.txt
================================================
opencv-python
numpy
scipy
imageio
================================================
FILE: setup.py
================================================
"""
*****************************************************
* Color Tracker
*
* Gabor Vecsei
* Website: https://gaborvecsei.com
* Blog: https://gaborvecsei.com
* LinkedIn: https://www.linkedin.com/in/gaborvecsei
* Github: https://github.com/gaborvecsei
*
*****************************************************
"""
from codecs import open
from os import path
from setuptools import setup, find_packages
VERSION = '0.1.1'
here = path.abspath(path.dirname(__file__))
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
with open(path.join(here, 'requirements.txt')) as f:
requirements = f.read().splitlines()
setup(
name='color_tracker',
version=VERSION,
description='Easy to use color tracking package for object tracking based on colors',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/gaborvecsei/Color-Tracker',
author='Gabor Vecsei',
author_email='vecseigabor.x@gmail.com',
license='MIT',
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: Education',
'Intended Audience :: Science/Research',
'Topic :: Education',
'Topic :: Software Development',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3'],
keywords='color tracker vecsei gaborvecsei color_tracker',
install_requires=requirements,
packages=find_packages(),
)