Python - Find the first white pixel and last one in each column - python

how can i find first and last white pixel in a column of binary image . To i calculate distance between points in image
I did but everything is still not resolved , please help me . I have consulted some ways but it's really not as expected .

For a column, you should have something like col = [[p0_r, p0_g, p0_b], ..., [pn_r, pn_g, pn_b]], to find the first and the last white pixels, you should do something like
first_white = col.index([0, 0, 0])
rev = col.copy()
rev.reverse() # reverse the column order
last_white = len(col) - rev.index([0, 0, 0]) - 1

You maybe one something like that and I hope it's help you
import cv2
import numpy as np
frame = cv2.imread("//Path/To/Image.png")
color_pixel = []
# Find White so RGB = [255, 255, 255]
rgb_color_to_find = [0xFF, 0xFF, 0xFF]
for i in range(frame.shape[1]):
# Reverse color_to_find_rgb (2 -> 1 -> 0) cause
# OpenCV use BGR and not RGB
tmp_col = np.where(
(frame[:, i, 0] == rgb_color_to_find[2])
& (frame[:, i, 1] == rgb_color_to_find[1])
& (frame[:, i, 2] == rgb_color_to_find[0])
)
# If minimum 2 points are find
if len(tmp_col[0]) >= 2:
first_pt = (i, tmp_col[0][0])
last_pt = (i, tmp_col[0][-1])
distance = tmp_col[0][-1] - tmp_col[0][0]
color_pixel.append(((first_pt, last_pt), distance))
For gray image just change some thing and you get:
import cv2
import numpy as np
frame = cv2.imread("//Path/To/Image.png", cv2.IMREAD_GRAYSCALE)
color_pixel = []
# Find White in Gray so White = 255 (Max value for uint8)
color_to_find = 255
for i in range(frame.shape[1]):
tmp_col = np.where(frame[:, i] == color_to_find)
# If minimum 2 points are find
if len(tmp_col[0]) >= 2:
first_pt = (i, tmp_col[0][0])
last_pt = (i, tmp_col[0][-1])
distance = tmp_col[0][-1] - tmp_col[0][0]
color_pixel.append(((first_pt, last_pt), distance))

Related

What is the Fastest way to change pixel values of an RGB image using Numpy / Opencv / scikit-image

Given a binary image, what is the fastest and Pythonic way to convert the image to RGB and then modify it's pixels?
I have these two ways but they don't feel good to me
def get_mask(rgb_image_path):
mask = np.array(Image.open(rgb_image_path).convert('L'), dtype = np.float32) # Mask should be Grayscale so each value is either 0 or 255
mask[mask == 255.0] = 1.0 # whereever there is 255, convert it to 1: (1 == 255 == White)
return mask
def approach1(mask):
mask = np.logical_not(mask)
mask = mask.astype(np.uint8)
mask = mask*255
red = mask.copy()
blue = mask.copy()
green = mask.copy()
red[red == 0] = 26
blue[blue == 0] = 237
green[green == 0] = 160
mask = np.stack([red, blue, green], axis = -1)
plt.imshow(mask)
def approach2(mask):
mask = np.logical_not(mask)
mask = mask.astype(np.uint8)
mask = np.stack([mask,mask,mask], axis = -1)
mask = mask*255
width,height, channels = mask.shape
for i in range(width):
for j in range(height):
if mask[i][j][0] == 0:
mask[i][j] = (26,237,160)
plt.imshow(mask)
Below is the Image
I suppose the most simple way is this:
def mask_coloring(mask):
expected_color = (26, 237, 160)
color_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
color_mask[mask == 255.0, :] = expected_color
plt.imshow(color_mask)
By the way, similar approach can be found here.
The easiest way to do this is to take advantage of the fact that you only have and only want 2 colours and to use a fast, space-efficient palette image that doesn't require you to iterate over all the pixels, or store 3 bytes per pixel:
from PIL import Image
# Open your image
im = Image.open('oqX2g.gif')
# Create a new palette where the 1st entry is black (0,0,0) and the 2nd is your colour (26,237,160) then pad out to 256 RGB entries
palette = [0,0,0] + [26,237,160] + [0,0,0] * 254
# Put palette into image and save
im.putpalette(palette)
im.save('result.png')
Read more about palette images here.
What if you want the black background to become magenta, and the foreground green? Oh, and you want it as an RGB Numpy array?
Just change the palette and convert:
from PIL import Image
import numpy as np
# Open your image, push in new palette
im = Image.open('oqX2g.gif')
palette = [255,0,255] + [0,255,0] + [0,0,0] * 254
im.putpalette(palette)
# Make RGB Numpy array
na = np.array(im.convert('RGB'))

Edge Detection minimum line length?

I'm trying to filter out short lines from my canny edge detection. Here's what I'm currently using as well as a brief explanation:
I start out by taking a single channel of the image and running CV2's Canny edge detection. Following that, I scan through each pixel and detect if there are any around it that are white (True, 255). If it is, I add it to a group of true pixels and then check every pixel around it (and keep looping until there are no white/True pixels left. I then replace all the pixels with black/False if the group count is less than a designated threshold (In this case, 100 pixels).
While this works (as shown below) it's awfully slow. I'm wondering if there's a faster, easier way to do this.
import cv2
img = cv2.imread("edtest.jpg")
img_r = img.copy()
img_r[:, :, 0] = 0
img_r[:, :, 1] = 0
img_r = cv2.GaussianBlur(img_r, (3, 3), 0)
basic_edge = cv2.Canny(img_r, 240, 250)
culled_edge = basic_edge.copy()
min_threshold = 100
for x in range(len(culled_edge)):
print(x)
for y in range(len(culled_edge[x])):
test_pixels = [(x, y)]
true_pixels = [(x, y)]
while len(test_pixels) != 0:
xorigin = test_pixels[0][0]
yorigin = test_pixels[0][1]
if 0 < xorigin < len(culled_edge) - 1 and 0 < yorigin < len(culled_edge[0]) - 1:
for testx in range(3):
for testy in range(3):
if culled_edge[xorigin-1+testx][yorigin - 1 + testy] == 255 and (xorigin-1+testx, yorigin-1+testy) not in true_pixels:
test_pixels.append((xorigin-1+testx, yorigin-1+testy))
true_pixels.append((xorigin-1+testx, yorigin-1+testy))
test_pixels.pop(0)
if 1 < len(true_pixels) < min_threshold:
for i in range(len(true_pixels)):
culled_edge[true_pixels[i][0]][true_pixels[i][1]] = 0
cv2.imshow("basic_edge", basic_edge)
cv2.imshow("culled_edge", culled_edge)
cv2.waitKey(0)
Source Image:
Canny Detection and Filtered (Ideal) Results:
The operation you are applying is called an area opening. I don't think there is an implementation in OpenCV, but you can find one in either scikit-image (skimage.morphology.area_opening) or DIPlib (dip.BinaryAreaOpening).
For example with DIPlib (disclosure: I'm an author) you'd amend your code as follows:
import diplib as dip
# ...
basic_edge = cv2.Canny(img_r, 240, 250)
min_threshold = 100
culled_edge = dip.BinaryAreaOpening(basic_edge > 0, min_threshold)
The output, culled_edge, is now a dip.Image object, which is compatible with NumPy arrays and you should be able to use it as such in many situations. If there's an issue, then you can cast it back to a NumPy array with culled_edge = np.array(culled_edge).

How to convert binary grid images to 2D arrays?

I've got some images of binary (black and white) grids that look like this:
Now, I want to convert such images to regular 2D NumPy arrays, where each cell must correspond to 0, if the source cell is white (or uncolored) and 1 if the cell is black. That is, the expected output is:
[[0,1,0,0,1],
[0,0,0,0,1],
[0,1,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,1,0],
[0,0,1,0,0]]
I've looked at a number of suggestions including this one, but they don't say anything about how I must reduce the raw pixels to a regular grid.
My current code:
import numpy as np
from PIL import Image
def from_img(imgfile, size, keep_ratio=True, reverse=False):
def resample(img_, size):
return img.resize(size, resample=Image.BILINEAR)
def makebw(img, threshold=200):
edges = (255 if reverse else 0, 0 if reverse else 255)
return img.convert('L').point(lambda x: edges[1] if x > threshold else edges[0], mode='1')
img = Image.open(imgfile)
if keep_ratio:
ratio = max(size) / max(img.size)
size = tuple(int(sz * ratio) for sz in img.size)
return np.array(makebw(resample(img, size)), dtype=int)
This code might be ok for images that don't contain borders between the cells, and only when specifying the number of rows and columns manually. But I am sure there must be a way of automating this routine by edge detection / resampling techniques...
Update
While there are good solutions (see suggested below) for even, regular black and white grids like shown above, the task is more difficult for uneven, noisy images with multiple non-BW colors like this one:
I'm now looking at an opencv implementation that detects contours and tries to single out the cell size to reconstruct the grid matrix. My current code:
import matplotlib.pyplot as plt
import numpy as np
import cv2
def find_contours(fpath, gray_thresh=150, extent_param=0.85, area_param=(0.0003, 0.3), ratio_param=(0.75, 1.33)):
"""
Finds contours (shapes) in an image (loading it from a file) and filters the contours
according to a number of parameters.
gray_thresh: grayscale threshold
extent_param: minimum extent of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#extent)
area_param: min and max ratio of contour area to image area
ratio_param: min and max ratio of contour (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_properties/py_contour_properties.html#aspect-ratio)
"""
image = cv2.imread(fpath)
# grayscale image
imgray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(imgray, gray_thresh, 255, 0)
# get all contours (see https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# get min and max contour area in pixels (from given ratios)
if area_param:
area = imgray.shape[0] * imgray.shape[1]
min_area = float(area) * area_param[0]
max_area = float(area) * area_param[1]
# filtered contours
contours2 = []
# contour sizes
sizes = []
# contour coords
pos = []
# iterate by found contours
for c in contours:
# get contour area
c_area = cv2.contourArea(c)
# get bounding rect
rect = cv2.boundingRect(c)
# get extent (ratio of contour area to bounding rect area)
extent = float(c_area) / (rect[2] * rect[3])
# get aspect ratio of bounding rect
ratio = float(rect[2]) / rect[3]
# perform filtering (leave rect-shaped contours or filter by extent)
if (len(c) == 4 or not extent_param or extent >= extent_param) and \
(not area_param or (c_area >= min_area and c_area <= max_area)) and \
(not ratio_param or (ratio >= ratio_param[0] and ratio <= ratio_param[1])):
# add filtered contour to list, as well as its size and pos
contours2.append(c)
sizes.append(rect[-2:])
pos.append(rect[:2])
# get most frequent block size (w, h), first and last block
size_mode = max(set(sizes), key=sizes.count)
first_pos = min(pos)
last_pos = max(pos)
# return original image, grayscale image, most frequent contour size, first and last contour coords
return image, imgray, contours2, size_mode, first_pos, last_pos
def get_mean_colors_of_contours(img, imgray, contours):
"""
Returns the mean colors of given contours and one common mean.
"""
l_means = []
for c in contours:
mask = np.zeros(imgray.shape, np.uint8)
cv2.drawContours(mask, [c], 0, 255, -1)
l_means.append(cv2.mean(img, mask=mask)[0])
return np.mean(l_means), l_means
def get_color(x):
if x == 'r':
return (255, 0, 0)
elif x == 'g':
return (0, 255, 0)
elif x == 'b':
return (0, 0, 255)
return x
def text_in_contours(img, contours, values, val_format=None, text_color='b', text_scale=1.0):
"""
Prints stuff inside given contours.
img: original image (array)
contours: identified contours
values: stuff to print (iterable of same length as contours)
val_format: optional callback function to format a single value before printing
text_color: color of output text (default = blue)
text_scale: initial font scale (font will be auto adjusted)
"""
text_color = get_color(text_color)
if not text_color: return
for c, val in zip(contours, values):
rect = cv2.boundingRect(c)
center = (rect[0] + rect[2] // 2, rect[1] + rect[3] // 2)
txt = val_format(val) if val_format else str(val)
if not txt: continue
font = cv2.FONT_HERSHEY_DUPLEX
fontScale = min(rect[2:]) * text_scale / 100
lineType = 1
text_size, _ = cv2.getTextSize(txt, font, fontScale, lineType)
text_origin = (center[0] - text_size[0] // 2, center[1] + text_size[1] // 2)
cv2.putText(img, txt, text_origin, font, fontScale, text_color, lineType, cv2.LINE_AA)
return img
def draw_contours(fpath, contour_color='r', contour_width=1, **kwargs):
"""
Finds contours in image and draws their outlines.
fpath: path to image file
contour_color: color used to outline contours (r,g,b, tuple or None)
contour_width: outline width
kwargs: args passed to find_contours()
"""
if not contour_color: return
contour_color = get_color(contour_color)
img, imgray, contours, size_mode, first_pos, last_pos = find_contours(fpath, **kwargs)
cv2.drawContours(img, contours, -1, contour_color, contour_width)
return img, imgray, contours, size_mode, first_pos, last_pos
def show_image(img, fig_height_inches=8):
"""
Shows an image in iPython notebook.
"""
height, width = img.shape[:2]
aspect = width / height
fig = plt.figure(figsize=(fig_height_inches * aspect, fig_height_inches))
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(img, interpolation='nearest', aspect='equal')
plt.show()
Now this helps me already identify the white cells in most cases, e.g.
img, imgray, contours, size_mode, first_pos, last_pos = draw_contours('sss4.jpg')
mean_col, cols = get_mean_colors_of_contours(img, imgray, contours)
print(f'mean color = {mean_col}')
on_contour = lambda val: str(int(val)) if (val / mean_col) >= 0.9 else None
img = text_in_contours(img, contours, cols, on_contour)
show_image(img, 15)
Output
mean color = 252.54154936140293
So, I only need now some way to reconstruct the grid with ones and zeros, adding ones in the missing spots (where no white cells were identified).
Given that you have a very nice grid with a regular shape, we can figure out the size of each tile by randomly sampling around and checking the size of our flood-filled area.
I used the mode of the counts I received back from the sample, but if you know some of the grids have a lot of black tiles, then you should probably take the smallest size returned by stipple() since anytime we hit a black tile, it'll include the entire background of the image which could overwhelm the count of white tiles.
Once we have the size of our tile, we can use that to index a pixel from each tile and check if it's white or black.
import cv2
import numpy as np
import random
import math
# stipple search
def stipple(mask, iters):
# get resolution
height, width = mask.shape[:2];
# do random checks
counts = [];
for a in range(iters):
# get random position
copy = np.copy(mask);
x = random.randint(0, width-1);
y = random.randint(0, height-1);
# fill
cv2.floodFill(copy, None, (x, y), 100);
# count
count = np.count_nonzero(copy == 100);
counts.append(count);
return counts;
# load image
gray = cv2.imread("tiles.jpg", cv2.IMREAD_GRAYSCALE);
# mask
mask = cv2.inRange(gray, 100, 255);
height, width = mask.shape[:2];
# check
sizes = stipple(mask, 10);
print(sizes);
# get most common size // or search for the smallest size
size = max(set(sizes), key=sizes.count);
# get side size
side = math.sqrt(size);
# get grid dimensions
grid_width = int(round(width / side));
grid_height = int(round(height / side));
print([grid_width, grid_height]);
# recalculate size to nearest rounded whole number
side = int(width / grid_width);
print(side);
# make grid
grid = [];
start_index = int(side / 2.0);
for y in range(start_index, height, side):
row = [];
for x in range(start_index, width, side):
row.append(mask[y,x] == 255);
grid.append(row[:]);
# print
out_str = "";
for row in grid:
for elem in row:
out_str += str(int(elem));
out_str += "\n";
print(out_str);
# show
cv2.imshow("Mask", mask);
cv2.waitKey(0);
My idea would be convert the input image to mode '1', somehow detect the tiles' width and height, resize the input image w.r.t. these, and simply convert to some NumPy array.
Detecting the tiles' width and height might work like this:
Detect changes between neighbouring pixels using np.diff, and create a union image from these information:
Calculate the distances between these detected changes, again using np.diff, np.sum, and np.nonzero.
Finally, get the median value of these distances using np.median, and from that, determine the number of rows and columns of the grid, and resize the input image accordingly.
Here's the full code:
import numpy as np
from PIL import Image
# Open image, convert to black and white mode
image = Image.open('grid.png').convert('1')
w, h = image.size
# Temporary NumPy array of type bool to work on
temp = np.array(image)
# Detect changes between neighbouring pixels
diff_y = np.diff(temp, axis=0)
diff_x = np.diff(temp, axis=1)
# Create union image of detected changes
temp = np.zeros_like(temp)
temp[:h-1, :] |= diff_y
temp[:, :w-1] |= diff_x
# Calculate distances between detected changes
diff_y = np.diff(np.nonzero(np.diff(np.sum(temp, axis=0))))
diff_x = np.diff(np.nonzero(np.diff(np.sum(temp, axis=1))))
# Calculate tile height and width
ht = np.median(diff_y[diff_y > 1]) + 2
wt = np.median(diff_x[diff_x > 1]) + 2
# Resize image w.r.t. tile height and width
array = (~np.array(image.resize((int(w/wt), int(h/ht))))).astype(int)
print(array)
For the given input image, we get the desired/expected output:
[[0 1 0 0 1]
[0 0 0 0 1]
[0 1 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 1 0]
[0 0 1 0 0]]
Full black columns or rows don't matter:
[[0 1 0 0 1]
[0 0 0 0 1]
[0 1 0 0 1]
[0 0 0 0 1]
[0 0 0 0 1]
[0 0 0 1 1]
[0 0 1 0 1]]
And, even single white tiles are enough:
[[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 1 1 1 1]
[1 0 1 1 1]
[1 1 1 1 1]]
For testing, I thresholded your input image, and saved it as single-channel PNG. For arbitrary JPG input images, you might want to have some thresholding before converting to mode '1' to avoid artifacts.
----------------------------------------
System information
----------------------------------------
Platform: Windows-10-10.0.16299-SP0
Python: 3.9.1
PyCharm: 2021.1.1
NumPy: 1.20.2
Pillow: 8.2.0
----------------------------------------

Image blending with laplacian pyramids yields bad image

I'm trying to implement an image blending algorithm that receives two images
im1, im2 and a binary mask and blends the two image accroding to that mask.
I'm using the following formula:
https://i.stack.imgur.com/uxz34.png
to build the laplacian pyramid of the blended image and then I reconstruct it
to receive the final image.
my code:
mask = mask.astype(np.float64)
pyr_im1, filter_vec = build_laplacian_pyramid(im1, max_levels,
filter_size_im)
pyr_im2 = build_laplacian_pyramid(im2, max_levels,
filter_size_im)[0]
pyr_mask = build_gaussian_pyramid(mask, max_levels,
filter_size_mask)[0]
# build the blended laplacian pyramid
l_out = [np.multiply(pyr_mask[k], pyr_im1[k])
+ np.multiply(1 - pyr_mask[k], pyr_im2[k])
for k in range(len(pyr_im1))]
im_blend = laplacian_to_image(l_out, filter_vec, [1] * len(l_out))
im_blend = np.clip(im_blend, 0, 1) # clip values to the range of [0,1]
return im_blend
Where mask is a binary mask with values of 0 or 1, im1 and im2 are np.float64
images normalized to the range of (0,1). The functions build_laplacian_pyramid,
build_gaussian_pyramid and laplacian_to_image work perfectly-I have tested
them and made sure that they work properly. When I use this code to try and
blend two images I get something like this:
https://i.stack.imgur.com/0NlKv.png
Are there any apperent issues with my code that can come to mind?
Thanks in advance
Hi I'm not sure what exactly is wrong but I think you need to reverse the mask when applying the blend.
Here is my code that seems to work:
def laplacianSameSize(outerImage, innerImage, mask, levels):
gpCEye = gaussianPyramid(innerImage, levels)
lpCEye = laplacianPyramid(gpCEye)
gpFrame = gaussianPyramid(outerImage, levels)
lpFrame = laplacianPyramid(gpFrame)
gpMask = gaussianPyramid(mask, levels)
gpMask.reverse()
LS = []
#Appling the mask
for lFrame, lCEye, gMask in zip(lpFrame, lpCEye, gpMask):
lFrame[gMask == 255] = lCEye[gMask == 255]
LS.append(lFrame)
# now reconstruct
ls_ = LS[0]
for i in range(1,levels+1):
size = (LS[i].shape[1], LS[i].shape[0])
ls_ = cv2.pyrUp(ls_,dstsize=size)
ls_ = ls_ + LS[i]
#Making it above 0 before becoming uint8
bound(ls_)
return ls_.astype(np.uint8)
With functions
def laplacianPyramid(gp):
levels = len(gp) - 1
lp = [gp[levels]]
for i in range(levels, 0, -1):
size = (gp[i - 1].shape[1], gp[i - 1].shape[0])
GE = cv2.pyrUp(gp[i], dstsize=size)
L = gp[i - 1] - GE
lp.append(L)
return lp
def gaussianPyramid(img, num_levels):
lower = img.copy()
gp = [np.float32(lower)]
for i in range(num_levels):
lower = cv2.pyrDown(lower)
gp.append(np.float32(lower))
return gp
def bound(im):
im[im < 0] = 0
im[im > 255] = 255
I also make sure to use floats until the end then bounding it within 0 to 255.
Hope this helps!

How to find the median (R,G,B) pixel value of all skin pixels in an image?

How to get the median pixel value (median RGB) of all skin pixels in the image here?
https://i.imgur.com/qU1wW6s.jpg
I tried the following:
if __name__ == '__main__':
image = cv2.imread('skin.png')
COLOR1 = [0,0,0]
COLOR2 = [10,10,10]
image_copy = image.copy()
black_pixels_mask = np.all(image <= COLOR2, axis=-1)
non_black_pixels_mask = ~black_pixels_mask
image_array = image_copy[non_black_pixels_mask]
med = np.median(image_array, axis=0)
print(med)
But this does not seem to give the correct answer. Please help!
This what I found: (185.0, 155.0, 135.0)
Here is the code. Pay attention that split returns the color sequence b,g,r.
import cv2
import numpy as np
image = cv2.imread('skin.jpg')
def getMedianImageChannels(im):
b, g, r = cv2.split(im) # Split channels
# Remove zeros
b = b[b != 0]
g = g[g != 0]
r = r[r != 0]
# median values
b_median = np.median(b)
r_median = np.median(r)
g_median = np.median(g)
return r_median,g_median,b_median
median = getMedianImageChannels(image)
print(median)
Let me know if this is what you were looking for. I hope it helps!

Categories