MMCQ 中位切分法 Java 代码实现

参考:
1、Modified Median Cut Quantization(MMCQ) Leptonica
http://tpgit.github.io/UnOfficialLeptDocs/leptonica/color-quantization.html
2、图像主题色提取算法 https://blog.csdn.net/shanglianlm/article/details/50051269
3、图像颜色提取 https://segmentfault.com/a/1190000009832996
4、The incredibly challenging task of sorting colours http://www.alanzucconi.com/2015/09/30/colour-sorting/
5、一种基于HSV空间的颜色相似度计算方法 https://wenku.baidu.com/view/f2f1b0f7a58da0116d17490e.html

package com.colortheme;

import static com.colortheme.ColorUtils.BLUE;
import static com.colortheme.ColorUtils.GREEN;
import static com.colortheme.ColorUtils.RED;
import static com.colortheme.ColorUtils.distanceToBGW;
import static com.colortheme.ColorUtils.getColorPart;
import static com.colortheme.ColorUtils.isSimilarColor;

import android.graphics.Bitmap;
import android.graphics.Color;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

/**
 * Created by hgm on 9/20/18.
 */

/**
 * The color space is divided up into a set of 3D rectangular regions (called `vboxes`)
 */
class VBox implements Comparable<VBox> {
    
    
    final int r1;
    final int r2;
    final int g1;
    final int g2;
    final int b1;
    final int b2;
    final Map<Integer, Long> mHisto;
    final long mNumPixs;
    final long mVolume;
    final int mAxis;
    final int mMultiple;
    private int mAvgColor = -1;

    VBox(int r1, int r2, int g1, int g2, int b1, int b2, int multiple, Map<Integer, Long> histo) {
    
    
        this.r1 = r1;
        this.r2 = r2;
        this.g1 = g1;
        this.g2 = g2;
        this.b1 = b1;
        this.b2 = b2;
        mMultiple = multiple;
        mHisto = histo;
        mNumPixs = population();
        final int rl = Math.abs(r2 - r1) + 1;
        final int gl = Math.abs(g2 - g1) + 1;
        final int bl = Math.abs(b2 - b1) + 1;
        mVolume = rl * gl * bl;
        final int max = Math.max(Math.max(rl, gl), bl);
        if (max == rl) {
    
    
            mAxis = RED;
        } else if (max == gl) {
    
    
            mAxis = GREEN;
        } else {
    
    
            mAxis = BLUE;
        }
    }

    private long population() {
    
    
        long sum = 0;
        for (int r = r1; r <= r2; r++) {
    
    
            for (int g = g1; g <= g2; g++) {
    
    
                for (int b = b1; b <= b2; b++) {
    
    
                    Long count = mHisto.get(MMCQ.getColorIndexWithRgb(r, g, b));
                    if (count != null) {
    
    
                        sum += count;
                    }
                }
            }
        }
        return sum;
    }

    public int getAvgColor() {
    
    
        if (mAvgColor == -1) {
    
    
            long total = 0;
            long rSum = 0;
            long gSum = 0;
            long bSum = 0;

            for (int r = r1; r <= r2; r++) {
    
    
                for (int g = g1; g <= g2; g++) {
    
    
                    for (int b = b1; b <= b2; b++) {
    
    
                        Long count = mHisto.get(MMCQ.getColorIndexWithRgb(r, g, b));
                        if (count != null) {
    
    
                            total += count;
                            rSum += count * (r + 0.5) * mMultiple;
                            gSum += count * (g + 0.5) * mMultiple;
                            bSum += count * (b + 0.5) * mMultiple;
                        }
                    }
                }
            }

            int r, g, b;
            if (total == 0) {
    
    
                r = (r1 + r2 + 1) * mMultiple / 2;
                g = (g1 + g2 + 1) * mMultiple / 2;
                b = (b2 + b2 + 1) * mMultiple / 2;
            } else {
    
    
                r = (int) (rSum / total);
                g = (int) (gSum / total);
                b = (int) (bSum / total);
            }
            mAvgColor = Color.rgb(r, g, b);
        }

        return mAvgColor;
    }

    public long getPriority() {
    
    
        return -mNumPixs;
    }

    @Override
    public int compareTo(VBox o) {
    
    
        long priority = getPriority();
        long oPriority = o.getPriority();
        return (priority < oPriority) ? -1 : ((priority == oPriority) ? 0 : 1);
    }
}

/**
 * Modified Median Cut Quantization(MMCQ)
 * Leptonica: http://tpgit.github.io/UnOfficialLeptDocs/leptonica/color-quantization.html
 */
public class MMCQ {
    
    
    private static final int MAX_ITERATIONS = 100;

    private Bitmap.Config mConfig;
    private int[] mPixelRGB;
    private int mMaxColor;
    private double mFraction = 0.85;
    private int mSigbits = 5;
    private int mRshift = 8 - mSigbits;
    private int mWidth;
    private int mHeight;
    private Map<Integer, Long> mPixHisto;

    /**
     * @param bitmap   Image data [[A, R, G, B], ...]
     * @param maxColor Between [2, 256]
     * @param fraction Between [0.3, 0.9]
     * @param sigbits  5 or 6
     */
    public MMCQ(Bitmap bitmap, int maxColor, double fraction, int sigbits) {
    
    
        if (maxColor < 2 || maxColor > 256) {
    
    
            throw new IllegalArgumentException("maxColor should between [2, 256]!");
        }
        mMaxColor = maxColor;
        if (fraction < 0.3 || fraction > 0.9) {
    
    
            throw new IllegalArgumentException("fraction should between [0.3, 0.9]!");
        }
        mFraction = fraction;
        if (sigbits < 5 || sigbits > 6) {
    
    
            throw new IllegalArgumentException("sigbits should between [5, 6]!");
        }
        mSigbits = sigbits;
        mRshift = 8 - mSigbits;

        int height = bitmap.getHeight();
        int width = bitmap.getWidth();
        double hScale = 100d / (double) height;
        double wScale = 100d / (double) width;
        double scale = Math.min(hScale, wScale);
        if (scale < 0.8) {
    
    
            bitmap = Bitmap.createScaledBitmap(
                    bitmap, (int) (scale * width), (int) (scale * height), false);
        }
        mConfig = bitmap.getConfig();
        mWidth = bitmap.getWidth();
        mHeight = bitmap.getHeight();
        mPixelRGB = new int[mWidth * mHeight];
        bitmap.getPixels(mPixelRGB, 0, mWidth, 0, 0, mWidth, mHeight);

        initPixHisto();
    }

    private void initPixHisto() {
    
    
        if (mPixHisto != null) {
    
    
            return;
        }

        mPixHisto = new HashMap<>();

        for (int color : mPixelRGB) {
    
    
            int alpha = Color.alpha(color);
            if (alpha < 128) {
    
    
                continue;
            }
            int red = Color.red(color) >> mRshift;
            int green = Color.green(color) >> mRshift;
            int blue = Color.blue(color) >> mRshift;
            Integer colorIndex = getColorIndexWithRgb(red, green, blue);
            Long count = mPixHisto.get(colorIndex);
            if (count == null) {
    
    
                mPixHisto.put(colorIndex, 1L);
            } else {
    
    
                mPixHisto.put(colorIndex, count + 1);
            }
        }
    }

    public static int getColorIndexWithRgb(int red, int green, int blue) {
    
    
        return (red << 16) | (green << 8) | blue;
    }

    private VBox createVBox() {
    
    
        int rMax = getMax(RED) >> mRshift;
        int rMin = getMin(RED) >> mRshift;
        int gMax = getMax(GREEN) >> mRshift;
        int gMin = getMin(GREEN) >> mRshift;
        int bMax = getMax(BLUE) >> mRshift;
        int bMin = getMin(BLUE) >> mRshift;

        return new VBox(rMin, rMax, gMin, gMax, bMin, bMax, 1 << mRshift, mPixHisto);
    }

    private int getMax(int which) {
    
    
        int max = 0;
        for (int color : mPixelRGB) {
    
    
            int value = getColorPart(color, which);
            if (max < value) {
    
    
                max = value;
            }
        }
        return max;
    }

    private int getMin(int which) {
    
    
        int min = Integer.MAX_VALUE;
        for (int color : mPixelRGB) {
    
    
            int value = getColorPart(color, which);
            if (min > value) {
    
    
                min = value;
            }
        }
        return min;
    }

    private static VBox[] medianCutApply(VBox vBox) {
    
    
        long nPixs = 0;

        switch (vBox.mAxis) {
    
    
            case RED: // Red axis is largest
                for (int r = vBox.r1; r <= vBox.r2; r++) {
    
    
                    for (int g = vBox.g1; g <= vBox.g2; g++) {
    
    
                        for (int b = vBox.b1; b <= vBox.b2; b++) {
    
    
                            Long count = vBox.mHisto.get(getColorIndexWithRgb(r, g, b));
                            if (count != null) {
    
    
                                nPixs += count;
                            }
                        }
                    }
                    if (nPixs >= vBox.mNumPixs / 2) {
    
    
                        int left = r - vBox.r1;
                        int right = vBox.r2 - r;
                        int r2 = (left >= right) ?
                                Math.max(vBox.r1, r - 1 - left / 2) :
                                Math.min(vBox.r2 - 1, r + right / 2);
                        VBox vBox1 = new VBox(vBox.r1, r2, vBox.g1, vBox.g2, vBox.b1, vBox.b2,
                                vBox.mMultiple, vBox.mHisto);
                        VBox vBox2 = new VBox(r2 + 1, vBox.r2, vBox.g1, vBox.g2, vBox.b1, vBox.b2,
                                vBox.mMultiple, vBox.mHisto);
                        //System.out.println("VBOX " + vBox1.mNumPixs + " " + vBox2.mNumPixs);
                        if (isSimilarColor(vBox1.getAvgColor(), vBox2.getAvgColor())) {
    
    
                            break;
                        } else {
    
    
                            return new VBox[]{
    
    vBox1, vBox2};
                        }
                    }
                }

            case GREEN: // Green axis is largest
                for (int g = vBox.g1; g <= vBox.g2; g++) {
    
    
                    for (int b = vBox.b1; b <= vBox.b2; b++) {
    
    
                        for (int r = vBox.r1; r <= vBox.r2; r++) {
    
    
                            Long count = vBox.mHisto.get(getColorIndexWithRgb(r, g, b));
                            if (count != null) {
    
    
                                nPixs += count;
                            }
                        }
                    }
                    if (nPixs >= vBox.mNumPixs / 2) {
    
    
                        int left = g - vBox.g1;
                        int right = vBox.g2 - g;
                        int g2 = (left >= right) ?
                                Math.max(vBox.g1, g - 1 - left / 2) :
                                Math.min(vBox.g2 - 1, g + right / 2);
                        VBox vBox1 = new VBox(vBox.r1, vBox.r2, vBox.g1, g2, vBox.b1, vBox.b2,
                                vBox.mMultiple, vBox.mHisto);
                        VBox vBox2 = new VBox(vBox.r1, vBox.r2, g2 + 1, vBox.g2, vBox.b1, vBox.b2,
                                vBox.mMultiple, vBox.mHisto);
                        //System.out.println("VBOX " + vBox1.mNumPixs + " " + vBox2.mNumPixs);
                        if (isSimilarColor(vBox1.getAvgColor(), vBox2.getAvgColor())) {
    
    
                            break;
                        } else {
    
    
                            return new VBox[]{
    
    vBox1, vBox2};
                        }
                    }
                }

            case BLUE: // Blue axis is largest
                for (int b = vBox.b1; b <= vBox.b2; b++) {
    
    
                    for (int r = vBox.r1; r <= vBox.r2; r++) {
    
    
                        for (int g = vBox.g1; g <= vBox.g2; g++) {
    
    
                            Long count = vBox.mHisto.get(getColorIndexWithRgb(r, g, b));
                            if (count != null) {
    
    
                                nPixs += count;
                            }
                        }
                    }
                    if (nPixs >= vBox.mNumPixs / 2) {
    
    
                        int left = b - vBox.b1;
                        int right = vBox.b2 - b;
                        int b2 = (left >= right) ?
                                Math.max(vBox.b1, b - 1 - left / 2) :
                                Math.min(vBox.b2 - 1, b + right / 2);
                        VBox vBox1 = new VBox(vBox.r1, vBox.r2, vBox.g1, vBox.g2, vBox.b1, b2,
                                vBox.mMultiple, vBox.mHisto);
                        VBox vBox2 = new VBox(vBox.r1, vBox.r2, vBox.g1, vBox.g2, b2 + 1, vBox.b2,
                                vBox.mMultiple, vBox.mHisto);
                        //System.out.println("VBOX " + vBox1.mNumPixs + " " + vBox2.mNumPixs);
                        if (isSimilarColor(vBox1.getAvgColor(), vBox2.getAvgColor())) {
    
    
                            break;
                        } else {
    
    
                            return new VBox[]{
    
    vBox1, vBox2};
                        }
                    }
                }
        }
        return new VBox[]{
    
    vBox, null};
    }

    private static void iterCut(int maxColor, PriorityQueue<VBox> boxQueue) {
    
    
        int nColors = 1;
        int nIters = 0;
        List<VBox> store = new ArrayList<>();
        while (true) {
    
    
            if (nColors >= maxColor || boxQueue.isEmpty()) {
    
    
                break;
            }
            VBox vBox = boxQueue.poll();
            if (vBox.mNumPixs == 0) {
    
    
                System.out.println("Vbox has no pixels");
                //boxQueue.offer(vBox);
                continue;
            }
            VBox[] vBoxes = medianCutApply(vBox);
            if (vBoxes[0] == vBox || vBoxes[0].mNumPixs == vBox.mNumPixs) {
    
    
                store.add(vBoxes[0]);
                continue;
            }
            boxQueue.offer(vBoxes[0]);
            //if (vBoxes[1] != null) {
    
    
            nColors += 1;
            boxQueue.offer(vBoxes[1]);
            //}
            nIters += 1;
            if (nIters >= MAX_ITERATIONS) {
    
    
                System.out.println("Infinite loop; perhaps too few pixels!");
                break;
            }
        }
        boxQueue.addAll(store);
    }

    public PriorityQueue<ThemeColor> quantize() {
    
    
        if (mWidth * mHeight < mMaxColor) {
    
    
            throw new IllegalArgumentException(
                    "Image({" + mWidth + "}x{" + mHeight + "}) too small to be quantized");
        }

        VBox oriVBox = createVBox();
        PriorityQueue<VBox> pOneQueue = new PriorityQueue<>(mMaxColor);
        pOneQueue.offer(oriVBox);
        int popColors = (int) (mMaxColor * mFraction);
        iterCut(popColors, pOneQueue);

        PriorityQueue<VBox> boxQueue = new PriorityQueue<>(mMaxColor, new Comparator<VBox>() {
    
    
            @Override
            public int compare(VBox o1, VBox o2) {
    
    
                long priority1 = o1.getPriority() * o1.mVolume;
                long priority2 = o2.getPriority() * o2.mVolume;
                return (priority1 < priority2) ? -1 : ((priority1 == priority2) ? 0 : 1);
            }
        });

        boxQueue.addAll(pOneQueue);
        pOneQueue.clear();

        iterCut(mMaxColor - popColors + 1, boxQueue);

        pOneQueue.addAll(boxQueue);
        boxQueue.clear();

        PriorityQueue<ThemeColor> themeColors = new PriorityQueue<>(mMaxColor);

        while (!pOneQueue.isEmpty()) {
    
    
            VBox vBox = pOneQueue.poll();
            double proportion = (double) vBox.mNumPixs / oriVBox.mNumPixs;
            if (proportion < 0.05) {
    
    
                continue;
            }
            ThemeColor themeColor = new ThemeColor(vBox.getAvgColor(), proportion, mConfig);
            themeColors.offer(themeColor);
        }

        return themeColors;
    }
}

class ThemeColor implements Comparable<ThemeColor> {
    
    
    public final int mColor;
    public final double mProportion;

    private final double mDistance;
    private final double mPriority;
    private final Bitmap.Config mConfig;
    private Bitmap mBitmap;

    public ThemeColor(int color, double proportion, Bitmap.Config config) {
    
    
        mColor = color;
        mProportion = proportion;
        System.out.println("proportion:" + mProportion +
                " RGB:" + Color.red(mColor) + " " + Color.green(mColor) + " " + Color.blue(mColor));
        // (...) / 3d * (3 / 2d)
        mDistance = distanceToBGW(mColor) * Math.sqrt(3 / 2d);
        mPriority = mProportion * (mDistance + 5 / 10d);
        mConfig = config;
    }

    @Override
    public int compareTo(ThemeColor themeColor) {
    
    
        double oPriority = themeColor.mPriority;
        return (mPriority > oPriority) ? -1 : ((mPriority < oPriority) ? 1 : 0);
    }

    public Bitmap getBitmap() {
    
    
        if (mBitmap == null) {
    
    
            int height = 300;
            int width = 300;
            int[] colors = new int[height * width];
            Arrays.fill(colors, mColor);
            mBitmap = Bitmap.createBitmap(colors, width, height, mConfig);
        }
        return mBitmap;
    }
}
package com.colortheme;

import android.graphics.Color;

import java.util.Arrays;

/**
 * Created by hgm on 9/29/18.
 */

public final class ColorUtils {
    
    
    public static final int ALPHA = 0;

    public static final int RED = Color.RED;
    public static final int GREEN = Color.GREEN;
    public static final int BLUE = Color.BLUE;

    public static final int BLACK = Color.BLACK;
    public static final int DKGRAY = Color.DKGRAY;
    public static final int GRAY = Color.GRAY;
    public static final int LTGRAY = Color.LTGRAY;
    public static final int WHITE = Color.WHITE;
    public static final int YELLOW = Color.YELLOW;
    public static final int CYAN = Color.CYAN;
    public static final int MAGENTA = Color.MAGENTA;

    public static int getColorPart(int color, int which) {
    
    
        switch (which) {
    
    
            case ALPHA:
                return Color.alpha(color);
            case RED:
                return Color.red(color);
            case GREEN:
                return Color.green(color);
            case BLUE:
                return Color.blue(color);
            default:
                throw new IllegalArgumentException(
                        "parameter which must be ALPHA/RED/GREEN/BLUE !");
        }
    }

    public static double distanceToBGW(int color) {
    
    
        int r = Color.red(color);
        int g = Color.green(color);
        int b = Color.blue(color);
        double rg = (r - g) / 255d;
        double gb = (g - b) / 255d;
        double br = (b - r) / 255d;
        return Math.sqrt((rg * rg + gb * gb + br * br) / 3d);
    }

    private static final double COLOR_TOLERANCE = 0.5;

    public static boolean isSimilarColor(int color1, int color2) {
    
    
        return colorDistance(color1, color2) < COLOR_TOLERANCE;
    }

    public static double colorDistance(int color1, int color2) {
    
    
        int r1 = Color.red(color1);
        int g1 = Color.green(color1);
        int b1 = Color.blue(color1);
        int r2 = Color.red(color2);
        int g2 = Color.green(color2);
        int b2 = Color.blue(color2);
        double rd = (r1 - r2) / 255d;
        double gd = (g1 - g2) / 255d;
        double bd = (b1 - b2) / 255d;
        double distance = Math.sqrt(rd * rd + gd * gd + bd * bd);
        return distance;
    }

    public static double colorDistanceToBlack(int color) {
    
    
        return colorDistance(color, Color.BLACK);
    }

    public static double colorDistanceToDkGray(int color) {
    
    
        return colorDistance(color, Color.DKGRAY);
    }

    public static double colorDistanceToGray(int color) {
    
    
        return colorDistance(color, Color.GRAY);
    }

    public static double colorDistanceToLtGray(int color) {
    
    
        return colorDistance(color, Color.LTGRAY);
    }

    public static double colorDistanceToWhite(int color) {
    
    
        return colorDistance(color, Color.WHITE);
    }

    public static double colorDistanceToRed(int color) {
    
    
        return colorDistance(color, Color.RED);
    }

    public static double colorDistanceToGreen(int color) {
    
    
        return colorDistance(color, Color.GREEN);
    }

    public static double colorDistanceToBlue(int color) {
    
    
        return colorDistance(color, Color.BLUE);
    }

    public static double colorDistanceToYellow(int color) {
    
    
        return colorDistance(color, Color.YELLOW);
    }

    public static double colorDistanceToCyan(int color) {
    
    
        return colorDistance(color, Color.CYAN);
    }

    public static double colorDistanceToMagenta(int color) {
    
    
        return colorDistance(color, Color.MAGENTA);
    }

    public static int categoryBGW(int color) {
    
    
        double dB = colorDistanceToBlack(color);
        double dDG = colorDistanceToDkGray(color);
        double dG = colorDistanceToGray(color);
        double dLG = colorDistanceToLtGray(color);
        double dW = colorDistanceToWhite(color);
        double[] arr = new double[]{
    
    dB, dDG, dG, dLG, dW};
        Arrays.sort(arr);
        double min1 = arr[0];
        if (min1 == dB) {
    
    
            return Color.BLACK;
        }
        if (min1 == dG) {
    
    
            return Color.GRAY;
        }
        if (min1 == dW) {
    
    
            return Color.WHITE;
        }
        // Now, min1 == dDG or min1 == dLG
        double min2 = arr[1];
        if (min2 == dB) {
    
    
            return Color.BLACK;
        }
        if (min2 == dG) {
    
    
            return Color.GRAY;
        }
        if (min2 == dW) {
    
    
            return Color.WHITE;
        }
        // It shouldn't reach here!
        throw new RuntimeException("It shouldn't reach here!");
    }
}

猜你喜欢

转载自blog.csdn.net/hegan2010/article/details/84308152