Applying ICC profile to a color - colors

I have this ICC file, which converts colors from RGB to XYZ. I am making my own ICC processor.
I want to convert a value [0.5, 0.5, 0.5] to XYZ and then, to sRGB. Here is what I get:
RGB = [0.5, 0.5, 0.5]
apply "A2B0":
XYZ = [0.1263921901414079, 0.13108369512741044, 0.1081292238059337]
to sRGB using "wtpt":
RGB = [0.39734262390746755, 0.39732431073870283, 0.3972382158377442]
However, the resulting sRGB should be different (brighter). There is also a "chad" tag (3x3 matrix), but I don't know when to apply it. How should the color [0.5, 0.5, 0.5] be handled by this ICC profile?

The link to your ICC profile is dead (says file is deleted).
But also: you need to linearize any gamma or TRC on the RGB before matrixing to XYZ, and must re-apply the TRC when going to sRGB.
And you must use the 3x3 matrix in and the reverse 3x3 matrix out.
AND: if you are using v2 or v4 ICC profiles, you also must do a chromatic adaptation to (and from) D50 as that is specified as the adapted WP of the ICC XYZ PCS. sRGB is D65, but the primaries in an ICC profile would be the D50 adapted primaries.
If you are starting with sRGB #808080, the D50 XYZ values should be something like:
0.20815334 0.21586051 0.17810748
Though since I can't see the ICC profile I don't know exactly what space you are coming from.
Matrix Math
The short explanation is each output channel is the sum of the products of the associated matrix line.
Here's the example, also this is the D65 matrix not the D50 adapted matrix:
|R|0.4123907993 0.3575843394 0.1804807884| |X|
|G|0.2126390059 0.7151686788 0.0721923154| = |Y|
|B|0.0193308187 0.1191947798 0.9505321522| |Z|
Here we are going from linear sRGB to XYZ. The sRGB TRC (gamma) must be removed before applying this matrix (not shown). The first column of the matrix is R, second column G, third B. The top ROW is X, middle Y, then Z.
So multiply each column row by it's input channel, then sum each row to the output channel.
R * 0.4123907993 + G * 0.3575843394 + B * 0.1804807884 = X
R * 0.2126390059 + G * 0.7151686788 + B * 0.0721923154 = Y
R * 0.0193308187 + G * 0.1191947798 + B * 0.9505321522 = Z
XYZ space is linear (no gamma) so you need to put the TRC back onto sRGB after coming out of XYZ.

Related

Transformed colors when painting semi-transparent in p5.js

A transformation seems to be applied when painting colors in p5.js with an alpha value lower than 255:
for (const color of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
clear();
background(color);
loadPixels();
print(pixels.slice(0, 4).join(','));
}
Input/Expected Output Actual Output (Firefox)
1,2,3,255 1,2,3,255 ✅
1,2,3,4 0,0,0,4
10,11,12,13 0,0,0,13
10,20,30,40 6,19,25,40
50,100,200,40 51,102,204,40
50,100,200,0 0,0,0,0
50,100,200,1 0,0,255,1
The alpha value is preserved, but the RGB information is lost, especially on low alpha values.
This makes visualizations impossible where, for example, 2D shapes are first drawn and then the visibility in certain areas is animated by changing the alpha values.
Can these transformations be turned off or are they predictable in any way?
Update: The behavior is not specific to p5.js:
const ctx = new OffscreenCanvas(1, 1).getContext('2d');
for (const [r,g,b,a] of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
ctx.clearRect(0, 0, 1, 1);
ctx.fillStyle = `rgba(${r},${g},${b},${a/255})`;
ctx.fillRect(0, 0, 1, 1);
console.log(ctx.getImageData(0, 0, 1, 1).data.join(','));
}
I could be way off here...but it looks like internally that in the background method if _isErasing is true then blendMode is called. By default this will apply a linear interpolation of colours.
See https://github.com/processing/p5.js/blob/9cd186349cdb55c5faf28befff9c0d4a390e02ed/src/core/p5.Renderer2D.js#L45
See https://p5js.org/reference/#/p5/blendMode
BLEND - linear interpolation of colours: C = A*factor + B. This is the
default blending mode.
So, if you set the blend mode to REPLACE I think it should work.
REPLACE - the pixels entirely replace the others and don't utilize
alpha (transparency) values.
i.e.
blendMode(REPLACE);
for (const color of [[1,2,3,255],[1,2,3,4],[10,11,12,13],[10,20,30,40],[50,100,200,40],[50,100,200,0],[50,100,200,1]]) {
clear();
background(color);
loadPixels();
print(pixels.slice(0, 4).join(','));
}
Internally, the HTML Canvas stores colors in a different way that cannot preserve RGB values when fully transparent. When writing and reading pixel data, conversions take place that are lossy due to the representation by 8-bit numbers.
Take for example this row from the test above:
Input/Expected Output Actual Output
10,20,30,40 6,19,25,40
IN (conventional alpha)
R
G
B
A
values
10
20
30
40 (= 15.6%)
Interpretation: When painting, add 15.6% of (10,20,30) to the 15.6% darkened (r,g,b) background.
Canvas-internal (premultiplied alpha)
R
G
B
A
R
G
B
A
calculation
10 * 0.156
20 * 0.156
30 * 0.156
40 (= 15.6%)
values
1.56
3.12
4.7
40
values (8-bit)
1
3
4
40
Interpretation: When painting, add (1,3,4) to the 15.6% darkened (r,g,b) background.
Premultiplied alpha allows faster painting and supports additive colors, that is, adding color values without darkening the background.
OUT (conventional alpha)
R
G
B
A
calculation
1 / 0.156
3 / 0.156
4 / 0.156
40
values
6.41
19.23
25.64
40
values (8-bit)
6
19
25
40
So the results are predictable, but due to the different internal representation, the transformation cannot be turned off.
The HTML specification explicitly mentions this in section 4.12.5.1.15 Pixel manipulation:
Due to the lossy nature of converting between color spaces and converting to and from premultiplied alpha color values, pixels that have just been set using putImageData(), and are not completely opaque, might be returned to an equivalent getImageData() as different values.
see also 4.12.5.7 Premultiplied alpha and the 2D rendering context

Scale and translate path from one coordinate system to another

I have a path output as shown in the image below, in a coordinate system 1 wherein the start point and the end point are (40,40) and (10,20) respectively.
I want to scale this path to a new coordinate system (coordinate system 2) with a known start and end point, the path has to scale and adjust between the new points.
I believe Affine transforms might help / linear algebra.
How do I achieve this ? and will this be accurate or will it distort ?
To find appropriate affine tranformation (there are many ways to transform two points into two another ones, but we choose the simplest way), you can apply these elementary steps:
Shift coordinates by (-startx, -starty)
Scale along X-axis with coefficient (newendx-newstartx)/(endx-startx) (here -80/3)
Scale along Y-axis with coefficient (newendy-newstarty)/(endy-starty) (here -35)
Shift coordinates by (newstartx, newstarty)
Resulting affine tranformation is product of these four matrices
Using Wolfram alpha to get matrix
M == {{c, 0, 0},
{0, d, 0},
{a*c + e, b*d + f, 1}}
where a,b,c,d,e,f are values from decription above (a = -startx and so on)
Now transform coordinates with multiplication of point coordinates and matrix M
(x, y, 1) * M = (newx, newy, 1)

svg feGaussianBlur: correlation between stdDeviation and size

When I blur an object in Inkscape by let's say 10%, it get's a filter with a feGaussionBlur with a stdDeviation of 10% * size / 2.
However the filter has a size of 124% (it is actually that big, Inkscape doesn't add a bit just to be on the safe-side).
Where does this number come from? My guess would be 100% + 2.4 * (2*stdDeviation/size), but then where does this 2.4 come from?
From the SVG 1.1 spec:
This filter primitive performs a Gaussian blur on the input image.
The Gaussian blur kernel is an approximation of the normalized convolution:
G(x,y) = H(x)I(y)
where
H(x) = exp(-x2/ (2s2)) / sqrt(2* pis2)
and
I(y) = exp(-y2/ (2t2)) / sqrt(2 pi*t2)
with 's' being the standard deviation in the x direction and 't' being the standard deviation in the y direction, as specified by ‘stdDeviation’.
The value of ‘stdDeviation’ can be either one or two numbers. If two numbers are provided, the first number represents a standard deviation value along the x-axis of the current coordinate system and the second value represents a standard deviation in Y. If one number is provided, then that value is used for both X and Y.
Even if only one value is provided for ‘stdDeviation’, this can be implemented as a separable convolution.
For larger values of 's' (s >= 2.0), an approximation can be used: Three successive box-blurs build a piece-wise quadratic convolution kernel, which approximates the Gaussian kernel to within roughly 3%.
let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
... if d is even, two box-blurs of size 'd' (the first one centered on the pixel boundary between the output pixel and the one to the left, the second one centered on the pixel boundary between the output pixel and the one to the right) and one box blur of size 'd+1' centered on the output pixel.
Note: the approximation formula also applies correspondingly to 't'.*

Converting an RGBW color to a standard RGB/HSB representation

I am building an interface for light management in a home automation system. I managed to control standard on/off and dimmable light for various providers with little problem, but now I am stuck with a problem related to RGB light.
The light I am currently using is an RGBW led strip - specifically, I am working with a low-cost RGBW light: the light is composed by four led and every led can be controlled individually.
To be more clear - I am working on some c# code that should retrieve the currently selected color and display it in the UI, and enable the user to specify a new color for the light. For setting the color and retrieving it I must use a command provider that enables me to send and receive commands via a web service.
The provider works with RGBW colors - the four component for Red, Green, Blue and White are used. To represent the current light color on my interface I would like to translate the RGBW color returned by the service to a more standard RGB/HSB scheme.
Searching the web, the only reference for color conversion that I found (excluding a c++ sample for rgb to rgbw conversion that, based on my understanding, must have some severe bug) is this article that shows a conversion from HSI to RGBW, which is the inverse of what I needed: link here
I am searching for some insight about how I can achieve this conversion (or a simple explanation of why it isn't possible). As far as I get the conversion from RGB to an RGBW is arbitrary - a single RGB value can be represented as multiple RGBW values, but the opposite conversion should be univocal. Also note that while I am using c#, feel free to refer to algorithms in other language too - language isn't the problem, the problem is that I don't know the math to do the color conversion.
How to convert RGB to RGBW is described in item 2.1 of the following paper
http://www.mirlab.org/conference_papers/International_Conference/ICASSP%202014/papers/p1214-lee.pdf
Think of your leds as a huge pixel. Oliver's answer uses Wo = min(Ri,Gi,Bi), what is computationaly cheap and just works. This paper explains other alternatives to minimize power consumption, what is good for a home automation project. I'm also on a home automation project with OpenHAB, Arduino and RGBW leds and on the one hand paper proposed alternative would be good, on the other hand a huge LUT would just not work and convert all values on arduino eighter.
I would suggest you to try correcting RGB values using not so energy eficient technics as cited in the paper:
Assuming Ri, Gi, Bi are color inputs, integers from 0 to 255, so Q = 255 and that your outputs are Wo, Ro, Go and Bo with values from 0 to 255:
M = max(Ri,Gi,Bi)
m = min(Ri,Gi,Bi)
Wo = if (m/M < 0.5) use ( (m*M) / (M-m) ) else M
Q = 255
K = (Wo + M) / m
Ro = floor( [ ( K * Ri ) - Wo ] / Q )
Go = floor( [ ( K * Gi ) - Wo ] / Q )
Bo = floor( [ ( K * Bi ) - Wo ] / Q )
Exercise it in a spreadsheet before implementing, experiment on Wo = m, m^2 and -m^3+m^2+m, correct Wo value with the right Qw and you will be surprised that the simple solution without correction is not what one would expect and that other answers do not vary that much. Check the paper for color distortion results, I suppose that if you do not correct RGB values you will end up with some washed out colors.
I have been reviewing the answer suggested by Roberto but the colors seemed dimmer and undersaturated in many cases. Taking the example of RGB = (255,255,2) leads to RGBW = (128,128,1,2).
Digging further, it seems that the paper by Chul Lee has an error in its equation for K. The equation, which comes from a paper by Lili Wang ("Trade-off between luminance and color in RGBW displays for mobile-phone usage") is actually:
K = (Wo + M)/M
Note that it is a capital M, not a lowercase m. Given this change, you also do not need Q since it scales properly by nature. Using the new K on the same RGB = (255,255,2) example leads to a much more reasonable RGBW = (255,255,0,2).
Putting it all together:
M = max(Ri,Gi,Bi)
m = min(Ri,Gi,Bi)
Wo = if (m/M < 0.5) use ( (m*M) / (M-m) ) else M
K = (Wo + M) / M
Ro = floor[ ( K * Ri ) - Wo ]
Go = floor[ ( K * Gi ) - Wo ]
Bo = floor[ ( K * Bi ) - Wo ]
A little late, but this still amazingly seems to be a tough thing to find tooling for.
This repo can do the job for you: https://github.com/iamh2o/rgbw_colorspace_converter/
I wrote this module with a friend in such a way that 'color' objects could be instantiated via several color systems, and the object could spit out translations to all the other colorsystems it supports- which after a LOT of research (a key piece being https://www.neltnerlabs.com/saikoled/how-to-convert-from-hsi-to-rgb-white ), we finally nailed the [HSI/HSL/HSV/RGB/HEX] -> RGBW conversion.
There are a ton of packages that have the general colorspace problem solved, but it seems the RGBW case is pretty specific to physical lighting/leds, and not applicable to digital displays, RGBW was not included in any modules I'd looked at.
And the killer feature of this module is that the color objects you instantiate can be manipulated in several color systems depending on your needs (different ones that you created it in), and it will keep all of the translations to the other spaces up to date- and it's super fast, we've not yet had it be a frame rate limiting component.
So something like this would be a loop through the fully bright, fully saturated rainbow (note how the RGB vs the HSV codes are far less amenable to programatic manipulation):
from rgbw_colorspace_converter.colors.converters import RGB
color = RGB(255,0,0)
ctr = 0
while ctr < 10:
color.hsv_h += .1
print(f"HSV:{color.hsv} RGB:{color.rgb} HSI:{color.hsi} HEX:{color.hex}")
ctr += 1
# "H" in hsv is actually expressed in 360 degrees, and it is cylindrical. We've normalized it to being between 0-1 (so H=0=H=1 - both are red)
HSV:(0.0, 1.0, 1.0) RGB:(255, 0, 0) HSI:(0.0, 1.0, 0.33333) HEX:#ff0000
HSV:(0.1, 1.0, 1.0) RGB:(255, 153, 0) HSI:(36.0, 1.0, 0.533328) HEX:#ff9900
HSV:(0.2, 1.0, 1.0) RGB:(203, 255, 0) HSI:(72.23529411764707, 1.0, 0.5986868235294117) HEX:#cbff00
HSV:(0.3, 1.0, 1.0) RGB:(51, 255, 0) HSI:(108.0, 1.0, 0.399996) HEX:#33ff00
HSV:(0.4, 1.0, 1.0) RGB:(0, 255, 102) HSI:(144.0, 1.0, 0.46666199999999997) HEX:#00ff66
HSV:(0.5, 1.0, 1.0) RGB:(0, 255, 255) HSI:(180.0, 1.0, 0.66666) HEX:#00ffff
HSV:(0.6, 1.0, 1.0) RGB:(0, 102, 255) HSI:(216.0, 1.0, 0.46666199999999997) HEX:#0066ff
HSV:(0.7, 1.0, 1.0) RGB:(50, 0, 255) HSI:(251.76470588235296, 1.0, 0.39868882352941176) HEX:#3200ff
HSV:(0.8, 1.0, 1.0) RGB:(204, 0, 255) HSI:(288.0, 1.0, 0.599994) HEX:#cc00ff
HSV:(0.9, 1.0, 1.0) RGB:(255, 0, 152) HSI:(324.2352941176471, 1.0, 0.5320208235294118) HEX:#ff0098
HSV:(1.0, 1.0, 1.0) RGB:(255, 0, 0) HSI:(0.0, 1.0, 0.33333) HEX:#ff0000
I know this is a fairly old question, but I'm looking at the RGB->RGBW algorithm and found this article which - whilst not the answer - may help?
http://web.archive.org/web/20101008153429/http://www.nouvoyance.com:80/files/pdf/Adding-a-White.pdf
In this, they suggest one conversion for RGB->RGBW is simply to create the W from min(R, G, B).
R -> R
G -> G
B -> B
W -> min(R, G, B)
The reverse of this (for your scenario) would simply be to throw away the W.
R -> R
G -> G
B -> B
W -> null

Projective transformation

Given two image buffers (assume it's an array of ints of size width * height, with each element a color value), how can I map an area defined by a quadrilateral from one image buffer into the other (always square) image buffer? I'm led to understand this is called "projective transformation".
I'm also looking for a general (not language- or library-specific) way of doing this, such that it could be reasonably applied in any language without relying on "magic function X that does all the work for me".
An example: I've written a short program in Java using the Processing library (processing.org) that captures video from a camera. During an initial "calibrating" step, the captured video is output directly into a window. The user then clicks on four points to define an area of the video that will be transformed, then mapped into the square window during subsequent operation of the program. If the user were to click on the four points defining the corners of a door visible at an angle in the camera's output, then this transformation would cause the subsequent video to map the transformed image of the door to the entire area of the window, albeit somewhat distorted.
Using linear algebra is much easier than all that geometry! Plus you won't need to use sine, cosine, etc, so you can store each number as a rational fraction and get the exact numerical result if you need it.
What you want is a mapping from your old (x,y) co-ordinates to your new (x',y') co-ordinates. You can do it with matrices. You need to find the 2-by-4 projection matrix P such that P times the old coordinates equals the new co-ordinates. We'll assume that you're mapping lines to lines (not, for instance, straight lines to parabolas). Because you have a projection (parallel lines don't stay parallel) and translation (sliding), you need a factor of (xy) and (1), too. Drawn as matrices:
[x ]
[a b c d]*[y ] = [x']
[e f g h] [x*y] [y']
[1 ]
You need to know a through h so solve these equations:
a*x_0 + b*y_0 + c*x_0*y_0 + d = i_0
a*x_1 + b*y_1 + c*x_1*y_1 + d = i_1
a*x_2 + b*y_2 + c*x_2*y_2 + d = i_2
a*x_3 + b*y_3 + c*x_3*y_3 + d = i_3
e*x_0 + f*y_0 + g*x_0*y_0 + h = j_0
e*x_1 + f*y_1 + g*x_1*y_1 + h = j_1
e*x_2 + f*y_2 + g*x_2*y_2 + h = j_2
e*x_3 + f*y_3 + g*x_3*y_3 + h = j_3
Again, you can use linear algebra:
[x_0 y_0 x_0*y_0 1] [a e] [i_0 j_0]
[x_1 y_1 x_1*y_1 1] * [b f] = [i_1 j_1]
[x_2 y_2 x_2*y_2 1] [c g] [i_2 j_2]
[x_3 y_3 x_3*y_3 1] [d h] [i_3 j_3]
Plug in your corners for x_n,y_n,i_n,j_n. (Corners work best because they are far apart to decrease the error if you're picking the points from, say, user-clicks.) Take the inverse of the 4x4 matrix and multiply it by the right side of the equation. The transpose of that matrix is P. You should be able to find functions to compute a matrix inverse and multiply online.
Where you'll probably have bugs:
When computing, remember to check for division by zero. That's a sign that your matrix is not invertible. That might happen if you try to map one (x,y) co-ordinate to two different points.
If you write your own matrix math, remember that matrices are usually specified row,column (vertical,horizontal) and screen graphics are x,y (horizontal,vertical). You're bound to get something wrong the first time.
EDIT
The assumption below of the invariance of angle ratios is incorrect. Projective transformations instead preserve cross-ratios and incidence. A solution then is:
Find the point C' at the intersection of the lines defined by the segments AD and CP.
Find the point B' at the intersection of the lines defined by the segments AD and BP.
Determine the cross-ratio of B'DAC', i.e. r = (BA' * DC') / (DA * B'C').
Construct the projected line F'HEG'. The cross-ratio of these points is equal to r, i.e. r = (F'E * HG') / (HE * F'G').
F'F and G'G will intersect at the projected point Q so equating the cross-ratios and knowing the length of the side of the square you can determine the position of Q with some arithmetic gymnastics.
Hmmmm....I'll take a stab at this one. This solution relies on the assumption that ratios of angles are preserved in the transformation. See the image for guidance (sorry for the poor image quality...it's REALLY late). The algorithm only provides the mapping of a point in the quadrilateral to a point in the square. You would still need to implement dealing with multiple quad points being mapped to the same square point.
Let ABCD be a quadrilateral where A is the top-left vertex, B is the top-right vertex, C is the bottom-right vertex and D is the bottom-left vertex. The pair (xA, yA) represent the x and y coordinates of the vertex A. We are mapping points in this quadrilateral to the square EFGH whose side has length equal to m.
Compute the lengths AD, CD, AC, BD and BC:
AD = sqrt((xA-xD)^2 + (yA-yD)^2)
CD = sqrt((xC-xD)^2 + (yC-yD)^2)
AC = sqrt((xA-xC)^2 + (yA-yC)^2)
BD = sqrt((xB-xD)^2 + (yB-yD)^2)
BC = sqrt((xB-xC)^2 + (yB-yC)^2)
Let thetaD be the angle at the vertex D and thetaC be the angle at the vertex C. Compute these angles using the cosine law:
thetaD = arccos((AD^2 + CD^2 - AC^2) / (2*AD*CD))
thetaC = arccos((BC^2 + CD^2 - BD^2) / (2*BC*CD))
We map each point P in the quadrilateral to a point Q in the square. For each point P in the quadrilateral, do the following:
Find the distance DP:
DP = sqrt((xP-xD)^2 + (yP-yD)^2)
Find the distance CP:
CP = sqrt((xP-xC)^2 + (yP-yC)^2)
Find the angle thetaP1 between CD and DP:
thetaP1 = arccos((DP^2 + CD^2 - CP^2) / (2*DP*CD))
Find the angle thetaP2 between CD and CP:
thetaP2 = arccos((CP^2 + CD^2 - DP^2) / (2*CP*CD))
The ratio of thetaP1 to thetaD should be the ratio of thetaQ1 to 90. Therefore, calculate thetaQ1:
thetaQ1 = thetaP1 * 90 / thetaD
Similarly, calculate thetaQ2:
thetaQ2 = thetaP2 * 90 / thetaC
Find the distance HQ:
HQ = m * sin(thetaQ2) / sin(180-thetaQ1-thetaQ2)
Finally, the x and y position of Q relative to the bottom-left corner of EFGH is:
x = HQ * cos(thetaQ1)
y = HQ * sin(thetaQ1)
You would have to keep track of how many colour values get mapped to each point in the square so that you can calculate an average colour for each of those points.
I think what you're after is a planar homography, have a look at these lecture notes:
http://www.cs.utoronto.ca/~strider/vis-notes/tutHomography04.pdf
If you scroll down to the end you'll see an example of just what you're describing. I expect there's a function in the Intel OpenCV library which will do just this.
There is a C++ project on CodeProject that includes source for projective transformations of bitmaps. The maths are on Wikipedia here. Note that so far as i know, a projective transformation will not map any arbitrary quadrilateral onto another, but will do so for triangles, you may also want to look up skewing transforms.
If this transformation has to look good (as opposed to the way a bitmap looks if you resize it in Paint), you can't just create a formula that maps destination pixels to source pixels. Values in the destination buffer have to be based on a complex averaging of nearby source pixels or else the results will be highly pixelated.
So unless you want to get into some complex coding, use someone else's magic function, as smacl and Ian have suggested.
Here's how would do it in principle:
map the origin of A to the origin of B via a traslation vector t.
take unit vectors of A (1,0) and (0,1) and calculate how they would be mapped onto the unit vectors of B.
this gives you a transformation matrix M so that every vector a in A maps to M a + t
invert the matrix and negate the traslation vector so for every vector b in B you have the inverse mapping b -> M-1 (b - t)
once you have this transformation, for each point in the target area in B, find the corresponding in A and copy.
The advantage of this mapping is that you only calculate the points you need, i.e. you loop on the target points, not the source points. It was a widely used technique in the "demo coding" scene a few years back.

Resources