More auto-dark stuff
Initial attempt at selective bitmap inverting
Use CIE_LAB colorspace for inverting instead of HSV
Test: Manually poking around
Change-Id: I014ff31eeae471ee7f6a40a6daa4e7099c2a7ff8
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index bac7a4d..1b15dbd 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -15,32 +15,38 @@
*/
#include "CanvasTransform.h"
+#include "utils/Color.h"
#include "Properties.h"
+#include <ui/ColorSpace.h>
#include <SkColorFilter.h>
#include <SkPaint.h>
-#include <log/log.h>
+
+#include <algorithm>
+#include <cmath>
namespace android::uirenderer {
static SkColor makeLight(SkColor color) {
- SkScalar hsv[3];
- SkColorToHSV(color, hsv);
- if (hsv[1] > .2f) return color;
- // hsv[1] *= .85f;
- // hsv[2] = std::min(1.0f, std::max(hsv[2], 1 - hsv[2]) * 1.3f);
- hsv[2] = std::max(hsv[2], 1.1f - hsv[2]);
- return SkHSVToColor(SkColorGetA(color), hsv);
+ Lab lab = sRGBToLab(color);
+ float invertedL = std::min(110 - lab.L, 100.0f);
+ if (invertedL > lab.L) {
+ lab.L = invertedL;
+ return LabToSRGB(lab, SkColorGetA(color));
+ } else {
+ return color;
+ }
}
static SkColor makeDark(SkColor color) {
- SkScalar hsv[3];
- SkColorToHSV(color, hsv);
- if (hsv[1] > .2f) return color;
- // hsv[1] *= .85f;
- // hsv[2] = std::max(0.0f, std::min(hsv[2], 1 - hsv[2]) * .7f);
- hsv[2] = std::min(hsv[2], 1.1f - hsv[2]);
- return SkHSVToColor(SkColorGetA(color), hsv);
+ Lab lab = sRGBToLab(color);
+ float invertedL = std::min(110 - lab.L, 100.0f);
+ if (invertedL < lab.L) {
+ lab.L = invertedL;
+ return LabToSRGB(lab, SkColorGetA(color));
+ } else {
+ return color;
+ }
}
static SkColor transformColor(ColorTransform transform, SkColor color) {
diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp
index 6408ce6..ab80d3d 100644
--- a/libs/hwui/HardwareBitmapUploader.cpp
+++ b/libs/hwui/HardwareBitmapUploader.cpp
@@ -286,7 +286,7 @@
eglDestroySyncKHR(display, fence);
}
- return sk_sp<Bitmap>(new Bitmap(buffer.get(), bitmap.info()));
+ return sk_sp<Bitmap>(new Bitmap(buffer.get(), bitmap.info(), Bitmap::computePalette(bitmap)));
}
}; // namespace android::uirenderer
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index a401b3f..7a8d026 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -17,6 +17,7 @@
#include "Caches.h"
#include "HardwareBitmapUploader.h"
+#include "Properties.h"
#include "renderthread/RenderProxy.h"
#include "utils/Color.h"
@@ -34,6 +35,7 @@
#include <SkToSRGBColorFilter.h>
#include <limits>
+#include <SkHighContrastFilter.h>
namespace android {
@@ -195,11 +197,13 @@
mPixelStorage.ashmem.size = mappedSize;
}
-Bitmap::Bitmap(GraphicBuffer* buffer, const SkImageInfo& info)
+Bitmap::Bitmap(GraphicBuffer* buffer, const SkImageInfo& info, BitmapPalette palette)
: SkPixelRef(info.width(), info.height(), nullptr,
bytesPerPixel(buffer->getPixelFormat()) * buffer->getStride())
, mInfo(validateAlpha(info))
- , mPixelStorageType(PixelStorageType::Hardware) {
+ , mPixelStorageType(PixelStorageType::Hardware)
+ , mPalette(palette)
+ , mPaletteGenerationId(getGenerationID()) {
mPixelStorage.hardware.buffer = buffer;
buffer->incStrong(buffer);
setImmutable(); // HW bitmaps are always immutable
@@ -326,7 +330,106 @@
if (image->colorSpace() != nullptr && !image->colorSpace()->isSRGB()) {
*outputColorFilter = SkToSRGBColorFilter::Make(image->refColorSpace());
}
+
+ // TODO: Move this to the canvas (or other?) layer where we have the target lightness
+ // mode and can selectively do the right thing.
+ if (palette() != BitmapPalette::Unknown && uirenderer::Properties::forceDarkMode) {
+ SkHighContrastConfig config;
+ config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
+ *outputColorFilter = SkHighContrastFilter::Make(config)->makeComposed(*outputColorFilter);
+ }
return image;
}
+class MinMaxAverage {
+public:
+
+ void add(float sample) {
+ if (mCount == 0) {
+ mMin = sample;
+ mMax = sample;
+ } else {
+ mMin = std::min(mMin, sample);
+ mMax = std::max(mMax, sample);
+ }
+ mTotal += sample;
+ mCount++;
+ }
+
+ float average() {
+ return mTotal / mCount;
+ }
+
+ float min() {
+ return mMin;
+ }
+
+ float max() {
+ return mMax;
+ }
+
+ float delta() {
+ return mMax - mMin;
+ }
+
+private:
+ float mMin = 0.0f;
+ float mMax = 0.0f;
+ float mTotal = 0.0f;
+ int mCount = 0;
+};
+
+BitmapPalette Bitmap::computePalette(const SkImageInfo& info, const void* addr, size_t rowBytes) {
+ ATRACE_CALL();
+
+ SkPixmap pixmap{info, addr, rowBytes};
+
+ // TODO: This calculation of converting to HSV & tracking min/max is probably overkill
+ // Experiment with something simpler since we just want to figure out if it's "color-ful"
+ // and then the average perceptual lightness.
+
+ MinMaxAverage hue, saturation, value;
+ int sampledCount = 0;
+
+ // Sample a grid of 100 pixels to get an overall estimation of the colors in play
+ const int x_step = std::max(1, pixmap.width() / 10);
+ const int y_step = std::max(1, pixmap.height() / 10);
+ for (int x = 0; x < pixmap.width(); x += x_step) {
+ for (int y = 0; y < pixmap.height(); y += y_step) {
+ SkColor color = pixmap.getColor(x, y);
+ if (!info.isOpaque() && SkColorGetA(color) < 75) {
+ continue;
+ }
+
+ sampledCount++;
+ float hsv[3];
+ SkColorToHSV(color, hsv);
+ hue.add(hsv[0]);
+ saturation.add(hsv[1]);
+ value.add(hsv[2]);
+ }
+ }
+
+ // TODO: Tune the coverage threshold
+ if (sampledCount < 5) {
+ ALOGV("Not enough samples, only found %d for image sized %dx%d, format = %d, alpha = %d",
+ sampledCount, info.width(), info.height(), (int) info.colorType(), (int) info.alphaType());
+ return BitmapPalette::Unknown;
+ }
+
+ ALOGV("samples = %d, hue [min = %f, max = %f, avg = %f]; saturation [min = %f, max = %f, avg = %f]",
+ sampledCount,
+ hue.min(), hue.max(), hue.average(),
+ saturation.min(), saturation.max(), saturation.average());
+
+ if (hue.delta() <= 20 && saturation.delta() <= .1f) {
+ if (value.average() >= .5f) {
+ return BitmapPalette::Light;
+ } else {
+ return BitmapPalette::Dark;
+ }
+ }
+ return BitmapPalette::Unknown;
+}
+
} // namespace android
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index dbd4456..d268042 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -34,6 +34,12 @@
Hardware,
};
+enum class BitmapPalette {
+ Unknown,
+ Light,
+ Dark,
+};
+
namespace uirenderer {
namespace renderthread {
class RenderThread;
@@ -63,7 +69,7 @@
Bitmap(void* address, void* context, FreeFunc freeFunc, const SkImageInfo& info,
size_t rowBytes);
Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes);
- Bitmap(GraphicBuffer* buffer, const SkImageInfo& info);
+ Bitmap(GraphicBuffer* buffer, const SkImageInfo& info, BitmapPalette palette = BitmapPalette::Unknown);
int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); }
@@ -103,6 +109,20 @@
*/
sk_sp<SkImage> makeImage(sk_sp<SkColorFilter>* outputColorFilter);
+ static BitmapPalette computePalette(const SkImageInfo& info, const void* addr, size_t rowBytes);
+
+ static BitmapPalette computePalette(const SkBitmap& bitmap) {
+ return computePalette(bitmap.info(), bitmap.getPixels(), bitmap.rowBytes());
+ }
+
+ BitmapPalette palette() {
+ if (!isHardware() && mPaletteGenerationId != getGenerationID()) {
+ mPalette = computePalette(info(), pixels(), rowBytes());
+ mPaletteGenerationId = getGenerationID();
+ }
+ return mPalette;
+ }
+
private:
virtual ~Bitmap();
void* getStorage() const;
@@ -111,6 +131,9 @@
const PixelStorageType mPixelStorageType;
+ BitmapPalette mPalette = BitmapPalette::Unknown;
+ uint32_t mPaletteGenerationId = -1;
+
bool mHasHardwareMipMap = false;
union {
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 75740e8..a3e7859 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -16,8 +16,10 @@
#include "Color.h"
-
#include <utils/Log.h>
+#include <ui/ColorSpace.h>
+
+#include <algorithm>
#include <cmath>
namespace android {
@@ -107,5 +109,97 @@
}
}
+template<typename T>
+static constexpr T clamp(T x, T min, T max) {
+ return x < min ? min : x > max ? max : x;
+}
+
+//static const float2 ILLUMINANT_D50_XY = {0.34567f, 0.35850f};
+static const float3 ILLUMINANT_D50_XYZ = {0.964212f, 1.0f, 0.825188f};
+static const mat3 BRADFORD = mat3{
+ float3{ 0.8951f, -0.7502f, 0.0389f},
+ float3{ 0.2664f, 1.7135f, -0.0685f},
+ float3{-0.1614f, 0.0367f, 1.0296f}
+};
+
+static mat3 adaptation(const mat3& matrix, const float3& srcWhitePoint, const float3& dstWhitePoint) {
+ float3 srcLMS = matrix * srcWhitePoint;
+ float3 dstLMS = matrix * dstWhitePoint;
+ return inverse(matrix) * mat3{dstLMS / srcLMS} * matrix;
+}
+
+namespace LabColorSpace {
+
+static constexpr float A = 216.0f / 24389.0f;
+static constexpr float B = 841.0f / 108.0f;
+static constexpr float C = 4.0f / 29.0f;
+static constexpr float D = 6.0f / 29.0f;
+
+float3 toXyz(const Lab& lab) {
+ float3 v { lab.L, lab.a, lab.b };
+ v[0] = clamp(v[0], 0.0f, 100.0f);
+ v[1] = clamp(v[1], -128.0f, 128.0f);
+ v[2] = clamp(v[2], -128.0f, 128.0f);
+
+ float fy = (v[0] + 16.0f) / 116.0f;
+ float fx = fy + (v[1] * 0.002f);
+ float fz = fy - (v[2] * 0.005f);
+ float X = fx > D ? fx * fx * fx : (1.0f / B) * (fx - C);
+ float Y = fy > D ? fy * fy * fy : (1.0f / B) * (fy - C);
+ float Z = fz > D ? fz * fz * fz : (1.0f / B) * (fz - C);
+
+ v[0] = X * ILLUMINANT_D50_XYZ[0];
+ v[1] = Y * ILLUMINANT_D50_XYZ[1];
+ v[2] = Z * ILLUMINANT_D50_XYZ[2];
+
+ return v;
+}
+
+Lab fromXyz(const float3& v) {
+ float X = v[0] / ILLUMINANT_D50_XYZ[0];
+ float Y = v[1] / ILLUMINANT_D50_XYZ[1];
+ float Z = v[2] / ILLUMINANT_D50_XYZ[2];
+
+ float fx = X > A ? pow(X, 1.0f / 3.0f) : B * X + C;
+ float fy = Y > A ? pow(Y, 1.0f / 3.0f) : B * Y + C;
+ float fz = Z > A ? pow(Z, 1.0f / 3.0f) : B * Z + C;
+
+ float L = 116.0f * fy - 16.0f;
+ float a = 500.0f * (fx - fy);
+ float b = 200.0f * (fy - fz);
+
+ return Lab {
+ clamp(L, 0.0f, 100.0f),
+ clamp(a, -128.0f, 128.0f),
+ clamp(b, -128.0f, 128.0f)
+ };
+}
+
+};
+
+Lab sRGBToLab(SkColor color) {
+ auto colorSpace = ColorSpace::sRGB();
+ float3 rgb;
+ rgb.r = SkColorGetR(color) / 255.0f;
+ rgb.g = SkColorGetG(color) / 255.0f;
+ rgb.b = SkColorGetB(color) / 255.0f;
+ float3 xyz = colorSpace.rgbToXYZ(rgb);
+ float3 srcXYZ = ColorSpace::XYZ(float3{colorSpace.getWhitePoint(), 1});
+ xyz = adaptation(BRADFORD, srcXYZ, ILLUMINANT_D50_XYZ) * xyz;
+ return LabColorSpace::fromXyz(xyz);
+}
+
+SkColor LabToSRGB(const Lab& lab, SkAlpha alpha) {
+ auto colorSpace = ColorSpace::sRGB();
+ float3 xyz = LabColorSpace::toXyz(lab);
+ float3 dstXYZ = ColorSpace::XYZ(float3{colorSpace.getWhitePoint(), 1});
+ xyz = adaptation(BRADFORD, ILLUMINANT_D50_XYZ, dstXYZ) * xyz;
+ float3 rgb = colorSpace.xyzToRGB(xyz);
+ return SkColorSetARGB(alpha,
+ static_cast<uint8_t>(rgb.r * 255),
+ static_cast<uint8_t>(rgb.g * 255),
+ static_cast<uint8_t>(rgb.b * 255));
+}
+
}; // namespace uirenderer
}; // namespace android
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index 2bec1f5..3c13a54 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -114,6 +114,16 @@
bool transferFunctionCloseToSRGB(const SkColorSpace* colorSpace);
sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace);
+
+struct Lab {
+ float L;
+ float a;
+ float b;
+};
+
+Lab sRGBToLab(SkColor color);
+SkColor LabToSRGB(const Lab& lab, SkAlpha alpha);
+
} /* namespace uirenderer */
} /* namespace android */