0

I have high resolution images which have very small blobs to be detected. A sub section of the image is as below full of small dot like blobs. Original img The blobs are always about the same size 4-5px diameter. I want to catch them using a hand-engineered kernel such as this

kernel= np.array([[ -2, -2,-1,-2,-2,-2,-1,-1,-2, -2],
                  [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -2],
                  [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -1],
                  [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                  [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                  [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                  [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -1],
                  [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -1],
                  [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -2],
                  [ -2, -2,-1,-2,-2,-2,-1,-1,-2, -2]])

And run this kernel over the image.

My code so far is like this

base_img = cv.imread(os.path.join(prepath, 'blevel.jpg'),0)

for i in range(0, base_img.shape[0]-10, 10):
    for j in range(0, base_img.shape[1]-10, 10):
        print(i,j)
        baseimg_sub = base_img[i:i+10, j:j+10]
        baseimg_sub = baseimg_sub - np.min(baseimg_sub)
        baseimg_sub = np.round(baseimg_sub / np.max(baseimg_sub), 1)
        corr_factor = np.sum(np.multiply(baseimg_sub, kernel)) / 100
        if corr_factor > 0.1 :
            cv.circle(base_img, (j+5, i+5), 7, (255,255,255))

But I cannot catch them. These are the detected blobs: Blobs detected image Which seems to be catching emtpy spaces mostly. Can someone tell me please what can I change in my kernel?

2 Answers 2

1

I suggest that you use the Laplacian of Gaussian filter. It is always a good idea to start with an established method to achieve a particular goal. It is implemented in skimage as skimage.feature.blob_log.

The Laplacian of Gaussian filter is the best simple filter you can use to detect small dots. Of course there are more complex algorithms that do better, but you should only attempt those if the easy method fails. The scientific literature is the best place to find these more complex algorithms.

One of the reasons your “hand-engineered kernel” doesn’t work is that it’s weights don’t sum to 0. Another one is that you contrast-stretch every small neighborhood in your image, which makes a bright dot look the same as an unfortunate bit of dark noise in the dark background. Another reason is that you match your template every 10 pixels, so you run a large chance of not placing your template at the center of a dot, and thus don’t detect it.

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

2 Comments

Thank you for your reply. I have tried the LoG method. It works well to some extend. But somehow it is very slow on high res images. I made the sum of filter kernel 0 now. But now when I use lower stride, the whole image becomes full of detected blobs. I basically detects every single step as a blob. I don't know what happens?
Oh I found out what the problem is, with lower stride. The problem was that I was drawing circles on the same image that I was using to detect blobs.
1

The convolution seems to be unreliable in this case. I have tried a FFT and filtered the high frequencies and then simply detected threshold values (this can be improved: note the stride you use: always a full kernel dimension; this works for smaller strides, e.g. 8; with smaller strides there is a risk of overlap). Maybe also use the back-transformed image as a basis for the convolution. You may want to try different filter values or even analyse the spectrum of the original image. The blobs seem to be quite frequent visually. The reference for the smooth is given in the code, not mine):

# https://akshaysin.github.io/fourier_transform.html

import numpy as np
import cv2 as cv
import os
from matplotlib import pyplot as plt
from scipy.signal import argrelextrema
from scipy import fftpack

def smooth(x, window_len=11, window='hanning'):
    """smooth the data using a window with requested size.
    
    This method is based on the convolution of a scaled window with the signal.
    The signal is prepared by introducing reflected copies of the signal 
    (with the window size) in both ends so that transient parts are minimized
    in the begining and end part of the output signal.
    
    input:
        x: the input signal 
        window_len: the dimension of the smoothing window; should be an odd integer
        window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'
            flat window will produce a moving average smoothing.

    output:
        the smoothed signal
        
    example:

    t=linspace(-2,2,0.1)
    x=sin(t)+randn(len(t))*0.1
    y=smooth(x)
    
    see also: 
    
    numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve
    scipy.signal.lfilter
 
    TODO: the window parameter could be the window itself if an array instead of a string
    NOTE: length(output) != length(input), to correct this: return y[(window_len/2-1):-(window_len/2)] instead of just y.
    """
    if x.ndim != 1:
        raise ValueError # "smooth only accepts 1 dimension arrays."
    if x.size < window_len:
        raise ValueError # "Input vector needs to be bigger than window size."
    if window_len<3:
        return x
    if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
        raise ValueError # "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'"

    s=np.r_[x[window_len-1:0:-1],x,x[-2:-window_len-1:-1]]
    #print(len(s))
    if window == 'flat': #moving average
        w=np.ones(window_len,'d')
    else:
        w=eval('np.'+window+'(window_len)')
    y=np.convolve(w/w.sum(), s, mode='valid')
    return y

if __name__ == '__main__':
    img = cv2.imread('blevel.jpg', 0)
    dft = cv2.dft(np.float32(img), flags=cv2.DFT_COMPLEX_OUTPUT)
    dft_shift = np.fft.fftshift(dft)

    magnitude_spectrum = 200 * np.log(cv2.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))

    rows, cols = img.shape
    crow, ccol = int(rows / 2), int(cols / 2)  # center

    # Circular HPF mask, center circle is 0, remaining all ones
    mask = np.ones((rows, cols, 2), np.uint8)
    r = 30
    center = [crow, ccol]
    x, y = np.ogrid[:rows, :cols]
    mask_area = (x - center[0]) ** 2 + (y - center[1]) ** 2 <= r*r # circular
    #mask_area = abs((x - center[0]) + y * 0.)  <= r # directional x
    #mask_area = abs(x * 0. + (y - center[1]))  <= r # directional y
    mask[mask_area] = 0

    # apply mask and inverse DFT
    fshift = dft_shift * mask

    fshift_mask_mag = 1000 * np.log(cv2.magnitude(fshift[:, :, 0], fshift[:, :, 1]))

    f_ishift = np.fft.ifftshift(fshift)
    img_back = cv2.idft(f_ishift)
    img_back = cv2.magnitude(img_back[:, :, 0], img_back[:, :, 1])
    
    prepath = ''
    base_img = cv.imread(os.path.join(prepath, 'blevel.jpg'),0)

    plt.figure(num=None, figsize=(14, 12), dpi=80, facecolor='w', edgecolor='k')

    plt.subplot(4, 2, 1), plt.imshow(img, cmap='gray')
    plt.title('Input Image'), plt.xticks([]), plt.yticks([])
    plt.subplot(4, 2, 2), plt.imshow(magnitude_spectrum, cmap='gray')
    plt.title('After FFT'), plt.xticks([]), plt.yticks([])
    plt.subplot(4, 2, 3), plt.imshow(fshift_mask_mag, cmap='gray')
    plt.title('FFT + Mask'), plt.xticks([]), plt.yticks([])
    plt.figure(figsize=(60,40))
    plt.subplot(4, 2, 4), plt.imshow(img_back, cmap='gray')
    plt.title('After FFT Inverse'), plt.xticks([]), plt.yticks([])
    #plt.show()

    kernel= np.array([[ -2, -2,-1,-2,-2,-2,-1,-1,-2, -2],
                      [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -2],
                      [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -1],
                      [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                      [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                      [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -2],
                      [ -2,  0, 0, 2, 2, 2, 2, 0, 0, -1],
                      [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -1],
                      [ -2,  0, 0, 0, 0, 0, 0, 0, 0, -2],
                      [ -2, -2,-1,-2,-2,-2,-1,-1,-2, -2]])

    prepath = ''
    #base_img = cv.imread(os.path.join(prepath, 'blevel.jpg'),0)
    #blob_img = cv.imread(os.path.join(prepath, 'blob0.jpg'),0)
    #kernel = np.array(blob_img)
    #kernel = kernel - np.min(kernel)
    #kx, ky = np.shape(kernel)
    kx, ky = 8, 8 # use w/o kernel
    ksum = np.sum(kernel)
    #imgsum = np.sum(img_back)
    #print(kx, ky, kernel)
    
    invlim = np.average(img_back) + 1.5*np.std(img_back)

    for i in range(0, img_back.shape[0]-kx, int(kx/1)): # note stride
        for j in range(0, img_back.shape[1]-ky, int(ky/1)):
            #print(i,j)
            baseimg_sub = img_back[i:i+kx, j:j+ky]
            #baseimg_sub = baseimg_sub - np.min(baseimg_sub)
            #baseimg_sub = np.round(baseimg_sub / np.max(baseimg_sub), 1)
            #corr_factor = np.sum(np.multiply(baseimg_sub, kernel)) / ksum / np.sum(baseimg_sub)
            corr_factor = np.average(baseimg_sub)
            if corr_factor > invlim:
                cv.circle(base_img, (j+5, i+5), 7, (255,255,255))

    plt.figure(figsize=(60,40))
    plt.subplot(4, 2, 5), plt.imshow(base_img, cmap='gray')
    plt.show()

This produces (note some artifacts at the top border):

enter image description here

This is the filtered image, for reference. The blobs are more prominent. enter image description here

4 Comments

Thank you for your reply. This really helps. I have to work more on FFT. But the problem with thresholding is that this is only a part of an actual bigger image, in different areas they have different background and blob intensities. Another question is that why does the stride needs to be the kernel size? Why can't I choose a lower stride value? With my method when I use lower strid, the whole image becomes full of detected blobs. What happens?
I have forgotten to comment on the stride, added it in my answer. It does not have to be kernel. If the blobs are close but do not overlap 1/2 kernel would still give not too many detects. Below that the method likely fails as blobs merge and a fixed kernel would no longer reliably detect them. I noticed an intensity separation between blobs, maybe try iteratively? FFT with low-pass, detect and remove blobs at larger strides and rerun with adjusted image? Time permits I'll test Cris's recommendation.
Oh I found out what the problem is, with lower stride. The problem was that I was drawing circles on the same image that I was using to detect blobs!
that happens. Cool!

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.