Question: I have a set of images petri dishes which unfortunately are not the highest quality (example below, axes aren't part of the images).
[![dish1](https://i.stack.imgur.com/x0ziO.png)](https://i.stack.imgur.com/x0ziO.png)
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:

[![enter image description here](https://i.stack.imgur.com/7ircL.png)](https://i.stack.imgur.com/7ircL.png)

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.

Answer: 
Hopefully, I'm not oversimplifying the problem, but from my point of view, using OpenCV with simple thresholding, morphological operations, and [`findContours`](https://docs.opencv.org/4.0.1/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0) should do the job.

Please, see the following code:

```py
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:

[![Mask](https://i.stack.imgur.com/bHdac.png)](https://i.stack.imgur.com/bHdac.png)

And, the final output looks like this:

[![Output](https://i.stack.imgur.com/hIJin.png)](https://i.stack.imgur.com/hIJin.png)
> 
> 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](https://www.learnopencv.com/blob-detection-using-opencv-python-c/)):

1. **Thresholding**: Convert the source images to several binary images by thresholding the source image with thresholds starting at minThreshold. These thresholds are incremented by `thresholdStep` until `maxThreshold`. So the first threshold is `minThreshold`, the second is `minThreshold + thresholdStep`, the third is `minThreshold + 2 x thresholdStep`, and so on.
2. **Grouping**: In each binary image, connected white pixels are grouped together. Let’s call these binary blobs.
3. **Merging**: The centers of the binary blobs in the binary images are computed, and blobs located closer than `minDistBetweenBlobs` are merged.
4. **Center & Radius Calculation**: The centers and radii of the new merged blobs are computed and returned.

*Find the code bellow the image.*

[![Output Image](https://i.stack.imgur.com/rc0mW.jpg)](https://i.stack.imgur.com/rc0mW.jpg)

```
# 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)

```