It is easy for human eyes to tell black from other colors. But how about computers?
I printed some color blocks on the normal A4 paper. Since there are three kinds of ink to compose a color image, cyan, magenta and yellow, I set the color of each block C=20%, C=30%, C=40%, C=50% and rest of two colors are 0. That is the first column of my source image. So far, no black (K of CMYK) ink is supposed to print. After that, I set the color of each dot K=100% and rest colors are 0 to print black dots.
You may feel my image is weird and awful. In fact, the image is magnified 30 times and how the ink cheat our eyes can be seen clearly. The color strips hamper me to recognize these black dots (the dot is printed as just one pixel in 800 dpi). Without the color background, I used to blur and do canny edge detector to extract the edge. However, when adding color background, simply do grayscale and edge detector cannot get good results because of the strips. How will my eyes do in order to solve such problems?
I determined to check the brightness of source image. I referred this article and formula:
brightness = sqrt( 0.299 R * R + 0.587 G * G + 0.114 B * B )
The brightness is more close to human perception and it works very well in the yellow background because the brightness of yellow is the highest compared with cyan and magenta. But how to make cyan and magenta strips as bright as possible? The expected result is that all the strips disappear.
More complicated image:
C=40%, M=40%
C=40%, Y=40%
Y=40%, M=40%
FFT result of C=40%, Y=40% brightness image
Anyone can give me some hints to remove the color strips?
#natan I tried FFT method you suggested me, but I was not lucky to get peak at both axis x and y. In order to plot the frequency as you did, I resized my image to square.
I would convert the image to the HSV colour space and then use the Value channel. This basically separates colour and brightness information.
This is the 50% cyan image
Then you can just do a simple threshold to isolate the dots.
I just did this very quickly and im sure you could get better results. Maybe find contours in the image and then remove any contours with a small area, to filter any remaining noise.
After inspecting the images, I decided that a robust threshold will be more simple than anything. For example, looking at the C=40%, M=40% photo, I first inverted the intensities so black (the signal) will be white just using
im=(abs(255-im));
we can inspect its RGB histograms using this :
hist(reshape(single(im),[],3),min(single(im(:))):max(single(im(:))));
colormap([1 0 0; 0 1 0; 0 0 1]);
so we see that there is a large contribution to some middle intensity whereas the "signal" which is now white, is mostly separated to higher value. I then applied a simple thresholds as follows:
thr = #(d) (max([min(max(d,[],1)) min(max(d,[],2))])) ;
for n=1:size(im,3)
imt(:,:,n)=im(:,:,n).*uint8(im(:,:,n)>1.1*thr(im(:,:,n)));
end
imt=rgb2gray(imt);
and got rid of objects smaller than some typical area size
min_dot_area=20;
bw=bwareaopen(imt>0,min_dot_area);
imagesc(bw);
colormap(flipud(bone));
here's the result together with the original image:
The origin of this threshold is from this code I wrote that assumed sparse signals in the form of 2-D peaks or blobs in a noisy background. By sparse I meant that there's no pile up of peaks. In that case, when projecting max(image) on the x or y axis (by (max(im,[],1) or (max(im,[],1) you get a good measure of the background. That is because you take the minimal intensity of the max(im) vector.
If you want to look at this differently you can look at the histogram of the intensities of the image. The background is supposed to be a normal distribution of some kind around some intensity, the signal should be higher than that intensity, but with much lower # of occurrences. By finding max(im) of one of the axes (x or y) you discover what was the maximal noise level.
You'll see that the threshold picks that point in the histogram where there are still some noise above it, but ALL the signal is above it too. that's why I adjusted it to be 1.1*thr. Last, there are many fancier ways to obtain a robust threshold, this is a quick and dirty way that in my view is good enough...
Thanks to everyone for posting his answer! After some search and attempt, I also come up with an adaptive method to extract these black dots from the color background. It seems that considering only the brightness could not solve the problem perfectly. Therefore natan's method which calculates and analyzes the RGB histogram is more robust. Unfortunately, I still cannot obtain a robust threshold to extract the black dots in other color samples, because things are getting more and more unpredictable when we add deeper color (e.g. Cyan > 60) or mix two colors together (e.g. Cyan = 50, Magenta = 50).
One day, I google "extract color" and TinEye's color extraction and color thief inspire me. Both of them are very cool application and the image processed by the former website is exactly what I want. So I determine to implement a similar stuff on my own. The algorithm I used here is k-means clustering. And some other related key words to search may be color palette, color quantation and getting dominant color.
I firstly apply Gaussian filter to smooth the image.
GaussianBlur(img, img, Size(5, 5), 0, 0);
OpenCV has kmeans function and it saves me a lot of time on coding. I modify this code.
// Input data should be float32
Mat samples(img.rows * img.cols, 3, CV_32F);
for (int i = 0; i < img.rows; i++) {
for (int j = 0; j < img.cols; j++) {
for (int z = 0; z < 3; z++) {
samples.at<float>(i + j * img.rows, z) = img.at<Vec3b>(i, j)[z];
}
}
}
// Select the number of clusters
int clusterCount = 4;
Mat labels;
int attempts = 1;
Mat centers;
kmeans(samples, clusterCount, labels, TermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS, 10, 0.1), attempts, KMEANS_PP_CENTERS, centers);
// Draw clustered result
Mat cluster(img.size(), img.type());
for (int i = 0; i < img.rows; i++) {
for(int j = 0; j < img.cols; j++) {
int cluster_idx = labels.at<int>(i + j * img.rows, 0);
cluster.at<Vec3b>(i, j)[0] = centers.at<float>(cluster_idx, 0);
cluster.at<Vec3b>(i, j)[1] = centers.at<float>(cluster_idx, 1);
cluster.at<Vec3b>(i, j)[2] = centers.at<float>(cluster_idx, 2);
}
}
imshow("clustered image", cluster);
// Check centers' RGB value
cout << centers;
After clustering, I convert the result to grayscale and find the darkest color which is more likely to be the color of the black dots.
// Find the minimum value
cvtColor(cluster, cluster, CV_RGB2GRAY);
Mat dot = Mat::zeros(img.size(), CV_8UC1);
cluster.copyTo(dot);
int minVal = (int)dot.at<uchar>(dot.cols / 2, dot.rows / 2);
for (int i = 0; i < dot.rows; i += 3) {
for (int j = 0; j < dot.cols; j += 3) {
if ((int)dot.at<uchar>(i, j) < minVal) {
minVal = (int)dot.at<uchar>(i, j);
}
}
}
inRange(dot, minVal - 5 , minVal + 5, dot);
imshow("dot", dot);
Let's test two images.
(clusterCount = 4)
(clusterCount = 5)
One shortcoming of the k-means clustering is one fixed clusterCount cannot be applied to every image. Also clustering is not so fast for larger images. That's the issue annoys me a lot. My dirty method for better real time performance (on iPhone) is to crop 1/16 of the image and cluster the smaller area. Then compare all the pixels in the original image with each cluster center, and pick the pixel that are the nearest to the "black" color. I simply calculate euclidean distance between two RGB colors.
A simple method is to just threshold all the pixels. Here is this idea expressed in pseudo code.
for each pixel in image
if brightness < THRESHOLD
pixel = BLACK
else
pixel = WHITE
Or if you're always dealing with cyan, magenta and yellow backgrounds then maybe you might get better results with the criteria
if pixel.r < THRESHOLD and pixel.g < THRESHOLD and pixel.b < THRESHOLD
This method will only give good results for easy images where nothing except the black dots is too dark.
You can experiment with the value of THRESHOLD to find a good value for your images.
I suggest to convert to some chroma-based color space, like LCH, and adjust simultaneous thresholds on lightness and chroma. Here is the result mask for L < 50 & C < 25 for the input image:
Seems like you need adaptive thresholds since different values work best for different areas of the image.
You may also use HSV or HSL as a color space, but they are less perceptually uniform than LCH, derived from Lab.
Related
I try to remove the white annotations of this image (the numbers and arrows), as well as the black grid, with MATLAB:
I tried to compute, for each pixel, the mode of neighbors, but this process is very slow and I get poor results.
How can I obtain an image like this one?
Thank you for your time.
The general name for such a task is inpainting. If you search for that you will find better methods than what I'm showing here. This is no more than a proof of concept. I'm using DIPimage 3 (because I'm an author and it's easy for me to use).
First we need to create a mask for the regions that we want to remove (inpaint). It is easy to find pixels where all three channels have a high value (white) or a low value (black):
img = readim('https://i.stack.imgur.com/16r9N.png');
% Find a mask for the areas to remove
whitemask = min(img,'tensor') > 50;
blackmask = max(img,'tensor') < 30;
mask = whitemask | blackmask;
This mask doesn't capture all of the black grid, if we increase the threshold we will also remove the dark region of sea off the coast of Spain. And it also captures the white outline of the coasts. We can do a little bit better than this with some additional filtering:
% Find a mask for the areas to remove
whitemask = min(img,'tensor') > 50;
whitemask = whitemask - pathopening(whitemask,50);
blackmask = max(img,'tensor');
blackmask2 = blackmask < 80;
blackmask2 = blackmask2 - areaopening(blackmask2,6);
blackmask = blackmask < 30 | blackmask2;
mask = whitemask | blackmask;
This produces the following mask:
Still far from perfect, but a good start for our proof of concept.
One simple inpainting method uses normalized convolution: using the inverse of the mask we made, convolve the image multiplied by the mask, and convolve the mask separately. The ratio of these two results is a smoothed image that doesn't take the masked pixels into account. Finally, we replace the pixels in the original image under the mask with the values from this normalized convolution:
% Solution 1: normalized convolution
smooth = gaussf(img * ~mask, 2) / gaussf(~mask, 2);
img(mask) = smooth(mask);
An alternative solution applies a closing on the image multiplied by the mask (note that this multiplication makes the pixels we don't want completely black; the closing will spread the surrounding colors over the black areas):
% Solution 2: morphology
smooth = iterate('closing',img * ~mask, 13);
img(mask) = smooth(mask);
I'm working in project that basically I have to detect the threes on image and delete the other information. I used HSV as segmentation and the function regionprops to detect each element. It works fine, but in same cases that has house roofs, they aren't deleted because the value of Hue is similar to the threes. So far, this is the result:
To remove the roofs, I thought that maybe is possible detecting the color green in each region detected. If the region dont have 70% of green (for example) that region is deleted. How can I do that? How Can I detect only the green color of the image?
Solution Explanation
Evaluating the level of green in a patch is an interesting idea. I suggest the following approach:
convert your patches from RGB to HSV color system. In the HSV color system it is easier to evaluate the hue (or - the color) of each pixel, by examining the first channel.
Find the range for green color in the hue system. In our case it is about [65/360,170/360], as can be seen here:
for each patch, calculate how many pixels have the hue value which is in the green range, and divide by the size of the connected component.
Code Expamle
The following function evaluate the "level of greenness" in a patch:
function [ res ] = evaluateLevelOfGreen( rgbPatch )
%EVALUATELEVELOFGREEN Summary of this function goes here
% Detailed explanation goes here
%determines the green threshold in the hue channel
GREEN_RANGE = [65,170]/360;
INTENSITY_T = 0.1;
%converts to HSV color space
hsv = rgb2hsv(rgbPatch);
%generate a region of intereset (only areas which aren't black)
relevanceMask = rgb2gray(rgbPatch)>0;
%finds pixels within the specified range in the H and V channels
greenAreasMask = hsv(:,:,1)>GREEN_RANGE(1) & hsv(:,:,1) < GREEN_RANGE(2) & hsv(:,:,3) > INTENSITY_T;
%returns the mean in thie relevance mask
res = sum(greenAreasMask(:)) / sum(relevanceMask(:));
end
Results
When using on green patches:
greenPatch1 = imread('g1.PNG');
evaluateLevelOfGreen(greenPatch1)
greenPatch2 = imread('g2.PNG');
evaluateLevelOfGreen(greenPatch2)
greenPatch3 = imread('g3.PNG');
evaluateLevelOfGreen(greenPatch3)
Results:
ans = 0.8230
ans = 0.8340
ans = 0.6030
when using on non green patches:
nonGreenPatch1 = imread('ng1.PNG');
evaluateLevelOfGreen(nonGreenPatch1)
result:
ans = 0.0197
I am doing a research for my higher studies in automation. I have done the automation part of the microscope but I need help in MATLAB. An example of what I would like to segment is shown here:
I need to extract the dark purple pixels from this image and only display that in a figure. It is almost like colour based segmentation but I just want to only take the dark purple pixel from the whole image.
What would I do in this case?
Here's something to get you started. Let's go with the theme of colour segmentation where you only want to extract pixels that are of a deep purple. I would like to point you to the HSV colour space before we get started. The HSV colour space is ideal for representing colours in a way that is most intuitive to humans. We tend to describe colours by their dominant colour, followed by attributes such as how washed out or how pure the colour is, and how bright or dark the colour is. The dominant colour is represented by the Hue, the appearance of how washed out or how pure the colour is is represented by the Saturation and the intensity of the colour is represented by the Value, and hence Hue-Saturation-Value, or the HSV colour space.
We can transform a RGB image so that it becomes HSV by rgb2hsv. This will return a 3D matrix that has the hue, saturation and value as 2D slices in a 3D matrix, much like a RGB image where each slices represents the red, green and blue channels. Let's see what each component looks like once we transform the image into HSV:
im = imread('https://www.cdc.gov/dpdx/images/malaria/ovale/Po_gametocyte_thickB.jpg');
hsv = rgb2hsv(im2double(im));
figure;
for idx = 1 : 3
subplot(1,3,idx);
imshow(hsv(:,:,idx));
end
The first line of code reads in an image from a URL. I'm going to use the one that Hoki referred you to, as it's the most simplest one to deal with. For self-containment, this is what the original image looks like:
Once we do this, we convert the image into the HSV colour space. It is important that you convert the image to double precision and you normalize each component to [0,1], and that is performed by im2double. Next, we spawn a new figure, and place each component in a single row over three columns. The first column represents the hue, next column the saturation and finally the last column being the value. This is the figure that we see:
With the first figure, it looks like the dominant colour is purple, whether it's a light shade or a dark shade of the colour, so the hue won't help us here. If you look at a HSV colour wheel:
(source: hobbitsandhobos.com)
Normalize the wheel so that it falls between [0,1] instead of 0 to 360 degrees. The hue is actually represented as degrees due to the nature of the colour space, but MATLAB normalizes this to [0,1]. You can see that purple falls within a hue of [0.6,0.8], which corresponds to the first figure I showed you that displays the hue for our image. If you examine the pixels around the image, they fluctuate between this range. Therefore, the hue won't help us much here.
What will certainly help us are the saturation and value components. If you take a look, the deep purple pixels have a higher saturation than the rest of the background, which makes sense because the deep purple has a much more pure version of purple than the rest of the background. For the value, you can see that the brightness of the dark purple is darker than the background.
We can use these two points as an exploit to segment out the purple colour in the image. The easiest thing to do would be to threshold the saturation and value planes so that any values that are within a certain range you keep while those that are outside you throw away. Therefore, you can do something like this:
sThresh = hsv(:,:,2) > 0.6 & hsv(:,:,2) < 0.9;
vThresh = hsv(:,:,3) > 0.4 & hsv(:,:,3) < 0.65;
I used impixelinfo and I hovered my mouse over the saturation and value components to examine what the values were for the deep purple regions. It looks like those pixels that are deep purple have a saturation value between 0.6 and 0.9, while the value component has values between 0.4 and 0.65. The above code will create two binary masks where true means that the pixel satisfies our criteria while false means it doesn't. Because I want to combine both things together and not leave any stone unturned, let's logical OR the masks together for the final result:
figure;
result = sThresh | vThresh;
imshow(result);
We will also show the result too. This is what we get:
As you can see, this does a pretty good job, but we have remnants of the red arrow that we don't want in the final result. To do a bit of cleanup, we can use morphology - specifically an opening filter of a small window so that we don't affect the pixels that we want as much. We can use imopen to perform our opening operation for us. A morphological opening removes isolated pixels that appear around your image. You use what is called a structuring element that is used to look at local neighbourhoods of your image. For the basics, any pixel regions that are as small as the shape that is contained within the structuring element get removed. Because we want to preserve the shape of the other objects, we can try using a 5 x 5 disk structuring element to clean these pixels up:
figure;
se = strel('disk', 2, 0);
final = imopen(result, se);
imshow(final);
This is what we get:
Not bad! There are some holes that we need to patch up, so let's fill in those holes with imfill:
figure;
final_noholes = imfill(final, 'holes');
imshow(final_noholes);
This is what we get:
OK! So we have our mask. The last thing we need to do is present the image so that you only show the deep purple colours from the original image, and nothing else. That can easily be achieved with bsxfun:
figure;
out = bsxfun(#times, im, uint8(final_noholes));
imshow(out);
The above operation takes your mask, and multiplies every pixel in your image by this mask. One small thing I'd like to point out is that the mask we found in the previous step needs to be cast to uint8, because bsxfun requires that the multiplication (or whatever operation you perform) need to be the same type. We replicate this mask in 3D so that you mask out the unwanted RGB pixels and only keep the ones you are looking for.
This is what we finally get:
As you can see, it isn't perfect, but it's certainly enough to get you started. Those thresholds are what are important, but with some very simple thresholding, I extracted most of the purple pixels out.
To make it easier for you, here's the code that I wrote above that can easily be copied and pasted into MATLAB for you to run:
clear all; close all; clc;
im = imread('https://www.cdc.gov/dpdx/images/malaria/ovale/Po_gametocyte_thickB.jpg');
hsv = rgb2hsv(im2double(im));
figure;
for idx = 1 : 3
subplot(1,3,idx);
imshow(hsv(:,:,idx));
end
sThresh = hsv(:,:,2) > 0.6 & hsv(:,:,2) < 0.9;
vThresh = hsv(:,:,3) > 0.4 & hsv(:,:,3) < 0.65;
figure;
result = sThresh | vThresh;
imshow(result);
figure;
se = strel('disk', 2, 0);
final = imopen(result, se);
imshow(final);
figure;
final_noholes = imfill(final, 'holes');
imshow(final_noholes);
figure;
out = bsxfun(#times, im, uint8(final_noholes));
imshow(out);
Good luck!
Try this:
function main
clc,clear
A = imread('https://www.cdc.gov/dpdx/images/malaria/ovale/Po_gametocyte_thickB.jpg');
subplot(1,2,1)
imshow(A)
RGB = [230 210 200]; % color you want
e = 40; % color shift
B = pix_in(A,RGB,e);
B = B + 255.*uint8(~B); % choosing white background
subplot(1,2,2)
imshow(B)
end
function B = pix_in(A,RGB,e)
% select specific pixels in image
% A - color image (3D matrix uint8)
% RGB - [R G B] - color to select
% e - color shift/deviation
A = double(A); % for same class operations (RGB - double)
[m, n, ~] = size(A);
RGB = reshape(RGB,1,1,3);
RGB = repmat(RGB,m,n,1); % creating 3D matrix
b = abs(A-RGB) < e; % logical 3D
b = sum(b,3) == 3; % if [R,G,B] of a pixel in range
B = A.*repmat(b,1,1,3); % selecting pixels those in range
B = uint8(B);
end
For exactly the same image
Opencv Code:
img = imread("testImg.png",0);
threshold(img, img_bw, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
Mat tmp;
img_bwR.copyTo(tmp);
findContours(tmp, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
// Get the moment
vector<Moments> mu(contours.size() );
for( int i = 0; i < contours.size(); i++ )
{ mu[i] = moments( contours[i], false );
}
// Display area (m00)
for( int i = 0; i < contours.size(); i++ )
{
cout<<mu[i].m00 <<endl;
// I also tried the code
//cout<<contourArea(contours.at(i))<<endl;
// But the result is the same
}
Matlab code:
Img = imread('testImg.png');
lvl = graythresh(Img);
bw = im2bw(Img,lvl);
stats = regionprops(bw,'Area');
for k = 1:length(stats)
Area = stats(k).Area; %m00
end
Any one has any thought on it? How to unify them? I think they use different methods to find contours.
I uploaded the test image at the link below so that someone who is interested in this can reproduce the procedure
It is a 100 by 100 small 8 bit grayscale image with only 0 and 255 pixel intensity. For simplicity, it only has one blob on it.
For OpenCV, the area of contour (image moment m00) is 609.5 (Very odd value)
For Matlab, the area of contour (image moment m00) is 763.
Thanks
Exist many different definitions of how contours should be extracted from binary image. For example it can be polygon that is the perimeter of white object in a binary image. If this definition was used by OpenCV, then areas of contours would be the same as areas of connected components found by Matlab. But this is not the case. Contour found by findContour() function is the polygon that connects centers of neighbor "edge pixels". Edge pixel is a white pixel that has black neighbor in N4 neighborhood.
Example: suppose you have an image whose size is 100x100 pixels. Every pixel above the diagonal is black. Every pixel below or on the diagonal is white (black triangle and white triangle). Exact separation polygon will have almost 200 vertexes at distance of 1 pixel: (0,0), (1,0), (1,1), (2,1), (2,2),.... (100,99), (100,100), (0,100). As you can see this definition is not very good from practical point of view. Polygon returned by OpenCV will have exactly 3 vertexes needed to define the triangle: (0,0), (99,99), (0,99). Its area is (99 x 99 / 2) pixels. It is not equal to number of white pixels. It is not even an integer. But this polygon is more practical than previous one.
Those are not the only possible definitions for polygon extraction. Many other definitions exist. Some of them (in my opinion) may be better than the one used by OpenCV. But this is the one that was implemented and used by a lot of people.
Currently there no effective workaround for your problem. If you want to get exactly same numbers from MATLAB and OpenCV you will have to draw the contours found by foundContours on some black image, and use function moments() on image. I know that upcoming OpenCV 3 have function that finds connected components but I didn't tried it myself.
I am looking for some ideas about an approach that will let me analyze an image, and determine how greenISH or brownISH or whiteISH it is... I am emphasizing ISH here because, I am interested in capturing ALL the shades of these colours. So far, I have done the following:
I have my UIImage, I have CGImageRef and I actually have the colour of the pixel itself (it's RGB and Alpha), what I don't know is how to quantify this, and determine all the green shades, blues, browns, yellows, purples etc... So, I can process each and every pixel, determine it's basic RGB, but I need some help in quantifying the colours it over a whole image.
Thanks for your ideas...
Alex.
One fairly good solution is to switch from RGB colour space to one of the Y colour spaces, such as YUV, YCrCb or any of those. In all cases the Y channel represents brightness and the other two channels together represent colour, relative to brightness. You probably want to factor brightness out, possibly with the caveat that all colours below a certain darkness are to be excluded, so getting Y separately is a helpful first step in itself.
Converting from RGB to YUV is achieved with a simple linear combination. Straight from Wikipedia and a thousand other sources:
y = 0.299*r + 0.587*g + 0.114*b;
u = -0.14713*r - 0.28886*g + 0.436*b;
v = 0.615*r - 0.51499*g - 0.10001*b;
Assuming you're keeping r, g and b in the range [0, 1], your first test might be:
if(y < 0.05)
{
// this colour is very dark, so it's considered to be as
// far as we allow from any colour we're interested in
}
To decide how close your colour then is to, say, green, work out the u and v components of the green you're interested in, as a proportion of the y:
r = b = 0;
g = 0;
y = 0.299*r + 0.587*g + 0.114*b = 0.587;
u = -0.14713*r - 0.28886*g + 0.436*b = -0.28886;
v = 0.615*r - 0.51499*g - 0.10001*b = -0.51499;
proportionOfU = u / y = -2.0479;
proportionOfV = v / y = -0.8773;
Subsequently, work out and compare the proportions of U and V for incoming colours and compare (e.g. with 2d planar distance) to those you've computed for the colour you're comparing to. Closer values are more similar. How you scale and use that metric depends on your application.
Notice that as y goes toward 0, the computed proportions become increasingly less precise because of the limited range of the input data, and are undefined when y is 0. Conceptually that's because all colours look exactly the same when there's no light on them. Checking that y is above at least a certain minimum value is the pragmatic way of working around this issue. This also means that you're not going to get sensible results if you try to say "how black is this picture?", though again that's because of the ambiguity between a surface that doesn't reflect any light and a surface that doesn't have any light falling upon it.