Image segmentation in python - python

I have the image
I am looking for python solution to break the shape in this image into smaller parts according to the contour in the image.
I have looked into solution on Canny and findContours in OpenCV but none of them works for me.
Edit:
Code used:
using Canny method
import cv2 import numpy as np
img = cv2.imread('area_of_blob_maxcontrast_white.jpg') edges = cv2.Canny(img, 100, 200)
cv2.imwrite('area_of_blob_maxcontrast_white_edges.jpg',edges)
using findContours method
import numpy as np
import argparse
import cv2
image = cv2.imread('area_of_blob_maxcontrast_white.png')
lower = np.array([0, 0, 0]) upper = np.array([15, 15, 15]) shapeMask = cv2.inRange(image, lower, upper)
(_,cnts, _) = cv2.findContours(shapeMask.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE) print "I found %d black shapes" % (len(cnts)) cv2.imshow("Mask", shapeMask)
for c in cnts:
# draw the contour and show it
cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
cv2.imshow("Image", image)
cv2.waitKey(0)

The trick is to make your faint single pixel boundary slightly bolder. I do it by changing any white pixel that has two adjacent black pixels (above, below, to the left or to the right) to black. (I do it extremely slow, though. I'm pretty sure there must be a smarter way to do it with OpenCV or Numpy.)
Here is my code:
#!/usr/bin/env python
import numpy as np
import cv2
THRESH = 240
orig = cv2.imread("map.png")
img = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
# Make the faint 1-pixel boundary bolder
rows, cols = img.shape
new_img = np.full_like(img, 255) # pure white image
for y in range(rows):
if not (y % 10):
print ('Row = %d (%.2f%%)' % (y, 100.*y/rows))
for x in range(cols):
score = 1 if y > 0 and img.item(y-1, x) < THRESH else 0
score += 1 if x > 0 and img.item(y, x-1) < THRESH else 0
score += 1 if y < rows-1 and img.item(y+1, x) < THRESH else 0
score += 1 if x < cols-1 and img.item(y, x+1) < THRESH else 0
if img.item(y, x) < THRESH or score >= 2:
new_img[y, x] = 0 # black pixels show boundary
cv2.imwrite('thresh.png', new_img)
# Find all contours on the map
_th, contours, hierarchy = cv2.findContours(new_img,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
print "Number of contours detected = %d" % len(contours)
# Fill second level regions on the map
coln = 0
colors = [
[127, 0, 255],
[255, 0, 127],
[255, 127, 0],
[127, 255, 0],
[0, 127, 255],
[0, 255, 127],
]
hierarchy = hierarchy[0]
for i in range(len(contours)):
area = cv2.contourArea(contours[i])
if hierarchy[i][3] == 1:
print (i, area)
coln = (coln + 1) % len(colors)
cv2.drawContours(orig, contours, i, colors[coln], -1)
cv2.imwrite("colored_map.png", orig)
Input image:
Output image:
Here I color only the direct descendants of the outmost contour (hierarchy[i][3] == 1). But you can change it to exclude the lakes.

Related

How to remove a contoured area from an image?

I have an image with a white background and some colored blocks. I have created multiple filters to find the colored blocks and get the contours for another purpose and I am able to draw the contours around the colored blocks.
These blocks are connected by some black lines which I would like to get the contours of. I was using the original contours to also get the lines but I was advised not to do that and instead remove the colored blocks from the image so that I would only remain with the black lines in the image making it easier to contour.
I have created a mask that would draw over the contoured block but when I display the final image the contoured blocks are black instead of white.
Is there any way to make the blocks white similar to the background so that I could remain with only the black lines?
From the images, you can see that the mask covers the image and the black line remains. However, I can't figure out how to make the blocks white instead of black so that only the line remains.
from ctypes import sizeof
from doctest import master
from cv2 import approxPolyDP, contourArea, cvtColor, inRange
import numpy as np
import cv2
kernel = (5, 5)
srcImg = cv2.imread('BluePurpleConnected.png', cv2.IMREAD_COLOR)
blur = cv2.GaussianBlur(srcImg, kernel, 0)
hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV)
grayImg = cv2.cvtColor(srcImg, cv2.COLOR_BGR2GRAY)
rows = int(grayImg.shape[0]/2)
cols = int(grayImg.shape[1]/2)
ker = np.ones((0, 0), 'uint8')
graySize = grayImg.shape
sig = 0.33
cPix = 200
bPix = 255
lPurp = np.array([130, 20, 10])
uPurp = np.array([145, 255, 255])
lBlue = np.array([94, 80, 2])
uBlue = np.array([126, 255, 255])
colArray = np.array([lPurp, uPurp, lBlue, uBlue])
masterStruc = []
maskArray = []
mask = np.ones(srcImg.shape[:2], dtype="uint8") * 255
x = 0
while x <= 1:
imgMask = cv2.inRange(hsv, colArray[2*x], colArray[2*x+1])
maskArray.append(imgMask)
v = np.median(imgMask)
lower = int(max(0, (1.0 - sig) * v))
upper = int(min(255, (1.0 + sig) * v))
bwImg = cv2.Canny(imgMask, lower, upper)
nbwImg = cv2.dilate(bwImg, kernel, iterations=1)
cImg = nbwImg
ret, threshold = cv2.threshold(cImg, cPix, bPix, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(threshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = 0
for cnt in contours:
curve = approxPolyDP(cnt, 0.02 * cv2.arcLength(cnt, True), True)
vert = len(curve)
area = cv2.contourArea(curve)
if(area > 1000):
if(vert == 4):
masterStruc.append([x, curve, area])
cv2.drawContours(mask, [curve], -1, 0, -1)
x = x + 1
srcImg = cv2.bitwise_and(srcImg, srcImg, mask=mask)
cv2.imshow('Mask', mask)
cv2.imshow('Image', srcImg)
if cv2.waitKey(0) & 0xFF == ord('q'):
cv2.destroyAllWindows()

How to extract board from sudoku image?

This is my input sudoku image : 0000.png and this is my desired output image : output.
I am trying to find the contours of the sudoku board so I can extract the board separately. This is the code I've tried so far.
import cv2
import operator
import numpy as np
import matplotlib.pyplot as plt
image = cv2.imread("0000.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5,5), 0)
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2)
contours,_ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
max_area = 0
c = 0
for i in contours:
area = cv2.contourArea(cv2.UMat(i))
if area > 1000:
if area > max_area:
max_area = area
best_cnt = i
image = cv2.drawContours(image, contours, c, (0, 255, 0), 3)
c+=1
mask = np.zeros((gray.shape),np.uint8)
cv2.drawContours(mask,[best_cnt],0,255,-1)
cv2.drawContours(mask,[best_cnt],0,0,2)
out = np.zeros_like(gray)
out[mask == 255] = gray[mask == 255]
blur = cv2.GaussianBlur(out, (11,11), 0)
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2)
c = 0
for i in contours:
area = cv2.contourArea(i)
if area > 1000/2:
cv2.drawContours(image, contours, c, (0, 255, 0), 3)
c+=1
These are the images I'm able to extract after finding adaptive thresholds and using contours to draw masks.
plt.imshow(out, cmap='gray')
plt.imshow(thresh, cmap='gray')
plt.imshow(image)
How can I modify my code to generate the output image, where the sudoku board is cropped as in output ?
EDIT :
I tried extracting the coordinates of the biggest blog and cropped the largest rectangle, but it's still not accurate as the output image I desire.
contours, h = cv2.findContours(out.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
polygon = contours[0]
bottom_right, _ = max(enumerate([pt[0][0] + pt[0][1] for pt in polygon]), key=operator.itemgetter(1))
top_left, _ = min(enumerate([pt[0][0] + pt[0][1] for pt in polygon]), key=operator.itemgetter(1))
bottom_left, _ = min(enumerate([pt[0][0] - pt[0][1] for pt in polygon]), key=operator.itemgetter(1))
top_right, _ = max(enumerate([pt[0][0] - pt[0][1] for pt in polygon]), key=operator.itemgetter(1))
box = [polygon[top_left][0], polygon[top_right][0], polygon[bottom_right][0], polygon[bottom_left][0]]
def distance_between(p1, p2):
"""Returns the scalar distance between two points"""
a = p2[0] - p1[0]
b = p2[1] - p1[1]
return np.sqrt((a ** 2) + (b ** 2))
def crop_and_warp(img, crop_rect):
"""Crops and warps a rectangular section from an image into a square of similar size."""
top_left, top_right, bottom_right, bottom_left = crop_rect[0], crop_rect[1], crop_rect[2], crop_rect[3]
src = np.array([top_left, top_right, bottom_right, bottom_left], dtype='float32')
side = max([
distance_between(bottom_right, top_right),
distance_between(top_left, bottom_left),
distance_between(bottom_right, bottom_left),
distance_between(top_left, top_right)
])
dst = np.array([[0, 0], [side - 1, 0], [side - 1, side - 1], [0, side - 1]], dtype='float32')
m = cv2.getPerspectiveTransform(src, dst)
return cv2.warpPerspective(img, m, (int(side), int(side)))
cropped = crop_and_warp(out, box)
plt.imshow(cropped, cmap='gray')
The border is not neatly extracted unlike my desired output image. The reason I want it in this format is because then then the image can be split into 81 images with the help of a 9x9 grid and I could detect all the numbers in each of the image. The black trace above the image is causing troubles there.

Separating the leaf from it's background [duplicate]

This question already has answers here:
Segmentation problem for tomato leaf images in PlantVillage Dataset
(2 answers)
Closed 3 years ago.
I have a set of images, all of which look almost like this leaf here:
I want to extract the leaf from the background, for which I used the GrabCut algorithm as used here.
As a different approach, I also used thresholding based on ratios of r, g and b values as here:
import numpy as np
import cv2
import matplotlib.pyplot as plt
testImg = cv2.imread('path_to_the_image')
testImg = cv2.resize(testImg, (256, 256))
#bgImg = cv2.imread('')
#blurBg = cv2.GaussianBlur(bgImg, (5, 5), 0)
#blurBg = cv2.resize(blurBg, (256, 256))
#testImg = cv2.GaussianBlur(testImg, (5, 5), 0)
cv2.imshow('testImg', testImg)
#plt.imshow(bgImg)
cv2.waitKey(0)
#plt.show()
modiImg = testImg.copy()
ht, wd = modiImg.shape[:2]
print(modiImg[0][0][0])
for i in range(ht):
for j in range(wd):
r = modiImg[i][j][0]
g = modiImg[i][j][1]
b = modiImg[i][j][2]
r1 = r/g
r2 = g/b
r3 = r/b
r4 = round((r1+r2+r3)/3, 1)
if g > r and g > b:
modiImg[i][j] = [255, 255, 255]
elif r4 >= 1.2:
modiImg[i][j] = [255, 255, 255]
else:
modiImg[i][j] = [0, 0, 0]
# if r4 <= 1.1:
# modiImg[i][j] = [0, 0, 0]
# elif g > r and g > b:
# modiImg[i][j] = [255, 255, 255]
# else:
# modiImg[i][j] = [255, 255, 255]
# elif r4 >= 1.2:
# modiImg[i][j] = [255, 255, 255]
# else:
# modiImg[i][j] = [0, 0, 0]
plt.imshow(modiImg)
plt.show()
testImg = testImg.astype(float)
alpha = modiImg.astype(float) / 255
testImg = cv2.multiply(alpha, testImg)
cv2.imshow('final', testImg/255)
cv2.waitKey(0)
But the dark spots on the leaf always go missing in the extracted leaf image as shown here:
Is there any other method to separate the leaf from its background, given that there is only one leaf per image, and the background is almost the same for other images that I have and also the leaves are positioned almost similarly as in here.
You can try image segmentation using HSV colormap.
Code:
img = cv2.imread('leaf.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# find the green color
mask_green = cv2.inRange(hsv, (36,0,0), (86,255,255))
# find the brown color
mask_brown = cv2.inRange(hsv, (8, 60, 20), (30, 255, 200))
# find the yellow color in the leaf
mask_yellow = cv2.inRange(hsv, (21, 39, 64), (40, 255, 255))
# find any of the three colors(green or brown or yellow) in the image
mask = cv2.bitwise_or(mask_green, mask_brown)
mask = cv2.bitwise_or(mask, mask_yellow)
# Bitwise-AND mask and original image
res = cv2.bitwise_and(img,img, mask= mask)
cv2.imshow("original", img)
cv2.imshow("final image", res)
cv2.waitKey(0)
cv2.destroyAllWindows()
Output:
Moreover, If you change lower range of yellow color from (21, 39, 64) to (14, 39, 64), then you will see that the small black spots present on the leaf start filling and will improve the result even further.
You may want to use a deep learning method. The U-Net is doing pretty good on tasks like that https://lmb.informatik.uni-freiburg.de/people/ronneber/u-net/. As I see they also provide a trained model. If you have Matlab and Caffee installed you should be able to copy your files into the right folder, run the program and receive the results you are looking for.
Thresholding is not a good idea for that kind of task. Your method should be able to recognize patterns instead of just looking at pixel's color.
A problem with the deep learning method is tough, that you either need a pretrained network that has trained the segmentation of RBG imges of leafes or you need data (RGB image of leafes and the corresponding segmentation).

Watershed Segmentation excluding alone object?

Problem
Using this answer to create a segmentation program, it is counting the objects incorrectly. I noticed that alone objects are being ignored or poor imaging acquisition.
I counted 123 objects and the program returns 117, as can be seen, bellow. The objects circled in red seem to be missing:
Using the following image from a 720p webcam:
Code
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import label
import urllib.request
# https://stackoverflow.com/a/14617359/7690982
def segment_on_dt(a, img):
border = cv2.dilate(img, None, iterations=5)
border = border - cv2.erode(border, None)
dt = cv2.distanceTransform(img, cv2.DIST_L2, 3)
plt.imshow(dt)
plt.show()
dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(np.uint8)
_, dt = cv2.threshold(dt, 140, 255, cv2.THRESH_BINARY)
lbl, ncc = label(dt)
lbl = lbl * (255 / (ncc + 1))
# Completing the markers now.
lbl[border == 255] = 255
lbl = lbl.astype(np.int32)
cv2.watershed(a, lbl)
print("[INFO] {} unique segments found".format(len(np.unique(lbl)) - 1))
lbl[lbl == -1] = 0
lbl = lbl.astype(np.uint8)
return 255 - lbl
# Open Image
resp = urllib.request.urlopen("https://i.stack.imgur.com/YUgob.jpg")
img = np.asarray(bytearray(resp.read()), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
## Yellow slicer
mask = cv2.inRange(img, (0, 0, 0), (55, 255, 255))
imask = mask > 0
slicer = np.zeros_like(img, np.uint8)
slicer[imask] = img[imask]
# Image Binarization
img_gray = cv2.cvtColor(slicer, cv2.COLOR_BGR2GRAY)
_, img_bin = cv2.threshold(img_gray, 140, 255,
cv2.THRESH_BINARY)
# Morphological Gradient
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN,
np.ones((3, 3), dtype=int))
# Segmentation
result = segment_on_dt(img, img_bin)
plt.imshow(np.hstack([result, img_gray]), cmap='Set3')
plt.show()
# Final Picture
result[result != 255] = 0
result = cv2.dilate(result, None)
img[result == 255] = (0, 0, 255)
plt.imshow(result)
plt.show()
Question
How to count the missing objects?
Answering your main question, watershed does not remove single objects. Watershed was functioning fine in your algorithm. It receives the predefined labels and perform segmentation accordingly.
The problem was the threshold you set for the distance transform was too high and it removed the weak signal from the single objects, thus preventing the objects from being labeled and sent to the watershed algorithm.
The reason for the weak distance transform signal was due to the improper segmentation during the color segmentation stage and the difficulty of setting a single threshold to remove noise and extract signal.
To remedy this, we need to perform proper color segmentation and use adaptive threshold instead of the single threshold when segmenting the distance transform signal.
Here is the code i modified. I have incorporated color segmentation method by #user1269942 in the code. Extra explanation is in the code.
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import label
import urllib.request
# https://stackoverflow.com/a/14617359/7690982
def segment_on_dt(a, img, img_gray):
# Added several elliptical structuring element for better morphology process
struct_big = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
struct_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
# increase border size
border = cv2.dilate(img, struct_big, iterations=5)
border = border - cv2.erode(img, struct_small)
dt = cv2.distanceTransform(img, cv2.DIST_L2, 3)
dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(np.uint8)
# blur the signal lighty to remove noise
dt = cv2.GaussianBlur(dt,(7,7),-1)
# Adaptive threshold to extract local maxima of distance trasnform signal
dt = cv2.adaptiveThreshold(dt, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 21, -9)
#_ , dt = cv2.threshold(dt, 2, 255, cv2.THRESH_BINARY)
# Morphology operation to clean the thresholded signal
dt = cv2.erode(dt,struct_small,iterations = 1)
dt = cv2.dilate(dt,struct_big,iterations = 10)
plt.imshow(dt)
plt.show()
# Labeling
lbl, ncc = label(dt)
lbl = lbl * (255 / (ncc + 1))
# Completing the markers now.
lbl[border == 255] = 255
plt.imshow(lbl)
plt.show()
lbl = lbl.astype(np.int32)
cv2.watershed(a, lbl)
print("[INFO] {} unique segments found".format(len(np.unique(lbl)) - 1))
lbl[lbl == -1] = 0
lbl = lbl.astype(np.uint8)
return 255 - lbl
# Open Image
resp = urllib.request.urlopen("https://i.stack.imgur.com/YUgob.jpg")
img = np.asarray(bytearray(resp.read()), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
## Yellow slicer
# blur to remove noise
img = cv2.blur(img, (9,9))
# proper color segmentation
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, (0, 140, 160), (35, 255, 255))
#mask = cv2.inRange(img, (0, 0, 0), (55, 255, 255))
imask = mask > 0
slicer = np.zeros_like(img, np.uint8)
slicer[imask] = img[imask]
# Image Binarization
img_gray = cv2.cvtColor(slicer, cv2.COLOR_BGR2GRAY)
_, img_bin = cv2.threshold(img_gray, 140, 255,
cv2.THRESH_BINARY)
plt.imshow(img_bin)
plt.show()
# Morphological Gradient
# added
cv2.morphologyEx(img_bin, cv2.MORPH_OPEN,cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)),img_bin,(-1,-1),10)
cv2.morphologyEx(img_bin, cv2.MORPH_ERODE,cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)),img_bin,(-1,-1),3)
plt.imshow(img_bin)
plt.show()
# Segmentation
result = segment_on_dt(img, img_bin, img_gray)
plt.imshow(np.hstack([result, img_gray]), cmap='Set3')
plt.show()
# Final Picture
result[result != 255] = 0
result = cv2.dilate(result, None)
img[result == 255] = (0, 0, 255)
plt.imshow(result)
plt.show()
Final results :
124 Unique items found.
An extra item was found because one of the object was divided to 2.
With proper parameter tuning, you might get the exact number you are looking. But i would suggest getting a better camera.
Looking at your code, it is completely reasonable so I'm just going to make one small suggestion and that is to do your "inRange" using HSV color space.
opencv docs on color spaces:
https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.html
another SO example using inRange with HSV:
How to detect two different colors using `cv2.inRange` in Python-OpenCV?
and a small code edits for you:
img = cv2.blur(img, (5,5)) #new addition just before "##yellow slicer"
## Yellow slicer
#mask = cv2.inRange(img, (0, 0, 0), (55, 255, 255)) #your line: comment out.
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) #new addition...convert to hsv
mask = cv2.inRange(hsv, (0, 120, 120), (35, 255, 255)) #new addition use hsv for inRange and an adjustment to the values.
Improving Accuracy
Detecting missing objects
im_1, im_2, im_3
I've count 12 missing objects: 2, 7, 8, 11, 65, 77, 78, 84, 92, 95, 96. edit: 85 too
117 found, 12 missing, 6 wrong
1° Attempt: Decrease Mask Sensibility
#mask = cv2.inRange(img, (0, 0, 0), (55, 255, 255)) #Current
mask = cv2.inRange(img, (0, 0, 0), (80, 255, 255)) #1' Attempt
inRange documentaion
im_4, im_5, im_6, im_7
[INFO] 120 unique segments found
120 found, 9 missing, 6 wrong

What does opencv threshold THRESH_BINARY do on colored images?

The documentation on THRESH_BINARY says:
dst(x,y) = maxval if src(x,y) > thresh else 0
Which to me does not imply that this won't work on colored images. I expected a two color output even when applied to a colored image, but the output is multicolor. Why? How can that be when the possible values the pixel x,y is assinged are maxval and 0 only?
Example:
from sys import argv
import cv2
import numpy as np
img = cv2.imread(argv[1])
ret, threshold = cv2.threshold(img, 120, 255, cv2.THRESH_BINARY)
cv2.imshow('threshold', threshold)
cv2.imshow('ori', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Suppose you've pixel from 3 channel RGB image with values rgb(66, 134, 244). Now suppose you give thresh value 135. What do you think will happen?
r = 66
g = 134
b = 244
if(r > thresh) r = 255 else r = 0; // we have r = 0
if(g > thresh) g = 255 else g = 0; // we have g = 0
if(b > thresh) b = 255 else b = 0; // we have b = 255
New pixel value is rgb(0, 0, 255). Since your image is an RGB color image, now the pixel color is BLUE instead of WHITE.
Thresholding applied to each color channel, separately. If less tahn threshold then color channel set to 0, if not then to maxval. Channels processed independently, that's why result is color image with several colors. The colors you can get are: (0,0,0), (255,0,0), (0,255,0), (255,255,0), (0,0,255),(255,0,255), (0,255,255) and (255,255,255).

Categories

Resources