How can I programatically set Path geometry using C++/WinRT + WinUI 3 - winrt-xaml

After decades of writing MFC apps, I'm trying to learn C++/WinRT and WinUI 3. To do this, I'm working through Petzold's "Programming Windows" 6th edition, converting the code from C# to C++/WinRT. I'm in Chapter 2, working on the PathMarkupSyntaxCode example. Here's the C# code:
public MainPage()
{
this.InitializeComponent();
Path path = new Path
{
Stroke = new SolidColorBrush(Colors.Red),
StrokeThickness = 12,
StrokeLineJoin = PenLineJoin.Round,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Data = PathMarkupToGeometry(
"M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100 " +
"M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100 " +
"M 150 0 L 150 100, 200 100 " +
"M 225 0 L 225 100, 275 100 " +
"M 300 50 A 25 50 0 1 0 300 49.9")
};
(this.Content as Grid).Children.Add(path);
}
Geometry PathMarkupToGeometry(string pathMarkup)
{
string xaml =
"<Path " +
"xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
"<Path.Data>" + pathMarkup + "</Path.Data></Path>";
Path path = XamlReader.Load(xaml) as Path;
// Detach the PathGeometry from the Path
Geometry geometry = path.Data;
path.Data = null;
return geometry;
}
and here's my (non-functional) C++/WinRT code:
MainWindow::MainWindow()
{
InitializeComponent();
std::string str;
str = "M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100 ";
str += "M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100 ";
str += "M 150 0 L 150 100, 200 100 ";
str += "M 225 0 L 225 100, 275 100 ";
str += "M 300 50 A 25 50 0 1 0 300 49.9";
Path path;
path.Stroke(SolidColorBrush(Colors::Red()));
path.StrokeThickness(12);
path.StrokeLineJoin(PenLineJoin::Round);
path.HorizontalAlignment(HorizontalAlignment::Center);
path.VerticalAlignment(VerticalAlignment::Center);
path.Data(PathMarkupToGeometry(str));
Grid().Children().Append(path);
}
Geometry MainWindow::PathMarkupToGeometry(const std::string& pathMarkup)
{
std::string xaml = "<Path ";
xaml += "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>";
xaml += "<Path.Data>";
xaml += pathMarkup;
xaml += "</Path.Data></Path>";
hstring str = winrt::to_hstring(xaml);
auto tmpl = XamlReader::Load(str);
Path path(tmpl.try_as<Path>());
Geometry geometry = path.Data();
return geometry;
}
The code in the first function that sets path.Data results in an error being thrown in Microsoft.UI.Xaml.h, in the function OnLaunched in the struct produce(). The reason for the error is that the parameter 'args' is 0.
It took me several hours to get this to even compile. (Thanks to IInspectable for the reference to try_as in a comment to another question.) I'm hoping someone who knows C++/WinRT can easily see my error. Any help would be greatly appreciated.
I was hoping the above code would print HELLO on the main window. Everything seems to be going fine in the second function until tmpl gets turned into a Path. After that, I don't know how to tell if the data is being passed correctly or not.

You forgot to detach the geometry from the path, and the constructor just creates a new grid, it doesn't gets the one from current content.
The strict equivalent would be this (note it's better to use std::wstring to benefit from natural hstring conversions - Windows is all Unicode for a long time now):
using namespace winrt;
using namespace Windows::Foundation;
using namespace Microsoft::UI;
using namespace Microsoft::UI::Xaml;
using namespace Microsoft::UI::Xaml::Controls;
using namespace Microsoft::UI::Xaml::Media;
using namespace Microsoft::UI::Xaml::Markup;
using namespace Microsoft::UI::Xaml::Shapes;
...
MainWindow::MainWindow()
{
InitializeComponent();
std::wstring str;
str = L"M 0 0 L 0 100 M 0 50 L 50 50 M 50 0 L 50 100 ";
str += L"M 125 0 C 60 -10, 60 60, 125 50, 60 40, 60 110, 125 100 ";
str += L"M 150 0 L 150 100, 200 100 ";
str += L"M 225 0 L 225 100, 275 100 ";
str += L"M 300 50 A 25 50 0 1 0 300 49.9";
Path path;
path.Stroke(SolidColorBrush(Colors::Red()));
path.StrokeThickness(12);
path.StrokeLineJoin(PenLineJoin::Round);
path.HorizontalAlignment(HorizontalAlignment::Center);
path.VerticalAlignment(VerticalAlignment::Center);
path.Data(PathMarkupToGeometry(str));
Content().try_as<Grid>().Children().Append(path);
}
Geometry MainWindow::PathMarkupToGeometry(const std::wstring& pathMarkup)
{
std::wstring xaml = L"<Path ";
xaml += L"xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>";
xaml += L"<Path.Data>";
xaml += pathMarkup;
xaml += L"</Path.Data></Path>";
auto path = XamlReader::Load(xaml).try_as<Path>();
Geometry geometry = path.Data();
path.Data(nullptr);
return geometry;
}
It's generally better to use XAML, bindings, templates, controls, custom controls, etc. than to load and mangle raw XAML "manually", but I've kept your original code spirit. Also Petzold's book might lag a bit with respect to the all new WinUI3. And all this is so much easier in C# but that's another subject :-)

Related

Why doesn't my if-statements return the right numbers?

First of all, this is my first week trying out C# or any other programming language for that matter, also my first post here on Stackoverflow!
Been working on this change calculator for a while now, trying to get it to round the result to either 0 if its <.25, 0.50 if it's between .25 and .75 and 1 if it's >.75. Seems like it's ignoring my if-statements, And on top of that the result I get isn't correct either. Some calculations ends up being negative, which I can't figure out why :/
double summa0 = vara - kontant; //item - change
var extrakt = (int)summa0; //removes decimals out of summa0 = 107
var avrundsSumma = summa0 - extrakt; //<--- extracts the decimals out of summa0
if (avrundsSumma < 0.25f)
{
avrundsSumma = Math.Floor(avrundsSumma);
}
else if (avrundsSumma > 0.75f) //Runs the decimals through if-statements
{
avrundsSumma = Math.Ceiling(avrundsSumma);
}
else
{
avrundsSumma = 0.5;
} // = in this case the result should be 1
double summa = extrakt + avrundsSumma; // 107 + 1 = 108
double attBetala = kontant - summa; // 500 - 108 = 392
Since I'm very new to this it's hard to know exactly which part of the code is causing the issue. When I run the code in CMD I get a negative result from "double summa = extrakt + avrundsSumma; // 107 + 1 = 108"
So instead of 108 I get -108.
Not sure what you mean by "Hard code the values" either :o

How to normalize path data to a 0 x 0 coordinate space?

I receive arbitrary path data that is sometimes in the 10s of thousands positions.
How would i normalize it so that the top and left positions are at 0 x 0?
So for a Path that was created at 1000x1000 would be:
M 1000 1000 L 1000 1050 L 1050 1100 Z
and after normalization would be:
M 0 0 L 0 50 L 50 100 Z
Example code:
path.pathData // M 1000x1000 L 1000x1050 L 1050x1100 Z
path.normalizeToPosition(0,0);
path.pathData // M 0x0 L 0x50 L 50x100 Z
I've seen this answer and it looks close to what I want but the difference is I want the top and left most positions to be 0x0.
To put it another way I want the viewBox to always start with "0 0" and the vector path top and left to be completely inside the viewBox area no matter what the current path data is.
Another example. If the path data is:
M -1000 -1000 L -1000 -1050 L -1050 -1100 Z
The function would modify it to
M 0 0 L 0 50 L 50 100 Z
You can use the SVG DOM path API. You'll need a polyfill on Chrome though.
var path = document.getElementById("path");
var segments = path.pathSegList;
var len = segments.numberOfItems;
var newPath = ""
for (var i=0; i<len; ++i) {
var pathSeg = segments[i];
switch(pathSeg.pathSegTypeAsLetter){
case 'M':
case 'L':
newPath += `${pathSeg.pathSegTypeAsLetter} ${pathSeg.x - segments[0].x} ${pathSeg.y - segments[0].y} `
break;
case 'Z':
newPath += 'Z';
break;
}
}
console.log(newPath);
path.setAttribute("d", newPath);
<svg>
<path id="path" d="M 1000 1000 L 1000 1050 L 1050 1100 Z"/>
</svg>

svg arc path getBBox result is wrong when the coordnate is very big in Chrome

There is a path element of "M 1050000 80 A 40 40 0 1 0 1050000 40", getBBox() result width and height are: 0.0000711679458618164, 0.00007629334868397564
If move the path 10000 pixels towards left, become "M 1040000 80 A 40 40 0 1 0 1040000 40", then getBBox() result width and height are: 74.625, 79.99937438964844
The element's width and height are not changed but getBBox() give different results.
What's the reason and how to avoid this issue ? getBoundingClientRect() ?
Testing snippets:
function $$(id){return document.getElementById(id);}
var b1 = $$("p1").getBBox();
$$("r1").innerHTML=('p1: width:'+b1.width +', height:'+ b1.height);
//0.0000711679458618164, 0.00007629334868397564
var b2 = $$("p2").getBBox();
$$("r2").innerHTML=('p2: width:'+b2.width +', height:'+ b2.height);
//74.625, 79.99937438964844
<svg width="110" height="110" viewBox="1050000 0 110 110">
<path id="p1" d="M 1050000 80 A 40 40 0 1 0 1050000 40" stroke="#880000" stroke-width="1" fill="none"></path>
</svg>
<svg width="110" height="110" viewBox="1040000 0 110 110">
<path id="p2" d="M 1040000 80 A 40 40 0 1 0 1040000 40" stroke="#880000" stroke-width="1" fill="none"></path>
</svg>
<p id="r1"></p>
<p id="r2"></p>
Has confirmed this is a bug of chromium: issue 678162. It's originally caused by the code bug of Skia engine. The fixing is committed.
Only some versions of Chrome has this bug. As my testing, Chrome 52 is ok, Chrome 55 has bug.
On my case, I just need to multiple the wrong bbox by 0x100000.
0.0000711679458618164 * 0x100000 == 74.625
0.00007629334868397564 * 0x100000 == 79.99937438964844
Finally I use a custom function to calculate bbox for chrome.
This function cannot handle the nested svg(s) or nested transforms.
function bbox (element) {
var svg = element.farthestViewportElement,
mtr = element.getScreenCTM().inverse(),
bcr = element.getBoundingClientRect(),
x1 = bcr.left, x2 = bcr.right,
y1 = bcr.top, y2 = bcr.bottom,
pts = [[x1,y1],[x1,y2],[x2,y1],[x2,y2]],
pt = svg.createSVGPoint(), pt2,
bb = {}, u;
if(x1===0 && x2===0 && y1===0 && y2===0){
return {x:0,y:0,width:0,height:0};
}
for(var i=0; i < 4; i++){
pt.x = pts[i][0];
pt.y = pts[i][1];
pt2 = pt.matrixTransform(mtr);
if(bb.xmin === u){
bb.xmin = bb.xmax = pt2.x;
bb.ymin = bb.ymax = pt2.y;
}else{
if(bb.xmin > pt2.x){
bb.xmin = pt2.x;
}else if(bb.xmax < pt2.x){
bb.xmax = pt2.x;
}
if(bb.ymin > pt2.y){
bb.ymin = pt2.y;
}else if(bb.ymax < pt2.y){
bb.ymax = pt2.y;
}
}
}
return {
x: bb.xmin,
y: bb.ymin,
width: bb.xmax - bb.xmin,
height: bb.ymax - bb.ymin
};
}

Star B-V color index to apparent RGB color

I'm trying to convert a star's B-V color index to an apparent RGB color. Besides look up tables and color ramps, it seems like there's no well known algorithm for doing this.
What's a B-V color index?
It's a number astronomers assign to a star to indicate its apparent color. Hot stars (low B-V) are blue/purple and cool stars (high B-V) are red with those white/orange stars in between.
Initial algorithm
B-V to Kelvin
var t = 4600 * ((1 / ((0.92 * bv) + 1.7)) +(1 / ((0.92 * bv) + 0.62)) );
Kelvin to xyY
If you model a star as a blackbody, then you can use a numerical approximation of the Planckian locus to compute the xy coordinates (CIE chromaticity)
// t to xyY
var x, y = 0;
if (t>=1667 && t<=4000) {
x = ((-0.2661239 * Math.pow(10,9)) / Math.pow(t,3)) + ((-0.2343580 * Math.pow(10,6)) / Math.pow(t,2)) + ((0.8776956 * Math.pow(10,3)) / t) + 0.179910;
} else if (t > 4000 && t <= 25000) {
x = ((-3.0258469 * Math.pow(10,9)) / Math.pow(t,3)) + ((2.1070379 * Math.pow(10,6)) / Math.pow(t,2)) + ((0.2226347 * Math.pow(10,3)) / t) + 0.240390;
}
if (t >= 1667 && t <= 2222) {
y = -1.1063814 * Math.pow(x,3) - 1.34811020 * Math.pow(x,2) + 2.18555832 * x - 0.20219683;
} else if (t > 2222 && t <= 4000) {
y = -0.9549476 * Math.pow(x,3) - 1.37418593 * Math.pow(x,2) + 2.09137015 * x - 0.16748867;
} else if (t > 4000 && t <= 25000) {
y = 3.0817580 * Math.pow(x,3) - 5.87338670 * Math.pow(x,2) + 3.75112997 * x - 0.37001483;
}
xyY to XYZ (Y = 1)
// xyY to XYZ, Y = 1
var Y = (y == 0)? 0 : 1;
var X = (y == 0)? 0 : (x * Y) / y;
var Z = (y == 0)? 0 : ((1 - x - y) * Y) / y;
XYZ to RGB
var r = 0.41847 * X - 0.15866 * Y - 0.082835 * Z;
var g = -0.091169 * X + 0.25243 * Y + 0.015708 * Z;
var b = 0.00092090 * X - 0.0025498 * Y + 0.17860 * Z;
Question
I ran this algorithm with the B-V color indexes: 1.2, 1.0, 0.59, 0.0, -0.29. This is what I got as output.
Why did I get this strange output? Hot stars are bluish but cold stars are brownish and there doesn't seem to be white/orange intermediate stars.
Update
Following on a comment by Ozan, it seemed like I was using a wrong matrix to convert XYZ to RGB. Since sRGB is the default color space on the web (or is it?), I'm now using the correct matrix followed by a gamma correction function (a = 0.055).
I now get this nice color ramp,
but there's still no red/violet at the extremities.
Demo
There's also a fiddle now that you can play with.
Update 2
If use a gamma of 0.5 and extend the range of B-V color indexes to be from 4.7 to -0.5, I get red at one extreme but still no violet. Here's the updated fiddle.
I use tabled interpolation instead. Some years back I found this table somewhere:
type r g b rrggbb B-V
O5(V) 155 176 255 #9bb0ff -0.32 blue
O6(V) 162 184 255 #a2b8ff
O7(V) 157 177 255 #9db1ff
O8(V) 157 177 255 #9db1ff
O9(V) 154 178 255 #9ab2ff
O9.5(V) 164 186 255 #a4baff
B0(V) 156 178 255 #9cb2ff
B0.5(V) 167 188 255 #a7bcff
B1(V) 160 182 255 #a0b6ff
B2(V) 160 180 255 #a0b4ff
B3(V) 165 185 255 #a5b9ff
B4(V) 164 184 255 #a4b8ff
B5(V) 170 191 255 #aabfff
B6(V) 172 189 255 #acbdff
B7(V) 173 191 255 #adbfff
B8(V) 177 195 255 #b1c3ff
B9(V) 181 198 255 #b5c6ff
A0(V) 185 201 255 #b9c9ff 0.00 White
A1(V) 181 199 255 #b5c7ff
A2(V) 187 203 255 #bbcbff
A3(V) 191 207 255 #bfcfff
A5(V) 202 215 255 #cad7ff
A6(V) 199 212 255 #c7d4ff
A7(V) 200 213 255 #c8d5ff
A8(V) 213 222 255 #d5deff
A9(V) 219 224 255 #dbe0ff
F0(V) 224 229 255 #e0e5ff 0.31 yellowish
F2(V) 236 239 255 #ecefff
F4(V) 224 226 255 #e0e2ff
F5(V) 248 247 255 #f8f7ff
F6(V) 244 241 255 #f4f1ff
F7(V) 246 243 255 #f6f3ff 0.50
F8(V) 255 247 252 #fff7fc
F9(V) 255 247 252 #fff7fc
G0(V) 255 248 252 #fff8fc 0.59 Yellow
G1(V) 255 247 248 #fff7f8
G2(V) 255 245 242 #fff5f2
G4(V) 255 241 229 #fff1e5
G5(V) 255 244 234 #fff4ea
G6(V) 255 244 235 #fff4eb
G7(V) 255 244 235 #fff4eb
G8(V) 255 237 222 #ffedde
G9(V) 255 239 221 #ffefdd
K0(V) 255 238 221 #ffeedd 0.82 Orange
K1(V) 255 224 188 #ffe0bc
K2(V) 255 227 196 #ffe3c4
K3(V) 255 222 195 #ffdec3
K4(V) 255 216 181 #ffd8b5
K5(V) 255 210 161 #ffd2a1
K7(V) 255 199 142 #ffc78e
K8(V) 255 209 174 #ffd1ae
M0(V) 255 195 139 #ffc38b 1.41 red
M1(V) 255 204 142 #ffcc8e
M2(V) 255 196 131 #ffc483
M3(V) 255 206 129 #ffce81
M4(V) 255 201 127 #ffc97f
M5(V) 255 204 111 #ffcc6f
M6(V) 255 195 112 #ffc370
M8(V) 255 198 109 #ffc66d 2.00
just interpolate the missing B-V indexes (linearly or better) before use
then use linear interpolation to get RGB=f(B-V);
find the closest two lines in table and interpolate between them ...
[edit1] heh just coincidentally come across this (original info I mentioned before)
[edit2] here is my approximation without any XYZ stuff
So the BV index is from < -0.4 , 2.0 >
here is mine (C++) code for conversion:
//---------------------------------------------------------------------------
void bv2rgb(double &r,double &g,double &b,double bv) // RGB <0,1> <- BV <-0.4,+2.0> [-]
{
double t; r=0.0; g=0.0; b=0.0; if (bv<-0.4) bv=-0.4; if (bv> 2.0) bv= 2.0;
if ((bv>=-0.40)&&(bv<0.00)) { t=(bv+0.40)/(0.00+0.40); r=0.61+(0.11*t)+(0.1*t*t); }
else if ((bv>= 0.00)&&(bv<0.40)) { t=(bv-0.00)/(0.40-0.00); r=0.83+(0.17*t) ; }
else if ((bv>= 0.40)&&(bv<2.10)) { t=(bv-0.40)/(2.10-0.40); r=1.00 ; }
if ((bv>=-0.40)&&(bv<0.00)) { t=(bv+0.40)/(0.00+0.40); g=0.70+(0.07*t)+(0.1*t*t); }
else if ((bv>= 0.00)&&(bv<0.40)) { t=(bv-0.00)/(0.40-0.00); g=0.87+(0.11*t) ; }
else if ((bv>= 0.40)&&(bv<1.60)) { t=(bv-0.40)/(1.60-0.40); g=0.98-(0.16*t) ; }
else if ((bv>= 1.60)&&(bv<2.00)) { t=(bv-1.60)/(2.00-1.60); g=0.82 -(0.5*t*t); }
if ((bv>=-0.40)&&(bv<0.40)) { t=(bv+0.40)/(0.40+0.40); b=1.00 ; }
else if ((bv>= 0.40)&&(bv<1.50)) { t=(bv-0.40)/(1.50-0.40); b=1.00-(0.47*t)+(0.1*t*t); }
else if ((bv>= 1.50)&&(bv<1.94)) { t=(bv-1.50)/(1.94-1.50); b=0.63 -(0.6*t*t); }
}
//---------------------------------------------------------------------------
[Notes]
This BV color is blackbody of defined temperature illumination so this represents star color viewed from space relative with the star. For visually correct colors you have to add atmospheric scattering effects of our atmosphere and Doppler effect for fast mowing stars!!! for example our Sun is 'White' but after light scatter the color varies from red (near horizon) to yellow (near nadir ... noon)
In case you want to visually correct the color these QAs might help:
Atmospheric scattering
RGB values of visible spectrum
multi spectral rendering
In case you need here are few conversion functions between temperature and BV:
//---------------------------------------------------------------------------
float bv2temp_K(float bv) // BV <-0.4,+2.0> [-] -> temp [ºK]
{
return 4600.0 * ((1.0 / ((0.92 * bv) + 1.7)) +(1.0 / ((0.92 * bv) + 0.62)) );
}
//---------------------------------------------------------------------------
float bv2temp_C(float bv) // BV <-0.4,+2.0> [-] -> temp [ºC]
{
return (-272.15) + (4600.0 * ((1.0 / ((0.92 * bv) + 1.7)) +(1.0 / ((0.92 * bv) + 0.62)) ));
}
//---------------------------------------------------------------------------
float temp_K2bv(float t) // BV <-0.4,+2.0> [-] <- temp [ºK]
{
float a,b,c,D;
a=(0.8464*t);
b=(2.1344*t)-8464.0;
c=(1.0540*t)-10672.0;
D=(b*b)-(4.0*a*c);
if (D<0.0) D=0.0; else D=sqrt(D);
return (-b+D)/(2.0*a);
// return (-b-D)/(2.0*a);
}
//---------------------------------------------------------------------------
float temp_C2bv(float t) // BV <-0.4,+2.0> [-] <- temp [ºC]
{
float a,b,c,D;
t+=-272.15;
a=(0.8464*t);
b=(2.1344*t)-8464.0;
c=(1.0540*t)-10672.0;
D=(b*b)-(4.0*a*c);
if (D<0.0) D=0.0; else D=sqrt(D);
return (-b+D)/(2.0*a);
// return (-b-D)/(2.0*a);
}
//---------------------------------------------------------------------------
And here preview of the BV=f(temp[K]) dependency (screenshot from my test app):
On top of this using approx search I managed also to obtain BV,temp from RGB color:
//---------------------------------------------------------------------------
typedef unsigned __int8 BYTE; // comment this out if you already have BYTEs
typedef unsigned __int32 DWORD; // comment this out if you already have DWORDs
const int _r=0; // channel order
const int _g=1;
const int _b=2;
const int _a=3;
union color
{
BYTE db[4]; // channel access
DWORD dd; // all 32 bit of color
// TColor c; // VCL/GDI color (you can ignore this)
};
//---------------------------------------------------------------------------
DWORD bv2rgb(float bv) // BV <-0.4,+2.0> [-] -> RGB(A) 32bit
{
color c;
float r,g,b,t;
r=0.0; g=0.0; b=0.0; if (bv<-0.3999999) bv=-0.3999999; if (bv> 2.0) bv= 2.0;
if ((bv>=-0.40)&&(bv<0.00)) { t=(bv+0.40)/(0.00+0.40); r=0.61+(0.11*t)+(0.1*t*t); }
else if ((bv>= 0.00)&&(bv<0.40)) { t=(bv-0.00)/(0.40-0.00); r=0.83+(0.17*t) ; }
else if ((bv>= 0.40)&&(bv<2.10)) { t=(bv-0.40)/(2.10-0.40); r=1.00 ; }
if ((bv>=-0.40)&&(bv<0.00)) { t=(bv+0.40)/(0.00+0.40); g=0.70+(0.07*t)+(0.1*t*t); }
else if ((bv>= 0.00)&&(bv<0.40)) { t=(bv-0.00)/(0.40-0.00); g=0.87+(0.11*t) ; }
else if ((bv>= 0.40)&&(bv<1.60)) { t=(bv-0.40)/(1.60-0.40); g=0.98-(0.16*t) ; }
else if ((bv>= 1.60)&&(bv<2.00)) { t=(bv-1.60)/(2.00-1.60); g=0.82 -(0.5*t*t); }
if ((bv>=-0.40)&&(bv<0.40)) { t=(bv+0.40)/(0.40+0.40); b=1.00 ; }
else if ((bv>= 0.40)&&(bv<1.50)) { t=(bv-0.40)/(1.50-0.40); b=1.00-(0.47*t)+(0.1*t*t); }
else if ((bv>= 1.50)&&(bv<1.94)) { t=(bv-1.50)/(1.94-1.50); b=0.63 -(0.6*t*t); }
r*=255.0; c.db[_r]=r;
g*=255.0; c.db[_g]=g;
b*=255.0; c.db[_b]=b;
c.db[_a]=0;
return c.dd;
}
//---------------------------------------------------------------------------
float rgb2bv(DWORD rgb) // BV <-0.4,+2.0> [-] <- RGB(A) 32bit
{
int d,dd;
union color c0,c1;
approx bv; double e;
c0.dd=rgb;
for (bv.init(-0.4,2.0,0.1,3,&e);!bv.done;bv.step())
{
c1.dd=bv2rgb(bv.a);
d=int(c0.db[_r])-int(c1.db[_r]); if (d<0) d=-d; dd =d;
d=int(c0.db[_g])-int(c1.db[_g]); if (d<0) d=-d; dd+=d;
d=int(c0.db[_b])-int(c1.db[_b]); if (d<0) d=-d; dd+=d;
e=dd;
}
return bv.aa;
}
//---------------------------------------------------------------------------
In case your pixelformat has different order of channels just change the _r,_g,_b,_a constants. However note that 24bpp RGB color precision leads to error up to ~300K on the edges of temp range, mid range 3169K .. 13000K has error up to ~50K so in order to gain better precision you need to use better color depth...
You asked for an algorithm, you will get one.
I researched this topic when I was rendering the data from the HYG database in Python3.5, with Pyglet and MongoDB. I'm happy with how my stars look in my starmap. The colors can be found at the bottom of this answer.
1. Color Index (B-V) to Temperature (K)
This is the function I used on the B-V (ci) data from the HYG database. In this example, ci is a B-V value from a list I'm running through.
temp = 4600 * (1 / (0.92 * ci + 1.7) + 1 / (0.92 * ci + 0.62))
2. Get a big table.
I took this one and I suggest you do too. Select the temperature column and the RGB or rgb values column as reference
3. Preprocess the data.
From the rgb table data, I generated three ordered lists (n=391) (my method: cleanup and selection with spreadsheet software and a text editor capable of having millions of cursors at a time, then imported the resulting comma-separated file by mongoDB so I could easily work with the lists of values in python through the pymongo wrapper, without too much clutter in the script file). The benefit of the method I will be laying out is that you can pluck color data from other tables that might use CMYK or HSV and adapt accordingly. You could even cross-reference. However, you should end up with lists that look like this from the (s)RGB table I suggested;
reds = [255, 255, ... , 155, 155]
greens = [56, 71, ..., 188,188]
blues = [0, 0, ..., 255, 255]
""" this temps list is also (n=391) and corresponds to the table values."""
temps = []
for i in range(1000,40100,100):
temps.append(i)
After this, I've applied some Gaussian smoothing to these lists (it helps to get better polynomials, since it gets rid of some fluctuation), after which I applied the polyfit() method (polynomial regression) from the numpy package to the temperature values with respect to the R, G and B values:
colors = [reds,greens,blues]
""" you can tweak the degree value to see if you can get better coeffs. """
def smoothListGaussian2(myarray, degree=3):
myarray = np.pad(myarray, (degree-1,degree-1), mode='edge')
window=degree*2-1
weight=np.arange(-degree+1, degree)/window
weight = np.exp(-(16*weight**2))
weight /= sum(weight)
smoothed = np.convolve(myarray, weight, mode='valid')
return smoothed
i=0
for color in colors:
color = smoothListGaussian2(color)
x = np.array(temps)
y = np.array(color)
names = ["reds","greens","blues"]
""" raise/lower the k value (third one) in c """
z = np.polyfit(x, y, 20)
f = np.poly1d(z)
#plt.plot(x,f(x),str(names[i][0]+"-"))
print("%sPoly = " % names[i], z)
i += 1
plt.show()
That gives you (n) coefficients (a) for polynomials of form:
.
Come to think of it now, you could probably use polyfit to come up with the coefficients to convert CI straight to RGB... and skip the CI to temperature conversion step, but by converting to temp first, the relation between temperature and the chosen color space is more clear.
4. The actual Algorithm: Plug temperature values into the RGB polynomials
As I said before, you can use other spectral data and other color spaces to fit polynomial curves to, this step would still be the same (with slight modifications)
Anyway, here's the simple code in full that I used (also, this is with k=20 polynomials):
import numpy as np
redco = [ 1.62098281e-82, -5.03110845e-77, 6.66758278e-72, -4.71441850e-67, 1.66429493e-62, -1.50701672e-59, -2.42533006e-53, 8.42586475e-49, 7.94816523e-45, -1.68655179e-39, 7.25404556e-35, -1.85559350e-30, 3.23793430e-26, -4.00670131e-22, 3.53445102e-18, -2.19200432e-14, 9.27939743e-11, -2.56131914e-07, 4.29917840e-04, -3.88866019e-01, 3.97307766e+02]
greenco = [ 1.21775217e-82, -3.79265302e-77, 5.04300808e-72, -3.57741292e-67, 1.26763387e-62, -1.28724846e-59, -1.84618419e-53, 6.43113038e-49, 6.05135293e-45, -1.28642374e-39, 5.52273817e-35, -1.40682723e-30, 2.43659251e-26, -2.97762151e-22, 2.57295370e-18, -1.54137817e-14, 6.14141996e-11, -1.50922703e-07, 1.90667190e-04, -1.23973583e-02,-1.33464366e+01]
blueco = [ 2.17374683e-82, -6.82574350e-77, 9.17262316e-72, -6.60390151e-67, 2.40324203e-62, -5.77694976e-59, -3.42234361e-53, 1.26662864e-48, 8.75794575e-45, -2.45089758e-39, 1.10698770e-34, -2.95752654e-30, 5.41656027e-26, -7.10396545e-22, 6.74083578e-18, -4.59335728e-14, 2.20051751e-10, -7.14068799e-07, 1.46622559e-03, -1.60740964e+00, 6.85200095e+02]
redco = np.poly1d(redco)
greenco = np.poly1d(greenco)
blueco = np.poly1d(blueco)
def temp2rgb(temp):
red = redco(temp)
green = greenco(temp)
blue = blueco(temp)
if red > 255:
red = 255
elif red < 0:
red = 0
if green > 255:
green = 255
elif green < 0:
green = 0
if blue > 255:
blue = 255
elif blue < 0:
blue = 0
color = (int(red),
int(green),
int(blue))
print(color)
return color
Oh, and some more notes and imagery...
The OBAFGKM black body temperature scale from my polynomials:
The plot for RGB [0-255] over temp [0-40000K],
+ : table data
curves : polynomial fit
A zoom-in on the least-fidelity values:
Here's the purple
As you can see, there's some deviation, but it is hardly noticeable with the naked eye and if you really want to improve on it (I don't), you have some other options:
Divide the lists where the green value is highest and see if you get better polynomials for the new left and right parts of the lists. A bit like this:
Write exception rules (maybe a simple k=2 or k=3 poly) for the values in this least-fidelity window.
Try other smoothing algorithms before you polyfit().
Try other sources or color spaces.
I'm also happy with the overall performance of my polynomials. When I'm loading the ~120000 star objects of my starmap with at minimum 18 colored vertices each, it only takes a few seconds, much to my surprise. There is room for improvement, however. For a more realistic view (instead of just running with the blackbody light radiation), I could add gravitational lensing, atmospheric effects, relativistic doppler, etc...
Oh, and the PURPLE, as promised.
Some other useful links:
My css fiddle, too lazy for jscript;
http://cssdeck.com/labs/tfwbfdzf
One of the best sites around for spectral shizzle;
http://www.handprint.com/ASTRO/specclass.html
Just in case anybody else needs to convert the handy C++ of #Spektre to python. I have taken some of the duplication out (that the compiler would no doubt have fixed) and the discontinuities for g when bv>=2.0 and b when
1.94<bv<1.9509
def bv2rgb(bv):
if bv < -0.4: bv = -0.4
if bv > 2.0: bv = 2.0
if bv >= -0.40 and bv < 0.00:
t = (bv + 0.40) / (0.00 + 0.40)
r = 0.61 + 0.11 * t + 0.1 * t * t
g = 0.70 + 0.07 * t + 0.1 * t * t
b = 1.0
elif bv >= 0.00 and bv < 0.40:
t = (bv - 0.00) / (0.40 - 0.00)
r = 0.83 + (0.17 * t)
g = 0.87 + (0.11 * t)
b = 1.0
elif bv >= 0.40 and bv < 1.60:
t = (bv - 0.40) / (1.60 - 0.40)
r = 1.0
g = 0.98 - 0.16 * t
else:
t = (bv - 1.60) / (2.00 - 1.60)
r = 1.0
g = 0.82 - 0.5 * t * t
if bv >= 0.40 and bv < 1.50:
t = (bv - 0.40) / (1.50 - 0.40)
b = 1.00 - 0.47 * t + 0.1 * t * t
elif bv >= 1.50 and bv < 1.951:
t = (bv - 1.50) / (1.94 - 1.50)
b = 0.63 - 0.6 * t * t
else:
b = 0.0
return (r, g, b)
As a correction to the code of #paddyg, which did not work for me (especially for color with bv < 0.4) : here is the exact same version of the C++ code of #Spektre, in Python :
def bv2rgb(bv):
if bv < -0.40: bv = -0.40
if bv > 2.00: bv = 2.00
r = 0.0
g = 0.0
b = 0.0
if -0.40 <= bv<0.00:
t=(bv+0.40)/(0.00+0.40)
r=0.61+(0.11*t)+(0.1*t*t)
elif 0.00 <= bv<0.40:
t=(bv-0.00)/(0.40-0.00)
r=0.83+(0.17*t)
elif 0.40 <= bv<2.10:
t=(bv-0.40)/(2.10-0.40)
r=1.00
if -0.40 <= bv<0.00:
t=(bv+0.40)/(0.00+0.40)
g=0.70+(0.07*t)+(0.1*t*t)
elif 0.00 <= bv<0.40:
t=(bv-0.00)/(0.40-0.00)
g=0.87+(0.11*t)
elif 0.40 <= bv<1.60:
t=(bv-0.40)/(1.60-0.40)
g=0.98-(0.16*t)
elif 1.60 <= bv<2.00:
t=(bv-1.60)/(2.00-1.60)
g=0.82-(0.5*t*t)
if -0.40 <= bv<0.40:
t=(bv+0.40)/(0.40+0.40)
b=1.00
elif 0.40 <= bv<1.50:
t=(bv-0.40)/(1.50-0.40)
b=1.00-(0.47*t)+(0.1*t*t)
elif 1.50 <= bv<1.94:
t=(bv-1.50)/(1.94-1.50)
b=0.63-(0.6*t*t)
return (r, g, b)
#Spektre's answer in Swift 3.0:
private func bv2ToRGB(for bv: CGFloat, logging: Bool = false) -> Color {
var bv = bv
var t: CGFloat = 0
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
if bv < -0.4 { bv = -0.4}
if bv > 2.0 { bv = 2.0}
switch bv {
case -0.4 ... 0.0:
t = (bv+0.40)/(0.00+0.40)
r = 0.61+(0.11*t)+(0.1*t*t)
case 0.0 ... 0.4:
t = (bv-0.00)/(0.40-0.00)
r = 0.83+(0.17*t)
case 0.4 ... 2.1:
t = (bv-0.40)/(2.10-0.40)
r = 1.00
default: break
}
switch bv {
case -0.4 ... 0.0:
t = (bv+0.40)/(0.00+0.40)
g = 0.70 + (0.07*t)+(0.1*t*t)
case 0.0 ... 0.4:
t = (bv-0.00)/(0.40-0.00)
g = 0.87 + (0.11*t)
case 0.4 ... 1.6:
t = (bv-0.40)/(1.60-0.40)
g = 0.98 - (0.16*t)
case 1.6 ... 2.0:
t = (bv-1.60)/(2.00-1.60)
g = 0.82 - (0.5*t*t)
default: break
}
switch bv {
case -0.4 ... 0.4:
t = (bv+0.40)/(0.40+0.40)
b = 1.0
case 0.4 ... 1.5:
t = (bv-0.40)/(1.50-0.40)
b = 1.00 - (0.47*t)+(0.1*t*t)
case 1.5 ... 1.94:
t = (bv-1.50)/(1.94-1.50)
b = 0.63 - (0.6*t*t)
default: break
}
#if os(OSX)
return NSColor(calibratedRed: r, green: g, blue: b, alpha: 1.0)
#else
return UIColor(red: r, green: g, blue: b, alpha: 1.0)
#endif
}
Why no violet or deep blue? Infinite color temperature, before being made less bluish by our atmosphere, has 1931 CIE coordinates of X=.240, y=.234.
The spectrum of a blackbody at infinite color temperature has spectral power distribution, in power per unit wavelength of bandwidth, being inversely proportional to wavelength to the 4th power. At 700nm, this is 10.7% as great as at 400nm.
In answer to the question why no violet? : I think the answer is that stars just aren't that colour. Or rather, they are not rendered that colour when we take pictures of them. The colours produced on this thread for various temperatures / B-V values seem pretty accurate to me. Take this picture I took of Albireo in Cygnus: https://www.flickr.com/photos/30974264#N02/6939409750/in/photolist-bB54th-bzdhKG
Albireo A (left) is a K type star with a B-V of 1.074 and Alberio B (right) is a B type star with a B-V of -0.06. Looking at the colours in the charts above for those B-V values, I'd say there's a pretty strong correlation with the picture.
Also, don't forget that even for very hot stars, there will still be some output at longer wavelengths, which will tend to desaturate the "blueness". Black-body radiation is broad spectrum.
Also based on the list (http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html) the following function uses kotlin to get a color for a temperature based on the 2deg scale:
fun getColorForTemp(temp: Int) = when (temp) {
in 0..1000 -> -52480
in 1000..1100 -> -52480
in 1100..1200 -> -47872
in 1200..1300 -> -44544
in 1300..1400 -> -41728
in 1400..1500 -> -39424
in 1500..1600 -> -37120
in 1600..1700 -> -35328
in 1700..1800 -> -33792
in 1800..1900 -> -32256
in 1900..2000 -> -30976
in 2000..2100 -> -29429
in 2100..2200 -> -28131
in 2200..2300 -> -26583
in 2300..2400 -> -25293
in 2400..2500 -> -24004
in 2500..2600 -> -22971
in 2600..2700 -> -21939
in 2700..2800 -> -20908
in 2800..2900 -> -19877
in 2900..3000 -> -18846
in 3000..3100 -> -18071
in 3100..3200 -> -17041
in 3200..3300 -> -16266
in 3300..3400 -> -15492
in 3400..3500 -> -14718
in 3500..3600 -> -13945
in 3600..3700 -> -13427
in 3700..3800 -> -12654
in 3800..3900 -> -12137
in 3900..4000 -> -11364
in 4000..4100 -> -10847
in 4100..4200 -> -10330
in 4200..4300 -> -9813
in 4300..4400 -> -9297
in 4400..4500 -> -8780
in 4500..4600 -> -8264
in 4600..4700 -> -7748
in 4700..4800 -> -7488
in 4800..4900 -> -6972
in 4900..5000 -> -6712
in 5000..5100 -> -6196
in 5100..5200 -> -5936
in 5200..5300 -> -5421
in 5300..5400 -> -5161
in 5400..5500 -> -4646
in 5500..5600 -> -4386
in 5600..5700 -> -4127
in 5700..5800 -> -3868
in 5800..5900 -> -3609
in 5900..6000 -> -3094
in 6000..6100 -> -2835
in 6100..6200 -> -2576
in 6200..6300 -> -2317
in 6300..6400 -> -2059
in 6400..6500 -> -1800
in 6500..6600 -> -1541
in 6600..6700 -> -1539
in 6700..6800 -> -66817
in 6800..6900 -> -198401
in 6900..7000 -> -329729
in 7000..7100 -> -526849
in 7100..7200 -> -658177
in 7200..7300 -> -789505
in 7300..7400 -> -921089
in 7400..7500 -> -1052417
in 7500..7600 -> -1118209
in 7600..7700 -> -1249537
in 7700..7800 -> -1380865
in 7800..7900 -> -1446657
in 7900..8000 -> -1578241
in 8000..8100 -> -1709569
in 8100..8200 -> -1775105
in 8200..8300 -> -1840897
in 8300..8400 -> -1972225
in 8400..8500 -> -2038017
in 8500..8600 -> -2103809
in 8600..8700 -> -2235137
in 8700..8800 -> -2300929
in 8800..8900 -> -2366721
in 8900..9000 -> -2432257
in 9000..9100 -> -2498049
in 9100..9200 -> -2563841
in 9200..9300 -> -2629633
in 9300..9400 -> -2695169
in 9400..9500 -> -2760961
in 9500..9600 -> -2826753
in 9600..9700 -> -2892289
in 9700..9800 -> -2958081
in 9800..9900 -> -3023617
in 9900..10000 -> -3089409
in 10000..10200 -> -3155201
in 10200..10300 -> -3220993
in 10300..10400 -> -3286529
in 10400..10600 -> -3352321
in 10600..10700 -> -3418113
in 10700..10800 -> -3483649
in 10800..10900 -> -3483905
in 10900..11000 -> -3549441
in 11000..11200 -> -3615233
in 11200..11300 -> -3681025
in 11300..11500 -> -3746561
in 11500..11700 -> -3812353
in 11700..11900 -> -3878145
in 11900..12000 -> -3943681
in 12000..12100 -> -3943937
in 12100..12200 -> -4009473
in 12200..12300 -> -4009729
in 12300..12500 -> -4075265
in 12500..12700 -> -4141057
in 12700..12800 -> -4206593
in 12800..12900 -> -4206849
in 12900..13200 -> -4272385
in 13200..13400 -> -4338177
in 13400..13500 -> -4403713
in 13500..13700 -> -4403969
in 13700..13900 -> -4469505
in 13900..14000 -> -4469761
in 14000..14300 -> -4535297
in 14300..14600 -> -4601089
in 14600..14700 -> -4666625
in 14700..15000 -> -4666881
in 15000..15200 -> -4732417
in 15200..15300 -> -4732673
in 15300..15700 -> -4798209
in 15700..16100 -> -4864001
in 16100..16200 -> -4929537
in 16200..16500 -> -4929793
in 16500..16800 -> -4995329
in 16800..17000 -> -4995585
in 17000..17400 -> -5061121
in 17400..17500 -> -5061377
in 17500..18000 -> -5126913
in 18000..18100 -> -5192449
in 18100..18600 -> -5192705
in 18600..18800 -> -5258241
in 18800..19200 -> -5258497
in 19200..19700 -> -5324033
in 19700..19900 -> -5324289
in 19900..20600 -> -5389825
in 20600..20700 -> -5390081
in 20700..21500 -> -5455617
in 21500..21700 -> -5521153
in 21700..22400 -> -5521409
in 22400..22800 -> -5586945
in 22800..23400 -> -5587201
in 23400..24200 -> -5652737
in 24200..24500 -> -5652993
in 24500..25700 -> -5718529
in 25700..27100 -> -5784321
in 27100..27400 -> -5849857
in 27400..28700 -> -5850113
in 28700..29500 -> -5915649
in 29500..30600 -> -5915905
in 30600..32000 -> -5981441
in 32000..32700 -> -5981697
in 32700..35000 -> -6047233
in 35000..35200 -> -6047489
in 35200..38300 -> -6113025
in 38300..38600 -> -6178561
in 38600..40000 -> -6178817
else -> -6178817
}

Resize (downsize) YUV420sp image

I am trying to resize (scale down) an image which comes in YUV420sp format. Is it possible to do such image resizing without converting it into RGB, so directly manipulating the YUV420sp pixel array? Where can I find such algorithm?
Thanks
YUV 4:2:0 planar looks like this:
----------------------
| Y | Cb|Cr |
----------------------
where:
Y = width x height pixels
Cb = Y / 4 pixels
Cr = Y / 4 pixels
Total num pixels (bytes) = width * height * 3 / 2
And the subsamling used like this:
Which means that each chroma-pixel-value is shared between 4 luma-pixels.
One approach is just to remove pixels, making sure that corresponding Y-Cb-Cr relationship are kept/recalculated.
Something close to the Nearest-neighbor interpolation but reversed.
Another approach is to first convert the 4:2:0 subsampling to 4:4:4
Here you have a 1 to 1 mapping between luma and chroma data.
This is the correct way to interpolate chroma between 4:2:0 and 4:2:2 (luma is already at correct resolution)
Code in python, follow html-link for c-dito.
Code is not very pythonic, just a direct translation of the c-version.
def __conv420to422(self, src, dst):
"""
420 to 422 - vertical 1:2 interpolation filter
Bit-exact with
http://www.mpeg.org/MPEG/video/mssg-free-mpeg-software.html
"""
w = self.width >> 1
h = self.height >> 1
for i in xrange(w):
for j in xrange(h):
j2 = j << 1
jm3 = 0 if (j<3) else j-3
jm2 = 0 if (j<2) else j-2
jm1 = 0 if (j<1) else j-1
jp1 = j+1 if (j<h-1) else h-1
jp2 = j+2 if (j<h-2) else h-1
jp3 = j+3 if (j<h-3) else h-1
pel = (3*src[i+w*jm3]
-16*src[i+w*jm2]
+67*src[i+w*jm1]
+227*src[i+w*j]
-32*src[i+w*jp1]
+7*src[i+w*jp2]+128)>>8
dst[i+w*j2] = pel if pel > 0 else 0
dst[i+w*j2] = pel if pel < 255 else 255
pel = (3*src[i+w*jp3]
-16*src[i+w*jp2]
+67*src[i+w*jp1]
+227*src[i+w*j]
-32*src[i+w*jm1]
+7*src[i+w*jm2]+128)>>8
dst[i+w*(j2+1)] = pel if pel > 0 else 0
dst[i+w*(j2+1)] = pel if pel < 255 else 255
return dst
Run this twice to get 4:4:4.
Then it's just a matter of removing rows and columns.
Or you can just quadruple the chroma-pixels to go from 4:2:0 to 4:4:4, remove rows and columns and then average 4 Cb/Cr values into 1 to get back to 4:2:0 again, it all depends on how strict you need to be :-)
Here is a Java function I use to scale down a YUV 420 (or NV21) by a factor of two.
The function takes the image in a byte array along with the width and height of the original image as an input and returns an image in a byte array which has width and heigh both equal to the half of the original width and height.
As a basis for my code I used this: Rotate an YUV byte array on Android
public static byte[] halveYUV420(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth/2 * imageHeight/2 * 3 / 2];
// halve yuma
int i = 0;
for (int y = 0; y < imageHeight; y+=2) {
for (int x = 0; x < imageWidth; x+=2) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// halve U and V color components
for (int y = 0; y < imageHeight / 2; y+=2) {
for (int x = 0; x < imageWidth; x += 4) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i++;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x + 1)];
i++;
}
}
return yuv;
}
YUV420sp has the Y in one plane and the U&V in another. If you split the U& V into separate planes, you can then perform the same scaling operation on each of the 3 planes in turn, without first having to go from 4:2:0 -> 4:4:4.
Have a look at the source code for libyuv; it just scales the planes:
https://code.google.com/p/libyuv/source/browse/trunk/source/scale.cc

Resources