Add API to visualize/debug/compare color spaces and colors

ColorSpace.createRenderer() returns a ColorSpace.Renderer which
can be used to visualize multiple color spaces and colors (each
associated with a specific color space) in the same CIE 1931 xyY
chromaticity diagram. This API was used to generate all the
diagrams shown in the documentation.

Bug: 32984164
Test: ColorSpaceTest in CTS
Change-Id: Ic94d939d5cfd563d826701f5e876b67b47ed56ec
This commit is contained in:
Romain Guy
2016-11-29 16:48:27 -08:00
parent 199e5a98dd
commit 15296a2d34
9 changed files with 759 additions and 0 deletions

View File

@@ -11979,6 +11979,7 @@ package android.graphics {
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Renderer createRenderer();
method public float[] fromXyz(float, float, float);
method public abstract float[] fromXyz(float[]);
method public static android.graphics.ColorSpace get(android.graphics.ColorSpace.Named);
@@ -12060,6 +12061,15 @@ package android.graphics {
enum_constant public static final android.graphics.ColorSpace.RenderIntent SATURATION;
}
public static class ColorSpace.Renderer {
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, int);
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, float, float, float, int);
method public android.graphics.ColorSpace.Renderer clip(boolean);
method public android.graphics.Bitmap render();
method public android.graphics.ColorSpace.Renderer showWhitePoint(boolean);
method public android.graphics.ColorSpace.Renderer size(int);
}
public static class ColorSpace.Rgb extends android.graphics.ColorSpace {
ctor public ColorSpace.Rgb(java.lang.String, float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator);
ctor public ColorSpace.Rgb(java.lang.String, float[], float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator, float, float);

View File

@@ -12462,6 +12462,7 @@ package android.graphics {
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Renderer createRenderer();
method public float[] fromXyz(float, float, float);
method public abstract float[] fromXyz(float[]);
method public static android.graphics.ColorSpace get(android.graphics.ColorSpace.Named);
@@ -12543,6 +12544,15 @@ package android.graphics {
enum_constant public static final android.graphics.ColorSpace.RenderIntent SATURATION;
}
public static class ColorSpace.Renderer {
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, int);
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, float, float, float, int);
method public android.graphics.ColorSpace.Renderer clip(boolean);
method public android.graphics.Bitmap render();
method public android.graphics.ColorSpace.Renderer showWhitePoint(boolean);
method public android.graphics.ColorSpace.Renderer size(int);
}
public static class ColorSpace.Rgb extends android.graphics.ColorSpace {
ctor public ColorSpace.Rgb(java.lang.String, float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator);
ctor public ColorSpace.Rgb(java.lang.String, float[], float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator, float, float);

View File

@@ -12004,6 +12004,7 @@ package android.graphics {
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace);
method public static android.graphics.ColorSpace.Connector connect(android.graphics.ColorSpace, android.graphics.ColorSpace.RenderIntent);
method public static android.graphics.ColorSpace.Renderer createRenderer();
method public float[] fromXyz(float, float, float);
method public abstract float[] fromXyz(float[]);
method public static android.graphics.ColorSpace get(android.graphics.ColorSpace.Named);
@@ -12085,6 +12086,15 @@ package android.graphics {
enum_constant public static final android.graphics.ColorSpace.RenderIntent SATURATION;
}
public static class ColorSpace.Renderer {
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, int);
method public android.graphics.ColorSpace.Renderer add(android.graphics.ColorSpace, float, float, float, int);
method public android.graphics.ColorSpace.Renderer clip(boolean);
method public android.graphics.Bitmap render();
method public android.graphics.ColorSpace.Renderer showWhitePoint(boolean);
method public android.graphics.ColorSpace.Renderer size(int);
}
public static class ColorSpace.Rgb extends android.graphics.ColorSpace {
ctor public ColorSpace.Rgb(java.lang.String, float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator);
ctor public ColorSpace.Rgb(java.lang.String, float[], float[], java.util.function.DoubleUnaryOperator, java.util.function.DoubleUnaryOperator, float, float);

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -16,12 +16,16 @@
package android.graphics;
import android.annotation.ColorInt;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Size;
import android.annotation.Nullable;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
/**
@@ -118,11 +122,38 @@ import java.util.function.DoubleUnaryOperator;
* and {@link #connect(ColorSpace, ColorSpace)}, are also guaranteed to be
* thread-safe.</p>
*
* <h3>Visualization and debugging</h3>
*
* <p>To visualize and debug color spaces, you can call {@link #createRenderer()}.
* The {@link Renderer} created by calling this method can be used to compare
* color spaces and locate specific colors on a CIE 1931 chromaticity diagram.</p>
*
* <p>The following code snippet shows how to render a bitmap that compares
* the color gamuts and white points of {@link Named#DCI_P3} and
* {@link Named#PRO_PHOTO_RGB}:</p>
*
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .size(768)
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .add(ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB), 0xff097ae9)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_renderer.png" />
* <figcaption style="text-align: center;">DCI-P3 vs ProPhoto RGB</figcaption>
* </p>
*
* <p>Please refer to the documentation of the {@link Renderer} class for more
* information about its options and capabilities.</p>
*
* @see #get(Named)
* @see Named
* @see Model
* @see Connector
* @see Adaptation
* @see Renderer
*/
@SuppressWarnings("StaticInitializerReferencesSubClass")
public abstract class ColorSpace {
@@ -1333,6 +1364,20 @@ public abstract class ColorSpace {
return sNamedColorSpaces[name.ordinal()];
}
/**
* <p>Creates a new {@link Renderer} that can be used to visualize and
* debug color spaces. See the documentation of {@link Renderer} for
* more information.</p>
*
* @return A new non-null {@link Renderer} instance
*
* @see Renderer
*/
@NonNull
public static Renderer createRenderer() {
return new Renderer();
}
static {
sNamedColorSpaces[Named.SRGB.ordinal()] = new ColorSpace.Rgb(
"sRGB IEC61966-2.1",
@@ -3113,4 +3158,688 @@ public abstract class ColorSpace {
};
}
}
/**
* <p>A color space renderer can be used to visualize and compare the gamut and
* white point of one or more color spaces. The output is an sRGB {@link Bitmap}
* showing a CIE 1931 xyY chromaticity diagram.</p>
*
* <p>The following code snippet shows how to compare the {@link Named#SRGB}
* and {@link Named#DCI_P3} color spaces:</p>
*
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .size(768)
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" />
* <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption>
* </p>
*
* <p>A renderer can also be used to show the location of specific colors,
* associated with a color space, in the CIE 1931 xyY chromaticity diagram.
* See {@link #add(ColorSpace, float, float, float, int)} for more information.</p>
*
* @see ColorSpace#createRenderer()
*/
public static class Renderer {
private static final int NATIVE_SIZE = 1440;
@IntRange(from = 128, to = Integer.MAX_VALUE)
private int mSize = 1024;
private boolean mShowWhitePoint = true;
private boolean mClip = false;
private final List<Pair<ColorSpace, Integer>> mColorSpaces = new ArrayList<>(2);
private final List<Point> mPoints = new ArrayList<>(0);
private Renderer() {
}
/**
* <p>Defines whether the chromaticity diagram should be clipped by the first
* registered color space. The default value is false.</p>
*
* <p>The following code snippet and image show the default behavior:</p>
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" />
* <figcaption style="text-align: center;">Clipping disabled</figcaption>
* </p>
*
* <p>Here is the same example with clipping enabled:</p>
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_clipped.png" />
* <figcaption style="text-align: center;">Clipping enabled</figcaption>
* </p>
*
* @param clip True to clip the chromaticity diagram to the first registered color space,
* false otherwise
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer clip(boolean clip) {
mClip = clip;
return this;
}
/**
* Sets the dimensions (width and height) in pixels of the output bitmap.
* The size must be at least 128px and defaults to 1024px.
*
* @param size The size in pixels of the output bitmap
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer size(@IntRange(from = 128, to = Integer.MAX_VALUE) int size) {
mSize = Math.max(128, size);
return this;
}
/**
* Shows or hides the white point of each color space in the output bitmap.
* The default is true.
*
* @param show True to show the white point of each color space, false
* otherwise
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer showWhitePoint(boolean show) {
mShowWhitePoint = show;
return this;
}
/**
* <p>Adds a color space to represent on the output CIE 1931 chromaticity
* diagram. The color space is represented as a triangle showing the
* footprint of its color gamut and, optionally, the location of its
* white point.</p>
*
* <p class="note">Color spaces with a color model that is not RGB are
* accepted but ignored.</p>
*
* <p>The following code snippet and image show an example of calling this
* method to compare {@link Named#SRGB sRGB} and {@link Named#DCI_P3 DCI-P3}:</p>
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison.png" />
* <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption>
* </p>
*
* <p>Adding a color space extending beyond the boundaries of the
* spectral locus will alter the size of the diagram within the output
* bitmap as shown in this example:</p>
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.DCI_P3), 0xffffc845)
* .add(ColorSpace.get(ColorSpace.Named.ACES), 0xff097ae9)
* .add(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), 0xff000000)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_comparison2.png" />
* <figcaption style="text-align: center;">sRGB vs DCI-P3</figcaption>
* </p>
*
* @param colorSpace The color space whose gamut to render on the diagram
* @param color The sRGB color to use to render the color space's gamut and white point
* @return This instance of {@link Renderer}
*
* @see #clip(boolean)
* @see #showWhitePoint(boolean)
*/
@NonNull
public Renderer add(@NonNull ColorSpace colorSpace, @ColorInt int color) {
mColorSpaces.add(new Pair<>(colorSpace, color));
return this;
}
/**
* <p>Adds a color to represent as a point on the chromaticity diagram.
* The color is associated with a color space which will be used to
* perform the conversion to CIE XYZ and compute the location of the point
* on the diagram. The point is rendered as a colored circle.</p>
*
* <p>The following code snippet and image show an example of calling this
* method to render the location of several sRGB colors as white circles:</p>
* <pre class="prettyprint">
* Bitmap bitmap = ColorSpace.createRenderer()
* .clip(true)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.0f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.1f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.2f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.3f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.4f, 0.1f, 0xffffffff)
* .add(ColorSpace.get(ColorSpace.Named.SRGB), 0.1f, 0.5f, 0.1f, 0xffffffff)
* .render();
* </pre>
* <p>
* <img src="{@docRoot}reference/android/images/graphics/colorspace_points.png" />
* <figcaption style="text-align: center;">
* Locating colors on the chromaticity diagram
* </figcaption>
* </p>
*
* @param colorSpace The color space of the color to locate on the diagram
* @param r The first component of the color to locate on the diagram
* @param g The second component of the color to locate on the diagram
* @param b The third component of the color to locate on the diagram
* @param pointColor The sRGB color to use to render the point on the diagram
* @return This instance of {@link Renderer}
*/
@NonNull
public Renderer add(@NonNull ColorSpace colorSpace, float r, float g, float b,
@ColorInt int pointColor) {
mPoints.add(new Point(colorSpace, new float[] { r, g, b }, pointColor));
return this;
}
/**
* <p>Renders the {@link #add(ColorSpace, int) color spaces} and
* {@link #add(ColorSpace, float, float, float, int) points} registered
* with this renderer. The output bitmap is an sRGB image with the
* dimensions specified by calling {@link #size(int)} (1204x1024px by
* default).</p>
*
* @return A new non-null {@link Bitmap} with the dimensions specified
* by {@link #size(int)} (1024x1024 by default)
*/
@NonNull
public Bitmap render() {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
float[] primaries = new float[6];
float[] whitePoint = new float[2];
int width = NATIVE_SIZE;
int height = NATIVE_SIZE;
Path path = new Path();
setTransform(canvas, width, height, primaries);
drawBox(canvas, width, height, paint, path);
drawLocus(canvas, width, height, paint, path, primaries);
drawGamuts(canvas, width, height, paint, path, primaries, whitePoint);
drawPoints(canvas, width, height, paint);
return bitmap;
}
/**
* Draws registered points at their correct position in the xyY coordinates.
* Each point is positioned according to its associated color space.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
*/
private void drawPoints(@NonNull Canvas canvas, int width, int height,
@NonNull Paint paint) {
paint.setStyle(Paint.Style.FILL);
float[] v = new float[3];
for (final Point point : mPoints) {
v[0] = point.mRgb[0];
v[1] = point.mRgb[1];
v[2] = point.mRgb[2];
point.mColorSpace.toXyz(v);
paint.setColor(point.mColor);
// XYZ to xyY, assuming Y=1.0
float sum = v[0] + v[1] + v[2];
canvas.drawCircle(width * v[0] / sum, height - height * v[1] / sum,
4.0f, paint);
}
}
/**
* Draws the color gamuts and white points of all the registered color
* spaces. Only color spaces with an RGB color model are rendered, the
* others are ignored.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
* @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
* @param whitePoint A pre-allocated array of 2 floats to avoid temporary allocations
*/
private void drawGamuts(
@NonNull Canvas canvas, int width, int height,
@NonNull Paint paint, @NonNull Path path,
@NonNull @Size(6) float[] primaries, @NonNull @Size(2) float[] whitePoint) {
for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
ColorSpace colorSpace = item.first;
int color = item.second;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries);
path.rewind();
path.moveTo(width * primaries[0], height - height * primaries[1]);
path.lineTo(width * primaries[2], height - height * primaries[3]);
path.lineTo(width * primaries[4], height - height * primaries[5]);
path.close();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(color);
canvas.drawPath(path, paint);
// Draw the white point
if (mShowWhitePoint) {
rgb.getWhitePoint(whitePoint);
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
canvas.drawCircle(width * whitePoint[0], height - height * whitePoint[1],
4.0f, paint);
}
}
}
/**
* Returns the primaries of the specified RGB color space. This method handles
* the special case of the {@link Named#EXTENDED_SRGB} family of color spaces.
*
* @param rgb The color space whose primaries to extract
* @param primaries A pre-allocated array of 6 floats that will hold the result
*/
@NonNull
@Size(6)
private static float[] getPrimaries(@NonNull Rgb rgb, @NonNull @Size(6) float[] primaries) {
// TODO: We should find a better way to handle these cases
if (rgb.equals(ColorSpace.get(Named.EXTENDED_SRGB)) ||
rgb.equals(ColorSpace.get(Named.LINEAR_EXTENDED_SRGB))) {
primaries[0] = 1.41f;
primaries[1] = 0.33f;
primaries[2] = 0.27f;
primaries[3] = 1.24f;
primaries[4] = -0.23f;
primaries[5] = -0.57f;
return primaries;
}
return rgb.getPrimaries(primaries);
}
/**
* Draws the CIE 1931 chromaticity diagram: the spectral locus and its inside.
* This method respect the clip parameter.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
* @param primaries A pre-allocated array of 6 floats to avoid temporary allocations
*/
private void drawLocus(
@NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
@NonNull Path path, @NonNull @Size(6) float[] primaries) {
int vertexCount = SPECTRUM_LOCUS_X.length * CHROMATICITY_RESOLUTION * 6;
float[] vertices = new float[vertexCount * 2];
int[] colors = new int[vertices.length];
computeChromaticityMesh(NATIVE_SIZE, NATIVE_SIZE, vertices, colors);
// Draw the spectral locus
if (mClip && mColorSpaces.size() > 0) {
for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
ColorSpace colorSpace = item.first;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries);
break;
}
path.rewind();
path.moveTo(width * primaries[0], height - height * primaries[1]);
path.lineTo(width * primaries[2], height - height * primaries[3]);
path.lineTo(width * primaries[4], height - height * primaries[5]);
path.close();
int[] solid = new int[colors.length];
Arrays.fill(solid, 0xff6c6c6c);
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, solid, 0, null, 0, 0, paint);
canvas.save();
canvas.clipPath(path);
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, colors, 0, null, 0, 0, paint);
canvas.restore();
} else {
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.length, vertices, 0,
null, 0, colors, 0, null, 0, 0, paint);
}
// Draw the non-spectral locus
int index = (CHROMATICITY_RESOLUTION - 1) * 12;
path.reset();
path.moveTo(vertices[index], vertices[index + 1]);
for (int x = 2; x < SPECTRUM_LOCUS_X.length; x++) {
index += CHROMATICITY_RESOLUTION * 12;
path.lineTo(vertices[index], vertices[index + 1]);
}
path.close();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xff000000);
canvas.drawPath(path, paint);
}
/**
* Draws the diagram box, including borders, tick marks, grid lines
* and axis labels.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param paint A pre-allocated paint used to avoid temporary allocations
* @param path A pre-allocated path used to avoid temporary allocations
*/
private void drawBox(@NonNull Canvas canvas, int width, int height, @NonNull Paint paint,
@NonNull Path path) {
// Draw the unit grid
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2.0f);
paint.setColor(0xffc0c0c0);
for (int i = 1; i <= 9; i++) {
canvas.drawLine(0.0f, height - (height * i / 10.0f),
0.9f * width, height - (height * i / 10.0f), paint);
canvas.drawLine(width * i / 10.0f, height,
width * i / 10.0f, 0.1f * height, paint);
}
// Draw tick marks
paint.setStrokeWidth(4.0f);
paint.setColor(0xff000000);
for (int i = 1; i <= 9; i++) {
canvas.drawLine(0.0f, height - (height * i / 10.0f),
width / 100.0f, height - (height * i / 10.0f), paint);
canvas.drawLine(width * i / 10.0f, height,
width * i / 10.0f, height - (height / 100.0f), paint);
}
// Draw the axis labels
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(36.0f);
paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
Rect bounds = new Rect();
for (int i = 1; i < 9; i++) {
String text = "0." + i;
paint.getTextBounds(text, 0, text.length(), bounds);
float y = height - (height * i / 10.0f);
canvas.drawText(text, -0.05f * width + 10, y + bounds.height() / 2.0f, paint);
float x = width * i / 10.0f;
canvas.drawText(text, x - bounds.width() / 2.0f,
height + bounds.height() + 16, paint);
}
paint.setStyle(Paint.Style.STROKE);
// Draw the diagram box
path.moveTo(0.0f, height);
path.lineTo(0.9f * width, height);
path.lineTo(0.9f * width, 0.1f * height);
path.lineTo(0.0f, 0.1f * height);
path.close();
canvas.drawPath(path, paint);
}
/**
* Computes and applies the Canvas transforms required to make the color
* gamut of each color space visible in the final image.
*
* @param canvas The canvas to transform
* @param width Width in pixel of the final image
* @param height Height in pixel of the final image
* @param primaries Array of 6 floats used to avoid temporary allocations
*/
private void setTransform(@NonNull Canvas canvas, int width, int height,
@NonNull @Size(6) float[] primaries) {
RectF primariesBounds = new RectF();
for (final Pair<ColorSpace, Integer> item : mColorSpaces) {
ColorSpace colorSpace = item.first;
if (colorSpace.getModel() != Model.RGB) continue;
Rgb rgb = (Rgb) colorSpace;
getPrimaries(rgb, primaries);
primariesBounds.left = Math.min(primariesBounds.left, primaries[4]);
primariesBounds.top = Math.min(primariesBounds.top, primaries[5]);
primariesBounds.right = Math.max(primariesBounds.right, primaries[0]);
primariesBounds.bottom = Math.max(primariesBounds.bottom, primaries[3]);
}
primariesBounds.left = Math.min(0.0f, primariesBounds.left);
primariesBounds.top = Math.min(0.0f, primariesBounds.top);
primariesBounds.right = Math.max(0.9f, primariesBounds.right);
primariesBounds.bottom = Math.max(0.9f, primariesBounds.bottom);
float scaleX = 0.9f / primariesBounds.width();
float scaleY = 0.9f / primariesBounds.height();
float scale = Math.min(scaleX, scaleY);
canvas.scale(mSize / (float) NATIVE_SIZE, mSize / (float) NATIVE_SIZE);
canvas.scale(scale, scale);
canvas.translate(
(primariesBounds.width() - 0.9f) * width / 2.0f,
(primariesBounds.height() - 0.9f) * height / 2.0f);
// The spectrum extends ~0.85 vertically and ~0.65 horizontally
// We shift the canvas a little bit to get nicer margins
canvas.translate(0.05f * width, -0.05f * height);
}
// X coordinates of the spectral locus in CIE 1931
private static final float[] SPECTRUM_LOCUS_X = {
0.175596f, 0.172787f, 0.170806f, 0.170085f, 0.160343f,
0.146958f, 0.139149f, 0.133536f, 0.126688f, 0.115830f,
0.109616f, 0.099146f, 0.091310f, 0.078130f, 0.068717f,
0.054675f, 0.040763f, 0.027497f, 0.016270f, 0.008169f,
0.004876f, 0.003983f, 0.003859f, 0.004646f, 0.007988f,
0.013870f, 0.022244f, 0.027273f, 0.032820f, 0.038851f,
0.045327f, 0.052175f, 0.059323f, 0.066713f, 0.074299f,
0.089937f, 0.114155f, 0.138695f, 0.154714f, 0.192865f,
0.229607f, 0.265760f, 0.301588f, 0.337346f, 0.373083f,
0.408717f, 0.444043f, 0.478755f, 0.512467f, 0.544767f,
0.575132f, 0.602914f, 0.627018f, 0.648215f, 0.665746f,
0.680061f, 0.691487f, 0.700589f, 0.707901f, 0.714015f,
0.719017f, 0.723016f, 0.734674f, 0.717203f, 0.699732f,
0.682260f, 0.664789f, 0.647318f, 0.629847f, 0.612376f,
0.594905f, 0.577433f, 0.559962f, 0.542491f, 0.525020f,
0.507549f, 0.490077f, 0.472606f, 0.455135f, 0.437664f,
0.420193f, 0.402721f, 0.385250f, 0.367779f, 0.350308f,
0.332837f, 0.315366f, 0.297894f, 0.280423f, 0.262952f,
0.245481f, 0.228010f, 0.210538f, 0.193067f, 0.175596f
};
// Y coordinates of the spectral locus in CIE 1931
private static final float[] SPECTRUM_LOCUS_Y = {
0.005295f, 0.004800f, 0.005472f, 0.005976f, 0.014496f,
0.026643f, 0.035211f, 0.042704f, 0.053441f, 0.073601f,
0.086866f, 0.112037f, 0.132737f, 0.170464f, 0.200773f,
0.254155f, 0.317049f, 0.387997f, 0.463035f, 0.538504f,
0.587196f, 0.610526f, 0.654897f, 0.675970f, 0.715407f,
0.750246f, 0.779682f, 0.792153f, 0.802971f, 0.812059f,
0.819430f, 0.825200f, 0.829460f, 0.832306f, 0.833833f,
0.833316f, 0.826231f, 0.814796f, 0.805884f, 0.781648f,
0.754347f, 0.724342f, 0.692326f, 0.658867f, 0.624470f,
0.589626f, 0.554734f, 0.520222f, 0.486611f, 0.454454f,
0.424252f, 0.396516f, 0.372510f, 0.351413f, 0.334028f,
0.319765f, 0.308359f, 0.299317f, 0.292044f, 0.285945f,
0.280951f, 0.276964f, 0.265326f, 0.257200f, 0.249074f,
0.240948f, 0.232822f, 0.224696f, 0.216570f, 0.208444f,
0.200318f, 0.192192f, 0.184066f, 0.175940f, 0.167814f,
0.159688f, 0.151562f, 0.143436f, 0.135311f, 0.127185f,
0.119059f, 0.110933f, 0.102807f, 0.094681f, 0.086555f,
0.078429f, 0.070303f, 0.062177f, 0.054051f, 0.045925f,
0.037799f, 0.029673f, 0.021547f, 0.013421f, 0.005295f
};
// Number of subdivision of the inside of the spectral locus
private static final int CHROMATICITY_RESOLUTION = 32;
private static final double ONE_THIRD = 1.0 / 3.0;
/**
* Computes a 2D mesh representation of the CIE 1931 chromaticity
* diagram.
*
* @param width Width in pixels of the mesh
* @param height Height in pixels of the mesh
* @param vertices Array of floats that will hold the mesh vertices
* @param colors Array of floats that will hold the mesh colors
*/
private static void computeChromaticityMesh(int width, int height,
@NonNull float[] vertices, @NonNull int[] colors) {
ColorSpace colorSpace = get(Named.SRGB);
float[] color = new float[3];
int vertexIndex = 0;
int colorIndex = 0;
for (int x = 0; x < SPECTRUM_LOCUS_X.length; x++) {
int nextX = (x % (SPECTRUM_LOCUS_X.length - 1)) + 1;
float a1 = (float) Math.atan2(
SPECTRUM_LOCUS_Y[x] - ONE_THIRD,
SPECTRUM_LOCUS_X[x] - ONE_THIRD);
float a2 = (float) Math.atan2(
SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD,
SPECTRUM_LOCUS_X[nextX] - ONE_THIRD);
float radius1 = (float) Math.pow(
sqr(SPECTRUM_LOCUS_X[x] - ONE_THIRD) +
sqr(SPECTRUM_LOCUS_Y[x] - ONE_THIRD),
0.5);
float radius2 = (float) Math.pow(
sqr(SPECTRUM_LOCUS_X[nextX] - ONE_THIRD) +
sqr(SPECTRUM_LOCUS_Y[nextX] - ONE_THIRD),
0.5);
// Compute patches; each patch is a quad with a different
// color associated with each vertex
for (int c = 1; c <= CHROMATICITY_RESOLUTION; c++) {
float f1 = c / (float) CHROMATICITY_RESOLUTION;
float f2 = (c - 1) / (float) CHROMATICITY_RESOLUTION;
double cr1 = radius1 * Math.cos(a1);
double sr1 = radius1 * Math.sin(a1);
double cr2 = radius2 * Math.cos(a2);
double sr2 = radius2 * Math.sin(a2);
// Compute the XYZ coordinates of the 4 vertices of the patch
float v1x = (float) (ONE_THIRD + cr1 * f1);
float v1y = (float) (ONE_THIRD + sr1 * f1);
float v1z = 1 - v1x - v1y;
float v2x = (float) (ONE_THIRD + cr1 * f2);
float v2y = (float) (ONE_THIRD + sr1 * f2);
float v2z = 1 - v2x - v2y;
float v3x = (float) (ONE_THIRD + cr2 * f2);
float v3y = (float) (ONE_THIRD + sr2 * f2);
float v3z = 1 - v3x - v3y;
float v4x = (float) (ONE_THIRD + cr2 * f1);
float v4y = (float) (ONE_THIRD + sr2 * f1);
float v4z = 1 - v4x - v4y;
// Compute the sRGB representation of each XYZ coordinate of the patch
colors[colorIndex ] = computeColor(color, v1x, v1y, v1z, colorSpace);
colors[colorIndex + 1] = computeColor(color, v2x, v2y, v2z, colorSpace);
colors[colorIndex + 2] = computeColor(color, v3x, v3y, v3z, colorSpace);
colors[colorIndex + 3] = colors[colorIndex];
colors[colorIndex + 4] = colors[colorIndex + 2];
colors[colorIndex + 5] = computeColor(color, v4x, v4y, v4z, colorSpace);
colorIndex += 6;
// Flip the mesh upside down to match Canvas' coordinates system
vertices[vertexIndex++] = v1x * width;
vertices[vertexIndex++] = height - v1y * height;
vertices[vertexIndex++] = v2x * width;
vertices[vertexIndex++] = height - v2y * height;
vertices[vertexIndex++] = v3x * width;
vertices[vertexIndex++] = height - v3y * height;
vertices[vertexIndex++] = v1x * width;
vertices[vertexIndex++] = height - v1y * height;
vertices[vertexIndex++] = v3x * width;
vertices[vertexIndex++] = height - v3y * height;
vertices[vertexIndex++] = v4x * width;
vertices[vertexIndex++] = height - v4y * height;
}
}
}
@ColorInt
private static int computeColor(@NonNull @Size(3) float[] color,
float x, float y, float z, @NonNull ColorSpace cs) {
color[0] = x;
color[1] = y;
color[2] = z;
cs.fromXyz(color);
return 0xff000000 |
(((int) (color[0] * 255.0f) & 0xff) << 16) |
(((int) (color[1] * 255.0f) & 0xff) << 8) |
(((int) (color[2] * 255.0f) & 0xff) );
}
private static double sqr(double v) {
return v * v;
}
private static class Point {
@NonNull final ColorSpace mColorSpace;
@NonNull final float[] mRgb;
final int mColor;
Point(@NonNull ColorSpace colorSpace,
@NonNull @Size(3) float[] rgb, @ColorInt int color) {
mColorSpace = colorSpace;
mRgb = rgb;
mColor = color;
}
}
}
}