Some weeks ago someone posted me a problem on segmenting regions of an image by using color information. She also attached a sample source code for doing this task. In this post this source code is analyzed and we also create a much better and general solution.
The original source code is the following:
pic = imread('image.jpg'); for mm = 1 : size(pic, 1) for nn = 1 : size(pic, 2) if pic(mm, nn, 1) < 80 && pic(mm, nn, 2) > 80 && pic(mm, nn, 3) > 100 gsc = 0.3 * pic(mm, nn, 1) + 0.59 * pic(mm, nn, 2) + 0.11 * pic(mm, nn, 3); pic(mm, nn, :) = [gsc gsc gsc]; end end end
This code runs through each pixel of the image. If the red value of the actual pixel is low enough while having high green and blue values, the pixel is replaced by it's gray equivalent. Although this code sample works, we shall consider the following things:
- The size of the image is constant, it could be stored only once.
- There are two nested for loops, which may increase the run-time a lot.
- It would be better to calculate some kind of distance between the current and the filtered color and do the filtering based on this distance. This solution is easier to modify and more general as it will be explained later.
- If we have a closer look, this is a typical task, where the advantages of vectorization can be used.
If you are not familiar with vectorization, have a look at this post containing a simple example on vectorization.
Calculating some kind of distance between the current and the filtered colors is a better solution than using comparisons. Of course in the original code it is enough to have three comparisons, but this number may increase to six, if we want to check if the R, G and B values are in given ranges. For example:
% check if an RGB color is in the +/-30 range of rgb(100, 110, 120) R = 128; G = 100; B = 220; result = R > 70 && R < 130 && G > 80 && G < 140 && B > 90 && B < 150;
For now calculate a distance instead of comparisons:
% calculate distance between an RGB color and rgb(100, 110, 120) R = 128; G = 100; B = 220; distance = (R - 100) .^ 2 + (G - 110) .^ 2 + (B - 120) .^ 2; % decision based on distance result = distance < 100;
The distance is proportional of the distance of the colors in the Cartesian coordinate system, and we use it to make a decision if the pixel shall be turned to gray. This kind of distance can be calculated easily in a vectorized way, in addition modifying R, G and B values is easy too, we have to update three values only.
Let us build our vectorized solution step-by-step. The goal is to segment the yellow color and turn it to gray on the following image:
The first part of the solution:
% define R, G and B components of required color R = 210; G = 175; B = 125; % read image image = imread('debrecen-original.jpg'); % extract R, G and B channels channelR = image(:, :, 1); channelG = image(:, :, 2); channelB = image(:, :, 3); % calculate gray image gray = rgb2gray(image);
First define the color we want to filter. I used this online color picker tool to identify rgb(210, 175, 125). Then the image is read and the red, green and blue channels are extracted. Also we calculate the gray equivalent of the image: this will be used for replacing the yellow pixels by gray ones.
Calculate the distance for each pixels:
% calculate differences for each channels dR = channelR - R; dG = channelG - G; dB = channelB - B; % calculate overall distance from the given RGB color d = dR .^ 2 + dG .^ 2 + dB .^ 2; % create a mask by thresholding the differences mask = d < 2000;
We calculate the differences in a vectorized way. As we can see, no for cycles are needed, we simply subtract constants from the channels. This way we get the dR, dG and dB matrices holding the differences. Later these matrices are summed in d. Also we create a binary mask identifying that pixels, which are close enough to the desired color. Our mask looks like the following:
Now replace the original pixels by gray ones where mask is true:
% copy the gray values where the mask is true channelR(mask) = gray(mask); channelG(mask) = gray(mask); channelB(mask) = gray(mask); % copy channels into the RGB image image(:, :, 1) = channelR; image(:, :, 2) = channelG; image(:, :, 3) = channelB;
We use logical indexing to select and replace the appropriate pixels identified by mask in the R, G and B channels. The channels are the copied back into the image. The result is the following:
The yellow color of the church has been turned into gray.
Although the task was solved successfully, to tell the truth, RGB color space is not the best one for using in such tasks. In a later post we will have a look at the CIE-Lab color space, which is much more appropriate for segmenting colors.