Noise Suppression with ImageMagick

This post is a tribute to Guillermo Luijk‘s Zero Noise technique [1].

To replicate this process under Linux, I’m using dcraw and ImageMagick. To compare apple to apple, I’ve used the photos from Guillermo’s article.

The aim of this technique is to replace noisy pixels from the original image (0EV) with the pixels from the other over-exposed images. Therefore you need to take a number of catches with 2EV apart (e.g: 0EV +2EV +4EV) using a tripod. The reference image is the 0EV catch and that’s where the technique will apply.

The first step is to convert the raw files to linear TIFF (16bits) using the sRGB colorspace with for example an automatic white balance.

max_ev=`ls -1 *.cr2 | tail -n 1`
max_ev_name=`echo ${max_ev} | cut -d '.' -f1`
dcraw -v a -W -o 1 -q 3 -4 -T -c ${max_ev} > ${max_ev_name}.tiff 2> dcraw.log
cat dcraw.log
rgbg=`grep 'multipliers' dcraw.log | cut -d ' ' -f2,3,4,5`
rm dcraw.log
for other in $( ls -r *.cr2 ); do
  if [ ${other} != ${max_ev} ]; then
    dcraw -v -r $rgbg -W -o $color -q 3 -4 -T ${other}
  fi
done



Second step is to create the merging masks (2 masks for 3 catches). A mask is produced from the negate of over-exposed image which is copied into the alpha channel [2] of the corrected version of the over-exposed image. For example, the corrected version of the +2EV image is 0EV.
To generate the corrected images, I’ve wrote a little program (based on Guillermo’s algorithm) using the ImageMagick C++ API. The program prints the exposure difference between 2 images.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <wand/MagickWand.h>

int main(int argc,char **argv) {
  MagickBooleanType status;
  MagickPixelPacket pixel1, pixel2;
  MagickWand *image1, *image2;
  PixelIterator *iterator1, *iterator2;
  PixelWand **pixels1, **pixels2;

  long y;  
  register long x;
  unsigned long width;

  double min = 65536 / pow(2, 6);
  double max = 65536 * 0.9;
  double sum1 = 0, sum2 = 0;

  if (argc != 3) {
      fprintf(stdout, "Usage: %s normal-image over-exposed-image\n", argv[0]);
      exit(0);
  }

  MagickWandGenesis();
  image1 = NewMagickWand();
  status = MagickReadImage(image1, argv[1]);
  if (status == MagickFalse) {
    return -1;
  }
  image2 = NewMagickWand();
  status = MagickReadImage(image2, argv[2]);
  if (status == MagickFalse) {
    return -1;
  }

  iterator1 = NewPixelIterator(image1);
  iterator2 = NewPixelIterator(image2);
  if ((iterator1 == (PixelIterator *) NULL) || (iterator2 == (PixelIterator *) NULL)) {
    return -1;
  }
  for (y=0; y < (long) MagickGetImageHeight(image1); y++) {
    pixels1 = PixelGetNextIteratorRow(iterator1, &width);
    pixels2 = PixelGetNextIteratorRow(iterator2, &width);
    if ((pixels1 == (PixelWand **) NULL) || (pixels2 == (PixelWand **) NULL)) {
      break;
    }
    for (x=0; x < (long) width; x++) {
      PixelGetMagickColor(pixels1[x], &pixel1);
      PixelGetMagickColor(pixels2[x], &pixel2);
      if ((pixel1.red >= min) && (pixel1.red <= max) && (pixel2.red >= min) && (pixel2.red <= max)) {
         sum1 += pixel1.red;
         sum2 += pixel2.red;
      }
      if ((pixel1.green >= min) && (pixel1.green <= max) && (pixel2.green >= min) && (pixel2.green <= max)) {
         sum1 += pixel1.green;
         sum2 += pixel2.green;
      }
      if ((pixel1.blue >= min) && (pixel1.blue <= max) && (pixel2.blue >= min) && (pixel2.blue <= max)) {
         sum1 += pixel1.blue;
         sum2 += pixel2.blue;
      }
    }
  }
  if (y < (long) MagickGetImageHeight(image1)) {

    return -1;
  }
  iterator1 = DestroyPixelIterator(iterator1);
  image1 = DestroyMagickWand(image1);
  iterator2 = DestroyPixelIterator(iterator2);
  image2 = DestroyMagickWand(image2);
  MagickWandTerminus();

  printf("%.100g\n", sum1/sum2);
  return(0);
}
min_ev=`ls -1r *.tiff | tail -n 1`
for ev in $( ls *.tiff ); do
  if [ ${ev} != ${min_ev} ]; then
    ev_name=`echo ${ev} | cut -d '.' -f1`
    ec=`exposure ${min_ev} ${ev}`
    convert ${ev} -evaluate multiply $ec ${ev_name}_corrected.tiff
    composite -compose CopyOpacity \( ${ev} -negate \) ${ev_name}_corrected.tiff ${ev_name}_mask.tiff
  fi
done



To get the final image, we need to merge the masks together and overlay the mask to the first image.

mask_files=`ls -r *_mask.tiff`
nb_mask=`ls  -r *_mask.tiff | wc -l`
if [[ $( echo "$nb_mask == 1.00" | bc ) == "1" ]]; then
  mv $mask_files final_mask.tiff
else
  composite ${mask_files} final_mask.tiff
fi
min_ev_name=`echo ${min_ev} | cut -d '.' -f1`
composite final_mask.tiff ${min_ev} ${min_ev_name}_fon.tiff



Here is the final bash script which takes in argument a folder with a number of catches. With the -c option, you can compile the latest dcraw and the exposure program (you need the ImageMagick development package). The script requires the following tools: bash, cut, tail, grep, bc, wget, gcc and ImageMagick Q16.

#!/bin/bash
compile=
color=1
wb=-w
ext=dng

while getopts 'w:e:c' OPTION
  do
    case $OPTION in
      w)      wb=-"$OPTARG"
               ;;
      e)      ext="$OPTARG"
               ;;
      c)      compile=1
               ;;
      ?)      printf "Usage: %s: [-w {dcraw white balance: a|w}] [-e {file extension}] [-c compile] {folder}\n" $(basename $0) >&2
               exit 2
               ;;
      esac
      done
shift $(($OPTIND - 1))

if [ -z $1 ]; then
  printf "Usage: %s: [-w {dcraw white balance: a|w}] [-e {file extension}] [-c compile] {folder}\n" $(basename $0) >&2
  exit 2;
fi

if [ "$compile" ]; then
  echo "--- compile dcraw"
  wget http://cybercom.net/~dcoffin/dcraw/dcraw.c
  gcc -o dcraw -O4 dcraw.c -lm -ljpeg -llcms
  rm dcraw.c
  echo "--- compile exposure"
  gcc `Magick-config --cflags --cppflags` -o exposure exposure.c `Magick-config --ldflags --libs`
fi

echo "--- process folder=${1}"
cd ${1}
rm *.tiff

echo "--- convert raw to tiff"
max_ev=`ls -1 *.${ext} | tail -n 1`
max_ev_name=`echo ${max_ev} | cut -d '.' -f1`
../dcraw -v $wb -W -o $color -q 3 -4 -T -c ${max_ev} > ${max_ev_name}.tiff 2> dcraw.log
cat dcraw.log
rgbg=`grep 'multipliers' dcraw.log | cut -d ' ' -f2,3,4,5`
rm dcraw.log
for other in $( ls -r *.${ext} ); do
  if [ ${other} != ${max_ev} ]; then
    ../dcraw -v -r $rgbg -W -o $color -q 3 -4 -T ${other}
  fi
done

min_ev=`ls -1r *.tiff | tail -n 1`
max_exp=0
for ev in $( ls *.tiff ); do
  if [ ${ev} != ${min_ev} ]; then
    ev_name=`echo ${ev} | cut -d '.' -f1`
    echo -n "--- calculate negative exposure between ${min_ev} and ${ev}="
    ec=`../exposure ${min_ev} ${ev}`
    echo $ec
    max_exp=`echo "1/$ec" | bc`
    echo "--- create a mask for ${ev}"
    convert ${ev} -evaluate multiply $ec ${ev_name}_corrected.tiff
    composite -compose CopyOpacity \( ${ev} -negate \) ${ev_name}_corrected.tiff ${ev_name}_mask.tiff
  fi
done

echo "--- merge the masks"
mask_files=`ls -r *_mask.tiff`
nb_mask=`ls  -r *_mask.tiff | wc -l`
if [[ $( echo "$nb_mask == 1.00" | bc ) == "1" ]]; then
  mv $mask_files final_mask.tiff
else
  composite ${mask_files} final_mask.tiff
fi

echo "--- overlay the mask with ${min_ev}"
min_ev_name=`echo ${min_ev} | cut -d '.' -f1`
composite final_mask.tiff ${min_ev} ${min_ev_name}_fon.tiff

rm *_mask.tiff *_corrected.tiff




[1] Guillermo Lujik – ZERO NOISE
[2] ImageMagick – Copy Opacity

Advertisements

~ by cedricbompart on December 1, 2009.

 
%d bloggers like this: