Improve/optimize accuracy of finding contours with OpenCV

I am trying to deal with a simple application which counts bacterium colonies at photos of Petri dishes. I am using python and cv2 library mainly.

I'm using the code above:

#reading image (reading is fixed for tests) and putting Opening morphological transformation to improve edge visibility
img = cv2.imread("image1.jpg",1)
img = cv2.resize(img,(500,500))
kernel = py.ones((7,7),py.uint8)
open = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

#converting colors to GRAY scale, setting threshold and contours. Setting a copy for result comprasion
img_gray = cv2.cvtColor(open, cv2.COLOR_BGR2GRAY)
copy = img.copy()
ret,thresh = cv2.threshold(img_gray,190,255,cv2.THRESH_BINARY)
im2,contours2,hierarchies = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

#drawing and counting countours (colonies)
visible_colonies = 0
for contour in (contours2):
  (x,y),radius = cv2.minEnclosingCircle(contour)
  center = (int(x),int(y))
  radius = int(radius)
  if(radius>2 and radius<25):
    cv2.circle(img,center,radius,(255,0,255),2)
    visible_colonies += 1
#showing result
plt.imshow(img)
plt.show()
print(visible_colonies)
plt.imshow(copy)
plt.show()

Nontheless, results are not enough accurate. To provide a sample: orginal orginal image, and image with contours

I can clearly see that there are many contours(colonies) which are not circled. At the same time, there are contours which point just nothing.

What I've tried to do:

  1. Dealing with noise (using opening transformation)
  2. Resizing the image for standardization
  3. Changing the settings of the opening kernel, threshold, finding contours and circles' radius.
  4. Using the adaptative threshold

What I'm suspecting:

  1. It's just a trial and error play with threshold/kernel/finding contours setting, with which I cannot catch up
  2. The image quality is to low
  3. There's too much noise on the image

And here goes my question finally - how to improve finding contours accuracy as much as possible? I don't want to use any simplifications there, I want this to be as accurate as it's possible.

Upvotes: 3

Views: 3342

Answers (1)

nathancy
nathancy

Reputation: 46600

A good approach here might be color thresholding with cv2.inRange(). The idea is to convert the image to HSV format and use a lower/upper color threshold to segment the colonies. We draw the detected colonies onto a mask then find contours on the mask.


Color segmented colonies drawn onto mask

enter image description here

Results

enter image description here

We can also keep track of the number of colonies

244

Potential optimizations would be to provide a higher resolution image for more accurate results. Other filters would be to use cv2.contourArea() and a minimum threshold area size if you wanted to only detect medium/large colonies.

import numpy as np
import cv2

image = cv2.imread('1.png')
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

lower = np.array([0, 71, 0], dtype="uint8")
upper = np.array([179, 255, 255], dtype="uint8")
mask = cv2.inRange(hsv, lower, upper)

cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]

colonies = []

for c in cnts:
    cv2.drawContours(image, [c], -1, (36, 255, 12), 2)
    colonies.append(c)

print(len(colonies))
cv2.imshow('mask', mask)
cv2.imshow('image', image)
cv2.imwrite('mask.png', mask)
cv2.imwrite('image.png', image)
cv2.waitKey()

The lower and upper bounds can be found using this script

import cv2
import sys
import numpy as np

def nothing(x):
    pass

useCamera=False

# Check if filename is passed
if (len(sys.argv) <= 1) :
    print("'Usage: python hsvThresholder.py <ImageFilePath>' to ignore camera and use a local image.")
    useCamera = True

# Create a window
cv2.namedWindow('image')

# create trackbars for color change
cv2.createTrackbar('HMin','image',0,179,nothing) # Hue is from 0-179 for Opencv
cv2.createTrackbar('SMin','image',0,255,nothing)
cv2.createTrackbar('VMin','image',0,255,nothing)
cv2.createTrackbar('HMax','image',0,179,nothing)
cv2.createTrackbar('SMax','image',0,255,nothing)
cv2.createTrackbar('VMax','image',0,255,nothing)

# Set default value for MAX HSV trackbars.
cv2.setTrackbarPos('HMax', 'image', 179)
cv2.setTrackbarPos('SMax', 'image', 255)
cv2.setTrackbarPos('VMax', 'image', 255)

# Initialize to check if HSV min/max value changes
hMin = sMin = vMin = hMax = sMax = vMax = 0
phMin = psMin = pvMin = phMax = psMax = pvMax = 0

# Output Image to display
if useCamera:
    cap = cv2.VideoCapture(0)
    # Wait longer to prevent freeze for videos.
    waitTime = 330
else:
    img = cv2.imread(sys.argv[1])
    output = img
    waitTime = 33

while(1):

    if useCamera:
        # Capture frame-by-frame
        ret, img = cap.read()
        output = img

    # get current positions of all trackbars
    hMin = cv2.getTrackbarPos('HMin','image')
    sMin = cv2.getTrackbarPos('SMin','image')
    vMin = cv2.getTrackbarPos('VMin','image')

    hMax = cv2.getTrackbarPos('HMax','image')
    sMax = cv2.getTrackbarPos('SMax','image')
    vMax = cv2.getTrackbarPos('VMax','image')

    # Set minimum and max HSV values to display
    lower = np.array([hMin, sMin, vMin])
    upper = np.array([hMax, sMax, vMax])

    # Create HSV Image and threshold into a range.
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower, upper)
    output = cv2.bitwise_and(img,img, mask= mask)

    # Print if there is a change in HSV value
    if( (phMin != hMin) | (psMin != sMin) | (pvMin != vMin) | (phMax != hMax) | (psMax != sMax) | (pvMax != vMax) ):
        print("(hMin = %d , sMin = %d, vMin = %d), (hMax = %d , sMax = %d, vMax = %d)" % (hMin , sMin , vMin, hMax, sMax , vMax))
        phMin = hMin
        psMin = sMin
        pvMin = vMin
        phMax = hMax
        psMax = sMax
        pvMax = vMax

    # Display output image
    cv2.imshow('image',output)

    # Wait longer to prevent freeze for videos.
    if cv2.waitKey(waitTime) & 0xFF == ord('q'):
        break

# Release resources
if useCamera:
    cap.release()
cv2.destroyAllWindows()

Upvotes: 7

Related Questions