Full Code of gaborvecsei/Color-Tracker for AI

master e9167f4a1ac8 cached
19 files
35.0 KB
9.0k tokens
69 symbols
1 requests
Download .txt
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:

<img src="art/yellow_cruiser.gif" width="400" alt="yellow-cruiser"></a> <img src="art/ball_tracking.gif" width="400" alt="ball-tracking"></a>

## 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: 

<a href='https://ko-fi.com/A0A5KN4E' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi5.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>

## 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 <camera_matrix>/<distortion_coefficients> 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(),
)
Download .txt
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
Download .txt
SYMBOL INDEX (69 symbols across 8 files)

FILE: color_tracker/tracker/tracker.py
  class ColorTracker (line 12) | class ColorTracker(object):
    method __init__ (line 13) | def __init__(self, max_nb_of_objects: int = None,
    method tracked_objects (line 38) | def tracked_objects(self) -> List[TrackedObject]:
    method frame (line 42) | def frame(self):
    method debug_frame (line 46) | def debug_frame(self):
    method set_frame_preprocessor (line 53) | def set_frame_preprocessor(self, preprocessor_func):
    method set_court_points (line 56) | def set_court_points(self, court_points):
    method set_tracking_callback (line 65) | def set_tracking_callback(self, tracking_callback: Callable[["ColorTra...
    method stop_tracking (line 68) | def stop_tracking(self):
    method _read_from_camera (line 76) | def _read_from_camera(camera, horizontal_flip: bool) -> np.ndarray:
    method _init_new_tracked_object (line 87) | def _init_new_tracked_object(self, obj_center):
    method track (line 93) | def track(self, camera: Union[Camera, cv2.VideoCapture], hsv_lower_val...

FILE: color_tracker/utils/camera/base_camera.py
  class Camera (line 6) | class Camera(object):
    method __init__ (line 11) | def __init__(self):
    method _init_camera (line 24) | def _init_camera(self):
    method start_camera (line 32) | def start_camera(self):
    method _read_from_camera (line 42) | def _read_from_camera(self):
    method _update_camera (line 51) | def _update_camera(self):
    method get_frame_width_and_height (line 62) | def get_frame_width_and_height(self):
    method read (line 70) | def read(self):
    method release (line 82) | def release(self):
    method is_running (line 89) | def is_running(self):
    method set_calibration_matrices (line 92) | def set_calibration_matrices(self, camera_matrix, distortion_coefficie...
    method activate_auto_undistortion (line 96) | def activate_auto_undistortion(self):
    method deactivate_auto_undistortion (line 99) | def deactivate_auto_undistortion(self):
    method _undistort_image (line 102) | def _undistort_image(self, image):
    method __enter__ (line 117) | def __enter__(self):
    method __exit__ (line 121) | def __exit__(self, exc_type, exc_val, exc_tb):

FILE: color_tracker/utils/camera/web_camera.py
  class WebCamera (line 6) | class WebCamera(Camera):
    method __init__ (line 11) | def __init__(self, video_src=0, start: bool = False):
    method _init_camera (line 22) | def _init_camera(self):
    method _read_from_camera (line 31) | def _read_from_camera(self):
    method release (line 41) | def release(self):

FILE: color_tracker/utils/color_range_detector.py
  class HSVColorRangeDetector (line 8) | class HSVColorRangeDetector:
    method __init__ (line 13) | def __init__(self, camera: Camera):
    method _init_trackbars (line 20) | def _init_trackbars(self):
    method _get_trackbar_values (line 41) | def _get_trackbar_values(self):
    method detect (line 48) | def detect(self):
  class _Trackbar (line 118) | class _Trackbar(object):
    method __init__ (line 119) | def __init__(self, name, parent_window_name, init_value=0, max_value=2...
    method get_value (line 127) | def get_value(self):

FILE: color_tracker/utils/helpers.py
  function crop_out_polygon_convex (line 10) | def crop_out_polygon_convex(image: np.ndarray, point_array: np.ndarray) ...
  function resize_img (line 27) | def resize_img(image: np.ndarray, min_width: int, min_height: int) -> np...
  function sort_contours_by_area (line 53) | def sort_contours_by_area(contours: np.ndarray, descending: bool = True)...
  function filter_contours_by_area (line 59) | def filter_contours_by_area(contours: np.ndarray, min_area: float = 0, m...
  function get_contour_centers (line 74) | def get_contour_centers(contours: np.ndarray) -> np.ndarray:
  function find_object_contours (line 93) | def find_object_contours(image: np.ndarray, hsv_lower_value: Union[Tuple...
  function get_bbox_for_contours (line 102) | def get_bbox_for_contours(contours: np.ndarray) -> np.ndarray:
  function calculate_distance_mtx (line 110) | def calculate_distance_mtx(tracked_objects: List[TrackedObject], points:...
  function solve_assignment (line 121) | def solve_assignment(cost_mtx: np.ndarray) -> List[int]:
  function remove_object_if_too_many_frames_skipped (line 130) | def remove_object_if_too_many_frames_skipped(tracked_objects: List[Track...

FILE: color_tracker/utils/tracker_object.py
  class TrackedObject (line 4) | class TrackedObject:
    method __init__ (line 5) | def __init__(self, id: int, max_nb_of_points: int = None):
    method id (line 14) | def id(self):
    method skipped_frames (line 18) | def skipped_frames(self):
    method skipped_frames (line 22) | def skipped_frames(self, value):
    method tracked_points (line 26) | def tracked_points(self):
    method last_point (line 30) | def last_point(self):
    method last_object_contour (line 34) | def last_object_contour(self):
    method last_object_contour (line 38) | def last_object_contour(self, value):
    method last_bbox (line 42) | def last_bbox(self):
    method last_bbox (line 46) | def last_bbox(self, value):
    method add_point (line 49) | def add_point(self, point):

FILE: color_tracker/utils/visualize.py
  function random_colors (line 10) | def random_colors(nb_of_colors: int, brightness: float = 1.0):
  function draw_tracker_points (line 20) | def draw_tracker_points(points, debug_image, color: Tuple[int, int, int]...
  function draw_debug_frame_for_object (line 32) | def draw_debug_frame_for_object(debug_frame, tracked_object: TrackedObje...

FILE: examples/tracking.py
  function get_args (line 13) | def get_args():
  function tracking_callback (line 26) | def tracking_callback(tracker: color_tracker.ColorTracker, verbose: bool...
  function main (line 41) | def main():
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (38K chars).
[
  {
    "path": ".github/workflows/publish_to_pypi.yml",
    "chars": 678,
    "preview": "name: Publish Color-Tracker to PyPI when a release is created\n\non:\n  release:\n    types: [published]\n\njobs:\n  build-pack"
  },
  {
    "path": ".gitignore",
    "chars": 63,
    "preview": "__pycache__\n.idea\ntest.py\nvideo_reader.py\ndist\nbuild\n*egg-info\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2018 Gábor Vecsei\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 3900,
    "preview": "[![Codacy Badge](https://api.codacy.com/project/badge/Grade/67f0a9e168b3457385f2f7fcd09a9afa)](https://www.codacy.com/ap"
  },
  {
    "path": "color_tracker/__init__.py",
    "chars": 528,
    "preview": "\"\"\"\n*****************************************************\n*               Color Tracker\n*\n*              Gabor Vecsei\n* "
  },
  {
    "path": "color_tracker/tracker/__init__.py",
    "chars": 34,
    "preview": "from .tracker import ColorTracker\n"
  },
  {
    "path": "color_tracker/tracker/tracker.py",
    "chars": 7774,
    "preview": "import warnings\nfrom typing import Union, List, Callable\n\nimport cv2\nimport numpy as np\n\nfrom color_tracker.utils import"
  },
  {
    "path": "color_tracker/utils/__init__.py",
    "chars": 78,
    "preview": "from .color_range_detector import HSVColorRangeDetector\nfrom .helpers import *"
  },
  {
    "path": "color_tracker/utils/camera/__init__.py",
    "chars": 66,
    "preview": "from .base_camera import Camera\nfrom .web_camera import WebCamera\n"
  },
  {
    "path": "color_tracker/utils/camera/base_camera.py",
    "chars": 3526,
    "preview": "import threading\n\nimport cv2\n\n\nclass Camera(object):\n    \"\"\"\n    Base Camera object\n    \"\"\"\n\n    def __init__(self):\n   "
  },
  {
    "path": "color_tracker/utils/camera/web_camera.py",
    "chars": 1173,
    "preview": "import cv2\n\nfrom color_tracker.utils.camera.base_camera import Camera\n\n\nclass WebCamera(Camera):\n    \"\"\"\n    Simple Webc"
  },
  {
    "path": "color_tracker/utils/color_range_detector.py",
    "chars": 4868,
    "preview": "import cv2\nimport numpy as np\n\nfrom color_tracker.utils import helpers\nfrom color_tracker.utils.camera import Camera\n\n\nc"
  },
  {
    "path": "color_tracker/utils/helpers.py",
    "chars": 4564,
    "preview": "from typing import List, Tuple, Union\n\nimport cv2\nimport numpy as np\nfrom scipy import optimize\n\nfrom color_tracker.util"
  },
  {
    "path": "color_tracker/utils/tracker_object.py",
    "chars": 1174,
    "preview": "import collections\n\n\nclass TrackedObject:\n    def __init__(self, id: int, max_nb_of_points: int = None):\n        self._i"
  },
  {
    "path": "color_tracker/utils/visualize.py",
    "chars": 2004,
    "preview": "import colorsys\nimport random\nfrom typing import Tuple\n\nimport cv2\n\nfrom color_tracker.utils.tracker_object import Track"
  },
  {
    "path": "examples/hsv_color_detector.py",
    "chars": 491,
    "preview": "import color_tracker\n\n# Init camera\ncam = color_tracker.WebCamera(video_src=0)\ncam.start_camera()\n\n# Init Range detector"
  },
  {
    "path": "examples/tracking.py",
    "chars": 2306,
    "preview": "import argparse\nfrom functools import partial\n\nimport cv2\n\nimport color_tracker\n\n# You can determine these values with t"
  },
  {
    "path": "requirements.txt",
    "chars": 34,
    "preview": "opencv-python\nnumpy\nscipy\nimageio\n"
  },
  {
    "path": "setup.py",
    "chars": 1535,
    "preview": "\"\"\"\n*****************************************************\n*               Color Tracker\n*\n*              Gabor Vecsei\n* "
  }
]

About this extraction

This page contains the full source code of the gaborvecsei/Color-Tracker GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (35.0 KB), approximately 9.0k tokens, and a symbol index with 69 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!