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.
Files changed (57) hide show
  1. kotoha-0.1.0a1/PKG-INFO +103 -0
  2. kotoha-0.1.0a1/README.md +70 -0
  3. kotoha-0.1.0a1/kotoha/__init__.py +33 -0
  4. kotoha-0.1.0a1/kotoha/_geometry_types.py +353 -0
  5. kotoha-0.1.0a1/kotoha/common.py +514 -0
  6. kotoha-0.1.0a1/kotoha/dataset_info.py +36 -0
  7. kotoha-0.1.0a1/kotoha/experiments/cpp/__init__.py +3 -0
  8. kotoha-0.1.0a1/kotoha/experiments/image_filter.py +348 -0
  9. kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/__init__.py +1 -0
  10. kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/heatmap_pose.py +103 -0
  11. kotoha-0.1.0a1/kotoha/experiments/model/pose/heatmap/utils.py +321 -0
  12. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/__init__.py +1 -0
  13. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/byte_track.py +96 -0
  14. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/__init__.py +0 -0
  15. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/basetrack.py +52 -0
  16. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/byte_tracker.py +352 -0
  17. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/kalman_filter.py +270 -0
  18. kotoha-0.1.0a1/kotoha/experiments/model/tracker/byte_track/tracker/matching.py +194 -0
  19. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/__init__.py +1 -0
  20. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/ocsort.py +28 -0
  21. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/__init__.py +0 -0
  22. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/association.py +379 -0
  23. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/kalmanfilter.py +1581 -0
  24. kotoha-0.1.0a1/kotoha/experiments/model/tracker/oc_sort/tracker/ocsort.py +345 -0
  25. kotoha-0.1.0a1/kotoha/experiments/npy_ref.py +331 -0
  26. kotoha-0.1.0a1/kotoha/experiments/voc.py +588 -0
  27. kotoha-0.1.0a1/kotoha/geo.py +103 -0
  28. kotoha-0.1.0a1/kotoha/io.py +456 -0
  29. kotoha-0.1.0a1/kotoha/model/__init__.py +3 -0
  30. kotoha-0.1.0a1/kotoha/model/typing.py +89 -0
  31. kotoha-0.1.0a1/kotoha/model/ultralytics/utils.py +287 -0
  32. kotoha-0.1.0a1/kotoha/model/ultralytics/yolov5.py +227 -0
  33. kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8.py +100 -0
  34. kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8obb.py +134 -0
  35. kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8pose.py +76 -0
  36. kotoha-0.1.0a1/kotoha/model/ultralytics/yolov8seg.py +68 -0
  37. kotoha-0.1.0a1/kotoha/polygon/__init__.py +13 -0
  38. kotoha-0.1.0a1/kotoha/polygon/array.py +108 -0
  39. kotoha-0.1.0a1/kotoha/polygon/core.py +259 -0
  40. kotoha-0.1.0a1/kotoha/polygon/geometry.py +200 -0
  41. kotoha-0.1.0a1/kotoha/typing.py +30 -0
  42. kotoha-0.1.0a1/kotoha/visual/__init__.py +22 -0
  43. kotoha-0.1.0a1/kotoha/visual/color.py +87 -0
  44. kotoha-0.1.0a1/kotoha/visual/visual.py +474 -0
  45. kotoha-0.1.0a1/kotoha.egg-info/PKG-INFO +103 -0
  46. kotoha-0.1.0a1/kotoha.egg-info/SOURCES.txt +55 -0
  47. kotoha-0.1.0a1/kotoha.egg-info/dependency_links.txt +1 -0
  48. kotoha-0.1.0a1/kotoha.egg-info/requires.txt +12 -0
  49. kotoha-0.1.0a1/kotoha.egg-info/top_level.txt +1 -0
  50. kotoha-0.1.0a1/pyproject.toml +57 -0
  51. kotoha-0.1.0a1/setup.cfg +4 -0
  52. kotoha-0.1.0a1/tests/test_cleanup.py +46 -0
  53. kotoha-0.1.0a1/tests/test_geo.py +49 -0
  54. kotoha-0.1.0a1/tests/test_model_typing.py +117 -0
  55. kotoha-0.1.0a1/tests/test_public_api.py +39 -0
  56. kotoha-0.1.0a1/tests/test_typing.py +161 -0
  57. kotoha-0.1.0a1/tests/test_vocio.py +10 -0
@@ -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.
@@ -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()}