How do I -level an image based on the "darkest" pixel in a given area?

Questions and postings pertaining to the usage of ImageMagick regardless of the interface. This includes the command-line utilities, as well as the C and C++ APIs. Usage questions are like "How do I use ImageMagick to create drop shadows?".
Post Reply
AlanD
Posts: 2
Joined: 2016-12-14T23:02:04-07:00
Authentication code: 1151

How do I -level an image based on the "darkest" pixel in a given area?

Post by AlanD »

I have some photographs I shot against a theoretically white background. The reality is that the background is hardly white. I'd like to adjust the image so that the background is absolutely white. The remainder of the image should get the same adjustment, as it was subject to the same lighting. I've got a pile of these images, so I'd like to automate the process.

In my favor, I have a large block in the corner of every image that is guaranteed to be background, the upper right 160x160 pixels. So my theory is that I can grab that subset of the image, find the "darkest" pixel, and use that as the reference white point.

Here's one of my input images: https://dl.dropboxusercontent.com/u/380 ... estion.jpg (It's a miniature from the board game Tannhauser.)

After much experimentation, here's what I ended up with:

Code: Select all

#! /bin/sh
DARKLEVEL=`convert input.jpg -format %c -crop 160x160+0+0 -fx 'min(r,g,b)'  histogram:info:- | head -n1 | awk -F'(' '{print $2}' | awk -F, '{print int(100*$1/255)}'`
convert input.jpg -level 0,$DARKLEVEL% output.jpg
(I'm not terribly fluent in awk; my apologies if that makes you cringe.)

It seems to work. The backgrounds are absolutely white, although I don't know if I over-did it.
  • Is there a better way to accomplish my goal?
  • Does what I'm doing actually accomplish my goal?
My copy of ImageMagick is from Ubuntu 16.04. I'm willing to upgrade. "convert --version" reports:

Code: Select all

Version: ImageMagick 6.8.9-9 Q16 x86_64 2016-11-29 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2014 ImageMagick Studio LLC
Features: DPC Modules OpenMP
Delegates: bzlib cairo djvu fftw fontconfig freetype jbig jng jpeg lcms lqr ltdl lzma openexr pangocairo png rsvg tiff wmf x xml zlib
User avatar
fmw42
Posts: 25562
Joined: 2007-07-02T17:14:51-07:00
Authentication code: 1152
Location: Sunnyvale, California, USA

Re: How do I -level an image based on the "darkest" pixel in a given area?

Post by fmw42 »

Try -auto-level or -contrast-stretch. They stretch the image according to the min and max values. auto-level does it automatically, -contrast-stretch allows clip values as percents in the histogram. -contrast-stretch 0 is almost the same as -auto-level.

If you want to stretch each channel separately use -channel rgb before -auto-level or -contrast-stretch
snibgo
Posts: 12159
Joined: 2010-01-23T23:01:33-07:00
Authentication code: 1151
Location: England, UK

Re: How do I -level an image based on the "darkest" pixel in a given area?

Post by snibgo »

But the soldier's face is lighter than the background. If you make the background white, the face will be clipped to white.

Finding the darkest background pixel is good. But you might want to make that a light shade of gray, not white.

For example, Windows BAT syntax, adjust for bash:

Code: Select all

for /F "usebackq" %%L in (`%IM%convert ^
  soldier.jpg ^
  -gravity NorthEast -crop 160x160+0+0 ^
  -format "FR=%%[fx:1.2*minima.r]\nFG=%%[fx:1.2*minima.g]\nFB=%%[fx:1.2*minima.b]\n" ^
  info:`) do set %%L

%IM%convert ^
  soldier.jpg ^
  -channel R -evaluate Divide %FR% ^
  -channel G -evaluate Divide %FG% ^
  -channel B -evaluate Divide %FB% ^
  +channel ^
  out.png
snibgo's IM pages: im.snibgo.com
AlanD
Posts: 2
Joined: 2016-12-14T23:02:04-07:00
Authentication code: 1151

Re: How do I -level an image based on the "darkest" pixel in a given area?

Post by AlanD »

A few general notes:

I expect a certain halo around the figure. However, I really don't want a scatter of not-quite-white pixels all over the background. It's okay if a few pixels inside of the figure get clipped to pure white, just so long as I don't do too much damage to it.

In response to specific suggestions:

I may be doing the wrong thing, but I -auto-level doesn't appear to do what I want. Doing "convert input.jpg -auto-level out.jpg", the resulting background isn't absolutely white. I wouldn't expect it to be, if I'm understanding -auto-level correctly, because it will only move the brightest pixel(s) value to white, so most of the slightly darker background would be lightened, but not turned absolutely white.

I had tried -contrast-stretch, and it seems tricky to calibrate for bulk images. For the given image, I had to go with "convert input.jpg -contrast-stretch 0x79% cs.jpg" to clear the background entirely (minus a bit of a halo around the figure itself, but I expect that). For others of my input images, 79% is clearly too large (eating aggressively into the figure) or too small (leaving a speckle of non-white pixels in the background). I couldn't figure out a way to automatically calculate it.

For anyone wanting snibgo's code in Bourne shell:

Code: Select all

#! /bin/sh

eval `convert input.jpg -gravity NorthWest -crop 160x160+0+0 -format "FR=%[fx:1.2*minima.r]\nFG=%[fx:1.2*minima.g]\nFB=%[fx:1.2*minima.b]\n" info:`

convert input.jpg   -channel R -evaluate Divide $FR  \
  -channel G -evaluate Divide $FG  \
  -channel B -evaluate Divide $FB  \
  +channel \
  out.png
(That's a fairly direct and clumsy translation.)

Taking snibgo's solution, applying it to my use of -level, and adjusting to be a bit more idiomatic, I ended up with this:

Code: Select all

#! /bin/sh

DARKLEVEL=`convert input.jpg -crop 160x160+0+0 -format "%[fx:100*min(minima.r,minima.g,minima.b)]" info:`
convert input.jpg -level 0,$DARKLEVEL% out.jpg
This works just off the single darkest value from any channel, and adjusts that point to absolute white. It also eliminates a pointless int() in my original. This also uses the upper left corner, not upper right. I meant upper left, but somehow typed upper right in my original post. My apologies.

I think that is better than my first attempt; it's more direct and easier to understand. There are a few things in there, but perhaps the most important piece is that I am now aware of minima, which I had previously overlooked.

This has been very educational, thank you! I'll definitely keep -evaluate Divide in mind. And this discussion helped me realize that perhaps I don't want one set of output images, but two: one set with a grey background for display, and a different set with a pure white background for later, manual manipulation. I'm also reconsidering if I want to do a single adjustment, or a per-channel adjustment.

Thank you!
User avatar
fmw42
Posts: 25562
Joined: 2007-07-02T17:14:51-07:00
Authentication code: 1152
Location: Sunnyvale, California, USA

Re: How do I -level an image based on the "darkest" pixel in a given area?

Post by fmw42 »

Sorry about the leads to -auto-level, etc. I did not look at your image and thought you just wanted to stretch the image to full dynamic range. So that technique does not apply to where you want an intermediate graylevel to be forced to white.
snibgo
Posts: 12159
Joined: 2010-01-23T23:01:33-07:00
Authentication code: 1151
Location: England, UK

Re: How do I -level an image based on the "darkest" pixel in a given area?

Post by snibgo »

If you want to make the background white (and thus part of the face also white), you can change my commands to remove the "1.2*" multiplier.

Another more complex method is to find the brightest pixel in the entire image, and use "-level" or another method to make that colour white.
snibgo's IM pages: im.snibgo.com
Post Reply