Reputation: 1022
I have a set of images petri dishes which unfortunately are not the highest quality (example below, axes aren't part of the images).
I'm trying to select the background and calculate its area in pixels with the following:
image = Image.open(path)
black_image = 1 * (np.asarray(image.convert('L')) < 12)
black_region = black_image.sum()
This yields the below:
If I am more stringent with my selection of black pixels, I miss pixels in other images, and if I am looser I end up selecting too much of the petri dish itself. Is there a way I can only select the pixels have a luma value less than 12 AND are contiguous with an edge? I'm open to openCV solutions too.
Upvotes: 5
Views: 3923
Reputation: 18915
Hopefully, I'm not oversimplifying the problem, but from my point of view, using OpenCV with simple thresholding, morphological operations, and findContours
should do the job.
Please, see the following code:
import cv2
import numpy as np
# Input
input = cv2.imread('images/x0ziO.png', cv2.IMREAD_COLOR)
# Input to grayscale
gray = cv2.cvtColor(input, cv2.COLOR_BGR2GRAY)
# Binary threshold
_, gray = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
# Morphological improvements of the mask
gray = cv2.morphologyEx(gray, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)))
# Find contours
cnts, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Filter large size contours; at the end, there should only be one left
largeCnts = []
for cnt in cnts:
if (cv2.contourArea(cnt) > 10000):
largeCnts.append(cnt)
# Draw (filled) contour(s)
gray = np.uint8(np.zeros(gray.shape))
gray = cv2.drawContours(gray, largeCnts, -1, 255, cv2.FILLED)
# Calculate background pixel area
bgArea = input.shape[0] * input.shape[1] - cv2.countNonZero(gray)
# Put result on input image
input = cv2.putText(input, 'Background area: ' + str(bgArea), (20, 30), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1.0, (255, 255, 255))
cv2.imwrite('images/output.png', input)
The intermediate "mask" image looks like this:
And, the final output looks like this:
Upvotes: 5
Reputation: 207540
If you take the very top line/row of your image and the very bottom line/row and threshold them you will get this diagram where I have placed the top row at the top and the bottom row at the bottom just outside the limits of the original image - there is no need for you to do that, I am just illustrating the technique.
Now look where the lines change from black to white and then white to black (circled in red at the top). Unfortunately, your images have annotations and axes which I had to trim off so your number will not be identically the same. On the top line/row, my image changes from black to white at column 319 and back to black at column 648. If I add those together I get 966 and divide by 2, the image centre on the x-axis is at column 483.
Looking at the bottom line/row the transitions (circled in red) are at columns 234 and 736 which add up to 970 which makes 485 when averaged, so we know the circle centre is on vertical image column 483-485 or say 484.
Then you should now be able to work out the image centre and radius and mask the image to accurately calculate the background.
Upvotes: 3
Reputation: 41757
Try the experimental floodfill()
method. https://pillow.readthedocs.io/en/5.1.x/reference/ImageDraw.html?highlight=floodfill#PIL.ImageDraw.PIL.ImageDraw.floodfill
If all your images are like the example, just pick two or four corners of your image to fill with, say, hot pink and count that.
See also Image Segmentation with Watershed Algorithm which is much like flood fill but without relying on a single unique color.
Upvotes: 2
Reputation: 10386
Since you are open to OpenCV approaches you could use a
SimpleBlobDetector
Obviously the result I got is also not perfect, since there are a lot of hyperparameters to set. The hyperparameters make it pretty flexible, so it is a decent place to start from.
This is what the Detector does (see details here):
thresholdStep
until maxThreshold
. So the first threshold is minThreshold
, the second is minThreshold + thresholdStep
, the third is minThreshold + 2 x thresholdStep
, and so on.Merging: The centers of the binary blobs in the binary images are computed, and blobs located closer than minDistBetweenBlobs
are merged.
Center & Radius Calculation: The centers and radii of the new merged blobs are computed and returned.
Find the code bellow the image.
# Standard imports
import cv2
import numpy as np
# Read image
im = cv2.imread("petri.png", cv2.IMREAD_COLOR)
# Setup SimpleBlobDetector parameters.
params = cv2.SimpleBlobDetector_Params()
# Change thresholds
params.minThreshold = 0
params.maxThreshold = 255
# Set edge gradient
params.thresholdStep = 5
# Filter by Area.
params.filterByArea = True
params.minArea = 10
# Set up the detector with default parameters.
detector = cv2.SimpleBlobDetector_create(params)
# Detect blobs.
keypoints = detector.detect(im)
# Draw detected blobs as red circles.
# cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS ensures the size of the circle corresponds to the size of blob
im_with_keypoints = cv2.drawKeypoints(im, keypoints, np.array([]), (0, 0, 255),
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Show keypoints
cv2.imshow("Keypoints", im_with_keypoints)
cv2.waitKey(0)
Upvotes: 3