Coverage for /home/ubuntu/cv-depot/python/cv_depot/ops/filter.py: 99%
123 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-08 20:26 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-08 20:26 +0000
1from typing import Any, Optional, Union # noqa F401
2from cv_depot.core.types import AnyColor # noqa F401
4import logging
6from lunchbox.enforce import Enforce
7from lunchbox.stopwatch import StopWatch
8import cv2
9import numpy as np
11from cv_depot.core.image import BitDepth, Image
12from cv_depot.core.color import BasicColor, Color
14LOGGER = logging.getLogger(__name__)
15# ------------------------------------------------------------------------------
18def gamma(image, value=1.0):
19 # type: (Image, float) -> Image
20 '''
21 Apply gamma correction to given image.
23 Args:
24 image (Image): Image to be modified.
25 value (int): Gamma value. Default: 1.0.
27 Raises:
28 EnforceError: If image is not an instance of Image.
29 EnforceError: If value is less than 0.
31 Returns:
32 Image: Gamma adjusted image.
33 '''
34 Enforce(image, 'instance of', Image)
35 Enforce(value, '>=', 0)
36 # --------------------------------------------------------------------------
38 bit_depth = image.bit_depth
39 array = image.to_bit_depth(BitDepth.FLOAT32).data
40 array = array**(1 / value)
41 output = Image.from_array(array).to_bit_depth(bit_depth)
42 return output
45def canny_edges(image, size=0):
46 # type: (Image, int) -> Image
47 '''
48 Apply a canny edge detection to given image.
50 Args:
51 image (Image): Image.
52 size (int, optional): Amount of dilation applied to result. Default: 0.
54 Raises:
55 EnforceError: If image is not an instance of Image.
56 EnforceError: If size is not an integer >= 0.
58 Returns:
59 Image: Edge detected image.
60 '''
61 Enforce(image, 'instance of', Image)
62 Enforce(size, 'instance of', int)
63 Enforce(size, '>=', 0)
64 # --------------------------------------------------------------------------
66 img = image.to_bit_depth(BitDepth.UINT8).data
67 img = cv2.Canny(img, 0, 0)
68 kernel = np.ones((3, 3), np.uint8)
69 img = cv2.dilate(img, kernel, iterations=size)
70 output = Image.from_array(img).to_bit_depth(image.bit_depth)
71 return output
74def tophat(image, amount, kind='open'):
75 # type: (Image, int, str) -> Image
76 '''
77 Apply tophat morphology operation to given image.
79 Args:
80 image (Image): Image to be modified.
81 amount (int): Amount of tophat operation.
82 kind (str, optional): Kind of operation to be performed.
83 Options include: open, close. Default: open.
85 Raises:
86 EnforceError: If image is not an instance of Image.
87 EnforceError: If amount is less than 0.
88 EnforceError: If kind is not one of: open, close.
90 Returns:
91 Image: Image with tophat operation applied.
92 '''
93 Enforce(image, 'instance of', Image)
94 Enforce(amount, '>=', 0)
95 msg = 'Illegal tophat kind: {a}. Legal tophat kinds: {b}.'
96 Enforce(kind, 'in', ['open', 'close'], message=msg)
97 # --------------------------------------------------------------------------
99 lut = dict(open=cv2.MORPH_CLOSE, close=cv2.MORPH_OPEN)
100 opt = lut[kind]
101 bit_depth = image.bit_depth
102 img = image.to_bit_depth(BitDepth.FLOAT32).data
103 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
104 arr = cv2 \
105 .morphologyEx(img, opt, kernel, iterations=amount) \
106 .astype(np.float32)
107 output = Image.from_array(arr).to_bit_depth(bit_depth)
108 return output
111def linear_lookup(lower=0, upper=1):
112 # type: (float, float) -> np.vectorize
113 r'''
114 Generates a linear lookup table with an upper and lower shoulder.
116 .. image:: images/linear_lut.png
118 Args:
119 lower (float, optional): Lower shoulder value. Default: 0.
120 upper (float, optional): Upper shoulder value. Default: 1.
122 Returns:
123 numpy.vectorize: Anonymous function that applies lut elementwise to a
124 given numpy array.
125 '''
126 lut = lambda x: min(max((x - lower), 0) * (1 / (upper - lower)), 1)
127 return np.vectorize(lut)
130def linear_smooth(image, blur=3, lower=0, upper=1):
131 # type: (Image, int, float, float) -> Image
132 '''
133 Blur given image then apply linear thresholding it.
135 Args:
136 image (Image): Image matte to be smoothed.
137 blur (int, optional): Size of blur. Default: 3.
138 lower (float, optional): Lower shoulder value. Default: 0.
139 upper (float, optional): Upper shoulder value. Default: 1.
141 Raises:
142 EnforceError: If image is not an instance of Image.
143 EnforceError: If blur is less than 0.
144 EnforceError: If lower or upper is less than 0.
145 EnforceError: If lower or upper is greater than 1.
146 EnforceError: If lower is greater than upper.
148 Returns:
149 Image: Smoothed image.
150 '''
151 Enforce(image, 'instance of', Image)
152 Enforce(blur, '>=', 0)
153 Enforce(lower, '>=', 0)
154 Enforce(lower, '<=', 1)
155 Enforce(upper, '>=', 0)
156 Enforce(upper, '<=', 1)
157 Enforce(lower, '>=', 0)
158 msg = 'Lower bound cannot be greater than upper bound. {a} > {b}'
159 Enforce(lower, '<=', upper, message=msg)
160 # --------------------------------------------------------------------------
162 bit_depth = image.bit_depth
163 img = image.to_bit_depth(BitDepth.FLOAT32).data
164 img = cv2.blur(img, (blur, blur))
165 lut = linear_lookup(lower=lower, upper=upper)
166 img = lut(img).astype(np.float32)
167 output = Image.from_array(img).to_bit_depth(bit_depth)
168 return output
171def key_exact_color(image, color, channel='a', invert=False):
172 # type: (Image, AnyColor, str, bool) -> Image
173 '''
174 Keys given image according to the color of its pixels values.
175 Where that pixel color exactly matches the given color, the mask channel
176 will be 1, otherwise it will be 0.
178 Args:
179 image (Image): Image to be evaluated.
180 color (Color or BasicColor): Color to be used for masking.
181 channel (str, optional): Mask channel name. Default: a.
182 invert (bool, optional): Whether to invert the mask. Default: False.
184 Raises:
185 EnforceError: If image is not an Image instance.
186 EnforceError: If channel is not a string.
187 EnforceError: If invert is not a boolean.
188 EnforceError: If RGB is not found in image channels.
190 Returns:
191 Image: Image with mask channel.
192 '''
193 Enforce(image, 'instance of', Image)
194 Enforce(channel, 'instance of', str)
195 Enforce(invert, 'instance of', bool)
196 # --------------------------------------------------------------------------
198 # get color
199 if isinstance(color, str):
200 color = BasicColor.from_string(color)
201 if isinstance(color, BasicColor):
202 color = Color.from_basic_color(color)
203 clr = color.to_array()
205 # determine num channels
206 rgb = list('rgb')
207 img = image.to_bit_depth(BitDepth.FLOAT32)
208 if image.num_channels == 1:
209 x = img.data[..., np.newaxis]
210 x = np.concatenate([x, x, x], axis=2)
211 img = Image.from_array(x)
212 else:
213 diff = sorted(list(set(rgb).difference(image.channels)))
214 msg = f'{diff} not found in image channels. '
215 msg += f'Given channels: {image.channels}.'
216 Enforce(len(diff), '==', 0, message=msg)
218 # create mask
219 mask = np.equal(clr, img[:, :, rgb].data)
220 mask = np.apply_along_axis(all, 2, mask) \
221 .astype(np.float32)[..., np.newaxis]
222 if invert:
223 mask = -1 * mask + 1
225 # add mask to image
226 chans = image.channels
227 if image.num_channels == 1:
228 arr = img[:, :, 'r'].data[..., np.newaxis]
229 else:
230 chans = list(filter(lambda x: x != channel, img.channels))
231 arr = img[:, :, chans].data
233 arr = np.concatenate([arr, mask], axis=2)
234 output = Image.from_array(arr).to_bit_depth(image.bit_depth)
235 output = output.set_channels(chans + [channel])
236 return output
239def kmeans(
240 image, # type: Image
241 num_centroids=10, # type: int
242 centroids=None, # type: Optional[list[tuple[int, int, int]]]
243 max_iterations=100, # type: int
244 accuracy=1.0, # type: float
245 epochs=10, # type: int
246 seeding='random', # type: str
247 generate_report=False # type: bool
248): # type: (...) -> Union[Image, tuple[Image, dict]]
249 '''
250 Applies k-means to the given image.
252 Args:
253 image (Image): Image instance.
254 num_centroids (int, optional): Number of centroids to use. Default: 10.
255 centroids (list, optional): List of triplets. Default: None.
256 max_iterations (int, optional): Maximum number of k-means updates
257 allowed per centroid. Default: 100.
258 accuracy (float, optional): Minimum accuracy required of clusters.
259 epochs (int, optional): Number of times algorithm is applied with
260 different initial labels. Default: 10.
261 seeding (str, optional): How intial centroids are generated. Default:
262 random. Options include: random, pp_centers.
263 generate_report (bool, optional): If true returns report in addition to
264 image. Default: False.
266 Raises:
267 EnforceError: If image is not an Image instance.
268 ValueError: If invalid seeding option is given.
270 Returns:
271 Image or tuple[Image, dict]: K-means image or K-means image and K-means
272 report.
273 '''
274 Enforce(image, 'instance of', Image)
276 stopwatch = StopWatch()
277 stopwatch.start()
278 # --------------------------------------------------------------------------
280 source_bit_depth = image.bit_depth
281 data = image.to_bit_depth(BitDepth.FLOAT32)\
282 .data.reshape((-1, image.num_channels))
284 seed = None
285 if seeding == 'random':
286 seed = cv2.KMEANS_RANDOM_CENTERS
287 elif seeding == 'pp_centers':
288 seed = cv2.KMEANS_PP_CENTERS
289 else:
290 msg = f'{seeding} is not a valid seeding option. Options include: '
291 msg += '[random, pp_centers].'
292 raise ValueError(msg)
294 # terminate centroid updates when max iteration or min accuracy is achieved
295 crit = cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
297 if centroids is not None:
298 num_centroids = len(centroids)
300 compactness, labels, centroids = cv2.kmeans(
301 data=data,
302 K=num_centroids,
303 bestLabels=centroids,
304 criteria=(crit, max_iterations, accuracy),
305 attempts=epochs,
306 flags=seed,
307 ) # type: ignore
309 centroids_ = centroids # type: Any
310 output = np.float32(centroids_)[labels.flatten()] # type: ignore
311 output = output.reshape(image.data.shape)
312 output = Image.from_array(output).to_bit_depth(source_bit_depth)
314 if generate_report:
315 report = dict(
316 compactness=compactness,
317 labels=labels,
318 centroids=centroids
319 )
320 stopwatch.stop()
321 LOGGER.warning(f'KmeansRuntime: {stopwatch.human_readable_delta}.')
322 return output, report
324 stopwatch.stop()
325 LOGGER.warning(f'Kmeans Runtime: {stopwatch.human_readable_delta}.')
326 return output