Detecting and fixing.
Here is an image with hot pixels:
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 is distracting. 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 |
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. It may depend on sensor temperature. 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 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:
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.
As J=5, when the surrounding pixels have a mean greater than 0.2, the central pixel is never regarded as hot (because values above 0.2, when multiplied by 5, is above 1.0). 0.2 in linear RGB is about 0.48 in sRGB.
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, such as rotating it to portrait format.
Next, step (2): find the hot pixels:
call %PICTBAT%hotPixels hpx_hot.tiff hpx_hot.lis echo hpxNUM_HOT_PIX=%hpxNUM_HOT_PIX%
hpxNUM_HOT_PIX=17
If hpxNUM_HOT_PIX is zero, no hot pixels were found.
This creates a text list of hot pixels, hpx_hot.lis:
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. The channel number is included just in case we are interested.
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
Some of these could be regarded as false positives. When the hot pixel is red or blue, demosaicing expands this to a 3x3 square. Hot green pixels don't spread as much because they have adjacent non-hot green pixels.
Cropping the mask image to the problem area shown above:
The mask image %IMG7%magick ^ %hpxMASK% ^ %CROP% ^ -scale 400%% ^ 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 |
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 |
|
The mask image %IMG7%magick ^ %hpxMASK% ^ %CROP2% ^ +write hpx_mask2_sm.png ^ -scale 200%% ^ hpx_mask2_sm_e.png |
|
Fixed. %IMG7%magick ^ hpx_dehot.tiff ^ %CROP2% ^ +write hpx_dehot2_sm.png ^ -scale 200%% ^ hpx_dehot2_sm_e.png |
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
As expected, this has fixed the small hot patches at the centres of the squares.
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:
For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.
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 del %OUTTEXT% 2>nul set NUM_HOT_PIX=0 ( for /F "tokens=1,2" %%X in (%TMPTEXT%) do ( set /A R="%%X %% 2 + %%Y %% 2 * 2" echo %%X %%Y 0 !R! ) ) >%OUTTEXT% if exist %OUTTEXT% for /F "usebackq tokens=1" %%L in (`wc -l %OUTTEXT%`) do set NUM_HOT_PIX=%%L call echoRestore @endlocal & set hpxOUTTEXT=%OUTTEXT%& set hpxMASK=%TMPFILE%& set hpxNUM_HOT_PIX=%NUM_HOT_PIX%
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
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% if not "%FILELIST%"=="" %IMG7%magick ^ %FILELIST% ^ +append ^ -scale 400%% ^ %OUTFILE%
All images on this page were created by the commands shown, using:
%IMG7%magick -version
Version: ImageMagick 7.1.0-42 Q16-HDRI x64 396d87c:20220709 https://imagemagick.org Copyright: (C) 1999 ImageMagick Studio LLC License: https://imagemagick.org/script/license.php Features: Cipher DPC HDRI OpenCL Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib Compiler: Visual Studio 2022 (193231332)
%IM7DEV%magick -version
Version: ImageMagick 7.1.0-20 Q32-HDRI x86_64 2021-12-29 https://imagemagick.org Copyright: (C) 1999-2021 ImageMagick Studio LLC License: https://imagemagick.org/script/license.php Features: Cipher DPC HDRI Modules OpenMP(4.5) Delegates (built-in): bzlib cairo fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png raqm rsvg tiff webp wmf x xml zip zlib Compiler: gcc (11.2)
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 23-Aug-2022 02:42:56.
Copyright © 2022 Alan Gibson.