pixelflow 0.5.0__tar.gz → 0.6.0__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.
- {pixelflow-0.5.0 → pixelflow-0.6.0}/LICENSE +1 -1
- {pixelflow-0.5.0 → pixelflow-0.6.0}/PKG-INFO +22 -12
- {pixelflow-0.5.0 → pixelflow-0.6.0}/README.md +15 -4
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/__init__.py +6 -2
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/detections/detections.py +144 -5
- pixelflow-0.6.0/pixelflow/masks.py +212 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/validators.py +0 -23
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow.egg-info/PKG-INFO +22 -12
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow.egg-info/SOURCES.txt +1 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pyproject.toml +6 -8
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/__init__.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/anchors.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/blur.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/box.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/classification.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/crossing.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/dot.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/filled_box.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/fps_counter.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/grid_overlay.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/heatmap.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/keypoint.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/keypoint_skeleton.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/label.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/mask.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/motion_dots.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/motion_trails.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/oval.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/pixelate.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/polygon.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/scale_bar.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/utils.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/annotators/zones.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/arrays.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/buffer.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/classifications/__init__.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/classifications/classifications.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/classifications/converters.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/colors.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/crossings.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/detections/__init__.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/detections/converters.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/detections/filters.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/labels.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/media.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/slicer.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/smoother.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/strategies.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/timer.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/tracker/__init__.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/tracker/bytetrack.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/tracker/kalman_filter.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/tracker/matching.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/tracker/track.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/transforms/__init__.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/transforms/detections.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/transforms/image.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/transforms/inverse.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow/zones.py +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow.egg-info/dependency_links.txt +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow.egg-info/requires.txt +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/pixelflow.egg-info/top_level.txt +0 -0
- {pixelflow-0.5.0 → pixelflow-0.6.0}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixelflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Computer vision processing library for visualizing and analyzing detection results from ML models
|
|
5
|
-
Author-email: Emrah Nazif <emrah@datamarkin.com
|
|
5
|
+
Author-email: Emrah Nazif <emrah@datamarkin.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
10
|
-
Project-URL: Documentation, https://pixelflow
|
|
7
|
+
Project-URL: Homepage, https://github.com/emrahnazif/pixelflow
|
|
8
|
+
Project-URL: Repository, https://github.com/emrahnazif/pixelflow
|
|
9
|
+
Project-URL: Issues, https://github.com/emrahnazif/pixelflow/issues
|
|
10
|
+
Project-URL: Documentation, https://emrah.fr/pixelflow/
|
|
11
11
|
Keywords: computer-vision,visualization,annotation,post-processing,tracking,opencv,yolo,detectron2,mayaku,ml-results
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,14 +15,13 @@ Classifier: Intended Audience :: Science/Research
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
22
|
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
24
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: shapely>=2.0.0
|
|
@@ -39,8 +38,8 @@ Dynamic: license-file
|
|
|
39
38
|
# PixelFlow
|
|
40
39
|
|
|
41
40
|
[](https://badge.fury.io/py/pixelflow)
|
|
42
|
-
[](https://github.com/emrahnazif/pixelflow/actions/workflows/ci.yml)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
44
43
|
[](https://opensource.org/licenses/MIT)
|
|
45
44
|
|
|
46
45
|
**The computer vision library that gets out of your way.**
|
|
@@ -132,6 +131,17 @@ detections = pf.from_easyocr(reader.readtext("sign.jpg"))
|
|
|
132
131
|
detections = pf.from_arrays(boxes, scores, class_ids, labels=model.class_names)
|
|
133
132
|
```
|
|
134
133
|
|
|
134
|
+
Segmentation models answer with raster masks. One call turns them into polygons,
|
|
135
|
+
whichever model they came from:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
detections = pf.from_arrays(boxes, scores, masks=masks) # boolean (N, H, W)
|
|
139
|
+
detections.masks_to_segments() # fills det.segments
|
|
140
|
+
detections.masks_to_segments(keep_masks=False) # ...and drops the rasters
|
|
141
|
+
|
|
142
|
+
polygon = pf.mask_to_polygon(mask) # or one mask at a time
|
|
143
|
+
```
|
|
144
|
+
|
|
135
145
|
### Classifications - When the Model Only Names
|
|
136
146
|
|
|
137
147
|
A classifier localises nothing, so its result carries no geometry at all:
|
|
@@ -457,7 +467,7 @@ report = detections.to_json_with_metrics()
|
|
|
457
467
|
|
|
458
468
|
## Documentation
|
|
459
469
|
|
|
460
|
-
Full documentation available at [https://
|
|
470
|
+
Full documentation available at [https://emrah.fr/pixelflow/](https://emrah.fr/pixelflow/)
|
|
461
471
|
|
|
462
472
|
## Contributing
|
|
463
473
|
|
|
@@ -469,4 +479,4 @@ MIT License - see LICENSE file for details.
|
|
|
469
479
|
|
|
470
480
|
---
|
|
471
481
|
|
|
472
|
-
Built with love by [
|
|
482
|
+
Built with love by [Emrah Nazif](https://emrah.fr)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# PixelFlow
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/py/pixelflow)
|
|
4
|
-
[](https://github.com/emrahnazif/pixelflow/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
8
|
**The computer vision library that gets out of your way.**
|
|
@@ -94,6 +94,17 @@ detections = pf.from_easyocr(reader.readtext("sign.jpg"))
|
|
|
94
94
|
detections = pf.from_arrays(boxes, scores, class_ids, labels=model.class_names)
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
Segmentation models answer with raster masks. One call turns them into polygons,
|
|
98
|
+
whichever model they came from:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
detections = pf.from_arrays(boxes, scores, masks=masks) # boolean (N, H, W)
|
|
102
|
+
detections.masks_to_segments() # fills det.segments
|
|
103
|
+
detections.masks_to_segments(keep_masks=False) # ...and drops the rasters
|
|
104
|
+
|
|
105
|
+
polygon = pf.mask_to_polygon(mask) # or one mask at a time
|
|
106
|
+
```
|
|
107
|
+
|
|
97
108
|
### Classifications - When the Model Only Names
|
|
98
109
|
|
|
99
110
|
A classifier localises nothing, so its result carries no geometry at all:
|
|
@@ -419,7 +430,7 @@ report = detections.to_json_with_metrics()
|
|
|
419
430
|
|
|
420
431
|
## Documentation
|
|
421
432
|
|
|
422
|
-
Full documentation available at [https://
|
|
433
|
+
Full documentation available at [https://emrah.fr/pixelflow/](https://emrah.fr/pixelflow/)
|
|
423
434
|
|
|
424
435
|
## Contributing
|
|
425
436
|
|
|
@@ -431,4 +442,4 @@ MIT License - see LICENSE file for details.
|
|
|
431
442
|
|
|
432
443
|
---
|
|
433
444
|
|
|
434
|
-
Built with love by [
|
|
445
|
+
Built with love by [Emrah Nazif](https://emrah.fr)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
__author__ = "
|
|
1
|
+
__version__ = "0.6.0"
|
|
2
|
+
__author__ = "Emrah NAZIF"
|
|
3
3
|
|
|
4
4
|
# Import core modules
|
|
5
5
|
from . import media
|
|
@@ -29,6 +29,7 @@ from .media import (
|
|
|
29
29
|
read_image, read_video, encode_image, display_video, display_image, save_image,
|
|
30
30
|
close_display, to_pil, from_pil,
|
|
31
31
|
)
|
|
32
|
+
from .masks import mask_to_polygon
|
|
32
33
|
from .zones import Zones
|
|
33
34
|
from .crossings import Crossings
|
|
34
35
|
from .slicer import SlicedInference, auto_slice_size
|
|
@@ -95,6 +96,9 @@ __all__ = [
|
|
|
95
96
|
"to_pil",
|
|
96
97
|
"from_pil",
|
|
97
98
|
|
|
99
|
+
# Mask geometry
|
|
100
|
+
"mask_to_polygon",
|
|
101
|
+
|
|
98
102
|
# Spatial analysis
|
|
99
103
|
"zones",
|
|
100
104
|
"Zones",
|
|
@@ -17,8 +17,8 @@ from PIL import Image
|
|
|
17
17
|
from pixelflow.validators import (validate_bbox,
|
|
18
18
|
validate_segments,
|
|
19
19
|
round_coord,
|
|
20
|
-
round_to_decimal
|
|
21
|
-
|
|
20
|
+
round_to_decimal)
|
|
21
|
+
from pixelflow.masks import mask_to_polygon, simplify_polygon
|
|
22
22
|
from typing import (List,
|
|
23
23
|
Iterator, Optional, Union, Any, Dict)
|
|
24
24
|
|
|
@@ -33,6 +33,11 @@ __all__ = ["KeyPoint", "Detection", "Detections"]
|
|
|
33
33
|
_PNG_MASK_DTYPES = frozenset(("bool", "uint8", "uint16"))
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _is_raster_mask(mask: Any) -> bool:
|
|
37
|
+
"""Whether an entry of `Detection.masks` is a pixel raster rather than a polygon."""
|
|
38
|
+
return isinstance(mask, np.ndarray)
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
def _is_png_encodable(mask: np.ndarray) -> bool:
|
|
37
42
|
"""Whether a mask array can be losslessly PNG-encoded by Pillow."""
|
|
38
43
|
if mask.size == 0 or str(mask.dtype) not in _PNG_MASK_DTYPES:
|
|
@@ -170,6 +175,11 @@ class Detection:
|
|
|
170
175
|
segments this field is not yet normalized to one shape.
|
|
171
176
|
segments (Optional[List[List[float]]]): Object outline as one polygon, normalized by
|
|
172
177
|
validate_segments to [[x, y], ...] at two decimals, or None.
|
|
178
|
+
Coordinates follow the pixel-corner convention bbox uses:
|
|
179
|
+
pixel (row i, column j) covers x from j to j + 1 and y
|
|
180
|
+
from i to i + 1. Polygons a model computed itself (the
|
|
181
|
+
Ultralytics converter passes them through) only
|
|
182
|
+
approximate this; masks_to_segments() makes them exact.
|
|
173
183
|
keypoints (Optional[List[KeyPoint]]): List of KeyPoint objects for pose/structure data.
|
|
174
184
|
class_id (Optional[Union[int, str]]): Numeric or string class identifier from model.
|
|
175
185
|
class_name (Optional[str]): Human-readable class name (e.g., "person", "vehicle").
|
|
@@ -373,7 +383,7 @@ class Detection:
|
|
|
373
383
|
if self.masks:
|
|
374
384
|
serializable_masks = []
|
|
375
385
|
for mask in self.masks:
|
|
376
|
-
if
|
|
386
|
+
if _is_raster_mask(mask):
|
|
377
387
|
if _is_png_encodable(mask):
|
|
378
388
|
# Binary mask - PNG-encode, then base64 for JSON
|
|
379
389
|
# transport. PNG compresses masks losslessly; encoding
|
|
@@ -533,8 +543,83 @@ class Detection:
|
|
|
533
543
|
- Recommended for real-time applications with complex segmentation masks
|
|
534
544
|
"""
|
|
535
545
|
if self.masks:
|
|
536
|
-
#
|
|
537
|
-
self.masks = [
|
|
546
|
+
# A raster is not a polygon: it passes through untouched, as documented.
|
|
547
|
+
self.masks = [
|
|
548
|
+
mask if _is_raster_mask(mask)
|
|
549
|
+
else simplify_polygon(mask, tolerance, preserve_topology)
|
|
550
|
+
for mask in self.masks
|
|
551
|
+
]
|
|
552
|
+
|
|
553
|
+
def masks_to_segments(self, tolerance: float = 1.5, keep_masks: bool = True) -> bool:
|
|
554
|
+
"""
|
|
555
|
+
Fill `segments` with the outline of this detection's raster mask.
|
|
556
|
+
|
|
557
|
+
Converts the raster in `masks` to one polygon with `mask_to_polygon` and
|
|
558
|
+
stores it in `segments`. Lets a result from any segmentation model be drawn,
|
|
559
|
+
stored or sent as a polygon without carrying the raster along.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
tolerance (float): Simplification tolerance in pixels, passed to
|
|
563
|
+
`mask_to_polygon`. Range: [0.0, inf). Default is 1.5.
|
|
564
|
+
keep_masks (bool): Whether to keep the raster in `masks` after converting.
|
|
565
|
+
False drops it to save memory. Default is True.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
bool: True if `segments` was set, False if the detection was left unchanged.
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
ValueError: If `tolerance` is negative.
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
>>> import numpy as np
|
|
575
|
+
>>> import pixelflow as pf
|
|
576
|
+
>>>
|
|
577
|
+
>>> mask = np.zeros((480, 640), dtype=bool)
|
|
578
|
+
>>> mask[100:200, 50:300] = True
|
|
579
|
+
>>> detection = pf.Detection(bbox=[50, 100, 300, 200], masks=[mask])
|
|
580
|
+
>>>
|
|
581
|
+
>>> # Fill segments, keep the raster for drawing
|
|
582
|
+
>>> detection.masks_to_segments()
|
|
583
|
+
True
|
|
584
|
+
>>> detection.segments
|
|
585
|
+
[[50.0, 100.0], [50.0, 200.0], [300.0, 200.0], [300.0, 100.0]]
|
|
586
|
+
>>>
|
|
587
|
+
>>> # Finer outline, raster dropped
|
|
588
|
+
>>> detection.masks_to_segments(tolerance=0.5, keep_masks=False)
|
|
589
|
+
True
|
|
590
|
+
>>> detection.masks is None
|
|
591
|
+
True
|
|
592
|
+
|
|
593
|
+
Notes:
|
|
594
|
+
- Modifies the detection in-place
|
|
595
|
+
- A detection with no mask, a polygon mask, or an empty raster is left
|
|
596
|
+
unchanged
|
|
597
|
+
- Existing `segments` are replaced when a raster is present: the raster
|
|
598
|
+
is what the model produced, and a polygon derived from it follows the
|
|
599
|
+
pixel-corner convention exactly
|
|
600
|
+
- Several rasters of the same shape are treated as parts of one object
|
|
601
|
+
and merged before tracing; `segments` still receives one outline
|
|
602
|
+
- With `keep_masks=False` only a converted raster is dropped. A raster
|
|
603
|
+
that produced no polygon stays, so no geometry is lost silently
|
|
604
|
+
"""
|
|
605
|
+
if not self.masks:
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
# Only a 2D raster can be traced; stacked or multi-channel arrays are left alone.
|
|
609
|
+
rasters = [m for m in self.masks if _is_raster_mask(m) and m.ndim == 2]
|
|
610
|
+
if not rasters:
|
|
611
|
+
return False
|
|
612
|
+
rasters = [m for m in rasters if m.shape == rasters[0].shape]
|
|
613
|
+
|
|
614
|
+
polygon = mask_to_polygon(np.logical_or.reduce(rasters), tolerance=tolerance)
|
|
615
|
+
if polygon is None:
|
|
616
|
+
return False
|
|
617
|
+
|
|
618
|
+
self.segments = polygon
|
|
619
|
+
if not keep_masks:
|
|
620
|
+
converted = {id(m) for m in rasters}
|
|
621
|
+
self.masks = [m for m in self.masks if id(m) not in converted] or None
|
|
622
|
+
return True
|
|
538
623
|
|
|
539
624
|
def copy(self) -> "Detection":
|
|
540
625
|
"""
|
|
@@ -796,6 +881,60 @@ class Detections:
|
|
|
796
881
|
detection.simplify_masks(tolerance=tolerance, preserve_topology=preserve_topology)
|
|
797
882
|
return self
|
|
798
883
|
|
|
884
|
+
def masks_to_segments(self, tolerance: float = 1.5, keep_masks: bool = True) -> 'Detections':
|
|
885
|
+
"""
|
|
886
|
+
Fill `segments` from the raster mask of every detection in the collection.
|
|
887
|
+
|
|
888
|
+
Turns the output of any segmentation model into polygons: each detection's
|
|
889
|
+
raster in `masks` is traced along its pixel corners, simplified, and stored in
|
|
890
|
+
`segments`. Every source then shares one polygon convention, whichever model
|
|
891
|
+
produced it.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
tolerance (float): Simplification tolerance in pixels. Higher values give
|
|
895
|
+
fewer points. Range: [0.0, inf). Default is 1.5.
|
|
896
|
+
keep_masks (bool): Whether to keep the rasters in `masks` after
|
|
897
|
+
converting. False drops them to save memory.
|
|
898
|
+
Default is True.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
Detections: Returns self for method chaining.
|
|
902
|
+
|
|
903
|
+
Raises:
|
|
904
|
+
ValueError: If `tolerance` is negative.
|
|
905
|
+
|
|
906
|
+
Example:
|
|
907
|
+
>>> import pixelflow as pf
|
|
908
|
+
>>>
|
|
909
|
+
>>> # SAM-style output: boxes and boolean (N, H, W) masks
|
|
910
|
+
>>> detections = pf.from_arrays(boxes, scores, masks=masks)
|
|
911
|
+
>>> detections.masks_to_segments()
|
|
912
|
+
>>> detections[0].segments # [[x, y], ...]
|
|
913
|
+
>>>
|
|
914
|
+
>>> # Draw outlines, which read `segments`
|
|
915
|
+
>>> image = pf.annotate.polygon(image, detections)
|
|
916
|
+
>>>
|
|
917
|
+
>>> # Keep only polygons, e.g. before serializing
|
|
918
|
+
>>> payload = detections.masks_to_segments(keep_masks=False).to_json()
|
|
919
|
+
>>>
|
|
920
|
+
>>> # Finer outlines at the cost of more points
|
|
921
|
+
>>> detections.masks_to_segments(tolerance=0.5)
|
|
922
|
+
|
|
923
|
+
Notes:
|
|
924
|
+
- Modifies detections in-place; order and every other field are untouched
|
|
925
|
+
- Detections without a raster mask, or with an empty one, are left unchanged
|
|
926
|
+
- Existing `segments` are replaced where a raster is present, which also
|
|
927
|
+
brings Ultralytics' own polygons onto the pixel-corner convention
|
|
928
|
+
- One outline per detection: holes and all but the largest region are
|
|
929
|
+
dropped. See `mask_to_polygon` for the exact rules
|
|
930
|
+
|
|
931
|
+
Performance Notes:
|
|
932
|
+
- A few milliseconds per detection at full-HD; cost follows object size
|
|
933
|
+
"""
|
|
934
|
+
for detection in self.detections:
|
|
935
|
+
detection.masks_to_segments(tolerance=tolerance, keep_masks=keep_masks)
|
|
936
|
+
return self
|
|
937
|
+
|
|
799
938
|
def copy(self) -> "Detections":
|
|
800
939
|
"""
|
|
801
940
|
Create a deep copy of all detections in the collection.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Raster masks to polygons.
|
|
2
|
+
|
|
3
|
+
Segmentation models answer in pixels - a boolean ``H x W`` raster per object - while
|
|
4
|
+
everything that stores, edits or transmits an outline wants a polygon. This module is
|
|
5
|
+
the bridge, and it settles the one question a bridge has to settle: where exactly a
|
|
6
|
+
polygon vertex sits relative to the pixels it encloses.
|
|
7
|
+
|
|
8
|
+
The convention is pixel corners. Pixel ``(row i, column j)`` covers the square from
|
|
9
|
+
``x = j`` to ``x = j + 1`` and ``y = i`` to ``y = i + 1``, and the polygon runs along
|
|
10
|
+
the outside edges of the pixels it encloses. A mask covering columns 10..19 therefore
|
|
11
|
+
spans ``x = 10`` to ``x = 20``, which is the same box ``[x1, y1, x2, y2]`` describes
|
|
12
|
+
for that object. Tracing through pixel centres instead - what ``cv2.findContours``
|
|
13
|
+
returns - shrinks every object by half a pixel on each side and turns a one-pixel-wide
|
|
14
|
+
line into a polygon of zero area.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import cv2
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from pixelflow.validators import validate_segments
|
|
21
|
+
|
|
22
|
+
__all__ = ["mask_to_polygon", "simplify_polygon"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _trace_outline(region: np.ndarray) -> np.ndarray:
|
|
26
|
+
"""
|
|
27
|
+
Trace the outer boundary of one connected region along its pixel corners.
|
|
28
|
+
|
|
29
|
+
Returns ``(N, 2)`` int32 ``(x, y)`` points in doubled coordinates: pixel corners
|
|
30
|
+
land on even numbers and the midpoints of pixel edges on odd ones, so the caller
|
|
31
|
+
halves the result. Staying in integers until then keeps every vertex exact.
|
|
32
|
+
"""
|
|
33
|
+
height, width = region.shape
|
|
34
|
+
|
|
35
|
+
# Each pixel becomes a 3x3 block on a (2H+1, 2W+1) grid: its centre at an
|
|
36
|
+
# odd/odd position, dilated out to its own corners. Neighbouring pixels share
|
|
37
|
+
# the grid points on their common edge, so the filled shape is the region's
|
|
38
|
+
# true footprint and its border passes through real pixel corners.
|
|
39
|
+
grid = np.zeros((2 * height + 1, 2 * width + 1), dtype=np.uint8)
|
|
40
|
+
grid[1::2, 1::2] = region
|
|
41
|
+
grid = cv2.dilate(grid, np.ones((3, 3), dtype=np.uint8))
|
|
42
|
+
|
|
43
|
+
contours, _ = cv2.findContours(grid, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
|
|
44
|
+
points = max(contours, key=len)[:, 0, :]
|
|
45
|
+
|
|
46
|
+
# OpenCV's border follower steps diagonally across a concave corner, from one
|
|
47
|
+
# edge midpoint to the next, because the corner itself has no background among
|
|
48
|
+
# its four neighbours. Every diagonal step is such a cut.
|
|
49
|
+
following = np.roll(points, -1, axis=0)
|
|
50
|
+
diagonal = np.abs(following - points).sum(axis=1) == 2
|
|
51
|
+
start, end = points[diagonal], following[diagonal]
|
|
52
|
+
# Each midpoint has one even coordinate; the corner takes the even one of each.
|
|
53
|
+
corners = np.where(start % 2 == 0, start, end)
|
|
54
|
+
return np.insert(points, np.nonzero(diagonal)[0] + 1, corners, axis=0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _simplify_outline(outline: np.ndarray, epsilon: float) -> np.ndarray:
|
|
58
|
+
"""
|
|
59
|
+
Douglas-Peucker on a closed outline, with its four extreme vertices pinned.
|
|
60
|
+
|
|
61
|
+
Simplifying the loop as a whole is free to drop the leftmost or topmost vertex
|
|
62
|
+
whenever it lies within `epsilon` of the result, which pulls the polygon's
|
|
63
|
+
bounding box inside the mask's. Cutting the loop at one vertex per side and
|
|
64
|
+
simplifying each arc between them keeps those vertices, so the box around the
|
|
65
|
+
polygon is exactly the box around the mask at any tolerance.
|
|
66
|
+
"""
|
|
67
|
+
xs, ys = outline[:, 0], outline[:, 1]
|
|
68
|
+
|
|
69
|
+
def extreme(primary, secondary):
|
|
70
|
+
# First along one axis, ties broken along the other.
|
|
71
|
+
candidates = np.flatnonzero(primary == primary.min())
|
|
72
|
+
return int(candidates[np.argmin(secondary[candidates])])
|
|
73
|
+
|
|
74
|
+
# Each side is a straight run of points; taking the end of the run rather than
|
|
75
|
+
# any point on it pins a real corner, so a rectangle keeps exactly four.
|
|
76
|
+
anchors = sorted([
|
|
77
|
+
extreme(xs, ys), # leftmost, then topmost
|
|
78
|
+
extreme(ys, -xs), # topmost, then rightmost
|
|
79
|
+
extreme(-xs, -ys), # rightmost, then bottommost
|
|
80
|
+
extreme(-ys, xs), # bottommost, then leftmost
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
arcs = []
|
|
84
|
+
for start, end in zip(anchors, anchors[1:] + [anchors[0] + len(outline)]):
|
|
85
|
+
# Both end anchors included; the last arc wraps round to the first anchor.
|
|
86
|
+
arc = outline.take(np.arange(start, end + 1), axis=0, mode="wrap")
|
|
87
|
+
# An open curve keeps both end points; the last one opens the next arc.
|
|
88
|
+
arcs.append(cv2.approxPolyDP(arc.reshape(-1, 1, 2), epsilon, False)[:-1, 0, :])
|
|
89
|
+
return np.concatenate(arcs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mask_to_polygon(mask, tolerance: float = 1.5):
|
|
93
|
+
"""
|
|
94
|
+
Convert one raster mask into one polygon outlining its largest region.
|
|
95
|
+
|
|
96
|
+
Traces the outer boundary of the largest connected region along its pixel
|
|
97
|
+
corners, then reduces the vertex count with Douglas-Peucker simplification.
|
|
98
|
+
The result is in the shape `Detection.segments` holds, so it can be assigned
|
|
99
|
+
directly.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
mask: 2D array of shape `(H, W)`. Non-zero pixels are the object, so a
|
|
103
|
+
boolean mask and a 0/255 uint8 mask behave the same. A probability
|
|
104
|
+
map must be thresholded by the caller first.
|
|
105
|
+
tolerance (float): Maximum distance in pixels a simplified edge may stray
|
|
106
|
+
from the traced outline. Range: [0.0, inf). `0` keeps
|
|
107
|
+
the exact pixel staircase and only removes points on
|
|
108
|
+
straight runs. Default is 1.5.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Optional[List[List[float]]]: The polygon as `[[x, y], ...]` in the mask's own
|
|
112
|
+
pixel coordinates, not closed (the first point is not repeated at the
|
|
113
|
+
end). None if the mask is empty.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If `mask` is not 2D, or `tolerance` is negative.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> import numpy as np
|
|
120
|
+
>>> import pixelflow as pf
|
|
121
|
+
>>>
|
|
122
|
+
>>> mask = np.zeros((480, 640), dtype=bool)
|
|
123
|
+
>>> mask[100:200, 50:300] = True
|
|
124
|
+
>>>
|
|
125
|
+
>>> # A rectangle comes back as its four pixel corners
|
|
126
|
+
>>> pf.mask_to_polygon(mask)
|
|
127
|
+
[[50.0, 100.0], [50.0, 200.0], [300.0, 200.0], [300.0, 100.0]]
|
|
128
|
+
>>>
|
|
129
|
+
>>> # Exact outline, every stair of the pixel boundary kept
|
|
130
|
+
>>> outline = pf.mask_to_polygon(mask, tolerance=0)
|
|
131
|
+
>>>
|
|
132
|
+
>>> # Assign straight to a detection
|
|
133
|
+
>>> detection = pf.Detection(bbox=[50, 100, 300, 200])
|
|
134
|
+
>>> detection.segments = pf.mask_to_polygon(mask)
|
|
135
|
+
|
|
136
|
+
Notes:
|
|
137
|
+
- Coordinates are pixel corners: a mask covering columns 10..19 spans
|
|
138
|
+
x = 10 to x = 20, so the polygon's bounding box equals the mask's
|
|
139
|
+
- One polygon per mask. Holes are filled and every region but the largest
|
|
140
|
+
is dropped, because `segments` holds a single outline
|
|
141
|
+
- Regions are 4-connected: pixels touching only at a corner are separate
|
|
142
|
+
regions. A region that pinches to a single corner still yields one
|
|
143
|
+
outline, which touches itself at that corner
|
|
144
|
+
- Simplification only removes vertices, and never the outermost one on
|
|
145
|
+
each side, so the remaining points are exact pixel corners and the
|
|
146
|
+
bounding box is exact at any tolerance
|
|
147
|
+
- The pixel staircase on a slanted edge is itself about one pixel of
|
|
148
|
+
noise. Below a tolerance of roughly 1.5 the stairs survive and a large
|
|
149
|
+
object keeps hundreds of points; at 1.5 it keeps tens
|
|
150
|
+
- No object is too small: a one-pixel mask returns the four corners of
|
|
151
|
+
that pixel at any tolerance, because the extreme vertices are kept
|
|
152
|
+
|
|
153
|
+
Performance Notes:
|
|
154
|
+
- One pass over the frame finds the occupied area; all other work is
|
|
155
|
+
proportional to that area, not the image
|
|
156
|
+
- In a 1920x1281 mask: well under 1 ms for a 20 px object, around 5 ms
|
|
157
|
+
for one 1000 px across
|
|
158
|
+
"""
|
|
159
|
+
mask = np.asarray(mask)
|
|
160
|
+
if mask.ndim != 2:
|
|
161
|
+
raise ValueError(f"mask must be 2D (H, W), got shape {mask.shape}")
|
|
162
|
+
if tolerance < 0:
|
|
163
|
+
raise ValueError(f"tolerance must be >= 0, got {tolerance}")
|
|
164
|
+
|
|
165
|
+
if mask.dtype != np.bool_:
|
|
166
|
+
mask = mask != 0
|
|
167
|
+
# A boolean array is already the 0/1 bytes OpenCV wants, so view it, don't copy it.
|
|
168
|
+
binary = np.ascontiguousarray(mask).view(np.uint8)
|
|
169
|
+
|
|
170
|
+
# Crop to the occupied part of the frame before anything else: an object is
|
|
171
|
+
# usually a small part of the image, and every later step costs by the pixel.
|
|
172
|
+
left, top, width, height = cv2.boundingRect(binary)
|
|
173
|
+
if width == 0:
|
|
174
|
+
return None
|
|
175
|
+
binary = binary[top:top + height, left:left + width]
|
|
176
|
+
|
|
177
|
+
# Label 0 is the background, so the regions start at 1.
|
|
178
|
+
_, labels, stats, _ = cv2.connectedComponentsWithStats(binary, connectivity=4)
|
|
179
|
+
largest = 1 + int(np.argmax(stats[1:, cv2.CC_STAT_AREA]))
|
|
180
|
+
x, y, width, height = (int(v) for v in stats[largest, :4])
|
|
181
|
+
region = (labels[y:y + height, x:x + width] == largest).astype(np.uint8)
|
|
182
|
+
|
|
183
|
+
outline = _simplify_outline(_trace_outline(region), 2.0 * tolerance) # doubled, like the points
|
|
184
|
+
return validate_segments(outline / 2.0 + (left + x, top + y))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def simplify_polygon(polygon_points, tolerance: float = 2.0, preserve_topology: bool = True):
|
|
188
|
+
"""
|
|
189
|
+
Reduce the number of points in one polygon with Douglas-Peucker simplification.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
polygon_points: The polygon as a sequence of (x, y) points. A closing point
|
|
193
|
+
that repeats the first one is accepted and dropped.
|
|
194
|
+
tolerance (float): Maximum distance in pixels a simplified edge may stray from
|
|
195
|
+
the original outline (higher = more simplified).
|
|
196
|
+
preserve_topology (bool): Accepted for backwards compatibility and ignored.
|
|
197
|
+
Plain Douglas-Peucker can, on a thin shape and a
|
|
198
|
+
large tolerance, produce an outline that crosses
|
|
199
|
+
itself.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List[List[float]]: The simplified polygon as ``[[x, y], ...]`` at the shared
|
|
203
|
+
precision, not closed.
|
|
204
|
+
"""
|
|
205
|
+
points = np.asarray(polygon_points, dtype=np.float32).reshape(-1, 2)
|
|
206
|
+
if len(points) > 1 and np.array_equal(points[0], points[-1]):
|
|
207
|
+
points = points[:-1]
|
|
208
|
+
if len(points) > 3:
|
|
209
|
+
# Douglas-Peucker only ever drops vertices, so the survivors are the caller's
|
|
210
|
+
# own points; validate_segments rounds the float32 artefacts back out of them.
|
|
211
|
+
points = cv2.approxPolyDP(points.reshape(-1, 1, 2), float(tolerance), True)[:, 0, :]
|
|
212
|
+
return validate_segments(points)
|
|
@@ -9,7 +9,6 @@ Optional fields; see round_coord for why a KeyPoint raises instead.
|
|
|
9
9
|
import math
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
|
-
from shapely.geometry import Polygon as Shapely_Polygon
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
# How much precision pixelflow keeps. Truncating pixel coordinates to whole
|
|
@@ -124,25 +123,3 @@ def round_to_decimal(value, decimals=CONFIDENCE_DECIMALS):
|
|
|
124
123
|
if value is not None:
|
|
125
124
|
return round(float(value), decimals)
|
|
126
125
|
return None
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def simplify_polygon(polygon_points: list, tolerance: float = 2.0, preserve_topology: bool = True) -> list:
|
|
130
|
-
"""
|
|
131
|
-
Simplifies a single polygon using Shapely.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
polygon_points (list): A list of tuples representing a polygon.
|
|
135
|
-
tolerance (float): The tolerance factor for simplification (higher = more simplified).
|
|
136
|
-
preserve_topology (bool): If True, the function will try to preserve the polygon's topology.
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
list: A simplified polygon represented as a list of tuples.
|
|
140
|
-
"""
|
|
141
|
-
# Convert the list of tuples to a Shapely Polygon
|
|
142
|
-
polygon = Shapely_Polygon(polygon_points)
|
|
143
|
-
|
|
144
|
-
# Simplify the polygon using the specified tolerance
|
|
145
|
-
simplified_polygon = polygon.simplify(tolerance=tolerance, preserve_topology=preserve_topology)
|
|
146
|
-
|
|
147
|
-
# Return the simplified coordinates as a list of tuples
|
|
148
|
-
return list(simplified_polygon.exterior.coords)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixelflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Computer vision processing library for visualizing and analyzing detection results from ML models
|
|
5
|
-
Author-email: Emrah Nazif <emrah@datamarkin.com
|
|
5
|
+
Author-email: Emrah Nazif <emrah@datamarkin.com>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
10
|
-
Project-URL: Documentation, https://pixelflow
|
|
7
|
+
Project-URL: Homepage, https://github.com/emrahnazif/pixelflow
|
|
8
|
+
Project-URL: Repository, https://github.com/emrahnazif/pixelflow
|
|
9
|
+
Project-URL: Issues, https://github.com/emrahnazif/pixelflow/issues
|
|
10
|
+
Project-URL: Documentation, https://emrah.fr/pixelflow/
|
|
11
11
|
Keywords: computer-vision,visualization,annotation,post-processing,tracking,opencv,yolo,detectron2,mayaku,ml-results
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,14 +15,13 @@ Classifier: Intended Audience :: Science/Research
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
22
|
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
24
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.10
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: shapely>=2.0.0
|
|
@@ -39,8 +38,8 @@ Dynamic: license-file
|
|
|
39
38
|
# PixelFlow
|
|
40
39
|
|
|
41
40
|
[](https://badge.fury.io/py/pixelflow)
|
|
42
|
-
[](https://github.com/emrahnazif/pixelflow/actions/workflows/ci.yml)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
44
43
|
[](https://opensource.org/licenses/MIT)
|
|
45
44
|
|
|
46
45
|
**The computer vision library that gets out of your way.**
|
|
@@ -132,6 +131,17 @@ detections = pf.from_easyocr(reader.readtext("sign.jpg"))
|
|
|
132
131
|
detections = pf.from_arrays(boxes, scores, class_ids, labels=model.class_names)
|
|
133
132
|
```
|
|
134
133
|
|
|
134
|
+
Segmentation models answer with raster masks. One call turns them into polygons,
|
|
135
|
+
whichever model they came from:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
detections = pf.from_arrays(boxes, scores, masks=masks) # boolean (N, H, W)
|
|
139
|
+
detections.masks_to_segments() # fills det.segments
|
|
140
|
+
detections.masks_to_segments(keep_masks=False) # ...and drops the rasters
|
|
141
|
+
|
|
142
|
+
polygon = pf.mask_to_polygon(mask) # or one mask at a time
|
|
143
|
+
```
|
|
144
|
+
|
|
135
145
|
### Classifications - When the Model Only Names
|
|
136
146
|
|
|
137
147
|
A classifier localises nothing, so its result carries no geometry at all:
|
|
@@ -457,7 +467,7 @@ report = detections.to_json_with_metrics()
|
|
|
457
467
|
|
|
458
468
|
## Documentation
|
|
459
469
|
|
|
460
|
-
Full documentation available at [https://
|
|
470
|
+
Full documentation available at [https://emrah.fr/pixelflow/](https://emrah.fr/pixelflow/)
|
|
461
471
|
|
|
462
472
|
## Contributing
|
|
463
473
|
|
|
@@ -469,4 +479,4 @@ MIT License - see LICENSE file for details.
|
|
|
469
479
|
|
|
470
480
|
---
|
|
471
481
|
|
|
472
|
-
Built with love by [
|
|
482
|
+
Built with love by [Emrah Nazif](https://emrah.fr)
|
|
@@ -7,11 +7,10 @@ name = "pixelflow"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Computer vision processing library for visualizing and analyzing detection results from ML models"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
|
-
{name = "Emrah Nazif", email = "emrah@datamarkin.com"}
|
|
14
|
-
{name = "Datamarkin", email = "support@datamarkin.com"}
|
|
13
|
+
{name = "Emrah Nazif", email = "emrah@datamarkin.com"}
|
|
15
14
|
]
|
|
16
15
|
|
|
17
16
|
keywords = [
|
|
@@ -34,7 +33,6 @@ classifiers = [
|
|
|
34
33
|
"License :: OSI Approved :: MIT License",
|
|
35
34
|
"Operating System :: OS Independent",
|
|
36
35
|
"Programming Language :: Python :: 3",
|
|
37
|
-
"Programming Language :: Python :: 3.9",
|
|
38
36
|
"Programming Language :: Python :: 3.10",
|
|
39
37
|
"Programming Language :: Python :: 3.11",
|
|
40
38
|
"Programming Language :: Python :: 3.12",
|
|
@@ -59,10 +57,10 @@ dev = [
|
|
|
59
57
|
]
|
|
60
58
|
|
|
61
59
|
[project.urls]
|
|
62
|
-
Homepage = "https://github.com/
|
|
63
|
-
Repository = "https://github.com/
|
|
64
|
-
Issues = "https://github.com/
|
|
65
|
-
Documentation = "https://pixelflow
|
|
60
|
+
Homepage = "https://github.com/emrahnazif/pixelflow"
|
|
61
|
+
Repository = "https://github.com/emrahnazif/pixelflow"
|
|
62
|
+
Issues = "https://github.com/emrahnazif/pixelflow/issues"
|
|
63
|
+
Documentation = "https://emrah.fr/pixelflow/"
|
|
66
64
|
|
|
67
65
|
[tool.setuptools.dynamic]
|
|
68
66
|
version = {attr = "pixelflow.__version__"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|