Using cv::erode with a small kernel and multiple iterations may be enough for your needs, even if it's not exact.
C++ code:
cv::Mat img = ...;
int iterations = 10;
cv::erode(img, img,
cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3)),
cv::Point(-1,-1),
iterations);
Demo:
# img is the image containing the original black contour
for form in [cv.MORPH_RECT, cv.MORPH_CROSS]:
eroded = cv.erode(img, cv.getStructuringElement(form, (3,3)), iterations=10)
contours, hierarchy = cv.findContours(~eroded, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
vis = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
cv.drawContours(vis, contours, 0, (0,0,255))
cv.drawContours(vis, contours, 1, (255,0,0))
show_image(vis)
10 iterations with cv.MORPH_RECT with a 3x3 kernel:
10 iterations with cv.MORPH_CROSS with a 3x3 kernel:
You can change the offset by adjusting the number of iterations.
A much more accurate approach would be to use cv::distanceTransform to find all pixels that lie approximately 10px away from the contour:
dist = cv.distanceTransform(img, cv.DIST_L2, cv.DIST_MASK_PRECISE)
ring = cv.inRange(dist, 9.5, 10.5) # take all pixels at distance between 9.5px and 10.5px
show_image(ring)
contours, hierarchy = cv.findContours(ring, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
vis = cv.cvtColor(img, cv.COLOR_GRAY2BGR)
cv.drawContours(vis, contours, 0, (0,0,255))
cv.drawContours(vis, contours, 2, (255,0,0))
show_image(vis)
You'll get two contours on each side of the original contour. Use findContours with RETR_EXTERNAL to recover only the outer contour. To also recover the inner contour, use RETR_LIST
cv::dilate
orcv::erode
will do the job to a certain degree (or detail). The super-fine solution will incorporate something like finding the center of mass, projecting thex, y
coordinates of the original contour into the right direction, and determining newx, y
coordinates for the resuling contour, thus a lot of interpolating and extrapolating, I assume. – Incontrovertible