Find new blobs comparing two different binary images - python

I have two images taken on same sample at t=0 and t=t. There are few new blobs present in image taken at t. I need to find these new blobs (new blobs are the blobs which are present in new XY location at t=t). I am wondering if someone can help?
I tried OR,AND,XOR, reconstructions but the issue is the blobs which are same between two images are not exactly the same. Sometimes they might have size difference which makes the problem complicated.
Image at t=0
Image at t=t

Instead of using OR,AND,XOR, we may sum the two images.
Before summing the images, replace the 255 values with 100 (keeping the range of uint8 [0, 255]).
In the summed image, there are going to be three values:
0 - Background
100 - Non-overlapping area
200 - Overlapping area
We may assume that pixels with value 100 that touches value 200 belongs to the same original blob.
For clearing the overlapping pixels (200) with the touching pixels (100 around them), we may use cv2.floodFill.
After clearing the overlapping pixels and the pixels around them, the pixels that are left (with value 100) are the new blobs.
Example for clearing the pixels using cv2.floodFill:
if sum_img[y, x] == 200:
cv2.floodFill(sum_img, None, (x, y), 0, loDiff=100, upDiff=0)
Setting loDiff=100 is used for filling pixels=100 (and pixels=200) with 0 value (200-loDiff=100, so the 100 is filled with zero).
For making the solution better, we may find contours (of pixels=200), and ignore the tiny contours.
Code sample:
import cv2
import numpy as np
# Read input images as Grayscale.
img1 = cv2.imread('image1.png', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('image2.png', cv2.IMREAD_GRAYSCALE)
# Replace 255 with 100 (we want the sum img1+img2 not to overflow)
img1[img1 >= 128] = 100
img2[img2 >= 128] = 100
# Sum two images - in the sum, the value of overlapping parts of blobs is going to be 200
sum_img = img1 + img2
cv2.floodFill(sum_img, None, (0, 0), 0, loDiff=0, upDiff=0) # Remove the white frame.
cv2.imshow('sum_img before floodFill', sum_img) # Show image for testing.
# Find pixels with value 200 (the overlapping blobs).
thesh = cv2.threshold(sum_img, 199, 255, cv2.THRESH_BINARY)[1]
# Find contours (of overlapping blobs parts)
cnts = cv2.findContours(thesh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]
# Iterate contours and fill the overlapping part, and the non-zero pixels around it (i.e with value 100) with zero.
for c in cnts:
area_tresh = 50
area = cv2.contourArea(c)
if area > area_tresh: # Ignore very tiny contours
x, y = tuple(c[0, 0]) # Get coordinates of first pixel in the contour
if sum_img[y, x] == 200:
cv2.floodFill(sum_img, None, (x, y), 0, loDiff=100, upDiff=0) # Setting loDiff=100 is set for filling pixels=100 (and pixels=200)
sum_img[sum_img == 200] = 0 # Remove the small remainders
#thesh = cv2.cvtColor(thesh, cv2.COLOR_GRAY2BGR) # Convert to BGR for testing (for drawing the contours)
#cv2.drawContours(thesh, cnts, -1, (0, 255, 0), 2) # Draw contours for testing
# Show images for testing.
cv2.imshow('thesh', thesh)
cv2.imshow('sum_img after floodFill', sum_img)
cv2.waitKey()
cv2.destroyAllWindows()
Note:
We may dilate the images first, if the two blobs in proximity are considered to be the same blob (I don't no if a blob can "swim")
Output sum_img (after floodFill):
Update:
The above solution finds the blobs that exist in image1 and not in image2 and blobs exist in image2 and not in image1.
In case we want to find only the blobs that are new in image2, and we also assume that blobs that are close in both images are the same one, we may add the following stages:
Dilate img1 and img2 before summing (two close blobs are going to be overlapping).
Remove the dilated pixels from sum_img at the end.
Remove from sum_img all the blobs that exist only in img1 (and not in img2).
Code sample:
import cv2
import numpy as np
# Read input images as Grayscale.
img1 = cv2.imread('image1.png', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('image2.png', cv2.IMREAD_GRAYSCALE)
# Replace 255 with 100 (we want the sum img1+img2 not to overflow)
img1[img1 >= 128] = 100
img2[img2 >= 128] = 100
# Dilate both images - assume close blobs are the same blob (two blobs are considered overlapped even if they are close but not tuching).
dilated_img1 = cv2.dilate(img1, np.ones((11, 11), np.uint8))
dilated_img2 = cv2.dilate(img2, np.ones((11, 11), np.uint8))
# Sum two images - in the sum, the value of overlapping parts of blobs is going to be 200
sum_img = dilated_img1 + dilated_img2
cv2.floodFill(sum_img, None, (0, 0), 0, loDiff=0, upDiff=0) # Remove the white frame.
#cv2.imshow('sum_img before floodFill', sum_img) # Show image for testing.
# Find pixels with value 200 (the overlapping blobs).
thesh = cv2.threshold(sum_img, 199, 255, cv2.THRESH_BINARY)[1]
# Find contours (of overlapping blobs parts)
cnts = cv2.findContours(thesh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]
# Iterate contours and fill the overlapping part, and the non-zero pixels around it (i.e with value 100) with zero.
for c in cnts:
area_tresh = 0 # Optional
area = cv2.contourArea(c)
if area > area_tresh: # Ignore very tiny contours
x, y = tuple(c[0, 0]) # Get coordinates of first pixel in the contour
if sum_img[y, x] == 200:
cv2.floodFill(sum_img, None, (x, y), 0, loDiff=100, upDiff=0) # Setting loDiff=100 is set for filling pixels=100 (and pixels=200)
sum_img[sum_img == 200] = 0 # Remove the small remainders
sum_img[(img1 == 0) & (dilated_img1 == 100)] = 0 # Remove dilated pixels from dilated_img1
sum_img[(img2 == 0) & (dilated_img2 == 100)] = 0 # Remove dilated pixels from dilated_img2
sum_img[(img1 == 100) & (img2 == 0)] = 0 # Remove all the blobs that are only in first image (assume new blobs are "bored" only in image2)
# Visualization:
merged_img = cv2.merge((sum_img*2, img1*2, img2*2))
# The output image is img1, without the
output_image = img1.copy()
output_image[sum_img == 100] = 0
# Show images for testing.
cv2.imshow('sum_img', sum_img)
cv2.imshow('merged_img', merged_img)
cv2.waitKey()
cv2.destroyAllWindows()
Output (sum_img*2):
Visualization for testing (merged_img):
Green - exist only in img1
Yellow - exist both in img1 and img2
Magenta - exist only in img2, and not too close to blob in img1 (we are looking for the magenta blobs).

Related

Splitting multicolumn image for OCR

I'm trying to crop both columns from several pages like this in order to later OCR, looking at splitting the page along the vertical line
What I've got so far is finding the header, so that it can be cropped out:
image = cv2.imread('014-page1.jpg')
im_h, im_w, im_d = image.shape
base_image = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Create rectangular structuring element and dilate
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,10))
dilate = cv2.dilate(thresh, kernel, iterations=1)
# Find contours and draw rectangle
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=lambda x: cv2.boundingRect(x)[1])
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
if h < 20 and w > 250:
cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
How could I split the page vertically, and grab the text in sequence from the columns? Or alternatively, is there a better way to go about this?
Here's my take on the problem. It involves selecting a middle portion of the image, assuming the vertical line is present through all the image (or at least passes through the middle of the page). I process this Region of Interest (ROI) and then reduce it to a row. Then, I get the starting and ending horizontal coordinates of the crop. With this information and then produce the final cropped images.
I tried to made the algorithm general. It can split all the columns if you have more than two columns in the original image. Let's check out the code:
# Imports:
import numpy as np
import cv2
# Image path
path = "D://opencvImages//"
fileName = "pmALU.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# To grayscale:
grayImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Otsu Threshold:
_, binaryImage = cv2.threshold(grayImage, 0, 255, cv2.THRESH_OTSU)
# Get image dimensions:
(imageHeight, imageWidth) = binaryImage.shape[:2]
# Set middle ROI dimensions:
middleVertical = 0.5 * imageHeight
roiWidth = imageWidth
roiHeight = int(0.1 * imageHeight)
middleRoiVertical = 0.5 * roiHeight
roiY = int(0.5 * imageHeight - middleRoiVertical)
The first portion of the code gets the ROI. I set it to crop around the middle of the image. Let's just visualize the ROI that will be used for processing:
The next step is to crop this:
# Slice the ROI:
middleRoi = binaryImage[roiY:roiY + roiHeight, 0:imageWidth]
showImage("middleRoi", middleRoi)
writeImage(path+"middleRoi", middleRoi)
This produces the following crop:
Alright. The idea is to reduce this image to one row. If I get the maximum value of all columns and store them in one row, I should get a big white portion where the vertical line passes through.
Now, there's a problem here. If I directly reduce this image, this would be the result (the following is an image of the reduced row):
The image is a little bit small, but you can see the row produces two black columns at the sides, followed by two white blobs. That's because the image has been scanned, additionally the text seems to be justified and some margins are produced at the sides. I only need the central white blob with everything else in black.
I can solve this in two steps: draw a white rectangle around the image before reducing it - this will take care of the black columns. After this, I can Flood-filling with black again at both sides of the reduced image:
# White rectangle around ROI:
rectangleThickness = int(0.01 * imageHeight)
cv2.rectangle(middleRoi, (0, 0), (roiWidth, roiHeight), 255, rectangleThickness)
# Image reduction to a row:
reducedImage = cv2.reduce(middleRoi, 0, cv2.REDUCE_MIN)
# Flood fill at the extreme corners:
fillPositions = [0, imageWidth - 1]
for i in range(len(fillPositions)):
# Get flood-fill coordinate:
x = fillPositions[i]
currentCorner = (x, 0)
fillColor = 0
cv2.floodFill(reducedImage, None, currentCorner, fillColor)
Now, the reduced image looks like this:
Nice. But there's another problem. The central black line produced a "gap" at the center of the row. Not a problem really, because I can fill that gap with an opening:
# Apply Opening:
kernel = np.ones((3, 3), np.uint8)
reducedImage = cv2.morphologyEx(reducedImage, cv2.MORPH_CLOSE, kernel, iterations=2)
This is the result. No more central gap:
Cool. Let's get the vertical positions (indices) where the transitions from black to white and vice versa occur, starting at 0:
# Get horizontal transitions:
whiteSpaces = np.where(np.diff(reducedImage, prepend=np.nan))[1]
I now know where to crop. Let's see:
# Crop the image:
colWidth = len(whiteSpaces)
spaceMargin = 0
for x in range(0, colWidth, 2):
# Get horizontal cropping coordinates:
if x != colWidth - 1:
x2 = whiteSpaces[x + 1]
spaceMargin = (whiteSpaces[x + 2] - whiteSpaces[x + 1]) // 2
else:
x2 = imageWidth
# Set horizontal cropping coordinates:
x1 = whiteSpaces[x] - spaceMargin
x2 = x2 + spaceMargin
# Clamp and Crop original input:
x1 = clamp(x1, 0, imageWidth)
x2 = clamp(x2, 0, imageWidth)
currentCrop = inputImage[0:imageHeight, x1:x2]
cv2.imshow("currentCrop", currentCrop)
cv2.waitKey(0)
You'll note I calculate a margin. This is to crop the margins of the columns. I also use a clamp function to make sure the horizontal cropping points are always within image dimensions. This is the definition of that function:
# Clamps an integer to a valid range:
def clamp(val, minval, maxval):
if val < minval: return minval
if val > maxval: return maxval
return val
These are the results (resized for the post, open them in a new tab to see the full image):
Let's check out how this scales to more than two columns. This is a modification of the original input, with more columns added manually, just to check out the results:
These are the four images produced:
In order to separate out the two columns you have to find the dividing line in the center.
You can use Sobel derivative filter in the x-axis to find the black vertical line. Follow this tutorial for more details on the Sobel filter operator.
sobel_vertical = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) # (1,0) for x direction derivatives
Extract the line position by thresholding the sobel result:
ret, sobel_thresh = cv.threshold(sobel_vertical,127,255,cv.THRESH_BINARY)
Then scanning the center columns for a column with high concentration of white values.
One way to do this would be to do a column-wise sum and then find the column with the maximum values. But there are other ways to do it.
sum_cols = np.add.reduce(sobel_thresh, axis = 1)
max_col = np.argmax(sum_cols)
In a case where there is no black dividing line you can skip the sobel. Just resize aggressively and search for the columns in the center with high concentration of white pixels.

Object Detection with OpenCV-Python

I am trying to detect all of the overlapping circle/ellipses shapes in this image all of which have digits on them. I have tried different types of image processing techniques using OpenCV, however I cannot detect the shapes that overlap the tree. I have tried erosion and dilation however it has not helped.
Any pointers on how to go about this would be great. I have attached my code below
original = frame.copy()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
canny = cv2.Canny(blurred, 120, 255, 1)
kernel = np.ones((5, 5), np.uint8)
dilate = cv2.dilate(canny, kernel, iterations=1)
# Find contours
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
image_number = 0
for c in cnts:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(frame, (x, y), (x + w, y + h), (36, 255, 12), 2)
ROI = original[y:y + h, x:x + w]
cv2.imwrite("ROI_{}.png".format(image_number), ROI)
image_number += 1
cv2.imshow('canny', canny)
cv2.imshow('image', frame)
cv2.waitKey(0)
Here's a possible solution. I'm assuming that the target blobs (the saucer-like things) are always labeled - that is, they always have a white number inside them. The idea is to create a digits mask, because their size and color seem to be constant. I use the digits as guide to obtain sample pixels of the ellipses. Then, I convert these BGR pixels to HSV, create a binary mask and use that info to threshold and locate the ellipses. Let's check out the code:
# imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "4dzfr.png"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Convert RGB to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Get binary image via Otsu:
binaryImage = np.where(grayscaleImage >= 200, 255, 0)
# The above operation converted the image to 32-bit float,
# convert back to 8-bit uint
binaryImage = binaryImage.astype(np.uint8)
The first step is to make a mask of the digits. I also created a deep copy of the BGR image. The digits are close to white (That is, an intensity close to 255). I use 200 as threshold and obtain this result:
Now, let's locate these contours on this binary mask. I'm filtering based on aspect ratio, as the digits have a distinct aspect ratio close to 0.70. I'm also filtering contours based on hierarchy - as I'm only interested on external contours (the ones that do not have children). That's because I really don't need contours like the "holes" inside the digit 8:
# Find the contours on the binary image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Store the sampled pixels here:
sampledPixels = []
# Look for the outer bounding boxes (no children):
for i, c in enumerate(contours):
# Get the contour bounding rectangle:
boundRect = cv2.boundingRect(c)
# Get the dimensions of the bounding rect:
rectX = boundRect[0]
rectY = boundRect[1]
rectWidth = boundRect[2]
rectHeight = boundRect[3]
# Compute the aspect ratio:
aspectRatio = rectWidth / rectHeight
# Create the filtering threshold value:
delta = abs(0.7 - aspectRatio)
epsilon = 0.1
# Get the hierarchy:
currentHierarchy = hierarchy[0][i][3]
# Prepare the list of sampling points (One for the ellipse, one for the circle):
samplingPoints = [ (rectX - rectWidth, rectY), (rectX, rectY - rectHeight) ]
# Look for the target contours:
if delta < epsilon and currentHierarchy == -1:
# This list will hold both sampling pixels:
pixelList = []
# Get sampling pixels from the two locations:
for s in range(2):
# Get sampling point:
sampleX = samplingPoints[s][0]
sampleY = samplingPoints[s][1]
# Get sample BGR pixel:
samplePixel = inputImageCopy[sampleY, sampleX]
# Store into temp list:
pixelList.append(samplePixel)
# convert list to tuple:
pixelList = tuple(pixelList)
# Save pixel value:
sampledPixels.append(pixelList)
Ok, there area a couple of things happening in the last snippet of code. We want to sample pixels from both the ellipse and the circle. We will use two sampling locations that are function of each digit's original position. These positions are defined in the samplingPoints tuple. For the ellipse, I'm sampling at a little before the top right position of the digit. For the circle, I'm sapling directly above the top right position - we end up with two pixels for each figure.
You'll notice I'm doing a little bit of data type juggling, converting lists to tuples. I want these pixels stored as a tuple for convenience. If I draw bounding rectangles of the digits, this would be the resulting image:
Now, let's loop through the pixel list, convert them to HSV and create a HSV mask over the original BGR image. The final bounding rectangles of the ellipses are stored in boundingRectangles, additionally I draw results on the deep copy of the original input:
# Final bounding rectangles are stored here:
boundingRectangles = []
# Loop through sampled pixels:
for p in range(len(sampledPixels)):
# Get current pixel tuple:
currentPixelTuple = sampledPixels[p]
# Prepare the HSV mask:
imageHeight, imageWidth = binaryImage.shape[:2]
hsvMask = np.zeros((imageHeight, imageWidth), np.uint8)
# Process the two sampling pixels:
for m in range(len(currentPixelTuple)):
# Get current pixel in the list:
currentPixel = currentPixelTuple[m]
# Create BGR Mat:
pixelMat = np.zeros([1, 1, 3], dtype=np.uint8)
pixelMat[0, 0] = currentPixel
# Convert the BGR pixel to HSV:
hsvPixel = cv2.cvtColor(pixelMat, cv2.COLOR_BGR2HSV)
H = hsvPixel[0][0][0]
S = hsvPixel[0][0][1]
V = hsvPixel[0][0][2]
# Create HSV range for this pixel:
rangeThreshold = 5
lowerValues = np.array([H - rangeThreshold, S - rangeThreshold, V - rangeThreshold])
upperValues = np.array([H + rangeThreshold, S + rangeThreshold, V + rangeThreshold])
# Create HSV mask:
hsvImage = cv2.cvtColor(inputImage.copy(), cv2.COLOR_BGR2HSV)
tempMask = cv2.inRange(hsvImage, lowerValues, upperValues)
hsvMask = hsvMask + tempMask
First, I create a 1 x 1 Matrix (or Numpy Array) with just a BGR pixel value - the first of two I previously sampled. In this way, I can use cv2.cvtColor to get the corresponding HSV values. Then, I create lower and upper threshold values for the HSV mask. However, the image seems synthetic, and a range-based thresholding could be reduced to a unique tuple. After that, I create the HSV mask using cv2.inRange.
This will yield the HSV mask for the ellipse. After applying the method for the circle we will end up with two HSV masks. Well, I just added the two arrays to combine both masks. At the end you will have something like this, this is the "composite" HSV mask created for the first saucer-like figure:
We can apply a little bit of morphology to join both shapes, just a little closing will do:
# Set kernel (structuring element) size:
kernelSize = 3
# Set morph operation iterations:
opIterations = 2
# Get the structuring element:
morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
# Perform closing:
hsvMask = cv2.morphologyEx(hsvMask, cv2.MORPH_CLOSE, morphKernel, None, None, opIterations,cv2.BORDER_REFLECT101)
This is the result:
Nice. Let's continue and get the bounding rectangles of all the shapes. I'm using the boundingRectangles list to store each bounding rectangle, like this:
# Process current contour:
currentContour, _ = cv2.findContours(hsvMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for _, c in enumerate(currentContour):
# Get the contour's bounding rectangle:
boundRect = cv2.boundingRect(c)
# Get the dimensions of the bounding rect:
rectX = boundRect[0]
rectY = boundRect[1]
rectWidth = boundRect[2]
rectHeight = boundRect[3]
# Store and set bounding rect:
boundingRectangles.append(boundRect)
color = (0, 0, 255)
cv2.rectangle(inputImageCopy, (int(rectX), int(rectY)),
(int(rectX + rectWidth), int(rectY + rectHeight)), color, 2)
cv2.imshow("Objects", inputImageCopy)
cv2.waitKey(0)
This is the image of the located rectangles once every sampled pixel is processed:

Area of a closed contour on a plot using python openCV

I am attempting to find the area inside an arbitrarily-shaped closed curve plotted in python (example image below). So far, I have tried to use both the alphashape and polygon methods to acheive this, but both have failed. I am now attempting to use OpenCV and the floodfill method to count the number of pixels inside the curve and then I will later convert that to an area given the area that a single pixel encloses on the plot.
Example image:
testplot.jpg
In order to do this, I am doing the following, which I adapted from another post about OpenCV.
import cv2
import numpy as np
# Input image
img = cv2.imread('testplot.jpg', cv2.IMREAD_GRAYSCALE)
# Dilate to better detect contours
temp = cv2.dilate(temp, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
# Find largest contour
cnts, _ = cv2.findContours(255-temp, cv2.RETR_TREE , cv2.CHAIN_APPROX_NONE) #255-img and cv2.RETR_TREE is to account for how cv2 expects the background to be black, not white, so I convert the background to black.
largestCnt = [] #I expect this to yield the blue contour
for cnt in cnts:
if (len(cnt) > len(largestCnt)):
largestCnt = cnt
# Determine center of area of largest contour
M = cv2.moments(largestCnt)
x = int(M["m10"] / M["m00"])
y = int(M["m01"] / M["m00"])
# Initial mask for flood filling, should cover entire figure
width, height = temp.shape
mask = img2 = np.ones((width + 2, height + 2), np.uint8) * 255
mask[1:width, 1:height] = 0
# Generate intermediate image, draw largest contour onto it, flood fill this contour
temp = np.zeros(temp.shape, np.uint8)
temp = cv2.drawContours(temp, largestCnt, -1, 255, cv2.FILLED)
_, temp, mask, _ = cv2.floodFill(temp, mask, (x, y), 255)
temp = cv2.morphologyEx(temp, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
area = cv2.countNonZero(temp) #Number of pixels encircled by blue line
I expect from this to get to a place where I have the same image as above, but with the center of the contour filled in white and the background and original blue contour in black. I end up with this:
result.jpg
While this at first glance appears to have accurately turned the area inside the contour white, the white area is actually larger than the area inside the contour and so the result I get is overestimating the number of pixels inside it.
Any input on this would be greatly appreciated. I am fairly new to OpenCV so I may have misunderstood something.
EDIT:
Thanks to a comment below, I made some edits and this is now my code, with edits noted:
import cv2
import numpy as np
# EDITED INPUT IMAGE: Input image
img = cv2.imread('testplot2.jpg', cv2.IMREAD_GRAYSCALE)
# EDIT: threshold
_, temp = cv2.threshold(img, 250, 255, cv2.THRESH_BINARY_INV)
# EDIT, REMOVED: Dilate to better detect contours
# Find largest contour
cnts, _ = cv2.findContours(temp, cv2.RETR_EXTERNAL , cv2.CHAIN_APPROX_NONE)
largestCnt = [] #I expect this to yield the blue contour
for cnt in cnts:
if (len(cnt) > len(largestCnt)):
largestCnt = cnt
# Determine center of area of largest contour
M = cv2.moments(largestCnt)
x = int(M["m10"] / M["m00"])
y = int(M["m01"] / M["m00"])
# Initial mask for flood filling, should cover entire figure
width, height = temp.shape
mask = img2 = np.ones((width + 2, height + 2), np.uint8) * 255
mask[1:width, 1:height] = 0
# Generate intermediate image, draw largest contour, flood filled
temp = np.zeros(temp.shape, np.uint8)
temp = cv2.drawContours(temp, largestCnt, -1, 255, cv2.FILLED)
_, temp, mask, _ = cv2.floodFill(temp, mask, (x, y), 255)
temp = cv2.morphologyEx(temp, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
area = cv2.countNonZero(temp) #Number of pixels encircled by blue line
I input a different image with the axes and the frame that python adds by default removed for ease. I get what I expect at the second step, so this image. However, in the enter image description here both the original contour and the area it encircles appear to have been made white, whereas I want the original contour to be black and only the area it encircles to be white. How might I acheive this?
The problem is your opening operation at the end. This morphological operation includes a dilation at the end that expands the white contour, increasing its area. Let’s try a different approach where no morphology is involved. These are the steps:
Convert your image to grayscale
Apply Otsu’s thresholding to get a binary image, let’s work with black and white pixels only.
Apply a first flood-fill operation at image location (0,0) to get rid of the outer white space.
Filter small blobs using an area filter
Find the “Curve Canvas” (The white space that encloses the curve) and locate and store its starting point at (targetX, targetY)
Apply a second flood-fill al location (targetX, targetY)
Get the area of the isolated blob with cv2.countNonZero
Let’s take a look at the code:
import cv2
import numpy as np
# Set image path
path = "C:/opencvImages/"
fileName = "cLIjM.jpg"
# Read Input image
inputImage = cv2.imread(path+fileName)
inputCopy = inputImage.copy()
# Convert BGR to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu + bias adjustment:
threshValue, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
This is the binary image you get:
Now, let’s flood-fill at the corner located at (0,0) with a black color to get rid of the first white space. This step is very straightforward:
# Flood-fill background, seed at (0,0) and use black color:
cv2.floodFill(binaryImage, None, (0, 0), 0)
This is the result, note how the first big white area is gone:
Let’s get rid of the small blobs applying an area filter. Everything below an area of 100 is gonna be deleted:
# Perform an area filter on the binary blobs:
componentsNumber, labeledImage, componentStats, componentCentroids = \
cv2.connectedComponentsWithStats(binaryImage, connectivity=4)
# Set the minimum pixels for the area filter:
minArea = 100
# Get the indices/labels of the remaining components based on the area stat
# (skip the background component at index 0)
remainingComponentLabels = [i for i in range(1, componentsNumber) if componentStats[i][4] >= minArea]
# Filter the labeled pixels based on the remaining labels,
# assign pixel intensity to 255 (uint8) for the remaining pixels
filteredImage = np.where(np.isin(labeledImage, remainingComponentLabels) == True, 255, 0).astype('uint8')
This is the result of the filter:
Now, what remains is the second white area, I need to locate its starting point because I want to apply a second flood-fill operation at this location. I’ll traverse the image to find the first white pixel. Like this:
# Get Image dimensions:
height, width = filteredImage.shape
# Store the flood-fill point here:
targetX = -1
targetY = -1
for i in range(0, width):
for j in range(0, height):
# Get current binary pixel:
currentPixel = filteredImage[j, i]
# Check if it is the first white pixel:
if targetX == -1 and targetY == -1 and currentPixel == 255:
targetX = i
targetY = j
print("Flooding in X = "+str(targetX)+" Y: "+str(targetY))
There’s probably a more elegant, Python-oriented way of doing this, but I’m still learning the language. Feel free to improve the script (and share it here). The loop, however, gets me the location of the first white pixel, so I can now apply a second flood-fill at this exact location:
# Flood-fill background, seed at (targetX, targetY) and use black color:
cv2.floodFill(filteredImage, None, (targetX, targetY), 0)
You end up with this:
As you see, just count the number of non-zero pixels:
# Get the area of the target curve:
area = cv2.countNonZero(filteredImage)
print("Curve Area is: "+str(area))
The result is:
Curve Area is: 1510
Here is another approach using Python/OpenCV.
Read Input
convert to HSV colorspace
Threshold on color range of blue
Find the largest contour
Get its area and print that
draw the contour as a white filled contour on black background
Save the results
Input:
import cv2
import numpy as np
# read image as grayscale
img = cv2.imread('closed_curve.jpg')
# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#select blu color range in hsv
lower = (24,128,115)
upper = (164,255,255)
# threshold on blue in hsv
thresh = cv2.inRange(hsv, lower, upper)
# get largest contour
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
area = cv2.contourArea(c)
print("Area =",area)
# draw filled contour on black background
result = np.zeros_like(thresh)
cv2.drawContours(result, [c], -1, 255, cv2.FILLED)
# save result
cv2.imwrite("closed_curve_thresh.jpg", thresh)
cv2.imwrite("closed_curve_result.jpg", result)
# view result
cv2.imshow("threshold", thresh)
cv2.imshow("result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Threshold Image:
Result Filled Contour On Black Background:
Area Result:
Area = 2347.0

Detecting and counting blobs/connected objects with opencv

I want to detect and count the objects inside an image that touch while ignoring what could be considered as a single object. I have the basic image, on which i tried applying a cv2.HoughCircles() method to try and identify some circles. I then parsed the returned array and tried using cv2.circle() to draw them on the image.
However, I seem to always get too many circles returned by cv2.HoughCircles() and couldn't figure out how to only count the objects that are touching.
This is the image i was working on
My code so far:
import numpy
import matplotlib.pyplot as pyp
import cv2
segmentet = cv2.imread('photo')
houghCircles = cv2.HoughCircles(segmented, cv2.HOUGH_GRADIENT, 1, 80, param1=450, param2=10, minRadius=30, maxRadius=200)
houghArray = numpy.uint16(houghCircles)[0,:]
for circle in houghArray:
cv2.circle(segmented, (circle[0], circle[1]), circle[2], (0, 250, 0), 3)
And this is the image i get, which is quite a far shot from want i really want.
How can i properly identify and count said objects?
Here is one way in Python OpenCV by getting contour areas and the convex hull area of the contours. The take the ratio (area/convex_hull_area). If small enough, then it is a cluster of blobs. Otherwise it is an isolated blob.
Input:
import cv2
import numpy as np
# read input image
img = cv2.imread('blobs_connected.jpg')
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold to binary
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]
# find contours
#label_img = img.copy()
contour_img = img.copy()
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
index = 1
isolated_count = 0
cluster_count = 0
for cntr in contours:
area = cv2.contourArea(cntr)
convex_hull = cv2.convexHull(cntr)
convex_hull_area = cv2.contourArea(convex_hull)
ratio = area / convex_hull_area
#print(index, area, convex_hull_area, ratio)
#x,y,w,h = cv2.boundingRect(cntr)
#cv2.putText(label_img, str(index), (x,y), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0,0,255), 2)
if ratio < 0.91:
# cluster contours in red
cv2.drawContours(contour_img, [cntr], 0, (0,0,255), 2)
cluster_count = cluster_count + 1
else:
# isolated contours in green
cv2.drawContours(contour_img, [cntr], 0, (0,255,0), 2)
isolated_count = isolated_count + 1
index = index + 1
print('number_clusters:',cluster_count)
print('number_isolated:',isolated_count)
# save result
cv2.imwrite("blobs_connected_result.jpg", contour_img)
# show images
cv2.imshow("thresh", thresh)
#cv2.imshow("label_img", label_img)
cv2.imshow("contour_img", contour_img)
cv2.waitKey(0)
Clusters in Red, Isolated blobs in Green:
Textual Information:
number_clusters: 4
number_isolated: 81
approach it in steps.
label connected components. two connected blobs get the same label because they're connected. so far so good.
now separate your blobs. use watershed (first comment) or whatever other method gives you results. I can't fully predict the watershed approach. it might deal with touching blobs of dissimilar size or it might do something silly. the sample/tutorial also assumes a minimum size (0.7 * max peak); plug in something absolute in pixels maybe.
then, for each separated blob, check which label it sits on (take coordinates of centroid to be safe), and note down a +1 for that label (a histogram).
any label that has more than one separated blob sitting on it, would be what you are looking for.

I can't find a way to fit contour on new image zero point

I have a binary image and I want to find contours, to fit the biggest one into a new image with the size of the contour as if a rectangle was around it. In other words, to fit the contour into a new image with lower size.
The find contours routine is finding a rectangle for the whole image, and I don't need it. I look a contour of dimension (width - 1, height - 1) and skip it.
I want to remove biggest rectangle and then fit the 2nd biggest rectangle into a new image. That biggest rectangle will make the limits of the new image. Then I want to draw contours into a new white image.
I just don't know enough about OpenCV and the best way of doing this.
h = img.shape[0]
w = img.shape[1]
ret, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)
# are these the best find contours params?
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# paint a new image white
img = np.zeros((384, 640, 1), np.uint8)
img[:-1] = 255
# resize the contours
for i in range(0, len(contours)):
for j in range(0, len(contours[i])):
for k in range(0, len(contours[i][j])):
if contours[i][j][k][1] != h - 1 or contours[i][j][k][0] != w -1:
contours[i][j][k][1] = 384 * contours[i][j][k][1] / h
contours[i][j][k][0] = 640 * contours[i][j][k][0] / w
I can't find a way of finding the rectangle for the whole document. The biggest rectangle is image width * height, but in the 2nd one, only black pixels are visible.
In the comments you state that you want the black pixels as the bounds of the image. In that case you can use the method below. It loads the image as grayscale and then inverts it. So the white in the original image is now black (value: 0) and the black becomes white (value: 255). Next all rows and columns are summed up. The first and last rows/columns that have a sum that is greater than zero are the bounds of the black pixels in the original image. YOu can use these values to slice a new image.
Result:
Code:
import cv2
import numpy as np
# load the image as grayscale
img = cv2.imread('mQTiR.png',0)
#invert the image
img_inv = cv2.bitwise_not(img)
# sum each row and each column of the inverted image
sumOfCols = np.sum(img_inv, axis=0)
sumOfRows = np.sum(img_inv, axis=1)
# get the indexes of the rows/cols that are nonzero (=black in scan)
nonzeroX = np.nonzero(sumOfCols)[0]
nonzeroY = np.nonzero(sumOfRows)[0]
# get the first and last indexes, these are the bounds of the roi
minY = nonzeroY[0]
maxY = nonzeroY[-1]
minX = nonzeroX[0]
maxX = nonzeroX[-1]
#create subimage
subimage = img[minY:maxY,minX:maxX]
#display subimage
cv2.imshow('Result',subimage)
cv2.waitKey(0)
cv2.destroyAllWindows()

Categories