/*
* Copyright 2013 Michael Evans <michaelcevans10@gmail.com>
*
* 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.
*/
package org.michaelevans.colorart.library;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
public class ColorArt {
private final double COLOR_THRESHOLD_MINIMUM_PERCENTAGE = 0.01;
private final double EDGE_COLOR_DISCARD_THRESHOLD = 0.3;
private final float MINIMUM_SATURATION_THRESHOLD = 0.15f;
private static final String LOG_TAG = ColorArt.class.getSimpleName();
private final Bitmap mBitmap;
private HashBag<Integer> mImageColors;
private int mBackgroundColor;
private Integer mPrimaryColor = null;
private Integer mSecondaryColor = null;
private Integer mDetailColor = null;
public ColorArt(Bitmap bitmap) {
mBitmap = Bitmap.createScaledBitmap(bitmap, 120, 120, false);
analyzeImage();
}
private void analyzeImage() {
mBackgroundColor = findEdgeColor();
findTextColors(mImageColors);
boolean hasDarkBackground = isDarkColor(mBackgroundColor);
if (mPrimaryColor == null) {
Log.d(LOG_TAG, "Unable to detect primary color in image");
if (hasDarkBackground) {
mPrimaryColor = Color.WHITE;
} else {
mPrimaryColor = Color.BLACK;
}
}
if (mSecondaryColor == null) {
Log.d(LOG_TAG, "Unable to detect secondary in image");
if (hasDarkBackground) {
mSecondaryColor = Color.WHITE;
} else {
mSecondaryColor = Color.BLACK;
}
}
if (mDetailColor == null) {
Log.d(LOG_TAG, "Unable to detect detail color in image");
if (hasDarkBackground) {
mDetailColor = Color.WHITE;
} else {
mDetailColor = Color.BLACK;
}
}
}
private int findEdgeColor() {
int height = mBitmap.getHeight();
int width = mBitmap.getWidth();
mImageColors = new HashBag<Integer>();
HashBag<Integer> leftImageColors = new HashBag<Integer>();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (x == 0) {
leftImageColors.add(mBitmap.getPixel(x, y));
}
mImageColors.add(mBitmap.getPixel(x, y));
}
}
ArrayList<CountedColor> sortedColors = new ArrayList<CountedColor>();
int randomColorThreshold = (int) (height * COLOR_THRESHOLD_MINIMUM_PERCENTAGE);
Iterator<Integer> iterator = leftImageColors.iterator();
while (iterator.hasNext()) {
Integer color = iterator.next();
int colorCount = leftImageColors.getCount(color);
if (colorCount < randomColorThreshold) {
continue;
}
CountedColor container = new CountedColor(color, colorCount);
sortedColors.add(container);
}
Collections.sort(sortedColors);
Iterator<CountedColor> sortedColorIterator = sortedColors.iterator();
if (!sortedColorIterator.hasNext()) {
return Color.BLACK;
}
CountedColor proposedEdgeColor = sortedColorIterator.next();
if (!proposedEdgeColor.isBlackOrWhite()) {
return proposedEdgeColor.getColor();
}
while (sortedColorIterator.hasNext()) {
CountedColor nextProposedColor = sortedColorIterator.next();
double edgeColorRatio = (double) nextProposedColor.getCount() / proposedEdgeColor.getCount();
if (edgeColorRatio <= EDGE_COLOR_DISCARD_THRESHOLD) {
break;
}
if (!nextProposedColor.isBlackOrWhite()) {
proposedEdgeColor = nextProposedColor;
break;
}
}
return proposedEdgeColor.getColor();
}
private void findTextColors(HashBag<Integer> colors) {
Iterator<Integer> iterator = colors.iterator();
int currentColor;
ArrayList<CountedColor> sortedColors = new ArrayList<CountedColor>();
boolean findDarkTextColor = !isDarkColor(mBackgroundColor);
while (iterator.hasNext()) {
currentColor = iterator.next();
currentColor = colorWithMinimumSaturation(currentColor, MINIMUM_SATURATION_THRESHOLD);
if (isDarkColor(currentColor) == findDarkTextColor) {
int colorCount = colors.getCount(currentColor);
CountedColor container = new CountedColor(currentColor, colorCount);
sortedColors.add(container);
}
}
Collections.sort(sortedColors);
for (CountedColor currentContainer : sortedColors) {
currentColor = currentContainer.getColor();
if (mPrimaryColor == null) {
if (isContrastingColor(currentColor, mBackgroundColor)) {
mPrimaryColor = currentColor;
}
} else if (mSecondaryColor == null) {
if (!isDistinctColor(mPrimaryColor, currentColor) ||
!isContrastingColor(currentColor, mBackgroundColor)) {
continue;
}
mSecondaryColor = currentColor;
} else if (mDetailColor == null) {
if (!isDistinctColor(mSecondaryColor, currentColor) ||
!isDistinctColor(mPrimaryColor, currentColor) ||
!isContrastingColor(currentColor, mBackgroundColor)) {
continue;
}
mDetailColor = currentColor;
break;
}
}
}
public int getBackgroundColor() {
return mBackgroundColor;
}
public int getPrimaryColor() {
return mPrimaryColor;
}
public int getSecondaryColor() {
return mSecondaryColor;
}
public int getDetailColor() {
return mDetailColor;
}
//helpers
private int colorWithMinimumSaturation(int color, float minSaturation) {
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
if (hsv[1] < minSaturation) {
return Color.HSVToColor(new float[]{hsv[0], minSaturation, hsv[2]});
}
return color;
}
private boolean isDarkColor(int color) {
double r = (double) Color.red(color) / 255;
double g = (double) Color.green(color) / 255;
double b = (double) Color.blue(color) / 255;
double lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return lum < 0.5;
}
private boolean isContrastingColor(int backgroundColor, int foregroundColor) {
double br = (double) Color.red(backgroundColor) / 255;
double bg = (double) Color.green(backgroundColor) / 255;
double bb = (double) Color.blue(backgroundColor) / 255;
double fr = (double) Color.red(foregroundColor) / 255;
double fg = (double) Color.green(foregroundColor) / 255;
double fb = (double) Color.blue(foregroundColor) / 255;
double bLum = 0.2126 * br + 0.7152 * bg + 0.0722 * bb;
double fLum = 0.2126 * fr + 0.7152 * fg + 0.0722 * fb;
double contrast;
if (bLum > fLum) {
contrast = (bLum + 0.05) / (fLum + 0.05);
} else {
contrast = (fLum + 0.05) / (bLum + 0.05);
}
return contrast > 1.6;
}
private boolean isDistinctColor(int colorA, int colorB) {
double r = (double) Color.red(colorA) / 255;
double g = (double) Color.green(colorA) / 255;
double b = (double) Color.blue(colorA) / 255;
double a = (double) Color.alpha(colorA) / 255;
double r1 = (double) Color.red(colorB) / 255;
double g1 = (double) Color.green(colorB) / 255;
double b1 = (double) Color.blue(colorB) / 255;
double a1 = (double) Color.alpha(colorB) / 255;
double threshold = .25; //.15
if (Math.abs(r - r1) > threshold ||
Math.abs(g - g1) > threshold ||
Math.abs(b - b1) > threshold ||
Math.abs(a - a1) > threshold) {
// check for grays, prevent multiple gray colors
if (Math.abs(r - g) < .03 && Math.abs(r - b) < .03 &&
(Math.abs(r1 - g1) < .03 && Math.abs(r1 - b1) < .03)) {
return false;
}
return true;
}
return false;
}
private class CountedColor implements Comparable<CountedColor> {
private final int mColor;
private final int mCount;
public CountedColor(int color, int count) {
mColor = color;
mCount = count;
}
@Override
public int compareTo(CountedColor another) {
return getCount() < another.getCount() ? -1 : (getCount() == another.getCount() ? 0 : 1);
}
public boolean isBlackOrWhite() {
double r = (double) Color.red(mColor) / 255;
double g = (double) Color.green(mColor) / 255;
double b = (double) Color.blue(mColor) / 255;
if ((r > .91 && g > .91 && b > .91) || (r < .09 && g < .09 && b < .09)) // color is white or black
return true;
return false;
}
public int getCount() {
return mCount;
}
public int getColor() {
return mColor;
}
}
}