[Feature Request] Contrast Limited Adaptive Histogram Equalization (CLAHE)

recently stumbled across the idea of CLAHE and it looks like something that could be handy to have
in the toolbox
an example found here: http://www.volkerschatz.com/science/colhe.html
also had some code that works on pnm images
and I also found mention its in opencv

https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#CLAHE

4 Likes

There it is!

I have dropped hints from time to time but never requested this myself. I use this all of the time! I normally call it secret sauce or local contrast in my PlayRaws but it is really CLAHE in action.

In particular, Mr Schatz’s implementation pnmclahe usually gives me what I need, which I compiled for win7. However, it only works for 8-bit or 16-bit ppms or pgms. CLAHE being around for ages has had slightly different implementations in various software but pnmclahe remains my favorite.

Another implementation worth mentioning, besides larger packages like opencv and the like, is @snibgo’s eqlTile.bat.

@David_Tschumperle @Carmelo_DrRaw: please consider adding CLAHE to your respective apps.

2 Likes

Hi @afre - that’s a very good suggestion, and having an example of code certainly helps. What I need to understand is wether it is possible to process image tiles in parallel, as this is a requirement of the VIPS machinery. However, as far as I understand CLHAE is a local adjustment and therefore it should be feasible…

Anyway, I’m looking into this. Thanks!

I should thank you for considering my feedback and doing the programming :slight_smile:. As I said CLAHE has been around for a very long time, not long after retinex emerged. So, code examples are bountiful and superior methods have since been developed (i.e., if you are interested in doing more reading :stuck_out_tongue:).

I’ve added a new (slow) filter in G’MIC today, based on local histogram equalization.
It’s probably not the same as CLAHE (I didn’t look at how it is done), but it’s probably worth testing anyway :slight_smile:

5 Likes

Gross–the eye is so crusty now :stuck_out_tongue:. I will slowly digest the code… and then compare…

Maybe some more info to make this task easier :slight_smile:
The code of this filter is defined like this :

#@gui Equalize local histograms : fx_equalize_local_histograms, fx_equalize_local_histograms_preview(0)
#@gui : Strength (%) = float(75,0,100)
#@gui : Mode = choice(2,"Raw","Hard","Soft")
#@gui : Radius = int(4,1,16)
#@gui : Sigma = float(100,0,256)
#@gui : Regularization = float(8,0,32)
#@gui : Reduce halos = bool(1)
#@gui : sep = separator(), Channel(s) = choice(16,"All","RGBA [all]","RGB [all]","RGB [red]","RGB [green]","RGB [blue]","RGBA [alpha]","Linear RGB [all]","Linear RGB [red]","Linear RGB [green]","Linear RGB [blue]","YCbCr [luminance]","YCbCr [blue-red chrominances]","YCbCr [blue chrominance]","YCbCr [red chrominance]","YCbCr [green chrominance]","Lab [lightness]","Lab [ab-chrominances]","Lab [a-chrominance]","Lab [b-chrominance]","Lch [ch-chrominances]","Lch [c-chrominance]","Lch [h-chrominance]","HSV [hue]","HSV [saturation]","HSV [value]","HSI [intensity]","HSL [lightness]","CMYK [cyan]","CMYK [magenta]","CMYK [yellow]","CMYK [key]","YIQ [luma]","YIQ [chromas]")
#@gui : sep = separator(), Preview type = choice("Full","Forward horizontal","Forward vertical","Backward horizontal","Backward vertical","Duplicate top","Duplicate left","Duplicate bottom","Duplicate right")
#@gui : sep = separator(), note = note("<small>Author: <i>David Tschumperl&#233;</i>.      Latest update: <i>2018/01/31</i>.</small>")
fx_equalize_local_histograms :
  b0="normal" b1="overlay" b2="softlight"
  repeat $! l[$>]
    +ac "_fx_equalize_local_histograms ${1-6}",$7,1
    blend ${b$2},{$1%}
  endl done

_fx_equalize_local_histograms :
  +n 0,511 round.
  f. "
    init(
      const boundary = 1;
      const N = $3;
      const sigma = ($6?1:-1)*(0.1+$4);
      weights = vector512();
      for (k = 0, k<size(weights), ++k, # Pre-compute exponentials
        weights[k] = sigma>=0?exp(-sqr(k/sigma)):1 - exp(-sqr(k/sigma))
      );
    );

    bins = vector512(0);

    for (c = 0, c<s, ++c,
      V = crop(x - N,y - N,0,c,2*N+1,2*N+1,1,1);
      for (k = 0, k<size(V), ++k, # Compute local weighted histogram
        val = V[k];
        diff = abs(val - V[size(V)/2]);
        bins[val]+=weights[diff];
      );
    );

    sum = 0;
    for (k = 0, k<size(bins), ++k,
      sum+=bins[k];
      bins[k] = sum;
    );
    bins/=max(1e-5,sum);

    P = I;
    size(P)==1?(P = bins[P[0]]; 0):
    size(P)==2?(P = [ bins[P[0]], bins[P[1]] ]; 0):
    size(P)==3?(P = [ bins[P[0]], bins[P[1]], bins[P[2]] ]; 0):
    size(P)==4?(P = [ bins[P[0]], bins[P[1]], bins[P[2]], bins[P[3]] ]; 0);
    P"
  n. 0,255
  if $5 norm.. bilateral. ..,$5,{2+$5} fi
  k.

fx_equalize_local_histograms_preview :
  gui_split_preview "fx_equalize_local_histograms $*",$-1

Basically, what it does is :

  • For each pixel of the image, it computes the histogram of a neighborhood of size (2*N+1)x(2*N+1) where N is the radius parameter. Then it computes the cumulative histogram, and use this as a function to equalize the histogram locally.
  • Instead of computing the classical histogram, it computes a weighted version of the histogram, in order to favor or exclude pixel values in the neighborhood that have a similar value as the center pixel. This part probably needs a bit of reworking anyway :slight_smile:

I was told darktable has support for clahe in the past and dropped it again. Code seems to be still there though.

The adaptive part of CLAHE (contrast-limited adaptive histogram equalisation) is generally implemented as a coarse adaptation, eg blending the results from a 2x2 array of tiles, or 6x6 or whatever. That’s what I describe in [Adaptive] Contrast-limited equalisation. This gives a result where the effect varies smoothly across an image. This is often desirable, of course.

However, sometimes we want a very localised adaptation, to get little equalisation in smooth areas (eg skin) and more in high-contrast areas (eg eyelashes).

CLAHE is not well-suited when we have many tiles, eg 10x10 or more because the histogram has to be calculated for each tile so it is slow. In addition, if the tile size is small, each tile has too few pixels for a histogram to be meaningful.

At the other extreme, Maximise local contrast operates at fine granularity, eg a tile size of 3x3 pixels, so it is highly responsive to changing conditions and can easily be masked to have strong effect in high-contrast areas and little or no effect in low-contrast areas.

Horses for courses, and all that.

2 Likes

At a glance this is per-pixel histogram is that right? If so I guess that could be speeded up using a ‘sliding window’ approach… no doubt you know this and the problem is the time/effort to produce it :slight_smile:

Exactly.
Having a sliding window to compute the histogram wouldn’t work in this case, because these are ‘weighted’ histograms, where the weights depend on the value of the central pixel, which are then changing drastically at each pixel.
So we cannot easily deduce the histogram of neighborhood for position (x+1,y), knowing the one at (x,y).
The best we could do is that in the case the central value stays close to the previous one, then speed up a little bit the histogram computation. But I’m afraid the tests it adds are not worth the gain we get.

Ah I see, it does seem to be usable anyway; about 1 minute on my old core2 machine for 1920x1440. Interesting algorithm… It’s difficult not to get hooked on this subject (for me at least)!

dunno how exactly it compairs to CLAHE but I like what I’m seeing
so another tool added to the box, Thanks!

1 Like

@Magnade Thanks for asking. I might have taken ten years :rofl:. As a Canadian, I am supposed to be polite, sorry :joy:.

Interesting idiom. Rarely come across it.

1 Like

I seem to recall looking at this before but not putting in any requests then ether for whatever reason
so it felt like it was time to ask away

Sorry, yes, it is short for “Different horses for different courses”, ie some horses race best on conditions found at particular race courses, so the appropriate horse (or tool) should be picked for the job.

Indian Cricket team management uses this phrase after every match (to justify dropping players who have done well in recent past)!!

1 Like

I use CLAHE mostly to equalize the brightness of low or high contrast images. Equalize local histograms does much less of that. I will probably use it the way I use map_tones. In terms of slowness, it takes more than 5 min (maybe 8 min) for a linear gamma 6020x4024 image. Zoom and press left and right keys to toggle between images.