split image on the basis of color - python-3.x

I have obtained an image after applying k-means with clusters = 3. Now I want to obtain 3 separate images on the basis of colours obtained after k-means.
For example, consider the attached image. Now I need
one image such that it contains only the blue square.
One having the letter v and one with just the background
Is there any possible way to do that using OpenCV and python.

The most general and simplest way to do it is using the three unique gray colors for each region. (Although I could find more than three gray levels in the above image, maybe due to variation as a result of compression of imgur. Though, at the end of the day, k-means should give exactly three BGR values)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
unique = np.unique(gray)
c1, c2, c3 = unique[0], unique[1], unique[2]
mask1 = np.zeros_like(gray)
mask1[gray == c1] = 255
mask2 = np.zeros_like(gray)
mask2[gray == c2] = 255
mask3 = np.zeros_like(gray)
mask3[mask3 == c3] = 255

You can solve the problem by calculating the histogram of the image.
The below plot shows the peaks of the image.
From this, you can threshold the colors. The code and result:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread("inputs/hist.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([gray],[0],None,[256],[0,256])
colors = np.where(hist>5000)
img_number = 0
for color in colors[0]:
print(color)
split_image = img.copy()
split_image[np.where(gray != color)] = 0
cv2.imwrite(str(img_number)+".jpg",split_image)
img_number+=1
plt.hist(gray.ravel(),256,[0,256])
plt.savefig('plt')
plt.show()
Results:

Related

Threshold in a superpixel opencv2

I'm trying to use cv2 module to receive pixel coordinates of relatively dark regions in an image.
First I divide it into super-pixels through the cv2.ximgproc.createSuperpixelSLIC() method.
Then I'd like to consider each super-pixel as a ROI, and threshold it based on its' the intensity, so that the darker regions (i.e., where the intensity is lower than some preconfigured threshold) will be 1, and 0 in regions where the intensity is relatively high (i.e., larger than this threshold).
I tried the following code, but the problem is that is highlights the background (as obviously it also dark).
import cv2
import numpy as np
# Parameters
IMG_FILE_PATH = 'PATH TO THE IMAGE'
REGION_SIZE = 200
RULER = 20
N = 10
SAMPLE_SIZE = 5
INTENSITY_TH = 100
# ---
# 1) Load the image
img = cv2.imread(IMG_FILE_PATH, cv2.IMREAD_GRAYSCALE)
# 2) Compute the superpixels
slic = cv2.ximgproc.createSuperpixelSLIC(img, region_size=REGION_SIZE, ruler=RULER)
# 3) Get the characteristics of the superpixels calculated
slic.iterate(N)
slic.enforceLabelConnectivity()
lbls = slic.getLabels()
num_slic = slic.getNumberOfSuperpixels()
# 4) Sample some of the superpixels
sample_idxs = np.random.choice(np.arange(num_slic), size=SAMPLE_SIZE, replace=False)
for idx in sample_idxs:
img_super_pixel = np.uint8(img * (lbls==idx).astype(np.int16))
ret, mask_fg = cv2.threshold(img_super_pixel, INTENSITY_TH, 255, cv2.THRESH_BINARY)
img_super_pixel_th = cv2.bitwise_and(img_super_pixel, img_super_pixel, mask=mask_fg)
cv2.imshow('Super-pixel', img_super_pixel)
cv2.imshow('Super-pixel - thresholded', img_super_pixel_th)
cv2.waitKey()
cv2.destroyAllWindows()
Here is a sample image:
Current Output Example:
So, as is seen - the background is represented with 1., obviously because it is less than the threshold, but what I need is that that only the black spots in the super-pixel would be white, and the background with the pixels which exceed the threshold in the super-pixel area, would be black.
Is there a way to apply threshold only on the ROI, viz. the super-pixel, and not on the background?
Thanks in advance.
I was able to solve this by manually checking the pixels in the region which are below the threshold, as shown in the following code:
import cv2
import numpy as np
import pandas as pd
from pathlib import Path
# Parameters
IMG_FILE_PATH = 'PATH_TO_IMAGE'
OUTDIR = Path('OUTPUT_FOLDER')
REGION_SIZE = 200
RULER = 20
N = 10
SAMPLE_SIZE = 5
INTENSITY_TH = 100
# ---
# 1) Load the image
img = cv2.imread(IMG_FILE_PATH, cv2.IMREAD_GRAYSCALE)
# 2) Compute the superpixels
slic = cv2.ximgproc.createSuperpixelSLIC(img, region_size=REGION_SIZE, ruler=RULER)
# 3) Get the characteristics of the superpixels calculated
slic.iterate(N)
slic.enforceLabelConnectivity()
mask_slic = slic.getLabelContourMask()
lbls = slic.getLabels()
num_slic = slic.getNumberOfSuperpixels()
# 4) Sample some of the superpixels
sample_idxs = np.random.choice(np.arange(num_slic), size=SAMPLE_SIZE, replace=False)
for idx in sample_idxs:
# 4.1) Create pandas.DataFrame to store the points and their validity based on the threshold
sp_pixels_df = pd.DataFrame(columns=['x', 'y', 'relevant'])
# 4.2) Get the current super-pixel
img_super_pixel = np.uint8(img * (lbls==idx).astype(np.int16))
# 4.3) Find the coordinates of the pixels inside the current super-pixel
img_super_pixel_idx = np.argwhere(lbls==idx)
# 4.4) Separate the x and y coordinates of the points which are located inside the superpixel
xs, ys = np.array([t[0] for t in img_super_pixel_idx]), np.array([t[1] for t in img_super_pixel_idx])
# 4.5) Find the pixels inside the superpixel, which intensity is below the threshold
low_intensity_pixels = img_super_pixel[tuple([xs, ys])] < INTENSITY_TH
# 4.6) Populate the pandas.DataFrame
sp_pixels_df['x'] = xs
sp_pixels_df['y'] = ys
sp_pixels_df['relevant'] = low_intensity_pixels
# 4.7) Get the valid pixel coordinates
relevant_points = sp_pixels_df.loc[sp_pixels_df.relevant, ['x', 'y']].values
# 4.8) Separate the x and y coordinates of the relevant points which are located inside the superpixel
relevant_xs, relevant_ys = np.array([t[0] for t in relevant_points]), np.array([t[1] for t in relevant_points])
# 4.9) Convert the gray-scale image to BGR to be able to mark the relevant pixels in red
img_super_pixel_highlighted = cv2.cvtColor(img_super_pixel, cv2.COLOR_GRAY2BGR)
# 4.10) Highlight the relevant pixels
img_super_pixel_highlighted[tuple([relevant_xs, relevant_ys])] = (0, 0, 255)
cv2.imshow('Original Superpixels', img_super_pixel)
cv2.imshow('Relevant pixels highlighted', img_super_pixel_highlighted)
cv2.waitKey()
cv2.destroyAllWindows()
Output:
Original:
Highlighted:
Cheers!

Draw or resize plotted quantized image with nearest neighbour scaling

Following this example of K means clustering I want to recreate the same - only I'm very keen for the final image to contain just the quantized colours (+ white background). As it is, the colour bars get smooshed together to create a pixel line of blended colours.
Whilst they look very similar, the image (top half) is what I've got from CV2 it contains 38 colours total.
The lower image only has 10 colours and is what I'm after.
Let's look at a bit of that with 6 times magnification:
I've tried :
# OpenCV and Python K-Means Color Clustering
# build a histogram of clusters and then create a figure
# representing the number of pixels labeled to each color
hist = colour_utils.centroid_histogram(clt)
bar = colour_utils.plot_colors(hist, clt.cluster_centers_)
bar = cv2.resize(bar, (460, 345), 0, 0, interpolation = cv2.INTER_NEAREST)
However, the resize seems to have no resizing effect or change the scaling type. I don't know what controls the initial image size either.
Confused.
Any ideas?
I recommend you to show the image using cv2.imshow, instead of using matplotlib.
cv2.imshow shows the image "pixel to pixel" by default, while matplotlib.pyplot matches the image dimensions to the size of the axes.
bar_bgr = cv2.cvtColor(bar, cv2.COLOR_RGB2BGR) # Convert RGB to BGR
cv2.imshow('bar', bar_bgr)
cv2.waitKey()
cv2.destroyAllWindows()
In case you want to use matplotlib, take a look at: Display image with a zoom = 1 with Matplotlib imshow() (how to?).
Code used for testing:
# import the necessary packages
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import argparse
#import utils
import cv2
def centroid_histogram(clt):
# grab the number of different clusters and create a histogram
# based on the number of pixels assigned to each cluster
numLabels = np.arange(0, len(np.unique(clt.labels_)) + 1)
(hist, _) = np.histogram(clt.labels_, bins = numLabels)
# normalize the histogram, such that it sums to one
hist = hist.astype("float")
hist /= hist.sum()
# return the histogram
return hist
def plot_colors(hist, centroids):
# initialize the bar chart representing the relative frequency
# of each of the colors
bar = np.zeros((50, 300, 3), dtype = "uint8")
startX = 0
# loop over the percentage of each cluster and the color of
# each cluster
for (percent, color) in zip(hist, centroids):
# plot the relative percentage of each cluster
endX = startX + (percent * 300)
cv2.rectangle(bar, (int(startX), 0), (int(endX), 50),
color.astype("uint8").tolist(), -1)
startX = endX
# return the bar chart
return bar
# load the image and convert it from BGR to RGB so that
# we can dispaly it with matplotlib
image = cv2.imread('chelsea.png')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# show our image
plt.figure()
plt.axis("off")
plt.imshow(image)
# reshape the image to be a list of pixels
image = image.reshape((image.shape[0] * image.shape[1], 3))
# cluster the pixel intensities
clt = KMeans(n_clusters = 5)
clt.fit(image)
# build a histogram of clusters and then create a figure
# representing the number of pixels labeled to each color
hist = centroid_histogram(clt)
bar = plot_colors(hist, clt.cluster_centers_)
# show our color bart
#plt.figure()
#plt.axis("off")
#plt.imshow(bar)
#plt.show()
bar = cv2.resize(bar, (460, 345), 0, 0, interpolation = cv2.INTER_NEAREST)
bar_bgr = cv2.cvtColor(bar, cv2.COLOR_RGB2BGR) # Convert RGB to BGR
cv2.imshow('bar', bar_bgr)
cv2.waitKey()
cv2.destroyAllWindows()

Remove Freckles from Simple Binary Image

I have the following NumPy array of a running man, which you can download here:
https://drive.google.com/file/d/1SfIEqGsBV_vA7iP4UjLdklLJlLdDzozL/view?usp=sharing
To display it, use this code:
import numpy as np
import matplotlib.pyplot as plt
# load data
data = np.load('running_man.npy')
# plot data
plt.imshow(data)
As you can see there is a lot of noise (freckles) in the image. I would like to get rid of it and retrieve a clean image of the runner. Any idea of how to do it?
This is what I have done so far:
from skimage import measure
# Find contours at a constant value of 1
contours = measure.find_contours(data, 1, fully_connected='high')
# Select the largest contiguous contour
contour = sorted(contours, key=lambda x: len(x))[-1]
# Create an empty image to store the masked array
r_mask = np.zeros_like(data, dtype='bool')
# Create a contour image by using the contour coordinates rounded to their nearest integer value
r_mask[np.round(contour[:, 0]).astype('int'), np.round(contour[:, 1]).astype('int')] = 1
# Fill in the hole created by the contour boundary
r_mask = ndimage.binary_fill_holes(r_mask)
# Invert the mask since one wants pixels outside of the region
r_mask = ~r_mask
plt.imshow(r_mask)
... but as you can see the outline is very rough !
What works well is to upload the image to an online jpg to SVG converter -> this makes the lines super smooth. ... but I want to be able to do it in python.
Idea:
I am looking for something that can keep the sharp corners, maybe something that detects the gradient along the edge and only keeps the point where the gradient is above a certain threshold...
For this specific image you can just use numpy:
import numpy as np
import matplotlib.pyplot as plt
data = np.load('running_man.npy')
data[data > 1] = 0
plt.xticks([])
plt.yticks([])
plt.imshow(data)
For a method that preserves the corners better, we can use median filters, but force the preservation of corners.
Masked Image
Mask after filtering
Recolored
import cv2
import numpy as np
# load image
img = cv2.imread("run.png");
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
# make mask
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU);
# median filter
med = cv2.medianBlur(thresh, 11);
med[thresh == 255] = 255;
# inverse filter
mask = cv2.bitwise_not(med);
med = cv2.medianBlur(mask, 3);
med[mask == 255] = 255;
# recolor
color = np.zeros_like(img);
color[med == 0] = (66, 239, 245);
color[med == 255] = (92, 15, 75);
# show
cv2.imshow("colored", color);
cv2.waitKey(0);

How to find the document edges in various coloured backgrounds using opencv python? [Document Scanning in various backgrounds]

I am currently have a document that needs to be smart scanned.
For that, I need to find proper contours of the document in any background so that I can do a warped perspective projection and detection with that image.
The main issue faced while doing this is that the document edge detects any kind of background.
I have tried to use the function HoughLineP and tried to find contours on the grayscale blurred image passed through canny edge detection until now.
MORPH = 9
CANNY = 84
HOUGH = 25
IM_HEIGHT, IM_WIDTH, _ = rescaled_image.shape
# convert the image to grayscale and blur it slightly
gray = cv2.cvtColor(rescaled_image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7,7), 0)
#dilate helps to remove potential holes between edge segments
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(MORPH,MORPH))
dilated = cv2.dilate(gray, kernel)
# find edges and mark them in the output map using the Canny algorithm
edged = cv2.Canny(dilated, 0, CANNY)
test_corners = self.get_corners(edged)
approx_contours = []
(_, cnts, hierarchy) = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:5]
# loop over the contours
for c in cnts:
# approximate the contour
approx = cv2.approxPolyDP(c, 80, True)
if self.is_valid_contour(approx, IM_WIDTH, IM_HEIGHT):
approx_contours.append(approx)
break
How to find a proper bounding box around the document via OpenCV code.
Any help will be much appreciated.
(The document is taken from the camera in any angle and any coloured background.)
Following code might help you to detect/segment the page in the image...
import cv2
import matplotlib.pyplot as plt
import numpy as np
image = cv2.imread('test_p.jpg')
image = cv2.imread('test_p.jpg')
print(image.shape)
ori = image.copy()
image = cv2.resize(image, (image.shape[1]//10,image.shape[0]//10))
Resized the image to make the operations more faster so that we can work on realtime..
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (11,11), 0)
edged = cv2.Canny(gray, 75, 200)
print("STEP 1: Edge Detection")
plt.imshow(edged)
plt.show()
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts[1], key = cv2.contourArea, reverse = True)[:5]
Here we will consider only first 5 contours from the sorted list based on area
Here the size of the gaussian blur is bit sensitive, so chose it accordingly based on the image size.
After the above operations image may look like..
for c in cnts:
### Approximating the contour
#Calculates a contour perimeter or a curve length
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.01 * peri, True)
# if our approximated contour has four points, then we
# can assume that we have found our screen
screenCnt = approx
if len(approx) == 4:
screenCnt = approx
break
# show the contour (outline)
print("STEP 2: Finding Boundary")
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)
image_e = cv2.resize(image,(image.shape[1],image.shape[0]))
cv2.imwrite('image_edge.jpg',image_e)
plt.imshow(image_e)
plt.show()
Final Image may look like...
Rest of the things may be handled after getting the final image...
Code Reference :- Git Repository
I guess this answer would be helpful...
There is a similar problem which is called orthographic projection.
Orthographic approaches
Rather than doing, Gaussian blur+morphological operation to get the edge of the document, try to do orthographic projection first and then find contours via your method.
For fining proper bounding box, try some preset values or a reference letter after which an orthographic projection will allow you to compute the height and hence the dimensions of the bounding box.

Opencv plot black pixels on a image

I have a input image similar to
I am referring to:
How to fill the gaps in letters after Canny edge detection
I want to plot black pixels on this image. The proposed solution on the above url is first find all black pixels using
import matplotlib.pyplot as pp
import numpy as np
image = pp.imread(r'/home/cris/tmp/Zuv3p.jpg')
bin = np.all(image<100, axis=2)
My question is dow do I plot this black pixels (data stored in bin ) on image while ignoring all other colour channels.
In the answer is stated that np.all(image<100, axis=2) is used to select pixels where R,G and B are all lower then 100, which is basically color separation. Personally, I like to use the HSV-colorspace for that.
Result:
Note: if you want to improve the green letters, it is best to create a separate mask for that, and tweak the hsv values for green.
Code:
import numpy as np
import cv2
# load image
img = cv2.imread("img.jpg")
# Convert BGR to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# define range of black color in HSV
lower_val = np.array([0,0,0])
upper_val = np.array([179,255,127])
# Threshold the HSV image to get only black colors
mask = cv2.inRange(hsv, lower_val, upper_val)
# invert mask to get black symbols on white background
mask_inv = cv2.bitwise_not(mask)
# display image
cv2.imshow("Mask", mask_inv)
cv2.imshow("Img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Resources