1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.glwallpaper; 18 19 import android.graphics.Bitmap; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.ColorMatrix; 23 import android.graphics.ColorMatrixColorFilter; 24 import android.graphics.Matrix; 25 import android.graphics.Paint; 26 import android.os.AsyncTask; 27 import android.os.Handler; 28 import android.os.Handler.Callback; 29 import android.os.Message; 30 import android.util.Log; 31 32 /** 33 * A helper class that computes threshold from a bitmap. 34 * Threshold will be computed each time the user picks a new image wallpaper. 35 */ 36 class ImageProcessHelper { 37 private static final String TAG = ImageProcessHelper.class.getSimpleName(); 38 private static final float DEFAULT_THRESHOLD = 0.8f; 39 private static final float DEFAULT_OTSU_THRESHOLD = 0f; 40 private static final float MAX_THRESHOLD = 0.89f; 41 private static final int MSG_UPDATE_THRESHOLD = 1; 42 43 /** 44 * This color matrix will be applied to each pixel to get luminance from rgb by below formula: 45 * Luminance = .2126f * r + .7152f * g + .0722f * b. 46 */ 47 private static final float[] LUMINOSITY_MATRIX = new float[] { 48 .2126f, .0000f, .0000f, .0000f, .0000f, 49 .0000f, .7152f, .0000f, .0000f, .0000f, 50 .0000f, .0000f, .0722f, .0000f, .0000f, 51 .0000f, .0000f, .0000f, 1.000f, .0000f 52 }; 53 54 private final Handler mHandler = new Handler(new Callback() { 55 @Override 56 public boolean handleMessage(Message msg) { 57 switch (msg.what) { 58 case MSG_UPDATE_THRESHOLD: 59 mThreshold = (float) msg.obj; 60 return true; 61 default: 62 return false; 63 } 64 } 65 }); 66 67 private float mThreshold = DEFAULT_THRESHOLD; 68 69 void start(Bitmap bitmap) { 70 new ThresholdComputeTask(mHandler).execute(bitmap); 71 } 72 73 float getThreshold() { 74 return Math.min(mThreshold, MAX_THRESHOLD); 75 } 76 77 private static class ThresholdComputeTask extends AsyncTask<Bitmap, Void, Float> { 78 private Handler mUpdateHandler; 79 80 ThresholdComputeTask(Handler handler) { 81 super(handler); 82 mUpdateHandler = handler; 83 } 84 85 @Override 86 protected Float doInBackground(Bitmap... bitmaps) { 87 Bitmap bitmap = bitmaps[0]; 88 if (bitmap != null) { 89 return new Threshold().compute(bitmap); 90 } 91 Log.e(TAG, "ThresholdComputeTask: Can't get bitmap"); 92 return DEFAULT_THRESHOLD; 93 } 94 95 @Override 96 protected void onPostExecute(Float result) { 97 Message msg = mUpdateHandler.obtainMessage(MSG_UPDATE_THRESHOLD, result); 98 mUpdateHandler.sendMessage(msg); 99 } 100 } 101 102 private static class Threshold { 103 public float compute(Bitmap bitmap) { 104 Bitmap grayscale = toGrayscale(bitmap); 105 int[] histogram = getHistogram(grayscale); 106 boolean isSolidColor = isSolidColor(grayscale, histogram); 107 108 // We will see gray wallpaper during the transition if solid color wallpaper is set, 109 // please refer to b/130360362#comment16. 110 // As a result, we use Percentile85 rather than Otsus if a solid color wallpaper is set. 111 ThresholdAlgorithm algorithm = isSolidColor ? new Percentile85() : new Otsus(); 112 return algorithm.compute(grayscale, histogram); 113 } 114 115 private Bitmap toGrayscale(Bitmap bitmap) { 116 int width = bitmap.getWidth(); 117 int height = bitmap.getHeight(); 118 119 Bitmap grayscale = Bitmap.createBitmap(width, height, bitmap.getConfig()); 120 Canvas canvas = new Canvas(grayscale); 121 ColorMatrix cm = new ColorMatrix(LUMINOSITY_MATRIX); 122 Paint paint = new Paint(); 123 paint.setColorFilter(new ColorMatrixColorFilter(cm)); 124 canvas.drawBitmap(bitmap, new Matrix(), paint); 125 126 return grayscale; 127 } 128 129 private int[] getHistogram(Bitmap grayscale) { 130 int width = grayscale.getWidth(); 131 int height = grayscale.getHeight(); 132 133 // TODO: Fine tune the performance here, tracking on b/123615079. 134 int[] histogram = new int[256]; 135 for (int row = 0; row < height; row++) { 136 for (int col = 0; col < width; col++) { 137 int pixel = grayscale.getPixel(col, row); 138 int y = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel); 139 histogram[y]++; 140 } 141 } 142 143 return histogram; 144 } 145 146 private boolean isSolidColor(Bitmap bitmap, int[] histogram) { 147 boolean solidColor = false; 148 int pixels = bitmap.getWidth() * bitmap.getHeight(); 149 150 // In solid color case, only one element of histogram has value, 151 // which is pixel counts and the value of other elements should be 0. 152 for (int value : histogram) { 153 if (value != 0 && value != pixels) { 154 break; 155 } 156 if (value == pixels) { 157 solidColor = true; 158 break; 159 } 160 } 161 return solidColor; 162 } 163 } 164 165 private static class Percentile85 implements ThresholdAlgorithm { 166 @Override 167 public float compute(Bitmap bitmap, int[] histogram) { 168 float per85 = DEFAULT_THRESHOLD; 169 int pixelCount = bitmap.getWidth() * bitmap.getHeight(); 170 float[] acc = new float[256]; 171 for (int i = 0; i < acc.length; i++) { 172 acc[i] = (float) histogram[i] / pixelCount; 173 float prev = i == 0 ? 0f : acc[i - 1]; 174 float next = acc[i]; 175 float idx = (float) (i + 1) / 255; 176 float sum = prev + next; 177 if (prev < 0.85f && sum >= 0.85f) { 178 per85 = idx; 179 } 180 if (i > 0) { 181 acc[i] += acc[i - 1]; 182 } 183 } 184 return per85; 185 } 186 } 187 188 private static class Otsus implements ThresholdAlgorithm { 189 @Override 190 public float compute(Bitmap bitmap, int[] histogram) { 191 float threshold = DEFAULT_OTSU_THRESHOLD; 192 float maxVariance = 0; 193 float pixelCount = bitmap.getWidth() * bitmap.getHeight(); 194 float[] w = new float[2]; 195 float[] m = new float[2]; 196 float[] u = new float[2]; 197 198 for (int i = 0; i < histogram.length; i++) { 199 m[1] += i * histogram[i]; 200 } 201 202 w[1] = pixelCount; 203 for (int tonalValue = 0; tonalValue < histogram.length; tonalValue++) { 204 float dU; 205 float variance; 206 float numPixels = histogram[tonalValue]; 207 float tmp = numPixels * tonalValue; 208 w[0] += numPixels; 209 w[1] -= numPixels; 210 211 if (w[0] == 0 || w[1] == 0) { 212 continue; 213 } 214 215 m[0] += tmp; 216 m[1] -= tmp; 217 u[0] = m[0] / w[0]; 218 u[1] = m[1] / w[1]; 219 dU = u[0] - u[1]; 220 variance = w[0] * w[1] * dU * dU; 221 222 if (variance > maxVariance) { 223 threshold = (tonalValue + 1f) / histogram.length; 224 maxVariance = variance; 225 } 226 } 227 return threshold; 228 } 229 } 230 231 private interface ThresholdAlgorithm { 232 float compute(Bitmap bitmap, int[] histogram); 233 } 234 } 235