machinevision-toolbox-python 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
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.
- {machinevision_toolbox_python-2.0.0.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/METADATA +13 -5
- {machinevision_toolbox_python-2.0.0.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/RECORD +21 -31
- {machinevision_toolbox_python-2.0.0.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/WHEEL +1 -1
- machinevisiontoolbox/BundleAdjust.py +1 -1
- machinevisiontoolbox/Camera.py +95 -80
- machinevisiontoolbox/ImageBlobs.py +209 -29
- machinevisiontoolbox/ImageCore.py +105 -97
- machinevisiontoolbox/ImageIO.py +5 -1
- machinevisiontoolbox/ImagePointFeatures.py +8 -6
- machinevisiontoolbox/ImageProcessing.py +1 -1
- machinevisiontoolbox/ImageRegionFeatures.py +9 -6
- machinevisiontoolbox/ImageWholeFeatures.py +113 -72
- machinevisiontoolbox/Sources.py +20 -4
- machinevisiontoolbox/__init__.py +1 -1
- machinevisiontoolbox/base/imageio.py +68 -33
- machinevisiontoolbox/bin/_iminteractive.py +461 -0
- machinevisiontoolbox/bin/bagtool.py +120 -6
- machinevisiontoolbox/bin/imtool.py +34 -5
- machinevisiontoolbox/bin/mvtbtool.py +12 -5
- machinevisiontoolbox/ImageCore.docstring +0 -2877
- machinevisiontoolbox/aruco0.pdf +0 -0
- machinevisiontoolbox/aruco50.pdf +0 -0
- machinevisiontoolbox/bin/labeltool.py +0 -147
- machinevisiontoolbox/blocks/Icons/icons.sh +0 -4
- machinevisiontoolbox/cvfuncs.py +0 -40
- machinevisiontoolbox/docbugs.py +0 -47
- machinevisiontoolbox/newcameras.py +0 -55
- machinevisiontoolbox/test_skimage.py +0 -12
- machinevisiontoolbox/testblobplots.py +0 -17
- machinevisiontoolbox/x.json +0 -296
- {machinevision_toolbox_python-2.0.0.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/entry_points.txt +0 -0
- {machinevision_toolbox_python-2.0.0.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,13 +3,15 @@ Detection and description of 2D blob (connected region) features in images.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import copy
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
import tempfile
|
|
9
11
|
import webbrowser
|
|
10
12
|
from collections import UserList, namedtuple
|
|
11
13
|
from dataclasses import dataclass
|
|
12
|
-
from typing import Any, cast
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
13
15
|
|
|
14
16
|
import cv2
|
|
15
17
|
import matplotlib.pyplot as plt
|
|
@@ -22,6 +24,9 @@ import spatialmath.base as smb
|
|
|
22
24
|
from machinevisiontoolbox.base import plot_labelbox, mpl_styling
|
|
23
25
|
from machinevisiontoolbox.decorators import array_result, scalar_result
|
|
24
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from machinevisiontoolbox._image_typing import _ImageBase
|
|
29
|
+
|
|
25
30
|
"""
|
|
26
31
|
NOTES
|
|
27
32
|
|
|
@@ -456,7 +461,10 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
456
461
|
|
|
457
462
|
## Equivalent ellipse
|
|
458
463
|
J = np.array([[M["mu20"], M["mu11"]], [M["mu11"], M["mu02"]]])
|
|
459
|
-
|
|
464
|
+
# J is real-symmetric, so use eigh (not eig): eig can return
|
|
465
|
+
# complex128 for a symmetric matrix due to floating-point
|
|
466
|
+
# noise even though the eigenvalues are mathematically real.
|
|
467
|
+
e, X = np.linalg.eigh(J)
|
|
460
468
|
blob_params["a"] = 2.0 * np.sqrt(e.max() / M["m00"])
|
|
461
469
|
blob_params["b"] = 2.0 * np.sqrt(e.min() / M["m00"])
|
|
462
470
|
|
|
@@ -629,7 +637,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
629
637
|
return self[:]
|
|
630
638
|
m: np.ndarray = np.array(mask).all(axis=0) # type: ignore[assignment]
|
|
631
639
|
|
|
632
|
-
return self[m]
|
|
640
|
+
return self[m.flatten()]
|
|
633
641
|
|
|
634
642
|
def sort(self, by: str = "area", reverse: bool = False) -> "Blobs": # type: ignore[override]
|
|
635
643
|
"""
|
|
@@ -1194,7 +1202,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
1194
1202
|
>>> blobs[2].level
|
|
1195
1203
|
>>> blobs.level
|
|
1196
1204
|
|
|
1197
|
-
:seealso: :meth:`color` :meth:`parent` :meth:`children` :meth:`dotfile`
|
|
1205
|
+
:seealso: :meth:`color` :meth:`parent` :meth:`children` :meth:`dotfile` :meth:`graph`
|
|
1198
1206
|
"""
|
|
1199
1207
|
return [b.level for b in self.data]
|
|
1200
1208
|
|
|
@@ -1247,7 +1255,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
1247
1255
|
|
|
1248
1256
|
A parent of -1 is the image background.
|
|
1249
1257
|
|
|
1250
|
-
:seealso: :meth:`id` :meth:`children` :meth:`level` :meth:`dotfile`
|
|
1258
|
+
:seealso: :meth:`id` :meth:`children` :meth:`level` :meth:`dotfile` :meth:`graph`
|
|
1251
1259
|
"""
|
|
1252
1260
|
return [b.parent.id if b.parent is not None else -1 for b in self.data]
|
|
1253
1261
|
|
|
@@ -1272,7 +1280,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
1272
1280
|
>>> blobs[6].id
|
|
1273
1281
|
|
|
1274
1282
|
|
|
1275
|
-
:seealso: :meth:`parent` :meth:`children` :meth:`level` :meth:`dotfile`
|
|
1283
|
+
:seealso: :meth:`parent` :meth:`children` :meth:`level` :meth:`dotfile` :meth:`graph`
|
|
1276
1284
|
"""
|
|
1277
1285
|
return [b.id for b in self.data]
|
|
1278
1286
|
|
|
@@ -1293,7 +1301,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
1293
1301
|
>>> blobs = im.blobs()
|
|
1294
1302
|
>>> blobs[5].children
|
|
1295
1303
|
|
|
1296
|
-
:seealso: :meth:`parent` :meth:`level` :meth:`dotfile`
|
|
1304
|
+
:seealso: :meth:`parent` :meth:`level` :meth:`dotfile` :meth:`graph`
|
|
1297
1305
|
"""
|
|
1298
1306
|
return [[c.id for c in b.children] for b in self.data]
|
|
1299
1307
|
|
|
@@ -2380,6 +2388,180 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
2380
2388
|
|
|
2381
2389
|
return image.__class__(labels)
|
|
2382
2390
|
|
|
2391
|
+
def graph(
|
|
2392
|
+
self,
|
|
2393
|
+
format: str = "dot",
|
|
2394
|
+
filename: str | io.TextIOBase | None = None,
|
|
2395
|
+
direction: str | None = None,
|
|
2396
|
+
) -> str:
|
|
2397
|
+
"""
|
|
2398
|
+
Render blob hierarchy graph text.
|
|
2399
|
+
|
|
2400
|
+
:param format: graph output format: ``"dot"``, ``"mermaid"``,
|
|
2401
|
+
``"mermaid_fenced"``, ``"graphml"`` or ``"elk"``
|
|
2402
|
+
:type format: str, optional
|
|
2403
|
+
:param filename: destination path or open text stream, defaults to None
|
|
2404
|
+
:type filename: str | file handle | None, optional
|
|
2405
|
+
:param direction: graph direction for DOT output, defaults to None
|
|
2406
|
+
:type direction: str, optional
|
|
2407
|
+
:return: rendered graph text
|
|
2408
|
+
:rtype: str
|
|
2409
|
+
|
|
2410
|
+
Creates graph text representing the blob hierarchy. If ``filename`` is
|
|
2411
|
+
provided, the rendered text is also written to it.
|
|
2412
|
+
|
|
2413
|
+
Mermaid outputs:
|
|
2414
|
+
|
|
2415
|
+
- ``"mermaid"`` returns plain Mermaid text.
|
|
2416
|
+
- ``"mermaid_fenced"`` returns Markdown fenced text suitable for
|
|
2417
|
+
direct display via ``IPython.display.Markdown``.
|
|
2418
|
+
|
|
2419
|
+
The blob hierarchy is represented as a directed graph where each node is
|
|
2420
|
+
a blob id and each edge points from parent blob to child blob.
|
|
2421
|
+
|
|
2422
|
+
:seealso: :meth:`dotfile` :meth:`parent` :meth:`children` :meth:`level`
|
|
2423
|
+
"""
|
|
2424
|
+
|
|
2425
|
+
graph_format = format.lower().replace("-", "_")
|
|
2426
|
+
|
|
2427
|
+
def _parent_id(blob: Any) -> int:
|
|
2428
|
+
parent = blob.parent
|
|
2429
|
+
if parent is None:
|
|
2430
|
+
return -1
|
|
2431
|
+
if isinstance(parent, int):
|
|
2432
|
+
return parent
|
|
2433
|
+
return parent.id
|
|
2434
|
+
|
|
2435
|
+
hierarchy_edges = [(_parent_id(blob), blob.id) for blob in self]
|
|
2436
|
+
node_ids_set = {blob.id for blob in self}
|
|
2437
|
+
if any(parent_id == -1 for parent_id, _ in hierarchy_edges):
|
|
2438
|
+
node_ids_set.add(-1)
|
|
2439
|
+
|
|
2440
|
+
if graph_format == "dot":
|
|
2441
|
+
lines = ["digraph {"]
|
|
2442
|
+
if direction is not None:
|
|
2443
|
+
lines.append(f"rankdir = {direction}")
|
|
2444
|
+
|
|
2445
|
+
for node_id in sorted(node_ids_set):
|
|
2446
|
+
lines.append(f' "{node_id}"')
|
|
2447
|
+
|
|
2448
|
+
for parent_id, child_id in hierarchy_edges:
|
|
2449
|
+
lines.append(f' "{parent_id}" -> "{child_id}"')
|
|
2450
|
+
|
|
2451
|
+
lines.append("}")
|
|
2452
|
+
text = "\n".join(lines) + "\n"
|
|
2453
|
+
|
|
2454
|
+
elif graph_format in ("mermaid", "mermaid_fenced"):
|
|
2455
|
+
lines = ["flowchart TB"]
|
|
2456
|
+
node_ids: dict[Any, str] = {}
|
|
2457
|
+
sorted_nodes = sorted(node_ids_set)
|
|
2458
|
+
|
|
2459
|
+
for i, node in enumerate(sorted_nodes):
|
|
2460
|
+
node_alias = f"b{i}"
|
|
2461
|
+
node_ids[node] = node_alias
|
|
2462
|
+
lines.append(f' {node_alias}["{node}"]')
|
|
2463
|
+
|
|
2464
|
+
for parent_id, child_id in hierarchy_edges:
|
|
2465
|
+
lines.append(f" {node_ids[parent_id]} --> {node_ids[child_id]}")
|
|
2466
|
+
|
|
2467
|
+
mermaid_text = "\n".join(lines) + "\n"
|
|
2468
|
+
if graph_format == "mermaid_fenced":
|
|
2469
|
+
text = f"```mermaid\n{mermaid_text}```\n"
|
|
2470
|
+
else:
|
|
2471
|
+
text = mermaid_text
|
|
2472
|
+
|
|
2473
|
+
elif graph_format == "graphml":
|
|
2474
|
+
|
|
2475
|
+
def _xml_escape(value: str) -> str:
|
|
2476
|
+
return (
|
|
2477
|
+
value.replace("&", "&")
|
|
2478
|
+
.replace("<", "<")
|
|
2479
|
+
.replace(">", ">")
|
|
2480
|
+
.replace('"', """)
|
|
2481
|
+
.replace("'", "'")
|
|
2482
|
+
)
|
|
2483
|
+
|
|
2484
|
+
lines = [
|
|
2485
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
2486
|
+
'<graphml xmlns="http://graphml.graphdrawing.org/xmlns"',
|
|
2487
|
+
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
|
|
2488
|
+
' xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns',
|
|
2489
|
+
' http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">',
|
|
2490
|
+
' <key id="node_label" for="node" attr.name="label" attr.type="string"/>',
|
|
2491
|
+
' <graph id="G" edgedefault="directed">',
|
|
2492
|
+
]
|
|
2493
|
+
|
|
2494
|
+
for node in sorted(node_ids_set):
|
|
2495
|
+
node_id = f"n{node}"
|
|
2496
|
+
lines.extend(
|
|
2497
|
+
[
|
|
2498
|
+
f' <node id="{node_id}">',
|
|
2499
|
+
f' <data key="node_label">{_xml_escape(str(node))}</data>',
|
|
2500
|
+
" </node>",
|
|
2501
|
+
]
|
|
2502
|
+
)
|
|
2503
|
+
|
|
2504
|
+
edge_id = 0
|
|
2505
|
+
for parent_id, child_id in hierarchy_edges:
|
|
2506
|
+
lines.append(
|
|
2507
|
+
f' <edge id="e{edge_id}" source="n{parent_id}" target="n{child_id}"/>'
|
|
2508
|
+
)
|
|
2509
|
+
edge_id += 1
|
|
2510
|
+
|
|
2511
|
+
lines.extend([" </graph>", "</graphml>"])
|
|
2512
|
+
text = "\n".join(lines) + "\n"
|
|
2513
|
+
|
|
2514
|
+
elif graph_format == "elk":
|
|
2515
|
+
children = []
|
|
2516
|
+
edges = []
|
|
2517
|
+
|
|
2518
|
+
for node in sorted(node_ids_set):
|
|
2519
|
+
children.append(
|
|
2520
|
+
{
|
|
2521
|
+
"id": f"n{node}",
|
|
2522
|
+
"labels": [{"text": str(node)}],
|
|
2523
|
+
"width": 60,
|
|
2524
|
+
"height": 40,
|
|
2525
|
+
}
|
|
2526
|
+
)
|
|
2527
|
+
|
|
2528
|
+
edge_id = 0
|
|
2529
|
+
for parent_id, child_id in hierarchy_edges:
|
|
2530
|
+
edges.append(
|
|
2531
|
+
{
|
|
2532
|
+
"id": f"e{edge_id}",
|
|
2533
|
+
"sources": [f"n{parent_id}"],
|
|
2534
|
+
"targets": [f"n{child_id}"],
|
|
2535
|
+
}
|
|
2536
|
+
)
|
|
2537
|
+
edge_id += 1
|
|
2538
|
+
|
|
2539
|
+
elk_graph = {
|
|
2540
|
+
"id": "root",
|
|
2541
|
+
"layoutOptions": {
|
|
2542
|
+
"org.eclipse.elk.algorithm": "layered",
|
|
2543
|
+
"org.eclipse.elk.direction": "DOWN",
|
|
2544
|
+
},
|
|
2545
|
+
"children": children,
|
|
2546
|
+
"edges": edges,
|
|
2547
|
+
}
|
|
2548
|
+
text = json.dumps(elk_graph, indent=2) + "\n"
|
|
2549
|
+
|
|
2550
|
+
else:
|
|
2551
|
+
raise ValueError(
|
|
2552
|
+
"unsupported graph format "
|
|
2553
|
+
f"'{format}', expected 'dot', 'mermaid', 'mermaid_fenced', 'graphml' or 'elk'"
|
|
2554
|
+
)
|
|
2555
|
+
|
|
2556
|
+
if filename is not None:
|
|
2557
|
+
if isinstance(filename, str):
|
|
2558
|
+
with open(filename, "w") as file:
|
|
2559
|
+
file.write(text)
|
|
2560
|
+
else:
|
|
2561
|
+
filename.write(text)
|
|
2562
|
+
|
|
2563
|
+
return text
|
|
2564
|
+
|
|
2383
2565
|
def dotfile(
|
|
2384
2566
|
self, filename: Any = None, direction: str | None = None, show: bool = False
|
|
2385
2567
|
) -> None:
|
|
@@ -2400,53 +2582,51 @@ class Blobs(UserList): # lgtm[py/missing-equals]
|
|
|
2400
2582
|
.. note:: If ``filename`` is a file object then the file will *not*
|
|
2401
2583
|
be closed after the GraphViz model is written.
|
|
2402
2584
|
|
|
2403
|
-
:seealso: :meth:`child` :meth:`parent` :meth:`level`
|
|
2585
|
+
:seealso: :meth:`graph` :meth:`child` :meth:`parent` :meth:`level`
|
|
2404
2586
|
"""
|
|
2405
2587
|
|
|
2406
2588
|
if show:
|
|
2407
2589
|
# create the temporary dotfile
|
|
2408
2590
|
filename = tempfile.TemporaryFile(mode="w")
|
|
2591
|
+
dot_text = self.graph(format="dot", direction=direction)
|
|
2592
|
+
|
|
2409
2593
|
if filename is None:
|
|
2410
2594
|
f = sys.stdout
|
|
2595
|
+
f.write(dot_text)
|
|
2411
2596
|
elif isinstance(filename, str):
|
|
2412
|
-
|
|
2597
|
+
with open(filename, "w") as f:
|
|
2598
|
+
f.write(dot_text)
|
|
2413
2599
|
else:
|
|
2414
2600
|
f = filename
|
|
2415
|
-
|
|
2416
|
-
print("digraph {", file=f)
|
|
2417
|
-
|
|
2418
|
-
if direction is not None:
|
|
2419
|
-
print(f"rankdir = {direction}", file=f)
|
|
2420
|
-
|
|
2421
|
-
# add the nodes including name and position
|
|
2422
|
-
for id, blob in enumerate(self):
|
|
2423
|
-
print(' "{:d}"'.format(id), file=f)
|
|
2424
|
-
print(
|
|
2425
|
-
' "{:d}" -> "{:d}"'.format(blob.parent if blob.parent else -1, id),
|
|
2426
|
-
file=f,
|
|
2427
|
-
)
|
|
2428
|
-
|
|
2429
|
-
print("}", file=f)
|
|
2601
|
+
f.write(dot_text)
|
|
2430
2602
|
|
|
2431
2603
|
if show:
|
|
2432
2604
|
# rewind the dot file, create PDF file in the filesystem, run dot
|
|
2433
|
-
|
|
2605
|
+
if filename is None:
|
|
2606
|
+
dot_stream = tempfile.TemporaryFile(mode="w+")
|
|
2607
|
+
dot_stream.write(dot_text)
|
|
2608
|
+
dot_stream.seek(0)
|
|
2609
|
+
elif isinstance(filename, str):
|
|
2610
|
+
dot_stream = open(filename, "r")
|
|
2611
|
+
else:
|
|
2612
|
+
f.seek(0)
|
|
2613
|
+
dot_stream = f
|
|
2614
|
+
|
|
2434
2615
|
pdffile = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False)
|
|
2435
2616
|
subprocess.run(
|
|
2436
2617
|
["dot", "-Tpdf"],
|
|
2437
|
-
stdin=cast(Any,
|
|
2618
|
+
stdin=cast(Any, dot_stream),
|
|
2438
2619
|
stdout=pdffile,
|
|
2439
2620
|
check=True,
|
|
2440
2621
|
)
|
|
2441
2622
|
|
|
2442
2623
|
# open the PDF file in browser (hopefully portable), then cleanup
|
|
2443
2624
|
webbrowser.open(f"file://{pdffile.name}")
|
|
2444
|
-
else:
|
|
2445
2625
|
if filename is None or isinstance(filename, str):
|
|
2446
|
-
|
|
2626
|
+
dot_stream.close()
|
|
2447
2627
|
|
|
2448
2628
|
|
|
2449
|
-
class ImageBlobsMixin:
|
|
2629
|
+
class ImageBlobsMixin(_ImageBase if TYPE_CHECKING else object):
|
|
2450
2630
|
def blobs(self, **kwargs) -> Blobs:
|
|
2451
2631
|
"""
|
|
2452
2632
|
Find and describe blobs in image
|
|
@@ -301,6 +301,57 @@ class Image(
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
# if dtype is not given, determine the appropriate type for the data
|
|
304
|
+
dtype = self._infer_dtype(image, dtype)
|
|
305
|
+
|
|
306
|
+
if binary:
|
|
307
|
+
image = image > 0
|
|
308
|
+
|
|
309
|
+
# change type of array to the determined dtype
|
|
310
|
+
if dtype is not None:
|
|
311
|
+
image = image.astype(dtype, copy=False)
|
|
312
|
+
|
|
313
|
+
self.name = name
|
|
314
|
+
|
|
315
|
+
color_dict = Image.colororder2dict(colororder)
|
|
316
|
+
|
|
317
|
+
image = self._reshape_to_size(image, size)
|
|
318
|
+
|
|
319
|
+
if image.ndim not in (2, 3):
|
|
320
|
+
raise ValueError(
|
|
321
|
+
"bad ndarray passed to Image constructor: must be 2D or 3D array"
|
|
322
|
+
)
|
|
323
|
+
if image.ndim == 3 and image.shape[2] == 1:
|
|
324
|
+
image = image[:, :, 0] # squeeze out singleton plane
|
|
325
|
+
|
|
326
|
+
if kwargs:
|
|
327
|
+
image = convert(image, **kwargs)
|
|
328
|
+
|
|
329
|
+
# assign the image to the object, copying if requested
|
|
330
|
+
if copy:
|
|
331
|
+
self._A = image.copy()
|
|
332
|
+
else:
|
|
333
|
+
self._A = image
|
|
334
|
+
|
|
335
|
+
# final check that colororder length matches number of planes
|
|
336
|
+
if colororder is not None:
|
|
337
|
+
if len(color_dict) != self.nplanes:
|
|
338
|
+
raise ValueError("colororder length does not match number of planes")
|
|
339
|
+
|
|
340
|
+
if colororder is None:
|
|
341
|
+
if self.nplanes == 3:
|
|
342
|
+
self.colororder = "RGB"
|
|
343
|
+
# warnings.warn("defaulting color to RGB")
|
|
344
|
+
else:
|
|
345
|
+
self.colororder = color_dict
|
|
346
|
+
|
|
347
|
+
self._stats = None
|
|
348
|
+
|
|
349
|
+
self.name = name
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _infer_dtype(image: np.ndarray, dtype: Dtype | bool | None) -> np.dtype | None:
|
|
353
|
+
"""Determine the pixel dtype for the constructor when ``dtype`` isn't
|
|
354
|
+
given explicitly, or resolve/validate it when it is."""
|
|
304
355
|
if dtype is None:
|
|
305
356
|
# no type given, automatically choose it
|
|
306
357
|
if np.issubdtype(image.dtype, np.floating):
|
|
@@ -335,17 +386,12 @@ class Image(
|
|
|
335
386
|
except TypeError:
|
|
336
387
|
raise ValueError("bad dtype argument passed to Image constructor")
|
|
337
388
|
|
|
338
|
-
|
|
339
|
-
image = image > 0
|
|
340
|
-
|
|
341
|
-
# change type of array to the determined dtype
|
|
342
|
-
if dtype is not None:
|
|
343
|
-
image = image.astype(dtype, copy=False)
|
|
344
|
-
|
|
345
|
-
self.name = name
|
|
346
|
-
|
|
347
|
-
color_dict = Image.colororder2dict(colororder)
|
|
389
|
+
return dtype
|
|
348
390
|
|
|
391
|
+
def _reshape_to_size(
|
|
392
|
+
self, image: np.ndarray, size: tuple | list | None
|
|
393
|
+
) -> np.ndarray:
|
|
394
|
+
"""Reshape 1D/2D pixel data per the constructor's ``size=`` argument."""
|
|
349
395
|
if isinstance(size, self.__class__):
|
|
350
396
|
# size is an Image instance, ignore the size/shape and use the image's shape
|
|
351
397
|
size = size.size
|
|
@@ -354,16 +400,7 @@ class Image(
|
|
|
354
400
|
if size is not None:
|
|
355
401
|
newsize = [size[1], size[0]]
|
|
356
402
|
|
|
357
|
-
if
|
|
358
|
-
nplanes = len(color_dict)
|
|
359
|
-
|
|
360
|
-
if len(size) == 3:
|
|
361
|
-
if nplanes != size[2]:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
"colororder length does not match number of planes in size"
|
|
364
|
-
)
|
|
365
|
-
newsize.append(nplanes)
|
|
366
|
-
elif len(size) == 3:
|
|
403
|
+
if len(size) == 3:
|
|
367
404
|
newsize.append(size[2])
|
|
368
405
|
|
|
369
406
|
if image.ndim == 1:
|
|
@@ -420,37 +457,7 @@ class Image(
|
|
|
420
457
|
|
|
421
458
|
image = image.reshape(*newsize)
|
|
422
459
|
|
|
423
|
-
|
|
424
|
-
raise ValueError(
|
|
425
|
-
"bad ndarray passed to Image constructor: must be 2D or 3D array"
|
|
426
|
-
)
|
|
427
|
-
if image.ndim == 3 and image.shape[2] == 1:
|
|
428
|
-
image = image[:, :, 0] # squeeze out singleton plane
|
|
429
|
-
|
|
430
|
-
if kwargs:
|
|
431
|
-
image = convert(image, **kwargs)
|
|
432
|
-
|
|
433
|
-
# assign the image to the object, copying if requested
|
|
434
|
-
if copy:
|
|
435
|
-
self._A = image.copy()
|
|
436
|
-
else:
|
|
437
|
-
self._A = image
|
|
438
|
-
|
|
439
|
-
# final check that colororder length matches number of planes
|
|
440
|
-
if colororder is not None:
|
|
441
|
-
if len(color_dict) != self.nplanes:
|
|
442
|
-
raise ValueError("colororder length does not match number of planes")
|
|
443
|
-
|
|
444
|
-
if colororder is None:
|
|
445
|
-
if self.nplanes == 3:
|
|
446
|
-
self.colororder = "RGB"
|
|
447
|
-
# warnings.warn("defaulting color to RGB")
|
|
448
|
-
else:
|
|
449
|
-
self.colororder = color_dict
|
|
450
|
-
|
|
451
|
-
self._stats = None
|
|
452
|
-
|
|
453
|
-
self.name = name
|
|
460
|
+
return image
|
|
454
461
|
|
|
455
462
|
@staticmethod
|
|
456
463
|
def _plane_stats(plane: np.ndarray) -> dict[str, float | int]:
|
|
@@ -2711,50 +2718,6 @@ class Image(
|
|
|
2711
2718
|
:seealso: :meth:`red` :meth:`green` :meth:`blue` :meth:`plane` :meth:`roi` :meth:`pixel`
|
|
2712
2719
|
"""
|
|
2713
2720
|
|
|
2714
|
-
def fixdims(
|
|
2715
|
-
out: np.ndarray, shape: tuple[int, ...], keys: tuple[Any, ...]
|
|
2716
|
-
) -> np.ndarray:
|
|
2717
|
-
# deal with the fact that some of the keys may have reduced the
|
|
2718
|
-
# dimensionality of the array, eg. a slice of span 0 or 1, or an integer
|
|
2719
|
-
# key.
|
|
2720
|
-
|
|
2721
|
-
# shape: (nrows, ncols, nplanes) in NumPy order
|
|
2722
|
-
# key: (rowspec, colspec, planespec) in NumPy order
|
|
2723
|
-
|
|
2724
|
-
def lenkey(key: int | slice, max: int) -> int:
|
|
2725
|
-
# compute the span of a particular key
|
|
2726
|
-
if isinstance(key, int):
|
|
2727
|
-
# int key has a span on 1
|
|
2728
|
-
return 1
|
|
2729
|
-
elif isinstance(key, slice):
|
|
2730
|
-
# slice key has a span depending on the slice and the corresponding
|
|
2731
|
-
# array dimension. we have to essentially replicate the logic of
|
|
2732
|
-
# slice() here.
|
|
2733
|
-
start = key.start if key.start is not None else 0
|
|
2734
|
-
stop = key.stop if key.stop is not None else max
|
|
2735
|
-
step = key.step if key.step is not None else 1
|
|
2736
|
-
n = stop - start
|
|
2737
|
-
if n < step:
|
|
2738
|
-
return 1
|
|
2739
|
-
else:
|
|
2740
|
-
return n // step
|
|
2741
|
-
|
|
2742
|
-
dims = list(shape)
|
|
2743
|
-
|
|
2744
|
-
if len(shape) == 2:
|
|
2745
|
-
dims = [lenkey(keys[0], shape[0]), lenkey(keys[1], shape[1])]
|
|
2746
|
-
elif len(shape) == 3:
|
|
2747
|
-
dims = [
|
|
2748
|
-
lenkey(keys[0], shape[0]),
|
|
2749
|
-
lenkey(keys[1], shape[1]),
|
|
2750
|
-
lenkey(keys[2], shape[2]),
|
|
2751
|
-
]
|
|
2752
|
-
# ignore loss of color dimension
|
|
2753
|
-
if dims[2] == 1:
|
|
2754
|
-
dims = dims[:2]
|
|
2755
|
-
|
|
2756
|
-
return out.reshape(tuple(dims))
|
|
2757
|
-
|
|
2758
2721
|
if (
|
|
2759
2722
|
isinstance(keys, tuple)
|
|
2760
2723
|
and len(keys) == 2
|
|
@@ -2791,7 +2754,7 @@ class Image(
|
|
|
2791
2754
|
out = self._A[keys]
|
|
2792
2755
|
|
|
2793
2756
|
if out.ndim < 3:
|
|
2794
|
-
out =
|
|
2757
|
+
out = self._fixdims(out, self._A.shape, keys)
|
|
2795
2758
|
|
|
2796
2759
|
else:
|
|
2797
2760
|
# greyscale image
|
|
@@ -2804,7 +2767,7 @@ class Image(
|
|
|
2804
2767
|
out = self._A[keys]
|
|
2805
2768
|
|
|
2806
2769
|
if out.ndim < 2:
|
|
2807
|
-
out =
|
|
2770
|
+
out = self._fixdims(out, self._A.shape, keys)
|
|
2808
2771
|
|
|
2809
2772
|
# a singleton plane dimensions is a grey scale image
|
|
2810
2773
|
if out.ndim == 3 and out.shape[2] == 1:
|
|
@@ -2825,6 +2788,51 @@ class Image(
|
|
|
2825
2788
|
else:
|
|
2826
2789
|
raise ValueError("invalid slice")
|
|
2827
2790
|
|
|
2791
|
+
@staticmethod
|
|
2792
|
+
def _lenkey(key: int | slice, max: int) -> int:
|
|
2793
|
+
"""Span of a single ``__getitem__`` key component (int or slice)."""
|
|
2794
|
+
if isinstance(key, int):
|
|
2795
|
+
# int key has a span on 1
|
|
2796
|
+
return 1
|
|
2797
|
+
elif isinstance(key, slice):
|
|
2798
|
+
# slice key has a span depending on the slice and the corresponding
|
|
2799
|
+
# array dimension. we have to essentially replicate the logic of
|
|
2800
|
+
# slice() here.
|
|
2801
|
+
start = key.start if key.start is not None else 0
|
|
2802
|
+
stop = key.stop if key.stop is not None else max
|
|
2803
|
+
step = key.step if key.step is not None else 1
|
|
2804
|
+
n = stop - start
|
|
2805
|
+
if n < step:
|
|
2806
|
+
return 1
|
|
2807
|
+
else:
|
|
2808
|
+
return n // step
|
|
2809
|
+
|
|
2810
|
+
@classmethod
|
|
2811
|
+
def _fixdims(
|
|
2812
|
+
cls, out: np.ndarray, shape: tuple[int, ...], keys: tuple[Any, ...]
|
|
2813
|
+
) -> np.ndarray:
|
|
2814
|
+
"""Restore dimensionality lost by ``__getitem__`` keys that collapse
|
|
2815
|
+
an axis (eg. a slice of span 0 or 1, or an integer key).
|
|
2816
|
+
|
|
2817
|
+
``shape``: (nrows, ncols, nplanes) in NumPy order.
|
|
2818
|
+
``keys``: (rowspec, colspec, planespec) in NumPy order.
|
|
2819
|
+
"""
|
|
2820
|
+
dims = list(shape)
|
|
2821
|
+
|
|
2822
|
+
if len(shape) == 2:
|
|
2823
|
+
dims = [cls._lenkey(keys[0], shape[0]), cls._lenkey(keys[1], shape[1])]
|
|
2824
|
+
elif len(shape) == 3:
|
|
2825
|
+
dims = [
|
|
2826
|
+
cls._lenkey(keys[0], shape[0]),
|
|
2827
|
+
cls._lenkey(keys[1], shape[1]),
|
|
2828
|
+
cls._lenkey(keys[2], shape[2]),
|
|
2829
|
+
]
|
|
2830
|
+
# ignore loss of color dimension
|
|
2831
|
+
if dims[2] == 1:
|
|
2832
|
+
dims = dims[:2]
|
|
2833
|
+
|
|
2834
|
+
return out.reshape(tuple(dims))
|
|
2835
|
+
|
|
2828
2836
|
def pixel(self, u: int, v: int) -> int | float | np.ndarray:
|
|
2829
2837
|
"""
|
|
2830
2838
|
Return pixel value
|
machinevisiontoolbox/ImageIO.py
CHANGED
|
@@ -178,10 +178,14 @@ class ImageIOMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
178
178
|
self.domain[1][0],
|
|
179
179
|
]
|
|
180
180
|
|
|
181
|
+
if self.colororder_str is not None:
|
|
182
|
+
colororder = self.colororder_str.replace(":", "")
|
|
183
|
+
else:
|
|
184
|
+
colororder = None
|
|
181
185
|
return idisp(
|
|
182
186
|
self._A,
|
|
183
187
|
title=display_title,
|
|
184
|
-
colororder=
|
|
188
|
+
colororder=colororder,
|
|
185
189
|
**kwargs,
|
|
186
190
|
)
|
|
187
191
|
|
|
@@ -3,7 +3,7 @@ Detection and matching of keypoint and descriptor features (SIFT, ORB, etc.) in
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
from typing import Any, Iterator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
|
7
7
|
|
|
8
8
|
import cv2
|
|
9
9
|
import matplotlib.lines as mlines
|
|
@@ -21,13 +21,15 @@ from machinevisiontoolbox.base import (
|
|
|
21
21
|
name2color,
|
|
22
22
|
safe_plt_show,
|
|
23
23
|
)
|
|
24
|
-
from machinevisiontoolbox._image_typing import _ImageBase
|
|
25
24
|
from machinevisiontoolbox.decorators import (
|
|
26
25
|
array_result,
|
|
27
26
|
array_result2,
|
|
28
27
|
scalar_result,
|
|
29
28
|
)
|
|
30
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from machinevisiontoolbox._image_typing import _ImageBase
|
|
32
|
+
|
|
31
33
|
# TODO, either subclass SIFTFeature(BaseFeature2D) or just use BaseFeature2D
|
|
32
34
|
# directly
|
|
33
35
|
|
|
@@ -280,7 +282,7 @@ class BaseFeature2D:
|
|
|
280
282
|
|
|
281
283
|
try: # Sort features into grid
|
|
282
284
|
nw, nh = nbins
|
|
283
|
-
except:
|
|
285
|
+
except (TypeError, ValueError):
|
|
284
286
|
nw = nbins
|
|
285
287
|
nh = nbins
|
|
286
288
|
|
|
@@ -294,8 +296,8 @@ class BaseFeature2D:
|
|
|
294
296
|
bins = np.zeros((nh, nw), dtype="int")
|
|
295
297
|
|
|
296
298
|
for f in self:
|
|
297
|
-
ix = f.p[0] // binwidth
|
|
298
|
-
iy = f.p[1] // binheight
|
|
299
|
+
ix = int(f.p[0].item() // binwidth)
|
|
300
|
+
iy = int(f.p[1].item() // binheight)
|
|
299
301
|
|
|
300
302
|
if bins[iy, ix] < nfeat:
|
|
301
303
|
keep.append(f)
|
|
@@ -2012,7 +2014,7 @@ class LUCIDFeature(BaseFeature2D):
|
|
|
2012
2014
|
pass
|
|
2013
2015
|
|
|
2014
2016
|
|
|
2015
|
-
class ImagePointFeaturesMixin(_ImageBase):
|
|
2017
|
+
class ImagePointFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
|
|
2016
2018
|
def _image2feature(
|
|
2017
2019
|
self,
|
|
2018
2020
|
cls,
|
|
@@ -1146,7 +1146,7 @@ class ImageProcessingMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
1146
1146
|
else:
|
|
1147
1147
|
try:
|
|
1148
1148
|
color = argcheck.getvector(image2, 3)
|
|
1149
|
-
except:
|
|
1149
|
+
except Exception:
|
|
1150
1150
|
raise ValueError("expecting a scalar, string or 3-vector")
|
|
1151
1151
|
if self.isbgr:
|
|
1152
1152
|
color = color[::-1]
|