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)
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.

[![enter image description here](https://i.stack.imgur.com/HJYC6.jpg)](https://i.stack.imgur.com/HJYC6.jpg)

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.