///////////////////////////////////////////////////////////////////////////////////////////////////
//  Facade to ImageJ functionality
///////////////////////////////////////////////////////////////////////////////////////////////////

//More information on:
//   Image:   https://imagej.nih.gov/ij/docs/guide/146-28.htmltoc-Section-28
//   Process: https://imagej.nih.gov/ij/docs/guide/146-29.html#toc-Section-29
//   Analyze: https://imagej.nih.gov/ij/docs/guide/146-30.html#toc-Section-30

Utils = Java.type('ch.psi.pshell.imaging.Utils')
Pair = Java.type('org.apache.commons.math3.util.Pair')


IJ = Java.type('ij.IJ')
ImageJ = Java.type('ij.ImageJ')
WindowManager = Java.type('ij.WindowManager')
ImagePlus = Java.type('ij.ImagePlus')
Prefs = Java.type('ij.Prefs')
FileSaver = Java.type('ij.io.FileSaver')

ImageProcessor = Java.type('ij.process.ImageProcessor')
ByteProcessor = Java.type('ij.process.ByteProcessor')
ShortProcessor = Java.type('ij.process.ShortProcessor')
ColorProcessor = Java.type('ij.process.ColorProcessor')
FloatProcessor = Java.type('ij.process.FloatProcessor')
ImageConverter = Java.type('ij.process.ImageConverter')
AutoThresholder = Java.type('ij.process.AutoThresholder')
LUT = Java.type('ij.process.LUT')
Measurements = Java.type('ij.measure.Measurements')

ResultsTable = Java.type('ij.measure.ResultsTable')
Analyzer = Java.type('ij.plugin.filter.Analyzer')
GaussianBlur = Java.type('ij.plugin.filter.GaussianBlur')
Filters = Java.type('ij.plugin.filter.Filters')
FFTFilter = Java.type('ij.plugin.filter.FFTFilter')
BackgroundSubtracter = Java.type('ij.plugin.filter.BackgroundSubtracter')
EDM = Java.type('ij.plugin.filter.EDM')
Shadows = Java.type('ij.plugin.filter.Shadows')
UnsharpMask = Java.type('ij.plugin.filter.UnsharpMask')
MaximumFinder = Java.type('ij.plugin.filter.MaximumFinder') 
EDM = Java.type('ij.plugin.filter.EDM')
Shadows = Java.type('ij.plugin.filter.Shadows')
UnsharpMask = Java.type('ij.plugin.filter.UnsharpMask')
RankFilters = Java.type('ij.plugin.filter.RankFilters')
Convolver = Java.type('ij.plugin.filter.Convolver')
ParticleAnalyzer = Java.type('ij.plugin.filter.ParticleAnalyzer')

ContrastEnhancer = Java.type('ij.plugin.ContrastEnhancer')
Thresholder = Java.type('ij.plugin.Thresholder')
ImageCalculator = Java.type('ij.plugin.ImageCalculator')
FFT = Java.type('ij.plugin.FFT')
Concatenator = Java.type('ij.plugin.Concatenator')

//ImageJ customizations
FFTMath = Java.type('ch.psi.pshell.imaging.ij.FFTMath')
FFTFilter = Java.type('ch.psi.pshell.imaging.ij.FFTFilter')
Binary = Java.type('ch.psi.pshell.imaging.ij.Binary')
Slicer = Java.type('ch.psi.pshell.imaging.ij.Slicer')


//This eliminates the error messages due to the bug on ij.gui.ImageWindow row 555 (ij is null)
try{
    _image_j
} catch(ex) {   	
    _image_j = new ImageJ(null, ImageJ.NO_SHOW)
}    
    
///////////////////////////////////////////////////////////////////////////////////////////////////
//Image creation, copying & saving
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function  load_image(image, title){
    /*
    image: file name or BufferedImage
    */    
    if (!is_defined(title))    title ="img"
    
    if (typeof image == 'string') 
        try{
            file = get_context().setup.expandPath(image)
        } catch(ex) {
        }
        try{
            image = Utils.newImage(file)
        } catch(ex) {
            //try loading from assembly
            image = get_context().setup.getAssemblyImage(image)
        }
    return new ImagePlus(title, image)
}
        
function load_array(array, width, height, title){
    /*
    array: 1d array if width and height defined , or else 2d array to be flattened.
    */  
    if (!is_defined(width))    width = null
    if (!is_defined(height))    height = null
    if (!is_defined(title))    title ="img"  
    //2D
    if ((width==null) && (height==null)){
        if (array.typecode == '[B') proc = new ByteProcessor(array[0].length, array.length, Convert.flatten(array))
        else if (array.typecode == '[S') proc = new ShortProcessor(array[0].length, array.length, Convert.flatten(array), null)
        else if (array.typecode == '[I') proc = new FloatProcessor(array[0].length, array.length, Convert.flatten(array))        
        else if (array.typecode == '[F') proc = new FloatProcessor(array[0].length, array.length, Convert.flatten(array))        
        else if (array.typecode == '[D') proc = new FloatProcessor(array[0].length, array.length, Convert.flatten(array))        
        else throw "Invalid array type"
    //1D
    }else{
        if (array.length > width*height)
            array = array.slice[0, width*height]
        if (array.typecode == 'b') proc = new yteProcessor(width, height, array)
        else if (array.typecode == 'h') proc = new ShortProcessor(width, height, array, null)
        else if (array.typecode == 'i') proc = new FloatProcessor(width, height, array)
        else if (array.typecode == 'f') proc = new FloatProcessor(width, height, array)
        else if (array.typecode == 'd') proc = new FloatProcessor(width, height, array)
        else throw "Invalid array type"
    }
    return new ImagePlus(title, proc)
}
        
function save_image(ip, path, format){
    /*
    Saves image or stack
    If parameters omitted, saves image again in same location, with same format.
    */
    if (!is_defined(path))    path = null
    if (!is_defined(format))    format = null
    var fs = new FileSaver(ip)
    if (path == null)fs.save()
    else{
        try{
            path = get_context().setup.expandPath(path)
        } catch(ex) {
        }      
        if (format == "bmp") fs.saveAsBmp(path) 
        else if (format == "fits") fs.saveAsFits(path) 
        else if (format == "gif") fs.saveAsGif(path) 
        else if (format == "jpeg") fs.saveAsJpeg(path) 
        else if (format == "lut") fs.saveAsLut(path) 
        else if (format == "pgm") fs.saveAsPgm(path) 
        else if (format == "png") fs.saveAsPng(path) 
        else if (format == "raw" && (ip.getImageStackSize()>1)) fs.saveAsRawStack(path) 
        else if (format == "raw") fs.saveAsRaw(path) 
        else if (format == "txt") fs.saveAsText(path)
        else if (format == "tiff" && (ip.getImageStackSize()>1)) fs.saveAsTiffStack(path)
        else if (format == "tiff") fs.saveAsTiff(path)
        else if (format == "zip") fs.saveAsZip(path)   
    }
}
        
function new_image(width, height, image_type, title, fill_color){
    /*
    type = "byte", "short", "color" or "float"
    */
    if (!is_defined(image_type))    image_type = "byte"
    if (!is_defined(title))    title = "img"
    if (!is_defined(fill_color))    fill_color = null
    if (image_type == "byte")  p= new ByteProcessor(width, height)
    else if (image_type == "short") p= new ShortProcessor(width, height)
    else if (image_type == "color") p= new ColorProcessor(width, height)
    else if (image_type == "float") p= new FloatProcessor(width, height)
    else  throw ("Invalid image type " + str(image_type))
    ret = new ImagePlus(title, p)    
    if (fill_color != null){
        p.setColor(fill_color)
        p.resetRoi()
        p.fill()
    }
    return ret    
}
        
function sub_image(ip, x, y, width, height){
    /*
    Returns new ImagePlus
    */
    ip.setRoi(x, y, width, height)
    p=ip.getProcessor().crop()
    return new ImagePlus(ip.getTitle() + " subimage", p)    
}
        
function copy_image(ip){
    return ip.duplicate()
}
        
function copy_image_to(ip_source, ip_dest, x, y){
    ip_source.deleteRoi()    
    ip_source.copy()
    ip_dest.setRoi(x, y, ip_source.getWidth(), ip_source.getHeight())
    ip_dest.paste()
    ip_dest.changes = false
    ip_dest.deleteRoi()
}
        
function pad_image(ip, left, right, top, bottom, fill_color){
	if (!is_defined(left))    left = 0
	if (!is_defined(right))    right = 0
	if (!is_defined(top))    top = 0
	if (!is_defined(bottom))    bottom = 0
	if (!is_defined(fill_color))    fill_color = null
    p=ip.getProcessor()
    width = p.getWidth() + left + right
    height =  p.getHeight() + top + bottom        
    image_type = get_image_type(ip)
    ret = new_image(width, height, image_type, ip.getTitle() + " padded", fill_color)
    ip.deleteRoi()
    ip.copy()
    ret.setRoi(left, top, p.getWidth(), p.getHeight())
    ret.paste()
    ret.changes = false
    ret.deleteRoi()
    return ret    
}
        
function get_image_type(ip){
    /*
    Returns: "byte", "short", "color" or "float"
    */
    p=ip.getProcessor()
    if (p instanceof ShortProcessor) return "short"
    else if (p instanceof ColorProcessor) return "color"
    else if (p instanceof FloatProcessor) return "float"
    return "byte"
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Image type conversion
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function grayscale(ip, in_place){
	if (!is_defined(in_place))    in_place = true
    ip = (in_place==true) ? ip : ip.duplicate()
    ic = new ImageConverter(ip)
    ic.convertToGray8()
    return ip
}
        
function  get_channel(ip, channel){
    /*
    Return a channel from a color image as a new  ImagePlus.
    channel: "red", "green","blue", "alpha", "brightness", 
    */
    proc = ip.getProcessor()    
    if (channel ==   "red") ret = proc.getChannel(1, null)
    else if (channel == "green") ret = proc.getChannel(2, null)
    else if (channel == "blue") ret = proc.getChannel(3, null)
    else if (channel == "alpha") ret = proc.getChannel(4, null)
    else if (channel == "brightness") ret =	proc.getBrightness()
    else throw ("Invalid channel " + channel)
    return new ImagePlus(ip.getTitle() + " channel: " + channel, ret)
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Thresholder
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function threshold(ip, min_threshold, max_threshold, in_place){
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().setThreshold(min_threshold, max_threshold, ImageProcessor.NO_LUT_UPDATE)    
    WindowManager.setTempCurrentImage(ip)
    new Thresholder().run("mask")
    return ip
}
        
function auto_threshold(ip, dark_background, method, in_place){
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(method))    method = AutoThresholder.getMethods()[0]
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().setAutoThreshold(method, dark_background , ImageProcessor.NO_LUT_UPDATE)
    WindowManager.setTempCurrentImage(ip)
    thresholder=new Thresholder().run("mask")
    return ip
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Binary functions    
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function binary_op(ip, op, dark_background, iterations, count, in_place){
    /*
    op = "erode","dilate", "open","close", "outline", "fill holes", "skeletonize"
    */
    if (!is_defined(dark_background))    dark_background = false
    if (!is_defined(iterations))    iterations = 1
    if (!is_defined(count))    count = 1
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    binary = new Binary()    
    Binary.count = count
    Binary.iterations = iterations
    Prefs.blackBackground=dark_background      
    binary.setup(op, ip)
    binary.run(ip.getProcessor())
    return ip
}
        
function binary_erode(ip, dark_background, iterations, count, in_place){
    return binary_op(ip, "erode", dark_background, iterations, count, in_place)
}
        
function binary_dilate(ip, dark_background, iterations, count, in_place){
    return binary_op(ip, "dilate", dark_background, iterations, count, in_place)
}
        
function binary_open(ip, dark_background, iterations, count, in_place){
    return binary_op(ip, "open", dark_background, iterations, count, in_place)
}
        
function binary_close(ip, dark_background, iterations, count, in_place){
    return binary_op(ip, "close", dark_background, iterations, count, in_place)
}
        
function binary_outline(ip, dark_background, in_place){
    return binary_op(ip, "outline", dark_background, 1, 1, in_place)
}
        
function binary_fill_holes(ip, dark_background, in_place){
    return binary_op(ip, "fill holes", dark_background, 1, 1, in_place)
}
        
function  binary_skeletonize(ip, dark_background, in_place){
    return binary_op(ip, "skeletonize", dark_background, 1, 1, in_place)
}    
        
function analyse_particles(ip, min_size, max_size, fill_holes, exclude_edges, extra_measurements,print_table, output_image, minCirc, maxCirc){
    /*
    Returns: tuple (ResultsTable results_table, ImagePlus output_image)
    output_image = "outlines", "overlay_outlines", "masks", "overlay_masks", "roi_masks" or null
    extra_measurements = mask with Measurements.CENTROID, PERIMETER, RECT, MIN_MAX, ELLIPSE, CIRCULARITY, AREA_FRACTION, INTEGRATED_DENSITY, INVERT_Y, FERET, KURTOSIS, MEDIAN, MODE, SKEWNESS, STD_DEV 
    Measurements is a mask of flags: https://imagej.nih.gov/ij/developer/api/ij/measure/Measurements.html.
    Returned ResultsTable hold public fields: https://imagej.nih.gov/ij/developer/api/ij/measure/ResultsTable.html
    
    */
    if (!is_defined(fill_holes))    fill_holes = true
    if (!is_defined(exclude_edges))    exclude_edges = true
    if (!is_defined(extra_measurements))    extra_measurements = 0
    if (!is_defined(print_table))    print_table = false
    if (!is_defined(output_image))    output_image = "outlines"
    if (!is_defined(minCirc))    minCirc = 0.0
    if (!is_defined(maxCirc))    maxCirc = 1.0
    rt = new ResultsTable()
    show_summary = false 
    var options = ParticleAnalyzer.SHOW_RESULTS | ParticleAnalyzer.CLEAR_WORKSHEET 
    /*
        ParticleAnalyzer.SHOW_ROI_MASKS | \
        //ParticleAnalyzer.RECORD_STARTS | \
        //ParticleAnalyzer.ADD_TO_MANAGER | \
        //ParticleAnalyzer.FOUR_CONNECTED | \
        //ParticleAnalyzer.IN_SITU_SHOW | \
        //ParticleAnalyzer.SHOW_NONE | \
    */
    if (show_summary)             options = options | ParticleAnalyzer.DISPLAY_SUMMARY 
    if (output_image == "outlines") options = options | ParticleAnalyzer.SHOW_OUTLINES 
    else if (output_image == "overlay_outlines") options = options | ParticleAnalyzer.SHOW_OVERLAY_OUTLINES
    else if (output_image == "masks")     options = options | ParticleAnalyzer.SHOW_MASKS
    else if (output_image == "overlay_masks")     options = options | ParticleAnalyzer.SHOW_OVERLAY_MASKS
    else if (output_image == "roi_masks")     options = options | ParticleAnalyzer.SHOW_ROI_MASKS
    //ParticleAnalyzer.SHOW_ROI_MASKS
    if (exclude_edges)            options = options | ParticleAnalyzer.EXCLUDE_EDGE_PARTICLES
    if (fill_holes)               options = options | ParticleAnalyzer.INCLUDE_HOLES
    measurements = Measurements.AREA  | Measurements.MEAN  |  Measurements.CENTER_OF_MASS | Measurements.RECT
    pa = new ParticleAnalyzer(options, measurements, rt, min_size, max_size, minCirc, maxCirc)
    pa.setHideOutputImage(true)
    ParticleAnalyzer.setResultsTable(rt)
    if (pa.analyze(ip)){
        if (print_table){
            print (rt.getColumnHeadings())
            for (var row= 0; row<rt.counter; row++){
                print (rt.getRowAsString(row))
            }
        }
        return [rt, pa.getOutputImage()]
    }
}        

///////////////////////////////////////////////////////////////////////////////////////////////////
//Image operators
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function op_image(ip1, ip2, op, float_result, in_place){
    /*
    op = "add","subtract",  "multiply","divide", "and", "or", "xor", "min", "max", "average", "difference" or "copy"
    */
    if (!is_defined(float_result))    float_result = false
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ic = new ImageCalculator()
    pars = op
    if (float_result) op = op + " float" 
    ic.run(pars, ip1, ip2)
    return ip1
}
        
function  op_const(ip, op, val, in_place){
    /*
    op = "add","subtract",  "multiply","divide", "and", "or", "xor", "min", "max", "gamma", "set" or "log", "exp", "sqr", "sqrt","abs"
    */
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    pr = ip.getProcessor()
    if (op == 'add') pr.add(val)
    else if (op == 'sub') pr.subtract(val)
    else if (op == 'multiply') pr.multiply(val)
    else if (op == 'divide' && val!=0) pr.multiply(1.0/val)
    else if (op == 'and') pr.and(val)
    else if (op == 'or') pr.or(val)
    else if (op == 'xor') pr.xor(val)
    else if (op == 'min') {pr.min(val);pr.resetMinAndMax()}
    else if (op == 'max') {pr.max(val);pr.resetMinAndMax()}
    else if (op == 'gamma' && (0.05 < val) && (val < 5.0)) pr.gamma(val)
    else if (op == 'set') pr.set(val)
    else if (op == 'log') pr.log()
    else if (op == 'exp') pr.exp()        
    else if (op == 'sqr') pr.sqr()        
    else if (op == 'sqrt') pr.sqrt()        
    else if (op == 'abs') {pr.abs();pr.resetMinAndMax()}
    else throw "Invalid operation " + op
    return ip
}
        
function op_fft(ip1, ip2, op, do_inverse){
    /*
    Images must have same sizes, and multiple of  2  height and width.
    op = "correlate" (complex conjugate multiply), "convolve" (Fourier domain multiply), "deconvolve" (Fourier domain divide)
    */
    if (!is_defined(do_inverse))    do_inverse = true
    var op_index
    if   (op == "correlate") op_index = 0
    else if (op == "convolve") op_index = 1
    else if (op == "deconvolve")  op_index = 2
    else throw "Invalid operation " + op  
    return new FFTMath().doMath(ip1, ip2, op_index, do_inverse)
}
        
function op_rank(ip, op, kernel_radius ,  dark_outliers ,threshold, in_place){
    /*
    op = "mean", "min", "max", "variance", "median", "close_maxima", "open_maxima", "remove_outliers", "remove_nan", "despeckle"
    */
    if (!is_defined(kernel_radius))    kernel_radius = 1
    if (!is_defined(dark_outliers))    dark_outliers = false
    if (!is_defined(threshold))    threshold = 50
    if (!is_defined(in_place))    in_place = true
    var filter_type 
    if   (op == "mean") filter_type = RankFilters.MEAN
    else if (op == "min") filter_type = RankFilters.MIN
    else if (op == "max") filter_type = RankFilters.MAX
    else if (op == "variance") filter_type = RankFilters.VARIANCE
    else if (op == "median") filter_type = RankFilters.MEDIAN
    else if (op == "close_maxima") filter_type = RankFilters.CLOSE
    else if (op == "open_maxima") filter_type = RankFilters.OPEN
    else if (op == "remove_outliers") filter_type = RankFilters.OUTLIERS
    else if (op == "remove_nan") filter_type = RankFilters.REMOVE_NAN
    else if (op == "despeckle") {filter_type = RankFilters.MEDIAN, kernel_radius = 1}
    else throw "Invalid operation " + op
    ip = in_place ? ip : ip.duplicate()
    new RankFilters().rank(ip.getProcessor(), kernel_radius, filter_type, dark_outliers ? RankFilters.DARK_OUTLIERS : RankFilters.BRIGHT_OUTLIERS ,threshold)
    return ip    
}
        
function op_edm(ip, op, dark_background, in_place){
    /*
    Euclidian distance map & derived  operations
    op ="edm", "watershed","points", "voronoi"
    */    
    if (!is_defined(op))    op = "edm"
    if (!is_defined(dark_background))    dark_background = false
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    pr = ip.getProcessor()
    var edm=new EDM()
    Prefs.blackBackground=dark_background      
    if  (op=="edm"){      
        //pr.setPixels(0, edm.makeFloatEDM(pr, 0, false));
        //pr.resetMinAndMax();                
        if (dark_background)  pr.invert()
        edm.toEDM(pr)
    }else{
        edm.setup(op, ip)
        edm.run(pr)
    }
    return ip
}
        
function watershed(ip, dark_background, in_place){
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(in_place))    in_place = true
    return op_edm(ip, "watershed", dark_background, in_place)
}
        
function ultimate_points(ip, dark_background, in_place){
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(in_place))    in_place = true
    return op_edm(ip, "points", dark_background, in_place)
}
        
function veronoi(ip, dark_background, in_place){
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(in_place))    in_place = true
    return op_edm(ip, "voronoi", dark_background, in_place)
}
        
function edm(ip, dark_background, in_place){
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(in_place))    in_place = true
    return op_edm(ip, "edm", dark_background, in_place)
}
        
function op_filter(ip, op, in_place){

    /*
    This is redundant as just calls processor methods.
    op ="invert", "smooth", "sharpen", "edge", "add"
    */    
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    f = new Filters()
    f.setup(op, ip )
    f.run(ip.getProcessor())
    return ip
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Other operations
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function gaussian_blur(ip, sigma_x, sigma_y, accuracy, in_place){
	if (!is_defined(sigma_x))    sigma_x = 3.0
	if (!is_defined(sigma_y))    sigma_y = 3.0
	if (!is_defined(accuracy))    accuracy = 0.01
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    new GaussianBlur().blurGaussian(ip.getProcessor(),  sigma_x, sigma_y, accuracy)
    return ip
}
        
function find_maxima(ip, tolerance, threshold, output_type, exclude_on_edges, is_edm ){
    /*
    Returns new ImagePlus
    tolerance:  maxima are accepted only if protruding more than this value  from the ridge to a higher maximum
    threshhold: minimum height of a maximum (uncalibrated);
    output_type = SINGLE_POINTS, IN_TOLERANCE or SEGMENTED.  No output image is created for output types POINT_SELECTION, LIST and COUNT.   
    */
    if (!is_defined(tolerance))    tolerance = 25
    if (!is_defined(threshold))    threshold =  ImageProcessor.NO_THRESHOLD
    if (!is_defined(output_type))    output_type = MaximumFinder.IN_TOLERANCE
    if (!is_defined(exclude_on_edges))    exclude_on_edges = false
    if (!is_defined(is_edm))    is_edm = false
    byte_processor =  new MaximumFinder().findMaxima(ip.getProcessor(), tolerance, threshold, output_type, exclude_on_edges, is_edm)     
    return new ImagePlus(ip.getTitle() + " maxima", byte_processor)
}
        
function get_maxima_points(ip, tolerance, exclude_on_edges){
    if (!is_defined(tolerance))    tolerance = 25
    if (!is_defined(exclude_on_edges))    exclude_on_edges = false
    polygon =  new MaximumFinder().getMaxima(ip.getProcessor(), tolerance, exclude_on_edges)
    return (polygon.xpoints, polygon.ypoints)
}
        
function enhance_contrast(ip, equalize_histo, saturated_pixels, normalize, stack_histo, in_place){
	if (!is_defined(equalize_histo))    equalize_histo = true
	if (!is_defined(saturated_pixels))    saturated_pixels =  0.5
	if (!is_defined(normalize))    normalize = false
	if (!is_defined(stack_histo))    stack_histo = false
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ce = new ContrastEnhancer()
    if (equalize_histo){
        ce.equalize(ip.getProcessor());
    } else{
        ce.stretchHistogram(ip.getProcessor(), saturated_pixels)
        if (normalize){
            ip.getProcessor().setMinAndMax(0,(ip.getProcessor().getBitDepth()==32) ? 1.0 : ip.getProcessor().maxValue())
        }
    }
    return ip   
}
        
function shadows(ip, op, in_place){
    /*
    op ="north","northeast", "east", "southeast","south", "southwest", "west","northwest"
    */
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    shadows= new Shadows()
    shadows.setup(op, ip)
    shadows.run(ip.getProcessor())
    return ip
}
        
function unsharp_mask(ip, sigma, weight, in_place){
    /*
    Float processor
    */    
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().snapshot()
    unsharp=new UnsharpMask()
    USmask.setup("  ", ip)
    USmask.sharpenFloat( ip.getProcessor(),sigma, weight)
    return ip
}
        
function subtract_background(ip, radius, create_background, dark_background, use_paraboloid, do_presmooth, correctCorners, rgb_brightness, in_place){
	if (!is_defined(radius))    radius = 50
	if (!is_defined(create_background))    create_background = false
	if (!is_defined(dark_background))    dark_background = false
	if (!is_defined(use_paraboloid))    use_paraboloid = true
	if (!is_defined(do_presmooth))    do_presmooth = true
	if (!is_defined(correctCorners))    correctCorners = true
    if (!is_defined(rgb_brightness))    rgb_brightness = false
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    if (rgb_brightness)
        new BackgroundSubtracter().rollingBallBrightnessBackground(ip.getProcessor(), radius, create_background,! dark_background, use_paraboloid, do_presmooth, correctCorners)
    else
        new BackgroundSubtracter().rollingBallBackground(ip.getProcessor(), radius, create_background, !dark_background, use_paraboloid, do_presmooth, correctCorners)        
    return ip
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//FFT
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function image_fft(ip, show){
	if (!is_defined(show))    show = true
    WindowManager.setTempCurrentImage(ip)
    fft = new FFT()
    fft.run("fft")    
    //TODO: how to avoid it to be created?
    //ret =  ImagePlus("FHT of " + ip.getTitle(), WindowManager.getCurrentImage().getProcessor())
    ret = WindowManager.getCurrentImage()
    if (show == false)
        WindowManager.getCurrentImage().hide()
    return ret
}
        
function image_ffti(ip, show){
	if (!is_defined(show))    show = true
    WindowManager.setTempCurrentImage(ip)
    fft = new FFT()
    fft.run("inverse")
    //WindowManager.getCurrentImage().hide()
    //TODO: how to avoid it to be created?
    //ret = WindowManager.getCurrentImage()
    //WindowManager.getCurrentImage().hide()
    //ret =  ImagePlus(ip.getTitle() + " ffti", WindowManager.getCurrentImage().getProcessor())
    ret = WindowManager.getCurrentImage()
    if (show == false)
        WindowManager.getCurrentImage().hide()
    
    return ret
}
        
function bandpass_filter(ip, small_dia_px, large_dia_px, suppress_stripes, stripes_tolerance_direction, autoscale_after_filtering, saturate_if_autoscale, display_filter, in_place){
    /*
    suppress_stripes = 0 for none, 1 for horizontal, 2 for vertical
    */
    if (!is_defined(suppress_stripes))    suppress_stripes = 0
    if (!is_defined(stripes_tolerance_direction))    stripes_tolerance_direction = 5.0
    if (!is_defined(autoscale_after_filtering))    autoscale_after_filtering = false
    if (!is_defined(saturate_if_autoscale))    saturate_if_autoscale = false
    if (!is_defined(display_filter))    display_filter = false
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    filter=  new FFTFilter();
    FFTFilter.filterLargeDia = large_dia_px
    FFTFilter.filterSmallDia = small_dia_px
    FFTFilter.choiceIndex = suppress_stripes
    FFTFilter.toleranceDia = stripes_tolerance_direction
    FFTFilter.doScalingDia = autoscale_after_filtering
    FFTFilter.saturateDia = saturate_if_autoscale
    FFTFilter.displayFilter =display_filter
    filter.setup(null, ip);
    filter.run(ip.getProcessor())
    return ip
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Convolution
///////////////////////////////////////////////////////////////////////////////////////////////////

KERNEL_BLUR = [[0.1111, 0.1111, 0.1111], [0.1111, 0.1111, 0.1111], [0.1111, 0.1111, 0.1111]]
KERNEL_SHARPEN = [[0.0, -0.75, 0.0], [-0.75, 4.0, -0.75], [0.0, -0.75, 0.0]]
KERNEL_SHARPEN_2 = [[-1.0, -1.0, -1.0], [-1.0, 9.0, -1.0], [-1.0, -1.0, -1.0]]
KERNEL_LIGHT = [[0.1, 0.1, 0.1], [0.1, 1.0, 0.1],[0.1, 0.1, 0.1]]
KERNEL_DARK = [[0.01, 0.01, 0.01],[0.01, 0.5, 0.01],[0.01, 0.01, 0.01]]
KERNEL_EDGE_DETECT = [[0.0, -0.75, 0.0], [-0.75, 3.0, -0.75], [0.0, -0.75, 0.0]]
KERNEL_EDGE_DETECT_2 = [[-0.5, -0.5, -0.5], [-0.5, 4.0, -0.5], [-0.5, -0.5, -0.5]]
KERNEL_DIFFERENTIAL_EDGE_DETECT = [[-1.0, 0.0, 1.0], [0.0, 0.0, 0.0], [1.0, 0.0, -1.0]]
KERNEL_PREWITT = [[-2.0, -1.0, 0.0], [-1.0,  0.0,  1.0 ], [0.0, 1.0, 2.0]]
KERNEL_SOBEL = [[2.0, 2.0, 0.0], [2.0,  0.0,  -2.0 ],  [0.0, -2.0, -2.0]]
        
function  convolve(ip, kernel, in_place){
    /*
    kernel: list of lists
    */
    if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    kernel_width = kernel.length
    kernel_height= kernel[0].length
    kernel =flatten(kernel)
    
    //Convolver().convolve(ip.getProcessor(), kernel, kernel_width, kernel_height) 
    //ip.getProcessor().convolve(to_array(kernel, '[f'), kernel_width, kernel_height)
    ip.getProcessor().convolve(kernel, kernel_width, kernel_height)
    return ip
}
        
///////////////////////////////////////////////////////////////////////////////////////////////////
//Shortcut  to ImageProcessor methods
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function invert(ip, in_place){
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().invert()
    return ip
}
        
function smooth(ip, in_place){
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().smooth()
    return ip
}
        
function sharpen(ip, in_place){
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().sharpen()
    return ip
}
        
function edges(ip, in_place){ //Sobel
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().findEdges()
    return ip
}
        
function noise(ip, sigma, in_place){
	if (!is_defined(sigma))    sigma =  25.0
	if (!is_defined(in_place))    in_place = true
    ip = in_place ? ip : ip.duplicate()
    ip.getProcessor().noise(sigma)
    return ip
}
        
function remap(ip, min, max, in_place){
	if (!is_defined(min))    min = null
	if (!is_defined(max))    max = null
	if (!is_defined(in_place))    in_place = true	
    ip = in_place ? ip : ip.duplicate()
    if ((min ==null) || (max == null)){
        stats = get_statistics(ip, Measurements.MIN_MAX)
        if (min== null) min = stats.min
        if (max == null) max = stats.max
    }
    ip.getProcessor().setMinAndMax(min, max)
    return ip
}
        
function set_lut(ip, r, g, b){
    /*
    r,g and b are lists of 256 integers
    */
    r =  r.map(function(x) {return (x<128) ? x : (x-256 )})
    g =  g.map(function(x) {return (x<128) ? x : (x-256 )})
    b =  b.map(function(x) {return (x<128) ? x : (x-256 )})    
    ip.setLut(new LUT(to_array(r,'b'),to_array(g,'b'),to_array(b,'b')))
}
        
function resize(ip, width, height){
    /*
    Returns new ImagePlus
    */
    p = ip.getProcessor().resize(width, height)
    return new ImagePlus(ip.getTitle() + " resized", p)
}
        
function binning(ip, factor){
    p=ip.getProcessor().bin(factor)
    return new ImagePlus(ip.getTitle() + " resized", p)
}
        
function get_histogram(ip, hist_min, hist_max, hist_bins, roi){
    /*
    hist_min, hist_max, hist_bins used only for float images (otherwise fixed to 0,255,256)
    roi is list  [x,y,w,h]
    */
    if (!is_defined(hist_min))    hist_min = 0
    if (!is_defined(hist_max))    hist_max = 0
    if (!is_defined(hist_bins))    hist_bins = 256
    if (!is_defined(roi))    roi = null
    if (roi == null) {
    	ip.deleteRoi()    
    } else {
    	ip.setRoi(roi[0],roi[1],roi[2],roi[3])
    }
    image_statistics = ip.getStatistics(0, hist_bins, hist_min, hist_max)    
    return to_array(image_statistics.getHistogram())
}
        
function get_array(ip){
    return ip.getProcessor().getIntArray()
}
        
function get_line(ip, x1, y1, x2, y2){
    return ip.getProcessor().getLine(x1, y1, x2, y2)
}
        
function get_pixel_range(ip){
    return (ip.getProcessor().getMin(), ip.getProcessor().getMax())
}
        
function get_num_channels(ip){
    return ip.getProcessor().getNChannels()
}
        
function is_binary(ip){
    return ip.getProcessor().isBinary()    
}
        
function get_pixel(ip, x, y){
    return ip.getProcessor().getPixel(x,y)
}
        
function get_pixel_array(ip, x, y){
    a = [0]*get_num_channels(ip)
    return ip.getProcessor().getPixel(x,y,a)
}
        
function get_pixels(ip){
    return ip.getProcessor().getPixels()
}
        
function get_width(ip){
    return ip.getProcessor().getWidth()
}
        
function get_height(ip){
    return ip.getProcessor().getHeight()
}
        
function get_row(ip, y){
    a = [0]*get_width(ip)
    array = jarray.array(a,'i')
    ip.getProcessor().getRow(0, y, array, get_width(ip))
    return array
}
        
function get_col(ip, x){
    a = [0]*get_height(ip)
    array = jarray.array(a,'i')
    ip.getProcessor().getColumn(x, 0, array, get_height(ip))
    return array     
}
        
function get_statistics(ip, measurements){
    /*
    Measurements is a mask of flags: https://imagej.nih.gov/ij/developer/api/ij/measure/Measurements.html.
    Statistics object hold public fields: https://imagej.nih.gov/ij/developer/api/ij/process/ImageStatistics.html
    */
    if (!is_defined(measurements))    measurements = null
    if (measurements == null){
        return ip.getStatistics()
    } else {
        return ip.getStatistics(measurements)
    }
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//Image stack functions
///////////////////////////////////////////////////////////////////////////////////////////////////
        
function create_stack(ip_list, keep, title){
    if (!is_defined(keep))    keep = true
    if (!is_defined(title))    title = null
    stack = new Concatenator().concatenate(ip_list, keep)
    if (title != null)
        stack.setTitle(title)
    return stack
}
        
function reslice(stack, start_at, vertically, flip, output_pixel_spacing, avoid_interpolation, title){
	if (!is_defined(start_at))    start_at = "Top"
	if (!is_defined(vertically))    vertically = true
	if (!is_defined(flip))    flip = true
	if (!is_defined(output_pixel_spacing))    output_pixel_spacing = 1.0
	if (!is_defined(avoid_interpolation))    avoid_interpolation = true
	if (!is_defined(title))    title = null
    ss = new Slicer()
    ss.rotate = vertically
    ss.startAt = start_at 
    ss.flip = flip
    ss.nointerpolate =  avoid_interpolation
    ss.outputZSpacing = output_pixel_spacing
    stack =  ss.reslice(stack)
    if (title != null)
        stack.setTitle(title)
    return stack
}