signa-core 0.1.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.
- signa_core-0.1.0/.gitignore +6 -0
- signa_core-0.1.0/LICENSE +21 -0
- signa_core-0.1.0/PKG-INFO +73 -0
- signa_core-0.1.0/README.md +54 -0
- signa_core-0.1.0/pyproject.toml +33 -0
- signa_core-0.1.0/signa_core/__init__.py +30 -0
- signa_core-0.1.0/signa_core/detect.py +69 -0
- signa_core-0.1.0/signa_core/dictionaries.py +89 -0
- signa_core-0.1.0/tests/test_detect.py +44 -0
signa_core-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Leiverkus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signa-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable, GUI-free ArUco marker detection core for the Signa/Evidentia pipeline (OpenCV).
|
|
5
|
+
Project-URL: Repository, https://github.com/leiverkus/signa
|
|
6
|
+
Author-email: Patrick Leiverkus <leiverkus@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: archaeology,aruco,evidentia,marker-detection,opencv,photogrammetry
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: opencv-contrib-python-headless>=4.7
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# signa-core
|
|
21
|
+
|
|
22
|
+
**Reusable, GUI-free ArUco marker detection for the Evidentia pipeline.**
|
|
23
|
+
|
|
24
|
+
`signa-core` is the pure OpenCV/numpy detection layer extracted from the
|
|
25
|
+
[Signa WebODM plugin](../README.md). It detects ArUco markers in images and
|
|
26
|
+
returns their ids, corners and centers — leaving the *interpretation* to the
|
|
27
|
+
caller:
|
|
28
|
+
|
|
29
|
+
- **Signa** (WebODM plugin) matches markers against surveyed coordinates → `gcp_list.txt`.
|
|
30
|
+
- **Mensura** matches markers against their known physical size → metric scale constraints.
|
|
31
|
+
|
|
32
|
+
Both share this one detection implementation instead of duplicating it (or
|
|
33
|
+
depending on `find-gcp`).
|
|
34
|
+
|
|
35
|
+
## Install / use
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install signa-core # or: pip install -e signa-core
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from signa_core import detect_markers
|
|
43
|
+
|
|
44
|
+
# dict_id 1 = DICT_4X4_100 (see DICT_CHOICES)
|
|
45
|
+
result = detect_markers(["img1.jpg", "img2.jpg"], dict_id=1, minrate=0.01, ignore=0.33)
|
|
46
|
+
# {'img1.jpg': [{'id': 7, 'corners': [[x, y], ...4], 'center': (cx, cy)}, ...], ...}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Licence
|
|
50
|
+
|
|
51
|
+
**MIT** — `signa-core` is the permissive, reusable core. The surrounding
|
|
52
|
+
**Signa WebODM plugin is AGPL-3.0** (WebODM-forced). This split mirrors the
|
|
53
|
+
Itinera pattern (MIT core + copyleft plugin).
|
|
54
|
+
|
|
55
|
+
## Note on the Signa worker
|
|
56
|
+
|
|
57
|
+
WebODM runs Signa's GCP detection by serializing a single self-contained
|
|
58
|
+
function into a Celery worker, so the plugin keeps its own inlined copy of the
|
|
59
|
+
detection primitive for that path. `signa-core` is the canonical, importable
|
|
60
|
+
version for all non-worker consumers; the two are kept behaviourally identical.
|
|
61
|
+
|
|
62
|
+
## Releasing (PyPI)
|
|
63
|
+
|
|
64
|
+
Published via **PyPI Trusted Publishing** (OIDC) — no API token is stored. See
|
|
65
|
+
[`.github/workflows/publish-signa-core.yml`](../.github/workflows/publish-signa-core.yml)
|
|
66
|
+
and its header for the one-time pypi.org publisher setup. To release:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# bump version in pyproject.toml, then:
|
|
70
|
+
git tag signa-core-v0.1.0 && git push origin signa-core-v0.1.0
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The workflow runs the tests, builds the sdist + wheel, and publishes them.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# signa-core
|
|
2
|
+
|
|
3
|
+
**Reusable, GUI-free ArUco marker detection for the Evidentia pipeline.**
|
|
4
|
+
|
|
5
|
+
`signa-core` is the pure OpenCV/numpy detection layer extracted from the
|
|
6
|
+
[Signa WebODM plugin](../README.md). It detects ArUco markers in images and
|
|
7
|
+
returns their ids, corners and centers — leaving the *interpretation* to the
|
|
8
|
+
caller:
|
|
9
|
+
|
|
10
|
+
- **Signa** (WebODM plugin) matches markers against surveyed coordinates → `gcp_list.txt`.
|
|
11
|
+
- **Mensura** matches markers against their known physical size → metric scale constraints.
|
|
12
|
+
|
|
13
|
+
Both share this one detection implementation instead of duplicating it (or
|
|
14
|
+
depending on `find-gcp`).
|
|
15
|
+
|
|
16
|
+
## Install / use
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install signa-core # or: pip install -e signa-core
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from signa_core import detect_markers
|
|
24
|
+
|
|
25
|
+
# dict_id 1 = DICT_4X4_100 (see DICT_CHOICES)
|
|
26
|
+
result = detect_markers(["img1.jpg", "img2.jpg"], dict_id=1, minrate=0.01, ignore=0.33)
|
|
27
|
+
# {'img1.jpg': [{'id': 7, 'corners': [[x, y], ...4], 'center': (cx, cy)}, ...], ...}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Licence
|
|
31
|
+
|
|
32
|
+
**MIT** — `signa-core` is the permissive, reusable core. The surrounding
|
|
33
|
+
**Signa WebODM plugin is AGPL-3.0** (WebODM-forced). This split mirrors the
|
|
34
|
+
Itinera pattern (MIT core + copyleft plugin).
|
|
35
|
+
|
|
36
|
+
## Note on the Signa worker
|
|
37
|
+
|
|
38
|
+
WebODM runs Signa's GCP detection by serializing a single self-contained
|
|
39
|
+
function into a Celery worker, so the plugin keeps its own inlined copy of the
|
|
40
|
+
detection primitive for that path. `signa-core` is the canonical, importable
|
|
41
|
+
version for all non-worker consumers; the two are kept behaviourally identical.
|
|
42
|
+
|
|
43
|
+
## Releasing (PyPI)
|
|
44
|
+
|
|
45
|
+
Published via **PyPI Trusted Publishing** (OIDC) — no API token is stored. See
|
|
46
|
+
[`.github/workflows/publish-signa-core.yml`](../.github/workflows/publish-signa-core.yml)
|
|
47
|
+
and its header for the one-time pypi.org publisher setup. To release:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# bump version in pyproject.toml, then:
|
|
51
|
+
git tag signa-core-v0.1.0 && git push origin signa-core-v0.1.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The workflow runs the tests, builds the sdist + wheel, and publishes them.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "signa-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reusable, GUI-free ArUco marker detection core for the Signa/Evidentia pipeline (OpenCV)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Patrick Leiverkus", email = "leiverkus@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["aruco", "opencv", "marker-detection", "photogrammetry", "archaeology", "evidentia"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.24",
|
|
26
|
+
"opencv-contrib-python-headless>=4.7",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/leiverkus/signa"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["signa_core"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""signa-core — reusable ArUco marker detection for the Evidentia pipeline.
|
|
3
|
+
|
|
4
|
+
GUI-/WebODM-free OpenCV detection shared by the Signa WebODM plugin (GCP
|
|
5
|
+
detection) and Mensura (metric scale calibration).
|
|
6
|
+
"""
|
|
7
|
+
from .detect import corner_center, detect_markers, prepare_image
|
|
8
|
+
from .dictionaries import (
|
|
9
|
+
DICT_CHOICES,
|
|
10
|
+
VALID_DICTS,
|
|
11
|
+
load_aruco,
|
|
12
|
+
make_detector,
|
|
13
|
+
make_dictionary,
|
|
14
|
+
make_parameters,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"detect_markers",
|
|
21
|
+
"corner_center",
|
|
22
|
+
"prepare_image",
|
|
23
|
+
"make_dictionary",
|
|
24
|
+
"make_parameters",
|
|
25
|
+
"make_detector",
|
|
26
|
+
"load_aruco",
|
|
27
|
+
"DICT_CHOICES",
|
|
28
|
+
"VALID_DICTS",
|
|
29
|
+
"__version__",
|
|
30
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Reusable ArUco marker detection (GUI-free, OpenCV).
|
|
3
|
+
|
|
4
|
+
The function mirrors the detection behaviour of the Signa WebODM plugin's
|
|
5
|
+
self-contained worker detector, but as a normal importable module so consumers
|
|
6
|
+
like Mensura (scale calibration) can reuse it. It returns per-image marker
|
|
7
|
+
detections (id, corners, center) and leaves the *interpretation* of those
|
|
8
|
+
markers — GCP matching (Signa) vs. metric scaling (Mensura) — to the caller.
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from .dictionaries import load_aruco, make_detector, make_dictionary, make_parameters
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def prepare_image(frame, cv2, enhance_contrast=True):
|
|
16
|
+
"""BGR frame -> grayscale, with an optional conservative equalization pass."""
|
|
17
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
18
|
+
if enhance_contrast and hasattr(cv2, "equalizeHist"):
|
|
19
|
+
return cv2.equalizeHist(gray)
|
|
20
|
+
return gray
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def corner_center(corner_set):
|
|
24
|
+
"""Mean of a marker's four corners as integer pixel coordinates."""
|
|
25
|
+
import numpy as np
|
|
26
|
+
pts = np.asarray(corner_set, dtype=float).reshape(-1, 2)
|
|
27
|
+
return (
|
|
28
|
+
int(round(float(np.mean(pts[:, 0])))),
|
|
29
|
+
int(round(float(np.mean(pts[:, 1])))),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_markers(image_paths, *, dict_id=1, minrate=0.01, ignore=0.33, adjust=True):
|
|
34
|
+
"""Detect ArUco markers across one or more images.
|
|
35
|
+
|
|
36
|
+
Parameters mirror the Signa detector: ``dict_id`` selects the ArUco
|
|
37
|
+
dictionary (see ``DICT_CHOICES``), ``minrate`` is the minimum marker
|
|
38
|
+
perimeter rate, ``ignore`` the ignored-margin-per-cell, ``adjust`` toggles
|
|
39
|
+
grayscale equalization.
|
|
40
|
+
|
|
41
|
+
Returns ``{image_basename: detections}`` where ``detections`` is a list of
|
|
42
|
+
``{'id': int, 'corners': [[x, y], ...4], 'center': (x, y)}`` — or ``None``
|
|
43
|
+
for an unreadable image.
|
|
44
|
+
"""
|
|
45
|
+
cv2, aruco = load_aruco()
|
|
46
|
+
dictionary = make_dictionary(dict_id, aruco)
|
|
47
|
+
detect = make_detector(dictionary, make_parameters(minrate, ignore, aruco), aruco)
|
|
48
|
+
|
|
49
|
+
results = {}
|
|
50
|
+
for path in image_paths:
|
|
51
|
+
frame = cv2.imread(path)
|
|
52
|
+
base = os.path.basename(path)
|
|
53
|
+
if frame is None:
|
|
54
|
+
results[base] = None
|
|
55
|
+
continue
|
|
56
|
+
gray = prepare_image(frame, cv2, adjust)
|
|
57
|
+
corners, ids, _ = detect(gray)
|
|
58
|
+
dets = []
|
|
59
|
+
if ids is not None:
|
|
60
|
+
for i in range(len(ids)):
|
|
61
|
+
marker_id = int(ids[i][0])
|
|
62
|
+
pts = corners[i].reshape(-1, 2)
|
|
63
|
+
dets.append({
|
|
64
|
+
"id": marker_id,
|
|
65
|
+
"corners": pts.tolist(),
|
|
66
|
+
"center": corner_center(corners[i]),
|
|
67
|
+
})
|
|
68
|
+
results[base] = dets
|
|
69
|
+
return results
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""ArUco dictionary and detector-parameter helpers (OpenCV-version compatible).
|
|
3
|
+
|
|
4
|
+
Pure OpenCV/numpy — no WebODM/Django. These are the shared, reusable detection
|
|
5
|
+
primitives extracted from the Signa WebODM plugin's self-contained worker
|
|
6
|
+
detector, so other consumers (e.g. Mensura's scale calibration) can reuse the
|
|
7
|
+
exact same detection behaviour without duplicating it.
|
|
8
|
+
|
|
9
|
+
The OpenCV ArUco API changed across versions (4.6 vs 4.7+); every helper here
|
|
10
|
+
handles both the legacy free-function form and the newer object-oriented form.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Single source of truth for the offered ArUco dictionaries, as (value, label)
|
|
14
|
+
# pairs. Ids 0..20 are OpenCV's predefined dictionaries; 99 is a legacy custom
|
|
15
|
+
# 3x3 dictionary built with extendDictionary for existing Signa marker sheets.
|
|
16
|
+
DICT_CHOICES = [
|
|
17
|
+
("0", "0 — DICT_4X4_50"),
|
|
18
|
+
("1", "1 — DICT_4X4_100"),
|
|
19
|
+
("2", "2 — DICT_4X4_250"),
|
|
20
|
+
("3", "3 — DICT_4X4_1000"),
|
|
21
|
+
("4", "4 — DICT_5X5_50"),
|
|
22
|
+
("5", "5 — DICT_5X5_100"),
|
|
23
|
+
("6", "6 — DICT_5X5_250"),
|
|
24
|
+
("7", "7 — DICT_5X5_1000"),
|
|
25
|
+
("8", "8 — DICT_6X6_50"),
|
|
26
|
+
("9", "9 — DICT_6X6_100"),
|
|
27
|
+
("10", "10 — DICT_6X6_250"),
|
|
28
|
+
("11", "11 — DICT_6X6_1000"),
|
|
29
|
+
("12", "12 — DICT_7X7_50"),
|
|
30
|
+
("13", "13 — DICT_7X7_100"),
|
|
31
|
+
("14", "14 — DICT_7X7_250"),
|
|
32
|
+
("15", "15 — DICT_7X7_1000"),
|
|
33
|
+
("16", "16 — DICT_ARUCO_ORIGINAL"),
|
|
34
|
+
("17", "17 — DICT_APRILTAG_16h5"),
|
|
35
|
+
("18", "18 — DICT_APRILTAG_25h9"),
|
|
36
|
+
("19", "19 — DICT_APRILTAG_36h10"),
|
|
37
|
+
("20", "20 — DICT_APRILTAG_36h11"),
|
|
38
|
+
("99", "99 — legacy custom 3×3"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
VALID_DICTS = {int(value) for value, _label in DICT_CHOICES}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_aruco():
|
|
45
|
+
"""Return ``(cv2, cv2.aruco)`` or raise a clear error if aruco is missing."""
|
|
46
|
+
import cv2 # noqa: PLC0415 — lazy: keeps import optional for non-detect consumers
|
|
47
|
+
try:
|
|
48
|
+
from cv2 import aruco
|
|
49
|
+
except ImportError as exc: # pragma: no cover
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"OpenCV with the ArUco module (cv2.aruco) is required — install "
|
|
52
|
+
"opencv-contrib-python(-headless)."
|
|
53
|
+
) from exc
|
|
54
|
+
return cv2, aruco
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_dictionary(dict_id, aruco=None):
|
|
58
|
+
if aruco is None:
|
|
59
|
+
_, aruco = load_aruco()
|
|
60
|
+
dict_id = int(dict_id)
|
|
61
|
+
if dict_id == 99:
|
|
62
|
+
if hasattr(aruco, "extendDictionary"):
|
|
63
|
+
return aruco.extendDictionary(32, 3)
|
|
64
|
+
return aruco.Dictionary_create(32, 3)
|
|
65
|
+
if hasattr(aruco, "getPredefinedDictionary"):
|
|
66
|
+
return aruco.getPredefinedDictionary(dict_id)
|
|
67
|
+
return aruco.Dictionary_get(dict_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def make_parameters(minrate, ignore, aruco=None):
|
|
71
|
+
if aruco is None:
|
|
72
|
+
_, aruco = load_aruco()
|
|
73
|
+
if hasattr(aruco, "DetectorParameters"):
|
|
74
|
+
params = aruco.DetectorParameters()
|
|
75
|
+
else:
|
|
76
|
+
params = aruco.DetectorParameters_create()
|
|
77
|
+
params.minMarkerPerimeterRate = float(minrate)
|
|
78
|
+
params.perspectiveRemoveIgnoredMarginPerCell = float(ignore)
|
|
79
|
+
return params
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def make_detector(dictionary, params, aruco=None):
|
|
83
|
+
"""Return ``detect(gray) -> (corners, ids, rejected)`` across OpenCV versions."""
|
|
84
|
+
if aruco is None:
|
|
85
|
+
_, aruco = load_aruco()
|
|
86
|
+
if hasattr(aruco, "ArucoDetector"):
|
|
87
|
+
detector = aruco.ArucoDetector(dictionary, params)
|
|
88
|
+
return lambda gray: detector.detectMarkers(gray)
|
|
89
|
+
return lambda gray: aruco.detectMarkers(gray, dictionary, parameters=params)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Synthetic round-trip: render an ArUco marker, then detect it back."""
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
cv2 = pytest.importorskip("cv2")
|
|
7
|
+
aruco = pytest.importorskip("cv2.aruco")
|
|
8
|
+
|
|
9
|
+
from signa_core import detect_markers, make_dictionary # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _render_marker(dict_id, marker_id, side=240, pad=80):
|
|
13
|
+
d = make_dictionary(dict_id, aruco)
|
|
14
|
+
if hasattr(aruco, "generateImageMarker"):
|
|
15
|
+
marker = aruco.generateImageMarker(d, marker_id, side)
|
|
16
|
+
else: # older OpenCV
|
|
17
|
+
marker = aruco.drawMarker(d, marker_id, side)
|
|
18
|
+
canvas = np.full((side + 2 * pad, side + 2 * pad), 255, dtype=np.uint8)
|
|
19
|
+
canvas[pad:pad + side, pad:pad + side] = marker
|
|
20
|
+
return cv2.cvtColor(canvas, cv2.COLOR_GRAY2BGR)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_detect_synthetic_marker(tmp_path):
|
|
24
|
+
img_path = tmp_path / "marker.png"
|
|
25
|
+
cv2.imwrite(str(img_path), _render_marker(dict_id=1, marker_id=7))
|
|
26
|
+
|
|
27
|
+
result = detect_markers([str(img_path)], dict_id=1, minrate=0.01, ignore=0.33)
|
|
28
|
+
dets = result["marker.png"]
|
|
29
|
+
|
|
30
|
+
assert dets, "no markers detected"
|
|
31
|
+
ids = {d["id"] for d in dets}
|
|
32
|
+
assert 7 in ids
|
|
33
|
+
det = next(d for d in dets if d["id"] == 7)
|
|
34
|
+
assert len(det["corners"]) == 4
|
|
35
|
+
cx, cy = det["center"]
|
|
36
|
+
# marker is centered on the canvas (side 240, pad 80 -> center at 200,200)
|
|
37
|
+
assert abs(cx - 200) <= 5 and abs(cy - 200) <= 5
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_unreadable_image_is_none(tmp_path):
|
|
41
|
+
bad = tmp_path / "nope.png"
|
|
42
|
+
bad.write_bytes(b"not an image")
|
|
43
|
+
result = detect_markers([str(bad)], dict_id=1)
|
|
44
|
+
assert result["nope.png"] is None
|