├── .github
└── workflows
│ ├── publish.yml
│ └── testing.yml
├── .gitignore
├── LICENSE
├── README.md
├── cvpack
├── __init__.py
├── cvtypes.py
├── extras
│ ├── __init__.py
│ ├── clustering.py
│ ├── imgproc.py
│ ├── iterators.py
│ └── matlab.py
├── imgio.py
├── py.typed
└── videoio.py
├── mypy.ini
├── poetry.lock
├── pyproject.toml
└── tests
├── __init__.py
├── test_point_size.py
├── test_rect.py
└── test_rotated_rect.py
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Poetry when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip poetry
24 | - name: Build and publish
25 | env:
26 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
27 | run: |
28 | poetry build
29 | poetry publish -u __token__ -p $PYPI_TOKEN
30 |
--------------------------------------------------------------------------------
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Static check and testing
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: [3.6, 3.7, 3.8, 3.9]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip poetry
29 | poetry install -n
30 | - name: Check format with black
31 | run: poetry run black cvpack/ tests/ --check
32 | - name: Static typing
33 | run: poetry run mypy cvpack --strict
34 | - name: Test with pytest
35 | run: poetry run pytest
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 | **/.pytest_cache
3 | **/.hypothesis
4 | .DS_Store
5 | .venv/
6 | .vscode/
7 | docs/
8 | dist/
9 | venv/
10 | .idea/
11 | *.egg-info/
12 | opencv-cpp-test/
13 | *.egg-info/
14 | cpp/
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alexander Reynolds
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cvpack
2 |
3 | OpenCV extensions for more Pythonic interactions.
4 |
5 | ## Install
6 |
7 | ```sh
8 | pip install cvpack-alkasm
9 | ```
10 |
11 | ## Types
12 |
13 | `cvpack` includes types that exist in the main C++ OpenCV codebase, but that aren't included in the Python bindings. They are compatible as arguments to OpenCV functions, and they implement the same interfaces (with some new additions). The types that are included are `Point`, `Point3`, `Rect`, `RotatedRect`, `Size`, `TermCriteria`. They are implemented as namedtuples, and as such are immutable.
14 |
15 | ```python
16 | import cvpack
17 |
18 | img = cvpack.imread("img.png")
19 | p1 = cvpack.Point(50, 50)
20 | p2 = cvpack.Point(100, 100)
21 | rect = cvpack.Rect.from_points(p1, p2)
22 | roi = img[rect.slice()]
23 | roi_size = cvpack.Size.from_image(roi)
24 | assert roi_size == rect.size()
25 | ```
26 |
27 | The overloaded constructors are available as `from_` classmethods, like `from_points` shown above. They also follow the same operator overloads that OpenCV has: two points summed is a point, adding a point to a rectangle shifts it, you can `&` two rectangles to get the intersection as a new rectangle, and so on.
28 |
29 | ## Image IO
30 |
31 | Wrappers for `imread`, `imwrite`, and `imshow` simplify usage by checking errors and allowing path-like objects for path arguments. Additionally, `cvpack` provides functions to read images from a URL (`imread_url`), display to a browser (`imshow_browser`) for statically serving images while working in an interpreter, and displaying images in a Jupyter notebook (`imshow_jupyter`) as HTML directly rather than the typical `plt.imshow` from `matplotlib`. Some other utilities related to display are also included.
32 |
33 | ```python
34 | from pathlib import Path
35 | import cvpack
36 |
37 | for path in Path("folder").glob("*.png"):
38 | img = cvpack.imread(path)
39 | big = cvpack.add_grid(cvpack.enlarge(img))
40 | cvpack.imshow_browser(img, route=str(path))
41 | ```
42 |
43 | ## Video IO
44 |
45 | Working with video requires acquiring and releasing resources, so `cvpack` provides context managers for video readers and writers which wrap the classes from OpenCV. Reading video frames is simplified to iterating over the capture object.
46 |
47 | ```python
48 | import cv2
49 | import cvpack
50 |
51 | with cvpack.VideoCapture("video.mp4") as cap:
52 | with cvpack.VideoWriter("reversed.mp4", fourcc=int(cap.fourcc), fps=cap.fps) as writer:
53 | for frame in cap:
54 | flipped = cv2.flip(frame, 0)
55 | writer.write(flipped)
56 | ```
57 |
--------------------------------------------------------------------------------
/cvpack/__init__.py:
--------------------------------------------------------------------------------
1 | """Utilities for computer vision."""
2 |
3 | from .imgio import *
4 | from .cvtypes import *
5 | from .videoio import *
6 |
--------------------------------------------------------------------------------
/cvpack/cvtypes.py:
--------------------------------------------------------------------------------
1 | """
2 | This module mimics some of the OpenCV built-in types that don't get translated
3 | into Python directly. Generally, these just get mapped to/from tuples.
4 |
5 | However, a lot of OpenCV code would benefit from types that describe the tuple,
6 | in addition to named attributes. As OpenCV expects tuples for these datatypes,
7 | subclassing from tuples and namedtuples allows for flexibility without breaking
8 | compatibility.
9 |
10 | Wherever it made sense, functionality was copied from OpenCV. For overloaded
11 | CPP functions, there is not a hard rule for deciding which version becomes the
12 | defacto Python method, but in some cases, both have been provided. Alternative
13 | constructors are given in the usual Python way---as classmethods prepended with
14 | the verb "from".
15 |
16 | Some liberties have been taken with respect to naming. In particular, camelCase
17 | method names have been translated to snake_case. Some methods and attributes
18 | are provided that OpenCV doesn't contain.
19 |
20 | Aside from Scalars (where you can use a numpy array for vector operations),
21 | the usual arithmetic operations available in OpenCV are available here.
22 |
23 | You can average two points by adding them and dividing by two; you can add a
24 | point to a rect to shift it; and so on.
25 | """
26 |
27 | from typing import NamedTuple, Tuple, Optional, Union
28 | from collections.abc import Sequence
29 | import enum
30 | import numpy as np
31 | import cv2 as cv
32 |
33 | SeqOperable = Union[float, Sequence, np.ndarray]
34 |
35 |
36 | class Point(NamedTuple):
37 | x: float
38 | y: float
39 |
40 | def __add__(self, other: SeqOperable) -> "Point":
41 | return self.__class__(*(np.array(self).__add__(np.array(other))))
42 |
43 | def __sub__(self, other: SeqOperable) -> "Point":
44 | return self.__class__(*(np.array(self).__sub__(np.array(other))))
45 |
46 | def __mul__(self, other: SeqOperable) -> "Point":
47 | return self.__class__(*(np.array(self).__mul__(np.array(other))))
48 |
49 | def __truediv__(self, other: SeqOperable) -> "Point":
50 | return self.__class__(*(np.array(self).__truediv__(np.array(other))))
51 |
52 | def __floordiv__(self, other: SeqOperable) -> "Point":
53 | return self.__class__(*(np.array(self).__floordiv__(np.array(other))))
54 |
55 | def __rsub__(self, other: SeqOperable) -> "Point":
56 | return self.__class__(*(np.array(self).__rsub__(np.array(other))))
57 |
58 | def __rtruediv__(self, other: SeqOperable) -> "Point":
59 | return self.__class__(*(np.array(self).__rtruediv__(np.array(other))))
60 |
61 | def __rfloordiv__(self, other: SeqOperable) -> "Point":
62 | return self.__class__(*(np.array(self).__rfloordiv__(np.array(other))))
63 |
64 | def __radd__(self, other: SeqOperable) -> "Point":
65 | return self.__class__(*(np.array(self).__radd__(np.array(other))))
66 |
67 | def __rmul__(self, other: SeqOperable) -> "Point":
68 | return self.__class__(*(np.array(self).__rmul__(np.array(other))))
69 |
70 | def __pos__(self) -> "Point":
71 | return self.__class__(*np.array(self).__pos__())
72 |
73 | def __neg__(self) -> "Point":
74 | return self.__class__(*np.array(self).__neg__())
75 |
76 | def __abs__(self) -> "Point":
77 | return self.__class__(*np.array(self).__abs__())
78 |
79 | def __round__(self, ndigits: Optional[int] = None) -> "Point":
80 | return self.__class__(*(round(v, ndigits=ndigits) for v in self))
81 |
82 | def __floor__(self) -> "Point":
83 | return self.__class__(*np.floor(self))
84 |
85 | def __ceil__(self) -> "Point":
86 | return self.__class__(*np.ceil(self))
87 |
88 | def cross(self, point: "Point") -> float:
89 | return float(np.cross(self, point))
90 |
91 | def dot(self, point: "Point") -> float:
92 | return float(np.dot(self, point))
93 |
94 | def ddot(self, point: "Point") -> float:
95 | return self.dot(point)
96 |
97 | def inside(self, rect: "Rect") -> bool:
98 | """checks whether the point is inside the specified rectangle"""
99 | rect = Rect(*rect)
100 | return rect.contains(self)
101 |
102 |
103 | class Point3(NamedTuple):
104 | x: float
105 | y: float
106 | z: float
107 |
108 | def __add__(self, other: SeqOperable) -> "Point3":
109 | return self.__class__(*(np.array(self).__add__(np.array(other))))
110 |
111 | def __sub__(self, other: SeqOperable) -> "Point3":
112 | return self.__class__(*(np.array(self).__sub__(np.array(other))))
113 |
114 | def __mul__(self, other: SeqOperable) -> "Point3":
115 | return self.__class__(*(np.array(self).__mul__(np.array(other))))
116 |
117 | def __truediv__(self, other: SeqOperable) -> "Point3":
118 | return self.__class__(*(np.array(self).__truediv__(np.array(other))))
119 |
120 | def __floordiv__(self, other: SeqOperable) -> "Point3":
121 | return self.__class__(*(np.array(self).__floordiv__(np.array(other))))
122 |
123 | def __rsub__(self, other: SeqOperable) -> "Point3":
124 | return self.__class__(*(np.array(self).__rsub__(np.array(other))))
125 |
126 | def __rtruediv__(self, other: SeqOperable) -> "Point3":
127 | return self.__class__(*(np.array(self).__rtruediv__(np.array(other))))
128 |
129 | def __rfloordiv__(self, other: SeqOperable) -> "Point3":
130 | return self.__class__(*(np.array(self).__rfloordiv__(np.array(other))))
131 |
132 | def __radd__(self, other: SeqOperable) -> "Point3":
133 | return self.__class__(*(np.array(self).__radd__(np.array(other))))
134 |
135 | def __rmul__(self, other: SeqOperable) -> "Point3":
136 | return self.__class__(*(np.array(self).__rmul__(np.array(other))))
137 |
138 | def __pos__(self) -> "Point3":
139 | return self.__class__(*np.array(self).__pos__())
140 |
141 | def __neg__(self) -> "Point3":
142 | return self.__class__(*np.array(self).__neg__())
143 |
144 | def __abs__(self) -> "Point3":
145 | return self.__class__(*np.array(self).__abs__())
146 |
147 | def __round__(self, ndigits: Optional[int] = None) -> "Point3":
148 | return self.__class__(*(round(v, ndigits=ndigits) for v in self))
149 |
150 | def __floor__(self) -> "Point3":
151 | return self.__class__(*np.floor(self))
152 |
153 | def __ceil__(self) -> "Point3":
154 | return self.__class__(*np.ceil(self))
155 |
156 | def cross(self, point: "Point3") -> "Point3":
157 | return self.__class__(*np.cross(self, point))
158 |
159 | def dot(self, point: "Point3") -> float:
160 | return float(np.dot(self, point))
161 |
162 | def ddot(self, point: "Point3") -> float:
163 | return self.dot(point)
164 |
165 |
166 | class Size(NamedTuple):
167 | width: float
168 | height: float
169 |
170 | def __add__(self, other: SeqOperable) -> "Size":
171 | return self.__class__(*(np.array(self).__add__(np.array(other))))
172 |
173 | def __sub__(self, other: SeqOperable) -> "Size":
174 | return self.__class__(*(np.array(self).__sub__(np.array(other))))
175 |
176 | def __mul__(self, other: SeqOperable) -> "Size":
177 | return self.__class__(*(np.array(self).__mul__(np.array(other))))
178 |
179 | def __truediv__(self, other: SeqOperable) -> "Size":
180 | return self.__class__(*(np.array(self).__truediv__(np.array(other))))
181 |
182 | def __floordiv__(self, other: SeqOperable) -> "Size":
183 | return self.__class__(*(np.array(self).__floordiv__(np.array(other))))
184 |
185 | def __rsub__(self, other: SeqOperable) -> "Size":
186 | return self.__class__(*(np.array(self).__rsub__(np.array(other))))
187 |
188 | def __rtruediv__(self, other: SeqOperable) -> "Size":
189 | return self.__class__(*(np.array(self).__rtruediv__(np.array(other))))
190 |
191 | def __rfloordiv__(self, other: SeqOperable) -> "Size":
192 | return self.__class__(*(np.array(self).__rfloordiv__(np.array(other))))
193 |
194 | def __radd__(self, other: SeqOperable) -> "Size":
195 | return self.__class__(*(np.array(self).__radd__(np.array(other))))
196 |
197 | def __rmul__(self, other: SeqOperable) -> "Size":
198 | return self.__class__(*(np.array(self).__rmul__(np.array(other))))
199 |
200 | def __pos__(self) -> "Size":
201 | return self.__class__(*np.array(self).__pos__())
202 |
203 | def __neg__(self) -> "Size":
204 | return self.__class__(*np.array(self).__neg__())
205 |
206 | def __abs__(self) -> "Size":
207 | return self.__class__(*np.array(self).__abs__())
208 |
209 | def __round__(self, ndigits: Optional[int] = None) -> "Size":
210 | return self.__class__(*(round(v, ndigits=ndigits) for v in self))
211 |
212 | def __floor__(self) -> "Size":
213 | return self.__class__(*np.floor(self))
214 |
215 | def __ceil__(self) -> "Size":
216 | return self.__class__(*np.ceil(self))
217 |
218 | def area(self) -> float:
219 | return self.height * self.width
220 |
221 | def empty(self) -> bool:
222 | """true if empty"""
223 | return self.width <= 0 or self.height <= 0
224 |
225 | @classmethod
226 | def from_image(cls, image: np.ndarray) -> "Size":
227 | h, w = image.shape[:2]
228 | return cls(w, h)
229 |
230 |
231 | class Rect(NamedTuple):
232 | """Mimics cv::Rect while maintaining compatibility with OpenCV's Python bindings.
233 |
234 | Reference: https://docs.opencv.org/master/d2/d44/classcv_1_1Rect__.html
235 | """
236 |
237 | x: float
238 | y: float
239 | width: float
240 | height: float
241 |
242 | def __add__(self, other: Union[Point, Size]) -> "Rect": # type: ignore[override]
243 | """Shift or alter the size of the rectangle.
244 | rect ± point (shifting a rectangle by a certain offset)
245 | rect ± size (expanding or shrinking a rectangle by a certain amount)
246 | """
247 | if isinstance(other, Point):
248 | origin = Point(self.x + other.x, self.y + other.y)
249 | return self.from_origin(origin, self.size())
250 | elif isinstance(other, Size):
251 | size = Size(self.width + other.width, self.height + other.height)
252 | return self.from_origin(self.tl(), size)
253 | raise NotImplementedError(
254 | "Adding to a rectangle generically is ambiguous.\n"
255 | "Add a Point to shift the top-left point, or a Size to expand the rectangle."
256 | )
257 |
258 | def __sub__(self, other: Union[Point, Size]) -> "Rect":
259 | """Shift or alter the size of the rectangle.
260 | rect ± point (shifting a rectangle by a certain offset)
261 | rect ± size (expanding or shrinking a rectangle by a certain amount)
262 | """
263 | if isinstance(other, Point):
264 | origin = Point(self.x - other.x, self.y - other.y)
265 | return self.from_origin(origin, self.size())
266 | elif isinstance(other, Size):
267 | w = max(self.width - other.width, 0)
268 | h = max(self.height - other.height, 0)
269 | return self.from_origin(self.tl(), Size(w, h))
270 | raise NotImplementedError(
271 | "Subtracting from a rectangle generically is ambiguous.\n"
272 | "Subtract a Point to shift the top-left point, or a Size to shrink the rectangle."
273 | )
274 |
275 | def __and__(self, other: "Rect") -> "Rect":
276 | """rectangle intersection"""
277 | x = max(self.x, other.x)
278 | y = max(self.y, other.y)
279 | w = min(self.x + self.width, other.x + other.width) - x
280 | h = min(self.y + self.height, other.y + other.height) - y
281 |
282 | return (
283 | self.__class__(0, 0, 0, 0)
284 | if (w <= 0 or h <= 0)
285 | else self.__class__(x, y, w, h)
286 | )
287 |
288 | def __or__(self, other: "Rect") -> "Rect":
289 | """minimum area rectangle containing self and other."""
290 | if self.empty():
291 | return other
292 | elif not other.empty():
293 | x = min(self.x, other.x)
294 | y = min(self.y, other.y)
295 | w = max(self.x + self.width, other.x + other.width) - x
296 | h = max(self.y + self.height, other.y + other.height) - y
297 | return self.__class__(x, y, w, h)
298 | return self
299 |
300 | def tl(self) -> Point:
301 | """top left point"""
302 | return Point(self.x, self.y)
303 |
304 | def br(self) -> Point:
305 | """bottom right point"""
306 | return Point(self.x + self.width, self.y + self.height)
307 |
308 | def area(self) -> float:
309 | return self.height * self.width
310 |
311 | def size(self) -> Size:
312 | """size (width, height) of the rectangle"""
313 | return Size(self.width, self.height)
314 |
315 | def contains(self, point: Point) -> bool:
316 | """checks whether the rectangle contains the point"""
317 | return (
318 | self.x <= point.x <= self.x + self.width
319 | and self.y <= point.y <= self.y + self.height
320 | )
321 |
322 | def empty(self) -> bool:
323 | """true if empty"""
324 | return self.width <= 0 or self.height <= 0
325 |
326 | @classmethod
327 | def from_points(cls, top_left: Point, bottom_right: Point) -> "Rect":
328 | """Alternative constructor using two points."""
329 | x1, y1 = top_left
330 | x2, y2 = bottom_right
331 | w = x2 - x1
332 | h = y2 - y1
333 | return cls(x1, y1, w, h)
334 |
335 | @classmethod
336 | def from_origin(cls, origin: Point, size: Size) -> "Rect":
337 | """Alternative constructor using a point and size."""
338 | x, y = origin
339 | w, h = size
340 | return cls(x, y, w, h)
341 |
342 | @classmethod
343 | def from_center(cls, center: Point, size: Size) -> "Rect":
344 | """Alternative constructor using a center point and size."""
345 | w, h = size
346 | xc, yc = center
347 | x = xc - w / 2
348 | y = yc - h / 2
349 | return cls(x, y, w, h)
350 |
351 | def slice(self) -> Tuple[slice, slice]:
352 | """Returns a slice for a numpy array. Not included in OpenCV.
353 |
354 | img[rect.slice()] == img[rect.y : rect.y + rect.height, rect.x : rect.x + rect.width]
355 | """
356 | return slice(self.y, self.y + self.height), slice(self.x, self.x + self.width)
357 |
358 | def center(self) -> Point:
359 | """Returns the center of the rectangle as a point (xc, yc). Not included in OpenCV.
360 |
361 | rect.center() == (rect.x + rect.width / 2, rect.y + rect.height / 2)
362 | """
363 | return Point(self.x + self.width / 2, self.y + self.height / 2)
364 |
365 | def intersection(self, other: "Rect") -> float:
366 | """Return the area of the intersection of two rectangles. Not included in OpenCV."""
367 | if self.empty() or other.empty():
368 | return 0
369 | w = min(self.x + self.width, other.x + other.width) - max(self.x, other.x)
370 | h = min(self.y + self.height, other.y + other.height) - max(self.y, other.y)
371 | return w * h
372 |
373 | def union(self, other: "Rect") -> float:
374 | """Return the area of the union of two rectangles. Not included in OpenCV."""
375 | return self.area() + other.area() - self.intersection(other)
376 |
377 |
378 | class RotatedRect(NamedTuple):
379 | center: Point
380 | size: Size
381 | angle: float
382 |
383 | def bounding_rect(self) -> Rect:
384 | """returns the minimal rectangle containing the rotated rectangle"""
385 | pts = self.points()
386 | r = Rect.from_points(
387 | Point(np.floor(min(pt.x for pt in pts)), np.floor(min(pt.y for pt in pts))),
388 | Point(np.ceil(max(pt.x for pt in pts)), np.ceil(max(pt.y for pt in pts))),
389 | )
390 | return r
391 |
392 | def points(self) -> Tuple[Point, Point, Point, Point]:
393 | """returns 4 vertices of the rectangle. The order is bottom left, top left, top right, bottom right."""
394 | b = np.cos(np.radians(self.angle)) * 0.5
395 | a = np.sin(np.radians(self.angle)) * 0.5
396 |
397 | pt0 = Point(
398 | self.center.x - a * self.size.height - b * self.size.width,
399 | self.center.y + b * self.size.height - a * self.size.width,
400 | )
401 | pt1 = Point(
402 | self.center.x + a * self.size.height - b * self.size.width,
403 | self.center.y - b * self.size.height - a * self.size.width,
404 | )
405 |
406 | pt2 = Point(2 * self.center.x - pt0.x, 2 * self.center.y - pt0.y)
407 | pt3 = Point(2 * self.center.x - pt1.x, 2 * self.center.y - pt1.y)
408 |
409 | return pt0, pt1, pt2, pt3
410 |
411 | @classmethod
412 | def from_points(cls, point1: Point, point2: Point, point3: Point) -> "RotatedRect":
413 | """Any 3 end points of the RotatedRect. They must be given in order (either clockwise or anticlockwise)."""
414 | center = (point1 + point3) * 0.5
415 | vecs = [point1 - point2, point2 - point3]
416 | x = max(np.linalg.norm(pt) for pt in (point1, point2, point3))
417 | a = min(np.linalg.norm(vecs[0]), np.linalg.norm(vecs[1]))
418 |
419 | # check that given sides are perpendicular
420 | if abs(vecs[0].dot(vecs[1])) * a > np.finfo(np.float32).eps * 9 * x * (
421 | np.linalg.norm(vecs[0]) * np.linalg.norm(vecs[1])
422 | ):
423 | raise ValueError(
424 | "The three points do not define a rotated rect. The three points should form a right triangle."
425 | )
426 |
427 | # wd_i stores which vector (0,1) or (1,2) will make the width
428 | # One of them will definitely have slope within -1 to 1
429 | wd_i = 1 if abs(vecs[1][1]) < abs(vecs[1][0]) else 0
430 | ht_i = (wd_i + 1) % 2
431 |
432 | angle = np.degrees(np.arctan2(vecs[wd_i][1], vecs[wd_i][0]))
433 | width = np.linalg.norm(vecs[wd_i])
434 | height = np.linalg.norm(vecs[ht_i])
435 | size = Size(width, height)
436 |
437 | return cls(center, size, angle)
438 |
439 |
440 | class TermCriteria(NamedTuple):
441 | class Type(enum.IntFlag): # type: ignore[misc]
442 | COUNT: int = cv.TermCriteria_COUNT
443 | MAX_ITER: int = cv.TermCriteria_MAX_ITER
444 | EPS: int = cv.TermCriteria_EPS
445 |
446 | # Without this, the MAX_ITER alias won't show up in some interpreters
447 | Type._member_names_ = ["COUNT", "MAX_ITER", "EPS"] # type: ignore[misc]
448 |
449 | type: int = Type.COUNT
450 | max_count: int = 0
451 | epsilon: float = 0
452 |
453 | def is_valid(self) -> bool:
454 | is_count = bool(self.type & self.Type.COUNT) and self.max_count > 0
455 | is_eps = bool(self.type & self.Type.EPS) and not np.isnan(self.epsilon)
456 | return is_count or is_eps
457 |
--------------------------------------------------------------------------------
/cvpack/extras/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alkasm/cvpack/7d7aceddf18aca03ac77ccf8e0da7f71ef6674a3/cvpack/extras/__init__.py
--------------------------------------------------------------------------------
/cvpack/extras/clustering.py:
--------------------------------------------------------------------------------
1 | # type: ignore
2 |
3 | import cv2
4 | import numpy as np
5 |
6 |
7 | TWO_PI = 2 * np.pi
8 |
9 |
10 | def kmeans_periodic(columns, intervals, data, *args, **kwargs):
11 | """Runs kmeans with periodicity in a subset of dimensions.
12 |
13 | Transforms columns with periodicity on the specified intervals into two
14 | columns with coordinates on the unit circle for kmeans. After running
15 | through kmeans, the centers are transformed back to the range specified
16 | by the intervals.
17 |
18 | Arguments
19 | ---------
20 | columns : sequence
21 | Sequence of indexes specifying the columns that have periodic data
22 | intervals : sequence of length-2 sequences
23 | Sequence of (min, max) intervals, one interval per column
24 | See help(cv2.kmeans) for all other arguments, which are passed through.
25 |
26 | Returns
27 | -------
28 | See help(cv2.kmeans) for outputs, which are passed through; except centers,
29 | which is modified so that it returns centers corresponding to the input
30 | data, instead of the transformed data.
31 |
32 | Raises
33 | ------
34 | cv2.error
35 | If len(columns) != len(intervals)
36 | """
37 |
38 | # Check each periodic column has an associated interval
39 | if len(columns) != len(intervals):
40 | raise cv2.error("number of intervals must be equal to number of columns")
41 |
42 | ndims = data.shape[1]
43 | ys = []
44 |
45 | # transform each periodic column into two columns with the x and y coordinate
46 | # of the angles for kmeans; x coord at original column, ys are appended
47 | for col, interval in zip(columns, intervals):
48 | a, b = min(interval), max(interval)
49 | width = b - a
50 | data[:, col] = TWO_PI * (data[:, col] - a) / width % TWO_PI
51 | ys.append(width * np.sin(data[:, col]))
52 | data[:, col] = width * np.cos(data[:, col])
53 |
54 | # append the ys to the end
55 | ys = np.array(ys).transpose()
56 | data = np.hstack((data, ys)).astype(np.float32)
57 |
58 | # run kmeans
59 | retval, bestLabels, centers = cv2.kmeans(data, *args, **kwargs)
60 |
61 | # transform the centers back to range they came from
62 | for i, (col, interval) in enumerate(zip(columns, intervals)):
63 | a, b = min(interval), max(interval)
64 | angles = np.arctan2(centers[:, ndims + i], centers[:, col]) % TWO_PI
65 | centers[:, col] = a + (b - a) * angles / TWO_PI
66 | centers = centers[:, :ndims]
67 |
68 | return retval, bestLabels, centers
69 |
--------------------------------------------------------------------------------
/cvpack/extras/imgproc.py:
--------------------------------------------------------------------------------
1 | # type: ignore
2 |
3 | import cv2 as cv
4 | import numpy as np
5 |
6 |
7 | def resize_pad(img, size, pad_color=(0, 0, 0), interpolation=None):
8 |
9 | h, w = img.shape[:2]
10 | sw, sh = size
11 | aspect = w / h
12 |
13 | if interpolation is None:
14 | interpolation = cv.INTER_AREA if (h > sh) or (w > sw) else cv.INTER_CUBIC
15 |
16 | # compute scaling and pad sizing
17 | if aspect > 1:
18 | nh = int(np.round(sw / aspect))
19 | new_size = (sw, nh)
20 | pad_vert = (sh - nh) // 2
21 | padding = (pad_vert, sh - pad_vert, 0, 0)
22 | elif aspect < 1:
23 | nw = int(np.round(sh * aspect))
24 | new_size = (nw, sh)
25 | pad_horz = (sw - nw) // 2
26 | padding = (0, 0, pad_horz, sw - pad_horz)
27 | else:
28 | new_size = (sw, sh)
29 | padding = (0, 0, 0, 0)
30 |
31 | # scale and pad
32 | scaled_img = cv.resize(img, new_size, interpolation=interpolation)
33 | scaled_img = cv.copyMakeBorder(
34 | scaled_img, *padding, borderType=cv.BORDER_CONSTANT, value=pad_color
35 | )
36 |
37 | return scaled_img
38 |
39 |
40 | def circular_gradient(w, h, center=None, radius=None, invert=False):
41 | """Makes a gradient, white at the center point, fading out to entirely black
42 | at radius. Colors are flipped if invert == True.
43 |
44 | By default, center is the center of (w, h) and radius is the distance to
45 | the nearer edge.
46 | """
47 | center = center or (w // 2, h // 2)
48 | radius = radius or min(w, h) // 2
49 |
50 | mask = np.zeros((h, w), dtype=np.uint8)
51 | mask = cv.circle(mask, center=center, radius=radius, color=255, thickness=-1)
52 | grad = cv.distanceTransform(mask, cv.DIST_L2, 3)
53 | grad = grad / grad.max()
54 |
55 | if invert:
56 | grad = 1 - grad
57 |
58 | return grad
59 |
60 |
61 | def circular_mask(w, h, center=None, radius=None):
62 | """Creates a circular binary/logical mask.
63 |
64 | Parameters
65 | ==========
66 | w, h : int
67 | Width and height of the mask.
68 | center : tuple(numeric, numeric) (default: (h/2, w/2))
69 | The center of the circular mask.
70 | radius : tuple(numeric, numeric) (default: nearest image bound to center)
71 | Radius of the circle extending from the center point. Note that pixels
72 | that touch the radius will be included.
73 |
74 | Returns
75 | =======
76 | mask : np.ndarray
77 | Boolean array with shape (h, w) where values are True inside the circle,
78 | False otherwise.
79 |
80 | Notes
81 | =====
82 | From this Stack Overflow answer: https://stackoverflow.com/a/44874588/5087436
83 | """
84 |
85 | if center is None: # use the middle of the image
86 | center = [int(w / 2), int(h / 2)]
87 | if radius is None: # use the smallest distance between the center and image walls
88 | radius = min(center[0], center[1], w - center[0], h - center[1])
89 |
90 | Y, X = np.ogrid[:h, :w]
91 | dist_from_center = np.sqrt((X - center[0]) ** 2 + (Y - center[1]) ** 2)
92 |
93 | return dist_from_center <= radius
94 |
--------------------------------------------------------------------------------
/cvpack/extras/iterators.py:
--------------------------------------------------------------------------------
1 | # type: ignore
2 |
3 | from itertools import tee
4 |
5 |
6 | def pairwise(iterable):
7 | """s -> (s0, s1), (s1, s2), ..."""
8 | a, b = tee(iterable)
9 | next(b, None)
10 | return zip(a, b)
11 |
12 |
13 | class LineIterator:
14 | """Iterates through the pixels in a line between two points.
15 |
16 | The line will be clipped on the image boundaries
17 | The line can be 4- or 8-connected.
18 | If left_to_right=True, then the iteration is always
19 | done from the left-most point to the right most,
20 | not to depend on the ordering of pt1 and pt2 parameters.
21 | """
22 |
23 | def __init__(self, img, pt1, pt2, connectivity=8, left_to_right=False):
24 |
25 | count = -1
26 | cols, rows = img.shape[:2]
27 | self.step = cols
28 | x1, y1 = pt1
29 | x2, y2 = pt2
30 |
31 | if not (0, 0, 0, 0) <= (x1, x2, y1, y2) < (cols, cols, rows, rows):
32 | clip, pt1, pt2 = cv2.clipLine((0, 0, cols, rows), pt1, pt2)
33 | x1, y1 = pt1
34 | x2, y2 = pt2
35 | if not clip:
36 | self.count = 0
37 | return
38 |
39 | bt_pix = 1
40 | istep = self.step
41 |
42 | dx = x2 - x1
43 | dy = y2 - y1
44 | s = -1 if dx < 0 else 0
45 |
46 | if left_to_right:
47 | dx = (dx ^ s) - s
48 | dy = (dy ^ s) - s
49 | x1 ^= (x1 ^ x2) & s
50 | y1 ^= (y1 ^ y2) & s
51 | else:
52 | dx = (dx ^ s) - s
53 | bt_pix = (bt_pix ^ s) - s
54 |
55 | self.index = y1 * istep + x1
56 |
57 | s = -1 if dy < 0 else 0
58 | dy = (dy ^ s) - s
59 | istep = (istep ^ s) - s
60 |
61 | # conditional swaps
62 | s = -1 if dy > dx else 0
63 | dx ^= dy & s
64 | dy ^= dx & s
65 | dx ^= dy & s
66 |
67 | bt_pix ^= istep & s
68 | istep ^= bt_pix & s
69 | bt_pix ^= istep & s
70 |
71 | assert dx >= 0 and dy >= 0
72 | if connectivity == 8:
73 | self.err = dx - (dy + dy)
74 | self.plus_delta = dx + dx
75 | self.plus_step = int(istep)
76 | self.count = dx + 1
77 | else:
78 | self.err = 0
79 | self.plus_delta = (dx + dx) + (dy + dy)
80 | self.plus_step = int(istep - bt_pix)
81 | self.count = dx + dy + 1
82 | self.minus_delta = -(dy + dy)
83 | self.minus_step = int(bt_pix)
84 |
85 | def __iter__(self):
86 | for i in range(self.count):
87 | y = int(self.index / self.step)
88 | x = int(self.index - y * self.step)
89 | yield (x, y)
90 | mask = -1 if self.err < 0 else 0
91 | self.err += self.minus_delta + (self.plus_delta & mask)
92 | self.index += self.minus_step + (self.plus_step & mask)
93 |
--------------------------------------------------------------------------------
/cvpack/extras/matlab.py:
--------------------------------------------------------------------------------
1 | # type: ignore
2 | import numpy as np
3 | import cv2
4 |
5 |
6 | def bwdist(
7 | img,
8 | method=cv2.DIST_L2,
9 | dist_mask=cv2.DIST_MASK_5,
10 | label_type=cv2.DIST_LABEL_CCOMP,
11 | ravel=True,
12 | ):
13 | """Mimics Matlab's bwdist function, similar to OpenCV's distanceTransform()
14 | but with different output.
15 |
16 | https://www.mathworks.com/help/images/ref/bwdist.html
17 |
18 | Available metrics:
19 | https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gaa2bfbebbc5c320526897996aafa1d8eb
20 |
21 | Available distance masks:
22 | https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#gaaa68392323ccf7fad87570e41259b497
23 |
24 | Available label types:
25 | https://docs.opencv.org/3.4/d7/d1b/group__imgproc__misc.html#ga3fe343d63844c40318ee627bd1c1c42f
26 | """
27 | flip = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV)[1]
28 | dist, labeled = cv2.distanceTransformWithLabels(flip, method, dist_mask)
29 |
30 | if ravel: # return linear indices if ravel == True (default)
31 | idx = np.zeros(img.shape, dtype=np.intp)
32 | idx_func = np.flatnonzero
33 | else: # return two-channel indices if ravel == False
34 | idx = np.zeros((*img.shape, 2), dtype=np.intp)
35 | idx_func = lambda masked: np.dstack(np.where(masked))
36 |
37 | for l in np.unique(labeled):
38 | mask = labeled == l
39 | idx[mask] = idx_func(img * mask)
40 | return dist, idx
41 |
42 |
43 | def imfill(bin_img):
44 | """Fills holes in the input binary image.
45 |
46 | Achieves the same output as the imfill(BW, 'holes') variant.
47 |
48 | https://www.mathworks.com/help/images/ref/imfill.html
49 | """
50 | contours = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
51 | return cv2.drawContours(bin_img, contours, -1, 255, -1)
52 |
--------------------------------------------------------------------------------
/cvpack/imgio.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from concurrent.futures import ThreadPoolExecutor
3 | from http.server import BaseHTTPRequestHandler, HTTPServer
4 | from pathlib import Path
5 | from typing import cast, Any, Optional, Tuple, Union
6 | import urllib.request
7 | import webbrowser
8 | import cv2 as cv
9 | import numpy as np
10 |
11 | __all__ = [
12 | "imread_url",
13 | "imwrite",
14 | "imshow",
15 | "imshow_jupyter",
16 | "imshow_browser",
17 | "color_labels",
18 | "normalize",
19 | "enlarge",
20 | "add_grid",
21 | ]
22 |
23 |
24 | def imread(imgpath: Union[Path, str], *args: Any, **kwargs: Any) -> np.ndarray:
25 | """Reads an image, providing helpful errors on failed reads.
26 |
27 | Allows pathlib.Path objects as well as strings for the imgpath.
28 | """
29 | img = cv.imread(str(imgpath), *args, **kwargs)
30 | if img is None:
31 | p = Path(imgpath)
32 | if p.exists():
33 | raise ValueError("Image is empty!")
34 | raise FileNotFoundError(f"Image path {p.absolute()} doesn't exist!")
35 |
36 | return img
37 |
38 |
39 | def imread_url(url: str, *args: Any, **kwargs: Any) -> np.ndarray:
40 | """Reads an image from a given url.
41 |
42 | Additional args and kwargs passed onto cv.imdecode()
43 | """
44 | r = urllib.request.urlopen(url)
45 | content_type = r.headers.get_content_maintype()
46 | if content_type != "image":
47 | raise ValueError(f"Unknown content type {content_type}.")
48 | buf = np.frombuffer(r.read(), np.uint8)
49 | return cv.imdecode(buf, *args, **kwargs)
50 |
51 |
52 | def imwrite(
53 | imgpath: Union[Path, str], img: np.ndarray, *args: Any, **kwargs: Any
54 | ) -> bool:
55 | """Writes an image, providing helpful errors on failed writes.
56 |
57 | Allows pathlib.Path objects as well as strings for the imgpath.
58 | Will create the directories included in the imgpath if they don't exist.
59 |
60 | Additional args and kwargs passed to cv.imwrite().
61 | """
62 | if img is None:
63 | raise ValueError("Image is empty!")
64 | Path(imgpath).parent.mkdir(parents=True, exist_ok=True)
65 | imgpath = str(imgpath)
66 | return cast(bool, cv.imwrite(imgpath, img, *args, **kwargs))
67 |
68 |
69 | def imshow(img: np.ndarray, wait: int = 0, window_name: str = "") -> int:
70 | """Combines cv.imshow() and cv.waitkey(), and checks for bad image reads."""
71 | if img is None:
72 | raise ValueError(
73 | "Image is empty; ensure you are reading from the correct path."
74 | )
75 | cv.imshow(window_name, img)
76 | return cast(int, cv.waitKey(wait) & 0xFF)
77 |
78 |
79 | def imshow_jupyter(img: np.ndarray) -> None:
80 | """Shows an image in a Jupyter notebook.
81 |
82 | Raises ValueError if img is None or if img cannot be encoded.
83 | """
84 | if img is None:
85 | raise ValueError("Image has no data (img is None).")
86 |
87 | success, encoded = cv.imencode(".png", img)
88 | if not success:
89 | raise ValueError("Error encoding image.")
90 |
91 | try:
92 | from IPython.display import Image, display
93 |
94 | display(Image(encoded))
95 | except ImportError:
96 | print("You must have IPython installed to use the IPython display.")
97 | raise
98 |
99 |
100 | def imshow_browser(
101 | img: np.ndarray, host: str = "localhost", port: int = 32830, route: str = "imshow"
102 | ) -> None:
103 | """Display an image in a browser.
104 |
105 | Spins up a single-request server to serve the image.
106 | Opens the browser to make that request, then shuts down the server.
107 | """
108 |
109 | class ImshowRequestHandler(_ImshowRequestHandler):
110 | imshow_img = img
111 | imshow_route = route
112 |
113 | server = HTTPServer((host, port), ImshowRequestHandler)
114 | with ThreadPoolExecutor(max_workers=1) as executor:
115 | # handle_request() blocks, so submit in an executor.
116 | # the browser can open the window and get served the image,
117 | # at which point the submitted task is completed.
118 | executor.submit(server.handle_request)
119 | webbrowser.open_new(f"http://{host}:{port}/{route}")
120 |
121 |
122 | def _html_imshow(img: np.ndarray) -> str:
123 | success, encoded_img = cv.imencode(".png", img)
124 |
125 | html = """
126 |
cvpack/imshow
127 |
128 |
129 |
130 |
131 |
132 |
133 |

134 |
135 |
136 |
137 | """
138 |
139 | b64_encoded_img = base64.b64encode(encoded_img).decode() if success else ""
140 | return html.format(encoded_img=b64_encoded_img)
141 |
142 |
143 | class _ImshowRequestHandler(BaseHTTPRequestHandler):
144 | imshow_route: str = ""
145 | imshow_img: Optional[np.ndarray] = None
146 |
147 | # handle GET request from browser
148 | def do_GET(self) -> None:
149 | self.send_response(200)
150 | if self.path == f"/{self.imshow_route}":
151 | self.send_header("Content-type", "text/html")
152 | self.end_headers()
153 | self.wfile.write(_html_imshow(self.imshow_img).encode())
154 | return
155 |
156 | # remove logging statements by returning nothing here
157 | def log_message(self, format: str, *args: Any) -> None:
158 | return
159 |
160 |
161 | def color_labels(labels: np.ndarray) -> np.ndarray:
162 | # Map component labels to hue val
163 | label_hue = np.uint8(179 * labels / np.max(labels))
164 | blank_ch = 255 * np.ones_like(label_hue)
165 | labeled_img = cv.merge([label_hue, blank_ch, blank_ch])
166 |
167 | # cvt to BGR for display
168 | labeled_img = cv.cvtColor(labeled_img, cv.COLOR_HSV2BGR)
169 |
170 | # set bg label to black
171 | labeled_img[label_hue == 0] = 0
172 | return labeled_img
173 |
174 |
175 | def normalize(img: np.ndarray) -> np.ndarray:
176 | return cv.normalize(img, None, 0, 255, cv.NORM_MINMAX, cv.CV_8U)
177 |
178 |
179 | def enlarge(img: np.ndarray, scale: int = 10) -> np.ndarray:
180 | return cv.resize(img, None, fx=scale, fy=scale, interpolation=cv.INTER_NEAREST)
181 |
182 |
183 | def add_grid(
184 | img: np.ndarray,
185 | spacing: int = 10,
186 | color: Union[float, Tuple[float, float, float]] = 200,
187 | ) -> np.ndarray:
188 | viz = img.copy()
189 | h, w = img.shape[:2]
190 |
191 | partial_grid = np.zeros((spacing, spacing), dtype=bool)
192 | partial_grid[0, :] = True
193 | partial_grid[:, 0] = True
194 | gridlines = np.tile(partial_grid, (h // spacing, w // spacing))
195 | viz[gridlines] = color
196 |
197 | pad_sizes = ((0, 1), (0, 1), (0, 0)) if len(img.shape) == 3 else ((0, 1), (0, 1))
198 | viz = np.pad(viz, pad_sizes)
199 | viz[-1, :] = color
200 | viz[:, -1] = color
201 |
202 | return viz
203 |
--------------------------------------------------------------------------------
/cvpack/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alkasm/cvpack/7d7aceddf18aca03ac77ccf8e0da7f71ef6674a3/cvpack/py.typed
--------------------------------------------------------------------------------
/cvpack/videoio.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Callable, Iterable, Optional, Type, Union, cast
2 | from pathlib import Path
3 | import cv2 as cv
4 | import numpy as np
5 | from .cvtypes import Size
6 |
7 |
8 | class VideoCaptureProperty:
9 | """Descriptors to alias cap.get(cv.CAP_PROP_*) and cap.set(cv.CAP_PROP_*, value).
10 |
11 | Raises AttributeError when setting if that property is not supported.
12 | """
13 |
14 | _set_err: str = (
15 | "Unable to set the property {p}. The property might not be supported by\n"
16 | "the backend used by the cv.VideoCapture() instance."
17 | )
18 |
19 | def __init__(self, prop: str):
20 | self.prop = prop
21 |
22 | def __get__(
23 | self, obj: "VideoCapture", objtype: Optional[Type["VideoCapture"]] = None
24 | ) -> float:
25 | return cast(float, obj.cap.get(self.prop))
26 |
27 | def __set__(self, obj: "VideoCapture", value: float) -> None:
28 | if not obj.cap.set(self.prop, value):
29 | raise AttributeError(self._set_err.format(p=self.prop))
30 |
31 |
32 | class VideoCapture:
33 | """An adapter for `cv.VideoCapture`, giving a more Pythonic interface."""
34 |
35 | pos_msec = VideoCaptureProperty(cv.CAP_PROP_POS_MSEC)
36 | pos_frames = VideoCaptureProperty(cv.CAP_PROP_POS_FRAMES)
37 | pos_avi_ratio = VideoCaptureProperty(cv.CAP_PROP_POS_AVI_RATIO)
38 | frame_width = VideoCaptureProperty(cv.CAP_PROP_FRAME_WIDTH)
39 | frame_height = VideoCaptureProperty(cv.CAP_PROP_FRAME_HEIGHT)
40 | fps = VideoCaptureProperty(cv.CAP_PROP_FPS)
41 | fourcc = VideoCaptureProperty(cv.CAP_PROP_FOURCC)
42 | frame_count = VideoCaptureProperty(cv.CAP_PROP_FRAME_COUNT)
43 | format = VideoCaptureProperty(cv.CAP_PROP_FORMAT)
44 |
45 | def __init__(self, *args: Any, **kwargs: Any) -> None:
46 | self.cap = cv.VideoCapture(*args, **kwargs)
47 | if not self.cap.isOpened():
48 | raise ValueError(
49 | f"Unable to open video source: args: {args} kwargs: {kwargs}"
50 | )
51 |
52 | def __getattr__(self, key: str) -> Any:
53 | return getattr(self.cap, key)
54 |
55 | def __iter__(self) -> Iterable[np.ndarray]:
56 | """Iterate through frames in the video."""
57 | noread = (False, None)
58 | if self.cap.isOpened():
59 | for _, frame in iter(self.cap.read, noread):
60 | yield frame
61 |
62 | def __enter__(self) -> "VideoCapture":
63 | """Enter the context manager."""
64 | return self
65 |
66 | def __exit__(self, *args: Any, **kwargs: Any) -> None:
67 | """Releases the video capture object on exiting the context manager."""
68 | self.cap.release()
69 |
70 |
71 | class VideoWriter:
72 | filename: str
73 | fourcc: int
74 | fps: float
75 |
76 | _nowriter = object()
77 |
78 | def __init__(
79 | self,
80 | filename: Union[Path, str],
81 | fourcc: Union[int, Iterable[str]] = "mp4v",
82 | fps: float = 30,
83 | frameSize: Any = None,
84 | **kwargs: Any,
85 | ) -> None:
86 | self.filename = str(filename)
87 | self.fourcc = (
88 | fourcc if isinstance(fourcc, int) else cv.VideoWriter_fourcc(*fourcc)
89 | )
90 | self.fps = fps
91 | self._kwargs = kwargs
92 |
93 | # wait to create writer based on first frame if size is not provided
94 | self._writer = (
95 | self._nowriter if frameSize is None else self._makewriter(frameSize)
96 | )
97 |
98 | def __enter__(self) -> "VideoWriter":
99 | return self
100 |
101 | def __exit__(self, *args: Any, **kwargs: Any) -> None:
102 | self.release()
103 |
104 | def _makewriter(self, frame_size: Any) -> cv.VideoWriter:
105 | return cv.VideoWriter(
106 | filename=self.filename,
107 | fourcc=self.fourcc,
108 | fps=self.fps,
109 | frameSize=frame_size,
110 | **self._kwargs,
111 | )
112 |
113 | def write(self, frame: np.ndarray) -> bool:
114 | try:
115 | return cast(bool, self._writer.write(frame))
116 | except AttributeError as e:
117 | if self._writer is self._nowriter:
118 | size = Size.from_image(frame)
119 | self._writer = self._makewriter(size)
120 | return self.write(frame)
121 | else:
122 | raise e
123 |
124 | def release(self) -> None:
125 | try:
126 | self._writer.release()
127 | except AttributeError as e:
128 | if self._writer is not self._nowriter:
129 | raise e
130 |
131 |
132 | class VideoPlayer:
133 | cap: VideoCapture
134 | rate: float
135 |
136 | _actions = {"quit": {ord("\x1b"), ord("q")}}
137 |
138 | def __init__(self, cap: VideoCapture) -> None:
139 | self.cap = cap
140 | self.rate = int(1000 / self.cap.fps)
141 |
142 | def play(
143 | self,
144 | window_name: str = "VideoPlayer",
145 | framefunc: Optional[Callable[[np.ndarray], np.ndarray]] = None,
146 | loop: bool = False,
147 | ) -> None:
148 | """Plays through the video file with OpenCV's imshow()."""
149 | frames = (
150 | self.cap.__iter__()
151 | if framefunc is None
152 | else map(framefunc, self.cap.__iter__())
153 | )
154 | for frame in frames:
155 | cv.imshow(window_name, frame)
156 | key = cv.waitKey(self.rate) & 0xFF
157 | if key in self._actions["quit"]:
158 | return
159 |
160 | if loop:
161 | return self.play(window_name, framefunc, loop)
162 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | # Global options:
2 |
3 | [mypy]
4 | python_version = 3.6
5 | strict = True
6 |
7 | # Per-module options:
8 |
9 | [mypy-cv2.*]
10 | ignore_missing_imports = True
11 |
12 | [mypy-numpy.*]
13 | ignore_missing_imports = True
14 |
15 | [mypy-IPython.display.*]
16 | ignore_missing_imports = True
17 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "appdirs"
3 | version = "1.4.4"
4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
5 | category = "dev"
6 | optional = false
7 | python-versions = "*"
8 |
9 | [[package]]
10 | name = "appnope"
11 | version = "0.1.2"
12 | description = "Disable App Nap on macOS >= 10.9"
13 | category = "main"
14 | optional = false
15 | python-versions = "*"
16 |
17 | [[package]]
18 | name = "atomicwrites"
19 | version = "1.4.0"
20 | description = "Atomic file writes."
21 | category = "dev"
22 | optional = false
23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
24 |
25 | [[package]]
26 | name = "attrs"
27 | version = "20.3.0"
28 | description = "Classes Without Boilerplate"
29 | category = "dev"
30 | optional = false
31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
32 |
33 | [package.extras]
34 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
35 | docs = ["furo", "sphinx", "zope.interface"]
36 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
37 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
38 |
39 | [[package]]
40 | name = "backcall"
41 | version = "0.2.0"
42 | description = "Specifications for callback functions passed in to an API"
43 | category = "main"
44 | optional = false
45 | python-versions = "*"
46 |
47 | [[package]]
48 | name = "black"
49 | version = "19.10b0"
50 | description = "The uncompromising code formatter."
51 | category = "dev"
52 | optional = false
53 | python-versions = ">=3.6"
54 |
55 | [package.dependencies]
56 | appdirs = "*"
57 | attrs = ">=18.1.0"
58 | click = ">=6.5"
59 | pathspec = ">=0.6,<1"
60 | regex = "*"
61 | toml = ">=0.9.4"
62 | typed-ast = ">=1.4.0"
63 |
64 | [package.extras]
65 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
66 |
67 | [[package]]
68 | name = "click"
69 | version = "7.1.2"
70 | description = "Composable command line interface toolkit"
71 | category = "dev"
72 | optional = false
73 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
74 |
75 | [[package]]
76 | name = "colorama"
77 | version = "0.4.4"
78 | description = "Cross-platform colored terminal text."
79 | category = "main"
80 | optional = false
81 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
82 |
83 | [[package]]
84 | name = "decorator"
85 | version = "5.0.7"
86 | description = "Decorators for Humans"
87 | category = "main"
88 | optional = false
89 | python-versions = ">=3.5"
90 |
91 | [[package]]
92 | name = "hypothesis"
93 | version = "5.49.0"
94 | description = "A library for property-based testing"
95 | category = "dev"
96 | optional = false
97 | python-versions = ">=3.6"
98 |
99 | [package.dependencies]
100 | attrs = ">=19.2.0"
101 | sortedcontainers = ">=2.1.0,<3.0.0"
102 |
103 | [package.extras]
104 | all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"]
105 | cli = ["click (>=7.0)", "black (>=19.10b0)"]
106 | codemods = ["libcst (>=0.3.16)"]
107 | dateutil = ["python-dateutil (>=1.4)"]
108 | django = ["pytz (>=2014.1)", "django (>=2.2)"]
109 | dpcontracts = ["dpcontracts (>=0.4)"]
110 | ghostwriter = ["black (>=19.10b0)"]
111 | lark = ["lark-parser (>=0.6.5)"]
112 | numpy = ["numpy (>=1.9.0)"]
113 | pandas = ["pandas (>=0.25)"]
114 | pytest = ["pytest (>=4.3)"]
115 | pytz = ["pytz (>=2014.1)"]
116 | redis = ["redis (>=3.0.0)"]
117 | zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"]
118 |
119 | [[package]]
120 | name = "importlib-metadata"
121 | version = "4.0.1"
122 | description = "Read metadata from Python packages"
123 | category = "dev"
124 | optional = false
125 | python-versions = ">=3.6"
126 |
127 | [package.dependencies]
128 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
129 | zipp = ">=0.5"
130 |
131 | [package.extras]
132 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
133 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
134 |
135 | [[package]]
136 | name = "ipython"
137 | version = "7.16.1"
138 | description = "IPython: Productive Interactive Computing"
139 | category = "main"
140 | optional = false
141 | python-versions = ">=3.6"
142 |
143 | [package.dependencies]
144 | appnope = {version = "*", markers = "sys_platform == \"darwin\""}
145 | backcall = "*"
146 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
147 | decorator = "*"
148 | jedi = ">=0.10"
149 | pexpect = {version = "*", markers = "sys_platform != \"win32\""}
150 | pickleshare = "*"
151 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
152 | pygments = "*"
153 | traitlets = ">=4.2"
154 |
155 | [package.extras]
156 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"]
157 | doc = ["Sphinx (>=1.3)"]
158 | kernel = ["ipykernel"]
159 | nbconvert = ["nbconvert"]
160 | nbformat = ["nbformat"]
161 | notebook = ["notebook", "ipywidgets"]
162 | parallel = ["ipyparallel"]
163 | qtconsole = ["qtconsole"]
164 | test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"]
165 |
166 | [[package]]
167 | name = "ipython-genutils"
168 | version = "0.2.0"
169 | description = "Vestigial utilities from IPython"
170 | category = "main"
171 | optional = false
172 | python-versions = "*"
173 |
174 | [[package]]
175 | name = "jedi"
176 | version = "0.18.0"
177 | description = "An autocompletion tool for Python that can be used for text editors."
178 | category = "main"
179 | optional = false
180 | python-versions = ">=3.6"
181 |
182 | [package.dependencies]
183 | parso = ">=0.8.0,<0.9.0"
184 |
185 | [package.extras]
186 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
187 | testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
188 |
189 | [[package]]
190 | name = "more-itertools"
191 | version = "8.7.0"
192 | description = "More routines for operating on iterables, beyond itertools"
193 | category = "dev"
194 | optional = false
195 | python-versions = ">=3.5"
196 |
197 | [[package]]
198 | name = "mypy"
199 | version = "0.790"
200 | description = "Optional static typing for Python"
201 | category = "dev"
202 | optional = false
203 | python-versions = ">=3.5"
204 |
205 | [package.dependencies]
206 | mypy-extensions = ">=0.4.3,<0.5.0"
207 | typed-ast = ">=1.4.0,<1.5.0"
208 | typing-extensions = ">=3.7.4"
209 |
210 | [package.extras]
211 | dmypy = ["psutil (>=4.0)"]
212 |
213 | [[package]]
214 | name = "mypy-extensions"
215 | version = "0.4.3"
216 | description = "Experimental type system extensions for programs checked with the mypy typechecker."
217 | category = "dev"
218 | optional = false
219 | python-versions = "*"
220 |
221 | [[package]]
222 | name = "numpy"
223 | version = "1.19.5"
224 | description = "NumPy is the fundamental package for array computing with Python."
225 | category = "main"
226 | optional = false
227 | python-versions = ">=3.6"
228 |
229 | [[package]]
230 | name = "opencv-python"
231 | version = "4.5.1.48"
232 | description = "Wrapper package for OpenCV python bindings."
233 | category = "main"
234 | optional = false
235 | python-versions = ">=3.6"
236 |
237 | [[package]]
238 | name = "packaging"
239 | version = "20.9"
240 | description = "Core utilities for Python packages"
241 | category = "dev"
242 | optional = false
243 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
244 |
245 | [package.dependencies]
246 | pyparsing = ">=2.0.2"
247 |
248 | [[package]]
249 | name = "parso"
250 | version = "0.8.2"
251 | description = "A Python Parser"
252 | category = "main"
253 | optional = false
254 | python-versions = ">=3.6"
255 |
256 | [package.extras]
257 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
258 | testing = ["docopt", "pytest (<6.0.0)"]
259 |
260 | [[package]]
261 | name = "pathspec"
262 | version = "0.8.1"
263 | description = "Utility library for gitignore style pattern matching of file paths."
264 | category = "dev"
265 | optional = false
266 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
267 |
268 | [[package]]
269 | name = "pexpect"
270 | version = "4.8.0"
271 | description = "Pexpect allows easy control of interactive console applications."
272 | category = "main"
273 | optional = false
274 | python-versions = "*"
275 |
276 | [package.dependencies]
277 | ptyprocess = ">=0.5"
278 |
279 | [[package]]
280 | name = "pickleshare"
281 | version = "0.7.5"
282 | description = "Tiny 'shelve'-like database with concurrency support"
283 | category = "main"
284 | optional = false
285 | python-versions = "*"
286 |
287 | [[package]]
288 | name = "pluggy"
289 | version = "0.13.1"
290 | description = "plugin and hook calling mechanisms for python"
291 | category = "dev"
292 | optional = false
293 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
294 |
295 | [package.dependencies]
296 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
297 |
298 | [package.extras]
299 | dev = ["pre-commit", "tox"]
300 |
301 | [[package]]
302 | name = "prompt-toolkit"
303 | version = "3.0.3"
304 | description = "Library for building powerful interactive command lines in Python"
305 | category = "main"
306 | optional = false
307 | python-versions = ">=3.6"
308 |
309 | [package.dependencies]
310 | wcwidth = "*"
311 |
312 | [[package]]
313 | name = "ptyprocess"
314 | version = "0.7.0"
315 | description = "Run a subprocess in a pseudo terminal"
316 | category = "main"
317 | optional = false
318 | python-versions = "*"
319 |
320 | [[package]]
321 | name = "py"
322 | version = "1.10.0"
323 | description = "library with cross-python path, ini-parsing, io, code, log facilities"
324 | category = "dev"
325 | optional = false
326 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
327 |
328 | [[package]]
329 | name = "pygments"
330 | version = "2.8.1"
331 | description = "Pygments is a syntax highlighting package written in Python."
332 | category = "main"
333 | optional = false
334 | python-versions = ">=3.5"
335 |
336 | [[package]]
337 | name = "pyparsing"
338 | version = "2.4.7"
339 | description = "Python parsing module"
340 | category = "dev"
341 | optional = false
342 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
343 |
344 | [[package]]
345 | name = "pytest"
346 | version = "5.4.3"
347 | description = "pytest: simple powerful testing with Python"
348 | category = "dev"
349 | optional = false
350 | python-versions = ">=3.5"
351 |
352 | [package.dependencies]
353 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
354 | attrs = ">=17.4.0"
355 | colorama = {version = "*", markers = "sys_platform == \"win32\""}
356 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
357 | more-itertools = ">=4.0.0"
358 | packaging = "*"
359 | pluggy = ">=0.12,<1.0"
360 | py = ">=1.5.0"
361 | wcwidth = "*"
362 |
363 | [package.extras]
364 | checkqa-mypy = ["mypy (==v0.761)"]
365 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
366 |
367 | [[package]]
368 | name = "regex"
369 | version = "2021.4.4"
370 | description = "Alternative regular expression module, to replace re."
371 | category = "dev"
372 | optional = false
373 | python-versions = "*"
374 |
375 | [[package]]
376 | name = "six"
377 | version = "1.15.0"
378 | description = "Python 2 and 3 compatibility utilities"
379 | category = "main"
380 | optional = false
381 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
382 |
383 | [[package]]
384 | name = "sortedcontainers"
385 | version = "2.3.0"
386 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
387 | category = "dev"
388 | optional = false
389 | python-versions = "*"
390 |
391 | [[package]]
392 | name = "toml"
393 | version = "0.10.2"
394 | description = "Python Library for Tom's Obvious, Minimal Language"
395 | category = "dev"
396 | optional = false
397 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
398 |
399 | [[package]]
400 | name = "traitlets"
401 | version = "4.3.3"
402 | description = "Traitlets Python config system"
403 | category = "main"
404 | optional = false
405 | python-versions = "*"
406 |
407 | [package.dependencies]
408 | decorator = "*"
409 | ipython-genutils = "*"
410 | six = "*"
411 |
412 | [package.extras]
413 | test = ["pytest", "mock"]
414 |
415 | [[package]]
416 | name = "typed-ast"
417 | version = "1.4.3"
418 | description = "a fork of Python 2 and 3 ast modules with type comment support"
419 | category = "dev"
420 | optional = false
421 | python-versions = "*"
422 |
423 | [[package]]
424 | name = "typing-extensions"
425 | version = "3.7.4.3"
426 | description = "Backported and Experimental Type Hints for Python 3.5+"
427 | category = "dev"
428 | optional = false
429 | python-versions = "*"
430 |
431 | [[package]]
432 | name = "wcwidth"
433 | version = "0.2.5"
434 | description = "Measures the displayed width of unicode strings in a terminal"
435 | category = "main"
436 | optional = false
437 | python-versions = "*"
438 |
439 | [[package]]
440 | name = "zipp"
441 | version = "3.4.1"
442 | description = "Backport of pathlib-compatible object wrapper for zip files"
443 | category = "dev"
444 | optional = false
445 | python-versions = ">=3.6"
446 |
447 | [package.extras]
448 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
449 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
450 |
451 | [metadata]
452 | lock-version = "1.1"
453 | python-versions = "^3.6"
454 | content-hash = "8c022a1174b1f1e7ef978468aa54d1f455ab25cace13b62ced28211ee7b71c8c"
455 |
456 | [metadata.files]
457 | appdirs = [
458 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
459 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
460 | ]
461 | appnope = [
462 | {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
463 | {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
464 | ]
465 | atomicwrites = [
466 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
467 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
468 | ]
469 | attrs = [
470 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
471 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
472 | ]
473 | backcall = [
474 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
475 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
476 | ]
477 | black = [
478 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
479 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
480 | ]
481 | click = [
482 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
483 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
484 | ]
485 | colorama = [
486 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
487 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
488 | ]
489 | decorator = [
490 | {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
491 | {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
492 | ]
493 | hypothesis = [
494 | {file = "hypothesis-5.49.0-py3-none-any.whl", hash = "sha256:e91111f2f01abf2566041c4c86366aa7f08bfd5b3d858cc77a545fcf67df335e"},
495 | {file = "hypothesis-5.49.0.tar.gz", hash = "sha256:36a4d5587c34193125d654b61bf9284e24a227d1edd339c49143378658a10c7d"},
496 | ]
497 | importlib-metadata = [
498 | {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"},
499 | {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"},
500 | ]
501 | ipython = [
502 | {file = "ipython-7.16.1-py3-none-any.whl", hash = "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64"},
503 | {file = "ipython-7.16.1.tar.gz", hash = "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"},
504 | ]
505 | ipython-genutils = [
506 | {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
507 | {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"},
508 | ]
509 | jedi = [
510 | {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"},
511 | {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
512 | ]
513 | more-itertools = [
514 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"},
515 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"},
516 | ]
517 | mypy = [
518 | {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
519 | {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
520 | {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
521 | {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
522 | {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
523 | {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
524 | {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
525 | {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
526 | {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
527 | {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
528 | {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
529 | {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
530 | {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
531 | {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
532 | ]
533 | mypy-extensions = [
534 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
535 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
536 | ]
537 | numpy = [
538 | {file = "numpy-1.19.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff"},
539 | {file = "numpy-1.19.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea"},
540 | {file = "numpy-1.19.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea"},
541 | {file = "numpy-1.19.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140"},
542 | {file = "numpy-1.19.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d"},
543 | {file = "numpy-1.19.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76"},
544 | {file = "numpy-1.19.5-cp36-cp36m-win32.whl", hash = "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a"},
545 | {file = "numpy-1.19.5-cp36-cp36m-win_amd64.whl", hash = "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827"},
546 | {file = "numpy-1.19.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f"},
547 | {file = "numpy-1.19.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f"},
548 | {file = "numpy-1.19.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c"},
549 | {file = "numpy-1.19.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080"},
550 | {file = "numpy-1.19.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d"},
551 | {file = "numpy-1.19.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28"},
552 | {file = "numpy-1.19.5-cp37-cp37m-win32.whl", hash = "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7"},
553 | {file = "numpy-1.19.5-cp37-cp37m-win_amd64.whl", hash = "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d"},
554 | {file = "numpy-1.19.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e"},
555 | {file = "numpy-1.19.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c"},
556 | {file = "numpy-1.19.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94"},
557 | {file = "numpy-1.19.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff"},
558 | {file = "numpy-1.19.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c"},
559 | {file = "numpy-1.19.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc"},
560 | {file = "numpy-1.19.5-cp38-cp38-win32.whl", hash = "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2"},
561 | {file = "numpy-1.19.5-cp38-cp38-win_amd64.whl", hash = "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa"},
562 | {file = "numpy-1.19.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd"},
563 | {file = "numpy-1.19.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa"},
564 | {file = "numpy-1.19.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8"},
565 | {file = "numpy-1.19.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371"},
566 | {file = "numpy-1.19.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb"},
567 | {file = "numpy-1.19.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"},
568 | {file = "numpy-1.19.5-cp39-cp39-win32.whl", hash = "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e"},
569 | {file = "numpy-1.19.5-cp39-cp39-win_amd64.whl", hash = "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e"},
570 | {file = "numpy-1.19.5-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73"},
571 | {file = "numpy-1.19.5.zip", hash = "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4"},
572 | ]
573 | opencv-python = [
574 | {file = "opencv-python-4.5.1.48.tar.gz", hash = "sha256:78a6db8467639383caedf1d111da3510a4ee1a0aacf2117821cae2ee8f92ce37"},
575 | {file = "opencv_python-4.5.1.48-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:bcb27773cfd5340b2b599b303d9f5499838ef4780c20c038f6030175408c64df"},
576 | {file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9646875c501788b1b098f282d777b667d6da69801739504f1b2fd1268970d1da"},
577 | {file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:ebe83901971a6755512424c4fe9f63341cca501b7c497bf608dd38ee31ba3f4c"},
578 | {file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:d8aefcb30b71064dbbaa2b0ace161a36464c29375a83998fbda39a1d1740f942"},
579 | {file = "opencv_python-4.5.1.48-cp36-cp36m-win32.whl", hash = "sha256:32dee1c9fd3e31e28edef7b56f868e2b40e280b7062304f9fb8a14dbc51547d5"},
580 | {file = "opencv_python-4.5.1.48-cp36-cp36m-win_amd64.whl", hash = "sha256:9c77d508e6822f1f40c727d21b822d017622d8305dce7eccf0ab06caac16d5c6"},
581 | {file = "opencv_python-4.5.1.48-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:4982fa8ccc38310a2bd93e06334ba090b12b6aff2f6fcb8ff9613e3c9bc48f48"},
582 | {file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c0503bfaa2b7b743d6ff5d81f1dd8428dbf4c33e7e4f836456d11be20c2e7721"},
583 | {file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:e27d062fa1098d90f48b6c047351c89816492a08906a021c973ce510b04a7b9d"},
584 | {file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:6d8434a45e8f75c4da5fd0068ce001f4f8e35771cc851d746d4721eeaf517e25"},
585 | {file = "opencv_python-4.5.1.48-cp37-cp37m-win32.whl", hash = "sha256:e2c17714da59d9d516ceef0450766ff9557ee232d62f702665af905193557582"},
586 | {file = "opencv_python-4.5.1.48-cp37-cp37m-win_amd64.whl", hash = "sha256:efac9893d9e21cfb599828801c755ecde8f1e657f05ec6f002efe19422456d5a"},
587 | {file = "opencv_python-4.5.1.48-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:e77d0feaff37326f62b127098264e2a7099deb476e38432b1083ce11cdedf560"},
588 | {file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ffc75c614b8dc3d8102f3ba15dafd6ec0400c7ffa71a91953d41511964ee50e0"},
589 | {file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c1159d91f29a85c3333edef6ca420284566d9bcdae46dda2fe7282515b48c8b6"},
590 | {file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:d16144c435b816c5536d5ff012c1a2b7e93155017db7103942ff7efb98c4df1f"},
591 | {file = "opencv_python-4.5.1.48-cp38-cp38-win32.whl", hash = "sha256:b2b9ac86aec5f2dd531545cebdea1a1ef4f81ef1fb1760d78b4725f9575504f9"},
592 | {file = "opencv_python-4.5.1.48-cp38-cp38-win_amd64.whl", hash = "sha256:30edebc81b260bcfeb760b3600c367c5261dfb2fe41e5d1408d5357d0867b40d"},
593 | {file = "opencv_python-4.5.1.48-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:e38fbd7b2db03204ec09930609b7313d6b6d2b271c8fe2c0aa271fa69b726a1b"},
594 | {file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:fc1472b825d26c8a4f1cfb172a90c3cc47733e4af7522276c1c2efe8f6006a8b"},
595 | {file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c4ea4f8b217f3e8be6247fc0787fb81797d85202c722523f41070124a7a621c7"},
596 | {file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:a1dfa0486db367594510c0c799ec7481247dc86e651b69008806d875ab731471"},
597 | {file = "opencv_python-4.5.1.48-cp39-cp39-win32.whl", hash = "sha256:5172cb37dfd8a0b4945b071a493eb36e5f17675a160637fa380f9c1d9d80535c"},
598 | {file = "opencv_python-4.5.1.48-cp39-cp39-win_amd64.whl", hash = "sha256:c8cc1f5ff3c352ebe756119014c4e4ec7ae5ac536d1f66b0316667ced37637c8"},
599 | ]
600 | packaging = [
601 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
602 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
603 | ]
604 | parso = [
605 | {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
606 | {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"},
607 | ]
608 | pathspec = [
609 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
610 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
611 | ]
612 | pexpect = [
613 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
614 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
615 | ]
616 | pickleshare = [
617 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
618 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
619 | ]
620 | pluggy = [
621 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
622 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
623 | ]
624 | prompt-toolkit = [
625 | {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"},
626 | {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"},
627 | ]
628 | ptyprocess = [
629 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
630 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
631 | ]
632 | py = [
633 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
634 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
635 | ]
636 | pygments = [
637 | {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
638 | {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
639 | ]
640 | pyparsing = [
641 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
642 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
643 | ]
644 | pytest = [
645 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
646 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
647 | ]
648 | regex = [
649 | {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"},
650 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"},
651 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"},
652 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"},
653 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"},
654 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"},
655 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"},
656 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"},
657 | {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"},
658 | {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"},
659 | {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"},
660 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"},
661 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"},
662 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"},
663 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"},
664 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"},
665 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"},
666 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"},
667 | {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"},
668 | {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"},
669 | {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"},
670 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"},
671 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"},
672 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"},
673 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"},
674 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"},
675 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"},
676 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"},
677 | {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"},
678 | {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"},
679 | {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"},
680 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"},
681 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"},
682 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"},
683 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"},
684 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"},
685 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"},
686 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"},
687 | {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"},
688 | {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"},
689 | {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"},
690 | ]
691 | six = [
692 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
693 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
694 | ]
695 | sortedcontainers = [
696 | {file = "sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f"},
697 | {file = "sortedcontainers-2.3.0.tar.gz", hash = "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"},
698 | ]
699 | toml = [
700 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
701 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
702 | ]
703 | traitlets = [
704 | {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
705 | {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},
706 | ]
707 | typed-ast = [
708 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
709 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
710 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
711 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
712 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
713 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
714 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
715 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
716 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
717 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
718 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
719 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
720 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
721 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
722 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
723 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
724 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
725 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
726 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
727 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
728 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
729 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
730 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
731 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
732 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
733 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
734 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
735 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
736 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
737 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
738 | ]
739 | typing-extensions = [
740 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
741 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
742 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
743 | ]
744 | wcwidth = [
745 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
746 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
747 | ]
748 | zipp = [
749 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
750 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
751 | ]
752 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "cvpack-alkasm"
3 | packages = [
4 | { include = "cvpack" },
5 | ]
6 | version = "1.1.0"
7 | description = "Utilities for OpenCV in Python"
8 | authors = ["Alexander Reynolds "]
9 | homepage = "https://github.com/alkasm/cvpack"
10 | repository = "https://github.com/alkasm/cvpack"
11 | keywords = ["cvpack", "computer vision", "cv", "opencv"]
12 | readme = "README.md"
13 | license = "MIT"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "License :: OSI Approved :: MIT License",
17 | "Operating System :: OS Independent",
18 | ]
19 | include = ["py.typed"]
20 |
21 | [tool.poetry.dependencies]
22 | python = "^3.6"
23 | opencv-python = "!=4.2.0.32"
24 | IPython = {version = "~=7.16", optional = true}
25 | numpy = "^1.19.5"
26 |
27 | [tool.poetry.dev-dependencies]
28 | pytest = "^5.4.1"
29 | black = "^19.10b0"
30 | hypothesis = "^5.8.0"
31 | IPython = "~=7.16"
32 | mypy = "^0.790"
33 |
34 | [build-system]
35 | requires = ["poetry>=0.12"]
36 | build-backend = "poetry.masonry.api"
37 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alkasm/cvpack/7d7aceddf18aca03ac77ccf8e0da7f71ef6674a3/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_point_size.py:
--------------------------------------------------------------------------------
1 | import math
2 | import functools
3 | from hypothesis import given, assume
4 | from hypothesis.strategies import builds, integers, one_of, none, fractions
5 | import pytest
6 | import numpy as np
7 | import cv2 as cv
8 | from cvpack import Point, Point3, Size
9 |
10 |
11 | # Hard coded test cases
12 |
13 |
14 | def make_type_drop_arg(type_, *args):
15 | if type_ == Point3:
16 | return type_(*args)
17 | return type_(*args[:-1])
18 |
19 |
20 | def test_unary_operators():
21 | for type_ in (Point, Point3, Size):
22 | T = functools.partial(make_type_drop_arg, type_)
23 | assert +T(1, 2, 1) == T(1, 2, 1)
24 | assert -T(1, 2, -1) == T(-1, -2, 1)
25 | assert abs(T(-1, -2, 1)) == T(1, 2, 1)
26 | assert round(T(1.1, 1.91, 2.5)) == T(1, 2, 2)
27 | assert round(T(1.1, 1.91, 1), ndigits=1) == T(1.1, 1.9, 1.0)
28 | assert math.floor(T(1.1, 1.9, 1.0)) == T(1, 1, 1)
29 | assert math.ceil(T(1.1, 1.9, 1.0)) == T(2, 2, 1)
30 |
31 |
32 | def test_binary_operators():
33 | for type_ in (Point, Point3, Size):
34 | T = functools.partial(make_type_drop_arg, type_)
35 | assert T(1, 3, 0) + T(1, 3, 1) == T(2, 6, 1)
36 | assert T(1, 3, 1.1) * T(1, 3, 2) == T(1, 9, 2.2)
37 | assert T(1, 3, 2) - T(2, 3, 0) == T(-1, 0, 2)
38 | assert T(2, 3, 2) / T(1, 2, 2) == T(2, 1.5, 1)
39 | assert T(2, 3, 2) // T(1, 2, 2) == T(2, 1, 1)
40 |
41 |
42 | def test_binary_operators_broadcast():
43 | for type_ in (Point, Point3, Size):
44 | T = functools.partial(make_type_drop_arg, type_)
45 | assert T(1, 3, 0) + 2 == T(3, 5, 2)
46 | assert T(1, 3, 0) * 2 == T(2, 6, 0)
47 | assert T(1, 3, 0) - 2 == T(-1, 1, -2)
48 | assert T(2, 3, 1) / 2 == T(1, 1.5, 0.5)
49 | assert T(2, 3, 1) // 2 == T(1, 1, 0)
50 |
51 |
52 | def test_binary_operators_broadcast_rhs():
53 | for type_ in (Point, Point3, Size):
54 | T = functools.partial(make_type_drop_arg, type_)
55 | assert 2 + T(1, 3, 0) == T(3, 5, 2)
56 | assert 2 * T(1, 3, 0) == T(2, 6, 0)
57 | assert 2 - T(1, 3, 0) == T(1, -1, 2)
58 | assert 3 / T(2, 3, 1) == T(1.5, 1, 3)
59 | assert 2 // T(2, 3, 1) == T(1, 0, 2)
60 |
61 |
62 | # generated test cases
63 | # using rationals to check math exactly instead of dealing with floating point errors
64 |
65 | Rationals = fractions()
66 | PositiveRationals = fractions(min_value=0)
67 | PointStrategy = builds(Point, Rationals, Rationals)
68 | Point3Strategy = builds(Point3, Rationals, Rationals, Rationals)
69 | SizeStrategy = builds(Size, PositiveRationals, PositiveRationals)
70 | ArithmeticTupleStrategy = one_of(PointStrategy, Point3Strategy, SizeStrategy)
71 |
72 |
73 | @given(ArithmeticTupleStrategy)
74 | def test_unary_operators(o):
75 | assert +o == o
76 | assert -o == tuple(-v for v in o)
77 | assert o == -(-o)
78 | assert abs(o) == tuple(abs(v) for v in o)
79 |
80 |
81 | @given(ArithmeticTupleStrategy, one_of(integers(min_value=0, max_value=10), none()))
82 | def test_unary_truncation_operators(o, ndigits):
83 | assume(all(math.isfinite(v) for v in o)) # cannot round/truncate/etc inf or nan
84 | assert round(o, ndigits) == tuple(round(v, ndigits) for v in o)
85 | assert math.floor(o) == tuple(math.floor(v) for v in o)
86 | assert math.ceil(o) == tuple(math.ceil(v) for v in o)
87 |
88 |
89 | @given(ArithmeticTupleStrategy, ArithmeticTupleStrategy)
90 | def test_binary_operators(lhs, rhs):
91 | assume(type(lhs) == type(rhs))
92 |
93 | # commutative
94 | assert lhs + rhs == rhs + lhs == tuple(l + r for l, r in zip(lhs, rhs))
95 | assert lhs * rhs == rhs * lhs == tuple(l * r for l, r in zip(lhs, rhs))
96 |
97 | # non-commutative
98 | assert lhs - rhs == tuple(l - r for l, r in zip(lhs, rhs))
99 | assume(all(v != 0 for v in rhs))
100 | assert lhs / rhs == tuple(l / r for l, r in zip(lhs, rhs))
101 | assert lhs // rhs == tuple(l // r for l, r in zip(lhs, rhs))
102 |
103 |
104 | @given(ArithmeticTupleStrategy, Rationals)
105 | def test_binary_operators_scalar(lhs, rhs):
106 | assert lhs + rhs == rhs + lhs == tuple(l + rhs for l in lhs)
107 | assert lhs * rhs == rhs * lhs == tuple(l * rhs for l in lhs)
108 | assert lhs - rhs == tuple(l - rhs for l in lhs)
109 | assert rhs - lhs == tuple(rhs - l for l in lhs)
110 |
111 | if rhs == 0.0:
112 | with pytest.raises(ZeroDivisionError):
113 | lhs / rhs
114 |
115 | with pytest.raises(ZeroDivisionError):
116 | lhs // rhs
117 |
118 | assume(not math.isclose(0, rhs))
119 | assert lhs / rhs == tuple(l / rhs for l in lhs)
120 | assert lhs // rhs == tuple(l // rhs for l in lhs)
121 |
122 |
123 | # OpenCV functions
124 |
125 | max_size = 50
126 |
127 |
128 | def blank_img():
129 | return np.zeros((max_size, max_size), dtype=np.uint8)
130 |
131 |
132 | ImagePointStrategy = builds(
133 | Point,
134 | integers(min_value=0, max_value=max_size - 1),
135 | integers(min_value=0, max_value=max_size - 1),
136 | )
137 | EllipseSizeStrategy = builds(
138 | Size,
139 | integers(min_value=1, max_value=max_size),
140 | integers(min_value=1, max_value=max_size),
141 | )
142 |
143 |
144 | @given(ImagePointStrategy)
145 | def test_cv_circle(p):
146 | img = cv.circle(blank_img(), p, 5, 255, -1)
147 | assert img[p.y, p.x] == 255
148 |
149 |
150 | @given(ImagePointStrategy)
151 | def test_cv_draw_marker(p):
152 | img = cv.drawMarker(blank_img(), p, 255)
153 | assert img[p.y, p.x] == 255
154 |
155 |
156 | @given(ImagePointStrategy, EllipseSizeStrategy)
157 | def test_cv_ellipse(p, s):
158 | img = cv.ellipse(blank_img(), p, s, 0, 0, 360, 255, -1)
159 | assert img[p.y, p.x] == 255
160 |
161 |
162 | @given(ImagePointStrategy, ImagePointStrategy)
163 | def test_cv_line(p1, p2):
164 | img = cv.line(blank_img(), p1, p2, 255, 2)
165 | assert img[p1.y, p1.x] == 255
166 | assert img[p2.y, p2.x] == 255
167 |
--------------------------------------------------------------------------------
/tests/test_rect.py:
--------------------------------------------------------------------------------
1 | from hypothesis import given, assume
2 | from hypothesis.strategies import builds, integers
3 | import numpy as np
4 | import cv2 as cv
5 | from cvpack import Rect
6 | from .test_point_size import Rationals, PositiveRationals, PointStrategy, SizeStrategy
7 |
8 |
9 | def test_rect_slice():
10 | img_rect = Rect(0, 0, 160, 90)
11 | a = np.random.rand(img_rect.height, img_rect.width)
12 | assert np.all(a == a[img_rect.slice()])
13 |
14 | rect = Rect(10, 12, 14, 16)
15 | assert np.all(
16 | a[rect.y : rect.y + rect.height, rect.x : rect.x + rect.width]
17 | == a[rect.slice()]
18 | )
19 |
20 | empty_rect = Rect(0, 0, 0, 0)
21 | assert a[empty_rect.slice()].size == 0
22 |
23 |
24 | RectStrategy = builds(Rect, Rationals, Rationals, PositiveRationals, PositiveRationals)
25 | Integers100 = integers(min_value=0, max_value=100)
26 | RegionStrategy = builds(Rect, Integers100, Integers100, Integers100, Integers100)
27 |
28 |
29 | @given(RectStrategy)
30 | def test_rect(r):
31 | assert r.area() == r.width * r.height
32 | assert r.area() == r.size().area()
33 | assert r.tl() == (r.x, r.y)
34 | assert r.br() == (r.x + r.width, r.y + r.height)
35 | assert r.size() == (r.width, r.height)
36 | assert r.empty() == r.size().empty()
37 | assert r.empty() == (r.area() == 0)
38 | assert r.center().x == r.x + r.width / 2
39 | assert r.center().y == r.y + r.height / 2
40 | assert r.contains(r.center())
41 | assert r.contains(r.tl())
42 | assert r.contains(r.br())
43 |
44 | assert r == Rect.from_center(r.center(), r.size())
45 | assert r == Rect.from_points(r.tl(), r.br())
46 |
47 |
48 | @given(RectStrategy)
49 | def test_rect_intersection_union(r):
50 | assert r.area() == r.intersection(r)
51 | assert r.area() == r.union(r)
52 |
53 | extended_rect = Rect.from_points(r.tl() - 1, r.br() + 1)
54 | assert r.area() == r.intersection(extended_rect)
55 | assert extended_rect.area() == r.union(extended_rect)
56 |
57 | non_intersecting_rect = Rect.from_origin(r.br(), r.size())
58 | assert r.intersection(non_intersecting_rect) == 0
59 | assert r.union(non_intersecting_rect) == r.area() + non_intersecting_rect.area()
60 |
61 | empty_rect = Rect(0, 0, 0, 0)
62 | assert r.intersection(empty_rect) == 0
63 | assert r.union(empty_rect) == r.area()
64 |
65 | intersecting_rect = Rect.from_center(r.br(), r.size())
66 | assert r.intersection(intersecting_rect) == r.area() / 4
67 | assert r.union(intersecting_rect) == r.area() * 7 / 4
68 |
69 |
70 | @given(RectStrategy, PointStrategy)
71 | def test_rect_point_ops(r, p):
72 | sp = r + p
73 | assert sp.tl() == r.tl() + p
74 | assert sp.br() == r.br() + p
75 | assert sp.center() == r.center() + p
76 | assert r.area() == sp.area()
77 | assert r.size() == sp.size()
78 |
79 | sn = r - p
80 | assert sn.tl() == r.tl() - p
81 | assert sn.br() == r.br() - p
82 | assert sn.center() == r.center() - p
83 | assert r.area() == sn.area()
84 | assert r.size() == sn.size()
85 |
86 |
87 | @given(RectStrategy, SizeStrategy)
88 | def test_rect_size_ops(r, s):
89 | e = r + s
90 | assert e.tl() == r.tl()
91 | assert e.br() == r.br() + s
92 | assert e.center() == r.center() + s / 2
93 | assert r.intersection(e) == r.area()
94 | assert r.union(e) == e.area()
95 | assert e.size() == r.size() + s
96 | assert r.width <= e.width
97 | assert r.height <= e.height
98 |
99 | assume(s.width <= r.width and s.height <= r.height)
100 | c = r - s
101 | assert c.tl() == r.tl()
102 | assert c.br() == r.br() - s
103 | assert c.center() == r.center() - s / 2
104 | assert r.intersection(c) == c.area()
105 | assert r.union(c) == r.area()
106 | assert c.size() == r.size() - s
107 | assert c.width <= r.width
108 | assert c.height <= r.height
109 |
110 |
111 | @given(RegionStrategy)
112 | def test_rect_slices(r):
113 | img = np.random.rand(200, 200)
114 | assert np.all(img[r.y : r.y + r.height, r.x : r.x + r.width] == img[r.slice()])
115 | assert img[r.slice()].shape == r.size()[::-1]
116 | assert img[r.slice()].size == r.area()
117 |
118 |
119 | # OpenCV functions
120 |
121 |
122 | @given(RegionStrategy)
123 | def test_cv_rectangle(r):
124 | blank_img = np.zeros((200, 200), dtype=np.uint8)
125 | pts_img = cv.rectangle(blank_img, r.tl(), r.br(), color=255, thickness=-1)
126 | rect_img = cv.rectangle(blank_img, r, color=255, thickness=-1)
127 | assert np.all(pts_img == rect_img)
128 | assert np.all(pts_img[r.slice()] == 255)
129 | assert np.all(rect_img[r.slice()] == 255)
130 | assert not np.any((pts_img == 0)[r.slice()])
131 | assert not np.any((rect_img == 0)[r.slice()])
132 |
--------------------------------------------------------------------------------
/tests/test_rotated_rect.py:
--------------------------------------------------------------------------------
1 | from hypothesis import given, assume
2 | from hypothesis.strategies import builds, integers
3 | from cvpack import Rect, RotatedRect, Point
4 | import pytest
5 |
6 | Integers = integers(min_value=-1_000_000, max_value=1_000_000)
7 | PositiveIntegers = integers(min_value=0, max_value=1_000_000)
8 |
9 | RationalRectStrategy = builds(
10 | Rect, Integers, Integers, PositiveIntegers, PositiveIntegers,
11 | )
12 |
13 |
14 | @given(RationalRectStrategy)
15 | def test_rotated_rect_to_from_rect(r):
16 | def assert_contains(rotated_rect, rect):
17 | # bounding rect is integral, so check intersection and union
18 | bound = rotated_rect.bounding_rect()
19 | assert bound.intersection(rect) == rect.area()
20 | assert bound.union(rect) == bound.area()
21 |
22 | rr0 = RotatedRect(r.center(), r.size(), 0)
23 | rr180 = RotatedRect(r.center(), r.size(), 180)
24 | assert_contains(rr0, r)
25 | assert_contains(rr180, r)
26 |
27 | tl = r.tl()
28 | tr = Point(r.x + r.width, r.y)
29 | br = r.br()
30 | bl = Point(r.x, r.y + r.height)
31 |
32 | triplets = [
33 | (tl, tr, br),
34 | (tr, br, bl),
35 | (br, bl, tl),
36 | (bl, tl, tr),
37 | ] # clockwise
38 | triplets += [t[::-1] for t in triplets] # counter-clockwise
39 |
40 | for p1, p2, p3 in triplets:
41 | rr = RotatedRect.from_points(p1, p2, p3)
42 | assert r.area() == rr.size.area()
43 | assert rr.angle % 90 == 0
44 | assert_contains(rr, r)
45 |
46 | assume(r.height > 1 and r.width > 1)
47 | for p1, p2, p3 in triplets:
48 | avgp = (p1 + p3) / 2
49 | with pytest.raises(ValueError):
50 | rr = RotatedRect.from_points(p1, avgp, p3)
51 |
--------------------------------------------------------------------------------