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