Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
max-line-length = 100
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude = .git,__pycache__,.venv,venv,.tox,build,dist,*.egg-info,docs/_build
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]
python-version: [3.11]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -21,6 +21,6 @@ jobs:
pip install -r requirements/requirements-dev.txt
- name: Check code format and style
run: |
isort **/*.py -c -v
black --check colordetect/ tests/
flake8 colordetect/ tests/
isort src/colordetect/ tests/ docs/conf.py -c -v
black --check src/colordetect/ tests/
flake8 src/colordetect/ tests/
14 changes: 11 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
ColorDetect Changelog
=====================

.. _1.6.6:
1.6.6 (28-08-2026)
==================
Feat
---------

- Migration to recommended packaging

.. _1.6.5:
1.6.5 (28-02-2026)
==================
Expand Down Expand Up @@ -53,7 +61,7 @@ Fix
Features
---------

- Perform color recognition on a video at a specific time
- Perform color recognition on a video at a specific time
- Extract image from video at a specific time


Expand All @@ -65,12 +73,12 @@ Docs
----

- Update contribution readme with pre-commit configuration.

Fix
---

- Linting of code

.. _1.5.0:
1.5.0 (11-08-2021)
=======================
Expand Down
Empty file removed __init__.py
Empty file.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
author = "Marvin Kweyu"

# The full version, including alpha/beta/rc tags
release = "1.6.5"
release = "1.6.6"

# -- General configuration ---------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/videocolor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ We could, **alternatively**, handle the saving ourselves and go as below:


>>> image.write_color_count(font_color=(255,255,255))
>>> image.save_image(location='path/to/directory/of/choice', filename='filenameofchoice.jpg')
>>> image.save_image(location='path/to/directory/of/choice', file_name='filenameofchoice.jpg')



Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
requires = ["uv_build"]
build-backend = "uv_build"

[project]
name = "ColorDetect"
version = "1.6.5"
version = "1.6.6"
authors = [
{name = "Marvin Kweyu", email = "hello@marvinkweyu.net"},
]
Expand Down Expand Up @@ -37,5 +37,5 @@ dev = [
"pre-commit",
]

[tool.setuptools.packages.find]
where = ["."]
[tool.pytest.ini_options]
pythonpath = ["src"]
File renamed without changes.
File renamed without changes.
23 changes: 15 additions & 8 deletions colordetect/color_detect.py → src/colordetect/color_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ def get_segmented_image(

segmented = cv2.bitwise_and(self.image_original, self.image_original, mask=mask)

for i in range(len(mask)):
for j in range(len(mask[i])):
if mask[i][j] != 0:
output_image[i][j] = self.image_original[i][j]
output_image[mask != 0] = self.image_original[mask != 0]

return output_image, gray, segmented, mask

def get_color_count(
self, color_count: int = 5, color_format: str = "human_readable"
self,
color_count: int = 5,
color_format: str = "human_readable",
max_pixels: int = 10_000,
) -> dict:
"""
.. _get_color_count:
Expand All @@ -188,6 +188,10 @@ def get_color_count(
* rgb - rgb(255, 255, 0) for yellow
* hex - #FFFF00 for yellow
* human_readable - yellow for yellow
max_pixels: int
Maximum number of pixels sampled for clustering. Reduces memory and
CPU usage on large images with negligible impact on color accuracy.
Set to None to disable downsampling.
:return: color description
"""

Expand All @@ -198,8 +202,11 @@ def get_color_count(

# convert image from BGR to RGB for better accuracy
rgb = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
reshape = rgb.reshape((rgb.shape[0] * rgb.shape[1], 3))
cluster = KMeans(n_clusters=color_count).fit(reshape)
reshape = rgb.reshape((-1, 3))
if max_pixels is not None and len(reshape) > max_pixels:
indices = np.random.choice(len(reshape), max_pixels, replace=False)
reshape = reshape[indices]
cluster = KMeans(n_clusters=color_count, n_init=10).fit(reshape)

unique_colors = self._find_unique_colors(cluster, cluster.cluster_centers_)

Expand Down Expand Up @@ -252,7 +259,7 @@ def _find_unique_colors(self, cluster, centroids) -> dict:

# Get the number of different clusters, create histogram, and normalize
labels = np.arange(0, len(np.unique(cluster.labels_)) + 1)
(hist, _) = np.histogram(cluster.labels_, bins=labels)
hist, _ = np.histogram(cluster.labels_, bins=labels)
hist = hist.astype("float")
hist /= hist.sum()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
>>> from colordetect import VideoColor
>>> user_video = VideoColor("<path_to_video>")
# where frame_color_count is the target most dominant colors to be found. Default set to 5
>>> colors = user_video.get_video_frames(frame_color_count=7)
>>> colors = user_video.get_video_frames(frame_color_count=7)
>>> colors
# alternatively shorten the dictionary to get a specific number of sorted colors from the whole lot
# shorten the result to the N most dominant colors across the whole video
>>> from colordetect import col_share
>>> top_colors = col_share.sort_order(object_description=colors, key_count=8)
"""

import datetime
import sys

import cv2

from . import col_share
Expand All @@ -31,7 +28,6 @@ class VideoColor:
"""

def __init__(self, video):
# super().__init__(video)
self.video_file = cv2.VideoCapture(video)
self.color_description = {}

Expand All @@ -51,12 +47,14 @@ def get_video_frames(
----------
frame_color_count: int
The number of most dominant colors to be obtained from a single frame
color_format:str
color_format: str
The format to return the color in.
Options
* hsv - (60°,100%,100%)
* rgb - rgb(255, 255, 0) for yellow
* hex - #FFFF00 for yellow
progress: bool
Show a progress bar during processing. Default False.
:return: color_description dictionary
"""
if not isinstance(frame_color_count, int):
Expand All @@ -74,30 +72,29 @@ def get_video_frames(

count = 0
total_frame_count = self.video_file.get(cv2.CAP_PROP_FRAME_COUNT)
while self.video_file.isOpened():
# how often to extract colors from video frames. Defaults to every 1 second
(success, image) = self._get_frame(time=count * 1000)
if not success:
break # Video is complete
image_object = ColorDetect(image)
colors = image_object.get_color_count(
color_count=frame_color_count, color_format=color_format
)
# merge dictionaries as they are created
self.color_description = {**self.color_description, **colors}
count += 1
current_frame_num = self.video_file.get(cv2.CAP_PROP_POS_FRAMES)
try:
while self.video_file.isOpened():
success, image = self._get_frame(time=count * 1000)
if not success:
break
colors = ColorDetect(image).get_color_count(
color_count=frame_color_count, color_format=color_format
)
self.color_description.update(colors)
count += 1
if progress:
col_share.progress_bar(
position=self.video_file.get(cv2.CAP_PROP_POS_FRAMES),
total_length=total_frame_count,
)
if progress:
# Flush bar to 100% for any trailing millis skipped at the end
col_share.progress_bar(
position=current_frame_num, total_length=total_frame_count
position=total_frame_count, total_length=total_frame_count
)
if progress:
col_share.progress_bar(
position=total_frame_count, total_length=total_frame_count
) # Cater for video with extra millis at the end that don't sum upto a full sec, and are thus skipped
finally:
self.video_file.release()

self.video_file.release()
print("\n")
return self.color_description

def _get_frame(self, time: int = 1000) -> tuple:
Expand All @@ -112,13 +109,10 @@ def _get_frame(self, time: int = 1000) -> tuple:
time: int
Time to get color from in parsed image

:return: ()
:return: (success, image)
"""
# read file every x time in milliseconds
self.video_file.set(cv2.CAP_PROP_POS_MSEC, time)
success, image = self.video_file.read()

return (success, image)
return self.video_file.read()

def get_time_frame_color(
self, color_count: int = 5, color_format: str = "rgb", time: int = 1000
Expand All @@ -136,7 +130,7 @@ def get_time_frame_color(
Time to get color from in video in milliseconds
color_count: int
Number of colors to return at the given time frame
color_format:str
color_format: str
The format to return the color in.
Options
* hsv - (60°,100%,100%)
Expand All @@ -157,23 +151,23 @@ def get_time_frame_color(
if time < 1:
raise ValueError("Cannot give negative time to extract color from")

if self._get_video_length() < time:
video_length = self._get_video_length()
if video_length < time:
raise ValueError(
f"The time given is longer than the video parsed. Provided {time} while length of video: {self._get_video_length()}"
f"The time given is longer than the video parsed. Provided {time} while length of video: {video_length}"
)

(success, image) = self._get_frame(time)
if success:
image_object = ColorDetect(image)
colors = image_object.get_color_count(
color_count=color_count, color_format=color_format
)

self.color_description = colors

self.video_file.release()
try:
success, image = self._get_frame(time)
if success:
image_object = ColorDetect(image)
self.color_description = image_object.get_color_count(
color_count=color_count, color_format=color_format
)
finally:
self.video_file.release()

return (image_object, self.color_description)
return image_object, self.color_description

def _get_video_length(self) -> int:
"""
Expand All @@ -182,7 +176,7 @@ def _get_video_length(self) -> int:
----------------
get the length of a video

return: the length of a video
return: the length of a video in milliseconds
"""
frames = self.video_file.get(cv2.CAP_PROP_FRAME_COUNT)
fps = self.video_file.get(cv2.CAP_PROP_FPS)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import matplotlib.colors as mcolors

from ..colordetect import ColorDetect
from colordetect import ColorDetect


def test_existence_of_image_vid_path(image, video):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest

from ..colordetect import ColorDetect, VideoColor, col_share
from colordetect import ColorDetect, VideoColor, col_share


def test_image_vid_parsed_to_class(image, video):
Expand Down Expand Up @@ -177,7 +177,7 @@ def test_get_time_frame_color_returns_video_colors_at_given_time(video):
"""
user_video = VideoColor(video)

(image, colors_at_time) = user_video.get_time_frame_color(time=10000)
image, colors_at_time = user_video.get_time_frame_color(time=10000)
assert isinstance(image, ColorDetect)
assert isinstance(colors_at_time, dict)

Expand Down
Loading