Skip to content

Commit de6077d

Browse files
committed
Issue #34: Update dot detector to match scikit-surgeryimage version.
1 parent 44a6a95 commit de6077d

3 files changed

Lines changed: 76 additions & 137 deletions

File tree

src/dots.cpp

Lines changed: 65 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
#include <cmath>
2222
#include <limits>
2323
#include <vector>
24-
#include <set>
2524

2625
namespace sks
2726
{
@@ -54,14 +53,6 @@ struct FiducialPoint
5453
}
5554
};
5655

57-
struct KeyPointSizeSorter
58-
{
59-
bool operator()(const cv::KeyPoint& k1, const cv::KeyPoint& k2) const
60-
{
61-
return k1.size > k2.size;
62-
}
63-
};
64-
6556
cv::Ptr<cv::SimpleBlobDetector> CreateDetector()
6657
{
6758
cv::SimpleBlobDetector::Params params;
@@ -85,8 +76,6 @@ cv::Mat ExtractDots(
8576
const cv::Mat& distortionCoefficients,
8677
const cv::Mat& gridPoints,
8778
const cv::Mat& indexesOfFourReferencePoints,
88-
int referenceImageWidth,
89-
int referenceImageHeight,
9079
bool isDistorted
9180
)
9281
{
@@ -102,49 +91,69 @@ cv::Mat ExtractDots(
10291
cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY,
10392
THRESHOLD_WINDOW_SIZE, THRESHOLD_OFFSET);
10493

105-
// Step 2: Detect blobs in the (possibly distorted) thresholded image
94+
// Step 2: Single blob detection on the thresholded image
10695
cv::Ptr<cv::SimpleBlobDetector> detector = CreateDetector();
10796
std::vector<cv::KeyPoint> keypoints;
10897
detector->detect(thresholded, keypoints);
10998

110-
// Step 3: Undistort and re-detect if needed
111-
cv::Mat undistortedImage;
112-
std::vector<cv::KeyPoint> undistortedKeypoints;
113-
114-
if (isDistorted)
99+
int numberOfKeypoints = static_cast<int>(keypoints.size());
100+
if (numberOfKeypoints <= 4)
115101
{
116-
cv::undistort(smoothed, undistortedImage, intrinsicMatrix, distortionCoefficients);
117-
118-
cv::Mat undistortedThresholded;
119-
cv::adaptiveThreshold(undistortedImage, undistortedThresholded, 255,
120-
cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY,
121-
THRESHOLD_WINDOW_SIZE, THRESHOLD_OFFSET);
122-
123-
detector->detect(undistortedThresholded, undistortedKeypoints);
102+
return emptyResult;
124103
}
125-
else
104+
105+
// Step 3: Extract keypoint coordinates (distorted image space) and sizes
106+
std::vector<cv::Point2f> distortedPts(numberOfKeypoints);
107+
std::vector<float> keypointSizes(numberOfKeypoints);
108+
for (int i = 0; i < numberOfKeypoints; i++)
126109
{
127-
undistortedImage = smoothed;
128-
undistortedKeypoints = keypoints;
110+
distortedPts[i] = keypoints[i].pt;
111+
keypointSizes[i] = keypoints[i].size;
129112
}
130113

131-
// Need at least 5 points (4 fiducials + at least 1 other)
132-
if (keypoints.size() <= 4 || undistortedKeypoints.size() <= 4)
114+
// Step 4: Get points in undistorted space for homography estimation
115+
std::vector<cv::Point2f> ptsForHomography;
116+
if (isDistorted)
133117
{
134-
return emptyResult;
118+
cv::Mat distortedMat(numberOfKeypoints, 1, CV_32FC2);
119+
for (int i = 0; i < numberOfKeypoints; i++)
120+
{
121+
distortedMat.at<cv::Vec2f>(i, 0) = cv::Vec2f(distortedPts[i].x,
122+
distortedPts[i].y);
123+
}
124+
cv::Mat undistortedMat;
125+
cv::undistortPoints(distortedMat, undistortedMat,
126+
intrinsicMatrix, distortionCoefficients,
127+
cv::noArray(), intrinsicMatrix);
128+
ptsForHomography.resize(numberOfKeypoints);
129+
for (int i = 0; i < numberOfKeypoints; i++)
130+
{
131+
cv::Vec2f pt = undistortedMat.at<cv::Vec2f>(i, 0);
132+
ptsForHomography[i] = cv::Point2f(pt[0], pt[1]);
133+
}
134+
}
135+
else
136+
{
137+
ptsForHomography = distortedPts;
135138
}
136139

137-
// Step 4: Find the four largest blobs (fiducials) in undistorted image
138-
std::sort(undistortedKeypoints.begin(), undistortedKeypoints.end(),
139-
KeyPointSizeSorter());
140+
// Step 5: Sort by size and pick biggest 4 as fiducials
141+
std::vector<int> sortedIndices(numberOfKeypoints);
142+
for (int i = 0; i < numberOfKeypoints; i++) sortedIndices[i] = i;
143+
std::sort(sortedIndices.begin(), sortedIndices.end(),
144+
[&keypointSizes](int a, int b) {
145+
return keypointSizes[a] < keypointSizes[b];
146+
});
140147

141148
std::vector<FiducialPoint> biggestFour;
142-
biggestFour.emplace_back(undistortedKeypoints[0].pt.x, undistortedKeypoints[0].pt.y);
143-
biggestFour.emplace_back(undistortedKeypoints[1].pt.x, undistortedKeypoints[1].pt.y);
144-
biggestFour.emplace_back(undistortedKeypoints[2].pt.x, undistortedKeypoints[2].pt.y);
145-
biggestFour.emplace_back(undistortedKeypoints[3].pt.x, undistortedKeypoints[3].pt.y);
149+
for (int i = numberOfKeypoints - 4; i < numberOfKeypoints; i++)
150+
{
151+
int idx = sortedIndices[i];
152+
biggestFour.emplace_back(ptsForHomography[idx].x,
153+
ptsForHomography[idx].y);
154+
}
146155

147-
// Step 5: Classify as TL, TR, BL, BR based on centroid
156+
// Step 6: Classify as TL, TR, BL, BR based on centroid
148157
double cx = 0, cy = 0;
149158
for (const auto& pt : biggestFour) { cx += pt.x; cy += pt.y; }
150159
cx /= 4.0; cy /= 4.0;
@@ -159,7 +168,7 @@ cv::Mat ExtractDots(
159168
// Sort by score: TL=0, TR=1, BL=2, BR=3
160169
std::sort(biggestFour.begin(), biggestFour.end());
161170

162-
// Step 6: Compute homography from detected fiducials to reference grid
171+
// Step 7: Compute homography from undistorted fiducials to reference grid
163172
cv::Mat sourceFiducials = cv::Mat::zeros(4, 2, CV_64F);
164173
cv::Mat targetFiducials = cv::Mat::zeros(4, 2, CV_64F);
165174
for (int i = 0; i < 4; i++)
@@ -177,35 +186,21 @@ cv::Mat ExtractDots(
177186
return emptyResult;
178187
}
179188

180-
// Step 7: Warp undistorted image to canonical face-on view, re-detect
181-
cv::Size refSize(referenceImageWidth, referenceImageHeight);
182-
cv::Mat warped;
183-
cv::warpPerspective(undistortedImage, warped, homography, refSize);
184-
185-
cv::Mat warpedThresholded;
186-
cv::adaptiveThreshold(warped, warpedThresholded, 255,
187-
cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY,
188-
THRESHOLD_WINDOW_SIZE, THRESHOLD_OFFSET);
189-
190-
std::vector<cv::KeyPoint> warpedKeypoints;
191-
detector->detect(warpedThresholded, warpedKeypoints);
189+
// Step 8: Warp all undistorted points into reference space using
190+
// perspectiveTransform (no image warping needed)
191+
std::vector<cv::Point2f> warpedPts;
192+
cv::perspectiveTransform(ptsForHomography, warpedPts, homography);
192193

193-
int numberOfWarpedKeypoints = static_cast<int>(warpedKeypoints.size());
194-
if (numberOfWarpedKeypoints == 0)
195-
{
196-
return emptyResult;
197-
}
198-
199-
// Step 8: For each warped dot, find closest point in reference grid
200-
std::vector<int> assignedGridIndex(numberOfWarpedKeypoints);
194+
// Step 9: Match each warped point to the nearest reference grid point
195+
std::vector<int> assignedGridIndex(numberOfKeypoints);
201196
double rmsError = 0;
202197

203-
for (int i = 0; i < numberOfWarpedKeypoints; i++)
198+
for (int i = 0; i < numberOfKeypoints; i++)
204199
{
205200
double bestDist = std::numeric_limits<double>::max();
206201
int bestIdx = -1;
207-
double wx = warpedKeypoints[i].pt.x;
208-
double wy = warpedKeypoints[i].pt.y;
202+
double wx = warpedPts[i].x;
203+
double wy = warpedPts[i].y;
209204

210205
for (int j = 0; j < gridPoints.rows; j++)
211206
{
@@ -222,72 +217,22 @@ cv::Mat ExtractDots(
222217
rmsError += std::sqrt(bestDist);
223218
}
224219

225-
rmsError /= static_cast<double>(numberOfWarpedKeypoints);
220+
rmsError /= static_cast<double>(numberOfKeypoints);
226221

227222
if (rmsError > RMS_TOLERANCE)
228223
{
229224
return emptyResult;
230225
}
231226

232-
// Step 9: Inverse-transform warped points back to undistorted coordinates
233-
std::vector<cv::Point2f> warpedPoints;
234-
cv::KeyPoint::convert(warpedKeypoints, warpedPoints);
235-
236-
cv::Mat homographyInv = homography.inv();
237-
std::vector<cv::Point2f> undistortedPoints;
238-
cv::perspectiveTransform(warpedPoints, undistortedPoints, homographyInv);
239-
240-
// Step 10: If distorted input, re-distort and match to original keypoints
241-
std::vector<cv::Point2f> finalImagePoints(numberOfWarpedKeypoints);
242-
243-
if (isDistorted)
244-
{
245-
double fx = intrinsicMatrix.at<double>(0, 0);
246-
double fy = intrinsicMatrix.at<double>(1, 1);
247-
double pcx = intrinsicMatrix.at<double>(0, 2);
248-
double pcy = intrinsicMatrix.at<double>(1, 2);
249-
double k1 = distortionCoefficients.at<double>(0, 0);
250-
double k2 = distortionCoefficients.at<double>(0, 1);
251-
double p1 = distortionCoefficients.at<double>(0, 2);
252-
double p2 = distortionCoefficients.at<double>(0, 3);
253-
double k3 = distortionCoefficients.at<double>(0, 4);
254-
255-
for (int i = 0; i < numberOfWarpedKeypoints; i++)
256-
{
257-
// Re-distort the undistorted point
258-
double relX = (undistortedPoints[i].x - pcx) / fx;
259-
double relY = (undistortedPoints[i].y - pcy) / fy;
260-
double r2 = relX * relX + relY * relY;
261-
double r4 = r2 * r2;
262-
double r6 = r2 * r4;
263-
double radial = 1.0 + k1 * r2 + k2 * r4 + k3 * r6;
264-
265-
double distX = relX * radial + (2.0 * p1 * relX * relY + p2 * (r2 + 2.0 * relX * relX));
266-
double distY = relY * radial + (p1 * (r2 + 2.0 * relY * relY) + 2.0 * p2 * relX * relY);
267-
268-
distX = distX * fx + pcx;
269-
distY = distY * fy + pcy;
270-
271-
finalImagePoints[i] = cv::Point2f(static_cast<float>(distX),
272-
static_cast<float>(distY));
273-
}
274-
}
275-
else
276-
{
277-
finalImagePoints = undistortedPoints;
278-
}
279-
280-
// Step 11: Remove duplicate ID assignments
281-
// Find which IDs appear more than once and exclude them
227+
// Step 10: Remove duplicate ID assignments (keep only unique matches)
282228
std::vector<int> idCount(gridPoints.rows, 0);
283-
for (int i = 0; i < numberOfWarpedKeypoints; i++)
229+
for (int i = 0; i < numberOfKeypoints; i++)
284230
{
285231
idCount[assignedGridIndex[i]]++;
286232
}
287233

288-
// Build final result with only unique assignments
289234
std::vector<int> validIndexes;
290-
for (int i = 0; i < numberOfWarpedKeypoints; i++)
235+
for (int i = 0; i < numberOfKeypoints; i++)
291236
{
292237
if (idCount[assignedGridIndex[i]] == 1)
293238
{
@@ -300,6 +245,7 @@ cv::Mat ExtractDots(
300245
return emptyResult;
301246
}
302247

248+
// Step 11: Build result using original distorted image coordinates
303249
cv::Mat result(static_cast<int>(validIndexes.size()), 6, CV_64F);
304250

305251
for (int i = 0; i < static_cast<int>(validIndexes.size()); i++)
@@ -308,8 +254,8 @@ cv::Mat ExtractDots(
308254
int gridIdx = assignedGridIndex[idx];
309255

310256
result.at<double>(i, 0) = gridPoints.at<double>(gridIdx, 0); // id
311-
result.at<double>(i, 1) = finalImagePoints[idx].x; // x_pix
312-
result.at<double>(i, 2) = finalImagePoints[idx].y; // y_pix
257+
result.at<double>(i, 1) = distortedPts[idx].x; // x_pix
258+
result.at<double>(i, 2) = distortedPts[idx].y; // y_pix
313259
result.at<double>(i, 3) = gridPoints.at<double>(gridIdx, 3); // x_mm
314260
result.at<double>(i, 4) = gridPoints.at<double>(gridIdx, 4); // y_mm
315261
result.at<double>(i, 5) = gridPoints.at<double>(gridIdx, 5); // z_mm

src/dots.h

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@ namespace sks
3131
* Algorithm overview:
3232
* 1. Gaussian blur and adaptive threshold the input image.
3333
* 2. Detect blobs using OpenCV's SimpleBlobDetector.
34-
* 3. If is_distorted is true, undistort the image and re-detect blobs.
34+
* 3. If isDistorted, undistort the detected point coordinates (not the
35+
* image) using cv::undistortPoints for homography estimation.
3536
* 4. Identify the four largest blobs as fiducial reference points.
3637
* 5. Classify fiducials as top-left, top-right, bottom-left, bottom-right.
3738
* 6. Compute a homography from the four fiducials to the reference grid.
38-
* 7. Warp the undistorted image to a canonical face-on view using the
39-
* homography, then re-detect blobs in the warped image.
40-
* 8. Assign each warped dot to its nearest reference grid point.
41-
* 9. Inverse-transform warped dot locations back to undistorted coordinates.
42-
* 10. If is_distorted, re-distort coordinates and match to original detections.
43-
* 11. Remove duplicate ID assignments (keep only unique matches).
39+
* 7. Warp all undistorted point coordinates into reference space using
40+
* cv::perspectiveTransform (no image warping).
41+
* 8. Assign each warped point to its nearest reference grid point.
42+
* 9. Remove duplicate ID assignments (keep only unique matches).
43+
* 10. Return the original detected pixel coordinates as image points.
4444
*
4545
* If the RMS matching error exceeds the tolerance, returns an empty matrix.
4646
*
@@ -53,10 +53,8 @@ namespace sks
5353
* \param indexesOfFourReferencePoints cv::Mat [4x1] CV_32S (int) containing
5454
* row indexes into gridPoints identifying the four fiducial dots
5555
* (top-left, top-right, bottom-left, bottom-right).
56-
* \param referenceImageWidth int width of the canonical warped image.
57-
* \param referenceImageHeight int height of the canonical warped image.
58-
* \param isDistorted bool if true, the input image has lens distortion and
59-
* will be undistorted internally. If false, skips undistortion.
56+
* \param isDistorted bool if true, detected point coordinates are undistorted
57+
* internally for homography estimation. If false, skips undistortion.
6058
* \return cv::Mat [Mx6] CV_64FC1 matrix where each row is
6159
* (id, x_pix, y_pix, x_mm, y_mm, z_mm) for each detected dot.
6260
* Returns an empty (0x6) matrix if detection fails.
@@ -67,8 +65,6 @@ cv::Mat ExtractDots(
6765
const cv::Mat& distortionCoefficients,
6866
const cv::Mat& gridPoints,
6967
const cv::Mat& indexesOfFourReferencePoints,
70-
int referenceImageWidth,
71-
int referenceImageHeight,
7268
bool isDistorted = true
7369
);
7470

src/python/bindings.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ PYBIND11_MODULE(sksurgeryopencvpython, m)
132132
&sks::ExtractDots,
133133
"Extract calibration dot locations from a camera image.\n\n"
134134
"Detects a grid of circular dots, identifies four large fiducial dots, "
135-
"warps to a canonical view, assigns grid IDs, and returns matched "
136-
"dot locations with duplicate removal.\n\n"
135+
"computes a homography to a reference grid, assigns grid IDs, and "
136+
"returns matched dot locations with duplicate removal.\n\n"
137137
"Args:\n"
138138
" image: ndarray [HxW] uint8 - greyscale image\n"
139139
" intrinsic_matrix: ndarray [3x3] float64 - camera intrinsics\n"
@@ -142,15 +142,12 @@ PYBIND11_MODULE(sksurgeryopencvpython, m)
142142
"(id, x_pix, y_pix, x_mm, y_mm, z_mm)\n"
143143
" reference_point_indexes: ndarray [4x1] int32 - indexes of four "
144144
"fiducial dots in grid_points\n"
145-
" reference_image_width: int - width of canonical warped image\n"
146-
" reference_image_height: int - height of canonical warped image\n"
147145
" is_distorted: bool - True if input has lens distortion (default True)\n\n"
148146
"Returns:\n"
149147
" ndarray [Mx6] float64 - detected dots "
150148
"(id, x_pix, y_pix, x_mm, y_mm, z_mm)",
151149
py::arg("image"), py::arg("intrinsic_matrix"),
152150
py::arg("distortion_coefficients"), py::arg("grid_points"),
153151
py::arg("reference_point_indexes"),
154-
py::arg("reference_image_width"), py::arg("reference_image_height"),
155152
py::arg("is_distorted") = true);
156153
}

0 commit comments

Comments
 (0)