android中图片色调识别探究
2015-09-05 23:32
579 查看
项目背景
最近项目组要做这样的一件事,通过访问网站拿到网站的favicon来根据favicon匹配它的颜色,色调然后调用我们自己的绘图板去绘制符合当前网站的图标,当时也在github上面找了下,看看有没有现成能借鉴的,但是失败了,但是无意间发现了android的v7下的palette包,貌似可以实现我想要的效果,但是项目里面不可能引入其他的依赖包,因为v7引用到了v4,一想到我的apk又要增加几十k果断的不干了,所以我就把v7的里面的源码拿出来改了改,然后v4里面的需要的也单独的剥离出来,这样即达到了我需要的效果,也没有给我的apk增加太多的大小。效果
我们这里先看看我要实现的效果,我们要提取这两个icon里面的主色调,然后看看我们提取的效果,
fuck,你在逗我?怎么会有红色呢?不急不急,听我慢慢道来。
实现
首先我们要做的就是移植v7这个包下面的pattern包,看看我的工程目录,需要移植的就是这几个类。我这里也不墨迹了,直接上代码吧,google的注释都是很到位的,也很nice我也不说了,我做的工作就是把这几个类整合提取出来,然后屏蔽一些兼容的问题。这里我先给出这几个类的代码。
package com.bobo.picdis.utils; /* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.graphics.Bitmap; import android.graphics.Color; import android.util.SparseIntArray; import com.bobo.picdis.utils.Palette.Swatch; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; /** * An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct * colors rather than representation colors. * * The color space is represented as a 3-dimensional cube with each dimension being an RGB * component. The cube is then repeatedly divided until we have reduced the color space to the * requested number of colors. An average color is then generated from each cube. * * What makes this different to median-cut is that median-cut divided cubes so that all of the cubes * have roughly the same population, where this quantizer divides boxes based on their color volume. * This means that the color space is divided into distinct colors, rather than representative * colors. */ final class ColorCutQuantizer { private static final String LOG_TAG = ColorCutQuantizer.class.getSimpleName(); private final float[] mTempHsl = new float[3]; private static final float BLACK_MAX_LIGHTNESS = 0.05f; private static final float WHITE_MIN_LIGHTNESS = 0.95f; private static final int COMPONENT_RED = -3; private static final int COMPONENT_GREEN = -2; private static final int COMPONENT_BLUE = -1; private final int[] mColors; private final SparseIntArray mColorPopulations; private final List<Swatch> mQuantizedColors; /** * Factory-method to generate a {@link ColorCutQuantizer} from a {@link Bitmap} object. * * @param bitmap Bitmap to extract the pixel data from * @param maxColors The maximum number of colors that should be in the result palette. */ static ColorCutQuantizer fromBitmap(Bitmap bitmap, int maxColors) { final int width = bitmap.getWidth(); final int height = bitmap.getHeight(); final int[] pixels = new int[width * height]; bitmap.getPixels(pixels, 0, width, 0, 0, width, height); return new ColorCutQuantizer(new ColorHistogram(pixels), maxColors); } /** * Private constructor. * * @param colorHistogram histogram representing an image's pixel data * @param maxColors The maximum number of colors that should be in the result palette. */ private ColorCutQuantizer(ColorHistogram colorHistogram, int maxColors) { final int rawColorCount = colorHistogram.getNumberOfColors(); final int[] rawColors = colorHistogram.getColors(); final int[] rawColorCounts = colorHistogram.getColorCounts(); // First, lets pack the populations into a SparseIntArray so that they can be easily // retrieved without knowing a color's index mColorPopulations = new SparseIntArray(rawColorCount); for (int i = 0; i < rawColors.length; i++) { mColorPopulations.append(rawColors[i], rawColorCounts[i]); } // Now go through all of the colors and keep those which we do not want to ignore mColors = new int[rawColorCount]; int validColorCount = 0; for (int color : rawColors) { if (!shouldIgnoreColor(color)) { mColors[validColorCount++] = color; } } if (validColorCount <= maxColors) { // The image has fewer colors than the maximum requested, so just return the colors mQuantizedColors = new ArrayList<Swatch>(); for (final int color : mColors) { mQuantizedColors.add(new Swatch(color, mColorPopulations.get(color))); } } else { // We need use quantization to reduce the number of colors mQuantizedColors = quantizePixels(validColorCount - 1, maxColors); } } /** * @return the list of quantized colors */ List<Swatch> getQuantizedColors() { return mQuantizedColors; } private List<Swatch> quantizePixels(int maxColorIndex, int maxColors) { // Create the priority queue which is sorted by volume descending. This means we always // split the largest box in the queue final PriorityQueue<Vbox> pq = new PriorityQueue<Vbox>(maxColors, VBOX_COMPARATOR_VOLUME); // To start, offer a box which contains all of the colors pq.offer(new Vbox(0, maxColorIndex)); // Now go through the boxes, splitting them until we have reached maxColors or there are no // more boxes to split splitBoxes(pq, maxColors); // Finally, return the average colors of the color boxes return generateAverageColors(pq); } /** * Iterate through the {@link java.util.Queue}, popping * {@link ColorCutQuantizer.Vbox} objects from the queue * and splitting them. Once split, the new box and the remaining box are offered back to the * queue. * * @param queue {@link java.util.PriorityQueue} to poll for boxes * @param maxSize Maximum amount of boxes to split */ private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) { while (queue.size() < maxSize) { final Vbox vbox = queue.poll(); if (vbox != null && vbox.canSplit()) { // First split the box, and offer the result queue.offer(vbox.splitBox()); // Then offer the box back queue.offer(vbox); } else { // If we get here then there are no more boxes to split, so return return; } } } private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) { ArrayList<Swatch> colors = new ArrayList<Swatch>(vboxes.size()); for (Vbox vbox : vboxes) { Swatch color = vbox.getAverageColor(); if (!shouldIgnoreColor(color)) { // As we're averaging a color box, we can still get colors which we do not want, so // we check again here colors.add(color); } } return colors; } /** * Represents a tightly fitting box around a color space. */ private class Vbox { // lower and upper index are inclusive private int mLowerIndex; private int mUpperIndex; private int mMinRed, mMaxRed; private int mMinGreen, mMaxGreen; private int mMinBlue, mMaxBlue; Vbox(int lowerIndex, int upperIndex) { mLowerIndex = lowerIndex; mUpperIndex = upperIndex; fitBox(); } int getVolume() { return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) * (mMaxBlue - mMinBlue + 1); } boolean canSplit() { return getColorCount() > 1; } int getColorCount() { return mUpperIndex - mLowerIndex + 1; } /** * Recomputes the boundaries of this box to tightly fit the colors within the box. */ void fitBox() { // Reset the min and max to opposite values mMinRed = mMinGreen = mMinBlue = 0xFF; mMaxRed = mMaxGreen = mMaxBlue = 0x0; for (int i = mLowerIndex; i <= mUpperIndex; i++) { final int color = mColors[i]; final int r = Color.red(color); final int g = Color.green(color); final int b = Color.blue(color); if (r > mMaxRed) { mMaxRed = r; } if (r < mMinRed) { mMinRed = r; } if (g > mMaxGreen) { mMaxGreen = g; } if (g < mMinGreen) { mMinGreen = g; } if (b > mMaxBlue) { mMaxBlue = b; } if (b < mMinBlue) { mMinBlue = b; } } } /** * Split this color box at the mid-point along it's longest dimension * * @return the new ColorBox */ Vbox splitBox() { if (!canSplit()) { throw new IllegalStateException("Can not split a box with only 1 color"); } // find median along the longest dimension final int splitPoint = findSplitPoint(); Vbox newBox = new Vbox(splitPoint + 1, mUpperIndex); // Now change this box's upperIndex and recompute the color boundaries mUpperIndex = splitPoint; fitBox(); return newBox; } /** * @return the dimension which this box is largest in */ int getLongestColorDimension() { final int redLength = mMaxRed - mMinRed; final int greenLength = mMaxGreen - mMinGreen; final int blueLength = mMaxBlue - mMinBlue; if (redLength >= greenLength && redLength >= blueLength) { return COMPONENT_RED; } else if (greenLength >= redLength && greenLength >= blueLength) { return COMPONENT_GREEN; } else { return COMPONENT_BLUE; } } /** * Finds the point within this box's lowerIndex and upperIndex index of where to split. * * This is calculated by finding the longest color dimension, and then sorting the * sub-array based on that dimension value in each color. The colors are then iterated over * until a color is found with at least the midpoint of the whole box's dimension midpoint. * * @return the index of the colors array to split from */ int findSplitPoint() { final int longestDimension = getLongestColorDimension(); // We need to sort the colors in this box based on the longest color dimension. // As we can't use a Comparator to define the sort logic, we modify each color so that // it's most significant is the desired dimension modifySignificantOctet(longestDimension, mLowerIndex, mUpperIndex); // Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1 Arrays.sort(mColors, mLowerIndex, mUpperIndex + 1); // Now revert all of the colors so that they are packed as RGB again modifySignificantOctet(longestDimension, mLowerIndex, mUpperIndex); final int dimensionMidPoint = midPoint(longestDimension); for (int i = mLowerIndex; i <= mUpperIndex; i++) { final int color = mColors[i]; switch (longestDimension) { case COMPONENT_RED: if (Color.red(color) >= dimensionMidPoint) { return i; } break; case COMPONENT_GREEN: if (Color.green(color) >= dimensionMidPoint) { return i; } break; case COMPONENT_BLUE: if (Color.blue(color) > dimensionMidPoint) { return i; } break; } } return mLowerIndex; } /** * @return the average color of this box. */ Swatch getAverageColor() { int redSum = 0; int greenSum = 0; int blueSum = 0; int totalPopulation = 0; for (int i = mLowerIndex; i <= mUpperIndex; i++) { final int color = mColors[i]; final int colorPopulation = mColorPopulations.get(color); totalPopulation += colorPopulation; redSum += colorPopulation * Color.red(color); greenSum += colorPopulation * Color.green(color); blueSum += colorPopulation * Color.blue(color); } final int redAverage = Math.round(redSum / (float) totalPopulation); final int greenAverage = Math.round(greenSum / (float) totalPopulation); final int blueAverage = Math.round(blueSum / (float) totalPopulation); return new Swatch(redAverage, greenAverage, blueAverage, totalPopulation); } /** * @return the midpoint of this box in the given {@code dimension} */ int midPoint(int dimension) { switch (dimension) { case COMPONENT_RED: default: return (mMinRed + mMaxRed) / 2; case COMPONENT_GREEN: return (mMinGreen + mMaxGreen) / 2; case COMPONENT_BLUE: return (mMinBlue + mMaxBlue) / 2; } } } /** * Modify the significant octet in a packed color int. Allows sorting based on the value of a * single color component. * * @see Vbox#findSplitPoint() */ private void modifySignificantOctet(final int dimension, int lowerIndex, int upperIndex) { switch (dimension) { case COMPONENT_RED: // Already in RGB, no need to do anything break; case COMPONENT_GREEN: // We need to do a RGB to GRB swap, or vice-versa for (int i = lowerIndex; i <= upperIndex; i++) { final int color = mColors[i]; mColors[i] = Color.rgb((color >> 8) & 0xFF, (color >> 16) & 0xFF, color & 0xFF); } break; case COMPONENT_BLUE: // We need to do a RGB to BGR swap, or vice-versa for (int i = lowerIndex; i <= upperIndex; i++) { final int color = mColors[i]; mColors[i] = Color.rgb(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF); } break; } } private boolean shouldIgnoreColor(int color) { ColorUtils.RGBtoHSL(Color.red(color), Color.green(color), Color.blue(color), mTempHsl); return shouldIgnoreColor(mTempHsl); } private static boolean shouldIgnoreColor(Swatch color) { return shouldIgnoreColor(color.getHsl()); } private static boolean shouldIgnoreColor(float[] hslColor) { return isWhite(hslColor) || isBlack(hslColor) || isNearRedILine(hslColor); } /** * @return true if the color represents a color which is close to black. */ private static boolean isBlack(float[] hslColor) { return hslColor[2] <= BLACK_MAX_LIGHTNESS; } /** * @return true if the color represents a color which is close to white. */ private static boolean isWhite(float[] hslColor) { return hslColor[2] >= WHITE_MIN_LIGHTNESS; } /** * @return true if the color lies close to the red side of the I line. */ private static boolean isNearRedILine(float[] hslColor) { return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; } /** * Comparator which sorts {@link Vbox} instances based on their volume, in descending order */ private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() { @Override public int compare(Vbox lhs, Vbox rhs) { return rhs.getVolume() - lhs.getVolume(); } }; }
然后是ColorHistogram这个类
package com.bobo.picdis.utils; /* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.util.Arrays; /** * Class which provides a histogram for RGB values. */ final class ColorHistogram { private final int[] mColors; private final int[] mColorCounts; private final int mNumberColors; /** * A new {@link ColorHistogram} instance. * * @param pixels array of image contents */ ColorHistogram(final int[] pixels) { // Sort the pixels to enable counting below Arrays.sort(pixels); // Count number of distinct colors mNumberColors = countDistinctColors(pixels); // Create arrays mColors = new int[mNumberColors]; mColorCounts = new int[mNumberColors]; // Finally count the frequency of each color countFrequencies(pixels); } /** * @return number of distinct colors in the image. */ int getNumberOfColors() { return mNumberColors; } /** * @return an array containing all of the distinct colors in the image. */ int[] getColors() { return mColors; } /** * @return an array containing the frequency of a distinct colors within the image. */ int[] getColorCounts() { return mColorCounts; } private static int countDistinctColors(final int[] pixels) { if (pixels.length < 2) { // If we have less than 2 pixels we can stop here return pixels.length; } // If we have at least 2 pixels, we have a minimum of 1 color... int colorCount = 1; int currentColor = pixels[0]; // Now iterate from the second pixel to the end, counting distinct colors for (int i = 1; i < pixels.length; i++) { // If we encounter a new color, increase the population if (pixels[i] != currentColor) { currentColor = pixels[i]; colorCount++; } } return colorCount; } private void countFrequencies(final int[] pixels) { if (pixels.length == 0) { return; } int currentColorIndex = 0; int currentColor = pixels[0]; mColors[currentColorIndex] = currentColor; mColorCounts[currentColorIndex] = 1; if (pixels.length == 1) { // If we only have one pixel, we can stop here return; } // Now iterate from the second pixel to the end, population distinct colors for (int i = 1; i < pixels.length; i++) { if (pixels[i] == currentColor) { // We've hit the same color as before, increase population mColorCounts[currentColorIndex]++; } else { // We've hit a new color, increase index currentColor = pixels[i]; currentColorIndex++; mColors[currentColorIndex] = currentColor; mColorCounts[currentColorIndex] = 1; } } } }
过后是ColorUtils这个类,这个类有些事在v4里面的包里面,需要自己去修改和剔除来。
package com.bobo.picdis.utils; /* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.graphics.Color; final class ColorUtils { private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; private static final int MIN_ALPHA_SEARCH_PRECISION = 10; private ColorUtils() {} /** * Composite two potentially translucent colors over each other and returns the result. */ private static int compositeColors(int fg, int bg) { final float alpha1 = Color.alpha(fg) / 255f; final float alpha2 = Color.alpha(bg) / 255f; float a = (alpha1 + alpha2) * (1f - alpha1); float r = (Color.red(fg) * alpha1) + (Color.red(bg) * alpha2 * (1f - alpha1)); float g = (Color.green(fg) * alpha1) + (Color.green(bg) * alpha2 * (1f - alpha1)); float b = (Color.blue(fg) * alpha1) + (Color.blue(bg) * alpha2 * (1f - alpha1)); return Color.argb((int) a, (int) r, (int) g, (int) b); } /** * Returns the luminance of a color. * * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef */ private static double calculateLuminance(int color) { double red = Color.red(color) / 255d; red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); double green = Color.green(color) / 255d; green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); double blue = Color.blue(color) / 255d; blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); } /** * Returns the contrast ratio between two colors. * * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef */ private static double calculateContrast(int foreground, int background) { if (Color.alpha(background) != 255) { throw new IllegalArgumentException("background can not be translucent"); } if (Color.alpha(foreground) < 255) { // If the foreground is translucent, composite the foreground over the background foreground = compositeColors(foreground, background); } final double luminance1 = calculateLuminance(foreground) + 0.05; final double luminance2 = calculateLuminance(background) + 0.05; // Now return the lighter luminance divided by the darker luminance return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); } /** * Finds the minimum alpha value which can be applied to {@code foreground} so that is has a * contrast value of at least {@code minContrastRatio} when compared to background. * * @return the alpha value in the range 0-255. */ private static int findMinimumAlpha(int foreground, int background, double minContrastRatio) { if (Color.alpha(background) != 255) { throw new IllegalArgumentException("background can not be translucent"); } // First lets check that a fully opaque foreground has sufficient contrast int testForeground = modifyAlpha(foreground, 255); double testRatio = calculateContrast(testForeground, background); if (testRatio < minContrastRatio) { // Fully opaque foreground does not have sufficient contrast, return error return -1; } // Binary search to find a value with the minimum value which provides sufficient contrast int numIterations = 0; int minAlpha = 0; int maxAlpha = 255; while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { final int testAlpha = (minAlpha + maxAlpha) / 2; testForeground = modifyAlpha(foreground, testAlpha); testRatio = calculateContrast(testForeground, background); if (testRatio < minContrastRatio) { minAlpha = testAlpha; } else { maxAlpha = testAlpha; } numIterations++; } // Conservatively return the max of the range of possible alphas, which is known to pass. return maxAlpha; } static int getTextColorForBackground(int backgroundColor, float minContrastRatio) { // First we will check white as most colors will be dark final int whiteMinAlpha = ColorUtils .findMinimumAlpha(Color.WHITE, backgroundColor, minContrastRatio); if (whiteMinAlpha >= 0) { return ColorUtils.modifyAlpha(Color.WHITE, whiteMinAlpha); } // If we hit here then there is not an translucent white which provides enough contrast, // so check black final int blackMinAlpha = ColorUtils .findMinimumAlpha(Color.BLACK, backgroundColor, minContrastRatio); if (blackMinAlpha >= 0) { return ColorUtils.modifyAlpha(Color.BLACK, blackMinAlpha); } // This should not happen! return -1; } static void RGBtoHSL(int r, int g, int b, float[] hsl) { final float rf = r / 255f; final float gf = g / 255f; final float bf = b / 255f; final float max = Math.max(rf, Math.max(gf, bf)); final float min = Math.min(rf, Math.min(gf, bf)); final float deltaMaxMin = max - min; float h, s; float l = (max + min) / 2f; if (max == min) { // Monochromatic h = s = 0f; } else { if (max == rf) { h = ((gf - bf) / deltaMaxMin) % 6f; } else if (max == gf) { h = ((bf - rf) / deltaMaxMin) + 2f; } else { h = ((rf - gf) / deltaMaxMin) + 4f; } s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); } hsl[0] = (h * 60f) % 360f; hsl[1] = s; hsl[2] = l; } static int HSLtoRGB (float[] hsl) { final float h = hsl[0]; final float s = hsl[1]; final float l = hsl[2]; final float c = (1f - Math.abs(2 * l - 1f)) * s; final float m = l - 0.5f * c; final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); final int hueSegment = (int) h / 60; int r = 0, g = 0, b = 0; switch (hueSegment) { case 0: r = Math.round(255 * (c + m)); g = Math.round(255 * (x + m)); b = Math.round(255 * m); break; case 1: r = Math.round(255 * (x + m)); g = Math.round(255 * (c + m)); b = Math.round(255 * m); break; case 2: r = Math.round(255 * m); g = Math.round(255 * (c + m)); b = Math.round(255 * (x + m)); break; case 3: r = Math.round(255 * m); g = Math.round(255 * (x + m)); b = Math.round(255 * (c + m)); break; case 4: r = Math.round(255 * (x + m)); g = Math.round(255 * m); b = Math.round(255 * (c + m)); break; case 5: case 6: r = Math.round(255 * (c + m)); g = Math.round(255 * m); b = Math.round(255 * (x + m)); break; } r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); return Color.rgb(r, g, b); } /** * Set the alpha component of {@code color} to be {@code alpha}. */ static int modifyAlpha(int color, int alpha) { return (color & 0x00ffffff) | (alpha << 24); } }
最后就是我们的重要部分了,对外接口的类Palette
package com.bobo.picdis.utils; /* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.graphics.Bitmap; import android.graphics.Color; import android.os.AsyncTask; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * A helper class to extract prominent colors from an image. * <p> * A number of colors with different profiles are extracted from the image: * <ul> * <li>Vibrant</li> * <li>Vibrant Dark</li> * <li>Vibrant Light</li> * <li>Muted</li> * <li>Muted Dark</li> * <li>Muted Light</li> * </ul> * These can be retrieved from the appropriate getter method. * * <p> * Instances can be created with the synchronous factory methods {@link #generate(Bitmap)} and * {@link #generate(Bitmap, int)}. * <p> * These should be called on a background thread, ideally the one in * which you load your images on. Sometimes that is not possible, so asynchronous factory methods * have also been provided: {@link #generateAsync(Bitmap, PaletteAsyncListener)} and * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}. These can be used as so: * * <pre> * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { * public void onGenerated(Palette palette) { * // Do something with colors... * } * }); * </pre> */ public final class Palette { /** * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)} */ public interface PaletteAsyncListener { /** * Called when the {@link Palette} has been generated. */ void onGenerated(Palette palette); } private static final int CALCULATE_BITMAP_MIN_DIMENSION = 100; private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; private static final float TARGET_DARK_LUMA = 0.26f; private static final float MAX_DARK_LUMA = 0.45f; private static final float MIN_LIGHT_LUMA = 0.55f; private static final float TARGET_LIGHT_LUMA = 0.74f; private static final float MIN_NORMAL_LUMA = 0.3f; private static final float TARGET_NORMAL_LUMA = 0.5f; private static final float MAX_NORMAL_LUMA = 0.7f; private static final float TARGET_MUTED_SATURATION = 0.3f; private static final float MAX_MUTED_SATURATION = 0.4f; private static final float TARGET_VIBRANT_SATURATION = 1f; private static final float MIN_VIBRANT_SATURATION = 0.35f; private static final float WEIGHT_SATURATION = 3f; private static final float WEIGHT_LUMA = 6f; private static final float WEIGHT_POPULATION = 1f; private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; private static final float MIN_CONTRAST_BODY_TEXT = 4.5f; private final List<Swatch> mSwatches; private final int mHighestPopulation; private Swatch mVibrantSwatch; private Swatch mMutedSwatch; private Swatch mDarkVibrantSwatch; private Swatch mDarkMutedSwatch; private Swatch mLightVibrantSwatch; private Swatch mLightMutedColor; /** * Generate a {@link Palette} from a {@link Bitmap} using the default number of colors. */ public static Palette generate(Bitmap bitmap) { return generate(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS); } /** * Generate a {@link Palette} from a {@link Bitmap} using the specified {@code numColors}. * Good values for {@code numColors} depend on the source image type. * For landscapes, a good values are in the range 12-16. For images which are largely made up * of people's faces then this value should be increased to 24-32. * * @param numColors The maximum number of colors in the generated palette. Increasing this * number will increase the time needed to compute the values. */ public static Palette generate(Bitmap bitmap, int numColors) { checkBitmapParam(bitmap); checkNumberColorsParam(numColors); // First we'll scale down the bitmap so it's shortest dimension is 100px final Bitmap scaledBitmap = scaleBitmapDown(bitmap); // Now generate a quantizer from the Bitmap ColorCutQuantizer quantizer = ColorCutQuantizer.fromBitmap(scaledBitmap, numColors); // If created a new bitmap, recycle it if (scaledBitmap != bitmap) { scaledBitmap.recycle(); } // Now return a ColorExtractor instance return new Palette(quantizer.getQuantizedColors()); } /** * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)} * will be called with the created instance. The resulting {@link Palette} is the same as * what would be created by calling {@link #generate(Bitmap)}. * * @param listener Listener to be invoked when the {@link Palette} has been generated. * * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance. */ public static AsyncTask<Bitmap, Void, Palette> generateAsync( Bitmap bitmap, PaletteAsyncListener listener) { return generateAsync(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS, listener); } /** * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)} * will be called with the created instance. The resulting {@link Palette} is the same as what * would be created by calling {@link #generate(Bitmap, int)}. * * @param listener Listener to be invoked when the {@link Palette} has been generated. * * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance. */ public static AsyncTask<Bitmap, Void, Palette> generateAsync( final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) { checkBitmapParam(bitmap); checkNumberColorsParam(numColors); checkAsyncListenerParam(listener); AsyncTask<Bitmap, Void, Palette> task = new AsyncTask<Bitmap, Void, Palette>() { @Override protected Palette doInBackground(Bitmap... params) { return generate(params[0], numColors); } @Override protected void onPostExecute(Palette colorExtractor) { super.onPostExecute(colorExtractor); listener.onGenerated(colorExtractor); } }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap); // return AsyncTaskCompat.executeParallel( // new AsyncTask<Bitmap, Void, Palette>() { // @Override // protected Palette doInBackground(Bitmap... params) { // return generate(params[0], numColors); // } // // @Override // protected void onPostExecute(Palette colorExtractor) { // listener.onGenerated(colorExtractor); // } // }, bitmap); return task; } private Palette(List<Swatch> swatches) { mSwatches = swatches; mHighestPopulation = findMaxPopulation(); mVibrantSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); mLightVibrantSwatch = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); mDarkVibrantSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); mMutedSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); mLightMutedColor = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f, TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); mDarkMutedSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA, TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); // Now try and generate any missing colors generateEmptySwatches(); } /** * Returns all of the swatches which make up the palette. */ public List<Swatch> getSwatches() { return Collections.unmodifiableList(mSwatches); } /** * Returns the most vibrant swatch in the palette. Might be null. */ public Swatch getVibrantSwatch() { return mVibrantSwatch; } /** * Returns a light and vibrant swatch from the palette. Might be null. */ public Swatch getLightVibrantSwatch() { return mLightVibrantSwatch; } /** * Returns a dark and vibrant swatch from the palette. Might be null. */ public Swatch getDarkVibrantSwatch() { return mDarkVibrantSwatch; } /** * Returns a muted swatch from the palette. Might be null. */ public Swatch getMutedSwatch() { return mMutedSwatch; } /** * Returns a muted and light swatch from the palette. Might be null. */ public Swatch getLightMutedSwatch() { return mLightMutedColor; } /** * Returns a muted and dark swatch from the palette. Might be null. */ public Swatch getDarkMutedSwatch() { return mDarkMutedSwatch; } /** * Returns the most vibrant color in the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getVibrantColor(int defaultColor) { return mVibrantSwatch != null ? mVibrantSwatch.getRgb() : defaultColor; } /** * Returns a light and vibrant color from the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getLightVibrantColor(int defaultColor) { return mLightVibrantSwatch != null ? mLightVibrantSwatch.getRgb() : defaultColor; } /** * Returns a dark and vibrant color from the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getDarkVibrantColor(int defaultColor) { return mDarkVibrantSwatch != null ? mDarkVibrantSwatch.getRgb() : defaultColor; } /** * Returns a muted color from the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getMutedColor(int defaultColor) { return mMutedSwatch != null ? mMutedSwatch.getRgb() : defaultColor; } /** * Returns a muted and light color from the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getLightMutedColor(int defaultColor) { return mLightMutedColor != null ? mLightMutedColor.getRgb() : defaultColor; } /** * Returns a muted and dark color from the palette as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available */ public int getDarkMutedColor(int defaultColor) { return mDarkMutedSwatch != null ? mDarkMutedSwatch.getRgb() : defaultColor; } /** * @return true if we have already selected {@code swatch} */ private boolean isAlreadySelected(Swatch swatch) { return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch || mLightVibrantSwatch == swatch || mMutedSwatch == swatch || mDarkMutedSwatch == swatch || mLightMutedColor == swatch; } private Swatch findColor(float targetLuma, float minLuma, float maxLuma, float targetSaturation, float minSaturation, float maxSaturation) { Swatch max = null; float maxValue = 0f; for (Swatch swatch : mSwatches) { final float sat = swatch.getHsl()[1]; final float luma = swatch.getHsl()[2]; if (sat >= minSaturation && sat <= maxSaturation && luma >= minLuma && luma <= maxLuma && !isAlreadySelected(swatch)) { float thisValue = createComparisonValue(sat, targetSaturation, luma, targetLuma, swatch.getPopulation(), mHighestPopulation); if (max == null || thisValue > maxValue) { max = swatch; maxValue = thisValue; } } } return max; } /** * Try and generate any missing swatches from the swatches we did find. */ private void generateEmptySwatches() { if (mVibrantSwatch == null) { // If we do not have a vibrant color... if (mDarkVibrantSwatch != null) { // ...but we do have a dark vibrant, generate the value by modifying the luma final float[] newHsl = copyHslValues(mDarkVibrantSwatch); newHsl[2] = TARGET_NORMAL_LUMA; mVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0); } } if (mDarkVibrantSwatch == null) { // If we do not have a dark vibrant color... if (mVibrantSwatch != null) { // ...but we do have a vibrant, generate the value by modifying the luma final float[] newHsl = copyHslValues(mVibrantSwatch); newHsl[2] = TARGET_DARK_LUMA; mDarkVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0); } } } /** * Find the {@link Swatch} with the highest population value and return the population. */ private int findMaxPopulation() { int population = 0; for (Swatch swatch : mSwatches) { population = Math.max(population, swatch.getPopulation()); } return population; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Palette palette = (Palette) o; if (mSwatches != null ? !mSwatches.equals(palette.mSwatches) : palette.mSwatches != null) { return false; } if (mDarkMutedSwatch != null ? !mDarkMutedSwatch.equals(palette.mDarkMutedSwatch) : palette.mDarkMutedSwatch != null) { return false; } if (mDarkVibrantSwatch != null ? !mDarkVibrantSwatch.equals(palette.mDarkVibrantSwatch) : palette.mDarkVibrantSwatch != null) { return false; } if (mLightMutedColor != null ? !mLightMutedColor.equals(palette.mLightMutedColor) : palette.mLightMutedColor != null) { return false; } if (mLightVibrantSwatch != null ? !mLightVibrantSwatch.equals(palette.mLightVibrantSwatch) : palette.mLightVibrantSwatch != null) { return false; } if (mMutedSwatch != null ? !mMutedSwatch.equals(palette.mMutedSwatch) : palette.mMutedSwatch != null) { return false; } if (mVibrantSwatch != null ? !mVibrantSwatch.equals(palette.mVibrantSwatch) : palette.mVibrantSwatch != null) { return false; } return true; } @Override public int hashCode() { int result = mSwatches != null ? mSwatches.hashCode() : 0; result = 31 * result + (mVibrantSwatch != null ? mVibrantSwatch.hashCode() : 0); result = 31 * result + (mMutedSwatch != null ? mMutedSwatch.hashCode() : 0); result = 31 * result + (mDarkVibrantSwatch != null ? mDarkVibrantSwatch.hashCode() : 0); result = 31 * result + (mDarkMutedSwatch != null ? mDarkMutedSwatch.hashCode() : 0); result = 31 * result + (mLightVibrantSwatch != null ? mLightVibrantSwatch.hashCode() : 0); result = 31 * result + (mLightMutedColor != null ? mLightMutedColor.hashCode() : 0); return result; } /** * Scale the bitmap down so that it's smallest dimension is * {@value #CALCULATE_BITMAP_MIN_DIMENSION}px. If {@code bitmap} is smaller than this, than it * is returned. */ private static Bitmap scaleBitmapDown(Bitmap bitmap) { final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight()); if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) { // If the bitmap is small enough already, just return it return bitmap; } final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension; return Bitmap.createScaledBitmap(bitmap, Math.round(bitmap.getWidth() * scaleRatio), Math.round(bitmap.getHeight() * scaleRatio), false); } private static float createComparisonValue(float saturation, float targetSaturation, float luma, float targetLuma, int population, int highestPopulation) { return weightedMean( invertDiff(saturation, targetSaturation), WEIGHT_SATURATION, invertDiff(luma, targetLuma), WEIGHT_LUMA, population / (float) highestPopulation, WEIGHT_POPULATION ); } /** * Copy a {@link Swatch}'s HSL values into a new float[]. */ private static float[] copyHslValues(Swatch color) { final float[] newHsl = new float[3]; System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); return newHsl; } /** * Returns a value in the range 0-1. 1 is returned when {@code value} equals the * {@code targetValue} and then decreases as the absolute difference between {@code value} and * {@code targetValue} increases. * * @param value the item's value * @param targetValue the value which we desire */ private static float invertDiff(float value, float targetValue) { return 1f - Math.abs(value - targetValue); } private static float weightedMean(float... values) { float sum = 0f; float sumWeight = 0f; for (int i = 0; i < values.length; i += 2) { float value = values[i]; float weight = values[i + 1]; sum += (value * weight); sumWeight += weight; } return sum / sumWeight; } private static void checkBitmapParam(Bitmap bitmap) { if (bitmap == null) { throw new IllegalArgumentException("bitmap can not be null"); } if (bitmap.isRecycled()) { throw new IllegalArgumentException("bitmap can not be recycled"); } } private static void checkNumberColorsParam(int numColors) { if (numColors < 1) { throw new IllegalArgumentException("numColors must be 1 of greater"); } } private static void checkAsyncListenerParam(PaletteAsyncListener listener) { if (listener == null) { throw new IllegalArgumentException("listener can not be null"); } } /** * Represents a color swatch generated from an image's palette. The RGB color can be retrieved * by calling {@link #getRgb()}. */ public static final class Swatch { private final int mRed, mGreen, mBlue; private final int mRgb; private final int mPopulation; private boolean mGeneratedTextColors; private int mTitleTextColor; private int mBodyTextColor; private float[] mHsl; Swatch(int rgbColor, int population) { mRed = Color.red(rgbColor); mGreen = Color.green(rgbColor); mBlue = Color.blue(rgbColor); mRgb = rgbColor; mPopulation = population; } Swatch(int red, int green, int blue, int population) { mRed = red; mGreen = green; mBlue = blue; mRgb = Color.rgb(red, green, blue); mPopulation = population; } /** * @return this swatch's RGB color value */ public int getRgb() { return mRgb; } /** * Return this swatch's HSL values. * hsv[0] is Hue [0 .. 360) * hsv[1] is Saturation [0...1] * hsv[2] is Lightness [0...1] */ public float[] getHsl() { if (mHsl == null) { // Lazily generate HSL values from RGB mHsl = new float[3]; ColorUtils.RGBtoHSL(mRed, mGreen, mBlue, mHsl); } return mHsl; } /** * @return the number of pixels represented by this swatch */ public int getPopulation() { return mPopulation; } /** * Returns an appropriate color to use for any 'title' text which is displayed over this * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. */ public int getTitleTextColor() { ensureTextColorsGenerated(); return mTitleTextColor; } /** * Returns an appropriate color to use for any 'body' text which is displayed over this * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. */ public int getBodyTextColor() { ensureTextColorsGenerated(); return mBodyTextColor; } private void ensureTextColorsGenerated() { if (!mGeneratedTextColors) { mTitleTextColor = ColorUtils.getTextColorForBackground(mRgb, MIN_CONTRAST_TITLE_TEXT); mBodyTextColor = ColorUtils.getTextColorForBackground(mRgb, MIN_CONTRAST_BODY_TEXT); mGeneratedTextColors = true; } } @Override public String toString() { return new StringBuilder(getClass().getSimpleName()) .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') .append(" [Population: ").append(mPopulation).append(']') .append(" [Title Text: #").append(Integer.toHexString(mTitleTextColor)).append(']') .append(" [Body Text: #").append(Integer.toHexString(mBodyTextColor)).append(']') .toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Swatch swatch = (Swatch) o; return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; } @Override public int hashCode() { return 31 * mRgb + mPopulation; } } }
讲解
需要注意的我修改的一个比较重要的地方,Palette里面的generateAsync方法,它返回的这一段代码return AsyncTaskCompat.executeParallel( // new AsyncTask<Bitmap, Void, Palette>() { // @Override // protected Palette doInBackground(Bitmap... params) { // return generate(params[0], numColors); // } // // @Override // protected void onPostExecute(Palette colorExtractor) { // listener.onGenerated(colorExtractor); // } // }, bitmap);
被我修改成了
AsyncTask<Bitmap, Void, Palette> task = new AsyncTask<Bitmap, Void, Palette>() { @Override protected Palette doInBackground(Bitmap... params) { return generate(params[0], numColors); } @Override protected void onPostExecute(Palette colorExtractor) { super.onPostExecute(colorExtractor); listener.onGenerated(colorExtractor); } }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap);
这个貌似不兼容api11以下,11以下可以直接调用task.execute方法,然后传入参数就可以了,我也是看了源码修改的,因为我的需求里面不需要兼容11的api,下面就看看使用的效果吧。上我们的Activity的代码。
ublic class MainActivity extends Activity implements Palette.PaletteAsyncListener { LinearLayout mRoot; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRoot = new LinearLayout(this); mRoot.setOrientation(LinearLayout.VERTICAL); setContentView(mRoot); //导入一个图片 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.examlist_naeti); Palette palette = Palette.generate(bitmap); //获取色调DarkMuted颜色 没有获取到这个模式下的颜色默认给一个red的红色 // int darkMultedColor = palette.getDarkMutedColor(Color.RED); // View view = new View(this); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 100); params.bottomMargin = 5; // view.setBackgroundColor(darkMultedColor); // mRoot.addView(view, params); // 获取色调DarkMuted颜色 // int darkcolorExtractor = palette.getDarkVibrantColor(Color.RED); // view = new View(this); // view.setLayoutParams(params); // view.setBackgroundColor(darkcolorExtractor); // mRoot.addView(view, params); // //获取色调DarkMuted颜色 // int lightMuteColor = palette.getLightMutedColor(Color.RED); // view = new View(this); // view.setLayoutParams(params); // view.setBackgroundColor(lightMuteColor); // mRoot.addView(view, params); //获取所有的图片分析结果 List<Palette.Swatch> lists = palette.getSwatches(); for (Palette.Swatch swatch : lists) { int color = swatch.getRgb(); View view = new View(this); view.setLayoutParams(params); view.setBackgroundColor(color); mRoot.addView(view, params); } //导入第二张图片异步去分析它 Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.examlist_cmep); //需要注意的是颜色的个数必须要大于1而且lisenler也不能为空。 AsyncTask<Bitmap, Void, Palette> palette1 = Palette.generateAsync(bitmap1, 3, this); } @Override public void onGenerated(Palette palette) { int darkMultedColor = palette.getDarkMutedColor(Color.RED); View view = new View(this); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 100); view.setBackgroundColor(darkMultedColor); mRoot.addView(view, params); int darkcolorExtractor = palette.getDarkVibrantColor(Color.RED); view = new View(this); view.setLayoutParams(params); view.setBackgroundColor(darkcolorExtractor); mRoot.addView(view, params); int lightMuteColor = palette.getLightMutedColor(Color.RED); view = new View(this); view.setLayoutParams(params); view.setBackgroundColor(lightMuteColor); mRoot.addView(view, params); } }
用法跟google提供的调色板palette是一样的,其中很多色调的颜色可以根据自己的需要去提取,也可以提取所有的颜色,这样提取到了当前图片的色调颜色,就可以很容易的匹配和选择我们需要的颜色了。源码地址:传送门
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories