I'm trying to replace a manual process done in PhotoShop with an automated process done on the server. Currently in PhotoShop the "Color Range" tool is used to select a range of colors using the "Fuzziness" factor and starting with either Black or White depending on the part of the process.
My initial approaches included both using thresholds for Luminescence in the L*a*b color space as well is DE94 between the candidate color and Black/White. In both cases I selected colors that shouldn't be selected and/or didn't select colors that should.
My hunch is that I should be using cones instead of spheres for my selection.
Can anyone give any insight into what PhotoShop is doing and if I'm heading the right direction? Also, if there is a library out there to do this that would be awesome I'm currently writing this in C.
From what I have seen in Photoshop, the algorithm could probably be similar to the following:
- Define a function that calculates the closeness of two colors: for example, use a Euclidian distance in the colorspace - that is, calculate the distance between the colors of the two pixels in the RGB space using the Euclidean distance formula.
- Next, adjust the intensity of each pixel by using a fallof function,
such as the Gaussian function. You will probably need to tweak
some parameters. To clarify: you calculate the distance of two
pixels in RGB space (not the distance in 2D pixel coordinates), and
then feed that into the falloff function which will provide a result
between 0.0 and 1.0. Multiply all color components of the current
pixel with the result of the falloff function for it. Do this for
each pixel of the image.
- If you want to add the range parameter of the effect, simply use the
same falloff function for each pixel again, but this time feed it
the Euclidean distance between the selected pixel and the current
pixel in the 2D space of pixels (the distance between pixel
coordinates on the image).
If you only want to select certain pixels, then instead of applying the effect directly at the pixels in the image you could store the falloff values in a matrix of double
s in range from 0.0 to 1.0. Then, choose a threshold value above which you will select the given pixel.
For example, if the step 2. for pixel at the coordinate (x, y) yielded 0.8 and the step 3. yielded 0.5, then the value of the matrix element with coordinates x and y should be 0.8*0.5=0.4
. If you picked a selection threshold below 0.4, you would select pixel (x, y), otherwise, you would not.
I don't know how photoshop does it under the hood, but this is a simple RGB as XYZ 3d vector approach:
rDelta = pixel.r - color.r
gDelta = pixel.g - color.g
bDelta = pixel.b - color.b
fuzziness = 0.1 // anything 0 to 1.0
maxDistance = fuzziness * 441 // max distance, black -> white
distance = Math.sqrt(rDelta * rDelta + gDelta * gDelta + bDelta * bDelta)
if (distance < maxDistance) includePixel()
else dontIncludePixel()
This is the pixel_difference function from the gimp source:
https://github.com/GNOME/gimp/blob/125cf2a2a3e1e85172af25871a2cda3638292fdb/app/core/gimpimage-contiguous-region.c#L290
static gfloat
pixel_difference (const gfloat *col1,
const gfloat *col2,
gboolean antialias,
gfloat threshold,
gint n_components,
gboolean has_alpha,
gboolean select_transparent,
GimpSelectCriterion select_criterion)
{
gfloat max = 0.0;
/* if there is an alpha channel, never select transparent regions */
if (! select_transparent && has_alpha && col2[n_components - 1] == 0.0)
return 0.0;
if (select_transparent && has_alpha)
{
max = fabs (col1[n_components - 1] - col2[n_components - 1]);
}
else
{
gfloat diff;
gint b;
if (has_alpha)
n_components--;
switch (select_criterion)
{
case GIMP_SELECT_CRITERION_COMPOSITE:
for (b = 0; b < n_components; b++)
{
diff = fabs (col1[b] - col2[b]);
if (diff > max)
max = diff;
}
break;
case GIMP_SELECT_CRITERION_R:
max = fabs (col1[0] - col2[0]);
break;
case GIMP_SELECT_CRITERION_G:
max = fabs (col1[1] - col2[1]);
break;
case GIMP_SELECT_CRITERION_B:
max = fabs (col1[2] - col2[2]);
break;
case GIMP_SELECT_CRITERION_H:
{
/* wrap around candidates for the actual distance */
gfloat dist1 = fabs (col1[0] - col2[0]);
gfloat dist2 = fabs (col1[0] - 1.0 - col2[0]);
gfloat dist3 = fabs (col1[0] - col2[0] + 1.0);
max = MIN (dist1, dist2);
if (max > dist3)
max = dist3;
}
break;
case GIMP_SELECT_CRITERION_S:
max = fabs (col1[1] - col2[1]);
break;
case GIMP_SELECT_CRITERION_V:
max = fabs (col1[2] - col2[2]);
break;
}
}
if (antialias && threshold > 0.0)
{
gfloat aa = 1.5 - (max / threshold);
if (aa <= 0.0)
return 0.0;
else if (aa < 0.5)
return aa * 2.0;
else
return 1.0;
}
else
{
if (max > threshold)
return 0.0;
else
return 1.0;
}
}
My educated guess would be that it is using an HSL color space, and the fuzziness is a parameter that selects all colors with a specific hue and saturation in a window of lightness (based on this).
Now the selection may just be doing a threshold calculation finding everything which is within that window (which would be a very small region in the colorspace). However, it may also be doing a statistical distance calculation. If the color sample actually small samples a window around the color, you can calculated the color variance and use something like a Mahalanobis distance calculation and threshold from that.
Again, that is just all conjecture, but maybe it'll help your thinking process.
Lastly, while this library does not have something like this directly implemented, OpenCV has many image processing tools that would make implementation easier.