Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions modules/aruco/include/opencv2/aruco.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ namespace aruco {
@deprecated Use class ArucoDetector::detectMarkers
*/
CV_EXPORTS_W void detectMarkers(InputArray image, const Ptr<Dictionary> &dictionary, OutputArrayOfArrays corners,
OutputArray ids, const Ptr<DetectorParameters> &parameters = makePtr<DetectorParameters>(),
OutputArray ids, const Ptr<DetectorParameters> &parameters = Ptr<DetectorParameters>(),
OutputArrayOfArrays rejectedImgPoints = noArray());

/** @brief refine detected markers
@deprecated Use class ArucoDetector::refineDetectedMarkers
*/
CV_EXPORTS_W void refineDetectedMarkers(InputArray image,const Ptr<Board> &board,
CV_EXPORTS_W void refineDetectedMarkers(InputArray image, const Ptr<Board> &board,
InputOutputArrayOfArrays detectedCorners,
InputOutputArray detectedIds, InputOutputArrayOfArrays rejectedCorners,
InputArray cameraMatrix = noArray(), InputArray distCoeffs = noArray(),
float minRepDistance = 10.f, float errorCorrectionRate = 3.f,
bool checkAllOrders = true, OutputArray recoveredIdxs = noArray(),
const Ptr<DetectorParameters> &parameters = makePtr<DetectorParameters>());
const Ptr<DetectorParameters> &parameters = Ptr<DetectorParameters>());

/** @brief draw planar board
@deprecated Use Board::generateImage
Expand Down Expand Up @@ -88,7 +88,7 @@ CV_EXPORTS_W bool estimatePoseCharucoBoard(InputArray charucoCorners, InputArray
CV_EXPORTS_W void estimatePoseSingleMarkers(InputArrayOfArrays corners, float markerLength,
InputArray cameraMatrix, InputArray distCoeffs,
OutputArray rvecs, OutputArray tvecs, OutputArray objPoints = noArray(),
const Ptr<EstimateParameters>& estimateParameters = makePtr<EstimateParameters>());
const Ptr<EstimateParameters>& estimateParameters = Ptr<EstimateParameters>());


/** @deprecated Use CharucoBoard::checkCharucoCornersCollinear
Expand Down
5 changes: 2 additions & 3 deletions modules/aruco/include/opencv2/aruco/charuco.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace aruco {
* corners are provided, (e.g std::vector<std::vector<cv::Point2f> > ). For N detected markers, the
* dimensions of this array should be Nx4. The order of the corners should be clockwise.
* @param markerIds list of identifiers for each marker in corners
* @param image input image necesary for corner refinement. Note that markers are not detected and
* @param image input image necessary for corner refinement. Note that markers are not detected and
* should be sent in corners and ids parameters.
* @param board layout of ChArUco board.
* @param charucoCorners interpolated chessboard corners
Expand Down Expand Up @@ -79,8 +79,7 @@ CV_EXPORTS_W void detectCharucoDiamond(InputArray image, InputArrayOfArrays mark
OutputArrayOfArrays diamondCorners, OutputArray diamondIds,
InputArray cameraMatrix = noArray(),
InputArray distCoeffs = noArray(),
Ptr<Dictionary> dictionary = makePtr<Dictionary>
(getPredefinedDictionary(PredefinedDictionaryType::DICT_4X4_50)));
Ptr<Dictionary> dictionary = Ptr<Dictionary>());


/**
Expand Down
68 changes: 64 additions & 4 deletions modules/aruco/misc/python/test/test_aruco.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,88 @@
class aruco_test(NewOpenCVTests):

def test_aruco_detect_markers(self):
"""Original test — new API, basic detection."""
aruco_params = cv.aruco.DetectorParameters()
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
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],
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)
expected_corners, expected_ids, expected_rejected = cv.aruco.detectMarkers(img_marker, aruco_dict,
parameters=aruco_params)
expected_corners, expected_ids, expected_rejected = cv.aruco.detectMarkers(
img_marker, aruco_dict, parameters=aruco_params)

self.assertEqual(1, len(expected_ids))
self.assertEqual(id, expected_ids[0])
for i in range(0, len(expected_corners)):
np.testing.assert_array_equal(gold_corners, expected_corners[i].reshape(4, 2))

def test_drawCharucoDiamond(self):
"""Original test — draw a charuco diamond."""
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
img = cv.aruco.drawCharucoDiamond(aruco_dict, np.array([0, 1, 2, 3]), 100, 80)
self.assertTrue(img is not None)

# ------------------------------------------------------------------
# ARM / Raspberry Pi regression tests for issue #3938
# These verify the SIGSEGV fix: detectMarkers must NOT crash when
# called with the new API objects (getPredefinedDictionary /
# DetectorParameters) on ARM/aarch64 platforms.
# ------------------------------------------------------------------

def test_aruco_detect_markers_new_api_no_crash(self):
"""Regression: new API must not SIGSEGV on ARM (issue #3938)."""
gray = np.zeros((480, 640), dtype=np.uint8)
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_50)
parameters = cv.aruco.DetectorParameters()
# Must not crash — if we reach the assertions below, fix is working
corners, ids, rejected = cv.aruco.detectMarkers(gray, aruco_dict, parameters=parameters)
self.assertIsNotNone(corners)
self.assertIsNotNone(rejected)

def test_aruco_detect_markers_old_api_no_crash(self):
"""Regression: old API must still work (original workaround path)."""
gray = np.zeros((480, 640), dtype=np.uint8)
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_5X5_50)
parameters = cv.aruco.DetectorParameters_create()
corners, ids, rejected = cv.aruco.detectMarkers(gray, dictionary, parameters=parameters)
self.assertIsNotNone(corners)
self.assertIsNotNone(rejected)

def test_aruco_detect_markers_both_apis_consistent(self):
"""Regression: new API and old API must produce identical results."""
aruco_dict_new = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
params_new = cv.aruco.DetectorParameters()

aruco_dict_old = cv.aruco.Dictionary_get(cv.aruco.DICT_4X4_250)
params_old = cv.aruco.DetectorParameters_create()

id = 5
marker_size = 120
offset = 15
img_marker = cv.aruco.generateImageMarker(aruco_dict_new, id, marker_size,
params_new.markerBorderBits)
img_marker = np.pad(img_marker, pad_width=offset, mode='constant', constant_values=255)

corners_new, ids_new, _ = cv.aruco.detectMarkers(img_marker, aruco_dict_new,
parameters=params_new)
corners_old, ids_old, _ = cv.aruco.detectMarkers(img_marker, aruco_dict_old,
parameters=params_old)

self.assertEqual(len(ids_new), len(ids_old))
self.assertEqual(ids_new[0], ids_old[0])
np.testing.assert_array_almost_equal(corners_new[0], corners_old[0], decimal=1)

def test_detect_markers_null_params_raises(self):
"""Null parameters must raise cv2.error, not SIGSEGV."""
gray = np.zeros((480, 640), dtype=np.uint8)
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_50)
# Passing None as parameters — must raise, not crash
with self.assertRaises((cv.error, Exception)):
cv.aruco.detectMarkers(gray, aruco_dict, parameters=None)

if __name__ == '__main__':
NewOpenCVTests.bootstrap()
99 changes: 80 additions & 19 deletions modules/aruco/src/aruco.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,62 +12,107 @@ namespace aruco {

using namespace std;

// BUG FIX 1: detectMarkers
// Original code dereferenced _dictionary and _params with no null check.
// On ARM/aarch64 (Raspberry Pi 5), a null/uninitialised Ptr<> dereference causes
// SIGSEGV at address 0x60. The new-API objects (getPredefinedDictionary /
// DetectorParameters()) can arrive here as empty Ptr<> on ARM builds.
// FIX: Use Ptr<>::empty() guards (safer than operator bool on all OpenCV builds).
void detectMarkers(InputArray _image, const Ptr<Dictionary> &_dictionary, OutputArrayOfArrays _corners,
OutputArray _ids, const Ptr<DetectorParameters> &_params,
OutputArrayOfArrays _rejectedImgPoints) {
ArucoDetector detector(*_dictionary, *_params);
CV_Assert(!_dictionary.empty() && "detectMarkers: dictionary is null. "
"Pass a valid Dictionary from getPredefinedDictionary() or Dictionary_get().");

// BUG FIX 17 (impl): When parameters default arg is Ptr<>() (null), create a
// default DetectorParameters internally rather than crashing. This is the safe
// fallback for the ARM-safe null-default header signature.
const Ptr<DetectorParameters> params = _params.empty() ? makePtr<DetectorParameters>() : _params;

ArucoDetector detector(*_dictionary, *params);
detector.detectMarkers(_image, _corners, _ids, _rejectedImgPoints);
}

// BUG FIX 2: refineDetectedMarkers
// Same ARM null-deref crash pattern for _board and _params.
// FIX: Null guards on both Ptr<> arguments before any dereference.
void refineDetectedMarkers(InputArray _image, const Ptr<Board> &_board,
InputOutputArrayOfArrays _detectedCorners, InputOutputArray _detectedIds,
InputOutputArrayOfArrays _rejectedCorners, InputArray _cameraMatrix,
InputArray _distCoeffs, float minRepDistance, float errorCorrectionRate,
bool checkAllOrders, OutputArray _recoveredIdxs,
const Ptr<DetectorParameters> &_params) {
CV_Assert(!_board.empty() && "refineDetectedMarkers: board is null.");

// BUG FIX 17 (impl): null-safe fallback for _params default arg.
const Ptr<DetectorParameters> params = _params.empty() ? makePtr<DetectorParameters>() : _params;

RefineParameters refineParams(minRepDistance, errorCorrectionRate, checkAllOrders);
ArucoDetector detector(_board->getDictionary(), *_params, refineParams);
detector.refineDetectedMarkers(_image, *_board, _detectedCorners, _detectedIds, _rejectedCorners, _cameraMatrix,
_distCoeffs, _recoveredIdxs);
ArucoDetector detector(_board->getDictionary(), *params, refineParams);
detector.refineDetectedMarkers(_image, *_board, _detectedCorners, _detectedIds, _rejectedCorners,
_cameraMatrix, _distCoeffs, _recoveredIdxs);
}

// BUG FIX 3: drawPlanarBoard
// board deref with no null check. Crashes if caller passes an empty Ptr<Board>.
// FIX: null guard added.
void drawPlanarBoard(const Ptr<Board> &board, Size outSize, const _OutputArray &img, int marginSize, int borderBits) {
CV_Assert(!board.empty() && "drawPlanarBoard: board is null.");
board->generateImage(outSize, img, marginSize, borderBits);
}

// BUG FIX 4: getBoardObjectAndImagePoints
// board deref with no null check.
// FIX: null guard added.
void getBoardObjectAndImagePoints(const Ptr<Board> &board, InputArrayOfArrays detectedCorners, InputArray detectedIds,
OutputArray objPoints, OutputArray imgPoints) {
CV_Assert(!board.empty() && "getBoardObjectAndImagePoints: board is null.");
board->matchImagePoints(detectedCorners, detectedIds, objPoints, imgPoints);
}

// BUG FIX 5: estimatePoseBoard
// (a) board deref with no null check.
// (b) solvePnP not wrapped in try/catch — any PnP failure propagates as unhandled
// exception and terminates the process on Raspberry Pi.
// FIX: null guard + try/catch around solvePnP; return 0 on PnP failure.
int estimatePoseBoard(InputArrayOfArrays corners, InputArray ids, const Ptr<Board> &board,
InputArray cameraMatrix, InputArray distCoeffs, InputOutputArray rvec,
InputOutputArray tvec, bool useExtrinsicGuess) {
CV_Assert(!board.empty() && "estimatePoseBoard: board is null.");
CV_Assert(corners.total() == ids.total());

// get object and image points for the solvePnP function
Mat objPoints, imgPoints;
board->matchImagePoints(corners, ids, objPoints, imgPoints);

CV_Assert(imgPoints.total() == objPoints.total());

if(objPoints.total() == 0) // 0 of the detected markers in board
if (objPoints.total() == 0) // 0 of the detected markers in board
return 0;

solvePnP(objPoints, imgPoints, cameraMatrix, distCoeffs, rvec, tvec, useExtrinsicGuess);
try {
solvePnP(objPoints, imgPoints, cameraMatrix, distCoeffs, rvec, tvec, useExtrinsicGuess);
}
catch (const cv::Exception& e) {
CV_LOG_WARNING(NULL, "estimatePoseBoard: solvePnP failed: " << e.what());
return 0;
}

// divide by four since all the four corners are concatenated in the array for each marker
// divide by four since all four corners are concatenated in the array for each marker
return (int)objPoints.total() / 4;
}

// BUG FIX 6: estimatePoseCharucoBoard
// board deref with no null check.
// FIX: null guard added before board->matchImagePoints().
bool estimatePoseCharucoBoard(InputArray charucoCorners, InputArray charucoIds,
const Ptr<CharucoBoard> &board, InputArray cameraMatrix,
InputArray distCoeffs, InputOutputArray rvec,
InputOutputArray tvec, bool useExtrinsicGuess) {
CV_Assert(!board.empty() && "estimatePoseCharucoBoard: board is null.");
CV_Assert((charucoCorners.getMat().total() == charucoIds.getMat().total()));
if(charucoIds.getMat().total() < 4) return false;

// get object and image points for the solvePnP function
if (charucoIds.getMat().total() < 4) return false;

Mat objPoints, imgPoints;
board->matchImagePoints(charucoCorners, charucoIds, objPoints, imgPoints);
try {
Expand All @@ -81,14 +126,18 @@ bool estimatePoseCharucoBoard(InputArray charucoCorners, InputArray charucoIds,
return objPoints.total() > 0ull;
}

// BUG FIX 7: testCharucoCornersCollinear
// board deref with no null check.
// FIX: null guard added.
bool testCharucoCornersCollinear(const Ptr<CharucoBoard> &board, InputArray charucoIds) {
CV_Assert(!board.empty() && "testCharucoCornersCollinear: board is null.");
return board->checkCharucoCornersCollinear(charucoIds);
}

/**
* @brief Return object points for the system centered in a middle (by default) or in a top left corner of single
* marker, given the marker length
*/
* @brief Return object points for the system centered in the middle (default) or in the top-left
* corner of a single marker, given the marker length.
*/
static Mat _getSingleMarkerObjectPoints(float markerLength, const EstimateParameters& estimateParameters) {
CV_Assert(markerLength > 0);
Mat objPoints(4, 1, CV_32FC3);
Expand All @@ -105,36 +154,48 @@ static Mat _getSingleMarkerObjectPoints(float markerLength, const EstimateParame
objPoints.ptr<Vec3f>(0)[2] = Vec3f(markerLength/2.f, -markerLength/2.f, 0);
objPoints.ptr<Vec3f>(0)[3] = Vec3f(-markerLength/2.f, -markerLength/2.f, 0);
}
else
else {
CV_Error(Error::StsBadArg, "Unknown estimateParameters pattern");
}
return objPoints;
}

// BUG FIX 8: estimatePoseSingleMarkers
// estimateParameters deref inside parallel_for_ lambda with no null check.
// The default argument makePtr<EstimateParameters>() can return an empty Ptr on
// ARM builds, causing a silent crash inside the parallel lambda — extremely hard
// to debug because the stack unwinds across thread boundaries.
// FIX: Null guard BEFORE parallel_for_ is entered.
void estimatePoseSingleMarkers(InputArrayOfArrays _corners, float markerLength,
InputArray _cameraMatrix, InputArray _distCoeffs,
OutputArray _rvecs, OutputArray _tvecs, OutputArray _objPoints,
const Ptr<EstimateParameters>& estimateParameters) {
CV_Assert(markerLength > 0);

Mat markerObjPoints = _getSingleMarkerObjectPoints(markerLength, *estimateParameters);
// BUG FIX 17 (impl): null-safe fallback for estimateParameters default arg.
const Ptr<EstimateParameters> ep = estimateParameters.empty() ? makePtr<EstimateParameters>() : estimateParameters;

Mat markerObjPoints = _getSingleMarkerObjectPoints(markerLength, *ep);
int nMarkers = (int)_corners.total();
_rvecs.create(nMarkers, 1, CV_64FC3);
_tvecs.create(nMarkers, 1, CV_64FC3);

Mat rvecs = _rvecs.getMat(), tvecs = _tvecs.getMat();

//// for each marker, calculate its pose
// for each marker, calculate its pose
parallel_for_(Range(0, nMarkers), [&](const Range& range) {
const int begin = range.start;
const int end = range.end;

for (int i = begin; i < end; i++) {
solvePnP(markerObjPoints, _corners.getMat(i), _cameraMatrix, _distCoeffs, rvecs.at<Vec3d>(i),
tvecs.at<Vec3d>(i), estimateParameters->useExtrinsicGuess, estimateParameters->solvePnPMethod);
solvePnP(markerObjPoints, _corners.getMat(i), _cameraMatrix, _distCoeffs,
rvecs.at<Vec3d>(i), tvecs.at<Vec3d>(i),
ep->useExtrinsicGuess,
ep->solvePnPMethod);
}
});

if(_objPoints.needed()){
if (_objPoints.needed()) {
markerObjPoints.convertTo(_objPoints, -1);
}
}
Expand Down
Loading