]> granicus.if.org Git - imagemagick/commitdiff
...
authorCristy <urban-warrior@imagemagick.org>
Sat, 24 Nov 2018 16:16:41 +0000 (11:16 -0500)
committerCristy <urban-warrior@imagemagick.org>
Sat, 24 Nov 2018 16:16:53 +0000 (11:16 -0500)
MagickCore/enhance.c

index b014ff4b1aa0a1ca5f8f744b52f2ee1fa7b35cad..d0ea04f51a35626e68ed7c60432d6395fc21bde6 100644 (file)
@@ -266,249 +266,475 @@ MagickExport MagickBooleanType BrightnessContrastImage(Image *image,
 %  contrast amplification is limited, so as to reduce this problem of noise
 %  amplification.
 %
+%  Adapted from implementation by Karel Zuiderveld, karel@cv.ruu.nl in
+%  "Graphics Gems IV", Academic Press, 1994.
+%
 %  The format of the CLAHEImage method is:
 %
-%      MagickBooleanType CLAHEImage(Image *image,const size_t width,
-%        const size_t height,const double bias,const double sans,
+%      MagickBooleanType CLAHEImage(Image *image,const size_t x_tiles,
+%        const size_t y_tiles,const size_t number_bins,const double clip_limit,
 %        ExceptionInfo *exception)
 %
 %  A description of each parameter follows:
 %
 %    o image: the image.
 %
-%    o width: the width of the local neighborhood.
+%    o x_tiles: number of tile divisions to use in horizontal direction.
 %
-%    o height: the height of the local neighborhood.
+%    o y_tiles: number of tile divisions to use in vertical direction.
 %
-%    o bias: the mean bias.
+%    o number_bins: number of bins for histogram ("dynamic range").
 %
-%    o sans: not used
+%    o clip_limit: contrast limit for localised changes in contrast. A limit
+%      than 1 results in standard non-contrast limited AHE.
 %
 %    o exception: return any errors or warnings in this structure.
 %
 */
-MagickExport MagickBooleanType CLAHEImage(Image *image,const size_t width,
-  const size_t height,const size_t bias,const double sans,
-  ExceptionInfo *exception)
+
+static void ClipCLAHEHistogram(const double clip_limit,const size_t number_bins,
+  size_t *histogram)
 {
-#define CLAHEImageTag  "CLAHE/Image"
+#define NumberCLAHEGrays  (65536)
+#define MaxCLAHETiles  (16)
 
-  CacheView
-    *image_view;
+  register ssize_t
+    i;
 
-  double
-    black[CompositePixelChannel+1],
-    *clahe_map,
-    *histogram,
-    *map,
-    white[CompositePixelChannel+1];
+  size_t
+    cumulative_excess,
+    previous_excess,
+    step;
 
-  MagickBooleanType
-    status;
+  ssize_t
+    delta;
 
-  MagickOffsetType
-    progress;
+  /*
+    Compute total number of excess pixels.
+  */
+  cumulative_excess=0;
+  for (i=0; i < (ssize_t) number_bins; i++)
+  {
+    delta=(ssize_t) histogram[i]-(ssize_t) clip_limit;
+    if (delta > 0)
+      cumulative_excess+=delta;
+  }
+  /*
+    Clip histogram and redistribute excess pixels across all bins.
+  */
+  step=cumulative_excess/number_bins;
+  delta=(ssize_t) (clip_limit-step);
+  for (i=0; i < (ssize_t) number_bins; i++)
+  {
+    if ((double) histogram[i] > clip_limit)
+      histogram[i]=(size_t) clip_limit;
+    else
+      if ((ssize_t) histogram[i] > delta)
+        {
+          cumulative_excess-=histogram[i]-delta;
+          histogram[i]=(size_t) clip_limit;
+        }
+      else
+        {
+          cumulative_excess-=step;
+          histogram[i]+=step;
+        }
+  }
+  /*
+    Redistribute remaining excess.
+  */
+  do
+  {
+    register size_t
+      *p;
+
+    size_t
+      *q;
+
+    previous_excess=cumulative_excess;
+    p=histogram;
+    q=histogram+number_bins;
+    while ((cumulative_excess != 0) && (p < q))
+    {
+      step=number_bins/cumulative_excess;
+      if (step < 1)
+        step=1;
+      for (p=histogram; (p < q) && (cumulative_excess != 0); p+=step)
+        if ((double) *p < clip_limit)
+          {
+            (*p)++;
+            cumulative_excess--;
+          }
+      p++;
+    }
+  } while ((cumulative_excess != 0) && (cumulative_excess < previous_excess));
+}
+
+static void GenerateCLAHEHistogram(const size_t width,const size_t tile_width,
+  const size_t tile_height,const size_t number_bins,const unsigned short *lut,
+  const unsigned short *pixels,size_t *histogram)
+{
+  register const unsigned short
+    *p;
 
   register ssize_t
     i;
 
+  /*
+    Classify the pixels into a gray histogram.
+  */
+  for (i=0; i < (ssize_t) number_bins; i++)
+    histogram[i]=0L;
+  p=pixels;
+  for (i=0; i < (ssize_t) tile_height; i++)
+  {
+    const unsigned short
+      *q;
+
+    q=p+tile_width;
+    while (p < q)
+      histogram[lut[*p++]]++;
+    q+=width;
+    p=q-tile_width;
+  }
+}
+
+static void InterpolateCLAHE(const size_t width,const size_t *Q12,
+  const size_t *Q22,const size_t *Q11,const size_t *Q21,const size_t tile_width,
+  const size_t tile_height,const unsigned short *lut,unsigned short *pixels)
+{
   ssize_t
     y;
 
+  unsigned short
+    intensity;
+
   /*
-    Allocate and initialize histogram arrays.
+    Bilinear interpolate four tiles to eliminate boundary artifacts.
   */
-  assert(image != (Image *) NULL);
-  assert(image->signature == MagickCoreSignature);
-  (void) width;
-  (void) height;
-  (void) bias;
-  (void) sans;
-  if (image->debug != MagickFalse)
-    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
-  clahe_map=(double *) AcquireQuantumMemory(MaxMap+1UL,MaxPixelChannels*
-    sizeof(*clahe_map));
-  histogram=(double *) AcquireQuantumMemory(MaxMap+1UL,MaxPixelChannels*
-    sizeof(*histogram));
-  map=(double *) AcquireQuantumMemory(MaxMap+1UL,MaxPixelChannels*sizeof(*map));
-  if ((clahe_map == (double *) NULL) || (histogram == (double *) NULL) ||
-      (map == (double *) NULL))
+  for (y=(ssize_t) tile_height; y > 0; y--)
+  {
+    register ssize_t
+      x;
+
+    for (x=(ssize_t) tile_width; x > 0; x--)
     {
-      if (map != (double *) NULL)
-        map=(double *) RelinquishMagickMemory(map);
-      if (histogram != (double *) NULL)
-        histogram=(double *) RelinquishMagickMemory(histogram);
-      if (clahe_map != (double *) NULL)
-        clahe_map=(double *) RelinquishMagickMemory(clahe_map);
-      ThrowBinaryException(ResourceLimitError,"MemoryAllocationFailed",
-        image->filename);
+      intensity=lut[*pixels];
+      *pixels++=(unsigned short ) (PerceptibleReciprocal((double) tile_width*
+        tile_height)*(y*(x*Q12[intensity]+(tile_width-x)*Q22[intensity])+
+        (tile_height-y)*(x*Q11[intensity]+(tile_width-x)*Q21[intensity])));
     }
+    pixels+=(width-tile_width);
+  }
+}
+
+static void GenerateCLAHELut(const unsigned short min_intensity,
+  const unsigned short max_intensity,const size_t number_bins,
+  unsigned short *lut)
+{
+  ssize_t
+    i;
+
+  unsigned short
+    delta;
+
   /*
-    Form histogram.
+    Scale input image [min_intensity,max_intensity] to [0,number_bins-1].
   */
-  status=MagickTrue;
-  (void) memset(histogram,0,(MaxMap+1)*GetPixelChannels(image)*
-    sizeof(*histogram));
-  image_view=AcquireVirtualCacheView(image,exception);
-  for (y=0; y < (ssize_t) image->rows; y++)
+  delta=(unsigned short) ((max_intensity-min_intensity)/number_bins+1);
+  for (i=(ssize_t) min_intensity; i <= (ssize_t) max_intensity; i++)
+    lut[i]=(unsigned short) ((i-min_intensity)/delta);
+}
+
+static void MapCLAHEHistogram(const unsigned short min_intensity,
+  const unsigned short max_intensity,const size_t number_bins,
+  const size_t number_pixels,size_t *histogram)
+{
+  double
+    scale,
+    sum;
+
+  register ssize_t
+    i;
+
+  /*
+    Rescale histogram to range [min-intensity .. max-intensity].
+  */
+  scale=(double) (max_intensity-min_intensity)/number_pixels;
+  sum=0.0;
+  for (i=0; i < (ssize_t) number_bins; i++)
   {
-    register const Quantum
-      *magick_restrict p;
+    sum+=histogram[i];
+    histogram[i]=(size_t) (min_intensity+scale*sum);
+    if (histogram[i] > max_intensity)
+      histogram[i]=max_intensity;
+  }
+}
 
-    register ssize_t
-      x;
 
-    if (status == MagickFalse)
-      continue;
-    p=GetCacheViewVirtualPixels(image_view,0,y,image->columns,1,exception);
-    if (p == (const Quantum *) NULL)
-      {
-        status=MagickFalse;
-        continue;
-      }
-    for (x=0; x < (ssize_t) image->columns; x++)
-    {
-      for (i=0; i < (ssize_t) GetPixelChannels(image); i++)
-      {
-        double
-          intensity;
+static MagickBooleanType CLAHE(const size_t width,const size_t height,
+  const unsigned short min_intensity,const unsigned short max_intensity,
+  const size_t x_tiles,const size_t y_tiles,const size_t number_bins,
+  const double clip_limit,unsigned short *pixels)
+{
+  register unsigned short
+    *p;
 
-        intensity=(double) p[i];
-        if ((image->channel_mask & SyncChannels) != 0)
-          intensity=GetPixelIntensity(image,p);
-        histogram[GetPixelChannels(image)*ScaleQuantumToMap(
-          ClampToQuantum(intensity))+i]++;
-      }
-      p+=GetPixelChannels(image);
+  size_t
+    *buffer,
+    limit,
+    number_pixels,
+    tile_height,
+    tile_width;
+
+  ssize_t
+    y;
+
+  unsigned short
+    lut[NumberCLAHEGrays];
+
+  /*
+    Constrast limited adapted histogram equalization.
+  */
+  assert(x_tiles < MaxCLAHETiles);
+  assert(y_tiles < MaxCLAHETiles);
+  assert((width % x_tiles) == 0);
+  assert((height % y_tiles) == 0);
+  assert(max_intensity < NumberCLAHEGrays);
+  assert(min_intensity < max_intensity);
+  assert((x_tiles >= 2) || (y_tiles >= 2));
+  assert(number_bins != 0);
+  if (clip_limit == 1.0)
+    return(MagickTrue);
+  buffer=(size_t *) AcquireQuantumMemory(x_tiles*y_tiles,number_bins*
+    sizeof(*buffer));
+  if (buffer == (size_t *) NULL)
+    return(MagickFalse);
+  tile_width=width/x_tiles;
+  tile_height=height/y_tiles;
+  number_pixels=tile_width*tile_height;
+  limit=1UL << 14;
+  if (clip_limit > 0.0)
+    {
+      limit=(size_t) (clip_limit*(tile_width*tile_height)/number_bins);
+      limit=(limit < 1UL) ? 1UL : limit;
     }
-  }
-  image_view=DestroyCacheView(image_view);
+  GenerateCLAHELut(min_intensity,max_intensity,number_bins,lut);
   /*
-    Integrate the histogram to get the equalization map.
+    Generate greylevel mappings for each tile.
   */
-  for (i=0; i < (ssize_t) GetPixelChannels(image); i++)
+  p=pixels;
+  for (y=0; y < (ssize_t) y_tiles; y++)
   {
-    double
-      intensity;
-
     register ssize_t
-      j;
+      x;
 
-    intensity=0.0;
-    for (j=0; j <= (ssize_t) MaxMap; j++)
+    for (x=0; x < (ssize_t) x_tiles; x++)
     {
-      intensity+=histogram[GetPixelChannels(image)*j+i];
-      map[GetPixelChannels(image)*j+i]=intensity;
+      size_t
+        *histogram;
+
+      histogram=buffer+(number_bins*(y*x_tiles+x));
+      GenerateCLAHEHistogram(width,tile_width,tile_height,number_bins,lut,p,
+        histogram);
+      ClipCLAHEHistogram((double) limit,number_bins,histogram);
+      MapCLAHEHistogram(min_intensity,max_intensity,number_bins,number_pixels,
+       histogram);
+      p+=tile_width;
     }
+    p+=(tile_height-1)*width;
   }
-  (void) memset(clahe_map,0,(MaxMap+1)*GetPixelChannels(image)*
-    sizeof(*clahe_map));
-  (void) memset(black,0,sizeof(*black));
-  (void) memset(white,0,sizeof(*white));
-  for (i=0; i < (ssize_t) GetPixelChannels(image); i++)
+  /*
+    Interpolate greylevel mappings to get CLAHE image.
+  */
+  p=pixels;
+  for (y=0; y <= (ssize_t) y_tiles; y++)
   {
-    register ssize_t
-      j;
+    OffsetInfo
+      offset;
 
-    black[i]=map[i];
-    white[i]=map[GetPixelChannels(image)*MaxMap+i];
-    if (black[i] != white[i])
-      for (j=0; j <= (ssize_t) MaxMap; j++)
-        clahe_map[GetPixelChannels(image)*j+i]=(double)
-          ScaleMapToQuantum((double) ((MaxMap*(map[
-          GetPixelChannels(image)*j+i]-black[i]))/(white[i]-black[i])));
-  }
-  histogram=(double *) RelinquishMagickMemory(histogram);
-  map=(double *) RelinquishMagickMemory(map);
-  if (image->storage_class == PseudoClass)
-    {
-      register ssize_t
-        j;
+    RectangleInfo
+      tile;
 
-      /*
-        CLAHE colormap.
-      */
-      for (j=0; j < (ssize_t) image->colors; j++)
+    register ssize_t
+      x;
+
+    tile.height=tile_height;
+    tile.y=y-1;
+    offset.y=tile.y+1;
+    if (y == 0)  /* top row */
       {
-        if ((GetPixelRedTraits(image) & UpdatePixelTrait) != 0)
-          {
-            PixelChannel channel = GetPixelChannelChannel(image,RedPixelChannel);
-            if (black[channel] != white[channel])
-              image->colormap[j].red=clahe_map[GetPixelChannels(image)*
-                ScaleQuantumToMap(ClampToQuantum(image->colormap[j].red))+
-                channel];
-          }
-        if ((GetPixelGreenTraits(image) & UpdatePixelTrait) != 0)
-          {
-            PixelChannel channel = GetPixelChannelChannel(image,
-              GreenPixelChannel);
-            if (black[channel] != white[channel])
-              image->colormap[j].green=clahe_map[GetPixelChannels(image)*
-                ScaleQuantumToMap(ClampToQuantum(image->colormap[j].green))+
-                channel];
-          }
-        if ((GetPixelBlueTraits(image) & UpdatePixelTrait) != 0)
-          {
-            PixelChannel channel = GetPixelChannelChannel(image,BluePixelChannel);
-            if (black[channel] != white[channel])
-              image->colormap[j].blue=clahe_map[GetPixelChannels(image)*
-                ScaleQuantumToMap(ClampToQuantum(image->colormap[j].blue))+
-                channel];
-          }
-        if ((GetPixelAlphaTraits(image) & UpdatePixelTrait) != 0)
+        tile.height=tile_height >> 1;
+        tile.y=0;
+        offset.y=0;
+      }
+    else
+      if (y == (ssize_t) y_tiles)  /* bottom row */
+        {
+          tile.height=tile_height >> 1;
+          tile.y=(ssize_t) y_tiles-1;
+          offset.y=tile.y;
+        }
+    for (x=0; x <= (ssize_t) x_tiles; x++)
+    {
+      tile.width=tile_width;
+      tile.x=x-1;
+      offset.x=tile.x+1;
+      if (x == 0)  /* left column */
+        {
+          tile.width=tile_width >> 1;
+          tile.x=0;
+          offset.x=0;
+        }
+      else
+        if (x == (ssize_t) x_tiles)  /* right column */
           {
-            PixelChannel channel = GetPixelChannelChannel(image,
-              AlphaPixelChannel);
-            if (black[channel] != white[channel])
-              image->colormap[j].alpha=clahe_map[GetPixelChannels(image)*
-                ScaleQuantumToMap(ClampToQuantum(image->colormap[j].alpha))+
-                channel];
+            tile.width=tile_width >> 1;
+            tile.x=(ssize_t) x_tiles-1;
+            offset.x=tile.x;
           }
-      }
+      InterpolateCLAHE(width,buffer+(number_bins*(tile.y*x_tiles+tile.x)),
+        buffer+(number_bins*(tile.y*x_tiles+offset.x)),
+        buffer+(number_bins*(offset.y*x_tiles+tile.x)),
+        buffer+(number_bins*(offset.y*x_tiles+offset.x)),tile.width,tile.height,
+        lut,p);
+      p+=tile.width;
     }
+    p+=width*(tile.height-1);
+  }
+  buffer=RelinquishMagickMemory(buffer);
+  return(MagickTrue);
+}
+
+MagickExport MagickBooleanType CLAHEImage(Image *image,const size_t x_tiles,
+  const size_t y_tiles,const size_t number_bins,const double clip_limit,
+  ExceptionInfo *exception)
+{
+#define CLAHEImageTag  "CLAHE/Image"
+
+  MagickBooleanType
+    status;
+
+  MagickOffsetType
+    progress;
+
+  MemoryInfo
+    *pixel_cache;
+
+  OffsetInfo
+    tile;
+
+  register ssize_t
+    i;
+
+  ssize_t
+    y;
+
+  size_t
+    height,
+    width;
+
+  unsigned short
+    *pixels;
+
   /*
-    CLAHE image.
+    Allocate and initialize histogram arrays.
   */
-  progress=0;
-  image_view=AcquireAuthenticCacheView(image,exception);
+  assert(image != (Image *) NULL);
+  assert(image->signature == MagickCoreSignature);
+  if (image->debug != MagickFalse)
+    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
+  if (SetImageStorageClass(image,DirectClass,exception) == MagickFalse)
+    return(MagickFalse);
+  status=MagickTrue;
+  tile.x=(ssize_t) (x_tiles < 2 ? 2 : x_tiles >= MaxCLAHETiles ?
+    MaxCLAHETiles-1 : x_tiles);
+  tile.y=(ssize_t) (y_tiles < 2 ? 2 : y_tiles >= MaxCLAHETiles ?
+    MaxCLAHETiles-1 : y_tiles);
+  width=((image->columns+tile.x/2)/tile.x)*tile.x;
+  height=((image->rows+tile.y/2)/tile.y)*tile.y;
+  pixel_cache=AcquireVirtualMemory(width,height*sizeof(*pixels));
+  if (pixel_cache == (MemoryInfo *) NULL)
+    ThrowBinaryException(ResourceLimitError,"MemoryAllocationFailed",
+      image->filename);
+  pixels=(unsigned short *) GetVirtualMemoryBlob(pixel_cache);
 #if defined(MAGICKCORE_OPENMP_SUPPORT)
   #pragma omp parallel for schedule(static) shared(progress,status) \
-    magick_number_threads(image,image,image->rows,1)
+    magick_number_threads(image,image,GetPixelChannels(image),1)
 #endif
-  for (y=0; y < (ssize_t) image->rows; y++)
+  for (i=0; i < (ssize_t) GetPixelChannels(image); i++)
   {
-    register Quantum
-      *magick_restrict q;
+    CacheView
+      *image_view;
 
     register ssize_t
       x;
 
+    size_t
+      n;
+
+    PixelChannel channel = GetPixelChannelChannel(image,i);
+    PixelTrait traits = GetPixelChannelTraits(image,channel);
+    if ((traits & UpdatePixelTrait) == 0)
+      continue;
     if (status == MagickFalse)
       continue;
-    q=GetCacheViewAuthenticPixels(image_view,0,y,image->columns,1,exception);
-    if (q == (Quantum *) NULL)
+    image_view=AcquireVirtualCacheView(image,exception);
+    n=0;
+    for (y=0; y < (ssize_t) height; y++)
+    {
+      register const Quantum
+        *magick_restrict p;
+
+      if (status == MagickFalse)
+        continue;
+      p=GetCacheViewVirtualPixels(image_view,0,y,width,1,exception);
+      if (p == (const Quantum *) NULL)
+        {
+          status=MagickFalse;
+          continue;
+        }
+      for (x=0; x < (ssize_t) image->columns; x++)
       {
-        status=MagickFalse;
+        pixels[n++]=ScaleQuantumToShort(p[i]);
+        p+=GetPixelChannels(image);
+      }
+    }
+    image_view=DestroyCacheView(image_view);
+    if (status == MagickFalse)
+      continue;
+    status=CLAHE(width,height,0,65535,(size_t) tile.x,(size_t) tile.y,
+      number_bins == 0 ? (size_t) 128 : number_bins,clip_limit,pixels);
+    if (status == MagickFalse)
+      {
+        (void) ThrowMagickException(exception,GetMagickModule(),
+          ResourceLimitError,"MemoryAllocationFailed","`%s'",image->filename);
         continue;
       }
-    for (x=0; x < (ssize_t) image->columns; x++)
+    image_view=AcquireAuthenticCacheView(image,exception);
+    n=0;
+    for (y=0; y < (ssize_t) image->rows; y++)
     {
-      register ssize_t
-        j;
+      register Quantum
+        *magick_restrict q;
 
-      for (j=0; j < (ssize_t) GetPixelChannels(image); j++)
-      {
-        PixelChannel channel = GetPixelChannelChannel(image,j);
-        PixelTrait traits = GetPixelChannelTraits(image,channel);
-        if (((traits & UpdatePixelTrait) == 0) || (black[j] == white[j]))
+      if (status == MagickFalse)
+        continue;
+      q=GetCacheViewAuthenticPixels(image_view,0,y,image->columns,1,exception);
+      if (q == (Quantum *) NULL)
+        {
+          status=MagickFalse;
           continue;
-        q[j]=ClampToQuantum(clahe_map[GetPixelChannels(image)*
-          ScaleQuantumToMap(q[j])+j]);
+        }
+      for (x=0; x < (ssize_t) image->columns; x++)
+      {
+        q[i]=ScaleShortToQuantum(pixels[n++]);
+        q+=GetPixelChannels(image);
       }
-      q+=GetPixelChannels(image);
+      if (SyncCacheViewAuthenticPixels(image_view,exception) == MagickFalse)
+        status=MagickFalse;
     }
-    if (SyncCacheViewAuthenticPixels(image_view,exception) == MagickFalse)
-      status=MagickFalse;
+    image_view=DestroyCacheView(image_view);
     if (image->progress_monitor != (MagickProgressMonitor) NULL)
       {
         MagickBooleanType
@@ -518,13 +744,13 @@ MagickExport MagickBooleanType CLAHEImage(Image *image,const size_t width,
         #pragma omp atomic
 #endif
         progress++;
-        proceed=SetImageProgress(image,CLAHEImageTag,progress,image->rows);
+        proceed=SetImageProgress(image,CLAHEImageTag,progress,
+          GetPixelChannels(image));
         if (proceed == MagickFalse)
           status=MagickFalse;
       }
   }
-  image_view=DestroyCacheView(image_view);
-  clahe_map=(double *) RelinquishMagickMemory(clahe_map);
+  pixel_cache=RelinquishVirtualMemory(pixel_cache);
   return(status);
 }
 \f