mirror of
https://github.com/opencv/opencv.git
synced 2026-07-26 13:53:03 +04:00
5e91b461bc
Identify ArUco markers based on threshold to reduce false positives #28289 **Goal:** parametrize the current marker identification process (pixel-based majority count) to reduce the number of false positives while maintaining high recall. Useful in high risk scenarios in which false positives are not acceptable. **Context:** This PR builds on top of https://github.com/opencv/opencv/pull/23190 in which we've introduced a pixel-based confidence in the marker detection. **Solution:** Include a new parameter: `validBitIdThreshold` used to identify markers based on the pixel count of each cell. Set the parameter default either to 50% which is equivalent to the current majority count implementation or to 49% which already singnificantly reduces the number of false positives (see details below). **Test coverage:** - Unit tests: `CV_ArucoDetectionThreshold`, `CV_InvertedArucoDetectionThreshold` - The impact of `validBitIdThreshold` on false positives was also tested using the benchmark dataset: `MIRFLICKR-25k` https://www.kaggle.com/datasets/skfrost19/mirflickr25k which contains random images without any markers. Every marker detection is a false positive. Example of images in the dataset:   **Results:** A threshold of 49% already allows to significantly reduce the number of false positives for the dict `DICT_4X4_1000`: - `5942` false positives for `validBitIdThreshold = 0.5` - `629` false positives for `validBitIdThreshold = 0.49` and `0.46` - number of false positives divided by `9.5` when compared to `validBitIdThreshold = 0.5` - `139` false positives for `validBitIdThreshold = 0.43` and `0.4` - number of false positives divided by `42` when compared to `validBitIdThreshold = 0.5` Dicts with a higher number of cells are not as impacted since it's much harder to obtain false positives. However, the less cells in a marker the further away it can be reliably detected, so the dict `DICT_4X4_1000` is commonly used. <img width="1280" height="800" alt="false_positive_image_rate" src="https://github.com/user-attachments/assets/1a0ee16a-221d-443e-835b-022ed6dea6b0" /> In the image attached, the values of `validBitIdThreshold` tested are: `0.10f, 0.20f, 0.30f, 0.40f, 0.43f, 0.46f, 0.49f, 0.50f, 0.53f, 0.56f, 0.60f, 0.70f, 0.80f, 0.90f` Summary of the results: [summary.csv](https://github.com/user-attachments/files/24315662/summary.csv) Note that we can also analyse the number of false positives per marker `id`. For example, here's the histogram for the dict `DICT_4X4_1000`. (The CSV attached contains all the results) <img width="1440" height="640" alt="false_positive_ids_DICT_4X4_1000_thr0 50" src="https://github.com/user-attachments/assets/af4f3ff8-9b8f-4682-9d51-a090c2610d8c" /> For example, the marker id 17 is detected 252 times with `validBitIdThreshold = 0.5` and only 34 times with `validBitIdThreshold = 0.49`. Looking at marker 17 (see below), we understand that this simple pattern randomly occurs in images. <img width="447" height="441" alt="Marker17" src="https://github.com/user-attachments/assets/f5d09227-b39b-4598-94f9-b529f8300703" /> Results for every dict and every `validBitIdThreshold` [per_id.csv](https://github.com/user-attachments/files/24315667/per_id.csv) **Missing coverage:** there is no labeled dataset with images containing markers to analyse the impact of on the recall (i.e. look at the true positive rate). For my specific use case (drones) any threshold above `0.4` allows to maintain a high recall in all conditions. ### Pull Request Readiness Checklist - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [ ] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake
495 lines
22 KiB
Python
495 lines
22 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Python 2/3 compatibility
|
|
from __future__ import print_function
|
|
|
|
import os, tempfile, numpy as np
|
|
from math import pi
|
|
|
|
import cv2 as cv
|
|
|
|
from tests_common import NewOpenCVTests
|
|
|
|
def getSyntheticRT(yaw, pitch, distance):
|
|
rvec = np.zeros((3, 1), np.float64)
|
|
tvec = np.zeros((3, 1), np.float64)
|
|
|
|
rotPitch = np.array([[-pitch], [0], [0]])
|
|
rotYaw = np.array([[0], [yaw], [0]])
|
|
|
|
rvec, tvec = cv.composeRT(rotPitch, np.zeros((3, 1), np.float64),
|
|
rotYaw, np.zeros((3, 1), np.float64))[:2]
|
|
|
|
tvec = np.array([[0], [0], [distance]])
|
|
return rvec, tvec
|
|
|
|
# see test_aruco_utils.cpp
|
|
def projectMarker(img, board, markerIndex, cameraMatrix, rvec, tvec, markerBorder):
|
|
markerSizePixels = 100
|
|
markerImg = cv.aruco.generateImageMarker(board.getDictionary(), board.getIds()[markerIndex], markerSizePixels, borderBits=markerBorder)
|
|
|
|
distCoeffs = np.zeros((5, 1), np.float64)
|
|
maxCoord = board.getRightBottomCorner()
|
|
objPoints = board.getObjPoints()[markerIndex]
|
|
for i in range(len(objPoints)):
|
|
objPoints[i][0] -= maxCoord[0] / 2
|
|
objPoints[i][1] -= maxCoord[1] / 2
|
|
objPoints[i][2] -= maxCoord[2] / 2
|
|
|
|
corners, _ = cv.projectPoints(objPoints, rvec, tvec, cameraMatrix, distCoeffs)
|
|
|
|
originalCorners = np.array([
|
|
[0, 0],
|
|
[markerSizePixels, 0],
|
|
[markerSizePixels, markerSizePixels],
|
|
[0, markerSizePixels],
|
|
], np.float32)
|
|
|
|
transformation = cv.getPerspectiveTransform(originalCorners, corners)
|
|
|
|
borderValue = 127
|
|
aux = cv.warpPerspective(markerImg, transformation, img.shape, None, cv.INTER_NEAREST, cv.BORDER_CONSTANT, borderValue)
|
|
|
|
assert(img.shape == aux.shape)
|
|
mask = (aux == borderValue).astype(np.uint8)
|
|
img = img * mask + aux * (1 - mask)
|
|
return img
|
|
|
|
def projectChessboard(squaresX, squaresY, squareSize, imageSize, cameraMatrix, rvec, tvec):
|
|
img = np.ones(imageSize, np.uint8) * 255
|
|
distCoeffs = np.zeros((5, 1), np.float64)
|
|
for y in range(squaresY):
|
|
startY = y * squareSize
|
|
for x in range(squaresX):
|
|
if (y % 2 != x % 2):
|
|
continue
|
|
startX = x * squareSize
|
|
|
|
squareCorners = np.array([[startX - squaresX*squareSize/2,
|
|
startY - squaresY*squareSize/2,
|
|
0]], np.float32)
|
|
squareCorners = np.stack((squareCorners[0],
|
|
squareCorners[0] + [squareSize, 0, 0],
|
|
squareCorners[0] + [squareSize, squareSize, 0],
|
|
squareCorners[0] + [0, squareSize, 0]))
|
|
|
|
projectedCorners, _ = cv.projectPoints(squareCorners, rvec, tvec, cameraMatrix, distCoeffs)
|
|
projectedCorners = projectedCorners.astype(np.int64)
|
|
projectedCorners = projectedCorners.reshape(1, 4, 2)
|
|
img = cv.fillPoly(img, [projectedCorners], 0)
|
|
|
|
return img
|
|
|
|
def projectCharucoBoard(board, cameraMatrix, yaw, pitch, distance, imageSize, markerBorder):
|
|
rvec, tvec = getSyntheticRT(yaw, pitch, distance)
|
|
|
|
img = np.ones(imageSize, np.uint8) * 255
|
|
for indexMarker in range(len(board.getIds())):
|
|
img = projectMarker(img, board, indexMarker, cameraMatrix, rvec, tvec, markerBorder)
|
|
|
|
chessboard = projectChessboard(board.getChessboardSize()[0], board.getChessboardSize()[1],
|
|
board.getSquareLength(), imageSize, cameraMatrix, rvec, tvec)
|
|
|
|
chessboard = (chessboard != 0).astype(np.uint8)
|
|
img = img * chessboard
|
|
return img, rvec, tvec
|
|
|
|
class aruco_objdetect_test(NewOpenCVTests):
|
|
|
|
def test_board(self):
|
|
p1 = np.array([[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], dtype=np.float32)
|
|
p2 = np.array([[1, 0, 0], [1, 1, 0], [2, 1, 0], [2, 0, 0]], dtype=np.float32)
|
|
objPoints = np.array([p1, p2])
|
|
dictionary = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
|
ids = np.array([0, 1])
|
|
|
|
board = cv.aruco.Board(objPoints, dictionary, ids)
|
|
np.testing.assert_array_equal(board.getIds().squeeze(), ids)
|
|
np.testing.assert_array_equal(np.ravel(np.array(board.getObjPoints())), np.ravel(np.concatenate([p1, p2])))
|
|
|
|
def test_idsAccessibility(self):
|
|
|
|
ids = np.arange(17)
|
|
rev_ids = ids[::-1]
|
|
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_250)
|
|
board = cv.aruco.CharucoBoard((7, 5), 1, 0.5, aruco_dict)
|
|
|
|
np.testing.assert_array_equal(board.getIds().squeeze(), ids)
|
|
|
|
board = cv.aruco.CharucoBoard((7, 5), 1, 0.5, aruco_dict, rev_ids)
|
|
np.testing.assert_array_equal(board.getIds().squeeze(), rev_ids)
|
|
|
|
board = cv.aruco.CharucoBoard((7, 5), 1, 0.5, aruco_dict, ids)
|
|
np.testing.assert_array_equal(board.getIds().squeeze(), ids)
|
|
|
|
def test_identify(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
|
expected_idx = 9
|
|
expected_rotation = 2
|
|
bit_marker = np.array([[0, 1, 1, 0], [1, 0, 1, 0], [1, 1, 1, 1], [0, 0, 1, 1]], dtype=np.uint8)
|
|
|
|
check, idx, rotation = aruco_dict.identify(bit_marker, 0)
|
|
|
|
self.assertTrue(check, True)
|
|
self.assertEqual(idx, expected_idx)
|
|
self.assertEqual(rotation, expected_rotation)
|
|
|
|
def test_getDistanceToId(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
|
idx = 7
|
|
rotation = 3
|
|
bit_marker = np.array([[0, 1, 0, 1], [0, 1, 1, 1], [1, 1, 0, 0], [0, 1, 0, 0]], dtype=np.uint8)
|
|
dist = aruco_dict.getDistanceToId(bit_marker, idx)
|
|
|
|
self.assertEqual(dist, 0)
|
|
|
|
def test_aruco_detector(self):
|
|
aruco_params = cv.aruco.DetectorParameters()
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
|
|
aruco_detector = cv.aruco.ArucoDetector(aruco_dict, aruco_params)
|
|
id = 2
|
|
marker_size = 100
|
|
offset = 10
|
|
img_marker = cv.aruco.generateImageMarker(aruco_dict, id, marker_size, aruco_params.markerBorderBits)
|
|
img_marker = np.pad(img_marker, pad_width=offset, mode='constant', constant_values=255)
|
|
gold_corners = np.array([[offset, offset],[marker_size+offset-1.0,offset],
|
|
[marker_size+offset-1.0,marker_size+offset-1.0],
|
|
[offset, marker_size+offset-1.0]], dtype=np.float32)
|
|
corners, ids, rejected = aruco_detector.detectMarkers(img_marker)
|
|
|
|
self.assertEqual(1, len(ids))
|
|
self.assertEqual(id, ids[0])
|
|
for i in range(0, len(corners)):
|
|
np.testing.assert_array_equal(gold_corners, corners[i].reshape(4, 2))
|
|
|
|
def test_aruco_detector_refine(self):
|
|
aruco_params = cv.aruco.DetectorParameters()
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
|
|
aruco_detector = cv.aruco.ArucoDetector(aruco_dict, aruco_params)
|
|
board_size = (3, 4)
|
|
board = cv.aruco.GridBoard(board_size, 5.0, 1.0, aruco_dict)
|
|
board_image = board.generateImage((board_size[0]*50, board_size[1]*50), marginSize=10)
|
|
|
|
corners, ids, rejected = aruco_detector.detectMarkers(board_image)
|
|
self.assertEqual(board_size[0]*board_size[1], len(ids))
|
|
|
|
part_corners, part_ids, part_rejected = corners[:-1], ids[:-1], list(rejected)
|
|
part_rejected.append(corners[-1])
|
|
|
|
refine_corners, refine_ids, refine_rejected, recovered_ids = aruco_detector.refineDetectedMarkers(board_image, board, part_corners, part_ids, part_rejected)
|
|
|
|
self.assertEqual(board_size[0] * board_size[1], len(refine_ids))
|
|
self.assertEqual(1, len(recovered_ids))
|
|
|
|
self.assertEqual(ids[-1], refine_ids[-1])
|
|
self.assertEqual((1, 4, 2), refine_corners[0].shape)
|
|
np.testing.assert_array_equal(corners, refine_corners)
|
|
|
|
def test_charuco_refine(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_50)
|
|
board_size = (3, 4)
|
|
board = cv.aruco.CharucoBoard(board_size, 1., .7, aruco_dict)
|
|
aruco_detector = cv.aruco.ArucoDetector(aruco_dict)
|
|
charuco_detector = cv.aruco.CharucoDetector(board)
|
|
cell_size = 100
|
|
image = board.generateImage((cell_size*board_size[0], cell_size*board_size[1]))
|
|
camera = np.array([[1, 0, 0.5],
|
|
[0, 1, 0.5],
|
|
[0, 0, 1]])
|
|
dist = np.array([0, 0, 0, 0, 0], dtype=np.float32).reshape(1, -1)
|
|
|
|
# generate gold corners of the ArUco markers for the test
|
|
gold_corners = np.array(board.getObjPoints())[:, :, 0:2]*cell_size
|
|
|
|
# detect corners
|
|
markerCorners, markerIds, _ = aruco_detector.detectMarkers(image)
|
|
|
|
# test refine
|
|
rejected = [markerCorners[-1]]
|
|
markerCorners, markerIds = markerCorners[:-1], markerIds[:-1]
|
|
markerCorners, markerIds, _, _ = aruco_detector.refineDetectedMarkers(image, board, markerCorners, markerIds,
|
|
rejected, cameraMatrix=camera, distCoeffs=dist)
|
|
|
|
charucoCorners, charucoIds, _, _ = charuco_detector.detectBoard(image, markerCorners=markerCorners,
|
|
markerIds=markerIds)
|
|
self.assertEqual(len(charucoIds), 6)
|
|
self.assertEqual(len(markerIds), 6)
|
|
|
|
for i, id in enumerate(markerIds.reshape(-1)):
|
|
np.testing.assert_allclose(gold_corners[id], markerCorners[i].reshape(4, 2), 0.01, 1.)
|
|
|
|
def test_write_read_dictionary(self):
|
|
try:
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_50)
|
|
markers_gold = aruco_dict.bytesList
|
|
|
|
# write aruco_dict
|
|
fd, filename = tempfile.mkstemp(prefix="opencv_python_aruco_dict_", suffix=".yml")
|
|
os.close(fd)
|
|
|
|
fs_write = cv.FileStorage(filename, cv.FileStorage_WRITE)
|
|
aruco_dict.writeDictionary(fs_write)
|
|
fs_write.release()
|
|
|
|
# reset aruco_dict
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250)
|
|
|
|
# read aruco_dict
|
|
fs_read = cv.FileStorage(filename, cv.FileStorage_READ)
|
|
aruco_dict.readDictionary(fs_read.root())
|
|
fs_read.release()
|
|
|
|
# check equal
|
|
self.assertEqual(aruco_dict.markerSize, 5)
|
|
self.assertEqual(aruco_dict.maxCorrectionBits, 3)
|
|
np.testing.assert_array_equal(aruco_dict.bytesList, markers_gold)
|
|
|
|
finally:
|
|
if os.path.exists(filename):
|
|
os.remove(filename)
|
|
|
|
def test_charuco_detector(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
|
|
board_size = (3, 3)
|
|
board = cv.aruco.CharucoBoard(board_size, 1.0, .8, aruco_dict)
|
|
charuco_detector = cv.aruco.CharucoDetector(board)
|
|
cell_size = 100
|
|
|
|
image = board.generateImage((cell_size*board_size[0], cell_size*board_size[1]))
|
|
|
|
# Note: Expected values adjusted by -0.5px after fixing the systematic offset bug in charuco_detector.cpp
|
|
# The fix removes the incorrect +0.5 offset that was added after cornerSubPix
|
|
list_gold_corners = []
|
|
for i in range(1, board_size[0]):
|
|
for j in range(1, board_size[1]):
|
|
list_gold_corners.append((j*cell_size - 0.5, i*cell_size - 0.5))
|
|
gold_corners = np.array(list_gold_corners, dtype=np.float32)
|
|
|
|
charucoCorners, charucoIds, markerCorners, markerIds = charuco_detector.detectBoard(image)
|
|
|
|
self.assertEqual(len(charucoIds), 4)
|
|
for i in range(0, 4):
|
|
self.assertEqual(charucoIds[i], i)
|
|
np.testing.assert_allclose(gold_corners, charucoCorners.reshape(-1, 2), 0.01, 0.1)
|
|
|
|
def test_detect_diamonds(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250)
|
|
board_size = (3, 3)
|
|
board = cv.aruco.CharucoBoard(board_size, 1.0, .8, aruco_dict)
|
|
charuco_detector = cv.aruco.CharucoDetector(board)
|
|
cell_size = 120
|
|
|
|
image = board.generateImage((cell_size*board_size[0], cell_size*board_size[1]))
|
|
|
|
# Note: Expected values adjusted by -0.5px after fixing the systematic offset bug in charuco_detector.cpp
|
|
# The fix removes the incorrect +0.5 offset that was added after cornerSubPix
|
|
list_gold_corners = [(cell_size - 0.5, cell_size - 0.5), (2*cell_size - 0.5, cell_size - 0.5),
|
|
(2*cell_size - 0.5, 2*cell_size - 0.5), (cell_size - 0.5, 2*cell_size - 0.5)]
|
|
gold_corners = np.array(list_gold_corners, dtype=np.float32)
|
|
|
|
diamond_corners, diamond_ids, marker_corners, marker_ids = charuco_detector.detectDiamonds(image)
|
|
|
|
self.assertEqual(diamond_ids.size, 4)
|
|
self.assertEqual(marker_ids.size, 4)
|
|
for i in range(0, 4):
|
|
self.assertEqual(diamond_ids[0][0][i], i)
|
|
np.testing.assert_allclose(gold_corners, np.array(diamond_corners, dtype=np.float32).reshape(-1, 2), 0.01, 0.1)
|
|
|
|
# check no segfault when cameraMatrix or distCoeffs are not initialized
|
|
def test_charuco_no_segfault_params(self):
|
|
dictionary = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_1000)
|
|
board = cv.aruco.CharucoBoard((10, 10), 0.019, 0.015, dictionary)
|
|
charuco_parameters = cv.aruco.CharucoParameters()
|
|
detector = cv.aruco.CharucoDetector(board)
|
|
detector.setCharucoParameters(charuco_parameters)
|
|
|
|
self.assertIsNone(detector.getCharucoParameters().cameraMatrix)
|
|
self.assertIsNone(detector.getCharucoParameters().distCoeffs)
|
|
|
|
def test_charuco_no_segfault_params_constructor(self):
|
|
dictionary = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_1000)
|
|
board = cv.aruco.CharucoBoard((10, 10), 0.019, 0.015, dictionary)
|
|
charuco_parameters = cv.aruco.CharucoParameters()
|
|
detector = cv.aruco.CharucoDetector(board, charucoParams=charuco_parameters)
|
|
|
|
self.assertIsNone(detector.getCharucoParameters().cameraMatrix)
|
|
self.assertIsNone(detector.getCharucoParameters().distCoeffs)
|
|
|
|
# similar to C++ test CV_CharucoDetection.accuracy
|
|
def test_charuco_detector_accuracy(self):
|
|
iteration = 0
|
|
cameraMatrix = np.eye(3, 3, dtype=np.float64)
|
|
imgSize = (500, 500)
|
|
params = cv.aruco.DetectorParameters()
|
|
params.minDistanceToBorder = 3
|
|
params.validBitIdThreshold = 0.5
|
|
|
|
board = cv.aruco.CharucoBoard((4, 4), 0.03, 0.015, cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250))
|
|
detector = cv.aruco.CharucoDetector(board, detectorParams=params)
|
|
|
|
cameraMatrix[0, 0] = cameraMatrix[1, 1] = 600
|
|
cameraMatrix[0, 2] = imgSize[0] / 2
|
|
cameraMatrix[1, 2] = imgSize[1] / 2
|
|
|
|
# for different perspectives
|
|
distCoeffs = np.zeros((5, 1), dtype=np.float64)
|
|
for distance in [0.2, 0.4]:
|
|
for yaw in range(-55, 51, 25):
|
|
for pitch in range(-55, 51, 25):
|
|
markerBorder = iteration % 2 + 1
|
|
iteration += 1
|
|
|
|
# create synthetic image
|
|
img, rvec, tvec = projectCharucoBoard(board, cameraMatrix, yaw * pi / 180, pitch * pi / 180, distance, imgSize, markerBorder)
|
|
|
|
params.markerBorderBits = markerBorder
|
|
detector.setDetectorParameters(params)
|
|
|
|
if (iteration % 2 != 0):
|
|
charucoParameters = cv.aruco.CharucoParameters()
|
|
charucoParameters.cameraMatrix = cameraMatrix
|
|
charucoParameters.distCoeffs = distCoeffs
|
|
detector.setCharucoParameters(charucoParameters)
|
|
|
|
charucoCorners, charucoIds, corners, ids = detector.detectBoard(img)
|
|
|
|
self.assertGreater(len(ids), 0)
|
|
|
|
copyChessboardCorners = board.getChessboardCorners()
|
|
copyChessboardCorners -= np.array(board.getRightBottomCorner()) / 2
|
|
|
|
projectedCharucoCorners, _ = cv.projectPoints(copyChessboardCorners, rvec, tvec, cameraMatrix, distCoeffs)
|
|
|
|
if charucoIds is None:
|
|
# Detection can fail at extreme viewing angles
|
|
self.assertTrue(abs(yaw) >= 45 or abs(pitch) >= 45,
|
|
f"Detection failed unexpectedly at yaw={yaw}, pitch={pitch}")
|
|
continue
|
|
|
|
for i in range(len(charucoIds)):
|
|
currentId = charucoIds[i]
|
|
self.assertLess(currentId, len(board.getChessboardCorners()))
|
|
|
|
reprErr = cv.norm(charucoCorners[i] - projectedCharucoCorners[currentId])
|
|
self.assertLessEqual(reprErr, 5)
|
|
|
|
def test_aruco_match_image_points(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
|
board_size = (3, 4)
|
|
board = cv.aruco.GridBoard(board_size, 5.0, 1.0, aruco_dict)
|
|
aruco_corners = np.array(board.getObjPoints())[:, :, :2]
|
|
aruco_ids = board.getIds()
|
|
obj_points, img_points = board.matchImagePoints(aruco_corners, aruco_ids)
|
|
aruco_corners = aruco_corners.reshape(-1, 2)
|
|
|
|
self.assertEqual(aruco_corners.shape[0], obj_points.shape[0])
|
|
self.assertEqual(img_points.shape[0], obj_points.shape[0])
|
|
self.assertEqual(2, img_points.shape[2])
|
|
np.testing.assert_array_equal(aruco_corners, obj_points[:, :, :2].reshape(-1, 2))
|
|
|
|
def test_charuco_match_image_points(self):
|
|
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
|
board_size = (3, 4)
|
|
board = cv.aruco.CharucoBoard(board_size, 5.0, 1.0, aruco_dict)
|
|
chessboard_corners = np.array(board.getChessboardCorners())[:, :2]
|
|
chessboard_ids = board.getIds()
|
|
obj_points, img_points = board.matchImagePoints(chessboard_corners, chessboard_ids)
|
|
|
|
self.assertEqual(chessboard_corners.shape[0], obj_points.shape[0])
|
|
self.assertEqual(img_points.shape[0], obj_points.shape[0])
|
|
self.assertEqual(2, img_points.shape[2])
|
|
np.testing.assert_array_equal(chessboard_corners, obj_points[:, :, :2].reshape(-1, 2))
|
|
|
|
def test_draw_detected_markers(self):
|
|
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
|
img = np.zeros((60, 60), dtype=np.uint8)
|
|
|
|
# add extra dimension in Python to create Nx4 Mat with 2 channels
|
|
points1 = np.array(detected_points).reshape(-1, 4, 1, 2)
|
|
img = cv.aruco.drawDetectedMarkers(img, points1, borderColor=255)
|
|
|
|
# check that the marker borders are painted
|
|
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
|
self.assertEqual(len(contours), 1)
|
|
self.assertEqual(img[10, 10], 255)
|
|
self.assertEqual(img[50, 10], 255)
|
|
self.assertEqual(img[50, 50], 255)
|
|
self.assertEqual(img[10, 50], 255)
|
|
|
|
# must throw Exception without extra dimension
|
|
points2 = np.array(detected_points)
|
|
with self.assertRaises(Exception):
|
|
img = cv.aruco.drawDetectedMarkers(img, points2, borderColor=255)
|
|
|
|
def test_draw_detected_charuco(self):
|
|
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
|
img = np.zeros((60, 60), dtype=np.uint8)
|
|
|
|
# add extra dimension in Python to create Nx1 Mat with 2 channels
|
|
points = np.array(detected_points).reshape(-1, 1, 2)
|
|
img = cv.aruco.drawDetectedCornersCharuco(img, points, cornerColor=255)
|
|
|
|
# check that the 4 charuco corners are painted
|
|
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
|
self.assertEqual(len(contours), 4)
|
|
for contour in contours:
|
|
center_x = round(np.average(contour[:, 0, 0]))
|
|
center_y = round(np.average(contour[:, 0, 1]))
|
|
center = [center_x, center_y]
|
|
self.assertTrue(center in detected_points[0])
|
|
|
|
# must throw Exception without extra dimension
|
|
points2 = np.array(detected_points)
|
|
with self.assertRaises(Exception):
|
|
img = cv.aruco.drawDetectedCornersCharuco(img, points2, borderColor=255)
|
|
|
|
def test_draw_detected_diamonds(self):
|
|
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
|
img = np.zeros((60, 60), dtype=np.uint8)
|
|
|
|
# add extra dimension in Python to create Nx4 Mat with 2 channels
|
|
points = np.array(detected_points).reshape(-1, 4, 1, 2)
|
|
img = cv.aruco.drawDetectedDiamonds(img, points, borderColor=255)
|
|
|
|
# check that the diamonds borders are painted
|
|
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
|
self.assertEqual(len(contours), 1)
|
|
self.assertEqual(img[10, 10], 255)
|
|
self.assertEqual(img[50, 10], 255)
|
|
self.assertEqual(img[50, 50], 255)
|
|
self.assertEqual(img[10, 50], 255)
|
|
|
|
# must throw Exception without extra dimension
|
|
points2 = np.array(detected_points)
|
|
with self.assertRaises(Exception):
|
|
img = cv.aruco.drawDetectedDiamonds(img, points2, borderColor=255)
|
|
|
|
def test_multi_dict_arucodetector(self):
|
|
aruco_params = cv.aruco.DetectorParameters()
|
|
aruco_dicts = [
|
|
cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250),
|
|
cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_250)
|
|
]
|
|
aruco_detector = cv.aruco.ArucoDetector(aruco_dicts, aruco_params)
|
|
id = 2
|
|
marker_size = 100
|
|
offset = 10
|
|
img_marker1 = cv.aruco.generateImageMarker(aruco_dicts[0], id, marker_size, aruco_params.markerBorderBits)
|
|
img_marker1 = np.pad(img_marker1, pad_width=offset, mode='constant', constant_values=255)
|
|
img_marker2 = cv.aruco.generateImageMarker(aruco_dicts[1], id, marker_size, aruco_params.markerBorderBits)
|
|
img_marker2 = np.pad(img_marker2, pad_width=offset, mode='constant', constant_values=255)
|
|
img_markers = np.concatenate((img_marker1, img_marker2), axis=1)
|
|
|
|
corners, ids, rejected, dictIndices = aruco_detector.detectMarkersMultiDict(img_markers)
|
|
|
|
self.assertEqual(2, len(ids))
|
|
self.assertEqual(id, ids[0])
|
|
self.assertEqual(id, ids[1])
|
|
self.assertEqual(2, len(dictIndices))
|
|
self.assertEqual(0, dictIndices[0])
|
|
self.assertEqual(1, dictIndices[1])
|
|
|
|
if __name__ == '__main__':
|
|
NewOpenCVTests.bootstrap()
|