Skip to content

Commit 31b29af

Browse files
Merge pull request #417 from chibai/master
add python wrap for estimate_tag_pose
2 parents 4cdd4fc + 7dc8384 commit 31b29af

File tree

7 files changed

+576
-3
lines changed

7 files changed

+576
-3
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ if(BUILD_PYTHON_WRAPPER AND Python3_Development_FOUND AND Python3_NumPy_FOUND)
173173

174174
include(CMake/vtkEncodeString.cmake)
175175

176-
foreach(X IN ITEMS detect py_type)
176+
foreach(X IN ITEMS detect py_type estimate_tag_pose)
177177
vtk_encode_string(
178178
INPUT ${CMAKE_CURRENT_SOURCE_DIR}/apriltag_${X}.docstring
179179
NAME apriltag_${X}_docstring
@@ -182,6 +182,7 @@ if(BUILD_PYTHON_WRAPPER AND Python3_Development_FOUND AND Python3_NumPy_FOUND)
182182
add_custom_target(apriltag_py_docstrings DEPENDS
183183
${PROJECT_BINARY_DIR}/apriltag_detect_docstring.h
184184
${PROJECT_BINARY_DIR}/apriltag_py_type_docstring.h
185+
${PROJECT_BINARY_DIR}/apriltag_estimate_tag_pose_docstring.h
185186
)
186187

187188
# set the SOABI manually since renaming the library via OUTPUT_NAME does not work on MSVC

apriltag_detect.docstring

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ a tuple containing the detections. Each detection is a dict with keys:
6060
of detection accuracy only for very small tags-- not effective for larger tags
6161
(where we could have sampled anywhere within a bit cell and still gotten a
6262
good detection.)
63+
64+
- homography: A 3x3 homography matrix that describes the projection from an
65+
"ideal" tag (with corners at (-1,1), (1,1), (1,-1), and (-1,-1)) to pixels
66+
in the image. This matrix can be used to map points from the tag's coordinate
67+
system to the image coordinate system, and is useful for pose estimation.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
estimate_tag_pose(detection, tagsize, fx, fy, cx, cy) -> dict
2+
3+
SYNOPSIS
4+
5+
import cv2
6+
import numpy as np
7+
from apriltag import apriltag
8+
9+
imagepath = '/tmp/tst.jpg'
10+
image = cv2.imread(imagepath, cv2.IMREAD_GRAYSCALE)
11+
detector = apriltag("tag36h11")
12+
13+
detections = detector.detect(image)
14+
if detections:
15+
# Estimate pose for the first detected tag
16+
# tagsize is the physical size of the tag in meters
17+
# fx, fy are focal lengths in pixels
18+
# cx, cy are principal point coordinates in pixels
19+
pose = detector.estimate_tag_pose(detections[0],
20+
tagsize=0.16, # 16cm tag
21+
fx=600, fy=600, # focal lengths
22+
cx=320, cy=240) # principal point
23+
print("Rotation matrix R:\n", pose['R'])
24+
print("Translation vector t:", pose['t'])
25+
print("Reprojection error:", pose['error'])
26+
27+
DESCRIPTION
28+
29+
The estimate_tag_pose() method estimates the 6-DOF pose (position and orientation)
30+
of a detected AprilTag in 3D space. This method requires the detection result from
31+
the detect() method, the physical size of the tag, and camera intrinsic parameters.
32+
33+
The pose estimation uses the homography matrix from the detection result to
34+
compute the transformation from the tag's coordinate system to the camera's
35+
coordinate system.
36+
37+
ARGUMENTS
38+
39+
- detection: A dictionary containing detection information returned by the
40+
detect() method. This dictionary must include the 'homography' key with the
41+
3x3 homography matrix.
42+
43+
- tagsize: The physical side length of the AprilTag in meters. This is the real-
44+
world size of the tag, which is necessary for computing the scale of the pose.
45+
46+
- fx: Focal length in the x direction in pixels. This is a camera intrinsic
47+
parameter that describes how the camera projects 3D points to 2D image space.
48+
49+
- fy: Focal length in the y direction in pixels. This is a camera intrinsic
50+
parameter that describes how the camera projects 3D points to 2D image space.
51+
52+
- cx: Principal point x coordinate in pixels. This is the x coordinate of the
53+
optical center of the camera in the image.
54+
55+
- cy: Principal point y coordinate in pixels. This is the y coordinate of the
56+
optical center of the camera in the image.
57+
58+
RETURNED VALUE
59+
60+
Returns a dictionary containing:
61+
62+
- 'R': 3x3 rotation matrix as a numpy array that represents the orientation
63+
of the tag in the camera coordinate system.
64+
65+
- 't': 3x1 translation vector as a numpy array (in meters) that represents the
66+
position of the tag in the camera coordinate system.
67+
68+
- 'error': The object-space error after the iteration process, representing
69+
the sum of squared reprojection errors between observed and estimated points
70+
in object space. A lower value indicates a better pose estimate.

apriltag_pywrap.c

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <signal.h>
1111

1212
#include "apriltag.h"
13+
#include "apriltag_pose.h"
1314
#include "tag36h10.h"
1415
#include "tag36h11.h"
1516
#include "tag25h9.h"
@@ -227,6 +228,7 @@ static PyObject* apriltag_detect(apriltag_py_t* self,
227228
PyObject* result = NULL;
228229
PyArrayObject* xy_c = NULL;
229230
PyArrayObject* xy_lb_rb_rt_lt = NULL;
231+
PyArrayObject* homography = NULL;
230232
PyArrayObject* image = NULL;
231233
PyObject* detections_tuple = NULL;
232234

@@ -311,13 +313,32 @@ static PyObject* apriltag_detect(apriltag_py_t* self,
311313
*(double*)PyArray_GETPTR2(xy_lb_rb_rt_lt, j, 1) = det->p[j][1];
312314
}
313315

316+
// Add homography matrix (3x3)
317+
homography = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3,3}), NPY_FLOAT64);
318+
if(homography == NULL)
319+
{
320+
Py_DECREF(xy_c);
321+
Py_DECREF(xy_lb_rb_rt_lt);
322+
PyErr_SetString(PyExc_RuntimeError, "Could not allocate homography array");
323+
goto done;
324+
}
325+
326+
for(int j=0; j<3; j++)
327+
{
328+
for(int k=0; k<3; k++)
329+
{
330+
*(double*)PyArray_GETPTR2(homography, j, k) = MATD_EL(det->H, j, k);
331+
}
332+
}
333+
314334
PyTuple_SET_ITEM(detections_tuple, i,
315-
Py_BuildValue("{s:i,s:f,s:i,s:N,s:N}",
335+
Py_BuildValue("{s:i,s:f,s:i,s:N,s:N,s:N}",
316336
"hamming", det->hamming,
317337
"margin", det->decision_margin,
318338
"id", det->id,
319339
"center", xy_c,
320-
"lb-rb-rt-lt", xy_lb_rb_rt_lt));
340+
"lb-rb-rt-lt", xy_lb_rb_rt_lt,
341+
"homography", homography));
321342
xy_c = NULL;
322343
xy_lb_rb_rt_lt = NULL;
323344
}
@@ -338,12 +359,148 @@ static PyObject* apriltag_detect(apriltag_py_t* self,
338359
return result;
339360
}
340361

362+
static PyObject* apriltag_estimate_tag_pose(apriltag_py_t* self,
363+
PyObject* args)
364+
{
365+
PyObject* result = NULL;
366+
PyObject* detection_dict = NULL;
367+
PyArrayObject* R_array = NULL;
368+
PyArrayObject* t_array = NULL;
369+
matd_t* H_matrix = NULL;
370+
double tagsize, fx, fy, cx, cy;
371+
372+
if(!PyArg_ParseTuple(args, "Oddddd",
373+
&detection_dict,
374+
&tagsize,
375+
&fx, &fy, &cx, &cy))
376+
return NULL;
377+
378+
if(!PyDict_Check(detection_dict))
379+
{
380+
PyErr_SetString(PyExc_TypeError, "First argument must be a detection dictionary");
381+
return NULL;
382+
}
383+
384+
// Extract detection information from the dictionary
385+
PyObject* py_id = PyDict_GetItemString(detection_dict, "id");
386+
PyObject* py_hamming = PyDict_GetItemString(detection_dict, "hamming");
387+
PyObject* py_margin = PyDict_GetItemString(detection_dict, "margin");
388+
PyObject* py_center = PyDict_GetItemString(detection_dict, "center");
389+
PyObject* py_corners = PyDict_GetItemString(detection_dict, "lb-rb-rt-lt");
390+
PyObject* py_homography = PyDict_GetItemString(detection_dict, "homography");
391+
392+
if(!py_id || !py_hamming || !py_margin || !py_center || !py_corners || !py_homography)
393+
{
394+
PyErr_SetString(PyExc_ValueError,
395+
"Detection dictionary is missing required fields. "
396+
"Make sure you're using a detection from the updated detect() method that includes 'homography'.");
397+
return NULL;
398+
}
399+
400+
// Create a temporary detection structure
401+
apriltag_detection_t det;
402+
det.family = self->tf;
403+
det.id = PyLong_AsLong(py_id);
404+
det.hamming = PyLong_AsLong(py_hamming);
405+
det.decision_margin = PyFloat_AsDouble(py_margin);
406+
407+
// Extract center
408+
PyArrayObject* center_array = (PyArrayObject*)py_center;
409+
det.c[0] = *(double*)PyArray_GETPTR1(center_array, 0);
410+
det.c[1] = *(double*)PyArray_GETPTR1(center_array, 1);
411+
412+
// Extract corners
413+
PyArrayObject* corners_array = (PyArrayObject*)py_corners;
414+
for(int i = 0; i < 4; i++)
415+
{
416+
det.p[i][0] = *(double*)PyArray_GETPTR2(corners_array, i, 0);
417+
det.p[i][1] = *(double*)PyArray_GETPTR2(corners_array, i, 1);
418+
}
419+
420+
// Extract and copy homography matrix
421+
PyArrayObject* homography_array = (PyArrayObject*)py_homography;
422+
H_matrix = matd_create(3, 3);
423+
if(!H_matrix)
424+
{
425+
PyErr_SetString(PyExc_RuntimeError, "Could not allocate homography matrix");
426+
return NULL;
427+
}
428+
429+
for(int i = 0; i < 3; i++)
430+
{
431+
for(int j = 0; j < 3; j++)
432+
{
433+
MATD_EL(H_matrix, i, j) = *(double*)PyArray_GETPTR2(homography_array, i, j);
434+
}
435+
}
436+
det.H = H_matrix;
437+
438+
// Setup detection info
439+
apriltag_detection_info_t info;
440+
info.det = &det;
441+
info.tagsize = tagsize;
442+
info.fx = fx;
443+
info.fy = fy;
444+
info.cx = cx;
445+
info.cy = cy;
446+
447+
// Estimate pose
448+
apriltag_pose_t pose;
449+
double error = estimate_tag_pose(&info, &pose);
450+
451+
// Create numpy arrays for R and t
452+
R_array = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3, 3}), NPY_FLOAT64);
453+
t_array = (PyArrayObject*)PyArray_SimpleNew(2, ((npy_intp[]){3, 1}), NPY_FLOAT64);
454+
455+
if(!R_array || !t_array)
456+
{
457+
PyErr_SetString(PyExc_RuntimeError, "Could not allocate output arrays");
458+
goto cleanup;
459+
}
460+
461+
// Copy rotation matrix
462+
for(int i = 0; i < 3; i++)
463+
{
464+
for(int j = 0; j < 3; j++)
465+
{
466+
*(double*)PyArray_GETPTR2(R_array, i, j) = MATD_EL(pose.R, i, j);
467+
}
468+
}
469+
470+
// Copy translation vector
471+
for(int i = 0; i < 3; i++)
472+
{
473+
*(double*)PyArray_GETPTR2(t_array, i, 0) = MATD_EL(pose.t, i, 0);
474+
}
475+
476+
result = Py_BuildValue("{s:N,s:N,s:d}",
477+
"R", R_array,
478+
"t", t_array,
479+
"error", error);
480+
R_array = NULL;
481+
t_array = NULL;
482+
483+
cleanup:
484+
if(H_matrix)
485+
matd_destroy(H_matrix);
486+
if(pose.R)
487+
matd_destroy(pose.R);
488+
if(pose.t)
489+
matd_destroy(pose.t);
490+
Py_XDECREF(R_array);
491+
Py_XDECREF(t_array);
492+
493+
return result;
494+
}
495+
341496

342497
#include "apriltag_detect_docstring.h"
343498
#include "apriltag_py_type_docstring.h"
499+
#include "apriltag_estimate_tag_pose_docstring.h"
344500

345501
static PyMethodDef apriltag_methods[] =
346502
{ PYMETHODDEF_ENTRY(apriltag_, detect, METH_VARARGS),
503+
PYMETHODDEF_ENTRY(apriltag_, estimate_tag_pose, METH_VARARGS),
347504
{NULL, NULL, 0, NULL}
348505
};
349506

test/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ add_library(getline OBJECT getline.c)
33
add_executable(test_detection test_detection.c)
44
target_link_libraries(test_detection ${PROJECT_NAME} getline)
55

6+
# Add the pose estimation test executable
7+
add_executable(test_tag_pose_estimation test_tag_pose_estimation.c)
8+
target_link_libraries(test_tag_pose_estimation ${PROJECT_NAME} getline)
9+
610
# test images with true detection
711
set(TEST_IMAGE_NAMES
812
"33369213973_9d9bb4cc96_c"
@@ -16,3 +20,13 @@ foreach(IMG IN LISTS TEST_IMAGE_NAMES)
1620
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
1721
)
1822
endforeach()
23+
24+
# Add pose estimation tests for each image
25+
foreach(IMG IN LISTS TEST_IMAGE_NAMES)
26+
add_test(NAME test_tag_pose_estimation_${IMG}
27+
COMMAND $<TARGET_FILE:test_tag_pose_estimation>
28+
data/${IMG}.jpg data/${IMG}.txt
29+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
30+
)
31+
endforeach()
32+

0 commit comments

Comments
 (0)