OpenCV deserves a few routines for non-maximum suppression, in particular for raster data like you have. the “usual” case is that someone has a bunch of bounding boxes that overlap… and that’s a silly situation to get into because half the time they come from raster data as well!
anyway, there’s a simple but tricky way to get NMS using morphological operations.
- dilate (grayscale) your accumulator array, use whatever structuring element (None is a 3x3 box kernel) and number of iterations that suit you
- pick those locations that are equal between original and this (a mask)
logic: the dilation spreads local maxima. everywhere you’ll know what the maximum within some distance is.
you still have to pick out true/non-zero from that mask but that’s np.nonzero
or cv.findNonzero
there are local maxima that aren’t peaks but more like plateaus. if you want to select for a peak shape in addition to that, you could try a median filter and check if the peak is sufficiently above that.
im = cv.imread("76b0dde62ebda812d35d3acfdf8750e55a955705.jpeg", cv.IMREAD_GRAYSCALE)
maxima = cv.dilate(im, None, iterations=20)
med = cv.medianBlur(im, ksize=19)
maxmask = (im == maxima)
medmask = (im.astype(np.int16) >= med.astype(np.int16) + 20) # peak property. avoiding unsigned difference.
mask = maxmask & medmask
canvas = cv.cvtColor(im, cv.COLOR_GRAY2BGR)
#mask = cv.dilate(mask.astype(np.uint8), None, iterations=3) # purely for display
y,x = np.nonzero(mask)
canvas[y,x] = (0,0,255)
cv.imshow("canvas", canvas)
cv.waitKey()
this can and will give you maxima adjacent/nearby to each other if they’re the exact same value.
if they’re really adjacent, you can ignore that if you run findContours or connectedComponents (instead of simply using the x and y from nonzero
). if they aren’t… you’d have to deal with it.
possible approach: take a kernel that looks like a filled arc (or box) centered on the pixel, and it covers everything to the top (half) and left (row), but nothing to the bottom (half) and right (row), or something of similar construction. I believe the condition is that the filter, rotated 180 degrees (i.e. flipped along x and y) and laid over itself, must be a partition (apart from the center value which is an implementation choice), i.e. everything covered with no overlap.
function to use would be filter2D
def funnykernel(radius):
kernel = np.zeros((2*radius+1, 2*radius+1), np.bool)
kernel[0:radius] = True # top half
kernel[radius, 0:radius] = True # left half of row
# center remains False
# kernel only reacts if there are nearby ones
return kernel
# use astype(np.uint8) if needed
# multiply by np.uint8(255) if needed
dst = cv.filter2D(...)
mask &= (dst == 0) # nonzero results indicate nearby peak -> suppress
if the applied filter sees not just the mask pixel itself but others in that area (check resulting value), it’s dominated and gets ignored. the kernel shape is important and tricky to get right for consistent results. if it’s wrong, you could be removing both of two points where just one should be removed.