https://doi.org/10.5201/ipol.2018.236
Raw File
Tip revision: cd8faa35b66794fecfc5c1e49c826d1a287ab410 authored by Software Heritage on 14 November 2018, 00:00:00 UTC
ipol: Deposit 602 in collection ipol
Tip revision: cd8faa3
libmlhe.cpp
/*----------------------------------------------------------------------------

Copyright (c) 2018 J.L. Lisani (joseluis.lisani@uib.es)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

----------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>


#include "libmlhe.h"

//Class constructor
mlhe::mlhe(unsigned char *Iinput, int w, int h): w(w), h(h)
{
    //initialize output image
    I=new unsigned char[w*h];
    memcpy(I, Iinput, w*h);
    
    //auxiliary variable
    marked = new bool[w*h];
}

//Class destructor
mlhe::~mlhe()
{
    delete[] I;
    delete[] marked;
}

//Return processed image
unsigned char *mlhe::get_result()
{
    unsigned char *Iout=new unsigned char[w*h];
    memcpy(Iout, I, w*h);
    return Iout;
}

//First call to recursive algorithm (Algorithm 2)
void mlhe::apply_algorithm(int L, int A, float rrmax, float rrmin, int option,
                           int PAE_N, float PAE_smin, float PAE_smax,
                           float CLAHE_clip)
{
    Lmax = L;
    Amin = A;
    rmax = rrmax;
    rmin = rrmin;
    optionHE = option;
    N = PAE_N;
    smin = PAE_smin;
    smax = PAE_smax;
    clip = CLAHE_clip;
    
    
    //initialize auxiliary variables
    int *selected = new int[w*h]; //list of selected pixels
    //initially all pixels are selected
    for (int n=0; n < w*h; n++) selected[n]=n;
    int nselected = w*h;
    
    //list of 'active' pixels for the  extraction of connected components
    for (int n=0; n < w*h; n++) marked[n]=false;
    
    
    //recursive_histo on selected pixels
    recursive_histo(selected, nselected, 0, 255);
    
    delete[] selected;
}


//Algorithm 7: contrast limited histogram equalization
void mlhe::clahe(int *selected, int nselected, int min, int max)
{
    float hist[256];
    float histAc[256];

    //check range of output values
    vector <int> values;
    for (int n=0; n < nselected; n++) values.push_back(I[selected[n]]);
    sort(values.begin(), values.end());
    float rangeI = (float) (values[nselected-1] - values[0]);
    if (rangeI == 0) return; //return if all the pixels have the same value

    //compute cumulative histogram of values
    memset(hist, 0, 256*sizeof(float));
    for (int n=0; n < nselected; n++) hist[I[selected[n]]]+=1.0f;
    //normalize histogram
    for (int n=0; n < 256; n++) hist[n] /= (float) nselected;
    
    //CLAHE step
    //clip values above threshold
    float pexcess = 0.0f;
    for (int n=min; n <= max; n++)
        if (hist[n] > clip) {
            pexcess += (hist[n]-clip);
            hist[n] = clip;
        }
    //distribute excess values among all levels
    float peach = pexcess / (max-min+1);
    for (int n=min; n <= max; n++) hist[n] += peach;
    
    //compute cumulative histogram of values
    histAc[0]= (float) hist[0];
    for (int n=1; n < 256; n++) histAc[n] = (float) hist[n] + histAc[n-1];  
    
    //check range of output values
    int v, vmin=255, vmax=0;
    values.clear();
    for (int n=0; n < nselected; n++) {
        //apply tone mapping (i.e. cumulative distribution function)
        v = min + (int) ((histAc[I[selected[n]]] * (float) (max-min)) + 0.5f);
        values.push_back(v);
        if (v < vmin) vmin = v;
        if (v > vmax) vmax = v;
    }
    if (vmin == vmax) return; //this can happen due to the rounding of 
                              //floats to integers
    
    //get output values
    for (int n=0; n < nselected; n++) {
        I[selected[n]] = values[n];
    }
    
    

}


//auxiliary data structure for storing linear segments used in PAE
struct pointxy {
    float x, y;
    float slopetonext;
};

//Algorithm 8: piecewise affine histogram equalization (PAE)
void mlhe::piecewise_affine_histogram_equalization(int *selected, int nselected,
                                                   int min, int max)
{
    int hist[256];
    float histAc[256];
        
    //check range of input values
    vector <int> values;
    for (int n=0; n < nselected; n++) values.push_back(I[selected[n]]);
    sort(values.begin(), values.end());
    float rangeI = (float) (values[nselected-1] - values[0]);
    if (rangeI == 0) return; //return if all the pixels have the same value
    
    //compute histogram of values
    memset(hist, 0, 256*sizeof(int));
    for (int n=0; n < nselected; n++) hist[I[selected[n]]]++;
    //compute cumulative histogram of values
    histAc[0]= (float) hist[0];
    for (int n=1; n < 256; n++) histAc[n] = (float) hist[n] + histAc[n-1];
    for (int n=0; n < 256; n++) histAc[n]/= (float) nselected;
    
    //get (x_i, y_i) points for the piecewise affine approximation of 
    //the cumulative histogram
    //initial point: (min, min)
    //separation between y_i points = 1/N, N=number of intervals (N >= 1)
    vector < pointxy > points;
    struct pointxy p;
    p.x = min;
    p.y = min;
    points.push_back(p);
    float next_y = 1.0f/(float) N;
    int i=1;
    int n=0;
    float slope;
    while (i < N) {
        for (;(n < 256) && (histAc[n] < next_y); n++);
        struct pointxy p;
        p.x = (float) n;
        p.y = (float) min + next_y * (max-min);
        if (p.x != points[i-1].x) { //not infinite slope
            slope = (p.y - points[i-1].y)/(p.x - points[i-1].x);
        } else slope = -1.0; //infinite slope
        if ((slope > smax) || (slope == -1)) 
            p.y = points[i-1].y + smax * (p.x - points[i-1].x);
        if (slope < smin)  
            p.y = points[i-1].y + smin * (p.x - points[i-1].x);
        points.push_back(p);
        i++;
        next_y += 1.0f/(float) N;
    }
    //add last interval with x_i=max
    p.x = (float) max;
    p.y = (float) max;
    if (p.x != points[i-1].x) { //not infinite slope
        slope = (p.y - points[i-1].y)/(p.x - points[i-1].x);
    } else slope = -1.0; //infinite slope
    if ((slope > smax) || (slope == -1)) 
        p.y = points[i-1].y + smax * (p.x - points[i-1].x);
    if (slope < smin)  
        p.y = points[i-1].y + smin * (p.x - points[i-1].x);
    points.push_back(p);
    //check that final value is = max
    //reject if > max, since it implies saturation
    //reject if < max since it implies that the dynamic range is not fully used
    //(it may happen when too few values are in the cc)
    if (p.y > max) {
        //rescale all y values so that final value=max, but keeping 
        //the initial value = min
        for (int k = 0; k < points.size(); k++) 
            points[k].y = (float) min + (points[k].y - (float) min) * \
                          ((float) max - (float) min)/(p.y - (float) min);
    }
    if (p.y < max) return;
        
    
    //assign corresponding interval to each value 
    //(assign index to initial point of interval)
    int interval[256];
    for (int i=0; i < points.size()-1; i++) {
        int i_initial = (int) (points[i].x + 0.5f);
        int i_final = (int) (points[i+1].x + 0.5f);
        for (int k=i_initial; k <= i_final; k++) interval[k] = i;
    }
    
    //add slope information
    for (int i=0; i < points.size(); i++) {
        if (i < points.size()-1) {
            if (points[i+1].x != points[i].x) { //not infinite slope
                points[i].slopetonext = \
                    (points[i+1].y - points[i].y)/(points[i+1].x - points[i].x);
            } else points[i].slopetonext = -1.0; //infinite slope
        } else points[i].slopetonext=0; //last point
    }
    
    //Apply transform
    float rangeF;
    int v, vmin=255, vmax=0;
    values.clear();
    for (int n=0; n < nselected; n++) {
        int i = interval[I[selected[n]]]; //get interval
        v = (int) ( points[i].y + points[i].slopetonext * \
            ( (float) I[selected[n]] - points[i].x ) + 0.5f);
        values.push_back(v);
        if (v < vmin) vmin = v;
        if (v > vmax) vmax = v;
    }
    if (vmin == vmax) return; //this can happen due to the rounding of 
                              //floats to integers
    
    //get output values
    for (int n=0; n < nselected; n++) {
        I[selected[n]] = values[n];
    }

}



//Algorithm 5: classical histogram equalization
void mlhe::histogram_equalization(int *selected, 
                                  int nselected, int min, int max)
{
    int hist[256];
    float histAc[256];

    //check range of input values
    vector <int> values;
    for (int n=0; n < nselected; n++) values.push_back(I[selected[n]]);
    sort(values.begin(), values.end());
    float rangeI = (float) (values[nselected-1] - values[0]);
    if (rangeI == 0) return; //return if all the pixels have the same value

    //compute histogram of values
    memset(hist, 0, 256*sizeof(int));
    for (int n=0; n < nselected; n++) hist[I[selected[n]]]++;
    //compute cumulative histogram of values
    histAc[0]= (float) hist[0];
    for (int n=1; n < 256; n++) histAc[n] = (float) hist[n] + histAc[n-1];
    for (int n=0; n < 256; n++) histAc[n]/= (float) nselected;
        
    //check range of output values
    float rangeF;
    int v, vmin=255, vmax=0;
    values.clear();
    for (int n=0; n < nselected; n++) {
        //apply tone mapping (i.e. cumulative distribution function)
        v = min + (int) ((histAc[I[selected[n]]] * (float) (max-min)) + 0.5f);
        values.push_back(v);
        if (v < vmin) vmin = v;
        if (v > vmax) vmax = v;
    }
    
    float ratioranges;
    if (vmin != vmax) { //vmin = vmax can happen due to the rounding of 
                        //floats to integers
    
        rangeF = (float) (vmax-vmin);

        ratioranges = (rangeI != 0)?(rangeF/rangeI):(-1);
        
        //check ratio between original and processed ranges of values
        if ((ratioranges != -1) && (ratioranges <= rmax) && \
            (ratioranges >= rmin)) {
            //get output values
            for (int n=0; n < nselected; n++) {
                I[selected[n]] = values[n];
            }
        }
    
    }
    
}


//Algorithm 4 (lines 8 to 15): fill connected component
void mlhe::fill_cc(vector<int> &cc, int p0)
{
    //recursively grow connected component, use 4-connectivity
    int p = p0;
    marked[p] = false; //remove from list so it is not used again
    bool grow = true;
    queue < int > listtogrow;
    int k=1;
    while (grow) {
        cc.push_back(p); 
        int x = p%w;
        int y = p/w;
        if ((x > 0) && (marked[x-1+y*w])) { //add left neighbor to list 
            listtogrow.push(x-1+y*w); 
            marked[x-1+y*w] = false;
        }
        if ((x+1 < w) && (marked[x+1+y*w])) { //add right neighbor to list 
            listtogrow.push(x+1+y*w); 
            marked[x+1+y*w] = false; 
        }
        if ((y > 0) && (marked[x+(y-1)*w])) { //add upper neighbor to list 
            listtogrow.push(x+(y-1)*w); 
            marked[x+(y-1)*w] = false; 
        }
        if ((y+1 < h) && (marked[x+(y+1)*w])) { //add lower neighbor to list 
            listtogrow.push(x+(y+1)*w); 
            marked[x+(y+1)*w] = false; 
        }
        grow = !listtogrow.empty();
        if (grow) {
            p = listtogrow.front(); //get first element of the list
            listtogrow.pop(); //remove from list
        }
    } 
}

//Algorithm 3: 
// - get bi-level sets
// - extract connected components (Algorithm 4)
// - apply Algorithm 2 to connected components (use recursivity)
void mlhe::process_bilevelset(int *selected, int nselected, 
                              int min, int max)
{
    //mark pixels belonging to the level set
    for (int n=0; n < nselected; n++)
        if ((I[selected[n]] >= min) && (I[selected[n]] <= max)) 
            marked[selected[n]]=true;
   
    //get connected components of level set (Algorithm 4: lines 1 to 7)
    vector< vector<int> > ccs; //a list of lists of pixels
    for (int n=0; n < nselected; n++) 
        if (marked[selected[n]]) {
            //new connected component: a list of pixels 
            vector<int> cc;
            fill_cc(cc, selected[n]);
            ccs.push_back(cc);
        }
    
   //apply recursive histo equalization to each connected component
   for (int i=0; i < ccs.size(); i++) {
        //list of selected pixels for further histogram equalization
        int nselected2 = ccs[i].size();
        if (nselected2 >= Amin) { //do not process small connected components
            int *selected2 = new int[nselected2];
            for (int n = 0; n < nselected2; n++) selected2[n] = ccs[i][n];
            //recursive_histo on selected pixels
            recursive_histo(selected2, nselected2, min, max);
            delete[] selected2;
        }
    }

}

//Algorithm 2: 
// - apply histogram equalization in range [min, max]
// - process bi-level set [min, (min+max)/2]
// - process bi-level set [(min+max)/2 + 1, max]
void mlhe::recursive_histo(int *selected, int nselected,
                           int min, int max)
{

    int level = (int) (log2(256.0 / (double) (max-min+1)) + 0.5);

    //histogram equalization
    switch (optionHE) {
        case 1: //classic method (Algorithm 5)
            histogram_equalization(selected, nselected, min, max);
            break;
        
        case 2: //PAE method (Algorithm 8)
            piecewise_affine_histogram_equalization(selected, nselected, min, max);
            break;
        
        case 3: //CLAHE-based method (Algorithm 7)
            clahe(selected, nselected, min, max);
            break;
            
        default: //this should never happen
            fprintf(stderr, "Invalid Equalization Option\n");
            exit(EXIT_FAILURE);
    }
    
    if (level+1 > Lmax) return;
    
    if (max - min <= 2) return; //not enough values in range for dyadic split
    
    //center of interval of values
    int med=(min+max)/2;
    
    //Process connected components [min, med] and [med+1, max]
    process_bilevelset(selected, nselected, min, med);
    process_bilevelset(selected, nselected, med+1, max);
     
}





back to top