kotoha 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotoha-0.1.0a1/PKG-INFO +103 -0
- kotoha-0.1.0a1/README.md +70 -0
- kotoha-0.1.0a1/kotoha/__init__.py +33 -0
- kotoha-0.1.0a1/kotoha/_geometry_types.py +353 -0
- kotoha-0.1.0a1/kotoha/common.py +514 -0
- kotoha-0.1.0a1/kotoha/dataset_info.py +36 -0
- kotoha-0.1.0a1/kotoha/experiments/cpp/__init__.py +3 -0
- kotoha-0.1.0a1/kotoha/experiments/image_filter.py +348 -0
- kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/__init__.py +1 -0
- kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/heatmap_pose.py +103 -0
- kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/utils.py +321 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/__init__.py +1 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/byte_track.py +96 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/__init__.py +0 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/basetrack.py +52 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/byte_tracker.py +352 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/kalman_filter.py +270 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/matching.py +194 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/__init__.py +1 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/ocsort.py +28 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/__init__.py +0 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/association.py +379 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/kalmanfilter.py +1581 -0
- kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/ocsort.py +345 -0
- kotoha-0.1.0a1/kotoha/experiments/npy_ref.py +331 -0
- kotoha-0.1.0a1/kotoha/experiments/voc.py +588 -0
- kotoha-0.1.0a1/kotoha/geo.py +103 -0
- kotoha-0.1.0a1/kotoha/io.py +456 -0
- kotoha-0.1.0a1/kotoha/model/__init__.py +3 -0
- kotoha-0.1.0a1/kotoha/model/typing.py +89 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/utils.py +287 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/yolov5.py +227 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8.py +100 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8obb.py +134 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8pose.py +76 -0
- kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8seg.py +68 -0
- kotoha-0.1.0a1/kotoha/polygon/__init__.py +13 -0
- kotoha-0.1.0a1/kotoha/polygon/array.py +108 -0
- kotoha-0.1.0a1/kotoha/polygon/core.py +259 -0
- kotoha-0.1.0a1/kotoha/polygon/geometry.py +200 -0
- kotoha-0.1.0a1/kotoha/typing.py +30 -0
- kotoha-0.1.0a1/kotoha/visual/__init__.py +22 -0
- kotoha-0.1.0a1/kotoha/visual/color.py +87 -0
- kotoha-0.1.0a1/kotoha/visual/visual.py +474 -0
- kotoha-0.1.0a1/kotoha.egg-info/PKG-INFO +103 -0
- kotoha-0.1.0a1/kotoha.egg-info/SOURCES.txt +55 -0
- kotoha-0.1.0a1/kotoha.egg-info/dependency_links.txt +1 -0
- kotoha-0.1.0a1/kotoha.egg-info/requires.txt +12 -0
- kotoha-0.1.0a1/kotoha.egg-info/top_level.txt +1 -0
- kotoha-0.1.0a1/pyproject.toml +57 -0
- kotoha-0.1.0a1/setup.cfg +4 -0
- kotoha-0.1.0a1/tests/test_cleanup.py +46 -0
- kotoha-0.1.0a1/tests/test_geo.py +49 -0
- kotoha-0.1.0a1/tests/test_model_typing.py +117 -0
- kotoha-0.1.0a1/tests/test_public_api.py +39 -0
- kotoha-0.1.0a1/tests/test_typing.py +161 -0
- kotoha-0.1.0a1/tests/test_vocio.py +10 -0
kotoha-0.1.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kotoha
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Image-processing utilities with geometry primitives, I/O helpers, visualization tools, and typed model outputs.
|
|
5
|
+
Keywords: image-processing
|
|
6
|
+
Classifier: Development Status :: 1 - Planning
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Intended Audience :: Education
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Software Development
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: opencv-python
|
|
23
|
+
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: xmltodict
|
|
26
|
+
Requires-Dist: shapely>=2.1.2
|
|
27
|
+
Requires-Dist: requests
|
|
28
|
+
Requires-Dist: pillow
|
|
29
|
+
Requires-Dist: tqdm
|
|
30
|
+
Provides-Extra: model
|
|
31
|
+
Requires-Dist: onnx; extra == "model"
|
|
32
|
+
Requires-Dist: onnxruntime; extra == "model"
|
|
33
|
+
|
|
34
|
+
# kotoha
|
|
35
|
+
|
|
36
|
+
`kotoha` is a small image-processing toolbox built around a stable set of geometry types, file helpers, visualization utilities, and typed model outputs.
|
|
37
|
+
|
|
38
|
+
## What Lives Where
|
|
39
|
+
|
|
40
|
+
- `kotoha`: minimal public API for stable geometry and detection result types
|
|
41
|
+
- `kotoha.typing`: geometry primitives and typed detection data structures
|
|
42
|
+
- `kotoha.io`: file and annotation helpers
|
|
43
|
+
- `kotoha.geo`: small geometry utilities
|
|
44
|
+
- `kotoha.visual`: drawing and display helpers
|
|
45
|
+
- `kotoha.model`: typed model outputs and Ultralytics-style ONNX wrappers
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Install the core toolbox:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install kotoha
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Install model-related extras as well:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install "kotoha[model]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Examples
|
|
62
|
+
|
|
63
|
+
### Geometry Types
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from kotoha import Point2f, Polygon, Rect
|
|
67
|
+
|
|
68
|
+
rect = Rect.from_xyxy(10, 20, 30, 40)
|
|
69
|
+
polygon = rect.to_polygon()
|
|
70
|
+
rotated = polygon.rotate_deg(90)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### File Helpers
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from kotoha.io import read_json, save_yaml
|
|
77
|
+
|
|
78
|
+
data = read_json("sample.json")
|
|
79
|
+
save_yaml("sample.yaml", data)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Typed Model Results
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from kotoha import Detection, Rect
|
|
86
|
+
|
|
87
|
+
detection = Detection(
|
|
88
|
+
box=Rect.from_xyxy(10, 20, 30, 40),
|
|
89
|
+
score=0.95,
|
|
90
|
+
class_id=1,
|
|
91
|
+
label="person",
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Public API Policy
|
|
96
|
+
|
|
97
|
+
The top-level `kotoha` namespace is intentionally minimal. Import utility functions from their submodules, for example:
|
|
98
|
+
|
|
99
|
+
- `from kotoha.io import imread`
|
|
100
|
+
- `from kotoha.geo import rotate_points`
|
|
101
|
+
- `from kotoha.visual import draw_bbox`
|
|
102
|
+
|
|
103
|
+
This keeps the stable API surface small and makes module boundaries explicit.
|
kotoha-0.1.0a1/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# kotoha
|
|
2
|
+
|
|
3
|
+
`kotoha` is a small image-processing toolbox built around a stable set of geometry types, file helpers, visualization utilities, and typed model outputs.
|
|
4
|
+
|
|
5
|
+
## What Lives Where
|
|
6
|
+
|
|
7
|
+
- `kotoha`: minimal public API for stable geometry and detection result types
|
|
8
|
+
- `kotoha.typing`: geometry primitives and typed detection data structures
|
|
9
|
+
- `kotoha.io`: file and annotation helpers
|
|
10
|
+
- `kotoha.geo`: small geometry utilities
|
|
11
|
+
- `kotoha.visual`: drawing and display helpers
|
|
12
|
+
- `kotoha.model`: typed model outputs and Ultralytics-style ONNX wrappers
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the core toolbox:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install kotoha
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Install model-related extras as well:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "kotoha[model]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Examples
|
|
29
|
+
|
|
30
|
+
### Geometry Types
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from kotoha import Point2f, Polygon, Rect
|
|
34
|
+
|
|
35
|
+
rect = Rect.from_xyxy(10, 20, 30, 40)
|
|
36
|
+
polygon = rect.to_polygon()
|
|
37
|
+
rotated = polygon.rotate_deg(90)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### File Helpers
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from kotoha.io import read_json, save_yaml
|
|
44
|
+
|
|
45
|
+
data = read_json("sample.json")
|
|
46
|
+
save_yaml("sample.yaml", data)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Typed Model Results
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from kotoha import Detection, Rect
|
|
53
|
+
|
|
54
|
+
detection = Detection(
|
|
55
|
+
box=Rect.from_xyxy(10, 20, 30, 40),
|
|
56
|
+
score=0.95,
|
|
57
|
+
class_id=1,
|
|
58
|
+
label="person",
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Public API Policy
|
|
63
|
+
|
|
64
|
+
The top-level `kotoha` namespace is intentionally minimal. Import utility functions from their submodules, for example:
|
|
65
|
+
|
|
66
|
+
- `from kotoha.io import imread`
|
|
67
|
+
- `from kotoha.geo import rotate_points`
|
|
68
|
+
- `from kotoha.visual import draw_bbox`
|
|
69
|
+
|
|
70
|
+
This keeps the stable API surface small and makes module boundaries explicit.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from .typing import (
|
|
2
|
+
Affine2D,
|
|
3
|
+
Circle,
|
|
4
|
+
Detection,
|
|
5
|
+
KeyPoint,
|
|
6
|
+
Line,
|
|
7
|
+
ObbDetection,
|
|
8
|
+
Point2f,
|
|
9
|
+
PointGeometryMixin,
|
|
10
|
+
PointLike,
|
|
11
|
+
Polygon,
|
|
12
|
+
PoseDetection,
|
|
13
|
+
Rect,
|
|
14
|
+
SegmentationDetection,
|
|
15
|
+
Vector2,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Affine2D",
|
|
20
|
+
"Circle",
|
|
21
|
+
"Detection",
|
|
22
|
+
"KeyPoint",
|
|
23
|
+
"Line",
|
|
24
|
+
"ObbDetection",
|
|
25
|
+
"Point2f",
|
|
26
|
+
"PointGeometryMixin",
|
|
27
|
+
"PointLike",
|
|
28
|
+
"Polygon",
|
|
29
|
+
"PoseDetection",
|
|
30
|
+
"Rect",
|
|
31
|
+
"SegmentationDetection",
|
|
32
|
+
"Vector2",
|
|
33
|
+
]
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from typing import NamedTuple, Self, overload, override
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
type PointLike = Sequence[float] | Point2f
|
|
12
|
+
type Vector2 = PointLike
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Line(NamedTuple):
|
|
16
|
+
# Line in the form ax + by + c = 0
|
|
17
|
+
a: float
|
|
18
|
+
b: float
|
|
19
|
+
c: float
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Circle:
|
|
23
|
+
x: float
|
|
24
|
+
y: float
|
|
25
|
+
radius: float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Point2f(NamedTuple):
|
|
30
|
+
x: float
|
|
31
|
+
y: float
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def transformed(
|
|
35
|
+
point: PointLike,
|
|
36
|
+
M: Sequence[Sequence[float]],
|
|
37
|
+
) -> Point2f:
|
|
38
|
+
return Point2f(
|
|
39
|
+
M[0][0] * point[0] + M[0][1] * point[1] + M[0][2],
|
|
40
|
+
M[1][0] * point[0] + M[1][1] * point[1] + M[1][2],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __add__(self, other):
|
|
44
|
+
if not isinstance(other, Point2f):
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
return Point2f(
|
|
47
|
+
self.x + other.x,
|
|
48
|
+
self.y + other.y,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __sub__(self, other):
|
|
52
|
+
if not isinstance(other, Point2f):
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
return Point2f(
|
|
55
|
+
self.x - other.x,
|
|
56
|
+
self.y - other.y,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def __mul__(self, other: float) -> "Point2f": ...
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def __mul__(self, other: PointLike) -> float: ...
|
|
64
|
+
|
|
65
|
+
def __mul__(self, other):
|
|
66
|
+
if isinstance(other, (int, float)):
|
|
67
|
+
return Point2f(
|
|
68
|
+
self.x * other,
|
|
69
|
+
self.y * other,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return self.x * other[0] + self.y * other[1]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Affine2D:
|
|
76
|
+
def __init__(self, M=None):
|
|
77
|
+
self.M = np.eye(3) if M is None else np.array(M, dtype=float)
|
|
78
|
+
|
|
79
|
+
def apply_matrix(self, M: Sequence[Sequence[float]]) -> Self:
|
|
80
|
+
self.M = self.M @ np.array(M, dtype=float)
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def translate(self, dx: float, dy: float) -> Self:
|
|
84
|
+
M = [
|
|
85
|
+
[1, 0, dx],
|
|
86
|
+
[0, 1, dy],
|
|
87
|
+
[0, 0, 1],
|
|
88
|
+
]
|
|
89
|
+
return self.apply_matrix(M)
|
|
90
|
+
|
|
91
|
+
def scale(self, sx: float, sy: float | None = None, pivot=(0, 0)) -> Self:
|
|
92
|
+
if sy is None:
|
|
93
|
+
sy = sx
|
|
94
|
+
|
|
95
|
+
px, py = pivot
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
self.translate(px, py)
|
|
99
|
+
.apply_matrix(
|
|
100
|
+
[
|
|
101
|
+
[sx, 0, 0],
|
|
102
|
+
[0, sy, 0],
|
|
103
|
+
[0, 0, 1],
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
.translate(-px, -py)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def rotate_deg(self, theta: float, pivot=(0, 0)) -> Self:
|
|
110
|
+
px, py = pivot
|
|
111
|
+
angle = math.radians(theta % 360)
|
|
112
|
+
c, s = math.cos(angle), math.sin(angle)
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
self.translate(px, py)
|
|
116
|
+
.apply_matrix(
|
|
117
|
+
[
|
|
118
|
+
[c, -s, 0],
|
|
119
|
+
[s, c, 0],
|
|
120
|
+
[0, 0, 1],
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
.translate(-px, -py)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def reflect_line(self, theta: float):
|
|
127
|
+
c, s = math.cos(theta), math.sin(theta)
|
|
128
|
+
|
|
129
|
+
return self.apply_matrix(
|
|
130
|
+
[
|
|
131
|
+
[c * c - s * s, 2 * c * s, 0],
|
|
132
|
+
[2 * c * s, s * s - c * c, 0],
|
|
133
|
+
[0, 0, 1],
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def reflect_x(self) -> Self:
|
|
138
|
+
return self.apply_matrix(
|
|
139
|
+
[
|
|
140
|
+
[1, 0, 0],
|
|
141
|
+
[0, -1, 0],
|
|
142
|
+
[0, 0, 1],
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def reflect_y(self) -> Self:
|
|
147
|
+
return self.apply_matrix(
|
|
148
|
+
[
|
|
149
|
+
[-1, 0, 0],
|
|
150
|
+
[0, 1, 0],
|
|
151
|
+
[0, 0, 1],
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def shear(self, kx=0.0, ky=0.0) -> Self:
|
|
156
|
+
return self.apply_matrix(
|
|
157
|
+
[
|
|
158
|
+
[1, kx, 0],
|
|
159
|
+
[ky, 1, 0],
|
|
160
|
+
[0, 0, 1],
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def matrix(self):
|
|
165
|
+
return self.M
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class PointGeometryMixin(ABC):
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def transformed(
|
|
171
|
+
self,
|
|
172
|
+
M: Sequence[Sequence[float]],
|
|
173
|
+
) -> Self:
|
|
174
|
+
raise NotImplementedError
|
|
175
|
+
|
|
176
|
+
def translate(self, dx: float, dy: float) -> Self:
|
|
177
|
+
M = [
|
|
178
|
+
[1, 0, dx],
|
|
179
|
+
[0, 1, dy],
|
|
180
|
+
[0, 0, 1],
|
|
181
|
+
]
|
|
182
|
+
return self.transformed(M)
|
|
183
|
+
|
|
184
|
+
def scale(
|
|
185
|
+
self, sx: float, sy: float | None = None
|
|
186
|
+
) -> Self:
|
|
187
|
+
if sy is None:
|
|
188
|
+
sy = sx
|
|
189
|
+
|
|
190
|
+
M = [
|
|
191
|
+
[sx, 0, 0],
|
|
192
|
+
[0, sy, 0],
|
|
193
|
+
[0, 0, 1],
|
|
194
|
+
]
|
|
195
|
+
return self.transformed(M)
|
|
196
|
+
|
|
197
|
+
def rotate_deg(
|
|
198
|
+
self,
|
|
199
|
+
theta: float,
|
|
200
|
+
pivot: PointLike = (0.0, 0.0),
|
|
201
|
+
):
|
|
202
|
+
px, py = pivot
|
|
203
|
+
|
|
204
|
+
theta = theta % 360
|
|
205
|
+
match theta:
|
|
206
|
+
case 0:
|
|
207
|
+
c, s = 1.0, 0.0
|
|
208
|
+
case 90:
|
|
209
|
+
c, s = 0.0, 1.0
|
|
210
|
+
case 180:
|
|
211
|
+
c, s = -1.0, 0.0
|
|
212
|
+
case 270:
|
|
213
|
+
c, s = 0.0, -1.0
|
|
214
|
+
case _:
|
|
215
|
+
angle = math.radians(theta)
|
|
216
|
+
c, s = np.cos(angle), np.sin(angle)
|
|
217
|
+
|
|
218
|
+
R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
|
219
|
+
T1 = np.array([[1, 0, -px], [0, 1, -py], [0, 0, 1]])
|
|
220
|
+
T2 = np.array([[1, 0, px], [0, 1, py], [0, 0, 1]])
|
|
221
|
+
|
|
222
|
+
M = (T2 @ R @ T1).tolist()
|
|
223
|
+
return self.transformed(M)
|
|
224
|
+
|
|
225
|
+
def reflect_x(self) -> Self:
|
|
226
|
+
M = [
|
|
227
|
+
[1, 0, 0],
|
|
228
|
+
[0, -1, 0],
|
|
229
|
+
[0, 0, 1],
|
|
230
|
+
]
|
|
231
|
+
return self.transformed(M)
|
|
232
|
+
|
|
233
|
+
def reflect_y(self) -> Self:
|
|
234
|
+
M = [
|
|
235
|
+
[-1, 0, 0],
|
|
236
|
+
[0, 1, 0],
|
|
237
|
+
[0, 0, 1],
|
|
238
|
+
]
|
|
239
|
+
return self.transformed(M)
|
|
240
|
+
|
|
241
|
+
def reflect(
|
|
242
|
+
self,
|
|
243
|
+
direction: PointLike,
|
|
244
|
+
):
|
|
245
|
+
vx, vy = direction
|
|
246
|
+
|
|
247
|
+
length = math.hypot(vx, vy)
|
|
248
|
+
if length == 0:
|
|
249
|
+
raise ValueError("direction vector cannot be zero")
|
|
250
|
+
|
|
251
|
+
ux = vx / length
|
|
252
|
+
uy = vy / length
|
|
253
|
+
|
|
254
|
+
M = [
|
|
255
|
+
[2 * ux * ux - 1, 2 * ux * uy, 0],
|
|
256
|
+
[2 * ux * uy, 2 * uy * uy - 1, 0],
|
|
257
|
+
[0, 0, 1],
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
return self.transformed(M)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(slots=True, frozen=True)
|
|
264
|
+
class KeyPoint(PointGeometryMixin):
|
|
265
|
+
p: Point2f
|
|
266
|
+
score: float | None = None
|
|
267
|
+
|
|
268
|
+
@override
|
|
269
|
+
def transformed(
|
|
270
|
+
self,
|
|
271
|
+
M: Sequence[Sequence[float]],
|
|
272
|
+
) -> Self:
|
|
273
|
+
point = Point2f.transformed(self.p, M)
|
|
274
|
+
return replace(self, p=point)
|
|
275
|
+
|
|
276
|
+
def to_list(self) -> list[float]:
|
|
277
|
+
return list(self.p)+([self.score] if self.score is not None else [])
|
|
278
|
+
|
|
279
|
+
def to_dict(self) -> dict:
|
|
280
|
+
return {"p": list(self.p), "score": self.score}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass(slots=True, frozen=True)
|
|
284
|
+
class Rect(PointGeometryMixin):
|
|
285
|
+
pmin: Point2f
|
|
286
|
+
pmax: Point2f
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def from_xyxy(
|
|
290
|
+
cls,
|
|
291
|
+
xmin: float,
|
|
292
|
+
ymin: float,
|
|
293
|
+
xmax: float,
|
|
294
|
+
ymax: float,
|
|
295
|
+
) -> Rect:
|
|
296
|
+
return cls(
|
|
297
|
+
Point2f(xmin, ymin),
|
|
298
|
+
Point2f(xmax, ymax),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def to_list(self) -> list[float]:
|
|
302
|
+
return list(self.pmin) + list(self.pmax)
|
|
303
|
+
|
|
304
|
+
def to_dict(self) -> dict:
|
|
305
|
+
return {"pmin": list(self.pmin), "pmax": list(self.pmax)}
|
|
306
|
+
|
|
307
|
+
def to_polygon(self) -> Polygon:
|
|
308
|
+
return Polygon(
|
|
309
|
+
[
|
|
310
|
+
self.pmin,
|
|
311
|
+
Point2f(self.pmax.x, self.pmin.y),
|
|
312
|
+
self.pmax,
|
|
313
|
+
Point2f(self.pmin.x, self.pmax.y),
|
|
314
|
+
]
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
@override
|
|
318
|
+
def transformed(
|
|
319
|
+
self,
|
|
320
|
+
M: Sequence[Sequence[float]],
|
|
321
|
+
) -> Self:
|
|
322
|
+
rect = self.to_polygon().transformed(M).to_rect()
|
|
323
|
+
return self.__class__(rect.pmin, rect.pmax)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@dataclass(slots=True, frozen=True)
|
|
327
|
+
class Polygon(PointGeometryMixin):
|
|
328
|
+
vertices: list[Point2f]
|
|
329
|
+
|
|
330
|
+
@override
|
|
331
|
+
def transformed(
|
|
332
|
+
self,
|
|
333
|
+
M: Sequence[Sequence[float]],
|
|
334
|
+
) -> Self:
|
|
335
|
+
vertices: list[Point2f] = []
|
|
336
|
+
for vertex in self.vertices:
|
|
337
|
+
vertices.append(Point2f.transformed(vertex, M))
|
|
338
|
+
|
|
339
|
+
return replace(self, vertices=vertices)
|
|
340
|
+
|
|
341
|
+
def to_rect(self) -> Rect:
|
|
342
|
+
xs = [vertex.x for vertex in self.vertices]
|
|
343
|
+
ys = [vertex.y for vertex in self.vertices]
|
|
344
|
+
return Rect.from_xyxy(min(xs), min(ys), max(xs), max(ys))
|
|
345
|
+
|
|
346
|
+
def __iter__(self):
|
|
347
|
+
yield from self.vertices
|
|
348
|
+
|
|
349
|
+
def to_list(self) -> list[list[float]]:
|
|
350
|
+
return [list(v) for v in self.vertices]
|
|
351
|
+
|
|
352
|
+
def to_dict(self) -> dict:
|
|
353
|
+
return {"vertices": self.to_list()}
|