package ij;
import ij.process.*;
import ij.gui.*;
import ij.plugin.*;
import ij.plugin.frame.*;
import ij.io.FileInfo;
import java.awt.*;
import java.awt.image.*;

public class CompositeImage extends ImagePlus {

    /** Display modes (note: TRANSPARENT mode has not yet been implemented) */
    public static final int COMPOSITE=1, COLOR=2, GRAYSCALE=3, TRANSPARENT=4;
    public static final int MAX_CHANNELS = 8;
    public static final Color[] colors = {Color.red, Color.green, Color.blue, Color.white, Color.cyan, Color.magenta, Color.yellow, Color.white};

    int[] rgbPixels;
    boolean newPixels;
    MemoryImageSource imageSource;
    Image awtImage;
    WritableRaster rgbRaster;
    SampleModel rgbSampleModel;
    BufferedImage rgbImage;
    ColorModel rgbCM;
    ImageProcessor[] cip;
    LUT[] lut;
    int currentChannel = -1;
    int previousChannel;
    int currentSlice = 1;
    int currentFrame = 1;
    boolean singleChannel;
    boolean[] active = new boolean[MAX_CHANNELS];
    int mode = COLOR;
    int bitDepth;
    double[] displayRanges;
    byte[][] channelLuts;
    boolean customLuts;
    boolean syncChannels;

    public CompositeImage(ImagePlus imp) {
        this(imp, COLOR);
    }
    
    public CompositeImage(ImagePlus imp, int mode) {
        if (mode<COMPOSITE || mode>GRAYSCALE)
            mode = COLOR;
        this.mode = mode;
        int channels = imp.getNChannels();
        bitDepth = getBitDepth();
        if (IJ.debugMode) IJ.log("CompositeImage: "+imp+" "+mode+" "+channels);
        ImageStack stack2;
        boolean isRGB = imp.getBitDepth()==24;
        if (isRGB) {
            if (imp.getImageStackSize()>1)
                throw new IllegalArgumentException("RGB stacks not supported");
            stack2 = getRGBStack(imp);
        } else
            stack2 = imp.getImageStack();
        int stackSize = stack2.getSize();
        if (channels==1 && isRGB)
            channels = 3;
        if (channels==1 && stackSize<=MAX_CHANNELS && !imp.dimensionsSet)
            channels = stackSize;
        if (channels<1 || (stackSize%channels)!=0)
            throw new IllegalArgumentException("stacksize not multiple of channels");
        if (mode==COMPOSITE && channels>MAX_CHANNELS)
            this.mode = COLOR;
        compositeImage = true;
        int z = imp.getNSlices();
        int t = imp.getNFrames();
        if (channels==stackSize || channels*z*t!=stackSize)
            setDimensions(channels, stackSize/channels, 1);
        else
            setDimensions(channels, z, t);
        setStack(imp.getTitle(), stack2);
        setCalibration(imp.getCalibration());
        FileInfo fi = imp.getOriginalFileInfo();
        if (fi!=null) {
            displayRanges = fi.displayRanges; 
            channelLuts = fi.channelLuts;
        }
        setFileInfo(fi);
        Object info = imp.getProperty("Info");
        if (info!=null)
            setProperty("Info", imp.getProperty("Info"));
        setProperties(imp.getPropertiesAsArray());      
        if (mode==COMPOSITE) {
            for (int i=0; i<MAX_CHANNELS; i++)
                active[i] = true;
        } else
            active[0] = true;
        //if (!(channels==3&&stackSize==3))
        setRoi(imp.getRoi());
        setOverlay(imp.getOverlay());
        if (channels!=stackSize)
            setOpenAsHyperStack(true);
    }

    @Override
    public Image getImage() {
        if (img==null)
            updateImage();
        return img;
    }
    
    public void updateChannelAndDraw() {
        if (!customLuts) singleChannel = true;
        updateAndDraw();
    }
    
    public void updateAllChannelsAndDraw() {
        if (mode!=COMPOSITE)
            updateChannelAndDraw();
        else {
            syncChannels = true;
            singleChannel = false;
            updateAndDraw();
        }
    }

    public ImageProcessor getChannelProcessor() {
        if (cip!=null && currentChannel!=-1)
            return cip[currentChannel];
        else
            return getProcessor();
    }

    synchronized void setup(int channels, ImageStack stack2) {
        if (stack2!=null && stack2.getSize()>0 && (stack2.getProcessor(1) instanceof ColorProcessor)) { // RGB?
            cip = null;
            lut = null;
            return;
        }
        setupLuts(channels);
        if (mode==COMPOSITE) {
            cip = new ImageProcessor[channels];
            for (int i=0; i<channels; ++i) {
                cip[i] = stack2.getProcessor(i+1);
                cip[i].setLut(lut[i]);
            }
            currentSlice = currentFrame = 1;
        }
    }

    void setupLuts(int channels) {
        if (ip==null)
            return;
        if (lut==null || lut.length<channels) {
            if (displayRanges!=null && channels!=displayRanges.length/2)
                displayRanges = null;
            if (displayRanges==null&&ip.getMin()==0.0&&ip.getMax()==0.0)
                ip.resetMinAndMax();
            lut = new LUT[channels];
            LUT lut2 = channels>MAX_CHANNELS?createLutFromColor(Color.white):null;
            for (int i=0; i<channels; ++i) {
                if (channelLuts!=null && i<channelLuts.length) {
                    lut[i] = createLutFromBytes(channelLuts[i]);
                    customLuts = true;
                } else if (i<MAX_CHANNELS)
                    lut[i] = createLutFromColor(colors[i]);
                else
                    lut[i] = (LUT)lut2.clone();
                if (displayRanges!=null) {
                    lut[i].min = displayRanges[i*2];
                    lut[i].max = displayRanges[i*2+1];
                } else {
                    lut[i].min = ip.getMin();
                    lut[i].max = ip.getMax();
                }
            }
            displayRanges = null;
        }
    }
    
    public void resetDisplayRanges() {
        int channels = getNChannels();
        if (lut==null)
            setupLuts(channels);
        ImageStack stack2 = getImageStack();
        if (lut==null || channels!=lut.length || channels>stack2.getSize() || channels>MAX_CHANNELS)
            return;
        for (int i=0; i<channels; ++i) {
            ImageProcessor ip2 = stack2.getProcessor(i+1);
            ip2.resetMinAndMax();
            lut[i].min = ip2.getMin();
            lut[i].max = ip2.getMax();
        }
    }

    public void updateAndDraw() {
        if (win==null) {
            img = null;
            return;
        }
        updateImage();
        if (win!=null)
            notifyListeners(UPDATED);
        draw();
    }

    public synchronized void updateImage() {
        int imageSize = width*height;
        int nChannels = getNChannels();
        int redValue, greenValue, blueValue;
        int ch = getChannel();
        
        //IJ.log("updateImage: "+ch+"/"+nChannels+" "+currentSlice+" "+currentFrame);
        if (ch>nChannels) ch = nChannels;
        boolean newChannel = false;
        if (ch-1!=currentChannel) {
            previousChannel = currentChannel;
            currentChannel = ch-1;
            newChannel = true;
        }

        ImageProcessor ip = getProcessor();
        if (mode!=COMPOSITE) {
            if (newChannel) {
                setupLuts(nChannels);
                LUT cm = lut[currentChannel];
                if (ip!=null && !(ip instanceof ColorProcessor)) {
                    if (mode==COLOR)
                        ip.setLut(cm);
                    if (!(cm.min==0.0&&cm.max==0.0))
                        ip.setMinAndMax(cm.min, cm.max);
                }
                if (!IJ.isMacro()) ContrastAdjuster.update();
                for (int i=0; i<MAX_CHANNELS; i++)
                    active[i] = i==currentChannel?true:false;
                Channels.updateChannels();
            }
            if (ip!=null)
                img = ip.createImage();
            return;
        }

        if (nChannels==1) {
            cip = null;
            rgbPixels = null;
            awtImage = null;
            if (ip!=null)
                img = ip.createImage();
            return;
        }
    
        if (cip==null||cip[0].getWidth()!=width||cip[0].getHeight()!=height||getBitDepth()!=bitDepth) {
            setup(nChannels, getImageStack());
            rgbPixels = null;
            rgbSampleModel = null;
            if (currentChannel>=nChannels) {
                setSlice(1);
                currentChannel = 0;
                newChannel = true;
            }
            bitDepth = getBitDepth();
        }
        
        if (newChannel) {
            getProcessor().setMinAndMax(cip[currentChannel].getMin(), cip[currentChannel].getMax());
            if (!IJ.isMacro()) ContrastAdjuster.update();
        }
        //IJ.log(nChannels+" "+ch+" "+currentChannel+"  "+newChannel);
                
        if (getSlice()!=currentSlice || getFrame()!=currentFrame) {
            currentSlice = getSlice();
            currentFrame = getFrame();
            int position = getStackIndex(1, currentSlice, currentFrame);
            if (cip==null) return;
            for (int i=0; i<nChannels; ++i)
                cip[i].setPixels(getImageStack().getProcessor(position+i).getPixels());
        }

        if (rgbPixels == null) {
            rgbPixels = new int[imageSize];
            newPixels = true;
            imageSource = null;
            rgbRaster = null;
            rgbImage = null;
        }
        cip[currentChannel].setMinAndMax(ip.getMin(),ip.getMax());
        int projectionMode = ImageProcessor.SUM_PROJECTION;
        String prop = getProp("CompositeProjection");
        if (prop!=null) {
            if (prop.contains("Max")||prop.contains("max"))
                projectionMode = ImageProcessor.MAX_PROJECTION;
            else if (prop.contains("Min")||prop.contains("min"))
                projectionMode = ImageProcessor.MIN_PROJECTION;
            else if (prop.contains("Invert")||prop.contains("invert"))
                projectionMode = ImageProcessor.INVERT_PROJECTION;
        }
        long t0 = IJ.debugMode?System.nanoTime():0L;
        if (singleChannel && nChannels<=3) {
            switch (currentChannel) {
                case 0: cip[0].updateComposite(rgbPixels, ImageProcessor.UPDATE_RED); break;
                case 1: cip[1].updateComposite(rgbPixels, ImageProcessor.UPDATE_GREEN); break;
                case 2: cip[2].updateComposite(rgbPixels, ImageProcessor.UPDATE_BLUE); break;
            }
        } else if (projectionMode==ImageProcessor.INVERT_PROJECTION){
            makeInvertedComposite(active);
        } else {
            if (cip==null) return;
            if (syncChannels) {
                ImageProcessor ip2 = getProcessor();
                double min=ip2.getMin(), max=ip2.getMax();
                for (int i=0; i<nChannels; i++) {
                    cip[i].setMinAndMax(min, max);
                    lut[i].min = min;
                    lut[i].max = max;
                }
                syncChannels = false;
            }
            if (active[0])
                cip[0].updateComposite(rgbPixels, ImageProcessor.SET_FIRST_CHANNEL);
            else {
                int fill = projectionMode==ImageProcessor.MIN_PROJECTION?0xffffff:0;
                for (int i=1; i<imageSize; i++)
                    rgbPixels[i] = fill;
            }
            if (cip==null || nChannels>cip.length)
                return;
            for (int i=1; i<nChannels; i++)
                if (active[i]) cip[i].updateComposite(rgbPixels, projectionMode);
        }
        if (IJ.debugMode) IJ.log(""+(System.nanoTime()-t0)/1000L);
        createBufferedImage();
        if (img==null && awtImage!=null)
            img = awtImage;
        singleChannel = false;
    }       
    
    // Creates multi-channel composite view with inverted LUTs
    // https://forum.image.sc/t/multi-channel-composite-view-with-inverted-luts-in-imagej-fiji/61163
    // Peter Haub, 12'2021
    private void makeInvertedComposite(boolean[] chnActive) {
        int bitDepth = getBitDepth();
        int w = getWidth();
        int h = getHeight();
        int nChn = getNChannels();
        
        int nChnActive = 0;
        for (int c=0; c<nChn; c++){
            if (chnActive[c])
                nChnActive++;
        }       
        
        byte[][] in8 = null;
        short[][] in16 = null;
        float[][] in32 = null;
        switch (bitDepth) {
            case 8: in8=new byte[nChn][]; break;
            case 16: in16=new short[nChn][]; break;
            case 32: in32=new float[nChn][]; break;
        }
        double[] mins = new double[nChn];
        double[] maxs = new double[nChn];
        double[] scale = new double[nChn];      
        LUT[] luts = getLuts();
        
        for (int c=0; c<nChn; c++){
            mins[c] = cip[c].getMin();
            maxs[c] = cip[c].getMax();
            scale[c] = (255.0 / (maxs[c] - mins[c]));           
            switch (bitDepth) {
                case 8: in8[c] = (byte[]) cip[c].getPixels(); break;
                case 16: in16[c] = (short[]) cip[c].getPixels(); break;
                case 32: in32[c] = (float[]) cip[c].getPixels(); break;
            }
        } 
                        
        int value;
        int[] v = new int[nChn];
        int[] r = new int[nChn]; int[] g = new int[nChn]; int[] b = new int[nChn];
        int sumR, sumG, sumB;
        int newR, newG, newB;
        
        for (int idx=0; idx<w*h; idx++) {
            for (int c=0; c<nChn; c++){
                switch (bitDepth) {
                    case 8: v[c] = (int)Math.floor(((in8[c][idx]&0xff)-mins[c])*scale[c]); break;
                    case 16: v[c] = (int)Math.floor(((in16[c][idx]&0xffff)-mins[c])*scale[c]); break;
                    case 32: v[c] = (int)Math.floor((in32[c][idx]-mins[c])*scale[c]); break;
                }
                v[c] = Math.min(v[c], 255);
                v[c] = Math.max(v[c], 0);
                r[c] = luts[c].getRed(v[c]);
                g[c] = luts[c].getGreen(v[c]);
                b[c] = luts[c].getBlue(v[c]);                   
            }

            // Modify 'composite merge' condition here
            sumR = sumG = sumB = 0;
            for (int c=0; c<nChn; c++){
                if (chnActive[c]){              
                    sumR += r[c];
                    sumG += g[c];
                    sumB += b[c];
                }
            }
            newR = sumR - (nChnActive-1)*255;
            newG = sumG - (nChnActive-1)*255;
            newB = sumB - (nChnActive-1)*255;

            newR = Math.max(newR, 0);
            newG = Math.max(newG, 0);
            newB = Math.max(newB, 0);
            value = newR*256*256 + newG*256 + newB;
            rgbPixels[idx] = value;
        }   
    }
       
    void createImage() {
        if (imageSource==null) {
            rgbCM = new DirectColorModel(32, 0xff0000, 0xff00, 0xff);
            imageSource = new MemoryImageSource(width, height, rgbCM, rgbPixels, 0, width);
            imageSource.setAnimated(true);
            imageSource.setFullBufferUpdates(true);
            awtImage = Toolkit.getDefaultToolkit().createImage(imageSource);
            newPixels = false;
        } else if (newPixels){
            imageSource.newPixels(rgbPixels, rgbCM, 0, width);
            newPixels = false;
        } else
            imageSource.newPixels();    
    }

    void createBufferedImage() {
        if (rgbSampleModel==null)
            rgbSampleModel = getRGBSampleModel();
        if (rgbRaster==null) {
            DataBuffer dataBuffer = new DataBufferInt(rgbPixels, width*height, 0);
            rgbRaster = Raster.createWritableRaster(rgbSampleModel, dataBuffer, null);
        }
        if (rgbImage==null)
            rgbImage = new BufferedImage(rgbCM, rgbRaster, false, null);
        awtImage = rgbImage;
    }

    SampleModel getRGBSampleModel() {
        rgbCM = new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
        WritableRaster wr = rgbCM.createCompatibleWritableRaster(1, 1);
        SampleModel sampleModel = wr.getSampleModel();
        sampleModel = sampleModel.createCompatibleSampleModel(width, height);
        return sampleModel;
    }

    ImageStack getRGBStack(ImagePlus imp) {
        ImageProcessor ip = imp.getProcessor();
        int w = ip.getWidth();
        int h = ip.getHeight();
        int size = w*h;
        byte[] r = new byte[size];
        byte[] g = new byte[size];
        byte[] b = new byte[size];
        ((ColorProcessor)ip).getRGB(r, g, b);
        ImageStack stack = new ImageStack(w, h);
        stack.addSlice("Red", r);   
        stack.addSlice("Green", g); 
        stack.addSlice("Blue", b);
        stack.setColorModel(ip.getDefaultColorModel());
        return stack;
    }

    public LUT createLutFromColor(Color color) {
        return LUT.createLutFromColor(color);
    }
    
    LUT createLutFromBytes(byte[] bytes) {
        if (bytes==null || bytes.length!=768)
            return createLutFromColor(Color.white);
        byte[] r = new byte[256];
        byte[] g = new byte[256];
        byte[] b = new byte[256];
        for (int i=0; i<256; i++) r[i] = bytes[i];
        for (int i=0; i<256; i++) g[i] = bytes[256+i];
        for (int i=0; i<256; i++) b[i] = bytes[512+i];
        return new LUT(r, g, b);
    }

    /** Returns the color used to display the image subtitle and "B&C" histogram. */
    public Color getChannelColor() {
        if (lut==null || mode==GRAYSCALE)
            return Color.black;
        IndexColorModel cm = lut[getChannelIndex()];
        if (cm==null)
            return Color.black;
        int index = cm.getMapSize() - 1;
        int r = cm.getRed(index);
        int g = cm.getGreen(index);
        int b = cm.getBlue(index);
        //IJ.log(index+" "+r+" "+g+" "+b);
        if (r<100 || g<100 || b<100)
            return new Color(r, g, b);
        else
            return Color.black;
    }

    public ImageProcessor getProcessor(int channel) {
        if (cip==null || channel>cip.length)
            return null;
        else
            return cip[channel-1];
    }

    public boolean[] getActiveChannels() {
        return active;
    }
    
    public synchronized void setMode(int mode) {
        if (mode<COMPOSITE || mode>GRAYSCALE)
            return;
        if (mode==COMPOSITE && getNChannels()>MAX_CHANNELS)
            mode = COLOR;
        if (!(mode==COMPOSITE && mode==this.mode)) {
            for (int i=0; i<MAX_CHANNELS; i++)
                active[i] = true;
        }
        if (this.mode!=COMPOSITE && mode==COMPOSITE)
            img = null;
        this.mode = mode;
        if (mode==COLOR || mode==GRAYSCALE) {
            if (cip!=null) {
                for (int i=0; i<cip.length; i++) {
                    if (cip[i]!=null) cip[i].setPixels(null);
                    cip[i] = null;
                }
            }
            cip = null;
            rgbPixels = null;
            awtImage = null;
            currentChannel = -1;
        }
        if (mode==GRAYSCALE || mode==TRANSPARENT)
            ip.setColorModel(ip.getDefaultColorModel());
        Channels.updateChannels();
    }

    public int getMode() {
        return mode;
    }
    
    public String getModeAsString() {
        switch (mode) {
            case COMPOSITE: return "composite";
            case COLOR: return "color";
            case GRAYSCALE: return "grayscale";
        }
        return "";
    }
    
    /* Returns the LUT used by the specified channel. */
    public LUT getChannelLut(int channel) {
        int channels = getNChannels();
        if (lut==null) setupLuts(channels);
        if (channel<1 || channel>lut.length)
            throw new IllegalArgumentException("Channel out of range: "+channel);
        return lut[channel-1];
    }
    
    /* Returns the LUT used by the current channel. */
    public LUT getChannelLut() {
        int c = getChannelIndex();
        return lut[c];
    }
    
    /* Returns a copy of this image's channel LUTs as an array. */
    public LUT[] getLuts() {
        int channels = getNChannels();
        if (lut==null)
            setupLuts(channels);
        LUT[] luts = new LUT[channels];
        for (int i=0; i<channels; i++) {
            if (i<lut.length)
                luts[i] = (LUT)lut[i].clone();
            else
                luts[i] = (LUT)lut[0].clone();
        }
        return luts;
    }

    /* Sets the channel LUTs with clones of the LUTs in 'luts'. */
    public void setLuts(LUT[] luts) {
        int channels = getNChannels();
        if (lut==null) setupLuts(channels);
        if (luts==null || luts.length<channels)
            throw new IllegalArgumentException("Lut array is null or too small");
        for (int i=0; i<channels; i++)
            setChannelLut(luts[i], i+1);
    }
    
    /** Copies the LUTs and display mode of 'imp' to this image. Does
        nothing if 'imp' is not a CompositeImage or 'imp' and this
        image do not have the same number of channels. */
    public synchronized void copyLuts(ImagePlus imp) {
        int channels = getNChannels();
        if (!imp.isComposite() || imp.getNChannels()!=channels)
            return;
        CompositeImage ci = (CompositeImage)imp;
        LUT[] luts = ci.getLuts();
        if (luts!=null && luts.length==channels) {
            lut = luts;
            cip = null;
        }
        int mode2 = ci.getMode();
        setMode(mode2);
        if (mode2==COMPOSITE) {
            boolean[] active2 = ci.getActiveChannels();
            for (int i=0; i<MAX_CHANNELS; i++)
                active[i] = active2[i];
        }
        if (ci.hasCustomLuts())
            customLuts = true;
    }

    int getChannelIndex() {
        int channels = getNChannels();
        if (lut==null) setupLuts(channels);
        int index = getChannel()-1;
        return index;
    }
    
    public void reset() {
        int nChannels = getNChannels();
        if (nChannels>MAX_CHANNELS && getMode()==COMPOSITE)
            setMode(COLOR);
        setup(nChannels, getImageStack());
    }
    
    public void completeReset() {
        cip = null;
        lut = null;
    }
    
    /* Sets the LUT of the current channel. */
    public void setChannelLut(LUT table) {
        int c = getChannelIndex();
        double min = lut[c].min;
        double max = lut[c].max;
        lut[c] = table;
        lut[c].min = min;
        lut[c].max = max;
        if (mode==COMPOSITE && cip!=null && c<cip.length) {
            cip[c].setColorModel(lut[c] );
            imageSource = null;
            newPixels = true;
            img = null;
        }
        currentChannel = -1;
        getProcessor().setLut(table);
        customLuts = true;
        if (!IJ.isMacro()) ContrastAdjuster.update();
    }
    
    /* Sets the LUT of the specified channel using a clone of 'table'. */
    public synchronized void setChannelLut(LUT table, int channel) {
        int channels = getNChannels();
        if (lut==null) setupLuts(channels);
        if (channel<1 || channel>lut.length)
            throw new IllegalArgumentException("Channel out of range");
        lut[channel-1] = (LUT)table.clone();
        if (getWindow()!=null && channel==getChannel())
            getProcessor().setLut(lut[channel-1]);
        if (cip!=null && cip.length>=channel && cip[channel-1]!=null)
            cip[channel-1].setLut(lut[channel-1]);
        else
            cip = null;
        customLuts = true;
    }

    /* Sets the IndexColorModel of the current channel. */
    public void setChannelColorModel(IndexColorModel cm) {
        setChannelLut(new LUT(cm,0.0,0.0));
    }
    
    public void setDisplayRange(double min, double max) {
        ip.setMinAndMax(min, max);
        int c = getChannelIndex();
        lut[c].min = min;
        lut[c].max = max;
        if (getWindow()==null && cip!=null && c<cip.length)
            cip[c].setLut(lut[c]);
    }

    public double getDisplayRangeMin() {
        if (lut!=null)
            return lut[getChannelIndex()].min;
        else
            return 0.0;
    }

    public double getDisplayRangeMax() {
        if (lut!=null)
            return lut[getChannelIndex()].max;
        else
            return 255.0;
    }

    public void resetDisplayRange() {
        if (getType()==GRAY16 && getDefault16bitRange()!=0) {
            int defaultRange = getDefault16bitRange();
            for (int i=1; i<=getNChannels(); i++) {
                LUT lut = getChannelLut(i);
                lut.min = 0;
                lut.max = Math.pow(2,defaultRange)-1;
                if (getWindow()!=null)
                    setChannelLut(lut, i);
            }
        } else {
            ip.resetMinAndMax();
            int c = getChannelIndex();
            lut[c].min = ip.getMin();
            lut[c].max = ip.getMax();
        }
    }
    
    public boolean hasCustomLuts() {
        return customLuts && mode!=GRAYSCALE;
    }
    
    public void close() {
        super.close();
        rgbPixels = null;
        imageSource = null;
        awtImage = null;
        rgbRaster = null;
        rgbSampleModel = null;
        rgbImage = null;
        rgbCM = null;
        if (cip!=null) {
            for (int i=0; i<cip.length; i++)
                cip[i] = null;
            cip = null;
        }
        if (lut!=null) {
            for (int i=0; i<lut.length; i++)
                lut[i] = null;
            lut = null;
        }
        if (channelLuts!=null) {
            for (int i=0; i<channelLuts.length; i++)
                channelLuts[i] = null;
            channelLuts = null;
        }
    }

    /** Deprecated */
    public synchronized void setChannelsUpdated() {
        if (cip!=null) {
            for (int i=0; i<cip.length; i++) {
                if (cip[i]!=null) cip[i].setPixels(null);
                cip[i] = null;
            }
        }
        cip = null;
        lut = null;
        img = null;
        currentChannel = -1;
        previousChannel = 0;
        currentSlice = currentFrame = 1;
        singleChannel = false;
        rgbPixels = null;
        awtImage = null;
        channelLuts = null;
        boolean[] active = new boolean[MAX_CHANNELS];
    }

}