Removing
Halos
|
Physpics
Annals
of Zweibieren
2007| |
The
bear at left has a "halo" of white pixels in the black background.
Roll the mouse over it: no halo. In this
case, the halo was removed only for black backgrounds, which
is the
best you can do with .gif images. This page shows how to banish the
halo
from any background, using .png images. |
Some
pages, like this one, have background colors. When icons appear, it is
better to show them with a transparent background, like ,
instead of in some fixed
background, like .
Extracting an icon or other image from a background is a common task,
yet fraught with challenges. The background may not be a solid color,
but several shades close to each other. With formats like JPEG, colors
may intermix across the background/image boundary. For anti-aliasing,
some pixels along the edge of the image may be a mixture of the image
color and the background. Some pixels of the background color may get
swept up along with the rest of the image. This latter is called a
halo,
and is illustrated above.The general task can usually be stated as converting some particular color, the transparency color, in the original image so that all pixels of that color are made transparent. There may need to be some fuzz in interpreting the color so nearby hues are also made transparent. In the work here, the transparency color is always taken to be the color of the leftmost pixel in the top row of the image, sometimes denoted by p{0,0}.
There are two cases. In the instances above the background surrounds the image. Pixels within the image that happen to be the transparency color should NOT be made transparent. In other instances, particularly text like ,
transparency
should apply to all pixels of the transparency color so the background
shows through the "holes" in the image. These two cases are covered by
two separate scripts that implement the techniques being described:
|
||||||
| On black backgrounds, the results below are not quite as good as the bear at right. For other backgrounds, the results are superior. The bears are .gif images (from ablewebs.com), while the techniques below rely on .png. Pixels in gif images can be either fully opaque or fully transparent, but not partially transparent. Png images can have different fractional transparency for each pixel. | |
|||||
| Examples below show the command on the left and the image against two backgrounds on the right. To get bearOnWhite.bmp, I put the original haloed bear on a white background, and ran this command: | |||
|
![]() |
![]() |
|
| Here are
three bear images—the same image across each row—on
various color
backgrounds. |
||||
| Bear with "halo."
The halo is
light, so the bear looks okay on light backgrounds. |
![]() |
![]() |
![]() |
![]() |
| Bear.gif adapted
for a black
background. Not so good on other backgrounds. |
![]() |
![]() |
![]() |
![]() |
| Bear-nosur.png
anti-aliased for
any
background with nosur. |
![]() |
![]() |
![]() |
![]() |
| Note
the
imperfection of bear.png on a black background.For this background,
hand tweaking would be needed, as it was with the original bear on a
black background. |
||||
|
The two scripts, nobg and nosur, have identical parameters and are invoked as follows, where brackets indicate optional elements: nobg
[-f fuzz-precent]
[-b blur-spread]
[-t] file
[outfile]or nosur
[-f fuzz-precent]
[-b blur-spread]
[-t] file
[outfile]Both scripts convert file to outfile, while making transparent the background (nobg) or surroundings (nosur). If outfile is omitted, the output is written to the file fff-nobg.png
or fff-nosur.png where fff is the name file with its extension removed. The optional switches are:
|
| To test
the algorithms
I collected some random images. I made a comprehensive page of samples with various processing and selected a
few to show here. |
![]() Text in PowerPoint |
![]() Logo for cgoban, the usual interface to the Kiseido go server |
![]() A photo I took in the Museum at HangYang University, Seoul |
![]() Two combined cliparts |
![]() Clipart |
|
![]() Once guarded a warning on the Toxic Well site. |
![]() Part of the logo for http://www.peteducation.com/ (yellow added) |
| Here are the best results for each image. They are unretouched, but I did adapt the script parameters to do as well as possible. |
Almost a
really good
result. It is too bad the algorithm does not poke more deeply into
little pockets.
|
![]() |
![]() |
| As a more severe test, I overlaid FRED on a green background. The result shown here is the upper left corner at 4x lifesize. It retains green. On a red background, some transparency is visible, but not enough, especially in pockets. | |||
|
![]() |
![]() |
|
| Without
the blurring of -b 10x3, the green is even more apparent. |
|||
The
default fuzz and
blur wipe out too much of the delicate filagree. These smaller values
work better.
|
![]() |
![]() |
With a
smaller fuzz we
get a bit of a halo/ but this bigger fuzz makes the top a bit thiner.
The smaller blur helps restore the thickness.
|
![]() |
![]() |
|
| The nobg script does work, but its weird to have the background color inside the skull. | |||
With
nobg, big gobs of
the middle of the pot are transparent, but nosur
does fine. Note that there is a bit of halo at the very bottom, but
that its partial transparency makes it seem almost natural. It could be
removed with hand massaging of the pixels.
|
![]() |
![]() |
The
vanilla nobg
leaves green halo blobs near the junctions of circle and square. Nosur
leaves a shadow in the lower right. By increasing the fuzz, we get a
really great result.
|
![]() |
![]() |
The fuzz
factor had to
be lowered for this image to avoid leaking background into the image.
(But nobg did just fine.)
|
![]() |
![]() |
For this
puppy, nobg
is the one that leaks background into the body. Attempts to remove the
halo on the lower edge will also leak background into the pup's body.
|
![]() |
![]() |
| The basic approach
is to see where
the image differs from the transparency color and make those parts of
the image transparent. To get anti-aliasing right, we blur the edges of
the difference mask and use those values to generate partial
transparency for edge pixels in the image. |
||
1. Make a
tile having
the transparency
color.
|
![]() |
|
2. Create
a version of
the image tiled with the transparency color.
|
![]() |
3.
Subtract the
transparency color from the image. The result is an image where black
means no difference and lighter colors are greater distance.
|
![]() |
4.
Threshold the image
to pure black/white. The black pixels will be those whose color was
within 5% of the transparency color.
|
![]() |
5. (Start
the magic)
Blur the difference image just around the edges.
|
![]() |
6. (Build
the magic)
To restrict the
blur to the opaque part of the image, we take the blurred
mask
and completely blacken it where the difference mask is black.
|
![]() |
|
| Of
the 54 available compose operators, only these four do the right thing:
Bumpmap Darken ColorBurn Multiply |
||
7. (Wave
the wand,
Shazam!) A final table
look-up gets the transparency for each pixel of the image. See Computing the Transparency Table
|
![]() |
| The key
difference from the nobg script is to use floodfill to find only that
portion of the background which reaches the outer edge of the image. To
make sure that pockets are not isolated due to the image touching the
edge, an early step surrounds the image with a one pixel border having
the edge color. |
||
1. Make a
tile of the
transparency color from the upper left pixel.
|
![]() |
|
2. Make
an image one
pixel larger in all directions and tile it with the transparency color.
|
![]() |
3. Insert
the original
image into the result of step 2. Offset it so there is a border of
transparency color all the way around.
|
![]() |
4.
Subtract the
transparency color image from the bordered image. Black indicates no
difference. The lighter the color the greater the difference.
|
![]() |
5.
Convert the image
to grayscale and then pure black/white. The remaining black pizels are
those that were within fuzzpct
of the transparency color.
|
![]() |
| 6a.
Multiply all pixel
values by .5. This does not affect black, but makes white gray. 6b. Floodfill all pixels that are reachable from the upper left corner. Make them all white.
|
![]() |
| There are now pixels of three colors in the mask: white for the transparent surround, gray for non-transparent, and black for interior pixels of the transparency color. | ||
7.
Threshold the image
so white remains white and the other two become black. Negate the image
to reverse the colors and make the transparency mask.
|
![]() |
|
8. The
rest is just
like nobg. Blur the mask and combine it to retain all the black pixels.
|
![]() |
9. Apply
the
transparency table to the original image under control of the
transparency mask from the previous step. Voila! The surround is
transparent.
|
![]() |
| The transparency of a PNG image is dictated by the
alpha
channel. This is described in the PNG
standard as: The alpha sample
determines a pixel's degree of opacity, where zero means fully
transparent and the maximum value means fully opaque.
At the heart of the nobg and nosur algorithms is this computation:
This is applied against an image sequence of three images:
The main thrust of the expression is to compute u[2].p{...}.a. This means to look at image u[2] (the transparency table), to extract a single pixel with .p, and to produce as the expression's value the .a part of that pixel. The .a part is the alpha channel value of the extracted pixel. The discussion below shows how that value is computed in the first place. For pixel u[2].p{...}, the column is selected by the expression v.r*10. Here v, the middle image in the sequence, is the transparency mask.and v.r is the red value of that pixel (The mask is grayscale, so r, b, and g have the same value). The value of v.r ranges from 0 to 1. It is multiplied by ten to select a column in transparencytable.png. In v, the transparency mask, pixels that should be transparent have the value 0; others that shold be opaque have a one. These show as black and white, respectively, in displays of these masks. Pixels just within the opaque part of the mask have values closer to zero, so they will be mostly transparent, further in pixels have higher transparency values and will be more opaque. Only pixels within two or three of the transparent areas have intermediate values in the transparency mask. The row of u[2].p{...} is selected based on the intensity of u, original image at the current pixel, via the expression u.intensity*10. The .intensity measures the perceived brightness of the pixel, ranging from 0 for black to 1 for white. The algorithm tries to make whiter colors more transparent. This is where the halo goes away. This is the magic. The table is such that there is more transparency for pixels as the intensity increases from top to bottom of transarencytable.png. The mutiplication by 10 is to convert the intensity value into an index into the table. |
The transparency table looks like this (when expanded
10 to
1):![]() After considerable tinkering, the spread sheet values below were found to give resonable images. In this table, the salmon colored cells all exceed 1 and are fully opaque. The aritmetic of table construction was constrained so the two gold cells have the values that appear in them. These values are called mintr and maxtr, respectively.
The parameters of the table are power and iDelta, which were finally decided to be best at 3 and 20, respectively. The expression for each cell of the table is then: mintr+mul*((10+iDelta-j)*i)^power
where i
and j are
the column and row,
respectively, and where mul
is computed so that maxtr
has
the desired value. Mul
turns
out to have the value 0.000000174897119. The spread sheet went on to
have a cell giving the exact -fx expression to be used in generating
transparency table.png.The Makefile section for generating the table looks like this: transparencytable.png:
Makefilexc:none
-matte
-channel A \The line starting "convert" sets parameters for the image. It is created as having no color with xc:none, then that line goes on to say the image will have an alpha channel and the -fx will write into it. The final line says where to store the resulting image. The spreadsheet also has a chart to depict the values of the expression. Its final shape is this: ![]() Here Edge Proximity corresponds to columns in the table and Color Intensity to rows. The transparency value is the contents of the cell. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| For
scripts, it is
inconvenient and time-consuming to write temporary images to file.
Consequently I organized the scripts to keep needed versions of the
image in memory. ImageMagick operates on a sequence of images, where
some operators combine all the images of the sequence into one final
image. The parentheses operator delimits a section where some computation is done on a subsequence of images. The -clone operator copies from the outer sequence into thei inner sequence. After the closing parenthesis, the image(s) of the inner sequence are appended to the next outer sequence. (In this work there is only one image left from an inner sequence. Sometimes it is dicarded with -delete.) In this section, the two scipts are broken down into their steps. A final +append at the end of a step produces an image which is the concatenation of all images in all existing sequences. |
| 1a. Make
enough copies
of the original image for all the subsequent steps
that need it. 1b. Make a tile of the transparent color. The -crop selects the upper left pixel; the -scale expands it to 100x100. This larger size is meant to reduce the overhead in subsequent steps. 1c. Save the tile into internal file mpr:pixel (because the -tile command must read from a file).
|
![]() |
2.
Throw away the tile from the end of the image sequence. Then specify to
do tiling using that tile. The tiling is done by the "-draw" and its
"reset" option causes all pixels to be replaced.
|
![]() |
3.
Subtract the
transparent color from each pixel of the original image. (This uses the
first copy of the image.)
|
![]() |
| 4a.
Convert the difference image to grayscale without an alpha channel.
Threshold the image so all values more than 5% as bright as white are
converted to white. The image is now a mask. 4b. Make a copy of the mask and blur its edges.
|
![]() |
5.
Combine the mask
and the blurred mask so blurring remains only in the parts that used to
be completely white.
|
![]() |
6. Read
in a copy of
the transparency table.
|
![]() |
| 7a.
Apply the transparency table and voila: the final image. The -channel
operation restricts the -fx so it only sets the value for the alpha
channel. 7b. Trim any edges that are wholly the transparency color.
|
![]() |
| For
nobg, the first step was to open a nested series of image sequences,
each beginning with the original image. In nosur, the required image
sequences do not all begin with the original image. Instead of many
nested sequences, nosur has an outer sequence with saved images. For
each sub-operation, an inner sequence is started and operand images are
cloned from the outer sequence. |
||
1. The
first
sub-operation creates a 100x100 tile of the transparency color. This
tile is saved into an internal file for later use.
|
![]() |
|
| 2a.
Delete the image
from the first sub-operation. 2b. The second sub-operation creates an image one pixel larger on all sides than the original image. This image is then covered over with the transparency tile from the internal file.
|
![]() |
3. The
third
sub-operation starts with the blank, bordered image just created and
then overlays on it the original image. The geometry operator positions
the image so its upper left is at the 1,1 pixel.
|
![]() |
| 4a. The
original image
is no longer needed. We have it yet, but with a border of transparency
colored pixels. 4b. Start the third suboperation by subtracting the blank image (-clone 0) from the bordered image (-clone 1).
|
![]() |
| 5a.
Process the
difference to make a mask as shown above in steps 4,5, and 6 of the
previous section. Make the difference image grayscale, convert to
black/white, then to black/gray, then floodfill to get
white/gray/black. Threshold back to white/black, and negate to get the
outer parts black for transparency. 5b. Clone the image and blur its edges.
|
![]() |
6.
Combine the mask
and blurred mask so the black from the mask is all black in the blurred
image.
|
![]() |
7.
Finally, apply the
transparency table to set the alpha channel of the image. Trim the
image to get rid of the border and any additional transparent rows or
columns.
|
![]() |
One of
the options for
nobg and nosur is -b blur-spreadThis section shows the effect of various values for blur-spread. It does so with false color images showing the values generated. The colors were generated with convert
-size 11x1 xc:white
\ -fill 'white' -draw
'point 0,0' \ -fill
'hsl(330,100,70)' -draw 'point
1,0' \ -fill 'hsl(10,100,56)'
-draw 'point 2,0'
\ -fill 'hsl(30,100,50)'
-draw 'point 3,0'
\ -fill 'hsl(50,100,50)'
-draw 'point 4,0'
\ -fill
'hsl(80,100,46)' -draw
'point 5,0' \ -fill
'hsl(110,100,50)' -draw 'point
6,0' \ -fill
'hsl(170,100,46)' -draw 'point
7,0' \ -fill
'hsl(210,100,50)' -draw 'point
8,0' \ -fill
'hsl(240,100,60)' -draw 'point
9,0' \ -fill
'hsl(270,100,50)' -draw 'point
10,0' \ colorarray.png(a value for 0,0 of hsl(290,100,85) would make its intensity consistent) These colors correspond to 0.0, 0.1, ... 0.9, 1.0: ![]() The 0.0 value is a wholly transparent background pixel. With the transparency algorithm described above, both the 0.9 and the 1.0 values map to entirely opaque pixels. |
To
simulate the mask,
I made this black/white image, BW.png![]() The false color images were constructed with this script: foreach
b
( \convert BW.png +matte \( +clone -blur $b \) \ -compose ColorBurn
-composite
colorarray.png \ -fx
'v.p{floor(10*u.intensity),0}'
blurMap-$b.png echo
"<br>" echo
"<br>" echo
$b"<br>" echo "<img
alt='map of blur $b'
src='blurMap-$b.png' \end |
| Here are
the images generated by the above command: |
||
2x.5![]() |
2x1![]() |
3x1![]() |
3x1.5![]() |
4x1![]() |
4x2![]() |
These
colors correspond to 0.0, 0.1, ... 0.9, 1.0: ![]() |
||
5x1![]() |
5x2![]() |
5x3![]() |
10x1![]() |
10x2![]() |
10x3![]() |
| In
general, the R
pixels closest to an edge are affected, but only S+1 of them are given
opacity less than one. For values with small S, like 2x.2 and 3x.3,
virtually no blurring occurs; all pixels are either transparent or
opaque. |
The script prefixSince both scripts have the same options, they share most of their code. The nobg version of the common code follows. The nosur version is close to it. |
|
#!/bin/shusage() { echo "*** usage:
nobg [-f
fuzz-precent] \ |
|
fuzzpct="5" |
|
#
process command line
switches |
|
|
|
#
set infile and
outfile from the remainder of the command line |
|
|
convert
$infile
+matte \( +clone
\
|
|
|
convert
$infile
\
\ \
\
\
\
\
\ \ \
\
\
-threshold 90%
-negate
\
\ \
\
\
\ |
The halo
scripts are now part of pictools.
They have been converted to tcsh (because of bash's ridiculous approach
to CRs). |
| Created: | 22 February 2007 | ![]() |
|
| Updated: |
8 September 2007 |
||
| Author: | Fred Hansen, <zweibieren@physpics.com> | ||
| ImageMagick: | 6.3.0 11/05/06 Q16 | ||
| URL: | http://physpics.com/pictools/halo/index.html |