blob: 703d5910500a553709ccf42aff1c8bcdd958626d [file] [log] [blame]
Ahan Wu67e7f102019-01-14 20:38:14 +08001/*
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
17package com.android.systemui.glwallpaper;
18
Ahan Wu3222d3f2020-03-11 20:24:00 +080019import static com.android.systemui.glwallpaper.ImageWallpaperRenderer.WallpaperTexture;
20
Ahan Wu67e7f102019-01-14 20:38:14 +080021import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.ColorMatrix;
25import android.graphics.ColorMatrixColorFilter;
26import android.graphics.Matrix;
27import android.graphics.Paint;
28import android.os.AsyncTask;
29import android.os.Handler;
30import android.os.Handler.Callback;
31import android.os.Message;
32import android.util.Log;
33
34/**
Ahan Wu330bc602019-04-25 15:04:39 +080035 * A helper class that computes threshold from a bitmap.
36 * Threshold will be computed each time the user picks a new image wallpaper.
Ahan Wu67e7f102019-01-14 20:38:14 +080037 */
38class ImageProcessHelper {
39 private static final String TAG = ImageProcessHelper.class.getSimpleName();
Ahan Wu330bc602019-04-25 15:04:39 +080040 private static final float DEFAULT_THRESHOLD = 0.8f;
41 private static final float DEFAULT_OTSU_THRESHOLD = 0f;
42 private static final float MAX_THRESHOLD = 0.89f;
43 private static final int MSG_UPDATE_THRESHOLD = 1;
Ahan Wu67e7f102019-01-14 20:38:14 +080044
45 /**
46 * This color matrix will be applied to each pixel to get luminance from rgb by below formula:
47 * Luminance = .2126f * r + .7152f * g + .0722f * b.
48 */
49 private static final float[] LUMINOSITY_MATRIX = new float[] {
50 .2126f, .0000f, .0000f, .0000f, .0000f,
51 .0000f, .7152f, .0000f, .0000f, .0000f,
52 .0000f, .0000f, .0722f, .0000f, .0000f,
53 .0000f, .0000f, .0000f, 1.000f, .0000f
54 };
55
56 private final Handler mHandler = new Handler(new Callback() {
57 @Override
58 public boolean handleMessage(Message msg) {
59 switch (msg.what) {
Ahan Wu330bc602019-04-25 15:04:39 +080060 case MSG_UPDATE_THRESHOLD:
61 mThreshold = (float) msg.obj;
Ahan Wu67e7f102019-01-14 20:38:14 +080062 return true;
63 default:
64 return false;
65 }
66 }
67 });
68
Ahan Wu330bc602019-04-25 15:04:39 +080069 private float mThreshold = DEFAULT_THRESHOLD;
Ahan Wu67e7f102019-01-14 20:38:14 +080070
Ahan Wu3222d3f2020-03-11 20:24:00 +080071 void start(WallpaperTexture texture) {
72 new ThresholdComputeTask(mHandler).execute(texture);
Ahan Wu67e7f102019-01-14 20:38:14 +080073 }
74
Ahan Wu330bc602019-04-25 15:04:39 +080075 float getThreshold() {
76 return Math.min(mThreshold, MAX_THRESHOLD);
Ahan Wu67e7f102019-01-14 20:38:14 +080077 }
78
Ahan Wu3222d3f2020-03-11 20:24:00 +080079 private static class ThresholdComputeTask extends AsyncTask<WallpaperTexture, Void, Float> {
Ahan Wu67e7f102019-01-14 20:38:14 +080080 private Handler mUpdateHandler;
81
Ahan Wu330bc602019-04-25 15:04:39 +080082 ThresholdComputeTask(Handler handler) {
Ahan Wu67e7f102019-01-14 20:38:14 +080083 super(handler);
84 mUpdateHandler = handler;
85 }
86
87 @Override
Ahan Wu3222d3f2020-03-11 20:24:00 +080088 protected Float doInBackground(WallpaperTexture... textures) {
89 WallpaperTexture texture = textures[0];
90 final float[] threshold = new float[] {DEFAULT_THRESHOLD};
91 if (texture == null) {
92 Log.e(TAG, "ThresholdComputeTask: WallpaperTexture not initialized");
93 return threshold[0];
Ahan Wu67e7f102019-01-14 20:38:14 +080094 }
Ahan Wu3222d3f2020-03-11 20:24:00 +080095
96 texture.use(bitmap -> {
97 if (bitmap != null) {
98 threshold[0] = new Threshold().compute(bitmap);
99 } else {
100 Log.e(TAG, "ThresholdComputeTask: Can't get bitmap");
101 }
102 });
103 return threshold[0];
Ahan Wu67e7f102019-01-14 20:38:14 +0800104 }
105
106 @Override
107 protected void onPostExecute(Float result) {
Ahan Wu330bc602019-04-25 15:04:39 +0800108 Message msg = mUpdateHandler.obtainMessage(MSG_UPDATE_THRESHOLD, result);
Ahan Wu67e7f102019-01-14 20:38:14 +0800109 mUpdateHandler.sendMessage(msg);
110 }
Ahan Wu330bc602019-04-25 15:04:39 +0800111 }
Ahan Wu67e7f102019-01-14 20:38:14 +0800112
Ahan Wu330bc602019-04-25 15:04:39 +0800113 private static class Threshold {
114 public float compute(Bitmap bitmap) {
115 Bitmap grayscale = toGrayscale(bitmap);
116 int[] histogram = getHistogram(grayscale);
117 boolean isSolidColor = isSolidColor(grayscale, histogram);
118
119 // We will see gray wallpaper during the transition if solid color wallpaper is set,
120 // please refer to b/130360362#comment16.
121 // As a result, we use Percentile85 rather than Otsus if a solid color wallpaper is set.
122 ThresholdAlgorithm algorithm = isSolidColor ? new Percentile85() : new Otsus();
123 return algorithm.compute(grayscale, histogram);
124 }
125
126 private Bitmap toGrayscale(Bitmap bitmap) {
Ahan Wu67e7f102019-01-14 20:38:14 +0800127 int width = bitmap.getWidth();
128 int height = bitmap.getHeight();
129
Ahan Wu287d8282019-12-17 16:23:17 +0800130 Bitmap grayscale = Bitmap.createBitmap(width, height, bitmap.getConfig(),
131 false /* hasAlpha */, bitmap.getColorSpace());
Ahan Wu330bc602019-04-25 15:04:39 +0800132 Canvas canvas = new Canvas(grayscale);
Ahan Wu67e7f102019-01-14 20:38:14 +0800133 ColorMatrix cm = new ColorMatrix(LUMINOSITY_MATRIX);
134 Paint paint = new Paint();
135 paint.setColorFilter(new ColorMatrixColorFilter(cm));
136 canvas.drawBitmap(bitmap, new Matrix(), paint);
137
Ahan Wu330bc602019-04-25 15:04:39 +0800138 return grayscale;
139 }
140
141 private int[] getHistogram(Bitmap grayscale) {
142 int width = grayscale.getWidth();
143 int height = grayscale.getHeight();
144
Ahan Wu67e7f102019-01-14 20:38:14 +0800145 // TODO: Fine tune the performance here, tracking on b/123615079.
146 int[] histogram = new int[256];
147 for (int row = 0; row < height; row++) {
148 for (int col = 0; col < width; col++) {
Ahan Wu330bc602019-04-25 15:04:39 +0800149 int pixel = grayscale.getPixel(col, row);
Ahan Wu67e7f102019-01-14 20:38:14 +0800150 int y = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel);
151 histogram[y]++;
152 }
153 }
154
155 return histogram;
156 }
157
Ahan Wu330bc602019-04-25 15:04:39 +0800158 private boolean isSolidColor(Bitmap bitmap, int[] histogram) {
159 boolean solidColor = false;
160 int pixels = bitmap.getWidth() * bitmap.getHeight();
161
162 // In solid color case, only one element of histogram has value,
163 // which is pixel counts and the value of other elements should be 0.
164 for (int value : histogram) {
165 if (value != 0 && value != pixels) {
166 break;
167 }
168 if (value == pixels) {
169 solidColor = true;
170 break;
171 }
172 }
173 return solidColor;
174 }
175 }
176
177 private static class Percentile85 implements ThresholdAlgorithm {
178 @Override
179 public float compute(Bitmap bitmap, int[] histogram) {
180 float per85 = DEFAULT_THRESHOLD;
Ahan Wu67e7f102019-01-14 20:38:14 +0800181 int pixelCount = bitmap.getWidth() * bitmap.getHeight();
182 float[] acc = new float[256];
183 for (int i = 0; i < acc.length; i++) {
184 acc[i] = (float) histogram[i] / pixelCount;
185 float prev = i == 0 ? 0f : acc[i - 1];
186 float next = acc[i];
187 float idx = (float) (i + 1) / 255;
188 float sum = prev + next;
189 if (prev < 0.85f && sum >= 0.85f) {
190 per85 = idx;
191 }
192 if (i > 0) {
193 acc[i] += acc[i - 1];
194 }
195 }
196 return per85;
197 }
198 }
Ahan Wu330bc602019-04-25 15:04:39 +0800199
200 private static class Otsus implements ThresholdAlgorithm {
201 @Override
202 public float compute(Bitmap bitmap, int[] histogram) {
203 float threshold = DEFAULT_OTSU_THRESHOLD;
204 float maxVariance = 0;
205 float pixelCount = bitmap.getWidth() * bitmap.getHeight();
206 float[] w = new float[2];
207 float[] m = new float[2];
208 float[] u = new float[2];
209
210 for (int i = 0; i < histogram.length; i++) {
211 m[1] += i * histogram[i];
212 }
213
214 w[1] = pixelCount;
215 for (int tonalValue = 0; tonalValue < histogram.length; tonalValue++) {
216 float dU;
217 float variance;
218 float numPixels = histogram[tonalValue];
219 float tmp = numPixels * tonalValue;
220 w[0] += numPixels;
221 w[1] -= numPixels;
222
223 if (w[0] == 0 || w[1] == 0) {
224 continue;
225 }
226
227 m[0] += tmp;
228 m[1] -= tmp;
229 u[0] = m[0] / w[0];
230 u[1] = m[1] / w[1];
231 dU = u[0] - u[1];
232 variance = w[0] * w[1] * dU * dU;
233
234 if (variance > maxVariance) {
235 threshold = (tonalValue + 1f) / histogram.length;
236 maxVariance = variance;
237 }
238 }
239 return threshold;
240 }
241 }
242
243 private interface ThresholdAlgorithm {
244 float compute(Bitmap bitmap, int[] histogram);
245 }
Ahan Wu67e7f102019-01-14 20:38:14 +0800246}