snibgo's ImageMagick pages

Hot pixels

Detecting and fixing.

Here is an image with hot pixels:

hpx_sampin.jpg
set HOT_NEF=%PICTLIB2%20180921\AGA_4034.NEF

exiftool -args -ISO -ExposureTime -FNumber %HOT_NEF% 
-ISO=6400
-ExposureTime=1/15
-FNumber=4.0
%DCRAW% -t 0 -w -6 -T -O hpx_samp.tiff %HOT_NEF%

Some photography, such as hand-held street-scenes at night, needs short exposures at high ISO (or normal ISO with multiplication in editing), which creates noise. In my photography, with a Nikon D800, this is mostly photon shot noise, which is caused by the nature of light rather than the camera.

I accept noise that adds to the aesthetic of the photo. But some noise always distracts. For example, the redness at the centre of this crop.

set CROP=-crop 151x101+4121+2034 +repage

We show the crop at 1:1, then scaled up 4x:

Crop of input showing the problem.

%IMG7%magick ^
  hpx_samp.tiff ^
  %CROP% ^
  +write hpx_samp_sm.png ^
  -scale 400%% ^
  hpx_samp_sm_e.png
hpx_samp_sm.pngjpg hpx_samp_sm_e.pngjpg

A red sensel (sensor element) has recorded a value that is too large, and the demosaicing algorithm has spread that large value to other pixels. The large value may be an outlier of photon shot noise. But the cause is not important; the distraction is annoying, and I want to remove it.

Normal noise-reduction can remove this; see Camera noise. On this page, we remove hot pixels without general noise-reduction.

The method

The method is suitable for Bayer cameras, with a repeating 2x2 colour filter array. The pattern might be RGGB, BGGR, GRBG or GBRG. It might work for larger patterns.

The method has three steps:

  1. Run dcraw to make a Bayer image.
  2. Call hotPixels.bat to create a list of problem pixels.
  3. Re-run dcraw declaring those pixels to be "bad".

hotPixels.bat calls srndMinMnkSd.bat to make a mask image that is white where the input pixel is substantially lighter than surrounding pixels, and from that it writes a list of the coordinates of those pixels to a text file, which dcraw can read as "dead" pixels.

I define "substantially lighter" as ...

v > J*mean(v) + K*std_dev(v) + L

... where ...

... and ...

J = 5
K = 10
L = 0.01

Reducing any of the parameters J, K and L will mark more pixels as bad. Setting all three to zero will mark all non-zero pixels as bad. The squares are 5x5 to ensure the surounding pixels include two that are the same channel as the central pixel. We operate in linear space.

srndMinMnkSd.bat uses process modules to calculate mean and SD. It could be modified to use purely IM features.

Mostly, v > mean(v) + K*std_dev(v) is all we need, but that will create false positives (ie it will incorrectly flag some pixels as "bad") where the mean, or SD, or both, are close to zero. The numbers shown capture all the pixels I consider to be bad, with almost no false positives.

So, step (1): make the Bayer image hpx_hot.tiff:

%DCRAW% -t 0 -W -o 0 -6 -r 1 1 1 1 -g 1 0 -D -d -T -O hpx_hot.tiff %HOT_NEF%

-t 0 prevents dcraw from automatically orienting the image, eg rotating it to portrait format.

Next, step (2): find the hot pixels:

call %PICTBAT%hotPixels hpx_hot.tiff hpx_hot.lis

The text list, hpx_hot.lis, is:

7270 93 0 2
2834 436 0 0
4392 634 0 0
6242 639 0 2
6234 803 0 2
6710 2007 0 2
4196 2084 0 0
7294 2610 0 0
2021 2634 0 1
261 2966 0 1
7270 2987 0 2
7205 3404 0 1
5270 3551 0 2
6829 4051 0 3
4121 4354 0 1
7284 4548 0 0
3777 4683 0 3

In each line, the four numbers are:x-coordinate, y-coordinate, zero (a timestamp), and an integer 0..3 representing the channel number in the 2x2 pattern (for this camera, one of RGGB). dcraw ignores any text after the timestamp.

Just for fun, we run the script listPixels.bat which creates an image of crops centred on each coordinate, appended together, scaled up by 400%.

call %PICTBAT%listPixels hpx_samp.tiff hpx_hot.lis hpx_hotpix.png
hpx_hotpix.pngjpg

Some of these could be regarded as false positives.

Cropping the mask image to the problem area shown above:

The mask image

%IMG7%magick ^
  %hpxMASK% ^
  %CROP% ^
  -scale 400%% ^
  hpx_mask1.png
hpx_mask1.png

Finally, step (3): use the text list as "dead pixels" for another run of dcraw. For this demonstration, we tell dcraw to directly make a sRGB version. In production use, we might want further processing before converting to sRGB.

%DCRAW% -P hpx_hot.lis -w -6 -T -O hpx_dehot.tiff %HOT_NEF%

We show the result, cropping to the same small area as before:

Fixed.

%IMG7%magick ^
  hpx_dehot.tiff ^
  %CROP% ^
  +write hpx_dehot_sm.png ^
  -scale 400%% ^
  hpx_dehot_sm_e.png
hpx_dehot_sm.pngjpg hpx_dehot_sm_e.pngjpg

The centre of the image no longer has red hotness. A less obtrusive red hotness, below the centre, remains.

In a different part of the image...

set CROP2=-crop 200x200+6140+620 +repage

... we have cyan hotness. The method identifies the problems (in the G1 channel) and fixes them:

Crop of input showing the problem.

%IMG7%magick ^
  hpx_samp.tiff ^
  %CROP2% ^
  +write hpx_samp2_sm.png ^
  -scale 200%% ^
  hpx_samp2_sm_e.png
hpx_samp2_sm.pngjpg hpx_samp2_sm_e.pngjpg

The mask image

%IMG7%magick ^
  %hpxMASK% ^
  %CROP2% ^
  +write hpx_mask2_sm.png ^
  -scale 200%% ^
  hpx_mask2_sm_e.png
hpx_mask2_sm.png hpx_mask2_sm_e.png

Fixed.

%IMG7%magick ^
  hpx_dehot.tiff ^
  %CROP2% ^
  +write hpx_dehot2_sm.png ^
  -scale 200%% ^
  hpx_dehot2_sm_e.png
hpx_dehot2_sm.pngjpg hpx_dehot2_sm_e.pngjpg

Just for fun, we re-run listPixels.bat to show the fixed results.

call %PICTBAT%listPixels hpx_dehot.tiff hpx_hot.lis hpx_dehotpix.png
hpx_dehotpix.pngjpg

As expected, this has fixed the small hot patches at the centres of the squares.

Alternative methods

The method shown above fixes hot pixels, without changing many other pixels, and is reasonably fast (though it does need two runs of dcraw). Other methods are possible:

  1. We could split the Bayer image into four images, one per RGGB channel, and analyse each channel separately.
  2. To identify hot sensels we might use some "maximum" algorithm, eg a sensel is hot if it is greater than the maximum of surrounding sensels, multiplied by a factor.
  3. Instead of re-running dcraw with a list of bad pixels, we could fix them with IM and re-create the Bayer image as a DNG file. See Processing Bayer pixels.
  4. We might identify hot sensels from a demosaiced image, eg by comparing the colour of a 3x3 square with surrounding colours.
  5. Correction could be made to demosaiced images.

Scripts

For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.

hotPixels.bat

rem From %1, a single-channel Bayer file,
rem makes text file %2, x and y coords of hot pixels.
rem %3 window size [5x5]
rem %4 factor J [5]
rem %5 factor K [10]
rem %6 offset L [0.01]

@if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 hpx

if not "%2"=="" if not "%2"=="." set OUTTEXT=%2

set WINDIMS=%3
if "%WINDIMS%"=="." set WINDIMS=
if "%WINDIMS%"=="" set WINDIMS=5x5

set FACTJ=%4
if "%FACTJ%"=="." set FACTJ=
if "%FACTJ%"=="" set FACTJ=5

set FACTK=%5
if "%FACTK%"=="." set FACTK=
if "%FACTK%"=="" set FACTK=10

set OFFSL=%6
if "%OFFSL%"=="." set OFFSL=
if "%OFFSL%"=="" set OFFSL=0.01

set TMPFILE=\temp\hp.miff
set TMPTEXT=\temp\hpt.lis

call %PICTBAT%srndMinMnkSd.bat %INFILE% %TMPFILE% %WINDIMS% %FACTJ% %FACTK% %OFFSL%

%IMG7%magick %TMPFILE% -transparent Black sparse-color: | sed -e 's/ /\n/g' | cut -d, -f1,2 --output-delimiter=" " >%TMPTEXT%
if ERRORLEVEL 1 exit /B 1

(
  for /F "tokens=1,2" %%X in (%TMPTEXT%) do (
    set /A R="%%X %% 2 + %%Y %% 2 * 2"
    echo %%X %%Y 0 !R!
  )
) >%OUTTEXT%

for /F "usebackq tokens=%%A" %%L in (`wc -l %OUTTEXT%`) do set NUM_PIX=%%L

call echoRestore

@endlocal & set hpxOUTTEXT=%OUTTEXT%& set hpxMASK=%TMPFILE%& set hpxNUM_PIX=%NUM_PIX%

srndMinMnkSd.bat

rem Find pixels that are greater than J * local mean plus k * local standard deviation,
rem where mean and SD are of windowed pixels _excluding_ central pixel.
rem
rem %1 input
rem %2 output
rem %3 window size [3x3]
rem %4 factor J [1]
rem %5 factor K [3]
rem %6 offset L [0]
rem Returns white where  v > [J * mean(v) + K * std_dev(v) + L],
rem otherwise black.
@rem
@rem Also uses:
@rem   smmsDEBUG format of filename for debugging files.
@rem     Must contain XX.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 smms

if not "%2"=="" if not "%2"=="." set OUTFILE=%2

set WINDIMS=%3
if "%WINDIMS%"=="." set WINDIMS=
if "%WINDIMS%"=="" set WINDIMS=3x3

call parseXxY 3 3 %WINDIMS% smms

echo %0: smms: %smms_X% %smms_Y%

set FACTJ=%4
if "%FACTJ%"=="." set FACTJ=
if "%FACTJ%"=="" set FACTJ=1

set FACTK=%5
if "%FACTK%"=="." set FACTK=
if "%FACTK%"=="" set FACTK=3

set OFFSL=%6
if "%OFFSL%"=="." set OFFSL=
if "%OFFSL%"=="" set OFFSL=0

set MFMT=^
0,^
%%[fx:%smms_X%*%smms_Y%/(%smms_X%*%smms_Y%-1)],^
%%[fx:-1/(%smms_X%*%smms_Y%-1)],^
0

echo %0: MFMT=%MFMT%

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -precision 16 ^
  -format "%MFMT%" ^
  xc:`) do set MARGS=%%L

echo %0: MARGS=%MARGS%

if "%smmsDEBUG%"=="" (
  set WrMn=
  set WrSd=
  set WrJMnKSd=
  set WrMinus=
) else (
  set WrMn=+write %smmsDEBUG:XX=Mn%
  set WrSd=+write %smmsDEBUG:XX=Sd%
  set WrJMnKSd=+write %smmsDEBUG:XX=JMnKSd%
  set WrMinus=+write %smmsDEBUG:XX=Minus%
)

%IM7DEV%magick ^
  %INFILE% ^
  -depth 32 ^
  -define compose:clamp=off ^
  -define quantum:format=floating-point ^
  +write mpr:INP ^
  ( -clone 0 ^
    -evaluate Pow 2 ^
    ( +clone ^
      -process 'integim' ^
      -process 'deintegim window %WINDIMS%' ^
    ) ^
    -compose Mathematics -define compose:args=%MARGS% -composite ^
  ) ^
  ( -clone 0 ^
    ( +clone ^
      -process 'integim' ^
      -process 'deintegim window %WINDIMS%' ^
    ) ^
    -compose Mathematics -define compose:args=%MARGS% -composite ^
    +write mpr:MN ^
    %WrMn% ^
    -evaluate Pow 2 ^
  ) ^
  -delete 0 ^
  -alpha off ^
  -compose MinusSrc -composite ^
  -clamp ^
  -evaluate Pow 0.5 ^
  %WrSd% ^
  mpr:MN ^
  -compose Mathematics -define compose:args=0,%FACTJ%,%FACTK%,%OFFSL% -composite ^
  %WrJMnKSd% ^
  mpr:INP ^
  -compose MinusDst -composite ^
  -clamp ^
  %WrMinus% ^
  -fill White +opaque Black ^
  %OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal

listPixels.bat

rem %1 input image
rem %2 text file of coordinates
rem %3 output image

set INFILE=%1
set INTEXT=%2
set OUTFILE=%3

set W_2=5
set H_2=5

set /A W=W_2*2+1
set /A H=H_2*2+1

set TMPDIR=\temp
set TMPIN=%TMPDIR%\lp.miff

%IMG7%magick %INFILE% %TMPIN%

set cnt=0
set FILELIST=
for /F "tokens=1,2 delims=, " %%A in (%INTEXT%) do (
  echo %%A %%B
  set /A L=%%A-%W_2%
  set /A T=%%B-%H_2%
  echo %W%x%H%+!L!+!T!

  set FILENAME=%TMPDIR%\lp_!cnt!.miff
  %IMG7%magick %TMPIN% -crop %W%x%H%+!L!+!T! +repage !FILENAME!

  set FILELIST=!FILELIST! !FILENAME!

  set /A cnt+=1
)

echo %FILELIST%

%IMG7%magick ^
  %FILELIST% ^
  +append ^
  -scale 400%% ^
  %OUTFILE%

All images on this page were created by the commands shown, using:

%IM7DEV%magick -version
Version: ImageMagick 7.0.8-64 Q32 x86_64 2019-12-18 https://imagemagick.org
Copyright: © 1999-2019 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png rsvg tiff webp wmf x xml zlib

Source file for this web page is hotpixels.h1. To re-create this web page, execute procH1 hotpixels.


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 2-January-2020.

Page created 05-Jan-2020 08:36:15.

Copyright © 2020 Alan Gibson.