calango 2.2.7__tar.gz → 2.2.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {calango-2.2.7 → calango-2.2.9}/PKG-INFO +13 -2
- {calango-2.2.7 → calango-2.2.9}/calango/__init__.py +3 -1
- {calango-2.2.7 → calango-2.2.9}/calango/media.py +259 -2
- {calango-2.2.7 → calango-2.2.9}/calango.egg-info/PKG-INFO +13 -2
- {calango-2.2.7 → calango-2.2.9}/calango.egg-info/requires.txt +1 -0
- {calango-2.2.7 → calango-2.2.9}/LICENSE +0 -0
- {calango-2.2.7 → calango-2.2.9}/README.md +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango/devices.py +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango/settings.py +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango/utils.py +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango.egg-info/SOURCES.txt +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango.egg-info/dependency_links.txt +0 -0
- {calango-2.2.7 → calango-2.2.9}/calango.egg-info/top_level.txt +0 -0
- {calango-2.2.7 → calango-2.2.9}/setup.cfg +0 -0
- {calango-2.2.7 → calango-2.2.9}/setup.py +0 -0
- {calango-2.2.7 → calango-2.2.9}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: calango
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.9
|
|
4
4
|
Summary: It looks like calango
|
|
5
5
|
Home-page: https://github.com/cereja-project/calango
|
|
6
6
|
Author: Joab Leite
|
|
@@ -15,6 +15,17 @@ Requires-Dist: cereja
|
|
|
15
15
|
Requires-Dist: opencv-python
|
|
16
16
|
Requires-Dist: matplotlib
|
|
17
17
|
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: scikit-learn
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
18
29
|
|
|
19
30
|
# Calango Project
|
|
20
31
|
|
|
@@ -6,13 +6,14 @@ import subprocess
|
|
|
6
6
|
import threading
|
|
7
7
|
import time
|
|
8
8
|
from abc import abstractmethod
|
|
9
|
-
from typing import Union, Tuple, Sequence, Iterator
|
|
9
|
+
from typing import Union, Tuple, Sequence, Iterator, List
|
|
10
10
|
from urllib.parse import urlparse
|
|
11
11
|
import cereja as cj
|
|
12
12
|
import cv2
|
|
13
13
|
import numpy as np
|
|
14
14
|
import logging
|
|
15
15
|
from matplotlib import pyplot as plt
|
|
16
|
+
from sklearn.cluster import KMeans
|
|
16
17
|
|
|
17
18
|
from .devices import Mouse
|
|
18
19
|
from .settings import ON_COLAB_JUPYTER
|
|
@@ -33,6 +34,14 @@ def is_url(val):
|
|
|
33
34
|
class Image(np.ndarray):
|
|
34
35
|
_GRAY_SCALE = 'GRAY_SCALE'
|
|
35
36
|
_color_mode = 'BGR'
|
|
37
|
+
COLORS_RANGE = {
|
|
38
|
+
'red': (np.array([0, 0, 100]), np.array([80, 80, 255])),
|
|
39
|
+
'green': (np.array([0, 100, 0]), np.array([80, 255, 80])),
|
|
40
|
+
'blue': (np.array([100, 0, 0]), np.array([255, 80, 80])),
|
|
41
|
+
'yellow': (np.array([0, 100, 100]), np.array([80, 255, 255])),
|
|
42
|
+
'cyan': (np.array([100, 100, 0]), np.array([255, 255, 80])),
|
|
43
|
+
'magenta': (np.array([100, 0, 100]), np.array([255, 80, 255])),
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
def __new__(cls, im: Union[str, np.ndarray] = None, color_mode: str = 'BGR', shape=None, dtype=None,
|
|
38
47
|
**kwargs) -> 'Image':
|
|
@@ -99,6 +108,129 @@ class Image(np.ndarray):
|
|
|
99
108
|
def set_border(self, size: int = 1, color: Tuple[int, int, int] = (0, 255, 0)):
|
|
100
109
|
cv2.rectangle(self, (size, size), (self.width - 1, self.height - 1), color, size)
|
|
101
110
|
|
|
111
|
+
def color_range(self, color: str):
|
|
112
|
+
assert color in self.COLORS_RANGE, f'Color {color} is not valid.'
|
|
113
|
+
return self.COLORS_RANGE[color]
|
|
114
|
+
|
|
115
|
+
def get_color_mask(self, lower, upper):
|
|
116
|
+
return cv2.inRange(self, lower, upper)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def calculate_color_percentage(img, lower_bgr, upper_bgr):
|
|
120
|
+
|
|
121
|
+
# Create a mask with the pixels that fall within the color range
|
|
122
|
+
color_mask = cv2.inRange(img, lower_bgr, upper_bgr)
|
|
123
|
+
|
|
124
|
+
# Count the number of pixels in the color range
|
|
125
|
+
color_pixel_count = np.count_nonzero(color_mask)
|
|
126
|
+
|
|
127
|
+
# Count the total number of pixels in the image
|
|
128
|
+
total_pixels = img.shape[0] * img.shape[1]
|
|
129
|
+
|
|
130
|
+
# Calculate the percentage of the color
|
|
131
|
+
color_percentage = (color_pixel_count / total_pixels) * 100
|
|
132
|
+
return color_percentage
|
|
133
|
+
|
|
134
|
+
def get_strides(self, kernel_size, strides=1):
|
|
135
|
+
"""
|
|
136
|
+
Returns batches of fixed window size (kernel_size) with a given stride
|
|
137
|
+
@param kernel_size: window size
|
|
138
|
+
@param strides: default is 1
|
|
139
|
+
"""
|
|
140
|
+
for i in range(0, self.shape[0] - kernel_size + 1, strides):
|
|
141
|
+
for j in range(0, self.shape[1] - kernel_size + 1, strides):
|
|
142
|
+
yield self[i:i + kernel_size, j:j + kernel_size]
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def blue_percentage(self):
|
|
146
|
+
return self.calculate_color_percentage(self, *self.color_range('blue'))
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def green_percentage(self):
|
|
150
|
+
return self.calculate_color_percentage(self, *self.color_range('green'))
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def red_percentage(self):
|
|
154
|
+
return self.calculate_color_percentage(self, *self.color_range('red'))
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def yellow_percentage(self):
|
|
158
|
+
return self.calculate_color_percentage(self, *self.color_range('yellow'))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def cyan_percentage(self):
|
|
162
|
+
return self.calculate_color_percentage(self, *self.color_range('cyan'))
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def magenta_percentage(self):
|
|
166
|
+
return self.calculate_color_percentage(self, *self.color_range('magenta'))
|
|
167
|
+
|
|
168
|
+
def hex_to_bgr(self, hex_color):
|
|
169
|
+
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
|
|
170
|
+
|
|
171
|
+
def bgr_to_hex(self, bgr_color):
|
|
172
|
+
return '%02x%02x%02x' % bgr_color
|
|
173
|
+
|
|
174
|
+
def parse_color(self, color):
|
|
175
|
+
if isinstance(color, str):
|
|
176
|
+
if color.startswith('#'):
|
|
177
|
+
return self.hex_to_bgr(color[1:])
|
|
178
|
+
return self.hex_to_bgr(color)
|
|
179
|
+
return color
|
|
180
|
+
|
|
181
|
+
def get_color_percentage(self, min_color, max_color):
|
|
182
|
+
return self.calculate_color_percentage(self, self.parse_color(min_color), self.parse_color(max_color))
|
|
183
|
+
|
|
184
|
+
def similarity(self, image: Image, method=cv2.TM_CCOEFF_NORMED):
|
|
185
|
+
result = cv2.matchTemplate(self, image, method)
|
|
186
|
+
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
|
187
|
+
return max_val
|
|
188
|
+
|
|
189
|
+
def _padding_points(self, points, distance: 0):
|
|
190
|
+
if not len(points):
|
|
191
|
+
return []
|
|
192
|
+
points = cj.geolinear.find_best_locations(points, distance)
|
|
193
|
+
return points
|
|
194
|
+
|
|
195
|
+
def _match_template(self, template: np.ndarray, method=cv2.TM_CCOEFF_NORMED, threshold=None, all_locations=False,
|
|
196
|
+
distance_between_points=1):
|
|
197
|
+
threshold = threshold or 0.0
|
|
198
|
+
result = []
|
|
199
|
+
res = cv2.matchTemplate(self, template, method)
|
|
200
|
+
if all_locations:
|
|
201
|
+
loc = np.where(res >= threshold)
|
|
202
|
+
for n, pt in enumerate(zip(*loc[::-1])):
|
|
203
|
+
acc = res[pt[1], pt[0]]
|
|
204
|
+
result.append([int(pt[0] + template.shape[1] // 2), int(pt[1] + template.shape[0] // 2), acc])
|
|
205
|
+
else:
|
|
206
|
+
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
|
207
|
+
if max_val >= threshold:
|
|
208
|
+
result.append(
|
|
209
|
+
[int(max_loc[0] + template.shape[1] // 2), int(max_loc[1] + template.shape[0] // 2), max_val])
|
|
210
|
+
|
|
211
|
+
if distance_between_points > 1:
|
|
212
|
+
result = self._padding_points(result, distance_between_points)
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
def match_template(self, template: Union[np.ndarray, List[np.ndarray]], method=cv2.TM_CCOEFF_NORMED,
|
|
216
|
+
threshold=None, all_locations=False, distance_between_points=1):
|
|
217
|
+
threshold = threshold or 0.0
|
|
218
|
+
|
|
219
|
+
result = []
|
|
220
|
+
if not isinstance(template, list):
|
|
221
|
+
template = [template]
|
|
222
|
+
for template in template:
|
|
223
|
+
res = self._match_template(template, method, threshold, all_locations=all_locations,
|
|
224
|
+
distance_between_points=distance_between_points)
|
|
225
|
+
result.extend(res)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
def find_locations_by_color(self, color, distance_between_points=1):
|
|
229
|
+
|
|
230
|
+
mask = self.get_color_mask(*self.color_range(color))
|
|
231
|
+
positions = np.where(mask == 255)[::-1]
|
|
232
|
+
return self._padding_points(list(zip(*positions)), distance_between_points)
|
|
233
|
+
|
|
102
234
|
@property
|
|
103
235
|
def mask(self):
|
|
104
236
|
return Image(np.zeros(self.shape[:2], dtype=np.uint8))
|
|
@@ -295,6 +427,14 @@ class Image(np.ndarray):
|
|
|
295
427
|
180: cv2.ROTATE_180
|
|
296
428
|
}.get(degrees))
|
|
297
429
|
|
|
430
|
+
def put_on_center(self, img):
|
|
431
|
+
img = Image(img)
|
|
432
|
+
x, y = self.center_position
|
|
433
|
+
x1 = x - img.width // 2
|
|
434
|
+
y1 = y - img.height // 2
|
|
435
|
+
self[y1:y1 + img.height, x1: x1 + img.width] = img
|
|
436
|
+
return self
|
|
437
|
+
|
|
298
438
|
def crop_by_center(self, size=None, keep_scale=False) -> Image:
|
|
299
439
|
assert size is None or isinstance(size, (list, tuple)) and cj.is_numeric_sequence(size) and len(
|
|
300
440
|
size) == 2, 'Send HxW image cropped output'
|
|
@@ -345,6 +485,10 @@ class Image(np.ndarray):
|
|
|
345
485
|
cv2.circle(self, position, radius, color, thickness)
|
|
346
486
|
return self
|
|
347
487
|
|
|
488
|
+
def rect(self, start, end, color=(0, 0, 0), thickness=1):
|
|
489
|
+
cv2.rectangle(self, start, end, color, thickness)
|
|
490
|
+
return self
|
|
491
|
+
|
|
348
492
|
def get_mask_circle(self, radius=None, position=None):
|
|
349
493
|
return self.mask.circle(radius=radius, position=position, color=(255, 255, 255), thickness=-1)
|
|
350
494
|
|
|
@@ -377,7 +521,43 @@ class Image(np.ndarray):
|
|
|
377
521
|
def save(self, p: str):
|
|
378
522
|
cv2.imwrite(p, self)
|
|
379
523
|
assert cj.Path(p).exists, f'Error saving image {p}'
|
|
524
|
+
def get_background_foreground_colors(self, k=2):
|
|
525
|
+
"""
|
|
526
|
+
Função para determinar as cores RGB do background e do foreground em uma imagem.
|
|
527
|
+
|
|
528
|
+
Parâmetros:
|
|
529
|
+
image (np.array): imagem em formato de array numpy.
|
|
530
|
+
k (int): número de clusters para segmentação de cores (2 significa fundo e primeiro plano).
|
|
531
|
+
|
|
532
|
+
Retorna:
|
|
533
|
+
dict: dicionário com as cores RGB do background e do foreground.
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
# Redimensiona a imagem para acelerar o processamento
|
|
537
|
+
resize_dim = (300, 300)
|
|
538
|
+
image_resized = cv2.resize(self, resize_dim)
|
|
539
|
+
image_rgb = cv2.cvtColor(image_resized, cv2.COLOR_BGR2RGB)
|
|
540
|
+
|
|
541
|
+
# Converte a imagem para um array de pixels
|
|
542
|
+
pixels = image_rgb.reshape(-1, 3)
|
|
543
|
+
|
|
544
|
+
# Aplica KMeans para segmentar em duas cores (background e foreground)
|
|
545
|
+
kmeans = KMeans(n_clusters=k, random_state=0)
|
|
546
|
+
kmeans.fit(pixels)
|
|
547
|
+
|
|
548
|
+
# Obtém as cores dos clusters
|
|
549
|
+
cluster_colors = kmeans.cluster_centers_
|
|
550
|
+
cluster_labels, counts = np.unique(kmeans.labels_, return_counts=True)
|
|
551
|
+
|
|
552
|
+
# Identifica o background como a cor com mais pixels
|
|
553
|
+
background_idx = cluster_labels[np.argmax(counts)]
|
|
554
|
+
foreground_idx = cluster_labels[np.argmin(counts)]
|
|
555
|
+
|
|
556
|
+
# Converte as cores para inteiros RGB
|
|
557
|
+
background_rgb = tuple(map(int, cluster_colors[background_idx]))
|
|
558
|
+
foreground_rgb = tuple(map(int, cluster_colors[foreground_idx]))
|
|
380
559
|
|
|
560
|
+
return {"background_rgb": background_rgb, "foreground_rgb": foreground_rgb}
|
|
381
561
|
def plot_colors_histogram(self):
|
|
382
562
|
# tuple to select colors of each channel line
|
|
383
563
|
colors = ("red", "green", "blue") if self._color_mode == 'RGB' else ('blue', 'green', 'red')
|
|
@@ -504,6 +684,24 @@ class Image(np.ndarray):
|
|
|
504
684
|
dir_path = cj.Path(dir_path)
|
|
505
685
|
return [cls(im_p.path) for im_p in dir_path.list_files(ext=ext)]
|
|
506
686
|
|
|
687
|
+
@classmethod
|
|
688
|
+
def _get_image_from_window(cls):
|
|
689
|
+
with cj.TempDir() as tmp:
|
|
690
|
+
while True:
|
|
691
|
+
window = yield
|
|
692
|
+
p = tmp.path.join('frame.bmp')
|
|
693
|
+
window.capture_image_bmp(p.path)
|
|
694
|
+
yield Image(p.path)
|
|
695
|
+
|
|
696
|
+
@classmethod
|
|
697
|
+
def from_window(cls, window: cj.Window) -> 'Image':
|
|
698
|
+
"""
|
|
699
|
+
Get image from window
|
|
700
|
+
"""
|
|
701
|
+
gen = cls._get_image_from_window()
|
|
702
|
+
next(gen)
|
|
703
|
+
return gen.send(window)
|
|
704
|
+
|
|
507
705
|
|
|
508
706
|
class VideoWriter:
|
|
509
707
|
def __init__(self, p, fourcc=None, width=None, height=None, fps=30):
|
|
@@ -814,6 +1012,63 @@ class Screen(_IVideo):
|
|
|
814
1012
|
self._capture = False
|
|
815
1013
|
|
|
816
1014
|
|
|
1015
|
+
class WindowStream(_IVideo):
|
|
1016
|
+
|
|
1017
|
+
def __init__(self, window: cj.Window, *args, fps=None, **kwargs):
|
|
1018
|
+
self._window = window
|
|
1019
|
+
self._fps = fps or 30
|
|
1020
|
+
self._total_frames = -1
|
|
1021
|
+
self._capture = True
|
|
1022
|
+
self._frames = self.__get_frame()
|
|
1023
|
+
self._width, self._height = self._window.size
|
|
1024
|
+
|
|
1025
|
+
def set_fps(self, fps: Union[int, float]) -> None:
|
|
1026
|
+
assert isinstance(fps, (int, float)), ValueError(f'{fps} value for fps is not valid. Send int or float.')
|
|
1027
|
+
self._fps = fps
|
|
1028
|
+
|
|
1029
|
+
@property
|
|
1030
|
+
def next_frame(self) -> Tuple[bool, Union[np.ndarray, None]]:
|
|
1031
|
+
return True, next(self._frames)
|
|
1032
|
+
|
|
1033
|
+
def __get_frame(self):
|
|
1034
|
+
with cj.TempDir() as tmp:
|
|
1035
|
+
while self._capture:
|
|
1036
|
+
p = tmp.path.join('frame.bmp')
|
|
1037
|
+
self._window.capture_image_bmp(p.path)
|
|
1038
|
+
yield Image(p.path)
|
|
1039
|
+
|
|
1040
|
+
@property
|
|
1041
|
+
def width(self) -> int:
|
|
1042
|
+
return self._window.size[0]
|
|
1043
|
+
|
|
1044
|
+
@property
|
|
1045
|
+
def height(self) -> int:
|
|
1046
|
+
return self._window.size[1]
|
|
1047
|
+
|
|
1048
|
+
@property
|
|
1049
|
+
def total_frames(self) -> int:
|
|
1050
|
+
return self._total_frames
|
|
1051
|
+
|
|
1052
|
+
@property
|
|
1053
|
+
def fps(self) -> Union[int, float]:
|
|
1054
|
+
return self._fps
|
|
1055
|
+
|
|
1056
|
+
@property
|
|
1057
|
+
def is_webcam(self) -> bool:
|
|
1058
|
+
return True
|
|
1059
|
+
|
|
1060
|
+
@property
|
|
1061
|
+
def is_stream(self) -> bool:
|
|
1062
|
+
return False
|
|
1063
|
+
|
|
1064
|
+
@property
|
|
1065
|
+
def is_opened(self) -> bool:
|
|
1066
|
+
return self._capture
|
|
1067
|
+
|
|
1068
|
+
def stop(self):
|
|
1069
|
+
self._capture = False
|
|
1070
|
+
|
|
1071
|
+
|
|
817
1072
|
class Video:
|
|
818
1073
|
|
|
819
1074
|
def __init__(self, *args, fps=None, frame_preprocess=None, **kwargs):
|
|
@@ -833,7 +1088,9 @@ class Video:
|
|
|
833
1088
|
|
|
834
1089
|
def _build(self):
|
|
835
1090
|
if len(self._args):
|
|
836
|
-
if isinstance(self._args[0],
|
|
1091
|
+
if isinstance(self._args[0], cj.Window):
|
|
1092
|
+
self._cap = WindowStream(window=self._args[0], *self._args[1:], **self._kwargs)
|
|
1093
|
+
elif isinstance(self._args[0], str):
|
|
837
1094
|
if self._args[0] == 'monitor':
|
|
838
1095
|
self._cap = Screen()
|
|
839
1096
|
elif cj.request.is_url(self._args[0]):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: calango
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.9
|
|
4
4
|
Summary: It looks like calango
|
|
5
5
|
Home-page: https://github.com/cereja-project/calango
|
|
6
6
|
Author: Joab Leite
|
|
@@ -15,6 +15,17 @@ Requires-Dist: cereja
|
|
|
15
15
|
Requires-Dist: opencv-python
|
|
16
16
|
Requires-Dist: matplotlib
|
|
17
17
|
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: scikit-learn
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
18
29
|
|
|
19
30
|
# Calango Project
|
|
20
31
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|