1

Currently I'm working on object detection for counting how many object presented on the frame. I already successfully separate some of them. There's still some object which is very close together which it turns into one blob I still don't know how to separate it properly since other touching object which just the tip can be separated. Also there are objects which in my distance transformation visualization looks pretty clear but some how the peak_local_max() function not recognized the object so its not giving a peak coordinates correctly and then on the watershed section that object is gone. Are there something I did wrong? Here is my debugging image.

Debug View

Here is my sample_image if you wanted to try.

sample_image

My watershed code is simple, I use a helper scipy's ndimage

import cv2 
import numpy as np
from skimage.feature import peak_local_max
from skimage.segmentation import watershed
from scipy import ndimage

gray_for_mask = cv2.cvtColor(working, cv2.COLOR_BGR2GRAY)
mask_glare = cv2.threshold(gray_for_mask, 200, 255, cv2.THRESH_BINARY)[1]
if np.count_nonzero(mask_glare) > 0:
    inpaint_mask = cv2.dilate(mask_glare, np.ones((3,3), np.uint8), iterations=1)
    working = cv2.inpaint(working, inpaint_mask, inpaintRadius=3, flags=cv2.INPAINT_TELEA)


hsv = cv2.cvtColor(working, cv2.COLOR_BGR2HSV)
lower_gold = np.array([10, 40, 100])   # H,S,V
upper_gold = np.array([45, 255, 255])
mask_color = cv2.inRange(hsv, lower_gold, upper_gold)


gray = cv2.cvtColor(working, cv2.COLOR_BGR2GRAY)
gray_blur = cv2.medianBlur(gray, 5)
adaptive_thresh = cv2.adaptiveThreshold(gray_blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV , 31 , 3)

close_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
adaptive_thresh = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_CLOSE, close_kernel, iterations=1)

combined = cv2.bitwise_or(mask_color, adaptive_thresh)

k_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))

mask = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, k_close, iterations=1)

mask = remove_small_components(mask, min_area=150)

k_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2,2))
mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, k_close, iterations=1)

contours, hierarchy = cv2.findContours(
    mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
)
mask_filled = mask.copy()
for i, cnt in enumerate(contours):
    if hierarchy[0][i][3] == -1:
        cv2.drawContours(mask_filled, [cnt], 0, 255, thickness=cv2.FILLED)
mask_filled = cv2.erode(mask_filled, kernel=np.ones((2,2), np.uint8), iterations=1)

dist = ndimage.distance_transform_edt(mask_filled.copy())
peaks = peak_local_max(dist, min_distance=40,
labels=mask_filled.copy())
dist_visual = cv2.normalize(dist, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) 

peak_map = cv2.cvtColor(dist_visual.copy() * 255, cv2.COLOR_GRAY2BGR)
for (py, px) in peaks:
    cv2.circle(peak_map, (px, py), 3, (0, 0, 255), -1)

local_max = np.zeros_like(dist, dtype=bool)
local_max[tuple(peaks.T)] = True
markers = ndimage.label(local_max)[0]
labels = watershed(-dist, markers, mask=mask_filled.copy())

watershed_vis = np.zeros((mask_filled.copy().shape[0], mask_filled.copy().shape[1], 3), dtype=np.uint8)
for label in np.unique(labels):
    if label == 0:
       continue

color = np.random.randint(15, 255, size=(3,), dtype=np.uint8)
watershed_vis[labels == label] = color
5
  • 1
    Please edit your question to include a minimal reproducible example so that readers can run your code to answer your question. Commented Nov 12 at 8:34
  • you mean like my entire code? @André Commented Nov 12 at 9:08
  • 2
    A minimal reproducible example should be stripped down to show the problem and run without having to guess imports, magic numbers etc. No need for the full code, but what you provided is not enough. How do you derive mask_filled from your input image? What is the signature of watershed? Most likely skimage.segmentation.watershed, but we should not have to guess, etc. Your code should read the input and generate the bottom right output, so that we are able to reproduce. Commented Nov 12 at 9:12
  • 1
    as with all CV/machine vision, lighting is critical. you should back-light the objects (and matte translucent background) so the objects appear entirely dark to the camera. what you did is to light them from this side. that causes reflections, highlights, ... Commented Nov 12 at 11:34
  • 1
    I just edited my code, its a bit long since I did many image processing. Sorry for taking too long to reply, its already time to clock out. @André Commented Nov 13 at 0:54

1 Answer 1

2

Your code already does a descent job. You can always tweak some parameters to include or exclude one or the other object, however, it would likely not generalize. I thus propose the following two additions, in order to improve your detection:

Run your watershed transform iteratively

After the first labelling, remove all labelled regions from the distance transform's output dist and repeat peak detection and labelling as long there are new peaks.

Peaks after first pass: 1st pass peaks

Peaks after second pass: 2nd pass peaks

Now you get the objects that have previously been missed due to your min_distance criterion.

Split connected objects

Join all labels and get a histogram of the object sizes from the labels masks and derive median_size. You can then detect outliers, e.g. if object_size > 1.5 * median_size, and run the peak detection and watershed once again per outlier. This time, set the num_peaks=2 to get exactly two peaks and reduce the min_distance.

The following worked on your attached image:

object_mask = (labels == label)
object_size = np.sum(object_mask)

if object_size > 1.5 * median_size:
    dist_obj  = dist * object_mask
    peaks_obj = peak_local_max(dist_obj, min_distance=5, labels=object_mask, num_peaks=2)

split 1 split 2

You will get new labels for each object. You can now join everything, re-label according to the coordinates and should get a pretty descent output.

This is what I get, note that I had to change some parameters of your code (is your attached input downsampled compared to what you use?).

final labels

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much Andre, out of all the things that I've tried this is the best outcome so far. May i see your code about the iterative watershedding and the splitting part? And yes for some reason my sample image got downgraded, it should be a 2560 x 1440

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.