I have an image of a circular-shaped mask, which is essentially a colored circle within a black image.
I want to remove all the blank space around the mask, such that the boundaries of the image align with the circle as such:
I've written up a script to do this by searching through every column and row until a pixel with a value greater than 0 appears. searching from left to right, right to left, top to bottom, and bottom to the top gets me the mask boundaries, allowing me to crop the original image. Here is the code:
ROWS, COLS, _ = img.shape
BORDER_RIGHT = (0,0)
BORDER_LEFT = (0,0)
right_found = False
left_found = False
# find borders of blank space for removal.
# left and right border
print('Searching for Right and Left corners')
for col in tqdm(range(COLS), position=0, leave=True):
for row in range(ROWS):
if left_found and right_found:
break
# searching from left to right
if not left_found and N.sum(img[row][col]) > 0:
BORDER_LEFT = (row, col)
left_found = True
# searching from right to left
if not right_found and N.sum(img[row][-col]) > 0:
BORDER_RIGHT = (row, img.shape[1] + (-col))
right_found = True
BORDER_TOP = (0,0)
BORDER_BOTTOM = (0,0)
top_found = False
bottom_found = False
# top and bottom borders
print('Searching for Top and Bottom corners')
for row in tqdm(range(ROWS), position=0, leave=True):
for col in range(COLS):
if top_found and bottom_found:
break
# searching top to bottom
if not top_found and N.sum(img[row][col]) > 0:
BORDER_TOP = (row, col)
top_found = True
# searching bottom to top
if not bottom_found and N.sum(img[-row][col]) > 0:
BORDER_BOTTOM = (img.shape[0] + (-row), col)
bottom_found = True
# crop left and right borders
new_img = img[:,BORDER_LEFT[1]: BORDER_RIGHT[1] ,:]
# crop top and bottom borders
new_img = new_img[BORDER_TOP[0] : BORDER_BOTTOM[0],:,:]
I was wondering whether there was a more efficient way to do this. With larger images, this can be quite time-consuming especially if the mask is relatively small with respect to the original image shape. thanks!
Assuming you have only this object inside the image, there are two ways to do this:
You can threshold the image, then use numpy.where to find all locations that are non-zero, then use numpy.min and numpy.max on the appropriate row and column locations that come out of numpy.where to give you the bounding rectangle.
You can first find the contour points of the object after you threshold with cv2.findContours. This should result in a single contour, so once you have these points you put this through cv2.boundingRect to return the top-left corner of the rectangle followed by the width and height of its extent.
The first method will work if there is a single object and efficiently at that. The second one will work if there is more than one object, but you have to know which contour the object of interest is in, then you simply index into the output of cv2.findContours and pipe this through cv2.boundingRect to get the rectangular dimensions of the object of interest.
However, the takeaway is that either of these methods is much more efficient than the approach you have proposed where you are manually looping over each row and column and calculating sums.
Pre-processing
These sets of steps are going to be common to both methods. In summary, we read in the image, then convert it to grayscale then threshold. I didn't have access to your original image so I read it in from Stack Overflow and cropped it so that the axes are not showing. This will apply to the second method as well.
Here's a reconstruction of your image where I've taken a snapshot.
First I'll read in the image directly from the Internet as well as import the relevant packages I need to get the job done:
import skimage.io as io
import numpy as np
import cv2
img = io.imread('https://i.stack.imgur.com/dj1a8.png')
Thankfully, Scikit image has a method that reads in images directly from the Internet: skimage.io.imread.
After, I'm going to convert the image to grayscale, then threshold it:
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
im = img_gray > 40
I use OpenCV's cv2.cvtColor to convert the image from colour to grayscale. After, I threshold the image so that any intensity above 40 is set to True and everything else is set to False. The threshold of 40 I chose by trial and error until I get a mask that appeared to be circular. Taking a look at this image we get:
Method #1
As I illustrated above, use numpy.where on the thresholded image, then use numpy.min and numpy.max find the appropriate top-left and bottom-right corners and crop the image:
(r, c) = np.where(im == 1)
min_row, min_col = np.min(r), np.min(c)
max_row, max_col = np.max(r), np.max(c)
im_crop = img[min_row:max_row+1, min_col:max_col+1]
numpy.where for a 2D array will return a tuple of row and column locations that are non-zero. If we find the minimum row and column location, that corresponds to the top-left corner of the bounding rectangle. Similarly, the maximum row and column location corresponds to the bottom-right corner of the bounding rectangle. What's nice is that numpy.min and numpy.max work in a vectorised fashion, meaning that it operates on entire NumPy arrays in a single sweep. This logic is used above, then we index into the original colour image to crop out the range of rows and columns that contain the object of interest. im_crop contains that result. Note that the maximum row and column needs to be added with 1 when we're indexing as slicing with the end indices are exclusive so adding with 1 ensures we include the pixel locations at the bottom right corner of the rectangle.
We therefore get:
Method #2
We will use cv2.findContours to find all contour points of all objects in the image. Because there's a single object, only one contour should result, so we use this contour to pipe into cv2.boundingRect to find the top-left corner of the bounding rectangle of the object, combined with its width and height to crop out the image.
cnt, _ = cv2.findContours(im.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
x, y, w, h = cv2.boundingRect(cnt[0])
im_crop = img[y:y+h, x:x+w]
Take note that we have to convert the thresholded image into unsigned 8-bit integer, as that is the type that the function is expecting. Furthermore, we use cv2.RETR_EXTERNAL as we only want to retrieve the coordinates of the outer perimeter of any objects we see in the image. We also use cv2.CHAIN_APPROX_NONE to return every possible contour point on the object. The cnt is a list of contours that was found in the image. The size of this list should only be 1, so we index into this directly and pipe this into cv2.boundingRect. We then use the top-left corner of the rectangle, combined with its width and height to crop out the object.
We therefore get:
Full Code
Here's the full code listing from start to finish. I've left comments below to delineate what methods #1 and #2 are. For now, method #2 has been commented out, but you can decide whichever one you want to use by simply commenting and uncommenting the relevant code.
import skimage.io as io
import cv2
import numpy as np
img = io.imread('https://i.stack.imgur.com/dj1a8.png')
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
im = img_gray > 40
# Method #1
(r, c) = np.where(im == 1)
min_row, min_col = np.min(r), np.min(c)
max_row, max_col = np.max(r), np.max(c)
im_crop = img[min_row:max_row+1, min_col:max_col+1]
# Method #2
#cnt, _ = cv2.findContours(im.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
#x, y, w, h = cv2.boundingRect(cnt[0])
#im_crop = img[y:y+h, x:x+w]
I have an image that only contains a tiled shape in it with everywhere else black. However, this tiled pattern can be shifted/offset anywhere in the image particularly over the image borders. Knowing that this shape can be fit inside the image after offsetting it and leaving the borders black, how can I calculate how many pixels in x and y coordinates it needs to get offset for that to happen in an optimized way?
Input image
Desired output after offset/shiftimg
My thought was getting connected components in the image, check which labels are on the border, calculate the longest distance between each axis shapes that are on the border and offsetting in the axis' with those values. It can work but I feel like there should be smarter ways.
So here is the details of what I put in my comment for doing that with Python/OpenCV/Numpy. Is this what you want?
Read the input
Convert to gray
Threshold to binary
Count the number of white pixels in each column and store in array
Find the first and last black (zero count) element in the array
Get the center x values
Crop the image into left and right parts at the center x
Stack them together horizontally in the opposite order
Save the result
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('black_white.jpg')
hh, ww = img.shape[:2]
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]
# count number of white pixels in columns as new array
count = np.count_nonzero(thresh, axis=0)
# get first and last x coordinate where black (count==0)
first_black = np.where(count==0)[0][0]
last_black = np.where(count==0)[0][-1]
# compute x center
black_center = (first_black + last_black) // 2
print(black_center)
# crop into two parts
left = img[0:hh, 0:black_center]
right = img[0:hh, black_center:ww]
# combine them horizontally after swapping
result = np.hstack([right, left])
# write result to disk
cv2.imwrite("black_white_rolled.jpg", result)
# display it
cv2.imshow("RESULT", result)
cv2.waitKey(0)
I have some signal images:
As you can inspect, some of them contain color signals and some are just gray/black color signals.
My task is to extract pure signal with white background only. That means I need to remove all but signal in the image.
I checked that dash lines, dotted lines, solid lines (top and bottom) have the same RGB value that are close to 0;0;0 (ex: 0;0;0, 2;2;2; or 8;8;8) in terms of RGB.
Therefore, first thing that came to my mind was to access RGB values of each pixel and assign white color if all RGB values are the same. Using this heavy computation I can extract all color signals, because RGB values are never same for colors like red, blue, green (or their shades to some extent).
However, that process would remove signals where signal's pixel values are the same. That happens with mostly black color signals (the first two samples for example).
I also thought of extracting the signal if it keeps its horizontal and some vertical continuity, but to be honest I don't know how to write the code for it.
I am not asking any code solution to this challenge.
I would like to have different opinions on how I can successfully extract the original signal.
I am looking forward to having your ideas, insights and sources. Thanks
Note: All of my images (about 3k) are in one folder and I am going to apply one universal algorithm to accomplish the task.
You can find the horizontal and vertical lines using Hough transform.
After finding the lines, it's simple to erase them.
Removing the lines is only the first stage, but it looks like a good starting point...
Keeping the colored pixels (as you suggested) is also simple task.
You have mentioned you are not asking any code solution, but I decided to demonstrate my suggestion using MATLAB code:
close all
clear
origI = imread('I.png'); %Read image
I = imbinarize(rgb2gray(origI)); %Convert to binary
I = ~I; %Invert - the line color should be white.
%Apply hough transform: Find lines with angles very close to 0 degrees and with angles close to 90 degrees.
[H,theta,rho] = hough(I, 'RhoResolution', 1, 'Theta', [-0.3:0.02:0.3, -90:0.02:-89.7, 89.7:0.02:89.98]);
P = houghpeaks(H, numel(H), 'Threshold', 0.1, 'NHoodSize', [11, 1]); %Use low thresholds
lines = houghlines(I,theta,rho,P,'FillGap',25,'MinLength',200); %Fill large gaps and keep only the long lines.
%Plot the lines for debugging, and erase them by drawing black lines over them
J = im2uint8(I);
figure, imshow(I), hold on
for k = 1:length(lines)
xy = [lines(k).point1; lines(k).point2];
plot(xy(:,1),xy(:,2),'LineWidth',2,'Color','green');
% Plot beginnings and ends of lines
plot(xy(1,1),xy(1,2),'x','LineWidth',2,'Color','yellow');
plot(xy(2,1),xy(2,2),'x','LineWidth',2,'Color','red');
% Draw black line over each line.
J = insertShape(J, 'Line', [xy(1,1), xy(1,2), xy(2,1), xy(2,2)], 'Color', 'Black');
end
%Covert J image to binary (because MATLAB function insertShape returns RGB output).
J = imbinarize(rgb2gray(J));
figure, imshow(J)
%Color mask: 1 where color is not black or white.
I = double(origI);
C = (abs(I(:,:,1) - I(:,:,2)) > 20) | (abs(I(:,:,1) - I(:,:,3)) > 20) | (abs(I(:,:,2) - I(:,:,3)) > 20);
figure, imshow(C)
%Build a mask that combines "lines" mask and "color" mask.
Mask = J | C;
Mask = cat(3, Mask, Mask, Mask);
%Put white color where mask value is 0.
K = origI;
K(~Mask) = 255;
figure, imshow(K)
Detected lines:
Result after deleting lines:
Final result:
As you can see there are still leftovers.
I Applied a second iteration (same code) over the above result.
Result was improved:
You may try removing the leftovers using morphological operations.
It's going to be difficult without erasing the dashed graph.
Iterating all the PNG image files:
Place the code in an m file (MATLAB script file).
Place the m file in the same folder of the PNG image files.
Here is the code:
%ExtractSignals.m
close all
clear
%List all PNG files in the working directory (where ExtractSignals.m is placed).
imagefiles = dir('*.png');
nfiles = length(imagefiles);
result_images = cell(1, nfiles); %Allocate cell array for storing output images
for ii = 1:nfiles
currentfilename = imagefiles(ii).name; %PNG file name
origI = imread(currentfilename); %Read image
%Verify origI is in RGB format (just in case...)
if (size(origI, 3) ~= 3)
error([currentfilename, ' is not RGB image format!']);
end
I = imbinarize(rgb2gray(origI)); %Convert to binary
I = ~I; %Invert - the line color should be white.
%Apply hough transform: Find lines with angles very close to 0 degrees and with angles close to 90 degrees.
[H,theta,rho] = hough(I, 'RhoResolution', 1, 'Theta', [-0.3:0.02:0.3, -90:0.02:-89.7, 89.7:0.02:89.98]);
P = houghpeaks(H, numel(H), 'Threshold', 0.1, 'NHoodSize', [11, 1]); %Use low thresholds
lines = houghlines(I,theta,rho,P,'FillGap',25,'MinLength',200); %Fill large gaps and keep only the long lines.
%Plot the lines for debugging, and erase them by drawing black lines over them
J = im2uint8(I);
%figure, imshow(I), hold on
for k = 1:length(lines)
xy = [lines(k).point1; lines(k).point2];
%plot(xy(:,1),xy(:,2),'LineWidth',2,'Color','green');
% Plot beginnings and ends of lines
%plot(xy(1,1),xy(1,2),'x','LineWidth',2,'Color','yellow');
%plot(xy(2,1),xy(2,2),'x','LineWidth',2,'Color','red');
% Draw black line over each line.
J = insertShape(J, 'Line', [xy(1,1), xy(1,2), xy(2,1), xy(2,2)], 'Color', 'Black');
end
%Covert J image to binary (because MATLAB function insertShape returns RGB output).
J = imbinarize(rgb2gray(J));
%figure, imshow(J)
%Color mask: 1 where color is not black or white.
I = double(origI);
C = (abs(I(:,:,1) - I(:,:,2)) > 20) | (abs(I(:,:,1) - I(:,:,3)) > 20) | (abs(I(:,:,2) - I(:,:,3)) > 20);
%figure, imshow(C)
%Build a mask that combines "lines" mask and "color" mask.
Mask = J | C;
Mask = cat(3, Mask, Mask, Mask);
%Put white color where mask value is 0.
K = origI;
K(~Mask) = 255;
%figure, imshow(K)
%Second iteration - applied by "copy and paste" of the above code (it is recommended to use a function instead).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
origI = K; %Set origI to the result of the first iteration
I = imbinarize(rgb2gray(origI)); %Convert to binary
I = ~I; %Invert - the line color should be white.
%Apply hough transform: Find lines with angles very close to 0 degrees and with angles close to 90 degrees.
[H,theta,rho] = hough(I, 'RhoResolution', 1, 'Theta', [-0.3:0.02:0.3, -90:0.02:-89.7, 89.7:0.02:89.98]);
P = houghpeaks(H, numel(H), 'Threshold', 0.1, 'NHoodSize', [11, 1]); %Use low thresholds
lines = houghlines(I,theta,rho,P,'FillGap',25,'MinLength',200); %Fill large gaps and keep only the long lines.
%Plot the lines for debugging, and erase them by drawing black lines over them
J = im2uint8(I);
%figure, imshow(I), hold on
for k = 1:length(lines)
xy = [lines(k).point1; lines(k).point2];
% Draw black line over each line.
J = insertShape(J, 'Line', [xy(1,1), xy(1,2), xy(2,1), xy(2,2)], 'Color', 'Black');
end
%Covert J image to binary (because MATLAB function insertShape returns RGB output).
J = imbinarize(rgb2gray(J));
%figure, imshow(J)
%Color mask: 1 where color is not black or white.
I = double(origI);
C = (abs(I(:,:,1) - I(:,:,2)) > 20) | (abs(I(:,:,1) - I(:,:,3)) > 20) | (abs(I(:,:,2) - I(:,:,3)) > 20);
%figure, imshow(C)
%Build a mask that combines "lines" mask and "color" mask.
Mask = J | C;
Mask = cat(3, Mask, Mask, Mask);
%Put white color where mask value is 0.
K = origI;
K(~Mask) = 255;
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%Store result image in a cell array
result_images{ii} = K;
end
%Display all result images
for ii = 1:nfiles
figure;
imshow(result_images{ii});
title(['Processed ', imagefiles(ii).name]);
end
I am working on a handwriting recognition code for a school project. We want to collect the data our self, and I’m currently working on a program that scans a document with handwritten letters on it and create a own image for every letter. I cut the image to the exact size of the letter, resize the letter so every letter has the same dimensions and place them on a white background so they have the same dimensions but the original ratio stays the same. I already have it working quite well, the only problem is when I have a little bit of noise in the picture, it doesn’t work. I have the image (see attachment) and a list of all the pixels of the image. What would be a good way to cut the image to the image boundaries and not to the noise.
the code I use to cut the image:
def cut_to_edge(image, data, width, height):
left = width
right = 0
down = 0
up = height
for i in range(len(data)):
for j in range(len(data[i])):
if data[i][j] < 225:
if j < left:
left = j
if j > right:
right = j
if i < up:
up = i
if i > down:
down = i
letter = image.crop((left, up, right, down))
return letter
image is the image (obviously),
data is a 2 dimensional list with every pixel ([[row1][row2]etc.]),
width and height are the dimensions of the image
The image I need to cut,
how it should look,
How it looks now
If your noise is like little points, you can write a Median filter and that will solve your problem. The main idea of this filter is to loop through the image pixel by pixel and replace each pixel value with the median of the neighboring pixels.
But first you need to identify the type of noise you have and then apply the right filter.