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 ================================================ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/67f0a9e168b3457385f2f7fcd09a9afa)](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) [![PyPI version](https://badge.fury.io/py/color-tracker.svg)](https://badge.fury.io/py/color-tracker) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3](https://img.shields.io/badge/Python-3-brightgreen.svg)](https://www.python.org/downloads/) [![DOI](https://zenodo.org/badge/101786270.svg)](https://zenodo.org/badge/latestdoi/101786270) # Color Tracker - Multi Object Tracker Easy to use **multi object tracking** package based on colors :art: yellow-cruiser ball-tracking ## 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: Buy Me a Coffee at ko-fi.com ## 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(), )