treizh commited on
Commit
ea7f9bb
·
verified ·
1 Parent(s): eac7367

Upload scripts/tap_image.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/tap_image.py +2096 -0
scripts/tap_image.py ADDED
@@ -0,0 +1,2096 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import itertools
3
+ from typing import Any
4
+ import math
5
+ import random
6
+ from collections import namedtuple, defaultdict
7
+ import warnings
8
+
9
+ from tqdm import tqdm
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from scipy.spatial.transform import Rotation as R
15
+
16
+ from sklearn.cluster import MeanShift
17
+
18
+ import skimage.feature as feature
19
+ from skimage import color
20
+ from skimage.feature import SIFT, match_descriptors, plot_matches
21
+ from skimage.filters import (
22
+ threshold_otsu,
23
+ threshold_triangle,
24
+ threshold_triangle,
25
+ threshold_isodata,
26
+ threshold_li,
27
+ threshold_yen,
28
+ threshold_mean,
29
+ threshold_minimum,
30
+ threshold_triangle,
31
+ threshold_yen,
32
+ )
33
+
34
+ import cv2
35
+ from PIL import Image
36
+
37
+ import matplotlib.pyplot as plt
38
+
39
+ import scripts.tap_const as tc
40
+
41
+
42
+ def _update_axis(
43
+ axis,
44
+ image,
45
+ title=None,
46
+ fontsize=18,
47
+ remove_axis=True,
48
+ title_loc="center",
49
+ ):
50
+ axis.imshow(image, origin="upper")
51
+ if title is not None:
52
+ axis.set_title(title, fontsize=fontsize, loc=title_loc)
53
+ if remove_axis is True:
54
+ axis.set_axis_off()
55
+
56
+
57
+ Circle = namedtuple("Circle", field_names="x,y,r")
58
+
59
+ font = cv2.FONT_HERSHEY_SIMPLEX
60
+
61
+ color_spaces = {
62
+ "rgb": ["red", "green", "blue"],
63
+ "hsv": ["h", "s", "v"],
64
+ "yiq": ["y", "i", "q"],
65
+ "lab": ["l", "a", "b"],
66
+ }
67
+ available_threholds = ["otsu", "triangle", "isodata", "li", "mean", "minimum", "yen"]
68
+
69
+
70
+ def bgr_to_rgb(clr: tuple) -> tuple:
71
+ """Converts from BGR to RGB
72
+
73
+ Arguments:
74
+ clr {tuple} -- Source color
75
+
76
+ Returns:
77
+ tuple -- Converted color
78
+ """
79
+ return (clr[2], clr[1], clr[0])
80
+
81
+
82
+ def rgb_to_bgr(clr: tuple) -> tuple:
83
+ """Converts from RGB to BGR
84
+
85
+ Arguments:
86
+ clr {tuple} -- Source color
87
+
88
+ Returns:
89
+ tuple -- Converted color
90
+ """
91
+ return (clr[0], clr[1], clr[2])
92
+
93
+
94
+ def load_image(file_name, file_path=None, rgb: bool = True, image_size: int = None):
95
+ path = (
96
+ file_name
97
+ if isinstance(file_name, Path) is True and file_name.is_file() is True
98
+ else file_path.joinpath(file_name)
99
+ )
100
+
101
+ try:
102
+ image = cv2.imread(str(path))
103
+ if rgb is True:
104
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
105
+ if image_size is not None:
106
+ image = cv2.resize(
107
+ image, dsize=(image_size, image_size), interpolation=cv2.INTER_LANCZOS4
108
+ )
109
+ return image
110
+ except Exception as e:
111
+ print(file_name)
112
+ print(f"Failed load imge: {str(e)}")
113
+ return None
114
+
115
+
116
+ def to_pil(image, size: tuple = None):
117
+ if image is None:
118
+ return None
119
+ ret = Image.fromarray(image)
120
+ if size is not None:
121
+ ret = ret.resize(size=size, resample=Image.Resampling.LANCZOS)
122
+ return ret
123
+
124
+
125
+ def to_cv(image, size: tuple = None):
126
+ if size is not None:
127
+ image = image.resize(size=size, resample=Image.Resampling.LANCZOS)
128
+ return np.array(image)
129
+
130
+
131
+ def rgb2X(rgb_color, target_color_space):
132
+ if isinstance(rgb_color, np.ndarray) is False:
133
+ rgb_color = np.array(rgb_color)
134
+ if target_color_space == "hsv":
135
+ return color.rgb2hsv(rgb_color)
136
+ elif target_color_space == "yiq":
137
+ return color.rgb2yiq(rgb_color)
138
+ elif target_color_space == "lab":
139
+ return color.rgb2lab(rgb_color)
140
+ else:
141
+ return rgb_color
142
+
143
+
144
+ def get_channels(image, color_space):
145
+ """Get all channels from a colorspace
146
+
147
+ Args:
148
+ image (np.ndarray): Source RGB image
149
+ color_space (str): colorspace
150
+
151
+ Raises:
152
+ NotImplementedError: Unknown color space
153
+
154
+ Returns:
155
+ tuple: channels
156
+ """
157
+ if color_space == "rgb":
158
+ return cv2.split(image)
159
+ elif color_space == "hsv":
160
+ return cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HSV))
161
+ elif color_space == "yiq":
162
+ return [
163
+ ((c - np.min(c)) / (np.max(c) - np.min(c)) * 255).astype(np.uint8)
164
+ for c in cv2.split(to_cv(color.rgb2yiq(to_pil(image))))
165
+ ]
166
+ elif color_space == "lab":
167
+ return cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2LAB))
168
+ else:
169
+ raise NotImplementedError(f"Unknowncolorspace {color_space}")
170
+
171
+
172
+ def get_channel(image, color_space, channel):
173
+ channels = get_channels(image=image, color_space=color_space)
174
+ if channel in ["red", "h", "y", "l"]:
175
+ return channels[0]
176
+ if channel in ["green", "s", "i", "a"]:
177
+ return channels[1]
178
+ if channel in ["blue", "v", "q", "b"]:
179
+ return channels[2]
180
+ else:
181
+ raise NotImplementedError(
182
+ f"Unknown combination color space {color_space}, channel {channel}"
183
+ )
184
+
185
+
186
+ def multi_and(image_list: tuple):
187
+ """Performs an AND with all the images in the tuple
188
+
189
+ :param image_list:
190
+ :return: image
191
+ """
192
+ img_lst = [i for i in image_list if i is not None]
193
+ list_len_ = len(img_lst)
194
+
195
+ if list_len_ == 0:
196
+ return None
197
+ elif list_len_ == 1:
198
+ return img_lst[0]
199
+ else:
200
+ res = cv2.bitwise_and(img_lst[0], img_lst[1])
201
+ if len(img_lst) > 2:
202
+ for current_image in img_lst[2:]:
203
+ res = cv2.bitwise_and(res, current_image)
204
+
205
+ return res
206
+
207
+
208
+ def multi_or(image_list: tuple):
209
+ """Performs an OR with all the images in the tuple
210
+
211
+ :param image_list:
212
+ :return: image
213
+ """
214
+ img_lst = [i for i in image_list if i is not None]
215
+ list_len_ = len(img_lst)
216
+
217
+ if list_len_ == 0:
218
+ return None
219
+ elif list_len_ == 1:
220
+ return img_lst[0]
221
+ else:
222
+ res = cv2.bitwise_or(img_lst[0], img_lst[1])
223
+ if list_len_ > 2:
224
+ for current_image in img_lst[2:]:
225
+ res = cv2.bitwise_or(res, current_image)
226
+ return res
227
+
228
+
229
+ def get_threshold(image, colorspace, channel, method, is_over: bool):
230
+ channel = get_channels(image=image, color_space=colorspace)[
231
+ color_spaces[colorspace].index(channel)
232
+ ]
233
+ if method == "otsu":
234
+ threshold = threshold_otsu(channel)
235
+ elif method == "triangle":
236
+ threshold = threshold_triangle(channel)
237
+ elif method == "isodata":
238
+ threshold = threshold_isodata(channel)
239
+ elif method == "li":
240
+ threshold = threshold_li(channel)
241
+ elif method == "mean":
242
+ threshold = threshold_mean(channel)
243
+ elif method == "minimum":
244
+ threshold = threshold_minimum(channel)
245
+ elif method == "yen":
246
+ threshold = threshold_yen(channel)
247
+ else:
248
+ raise NotImplementedError(f"Unknown method {method}")
249
+
250
+ return threshold, channel > threshold if is_over is True else channel < threshold
251
+
252
+
253
+ class Rectangle:
254
+ def __init__(self, x, y, w, h, score=None, user="dummy") -> None:
255
+ self.x = int(x)
256
+ self.y = int(y)
257
+ self.w = int(w)
258
+ self.h = int(h)
259
+ self.score = score
260
+ self.user = user
261
+
262
+ def __repr__(self) -> str:
263
+ return f"[{self.x},{self.y}:{self.w},{self.h}]"
264
+
265
+ def __str__(self) -> str:
266
+ return f"[{self.x},{self.y}:{self.w},{self.h}]"
267
+
268
+ def draw(self, image, color=tc.C_WHITE, thickness=2):
269
+ return cv2.rectangle(
270
+ image,
271
+ pt1=(self.x1, self.y1),
272
+ pt2=(self.x2, self.y2),
273
+ color=color,
274
+ thickness=thickness,
275
+ )
276
+
277
+ def draw_as_bbox(self, image, color=tc.C_WHITE, thickness=2):
278
+ return cv2.circle(
279
+ self.draw(image=image, color=color, thickness=thickness),
280
+ center=(self.cx, self.cy),
281
+ radius=5,
282
+ color=tuple(reversed(color)),
283
+ thickness=thickness,
284
+ )
285
+
286
+ def to_dict(self, extended: bool = False):
287
+ out = self.__dict__
288
+ if extended is False:
289
+ return out
290
+ return out | {
291
+ "cx": self.cx,
292
+ "cy": self.cy,
293
+ "x1": self.x1,
294
+ "x2": self.x2,
295
+ "y1": self.y1,
296
+ "y2": self.y2,
297
+ }
298
+
299
+ def assign(self, src):
300
+ self.x = src.x
301
+ self.y = src.y
302
+ self.w = src.w
303
+ self.h = src.h
304
+
305
+ self.user = src.user
306
+ self.score = src.score
307
+
308
+ def union(self, b):
309
+ posX = min(self.x, b.x)
310
+ posY = min(self.y, b.y)
311
+
312
+ res = Rectangle(
313
+ x=posX,
314
+ y=posY,
315
+ w=max(self.right, b.right) - posX,
316
+ h=max(self.bottom, b.bottom) - posY,
317
+ user=self.user,
318
+ )
319
+ return res
320
+
321
+ def intersection(self, b):
322
+ posX = max(self.x, b.x)
323
+ posY = max(self.y, b.y)
324
+
325
+ candidate = Rectangle(
326
+ x=posX,
327
+ y=posY,
328
+ w=min(self.right, b.right) - posX,
329
+ h=min(self.bottom, b.bottom) - posY,
330
+ )
331
+ if candidate.w > 0 and candidate.h > 0:
332
+ return candidate
333
+ return Rectangle(0, 0, 0, 0)
334
+
335
+ def ratio(self, b):
336
+ return self.intersection(b).area / self.union(b).area
337
+
338
+ def contains(self, b, ratio):
339
+ return self.ratio(b) > ratio
340
+
341
+ def to_bbox(self):
342
+ return [self.x, self.y, self.right, self.bottom]
343
+
344
+ @classmethod
345
+ def from_row(cls, row):
346
+ return cls(x=row.x, y=row.y, w=row.w, h=row.h)
347
+
348
+ @classmethod
349
+ def from_bbox(cls, bbox):
350
+ x, y, w, h = bbox
351
+ return cls(x=x, y=y, w=w, h=h)
352
+
353
+ @property
354
+ def bottom(self):
355
+ return self.y + self.h
356
+
357
+ @property
358
+ def right(self):
359
+ return self.x + self.w
360
+
361
+ @property
362
+ def start_row(self):
363
+ return self.y
364
+
365
+ @property
366
+ def end_row(self):
367
+ return self.y + self.h
368
+
369
+ @property
370
+ def start_col(self):
371
+ return self.x
372
+
373
+ @property
374
+ def end_col(self):
375
+ return self.x + self.w
376
+
377
+ @property
378
+ def cx(self):
379
+ return self.x + self.w // 2
380
+
381
+ @property
382
+ def cy(self):
383
+ return self.y + self.h // 2
384
+
385
+ @property
386
+ def x1(self):
387
+ return self.x
388
+
389
+ @property
390
+ def x2(self):
391
+ return self.x + self.w
392
+
393
+ @property
394
+ def y1(self):
395
+ return self.y
396
+
397
+ @property
398
+ def y2(self):
399
+ return self.y + self.h
400
+
401
+ @property
402
+ def area(self):
403
+ return self.w * self.h
404
+
405
+
406
+ def get_brightness(image, bbox: Rectangle = None):
407
+ if bbox is None:
408
+ r, g, b = cv2.split(image)
409
+ else:
410
+ r, g, b = cv2.split(
411
+ image[bbox.start_row : bbox.end_row, bbox.start_col : bbox.end_col]
412
+ )
413
+ return np.sqrt(
414
+ 0.241 * np.power(r.astype(float), 2)
415
+ + 0.691 * np.power(g.astype(float), 2)
416
+ + 0.068 * np.power(b.astype(float), 2)
417
+ ).mean()
418
+
419
+
420
+ def get_sharpness(image):
421
+ return cv2.Laplacian(
422
+ cv2.cvtColor(image, cv2.COLOR_BGR2GRAY),
423
+ cv2.CV_64F,
424
+ ).var()
425
+
426
+
427
+ def masked_image(image, mask, background_alpha: float = 0.8):
428
+ background = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) * background_alpha
429
+ background[background > 255] = 255
430
+ background = cv2.merge([background, background, background]).astype(np.uint8)
431
+ return cv2.bitwise_or(
432
+ cv2.bitwise_and(background, background, mask=255 - mask),
433
+ cv2.bitwise_and(image, image, mask=mask),
434
+ )
435
+
436
+
437
+ def remove_empty_boxes(boxes):
438
+ return boxes.dropna(subset=["x", "y", "w", "h"])
439
+
440
+
441
+ def draw_boxes(image, boxes, thickness=2):
442
+ boxes_ = remove_empty_boxes(boxes=boxes)
443
+ if len(boxes_) > 0:
444
+ for rect in [Rectangle.from_row(row) for _, row in boxes_.iterrows()]:
445
+ image = rect.draw(image=image, color=tc.C_FUCHSIA, thickness=thickness)
446
+ return image
447
+
448
+
449
+ def rotate_image(image, angle):
450
+ (h, w) = image.shape[:2]
451
+ return cv2.warpAffine(
452
+ image, cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0), (w, h)
453
+ )
454
+
455
+
456
+ def fix_brightness(
457
+ image,
458
+ brightness_target=70,
459
+ bbox: Rectangle = Rectangle(10, 10, 100, 100),
460
+ pass_through: bool = False,
461
+ ):
462
+ avg_bright = get_brightness(image=image, bbox=bbox)
463
+
464
+ if pass_through is True:
465
+ return image
466
+ elif avg_bright > brightness_target:
467
+ gamma = brightness_target / avg_bright
468
+ if gamma != 1:
469
+ inv_gamma = 1.0 / gamma
470
+ table = np.array(
471
+ [((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]
472
+ ).astype("uint8")
473
+ return cv2.LUT(src=image, lut=table)
474
+ else:
475
+ return image
476
+ else:
477
+ return cv2.convertScaleAbs(
478
+ src=image,
479
+ alpha=(brightness_target + avg_bright) / (2 * avg_bright),
480
+ beta=(brightness_target - avg_bright) / 2,
481
+ )
482
+
483
+
484
+ def update_data(data, new_data):
485
+ for k, v in new_data.items():
486
+ if k not in data:
487
+ data[k] = []
488
+ data[k].append(v)
489
+
490
+ return data
491
+
492
+
493
+ def add_perceived_bightness_data(image, data):
494
+ b, g, r = cv2.split(image)
495
+ s = np.sqrt(
496
+ 0.241 * np.power(r.astype(float), 2)
497
+ + 0.691 * np.power(g.astype(float), 2)
498
+ + 0.068 * np.power(b.astype(float), 2)
499
+ )
500
+ return update_data(
501
+ data=data,
502
+ new_data={
503
+ "cl_bright_mean": s.mean(),
504
+ "cl_bright_std": np.std(s),
505
+ "cl_bright_min": s.min(),
506
+ "cl_bright_max": s.max(),
507
+ },
508
+ )
509
+
510
+
511
+ def add_hsv_data(image, data):
512
+ h, s, *_ = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HSV))
513
+ return update_data(
514
+ data=data,
515
+ new_data={
516
+ "cl_hue_mean": h.mean(),
517
+ "cl_hue_std": np.std(h),
518
+ "cl_hue_min": h.min(),
519
+ "cl_hue_max": h.max(),
520
+ "cl_sat_mean": s.mean(),
521
+ "cl_sat_std": np.std(s),
522
+ "cl_sat_min": s.min(),
523
+ "cl_sat_max": s.max(),
524
+ },
525
+ )
526
+
527
+
528
+ def add_lab_data(image, data):
529
+ _, a, b = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2LAB))
530
+ return update_data(
531
+ data=data,
532
+ new_data={
533
+ "cl_a_mean": a.mean(),
534
+ "cl_a_std": np.std(a),
535
+ "cl_a_min": a.min(),
536
+ "cl_a_max": a.max(),
537
+ "cl_b_mean": b.mean(),
538
+ "cl_b_std": np.std(b),
539
+ "cl_b_min": b.min(),
540
+ "cl_b_max": b.max(),
541
+ },
542
+ )
543
+
544
+
545
+ def add_yiq_data(image, data):
546
+ _, i, q = get_channels(image=image, color_space="yiq")
547
+ return update_data(
548
+ data=data,
549
+ new_data={
550
+ "cl_i_mean": i.mean(),
551
+ "cl_i_std": np.std(i),
552
+ "cl_i_min": i.min(),
553
+ "cl_i_max": i.max(),
554
+ "cl_q_mean": q.mean(),
555
+ "cl_q_std": np.std(q),
556
+ "cl_q_min": q.min(),
557
+ "cl_q_max": q.max(),
558
+ },
559
+ )
560
+
561
+
562
+ def add_grey_comatrix_data(image, data, distances, angles) -> dict:
563
+ graycom = feature.graycomatrix(
564
+ cv2.cvtColor(image, cv2.COLOR_BGR2GRAY),
565
+ [x[2] for x in distances],
566
+ [x[2] for x in angles],
567
+ levels=256,
568
+ )
569
+
570
+ new_data = {}
571
+ for k, v in dict(
572
+ contrast=np.array(feature.graycoprops(graycom, "contrast")),
573
+ dissimilarity=np.array(feature.graycoprops(graycom, "dissimilarity")),
574
+ homogeneity=np.array(feature.graycoprops(graycom, "homogeneity")),
575
+ energy=np.array(feature.graycoprops(graycom, "energy")),
576
+ correlation=np.array(feature.graycoprops(graycom, "correlation")),
577
+ asm=np.array(feature.graycoprops(graycom, "ASM")),
578
+ ).items():
579
+ for e in itertools.product(distances, angles):
580
+ new_data[f"gp_{k}_{e[0][0]}_{e[1][0]}"] = v[e[0][1]][e[1][1]]
581
+
582
+ return update_data(data=data, new_data=new_data)
583
+
584
+
585
+ def get_sharpness(image):
586
+ return cv2.Laplacian(
587
+ cv2.cvtColor(image, cv2.COLOR_BGR2GRAY),
588
+ cv2.CV_64F,
589
+ ).var()
590
+
591
+
592
+ def add_image_data(
593
+ df,
594
+ file_path_column,
595
+ path_to_images=None,
596
+ distances=[["d1", 0, 1], ["d10", 1, 10]],
597
+ angles=[["a0", 0, 0], ["api4", 1, np.pi / 4]],
598
+ image_size: int = 0,
599
+ ):
600
+ patch_data = defaultdict(list)
601
+ for file_path in tqdm(df[file_path_column], desc="Embedding image data"):
602
+ image = load_image(file_name=file_path, file_path=path_to_images)
603
+ if image_size > 0:
604
+ image = cv2.resize(image, dsize=(image_size, image_size))
605
+ patch_data[file_path_column].append(file_path)
606
+ patch_data = add_perceived_bightness_data(image=image, data=patch_data)
607
+ patch_data = add_hsv_data(image=image, data=patch_data)
608
+ patch_data = add_lab_data(image=image, data=patch_data)
609
+ patch_data = add_yiq_data(image=image, data=patch_data)
610
+ patch_data = add_grey_comatrix_data(
611
+ image=image,
612
+ data=patch_data,
613
+ distances=distances,
614
+ angles=angles,
615
+ )
616
+ patch_data["sharpness"].append(get_sharpness(image))
617
+
618
+ return pd.merge(
619
+ left=df, right=pd.DataFrame(data=patch_data), on=file_path_column, how="left"
620
+ )
621
+
622
+
623
+ def filter_contours(mask, min_contour_size):
624
+ return cv2.bitwise_and(
625
+ cv2.drawContours(
626
+ np.zeros_like(mask),
627
+ [
628
+ c
629
+ for c in cv2.findContours(
630
+ mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_TC89_L1
631
+ )[-2:-1][0]
632
+ if cv2.contourArea(c) > min_contour_size and c.any()
633
+ ],
634
+ -1,
635
+ 255,
636
+ -1,
637
+ ),
638
+ mask,
639
+ )
640
+ return (
641
+ cv2.drawContours(
642
+ mask,
643
+ contours=[
644
+ c
645
+ for c in get_external_contours(mask)
646
+ if cv2.contourArea(c) < min_contour_size
647
+ ],
648
+ contourIdx=-1,
649
+ color=0,
650
+ thickness=-1,
651
+ )
652
+ if min_contour_size > 0
653
+ else mask
654
+ )
655
+
656
+
657
+ def get_raw_mask(
658
+ image,
659
+ thresholds: list,
660
+ min_contour_size: int = 0,
661
+ bitwise_op: str = "and",
662
+ delete_over: int = -1,
663
+ ):
664
+ """Create a mask seed fh,or SAM
665
+
666
+ Args:
667
+ image (np.array): Source image
668
+ thresholds (list): list of thresholds to be applies
669
+ cm_min_contour_size (int): Contours below threshold will be discarded
670
+ """
671
+ mask = thresholds[0].threshold(image)
672
+ for threshold in thresholds[1:]:
673
+ if bitwise_op == "and":
674
+ mask = cv2.bitwise_and(mask, threshold.threshold(image))
675
+ elif bitwise_op == "or":
676
+ mask = cv2.bitwise_or(mask, threshold.threshold(image))
677
+ else:
678
+ raise NotImplementedError
679
+
680
+ if min_contour_size > 0:
681
+ mask = filter_contours(mask, min_contour_size=min_contour_size)
682
+
683
+ if delete_over > 0:
684
+ h, w = mask.shape
685
+ mask = cv2.bitwise_and(
686
+ mask,
687
+ cv2.circle(
688
+ np.zeros_like(mask),
689
+ center=(w // 2, h // 2),
690
+ color=255,
691
+ radius=delete_over,
692
+ thickness=-1,
693
+ ),
694
+ )
695
+
696
+ return mask
697
+
698
+
699
+ def build_distance_map(
700
+ mask, threshold: float = 0.8, demo: bool = False, label: int = 1
701
+ ):
702
+ dist_transform = cv2.distanceTransform(
703
+ mask, cv2.DIST_L2, cv2.DIST_MASK_PRECISE
704
+ ).astype(np.uint8)
705
+ threshold = np.max(dist_transform) * threshold
706
+ ret = dist_transform.copy()
707
+ ret[ret >= threshold] = 255
708
+ ret[ret < threshold] = 0
709
+ dt = (dist_transform.astype(float) / np.max(dist_transform) * 255).astype(np.uint8)
710
+ dt = cv2.equalizeHist(dist_transform)
711
+ if demo is True:
712
+ if label == 1:
713
+ return cv2.merge([np.zeros_like(mask), ret, np.zeros_like(mask)])
714
+ elif label == 0:
715
+ return cv2.merge([ret, np.zeros_like(mask), np.zeros_like(mask)])
716
+ else:
717
+ raise NotImplementedError
718
+ else:
719
+ return ret
720
+
721
+
722
+ def get_contours(mask, retrieve_mode, method):
723
+ return [
724
+ c
725
+ for c in cv2.findContours(mask.copy(), retrieve_mode, method)[-2:-1][0]
726
+ if cv2.contourArea(c, True) < 0
727
+ ]
728
+
729
+
730
+ def get_seeds(
731
+ mask, modes: dict = {"rnd": 10}, label: int = 1, max_seeds: int = 0
732
+ ) -> dict:
733
+ input_points = pd.Series()
734
+ if "rnd" in modes:
735
+ nz = np.count_nonzero(mask)
736
+ if nz > 0:
737
+ input_points = pd.concat(
738
+ [
739
+ input_points,
740
+ pd.Series([list(p[0]) for p in cv2.findNonZero(mask)]).sample(
741
+ n=nz // modes["rnd"]
742
+ ),
743
+ ]
744
+ )
745
+ if "dist" in modes:
746
+ dt = cv2.equalizeHist(
747
+ cv2.distanceTransform(mask, cv2.DIST_L2, cv2.DIST_MASK_PRECISE).astype(
748
+ np.uint8
749
+ )
750
+ )
751
+ nz = np.count_nonzero(dt)
752
+ if nz > 0:
753
+ candidates = cv2.findNonZero(dt)
754
+ input_points = pd.concat(
755
+ [
756
+ input_points,
757
+ pd.Series(
758
+ [
759
+ p[0]
760
+ for p in random.choices(
761
+ population=candidates,
762
+ weights=list(dt[np.nonzero(dt)] / 255),
763
+ k=candidates.size // int(modes["dist"]),
764
+ )
765
+ ]
766
+ ),
767
+ ]
768
+ )
769
+
770
+ if len(input_points) > max_seeds:
771
+ input_points = input_points.sample(n=max_seeds)
772
+ input_points = np.array(input_points.to_list())
773
+
774
+ return {
775
+ "input_points": input_points,
776
+ "input_labels": np.array([label for _ in range(len(input_points))]),
777
+ }
778
+
779
+
780
+ def get_grid(mask, dots_per_rc):
781
+ grid = np.zeros_like(mask)
782
+ step = np.max(grid.shape[:2]) // dots_per_rc
783
+ grid[::step, ::step] = 1
784
+ grid[step // 2 :: step, step // 2 :: step] = 1
785
+ return grid.astype(np.uint8)
786
+
787
+
788
+ def get_grid_seeds(
789
+ mask_keep, mask_discard, keep_dots_per_row: int, discard_dots_per_row: int
790
+ ):
791
+ keep_points = np.array(
792
+ [
793
+ p[0]
794
+ for p in cv2.findNonZero(
795
+ cv2.bitwise_and(
796
+ mask_keep, get_grid(mask=mask_keep, dots_per_rc=keep_dots_per_row)
797
+ )
798
+ )
799
+ ]
800
+ )
801
+
802
+ discard_points = np.array(
803
+ [
804
+ p[0]
805
+ for p in cv2.findNonZero(
806
+ cv2.bitwise_and(
807
+ mask_discard,
808
+ get_grid(mask=mask_discard, dots_per_rc=discard_dots_per_row),
809
+ )
810
+ )
811
+ ]
812
+ )
813
+
814
+ return merge_seeds(
815
+ [
816
+ {
817
+ "input_points": keep_points,
818
+ "input_labels": np.array([1 for _ in range(len(keep_points))]),
819
+ },
820
+ {
821
+ "input_points": discard_points,
822
+ "input_labels": np.array([0 for _ in range(len(discard_points))]),
823
+ },
824
+ ]
825
+ )
826
+
827
+
828
+ def merge_seeds(seeds_list: list):
829
+ try:
830
+ return {
831
+ "input_points": np.concatenate(
832
+ [a["input_points"] for a in seeds_list if a["input_points"].size > 0]
833
+ ),
834
+ "input_labels": np.concatenate(
835
+ [a["input_labels"] for a in seeds_list if a["input_labels"].size > 0]
836
+ ),
837
+ }
838
+ except Exception as e:
839
+ return {
840
+ "input_points": np.array([]),
841
+ "input_labels": np.array([]),
842
+ }
843
+
844
+
845
+ def enlarge_contours(mask, increase_by) -> list:
846
+ return Morphologer(
847
+ op="dilate", kernel_size=increase_by, proc_count=1, kernel=cv2.MORPH_ELLIPSE
848
+ )(mask)
849
+
850
+
851
+ class Morphologer:
852
+ allowed_ops = ["none", "erode", "dilate", "open", "close"]
853
+
854
+ def __init__(
855
+ self, op=None, kernel_size=3, proc_count=1, kernel=cv2.MORPH_RECT
856
+ ) -> None:
857
+ self.op = op
858
+ self.kernel_size = kernel_size
859
+ self.proc_count = proc_count
860
+ self.kernel = kernel
861
+
862
+ def __call__(self, mask) -> Any:
863
+ return self.process(mask=mask)
864
+
865
+ def __repr__(self) -> str:
866
+ return f"{self.op}|{self.kernel_size}|{self.proc_count}|{self.kernel}"
867
+
868
+ def to_json(self):
869
+ return self.__dict__
870
+
871
+ def from_json(self, d: dict):
872
+ for k, v in d.items():
873
+ setattr(self, k, v)
874
+
875
+ @classmethod
876
+ def create_from_json(cls, d: dict):
877
+ out = cls()
878
+ out.from_json(d)
879
+ return out
880
+
881
+ def update(self, op, kernel_size, proc_count, kernel):
882
+ self.op = op
883
+ self.kernel_size = kernel_size
884
+ self.proc_count = proc_count
885
+ self.kernel = kernel
886
+
887
+ def process(self, mask):
888
+ if self.kernel_size > 2:
889
+ if self.op == "erode":
890
+ op = cv2.MORPH_ERODE
891
+ elif self.op == "dilate":
892
+ op = cv2.MORPH_DILATE
893
+ elif self.op == "open":
894
+ op = cv2.MORPH_OPEN
895
+ elif self.op == "close":
896
+ op = cv2.MORPH_CLOSE
897
+ elif self.op == "none":
898
+ return mask
899
+ else:
900
+ raise NotImplementedError
901
+
902
+ for _ in range(self.proc_count):
903
+ mask = cv2.morphologyEx(
904
+ mask,
905
+ op=op,
906
+ kernel=cv2.getStructuringElement(
907
+ shape=self.kernel, ksize=(self.kernel_size, self.kernel_size)
908
+ ),
909
+ )
910
+ return mask
911
+
912
+
913
+ class Thresholder:
914
+ def __init__(
915
+ self,
916
+ color_space: str = "hsv",
917
+ channel: str = "h",
918
+ min=0,
919
+ max=255,
920
+ ) -> None:
921
+ self.color_space = color_space
922
+ self.channel = channel
923
+ self.min = min
924
+ self.max = max
925
+
926
+ def __repr__(self) -> str:
927
+ return f"{self.color_space}|{self.channel}|{self.min}|{self.max}"
928
+
929
+ def __str__(self) -> str:
930
+ return f"{self.color_space}, {self.channel}: {self.min}->{self.max}"
931
+
932
+ def update(self, min_max):
933
+ self.min = min_max[0]
934
+ self.max = min_max[1]
935
+
936
+ def to_json(self):
937
+ return self.__dict__
938
+
939
+ def from_json(self, d: dict):
940
+ for k, v in d.items():
941
+ setattr(self, k, v)
942
+
943
+ @classmethod
944
+ def create_from_json(cls, d: dict):
945
+ out = cls()
946
+ out.from_json(d)
947
+ return out
948
+
949
+ def threshold(self, image):
950
+ channel = get_channel(
951
+ image=image, color_space=self.color_space, channel=self.channel
952
+ )
953
+ return cv2.inRange(channel, self.min, self.max)
954
+
955
+ def __call__(self, image) -> Any:
956
+ return self.threshold(image=image)
957
+
958
+ @property
959
+ def display_name(self) -> str:
960
+ return f"{self.color_space}: {self.channel}"
961
+
962
+ @property
963
+ def as_tuple(self) -> tuple:
964
+ return self.min, self.max
965
+
966
+
967
+ def check_hue(
968
+ image, mask, cnt, ok_hue_thresholder: Thresholder, ok_hue_pxl_ratio: float = 0.2
969
+ ):
970
+ x, y, w, h = [int(i) for i in cv2.boundingRect(cnt)]
971
+ masked_image = cv2.bitwise_and(
972
+ image, image, mask=cv2.drawContours(np.zeros_like(mask), [cnt], -1, 255, -1)
973
+ )
974
+ box_hue = cv2.split(
975
+ cv2.cvtColor(masked_image[y : y + h, x : x + w], cv2.COLOR_RGB2HSV)
976
+ )[0]
977
+ return (
978
+ box_hue[
979
+ (box_hue > ok_hue_thresholder.min) & (box_hue < ok_hue_thresholder.max)
980
+ ].size
981
+ > np.count_nonzero(box_hue) * ok_hue_pxl_ratio
982
+ )
983
+
984
+
985
+ def get_distance_data(hull, origin, max_dist):
986
+ """
987
+ Calculates distances from origin to contour barycenter,
988
+ also returns surface data
989
+ :param hull:
990
+ :param origin:
991
+ :param max_dist:
992
+ :return: dict
993
+ """
994
+ m = cv2.moments(hull)
995
+ if m["m00"] != 0:
996
+ cx_ = int(m["m10"] / m["m00"])
997
+ cy_ = int(m["m01"] / m["m00"])
998
+ dist_ = math.sqrt(math.pow(cx_ - origin.x, 2) + math.pow(cy_ - origin.y, 2))
999
+ dist_scaled_inverted = 1 - dist_ / max_dist
1000
+ res_ = dict(
1001
+ dist=dist_,
1002
+ cx=cx_,
1003
+ cy=cy_,
1004
+ dist_scaled_inverted=dist_scaled_inverted,
1005
+ area=cv2.contourArea(hull),
1006
+ scaled_area=cv2.contourArea(hull) * math.pow(dist_scaled_inverted, 2),
1007
+ )
1008
+ else:
1009
+ res_ = dict(
1010
+ dist=0,
1011
+ cx=0,
1012
+ cy=0,
1013
+ dist_scaled_inverted=0,
1014
+ area=0,
1015
+ scaled_area=0,
1016
+ )
1017
+ return res_
1018
+
1019
+
1020
+ def check_size(cnt, tolerance_area):
1021
+ return (tolerance_area is not None) and (
1022
+ (tolerance_area < 0) or cv2.contourArea(cnt) >= tolerance_area
1023
+ )
1024
+
1025
+
1026
+ def is_contour_overlap(mask, cmp_contour, master_contour) -> bool:
1027
+ cmp_image = cv2.drawContours(np.zeros_like(mask), [cmp_contour], -1, 255, -1)
1028
+ master_image = cv2.drawContours(np.zeros_like(mask), [master_contour], -1, 255, -1)
1029
+ return cv2.bitwise_and(cmp_image, master_image).any() == True
1030
+
1031
+
1032
+ def is_distance_ok(mask, cmp_contour, master_contour, tolerance_distance=None) -> bool:
1033
+ # print(cv2.minEnclosingCircle(cmp_contour))
1034
+ cmp_image = enlarge_contours(
1035
+ cv2.drawContours(np.zeros_like(mask), [cmp_contour], -1, 255, -1),
1036
+ increase_by=tolerance_distance,
1037
+ )
1038
+ # print(cv2.minEnclosingCircle(get_external_contours(cmp_image)[0]))
1039
+
1040
+ # print(cv2.minEnclosingCircle(master_contour))
1041
+ master_image = enlarge_contours(
1042
+ cv2.drawContours(np.zeros_like(mask), [master_contour], -1, 255, -1),
1043
+ increase_by=tolerance_distance,
1044
+ )
1045
+ # print(cv2.minEnclosingCircle(get_external_contours(master_image)[0]))
1046
+
1047
+ bit_image = cv2.bitwise_and(cmp_image, master_image)
1048
+ return bit_image.any() == True
1049
+
1050
+
1051
+ def fast_check_contour(
1052
+ mask, cmp_contour, master_contour, tolerance_area=None, tolerance_distance=None
1053
+ ):
1054
+ # Check contour intersection
1055
+ if is_contour_overlap(
1056
+ mask=mask, cmp_contour=cmp_contour, master_contour=master_contour
1057
+ ):
1058
+ return tc.KLC_OVERLAPS
1059
+
1060
+ # Check contour distance
1061
+ ok_dist = is_distance_ok(mask, cmp_contour, master_contour, tolerance_distance)
1062
+
1063
+ # Check contour size
1064
+ ok_size = check_size(cnt=cmp_contour, tolerance_area=tolerance_area)
1065
+
1066
+ if ok_size and ok_dist:
1067
+ return tc.KLC_OK_TOLERANCE
1068
+ elif not ok_size and not ok_dist:
1069
+ return tc.KLC_SMALL_FAR
1070
+ elif not ok_size:
1071
+ return tc.KLC_SMALL
1072
+ elif not ok_dist:
1073
+ return tc.KLC_FAR
1074
+
1075
+
1076
+ # MARK: fast Keep linled contours
1077
+ def fast_keep_linked_contours(
1078
+ src_mask,
1079
+ tolerance_distance: int,
1080
+ tolerance_area: int,
1081
+ morph_op: Morphologer,
1082
+ min_contour_size: int = 0,
1083
+ epsilon: float = 0.001,
1084
+ skip_linked_contours: bool = False,
1085
+ mean_channel_data: dict = None,
1086
+ source_image=None,
1087
+ ) -> dict:
1088
+ mask = src_mask.copy()
1089
+ # Delete all small contours
1090
+ if min_contour_size > 0:
1091
+ mask = filter_contours(mask=mask, min_contour_size=min_contour_size)
1092
+
1093
+ # Apply morphology operation to remove vnoise
1094
+ if morph_op is not None:
1095
+ mask = morph_op(mask)
1096
+
1097
+ contours = get_external_contours(mask)
1098
+ # print(len(contours))
1099
+ # ret = []
1100
+ # canvas = cv2.drawContours(
1101
+ # cv2.merge([np.zeros_like(src_mask) for _ in range(3)]),
1102
+ # contours,
1103
+ # -1,
1104
+ # tc.C_WHITE,
1105
+ # -1,
1106
+ # )
1107
+ # canvas = cv2.drawContours(canvas, contours, -1, tc.C_FUCHSIA, 4)
1108
+ # ret.append(canvas)
1109
+
1110
+ # Skip if not Enough contours
1111
+ if len(contours) == 0:
1112
+ return {
1113
+ "im_clean_mask": np.zeros_like(src_mask),
1114
+ "im_clean_mask_demo": cv2.merge(
1115
+ [
1116
+ np.zeros_like(src_mask),
1117
+ np.zeros_like(src_mask),
1118
+ np.zeros_like(src_mask),
1119
+ ]
1120
+ ),
1121
+ }
1122
+ elif len(contours) == 1 or skip_linked_contours is True:
1123
+ clean_mask = cv2.bitwise_and(
1124
+ src_mask,
1125
+ cv2.drawContours(
1126
+ image=np.zeros_like(mask),
1127
+ contours=contours,
1128
+ contourIdx=-1,
1129
+ color=255,
1130
+ thickness=-1,
1131
+ ),
1132
+ )
1133
+ return {
1134
+ "im_clean_mask": clean_mask,
1135
+ "im_clean_mask_demo": cv2.merge(
1136
+ [
1137
+ src_mask,
1138
+ clean_mask,
1139
+ cv2.drawContours(
1140
+ image=np.zeros_like(mask),
1141
+ contours=[
1142
+ cv2.approxPolyDP(
1143
+ cnt, epsilon * cv2.arcLength(cnt, True), True
1144
+ )
1145
+ for cnt in contours
1146
+ ],
1147
+ contourIdx=-1,
1148
+ color=255,
1149
+ thickness=-1,
1150
+ ),
1151
+ ]
1152
+ ),
1153
+ }
1154
+
1155
+ # Find root contour
1156
+ if mean_channel_data is not None and source_image is not None:
1157
+ # Use contour with matching mean color
1158
+ channel = get_channel(
1159
+ image=source_image,
1160
+ color_space=mean_channel_data["color_space"],
1161
+ channel=mean_channel_data["channel"],
1162
+ )
1163
+ df = pd.DataFrame(
1164
+ data={
1165
+ "area": [cv2.contourArea(c) for c in contours],
1166
+ "mean_dist": [
1167
+ abs(
1168
+ cv2.mean(
1169
+ src=channel.flatten(),
1170
+ mask=cv2.drawContours(
1171
+ np.zeros_like(mask), [c], -1, 255, -1
1172
+ ).flatten(),
1173
+ )[0]
1174
+ - mean_channel_data["mean"]
1175
+ )
1176
+ for c in contours
1177
+ ],
1178
+ }
1179
+ )
1180
+ if abs(df.mean_dist.min() - df.mean_dist.max()) < 4:
1181
+ root_index = df[df.area == df.area.max()].index[0]
1182
+ else:
1183
+ X = np.reshape(df.mean_dist.to_list(), (-1, 1))
1184
+ ms = MeanShift()
1185
+ ms.fit(X)
1186
+ df["level"] = ms.predict(X)
1187
+ df["level_dist_min"] = (
1188
+ df.groupby("level").transform(lambda x: x.mean()).mean_dist
1189
+ )
1190
+ df = df[df["level_dist_min"] == df["level_dist_min"].min()]
1191
+ root_index = df[df.area == df.area.max()].index[0]
1192
+
1193
+ root_contour = contours[root_index]
1194
+ good_contours = [contours.pop(root_index)]
1195
+ unknown_contours = []
1196
+ else:
1197
+ # Use bigger contour in the center
1198
+
1199
+ # Find the largest contour
1200
+ root_contour = contours[0]
1201
+ big_idx = 0
1202
+ h, w = src_mask.shape
1203
+ roi_root = Circle(w // 2, h // 2, max(h, w) // 2)
1204
+ dist_max = roi_root.r
1205
+
1206
+ max_area = 0
1207
+ for i, contour in enumerate(contours):
1208
+ morph_dict = get_distance_data(contour, roi_root, dist_max)
1209
+
1210
+ if morph_dict["scaled_area"] > max_area:
1211
+ max_area = morph_dict["scaled_area"]
1212
+ root_contour = contour
1213
+ big_idx = i
1214
+
1215
+ # parse all contours and switch
1216
+ good_contours = [contours.pop(big_idx)]
1217
+ unknown_contours = []
1218
+
1219
+ # print(len(contours) + len(good_contours))
1220
+ # canvas = cv2.drawContours(
1221
+ # cv2.merge([np.zeros_like(src_mask) for _ in range(3)]),
1222
+ # contours,
1223
+ # -1,
1224
+ # tc.C_WHITE,
1225
+ # -1,
1226
+ # )
1227
+ # canvas = cv2.drawContours(canvas, contours, -1, tc.C_FUCHSIA, 4)
1228
+ # # canvas = cv2.drawContours(canvas, good_contours, -1, tc.C_GREEN, -1)
1229
+ # canvas = cv2.drawContours(canvas, good_contours, -1, tc.C_LIME, 4)
1230
+ # ret.append(canvas)
1231
+ # return ret
1232
+
1233
+ while len(contours) > 0:
1234
+ contour = contours.pop()
1235
+ res = fast_check_contour(
1236
+ mask=src_mask,
1237
+ cmp_contour=contour,
1238
+ master_contour=root_contour,
1239
+ tolerance_area=tolerance_area,
1240
+ tolerance_distance=tolerance_distance,
1241
+ )
1242
+ if res == tc.KLC_FULLY_INSIDE:
1243
+ pass
1244
+ elif res in [tc.KLC_OVERLAPS, tc.KLC_OK_TOLERANCE]:
1245
+ good_contours.append(contour)
1246
+ else:
1247
+ unknown_contours.append(contour)
1248
+
1249
+ # Try to aggregate unknown contours to good contours
1250
+ stable = False
1251
+ while not stable:
1252
+ stable = True
1253
+ i = 0
1254
+ iter_count = 1
1255
+ while i < len(unknown_contours):
1256
+ contour = unknown_contours[i]
1257
+ res = tc.KLC_SMALL_FAR
1258
+ for good_contour in good_contours:
1259
+ res = fast_check_contour(
1260
+ mask=src_mask,
1261
+ cmp_contour=contour,
1262
+ master_contour=good_contour,
1263
+ tolerance_area=tolerance_area,
1264
+ tolerance_distance=tolerance_distance,
1265
+ )
1266
+ if res == tc.KLC_FULLY_INSIDE:
1267
+ del unknown_contours[i]
1268
+ stable = False
1269
+ break
1270
+ elif res in [tc.KLC_OVERLAPS, tc.KLC_OK_TOLERANCE]:
1271
+ good_contours.append(unknown_contours.pop(i))
1272
+ stable = False
1273
+ break
1274
+ elif res in [
1275
+ tc.KLC_SMALL_FAR,
1276
+ tc.KLC_SMALL,
1277
+ tc.KLC_FAR,
1278
+ ]:
1279
+ pass
1280
+ if res in [
1281
+ tc.KLC_SMALL_FAR,
1282
+ tc.KLC_SMALL,
1283
+ tc.KLC_FAR,
1284
+ ]:
1285
+ i += 1
1286
+ iter_count += 1
1287
+
1288
+ # At this point we have the zone were the contours are allowed to be
1289
+ contour_template = cv2.bitwise_and(
1290
+ cv2.drawContours(np.zeros_like(mask), good_contours, -1, 255, -1),
1291
+ mask,
1292
+ )
1293
+ out_mask = np.zeros_like(mask)
1294
+ for cnt in get_contours(src_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE):
1295
+ if (
1296
+ cv2.contourArea(cnt, oriented=True) < 0
1297
+ and cv2.bitwise_and(
1298
+ contour_template,
1299
+ cv2.drawContours(np.zeros_like(mask), [cnt], 0, 255, -1),
1300
+ ).any()
1301
+ ):
1302
+ out_mask = cv2.drawContours(out_mask, [cnt], 0, 255, -1)
1303
+
1304
+ im_clean_mask = cv2.bitwise_and(out_mask, src_mask)
1305
+
1306
+ im_clean_mask_demo = cv2.merge(
1307
+ [np.zeros_like(src_mask), np.zeros_like(src_mask), np.zeros_like(src_mask)]
1308
+ )
1309
+ for contour in good_contours:
1310
+ im_clean_mask_demo = cv2.drawContours(
1311
+ im_clean_mask_demo, [contour], 0, tc.C_WHITE, -1
1312
+ )
1313
+ im_clean_mask_demo = cv2.drawContours(
1314
+ im_clean_mask_demo, [root_contour], 0, tc.C_GREEN, -1
1315
+ )
1316
+ for contour in unknown_contours:
1317
+ ok_size = check_size(cnt=contour, tolerance_area=tolerance_area)
1318
+ ok_distance = is_distance_ok(
1319
+ mask=im_clean_mask,
1320
+ cmp_contour=contour,
1321
+ master_contour=good_contours[0],
1322
+ tolerance_distance=tolerance_distance,
1323
+ )
1324
+ contour_color = (
1325
+ bgr_to_rgb(tc.C_FUCHSIA)
1326
+ if ok_distance is False and ok_size is False
1327
+ else bgr_to_rgb(tc.C_RED) if ok_size is False else bgr_to_rgb(tc.C_BLUE)
1328
+ )
1329
+ im_clean_mask_demo = cv2.drawContours(
1330
+ im_clean_mask_demo, [contour], 0, contour_color, -1
1331
+ )
1332
+ im_clean_mask_demo = cv2.bitwise_and(
1333
+ im_clean_mask_demo, im_clean_mask_demo, mask=src_mask
1334
+ )
1335
+ return {
1336
+ "im_clean_mask": im_clean_mask,
1337
+ "im_clean_mask_demo": im_clean_mask_demo,
1338
+ }
1339
+
1340
+
1341
+ def get_cnt_center(cnt):
1342
+ mmnt = cv2.moments(cnt)
1343
+ return int(mmnt["m10"] / mmnt["m00"]), int(mmnt["m01"] / mmnt["m00"])
1344
+
1345
+
1346
+ def dbg_min_distance(mask, cnt1, cnt2):
1347
+ if cv2.bitwise_and(
1348
+ cv2.drawContours(mask, [cnt1], -1, 255, -1),
1349
+ cv2.drawContours(mask, [cnt2], -1, 255, -1),
1350
+ ).any():
1351
+ return 0, get_cnt_center(cnt1), get_cnt_center(cnt2)
1352
+ min_dist = 1000000000000
1353
+ for cur_pt1 in cnt1:
1354
+ for cur_pt2 in cnt2:
1355
+ cur_dist = cv2.norm(cur_pt1, cur_pt2)
1356
+ if abs(cur_dist) < min_dist:
1357
+ min_dist = abs(cur_dist)
1358
+ pt1 = cur_pt1
1359
+ pt2 = cur_pt2
1360
+ return min_dist, pt1[0], pt2[0]
1361
+
1362
+
1363
+ def min_distance(mask, cnt1, cnt2):
1364
+ if cv2.bitwise_and(
1365
+ cv2.drawContours(np.zeros_like(mask), [cnt1], -1, 255, -1),
1366
+ cv2.drawContours(np.zeros_like(mask), [cnt2], -1, 255, -1),
1367
+ ).any():
1368
+ return 0
1369
+ return min(*[cv2.norm(pt1, pt2) for pt1, pt2 in itertools.product(cnt1, cnt2)])
1370
+
1371
+
1372
+ def check_dist(distance, tolerance_distance):
1373
+ return (tolerance_distance is not None) and (
1374
+ tolerance_distance < 0 or distance <= tolerance_distance
1375
+ )
1376
+
1377
+
1378
+ def check_hull(
1379
+ mask, cmp_hull, master_hull, tolerance_area=None, tolerance_distance=None
1380
+ ):
1381
+ # Check hull intersection
1382
+ if cv2.bitwise_and(
1383
+ cv2.drawContours(np.zeros_like(mask), [cmp_hull], -1, 255, -1),
1384
+ cv2.drawContours(np.zeros_like(mask), [master_hull], -1, 255, -1),
1385
+ ).any():
1386
+ return tc.KLC_OVERLAPS
1387
+
1388
+ # Check point to point
1389
+ min_dist = min_distance(mask=mask, cnt1=cmp_hull, cnt2=master_hull)
1390
+ if min_dist <= 0:
1391
+ return tc.KLC_OVERLAPS
1392
+ else:
1393
+ ok_size = check_size(cnt=cmp_hull, tolerance_area=tolerance_area)
1394
+ ok_dist = check_dist(distance=min_dist, tolerance_distance=tolerance_distance)
1395
+ if ok_size and ok_dist:
1396
+ return tc.KLC_OK_TOLERANCE
1397
+ elif not ok_size and not ok_dist:
1398
+ return tc.KLC_SMALL_FAR
1399
+ elif not ok_size:
1400
+ return tc.KLC_SMALL
1401
+ elif not ok_dist:
1402
+ return tc.KLC_FAR
1403
+
1404
+
1405
+ # MARK: Keep linled contours
1406
+ def keep_linked_contours(
1407
+ src_mask,
1408
+ tolerance_distance: int,
1409
+ tolerance_area: int,
1410
+ morph_op: Morphologer,
1411
+ min_contour_size: int = 0,
1412
+ epsilon: float = 0.001,
1413
+ skip_linked_contours: bool = False,
1414
+ mean_channel_data: dict = None,
1415
+ source_image=None,
1416
+ ) -> dict:
1417
+ mask = src_mask.copy()
1418
+ # Delete all small contours
1419
+ if min_contour_size > 0:
1420
+ mask = filter_contours(mask=mask, min_contour_size=min_contour_size)
1421
+
1422
+ # Apply morphology operation to remove vnoise
1423
+ if morph_op is not None:
1424
+ mask = morph_op(mask)
1425
+
1426
+ contours = get_external_contours(mask)
1427
+
1428
+ if len(contours) == 0:
1429
+ return {
1430
+ "im_clean_mask": np.zeros_like(src_mask),
1431
+ "im_clean_mask_demo": cv2.merge(
1432
+ [
1433
+ np.zeros_like(src_mask),
1434
+ np.zeros_like(src_mask),
1435
+ np.zeros_like(src_mask),
1436
+ ]
1437
+ ),
1438
+ }
1439
+ elif len(contours) == 1 or skip_linked_contours is True:
1440
+ clean_mask = cv2.bitwise_and(
1441
+ src_mask,
1442
+ cv2.drawContours(
1443
+ image=np.zeros_like(mask),
1444
+ contours=contours,
1445
+ contourIdx=-1,
1446
+ color=255,
1447
+ thickness=-1,
1448
+ ),
1449
+ )
1450
+ return {
1451
+ "im_clean_mask": clean_mask,
1452
+ "im_clean_mask_demo": cv2.merge(
1453
+ [
1454
+ src_mask,
1455
+ clean_mask,
1456
+ cv2.drawContours(
1457
+ image=np.zeros_like(mask),
1458
+ contours=[
1459
+ cv2.approxPolyDP(
1460
+ cnt, epsilon * cv2.arcLength(cnt, True), True
1461
+ )
1462
+ for cnt in contours
1463
+ ],
1464
+ contourIdx=-1,
1465
+ color=255,
1466
+ thickness=-1,
1467
+ ),
1468
+ ]
1469
+ ),
1470
+ }
1471
+
1472
+ if mean_channel_data is not None and source_image is not None:
1473
+ channel = get_channel(
1474
+ image=source_image,
1475
+ color_space=mean_channel_data["color_space"],
1476
+ channel=mean_channel_data["channel"],
1477
+ )
1478
+ contours = get_external_contours(mask)
1479
+ df = pd.DataFrame(
1480
+ data={
1481
+ "area": [cv2.contourArea(c) for c in contours],
1482
+ "mean_dist": [
1483
+ abs(
1484
+ cv2.mean(
1485
+ src=channel.flatten(),
1486
+ mask=cv2.drawContours(
1487
+ np.zeros_like(mask), [c], -1, 255, -1
1488
+ ).flatten(),
1489
+ )[0]
1490
+ - mean_channel_data["mean"]
1491
+ )
1492
+ for c in contours
1493
+ ],
1494
+ }
1495
+ )
1496
+ if abs(df.mean_dist.min() - df.mean_dist.max()) < 4:
1497
+ root_index = df[df.area == df.area.max()].index[0]
1498
+ else:
1499
+ X = np.reshape(df.mean_dist.to_list(), (-1, 1))
1500
+ ms = MeanShift()
1501
+ ms.fit(X)
1502
+ df["level"] = ms.predict(X)
1503
+ df["level_dist_min"] = (
1504
+ df.groupby("level").transform(lambda x: x.mean()).mean_dist
1505
+ )
1506
+ df = df[df["level_dist_min"] == df["level_dist_min"].min()]
1507
+ root_index = df[df.area == df.area.max()].index[0]
1508
+
1509
+ hulls = [
1510
+ cv2.approxPolyDP(cnt, epsilon * cv2.arcLength(cnt, True), True)
1511
+ for cnt in contours
1512
+ ]
1513
+ root_hull = hulls[root_index]
1514
+ good_hulls = [hulls.pop(root_index)]
1515
+ unknown_hulls = []
1516
+ else:
1517
+ # Transform all contours into approximations
1518
+ hulls = [
1519
+ cv2.approxPolyDP(cnt, epsilon * cv2.arcLength(cnt, True), True)
1520
+ for cnt in contours
1521
+ ]
1522
+
1523
+ # Find the largest hull
1524
+ root_hull = hulls[0]
1525
+ big_idx = 0
1526
+ h, w = src_mask.shape
1527
+ roi_root = Circle(w // 2, h // 2, max(h, w) // 2)
1528
+ dist_max = roi_root.r
1529
+
1530
+ max_area = 0
1531
+ for i, hull in enumerate(hulls):
1532
+ morph_dict = get_distance_data(hull, roi_root, dist_max)
1533
+
1534
+ if morph_dict["scaled_area"] > max_area:
1535
+ max_area = morph_dict["scaled_area"]
1536
+ root_hull = hull
1537
+ big_idx = i
1538
+
1539
+ # parse all hulls and switch
1540
+ good_hulls = [hulls.pop(big_idx)]
1541
+ unknown_hulls = []
1542
+
1543
+ while len(hulls) > 0:
1544
+ hull = hulls.pop()
1545
+ res = check_hull(
1546
+ mask=src_mask,
1547
+ cmp_hull=hull,
1548
+ master_hull=root_hull,
1549
+ tolerance_area=tolerance_area,
1550
+ tolerance_distance=tolerance_distance,
1551
+ )
1552
+ if res == tc.KLC_FULLY_INSIDE:
1553
+ pass
1554
+ elif res in [tc.KLC_OVERLAPS, tc.KLC_OK_TOLERANCE]:
1555
+ good_hulls.append(hull)
1556
+ else:
1557
+ unknown_hulls.append(hull)
1558
+
1559
+ # Try to aggregate unknown hulls to good hulls
1560
+ stable = False
1561
+ while not stable:
1562
+ stable = True
1563
+ i = 0
1564
+ iter_count = 1
1565
+ while i < len(unknown_hulls):
1566
+ hull = unknown_hulls[i]
1567
+ res = tc.KLC_SMALL_FAR
1568
+ for good_hull in good_hulls:
1569
+ res = check_hull(
1570
+ mask=src_mask,
1571
+ cmp_hull=hull,
1572
+ master_hull=good_hull,
1573
+ tolerance_area=tolerance_area,
1574
+ tolerance_distance=tolerance_distance,
1575
+ )
1576
+ if res == tc.KLC_FULLY_INSIDE:
1577
+ del unknown_hulls[i]
1578
+ stable = False
1579
+ break
1580
+ elif res in [tc.KLC_OVERLAPS, tc.KLC_OK_TOLERANCE]:
1581
+ good_hulls.append(unknown_hulls.pop(i))
1582
+ stable = False
1583
+ break
1584
+ elif res in [
1585
+ tc.KLC_SMALL_FAR,
1586
+ tc.KLC_SMALL,
1587
+ tc.KLC_FAR,
1588
+ ]:
1589
+ pass
1590
+ if res in [
1591
+ tc.KLC_SMALL_FAR,
1592
+ tc.KLC_SMALL,
1593
+ tc.KLC_FAR,
1594
+ ]:
1595
+ i += 1
1596
+ iter_count += 1
1597
+
1598
+ # At this point we have the zone were the contours are allowed to be
1599
+ hull_template = cv2.bitwise_and(
1600
+ cv2.drawContours(np.zeros_like(mask), good_hulls, -1, 255, -1),
1601
+ mask,
1602
+ )
1603
+ out_mask = np.zeros_like(mask)
1604
+ for cnt in get_contours(src_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE):
1605
+ if (
1606
+ cv2.contourArea(cnt, oriented=True) < 0
1607
+ and cv2.bitwise_and(
1608
+ hull_template,
1609
+ cv2.drawContours(np.zeros_like(mask), [cnt], 0, 255, -1),
1610
+ ).any()
1611
+ ):
1612
+ out_mask = cv2.drawContours(out_mask, [cnt], 0, 255, -1)
1613
+
1614
+ im_clean_mask = cv2.bitwise_and(out_mask, src_mask)
1615
+
1616
+ im_clean_mask_demo = cv2.merge(
1617
+ [np.zeros_like(src_mask), np.zeros_like(src_mask), np.zeros_like(src_mask)]
1618
+ )
1619
+ for hull in good_hulls:
1620
+ im_clean_mask_demo = cv2.drawContours(
1621
+ im_clean_mask_demo, [hull], 0, tc.C_WHITE, -1
1622
+ )
1623
+ im_clean_mask_demo = cv2.drawContours(
1624
+ im_clean_mask_demo, [root_hull], 0, tc.C_GREEN, -1
1625
+ )
1626
+ for hull in unknown_hulls:
1627
+ ok_size = check_size(cnt=hull, tolerance_area=tolerance_area)
1628
+ min_dist = (
1629
+ min_distance(im_clean_mask_demo, hull, good_hulls[0])
1630
+ if len(good_hulls) == 1
1631
+ else min(
1632
+ *[
1633
+ min_distance(im_clean_mask_demo, hull, good_hull)
1634
+ for good_hull in good_hulls
1635
+ ]
1636
+ )
1637
+ )
1638
+ ok_distance = check_dist(distance=min_dist, tolerance_distance=tolerance_area)
1639
+ contour_color = (
1640
+ bgr_to_rgb(tc.C_FUCHSIA)
1641
+ if ok_distance is False and ok_size is False
1642
+ else bgr_to_rgb(tc.C_RED) if ok_size is False else bgr_to_rgb(tc.C_BLUE)
1643
+ )
1644
+ im_clean_mask_demo = cv2.drawContours(
1645
+ im_clean_mask_demo, [hull], 0, contour_color, -1
1646
+ )
1647
+ im_clean_mask_demo = cv2.bitwise_and(
1648
+ im_clean_mask_demo, im_clean_mask_demo, mask=src_mask
1649
+ )
1650
+ return {
1651
+ "im_clean_mask": im_clean_mask,
1652
+ "im_clean_mask_demo": im_clean_mask_demo,
1653
+ }
1654
+
1655
+
1656
+ def get_external_contours(mask):
1657
+ external_contours = []
1658
+ contours, hierarchys = cv2.findContours(
1659
+ mask, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE
1660
+ )
1661
+ if len(contours) == 0:
1662
+ return []
1663
+ for cnt, hier in zip(contours, hierarchys[0]):
1664
+ if hier[-1] == -1:
1665
+ external_contours.append(cnt)
1666
+ if len(external_contours) == 0:
1667
+ return []
1668
+ external_contours.sort(key=lambda x: cv2.contourArea(x, oriented=False))
1669
+ return external_contours
1670
+
1671
+
1672
+ def get_internal_contours(mask):
1673
+ internal_contours = []
1674
+ contours, hierarchys = cv2.findContours(
1675
+ mask, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE
1676
+ )
1677
+ if len(contours) == 0:
1678
+ return []
1679
+ for cnt, hier in zip(contours, hierarchys[0]):
1680
+ if hier[-1] != -1:
1681
+ internal_contours.append(cnt)
1682
+ if len(internal_contours) == 0:
1683
+ return []
1684
+ internal_contours.sort(key=lambda x: cv2.contourArea(x, oriented=False))
1685
+ return internal_contours
1686
+
1687
+
1688
+ def get_filled_contours(mask):
1689
+ return [
1690
+ cnt
1691
+ for cnt in cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
1692
+ if cv2.bitwise_and(
1693
+ mask, cv2.drawContours(np.zeros_like(mask), [cnt], -1, 255, -1)
1694
+ ).any()
1695
+ == True
1696
+ ]
1697
+
1698
+
1699
+ def get_main_contour(mask):
1700
+ external_contours = get_external_contours(mask)
1701
+ if len(external_contours) == 0:
1702
+ return None
1703
+ elif len(external_contours) == 1:
1704
+ return external_contours[0]
1705
+ else:
1706
+ big_idx = 0
1707
+ h, w = mask.shape
1708
+ roi_root = Circle(w // 2, h // 2, max(h, w) // 2)
1709
+ dist_max = roi_root.r
1710
+
1711
+ max_area = 0
1712
+ for i, hull in enumerate(external_contours):
1713
+ morph_dict = get_distance_data(hull, roi_root, dist_max)
1714
+
1715
+ if morph_dict["scaled_area"] > max_area:
1716
+ max_area = morph_dict["scaled_area"]
1717
+ big_idx = i
1718
+
1719
+ return external_contours[big_idx]
1720
+
1721
+
1722
+ def get_suspect_contours(mask):
1723
+ external_contours = get_external_contours(mask)
1724
+ if len(external_contours) == 0:
1725
+ return []
1726
+ big_idx = 0
1727
+ h, w = mask.shape
1728
+ roi_root = Circle(w // 2, h // 2, max(h, w) // 2)
1729
+ dist_max = roi_root.r
1730
+
1731
+ max_area = 0
1732
+ for i, hull in enumerate(external_contours):
1733
+ morph_dict = get_distance_data(hull, roi_root, dist_max)
1734
+
1735
+ if morph_dict["scaled_area"] > max_area:
1736
+ max_area = morph_dict["scaled_area"]
1737
+ big_idx = i
1738
+
1739
+ external_contours.pop(big_idx)
1740
+
1741
+ return external_contours
1742
+
1743
+
1744
+ def get_contours_dict(mask):
1745
+ main = get_main_contour(mask)
1746
+ internal = get_internal_contours(mask)
1747
+ suspect = get_suspect_contours(mask)
1748
+ ret = {}
1749
+ if main is not None:
1750
+ ret["main"] = main
1751
+ if internal:
1752
+ ret["internal"] = internal
1753
+ if suspect:
1754
+ ret["suspect"] = suspect
1755
+ return ret
1756
+
1757
+
1758
+ def find_matches(
1759
+ previous_image,
1760
+ previous_mask,
1761
+ current_image,
1762
+ current_mask,
1763
+ safe_ratio: float = 0.5,
1764
+ plot_debug: dict = {},
1765
+ ):
1766
+ desc_extractor = SIFT()
1767
+ # Previous image
1768
+ masked_previous_image = cv2.equalizeHist(
1769
+ cv2.cvtColor(
1770
+ cv2.bitwise_and(previous_image, previous_image, mask=previous_mask),
1771
+ cv2.COLOR_RGB2GRAY,
1772
+ )
1773
+ )
1774
+ desc_extractor.detect_and_extract(masked_previous_image)
1775
+ kp_previous = desc_extractor.keypoints
1776
+ desc_previous = desc_extractor.descriptors
1777
+ # Current image
1778
+ masked_current_image = cv2.equalizeHist(
1779
+ cv2.cvtColor(
1780
+ cv2.bitwise_and(current_image, current_image, mask=current_mask),
1781
+ cv2.COLOR_RGB2GRAY,
1782
+ )
1783
+ )
1784
+ desc_extractor.detect_and_extract(masked_current_image)
1785
+ kp_current = desc_extractor.keypoints
1786
+ desc_current = desc_extractor.descriptors
1787
+ # Find matches
1788
+ matches = match_descriptors(
1789
+ desc_previous, desc_current, max_ratio=0.8, cross_check=True
1790
+ )
1791
+ matches_previous = kp_previous[matches[:, 0]]
1792
+ matches_current = kp_current[matches[:, 1]]
1793
+ distances = np.array(
1794
+ [np.linalg.norm(p1 - p2) for p1, p2 in zip(matches_previous, matches_current)]
1795
+ )
1796
+ matches_previous = matches_previous[
1797
+ (distances < np.median(distances) / safe_ratio)
1798
+ & (distances > np.median(distances) * safe_ratio)
1799
+ ]
1800
+ matches_current = matches_current[
1801
+ (distances < np.median(distances) / safe_ratio)
1802
+ & (distances > np.median(distances) * safe_ratio)
1803
+ ]
1804
+ if plot_debug:
1805
+ if "ax" in plot_debug:
1806
+ ax = plot_debug["ax"]
1807
+ else:
1808
+ _, ax = plt.subplots(nrows=1, ncols=1, figsize=(20, 10))
1809
+ plot_matches(
1810
+ ax=ax,
1811
+ image1=plot_debug["previous"],
1812
+ image2=plot_debug["current"],
1813
+ keypoints1=kp_previous,
1814
+ keypoints2=kp_current,
1815
+ matches=matches,
1816
+ only_matches=plot_debug.get("only_matches", True),
1817
+ )
1818
+ ax.axis("off")
1819
+ if "title" in plot_debug:
1820
+ ax.set_title(plot_debug["title"])
1821
+ if "ax" not in plot_debug:
1822
+ plt.show()
1823
+
1824
+ return matches_previous, matches_current
1825
+
1826
+
1827
+ def find_rotation_anlge(
1828
+ previous_image,
1829
+ previous_mask,
1830
+ current_image,
1831
+ current_mask,
1832
+ plot_debug: dict = {},
1833
+ ):
1834
+ matches_previous, matches_current = find_matches(
1835
+ previous_image=previous_image,
1836
+ previous_mask=previous_mask,
1837
+ current_image=current_image,
1838
+ current_mask=current_mask,
1839
+ plot_debug=plot_debug,
1840
+ )
1841
+ rot, *_ = R.align_vectors(
1842
+ np.pad(
1843
+ matches_previous - matches_previous.mean(axis=0),
1844
+ pad_width=[0, 1],
1845
+ mode="constant",
1846
+ ),
1847
+ np.pad(
1848
+ matches_current - matches_current.mean(axis=0),
1849
+ pad_width=[0, 1],
1850
+ mode="constant",
1851
+ ),
1852
+ return_sensitivity=True,
1853
+ )
1854
+ return rot.as_euler("zyx", degrees=True)[0]
1855
+
1856
+
1857
+ def match_previous_rotation(
1858
+ previous_image,
1859
+ previous_mask,
1860
+ current_image,
1861
+ current_mask,
1862
+ plot_debug: dict = {},
1863
+ ):
1864
+ if plot_debug:
1865
+ fig = plt.figure(
1866
+ figsize=plot_debug["fig_size"] if "fig_size" in plot_debug else (8, 8)
1867
+ )
1868
+ grid_spec = fig.add_gridspec(nrows=2, ncols=3)
1869
+ plt_descriptors = fig.add_subplot(grid_spec[0, :])
1870
+ plot_debug = plot_debug | {
1871
+ "previous": cv2.bitwise_and(
1872
+ previous_image, previous_image, mask=previous_mask
1873
+ ),
1874
+ "current": cv2.bitwise_and(current_image, current_image, mask=current_mask),
1875
+ "ax": plt_descriptors,
1876
+ }
1877
+
1878
+ angle = find_rotation_anlge(
1879
+ previous_image=previous_image,
1880
+ previous_mask=previous_mask,
1881
+ current_image=current_image,
1882
+ current_mask=current_mask,
1883
+ plot_debug=plot_debug,
1884
+ )
1885
+ ret = rotate_image(current_image, angle=angle)
1886
+ if plot_debug:
1887
+ plt_descriptors.set_title(f"{plot_debug['title']}, angle={angle:.2f} ")
1888
+ _update_axis(
1889
+ axis=fig.add_subplot(grid_spec[1, 0]),
1890
+ image=previous_image,
1891
+ title="previous image",
1892
+ )
1893
+ _update_axis(
1894
+ axis=fig.add_subplot(grid_spec[1, 1]), image=ret, title="rotated image"
1895
+ )
1896
+ _update_axis(
1897
+ axis=fig.add_subplot(grid_spec[1, 2]),
1898
+ image=current_image,
1899
+ title="current image",
1900
+ )
1901
+ plt.show()
1902
+ return ret
1903
+
1904
+
1905
+ def sift_contours(
1906
+ clean_mask, target_mask, morph_op: Morphologer = Morphologer(op="none")
1907
+ ):
1908
+ contours = get_contours_dict(morph_op(target_mask))
1909
+ if "suspect" not in contours:
1910
+ return target_mask
1911
+ suspects = contours["suspect"]
1912
+ goods = []
1913
+
1914
+ i = 0
1915
+ cm = morph_op(clean_mask)
1916
+ while i < len(suspects):
1917
+ if cv2.bitwise_and(
1918
+ cv2.drawContours(np.zeros_like(cm), suspects, i, 255, -1),
1919
+ cm,
1920
+ ).any():
1921
+ goods.append(suspects.pop(i))
1922
+ else:
1923
+ i += 1
1924
+
1925
+ # Finalize
1926
+ ret = cv2.drawContours(
1927
+ np.zeros_like(cm),
1928
+ contours=[contours["main"]] + goods,
1929
+ contourIdx=-1,
1930
+ color=255,
1931
+ thickness=-1,
1932
+ )
1933
+ ret = cv2.drawContours(
1934
+ ret,
1935
+ contours=contours.get("internal", []),
1936
+ contourIdx=-1,
1937
+ color=0,
1938
+ thickness=-1,
1939
+ )
1940
+ return ret
1941
+
1942
+
1943
+ def draw_mask(
1944
+ image,
1945
+ mask,
1946
+ background_type: str = "bw",
1947
+ draw_contours: bool = True,
1948
+ mask_properties: list = [],
1949
+ contours_thickness: int = 4,
1950
+ ):
1951
+ foreground = cv2.bitwise_and(image, image, mask=mask)
1952
+ if background_type == "bw":
1953
+ background = cv2.cvtColor(
1954
+ cv2.bitwise_and(image, image, mask=255 - mask), cv2.COLOR_RGB2GRAY
1955
+ )
1956
+ background = background * 0.4
1957
+ background[background > 255] = 255
1958
+ background = cv2.merge([background, background, background]).astype(np.uint8)
1959
+ elif background_type == "source":
1960
+ background = image.copy()
1961
+ elif isinstance(background_type, tuple):
1962
+ background = np.full_like(image, background_type)
1963
+ else:
1964
+ raise NotImplementedError
1965
+ out = cv2.bitwise_or(foreground, background)
1966
+ if draw_contours is True or isinstance(draw_contours, tuple):
1967
+ cur_color = 0
1968
+ colors = (
1969
+ [
1970
+ tc.C_BLUE,
1971
+ tc.C_BLUE_VIOLET,
1972
+ tc.C_CABIN_BLUE,
1973
+ tc.C_CYAN,
1974
+ tc.C_FUCHSIA,
1975
+ tc.C_LIGHT_STEEL_BLUE,
1976
+ tc.C_MAROON,
1977
+ tc.C_ORANGE,
1978
+ tc.C_PURPLE,
1979
+ tc.C_RED,
1980
+ tc.C_TEAL,
1981
+ tc.C_YELLOW,
1982
+ ]
1983
+ if isinstance(draw_contours, bool)
1984
+ else [draw_contours] if isinstance(draw_contours, tuple) else [tc.C_WHITE]
1985
+ )
1986
+ for cnt in cv2.findContours(
1987
+ mask, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_NONE
1988
+ )[0]:
1989
+ out = cv2.drawContours(
1990
+ out,
1991
+ [cnt],
1992
+ contourIdx=0,
1993
+ color=colors[cur_color],
1994
+ thickness=contours_thickness,
1995
+ )
1996
+ cur_color += 1
1997
+ if cur_color > len(colors) - 1:
1998
+ cur_color = 0
1999
+ if tc.MP_CENTROID in mask_properties:
2000
+ moments = cv2.moments(mask, binaryImage=True)
2001
+ cmx, cmy = (
2002
+ moments["m10"] / moments["m00"],
2003
+ moments["m01"] / moments["m00"],
2004
+ )
2005
+ out = cv2.circle(out, (int(cmx), int(cmy)), 10, tc.C_BLUE, 4)
2006
+
2007
+ return out
2008
+
2009
+
2010
+ def draw_marker(
2011
+ image,
2012
+ pos: list,
2013
+ marker_size: int,
2014
+ thickness: int,
2015
+ colors: tuple,
2016
+ marker_type=cv2.MARKER_CROSS,
2017
+ ):
2018
+ return cv2.drawMarker(
2019
+ cv2.drawMarker(
2020
+ image,
2021
+ pos,
2022
+ color=colors[0],
2023
+ markerType=marker_type,
2024
+ markerSize=marker_size,
2025
+ thickness=thickness,
2026
+ ),
2027
+ pos,
2028
+ color=colors[1],
2029
+ markerType=marker_type,
2030
+ markerSize=marker_size // 2,
2031
+ thickness=thickness // 2,
2032
+ )
2033
+
2034
+
2035
+ def draw_seeds(image, seeds, selected_label=None, marker_size=None, marker_thickness=4):
2036
+ marker_size = max(image.shape) // 50 if marker_size is None else marker_size
2037
+
2038
+ for seed, label in zip(seeds["input_points"], seeds["input_labels"]):
2039
+ if selected_label is not None and label != selected_label:
2040
+ continue
2041
+ image = draw_marker(
2042
+ image=image,
2043
+ pos=seed,
2044
+ marker_size=marker_size,
2045
+ thickness=marker_thickness,
2046
+ colors=(tc.C_WHITE, tc.C_BLUE if label == 0 else tc.C_LIME),
2047
+ marker_type=cv2.MARKER_TILTED_CROSS if label == 0 else cv2.MARKER_SQUARE,
2048
+ )
2049
+
2050
+ return image
2051
+
2052
+
2053
+ def get_concat_h_multi_resize(im_list, resample=Image.Resampling.BICUBIC):
2054
+ min_height = min(im.height for im in im_list)
2055
+ im_list_resize = [
2056
+ im.resize(
2057
+ (int(im.width * min_height / im.height), min_height), resample=resample
2058
+ )
2059
+ for im in im_list
2060
+ ]
2061
+ total_width = sum(im.width for im in im_list_resize)
2062
+ dst = Image.new("RGB", (total_width, min_height))
2063
+ pos_x = 0
2064
+ for im in im_list_resize:
2065
+ dst.paste(im, (pos_x, 0))
2066
+ pos_x += im.width
2067
+ return dst
2068
+
2069
+
2070
+ def get_concat_v_multi_resize(im_list, resample=Image.Resampling.BICUBIC):
2071
+ min_width = min(im.width for im in im_list)
2072
+ im_list_resize = [
2073
+ im.resize((min_width, int(im.height * min_width / im.width)), resample=resample)
2074
+ for im in im_list
2075
+ ]
2076
+ total_height = sum(im.height for im in im_list_resize)
2077
+ dst = Image.new("RGB", (min_width, total_height))
2078
+ pos_y = 0
2079
+ for im in im_list_resize:
2080
+ dst.paste(im, (0, pos_y))
2081
+ pos_y += im.height
2082
+ return dst
2083
+
2084
+
2085
+ def get_concat_tile_resize(im_list_2d, resample=Image.Resampling.BICUBIC):
2086
+ im_list_v = [
2087
+ get_concat_h_multi_resize(im_list_h, resample=resample)
2088
+ for im_list_h in im_list_2d
2089
+ ]
2090
+ return get_concat_v_multi_resize(im_list_v, resample=resample)
2091
+
2092
+
2093
+ def get_tiles(img_list, row_count, resample=Image.Resampling.BICUBIC):
2094
+ if isinstance(img_list, np.ndarray) is False:
2095
+ img_list = np.asarray(img_list, dtype="object")
2096
+ return get_concat_tile_resize(np.split(img_list, row_count), resample)