snibgo's ImageMagick pages

Colours as barycentric coordinates

Pushing colours in xyY space.

This process module barymap is designed to manipulate colours from a raw camera image before it has been encoded with an RGB profile (such as sRGB, Rec2020, or ACES2065-1). The camera may have apparently captured chromaticities that do not fit within an RGB profile, or even within the locus of observable colours. Manipulating xyY encodings before conversion to RGB may give more satisfactory final results.

When an image is recorded with red, green and blue channels, the values in those channels, normalised so their sum is 1.0, are barycentric coordinates in the triangle formed by the primaries in xyY space.

We use those barycentric coordinates to map colours from one set of primaries to a different set of primaries, optionally including white point adjustment.

For images in xyY, this provides a mechanism to convert colours from the primaries and white point of one colorspace to those of another colorspace, eg between sRGB and Rec2020. Or to shepard OOH values inside the horseshoe (aka standard-observer spectral locus).

Beware of the distinction. IM's -profile uses LCMS to convert pixel values between colourspaces, so that the colours themselves are unchanged (within the limitations of the colorspaces). This module doesn't do that. (But it might do that, as an option, in the future.) Instead, it leaves the colorspace unchanged (it is xyY), and changes colours so their relative positions within the chromaticity triangle is unchanged, while changing that triangle.

This changes chromaticities only, ie the x and y channels of xyY images. The Y channel is unchanged. The alpha channel, if any, is ignored and unchanged.

Some literature refers to "Yxy" colorspace. This is the same as xyY, with the channels in a different order.

This page originated in the pixls.us discussion Camera gamut outside horseshoe?.

See also Squishing xyY into shape, which is more automated so is simpler to control.

References

See also references in the source code barymap.c.

Conversion between cartesian and barycentric coordinates

Consider a point and a triangle. The point's barycentric coordinate from one corner of the triangle is one minus its distance from that corner divided by the distance from that corner through the point to the (extended) opposite side. So the barycentric coordinate starts at 1.0 at the corner, decreases to 0.0 as we move towards the opposite side, and is then increasingly negative as we move away from the triangle.

With this definition, the sum of the three barycentric coordinates is 1.0.

Given primaries at (x1,y1), (x2,y2) and (x3,y3), we can convert between cartesian (x,y) and barycentric (b1,b2,b3) coordinates.

Converting from cartesian to barycentric:

det = (y2-y3)*(x1-x3)+(x3-x2)*(y1-y3)

b1 = [(y2-y3)*(x-x3)+(x3-x2)*(y-y3)]/det
b2 = [(y3-y1)*(x-x3)+(x1-x3)*(y-y3)]/det
b3 = 1 - b1 - b2

We can then clamp so 0 <= b1, b2, b3 <= 1.

Converting from barycentric to cartesian:

x = b1*x1 + b2*x2 + b3*x3
y = b1*y1 + b2*y2 + b3*y3

We can then clamp so 0 <= x, y <= 1.

The two sets of equations can be used to transform the xy components of a colour defined by one set of primaries into a different colour with the same barycentric coordinates in a different set of primaries. The transformation could be expressed more directly as:

x' = a*x + b*y + c 
y' = d*x + e*y + f

... where a to f are constants, calculated from the coordinates of the six pimaries. This module doesn't do that, but always calculates indirectly, via the barycentric coordinates.

When we also care about the white point, imagine a line from the WP to each primary. Then every point within the primaries is in one of those triangles, and can be processed as above. If it is outside all three triangles, we use the triangle of primaries, ignoring the white point. The processing is messy: for each pixel we need to find which (if any) of the three triangles contains it. So for each pixel we may need to calculate barycentric coordinates three times.

Mapping triangles

The module transforms a colour with known cartesian coordinates from one triangle of primaries to another in two steps:

  1. Convert cartesian (x,y) to barycentric (b1,b2,b3) using the input triangle.
  2. Convert that barycentric (b1,b2,b3) to a new cartesian (x',y') using the output triangle.

When we ignore the white point, the process is simple: the module maps the input triangle of primaries to the output triangle of primaries. This works well for chromaticities that are inside or outside the triangle.

When we take the white point into account, the module divides the input triangle of primaries into three triangles, each with the WP and two of the primaries at the corners. It does the same with the output triangle of primaries. Then it maps each of the three input triangles to the corresponding output triangle. However, this doesn't work well for chromaticities that are outside all three triangles. For those chromaticities, it reverts to using the single large input and output triangle of primaries, ignoring the white points. This scheme avoids discontinuities.

Mapping from one triangle of primaries to a triangle of different primaries is analogous to assigning an ICC profile (not converting to that profile). However, the image starts and remains in xyY colorspace, and colours change.

Process module

The processing of each pixel by the module can be summarized:

barydiag.png

Chromatic adaptation can occur:

The effect of doTriangles is another form of chromatic adaptation, unless ignoreWP is set. The module issues a warning to stdout that chromatic adaptation may be applied twice if:

Clamping may also be applied by the code summarised here as GBP, AngMults and doTriangles.

Clamping applies only to cartesian coordinates (x,y) and barycentric coordinates (b1,b2,b3). The module does not clamp XYZ or RGB. However, if built with non-HDRI IM, outputs will be clamped to 0 to 100% of QuantumRange.

For xyY input, to put OOH back in the box:

  1. Set input primaries to values outside horseshoe.
  2. Set output primaries to values inside horseshoe.
  3. For each pixel:
    1. If currently in xyY, possibly clamp, then convert to barycentric wrt input primaries.
    2. Possibly clamp.
    3. From the input barycentric coords, use those as output barycentric coords.
    4. If required, calc output x,y, then possibly clamp.
    5. If required, convert that x,y to barycentric wrt some other primaries, then possibly clamp.

The module ignores any profiles embedded in the image.

The module reads the input image colorspace, primaries, white point and gamma, but these can be overridden by menu options (ic, ip, iw, and ig). This metadata for the output is unchanged, but can be set by menu options (oc, op, ow, and og).

Beware that some file formats do not record this metadata. Miff and tiff seem okay.

Transparency in the image has no effect.

%IM7DEV%magick xc: -process 'barymap help' xc: 
Usage: -process 'barymap [OPTION]...'
process barycentric coordinates.

  ic,  inChannels string   RGB or xyY or XYZ [xyY]
  ip,  inPrim 6_floats     xy coords of input primaries [sRGB]
  iw,  inWP 2_floats       xy coords of input white point
  it,  inTransfer number   name or gamma of input
  xw,  XYZwp 2_floats      xy coords of XYZ white point
  gn2, gain2 number        gain factor for squared distance from WP [0]
  gn,  gain number         gain factor for distance from WP [1]
  bs,  bias number         bias for distance from WP [0]
  pow, power number        power factor for distance from WP [1]
  am,  angleMult string    list of: direction,width,factor
  st,  skipTri             skip triangle processing
  ign, ignoreWP            ignore white point for triangulation
  clC, clampCartesian      clamp cartesian coordinates
  clB, clampBarycentric    clamp barycentric coordinates
  oc,  outChannels string  RGB or xyY or XYZ [xyY]
  op,  outPrim 6_floats    xy coords of output primaries [sRGB]
  ow,  outWP 2_floats      xy coords of output white point
  ot,  outTransfer number  name or gamma of output
  ca,  chromAdap string    name of Chromatic Adaptation Transform [Bradford]
  r16, round16             round some fractions to 16 binary places
  li,  list string         write list to stdout or stderr
  f,   file string         write to file stream stdout or stderr
  v,   verbose             write text information to stdout
  v2,  verbose2            write more text information
  v9,  verbose9            write voluminous text information
       version             write version information to stdout
Option Description
Short
form
Long form
ic string inChannels string Channels (colour model) of input.
String is one of RGB, xyY or XYZ.
Default: from input image.
ip string inPrim string Input primaries.
String is one of:
    a named set of primaries, eg sRGB;
    or a comma-separated list of six numbers.
Default: from input image.
iw string inWP string Input white point.
String is one of:
    a named white point, eg D60;
    a number suffixed with K or k, eg 5000K or 5000k;
    or a comma-separated list of two numbers, the xy coordinates.
Default: taken from inPrim.
it number inTransfer number Gamma of input, or named transfer function.
Default: taken from inPrim.
xw string XYZwp string xy coordinates of XYZ white point.
String is one of:
    a named white point, eg D60;
    a number suffixed with K or k, eg 5000K or 5000k;
    or a comma-separated list of two numbers, the xy coordinates.
Default: taken from inWP.
gn2 number gain2 number Gain factor for squared distance from WP.
Default: 0.0.
gn number gain number Gain factor for distance from WP.
Default: 1.0.
bs number bias number Bias added to distance from WP.
Default: 0.0.
pow number power number Power factor for distance from WP.
Default: 1.0.
am string angleMult string Input white point.
String is list of tuples separated by semi-colon ';'.
Each tuple is three numbers separated by comma ','.
The three numbers are:
    distance
    width
    factor
Default: angleMult is not used.
st skipTri Skip triangle processing.
ign ignoreWP Ignore white point for triangulation.
clC clampCartesian Clamp cartesian coordinates.
clB clampBarycentric Clamp barycentric coordinates.
oc string outChannels string Channels (colour model) of output.
String is one of RGB, xyY or XYZ.
Default: inChannels.
op string outPrim string Output primaries.
String is one of:
    a named set of primaries, eg sRGB;
    or a comma-separated list of six numbers.
Default: from inPrim.
ow string outWP string Output white point.
String is one of:
    a named white point, eg D60;
    a number suffixed with K or k, eg 5000K or 5000k;
    or a comma-separated list of two numbers, the xy coordinates.
Default: taken from outPrim.
ot number outTransfer number Gamma of output, or named transfer function.
Default: taken from outPrim.
ca string chromAdap string Name of Chromatic Adaptation Transform (CAT).
Default: Bradford.
Use list cats to see available CATs
r16 round16 Round some fractions to 16 binary places.
list string list string Write list to stderr or stdout.
String is one of list, primaries, illuminants, transfers, cats, or wpnums.
string file string Write text data to stderr or stdout.
Default = stderr.
v verbose Write some text output to stderr.
v2 verbose2 Write more text.
v9 verbose9 Write voluminous text (every pixel).
version Write version information to stdout.

When ignoreWP is specified, doTriangles will convert colours according to only the primaries, ignoring the white ponts. However, the gain-bias-power and angleMult calculations with regard to the white points, and chromatic adaptation when creating XYZ or RGB outputs, are still available.

For inPrim and outPrim, we can use a named set of primaries, or define any primaries we want by listing the x and y value of each primary, so six floating-point numbers, separated by commas, with no spaces. Generally the order should be: x and y of red primary, x and y of green primary, x and y of blue primary.

Similarly, white points inWP and outWP can be specified by name such as D50, or by giving two floating-point numbers, the x and y of the WP, or by correlated colour temperature such as 5000K or 5000k. When k is used, the number is multiplied by 1.4388/1.4380.

Similarly, the transfer function can be specified by name such as sRGB or a floating-point number such as 2.2 which gives a simple power curve.

Choosing a named set of primaries will also override the corresponding white point coordinates and tone response curve. (This is because the primaries are named after colour spaces, which usually also define the illuminant WP and TRC.) If we want to choose a different WP or TRC from the standard, we need to define that after the named primaries.

The primaries must form a triangle, ie they must not be colinear. (When they are colinear, one primary can be made by a suitable mix of the other two, so they are not really primaries, and colours can only be defined on some straight line on the chromaticity diagram.) Typically, primaries have x and y between 0.0 and 1.0, though slightly negative values are common. White point coordinates should be within the triangle primaries, typically between 0.3 and 0.4. The module permits values outside the expected ranges.

When inPrim is not specified, the default is equivalent to inPrim sRGB. When outPrim is not specified, the default is equivalent to outPrim sRGB.

To clamp xyY values to a given colorspace, specify that colorspace for both in and out, and specify clampBarycentric.

If clampCartesian is specified, the module clamps 0 <= x,y <= 1 whenever the coordinates x,y are read or calculated.

If clampBarycentric is specified, the module clamps 0 <= b1,b2,b3 <= 1 whenever those coordinates calculated. When clamping occurs, the other coordinates are adjusted to maintain the identity b1+b2+B3=1.

If specified, gain, gain2, bias and power (sometimes called "gamma, gain and bias" or "slope, offset and power") move colours towards or away from the input white point, using the formula:

v' = (v2 * gain2 + v * gain + bias)power

...where v is the distance of the colour in xyY space from the input WP, projected on the xy plane. This is the chroma of the colour which is, loosely speaking, saturation.

Default gain is 1.0, which has no effect. This multiplies distances from the WP. A larger number (eg 1.1 or 3) will increase the distance from the whitepoint, spreading all colours outwards, increasing chroma. A smaller number (eg 0.9 or 0.5) will do the opposite, decreasing chroma.

Default gain2 is 0.0, which has no effect. It is like gain, but operates on the square of the distance.

Default bias is 0.0, which has no effect. This adds to distances from the WP. A larger number (eg 0.1 or 1.1) will increase the distance from the whitepoint, spreading all colours outwards, increasing chroma. A smaller number (eg -0.1 or -0.5) will do the opposite, decreasing chroma, but any colours within that radius will be negated unless clampCartesian is used.

Default power is 1.0, which has no effect. A larger number (eg 1.1 or 3) will decrease the distance from the whitepoint, squishing most colours towards the WP, decreasing chroma, with the greatest proprtional changes occuring to colours nearest the WP. A smaller number (eg 0.9 or 0.5) will do the opposite, increasing chroma. Most colours will be within the radius of 1.0 from the white point. For any colours outside this radius, power has the opposite effect.

The order gain2, gain, bias and power are specified in the command makes no difference.

We can use a combination of these parameters to create various effects. For example, we might want to reduce the chroma of colours with high chroma. If we use bias=0 and power=1, the equation becomes:

v' = v2 * gain2 + v * gain
At a certain chroma u, we want the chroma to be unchanged, so v' = v = u, and:
1  = u * gain2 + gain
gain = 1 - u * gain2

At another chroma u2, we want to reduce the chroma to be k*u2 where 0 <= k <= 1, so v = u2 and v' = k*u2, and:

k*u2 = u22 * gain2 + u2 * gain
k = u2 * gain2 + gain

Combining these conditions:

k = u2 * gain2 + 1 - u * gain2
  = gain2 * (u2 - u) + 1

gain2 = (k - 1) / (u2 - u)

For example, if we want chroma unchanged at u=0.1, and at chroma u2=0.3 we want chroma to be 0.25 then k=0.25/0.3 = 0.8333:

gain2 = (0.8333 - 1) / (0.3 - 0.1)
      = -0.8335

gain = 1 - u * gain2
     = 1 - 0.1 * (-0.8335)
     = 1.08335

However, the value v' will reach a maximum at:

dv'/dv = 0

v' = v2 * gain2 + v * gain

dv'/dv = 2*v * gain2 + gain = 0
v = -gain / (2*gain2)

This occurs at:

v = -1.08335 / (2 * -0.8335)
  = 0.65

For input chromas greater than this limit, increasing the input chroma will decrease the output chroma.

AngMult takes a string of tuples of numbers, with no spaces. Tuples must be separated with semicolons ';' and the three numbers in each tuple must be separated with commas ','. Each tuple has three numbers: direction, width, and factor. direction and width are in degrees. Direction 0°=360° is northwards from the whitepoint, 90° is east, and so on. For each pixel xy, the module calculates the angle. If this is within direction plus or minus half the width, the distance is multiplied by factor, modulated by the angular distance from the direction. For example:

AngMult 0,15,1.3;120,60,0.8

The example has two tuples. The first affects colours in a northwards direction from the WP (roughly "green"), plus or minus 7.5°. Those colours will increase in chroma, by a maximum factor of 1.3, trailing off as the colour angle approaches +7.5° or -7.5°. The second affects colours with a hue angle of 120° (roughly "red") with a wide range, and chroma for these is decreased. Tuples are applied in the same sequence they are given.

Gain-bias-power is applied before AngMult, and both are applied to xy coordinates before conversion to barycentric coordinates.

We can list things, such as the xyz and XYZ coordinates of illuminants:

%IM7DEV%magick xc: -process 'barymap list wpnums' NULL: 
WpNums:
  name,  x,y,z  X,Y,Z
  A  0.44757, 0.40745, 0.14498,  1.0984661, 1, 0.3558228
  D50  0.34567, 0.3585, 0.29583,  0.96421199, 1, 0.82518828
  D60  0.32168, 0.33767, 0.34065,  0.95264607, 1, 1.0088252
  D65  0.31271, 0.32902, 0.35827,  0.95042855, 1, 1.0889004
  D65s  0.3127, 0.329, 0.3583,  0.95045593, 1, 1.0890578
  D75  0.29902, 0.31485, 0.38613,  0.94972209, 1, 1.2263935
  D100  0.2824, 0.2898, 0.4278,  0.97446515, 1, 1.4761905
  D200  0.258, 0.2574, 0.4846,  1.002331, 1, 1.8826729
  D300  0.2516, 0.2481, 0.5003,  1.0141072, 1, 2.0165256
  D400  0.2487, 0.2438, 0.5075,  1.0200984, 1, 2.0816243
  E  0.33333333, 0.33333333, 0.33333333,  1, 1, 1
  Aa  0.44757268, 0.40743985, 0.14498747,  1.0985, 1, 0.35585
  D50a  0.34566919, 0.35849618, 0.29583463,  0.96422, 1, 0.82521
  D55a  0.33242424, 0.34742609, 0.32014967,  0.95682, 1, 0.92149
  D65a  0.31272661, 0.32902313, 0.35825025,  0.95047, 1, 1.08883
  D75a  0.29902081, 0.31485155, 0.38612764,  0.94972, 1, 1.22638
  Ea  0.33333333, 0.33333333, 0.33333333,  1, 1, 1
  D50i  0.34570291, 0.3585386, 0.29575849,  0.9642, 1, 0.8249

By including "round16" or "r16" before "list", the numbers are rounded to 16 binary places in the fractional part:

%IM7DEV%magick xc: -process 'barymap r16 list wpnums' NULL: 
WpNums:
  name,  x,y,z  X,Y,Z
  A  0.4475708, 0.40745544, 0.14497375,  1.098465, 1, 0.3558197
  D50  0.34567261, 0.35850525, 0.2958374,  0.96421814, 1, 0.82519531
  D60  0.32168579, 0.337677, 0.34065247,  0.95265198, 1, 1.0088196
  D65  0.31271362, 0.32902527, 0.35827637,  0.95042419, 1, 1.0888977
  D65s  0.31269836, 0.32899475, 0.35830688,  0.95045471, 1, 1.0890503
  D75  0.29902649, 0.31484985, 0.38612366,  0.94972229, 1, 1.2263947
  D100  0.28239441, 0.28979492, 0.42779541,  0.97447205, 1, 1.4761963
  D200  0.25799561, 0.25740051, 0.48460388,  1.0023346, 1, 1.8826752
  D300  0.25160217, 0.24809265, 0.50030518,  1.0141144, 1, 2.0165253
  D400  0.248703, 0.24380493, 0.50750732,  1.0200958, 1, 2.0816193
  E  0.33332825, 0.33332825, 0.33332825,  1, 1, 1
  Aa  0.4475708, 0.40744019, 0.14498901,  1.0984955, 1, 0.35585022
  D50a  0.34567261, 0.35848999, 0.2958374,  0.96421814, 1, 0.82521057
  D55a  0.33242798, 0.34742737, 0.32014465,  0.95681763, 1, 0.92149353
  D65a  0.31272888, 0.32902527, 0.35824585,  0.95046997, 1, 1.0888367
  D75a  0.29902649, 0.31484985, 0.38612366,  0.94972229, 1, 1.2263794
  Ea  0.33332825, 0.33332825, 0.33332825,  1, 1, 1
  D50i  0.34570312, 0.35853577, 0.29576111,  0.96420288, 1, 0.8249054

Colour model conversions

The module can convert between colour models RGB, XYZ and xyY. ImageMagick's "-colorspace" operation can also do this, but is limited to the primaries, WP and transfer of its built-in colorspaces (which includes sRGB but not Rec2020). ImageMagick's "-profile" operation can also do this, using data from ICC profile files. The barymap module is more flexible.

RGB to XYZ

  1. Transfer curve to linearize;
  2. 3x3 matrix calculated from the RGB primaries and white point;
  3. chromatic adaptation transformation from the RGB WP to the XYZ WP.

XYZ to RGB

  1. Chromatic adaptation transformation from the XYZ WP to the RGB WP;
  2. inverse 3x3 matrix calculated from the RGB primaries and white point;
  3. transfer curve to delinearize.

XYZ to xyY, and xyY to XYZ

The module converts between XYZ and xyY using the simple formulae shown in Camera colour primaries: the CIE horseshoe.

Transfer curve

A transfer curve (aka tone reproduction curve) converts values in R, G and B channels between linear and non-linear. This is either a simple power curve, or piecewise with a linear piece and a power piece. The module applies the same transfer to all three channels.

Power transfer curve

The simple power curve is:

vnonlin = vlininvPower 

vlin = vnonlinpower 

Piecewise transfer curve

A piecewise transfer has a linear portion between zero and a limit, and a power curve beween the limit and one.

In the direction from linear to non-linear:

Where vlin <= limit:

vnonlin = slope * vlin

Where vlin > limit:

vnonlin = offsetP1 * vlininvPpower - offset

In the opposite direction, from non-linear to linear:

Where vnonlin <= limit2:

vlin = vlin / slope

Where vnonlin > limit2:

vlin = [(vlin+offset) / offsetP1] power - offset

The module takes values of power, slope, limit and offset from a built-in table, and calculates:

invPower = 1 / power

limit2 = limit / slope

offsetP1 = offset + 1.0

Ideally, the linear piece and the power piece touch each other at limit, and have the same gradient there. So out of power, slope, limit and offset, only two are independent variables, and we can calculate the other two from those. For example:

limit =  offset   
        power - 1

slope = (1+offset)power.(power-1)power-1 
            offsetpower-1.powerpower

For example, if offset=0.055 and power=2.4, then limit=0.03928571428571429 and slope = 12.92321018078786.

Test round trip

We can test the round-trip RGB→XYZ→xyY→XYZ→RGB (where the new RGB has different primaries, white point and transfer), then back to the original sRGB in a second call to barymap.

%IM7DEV%magick ^
  toes.png ^
  ( -clone 0 ^
    -process 'barymap inPrim sRGB skipTri outPrim 0.6,0.3,0.33,0.66,0.18,0.7 ow 0.31,0.32 outTransfer 2.9' ^
    -process 'barymap skipTri inTransfer 2.9 outPrim sRGB' ^
  ) ^
  -metric RMSE -format "%%[distortion]\n" -compare ^
  info: 
7.0269014e-16

IM doesn't have metadata that can fully capture the transfer function, so the inTransfer of the second process call must be set to the outTransfer of the first.

The complete round-trip is accurate to about 1 part in 248, which is the precision of the 64-bit floating point arithmetic that is used in the process module.

Compare to "-colorspace"

%IM7DEV%magick ^
  toes.png ^
  ( -clone 0 -process 'barymap iw D65a oc XYZ ow D65a' ) ^
  ( -clone 0 -colorspace XYZ ) ^
  -delete 0 ^
  -metric RMSE -format "%%[distortion]\n" -compare ^
  info: 
1.7345407e-08
%IM7DEV%magick ^
  toes.png ^
  ( -clone 0 -process 'barymap iw D65a oc xyY ow D65a' ) ^
  ( -clone 0 -colorspace xyY ) ^
  -delete 0 ^
  -metric RMSE -format "%%[distortion]\n" -compare ^
  info: 
2.4386621e-08
%IM7DEV%magick ^
  toes.png ^
  ( -clone 0 -process 'barymap it 1 iw D65a oc XYZ ow D65a' ) ^
  ( -clone 0 -set colorspace RGB -colorspace XYZ ) ^
  -delete 0 ^
  -metric RMSE -format "%%[distortion]\n" -compare ^
  info: 
3.0657862e-08

As noted in Camera colour primaries: standard illuminants, IM's matrix for RGB→XYZ seems to be accurate to just 7 decimal places.

Compare to "-profile"

We make an image with fully saturated red, green and blue pixels, and a gray pixel. We assign a Rec2020 profile, and convert that to an XYZ profile. ICC profiles always have D50 as the standard illuminant, but Rec2020 primaries are cited with D65 as the illuminant. We use barymap to chromatically adapt from D50 to D65, and write the pixel values as text numbers.

%IM7DEV%magick ^
  xc:#f00 xc:#0f0 xc:#00f xc:#888 +append +repage ^
  -profile %ICCPROF%\Rec2020-elle-V4-g10.icc ^
  -profile %ICCPROF%\XYZ-D50-Identity-elle-V4.icc ^
  -process 'barymap r16 ic XYZ oc XYZ iw D50i ow D65s skipTri f stdout v' ^
  -set colorspace XYZ -colorspace xyY ^
  +depth txt: 
# ImageMagick pixel enumeration: 4,1,4294967295,xyy
0,0: (3.0408707e+09,1.2541262e+09,1.1282849e+09)  #B540011D4AC0726F43404319  xyy(180.54201,74.459748,66.988323)
1,0: (7.3010566e+08,3.4231329e+09,2.912042e+09)  #2B848727CC08DCBBAD923C54  xyy(43.347698,203.23761,172.89322)
2,0: (5.6261978e+08,1.9756077e+08,2.5469374e+08)  #2188E5780BC689BB0F2E516F  xyy(33.403757,11.729541,15.12163)
3,0: (1.3430188e+09,1.4130473e+09,2.290678e+09)  #500CD72F543963C98888F8B5  xyy(79.73746,83.895181,136.00171)

Compare these results to the Rec2020 colorimetry according to Wikipedia:

x y
Red 0.708 0.292
Green 0.170 0.797
Blue 0.131 0.046
White point 0.3127 0.3290

The numbers agree to four decimal places, which is all we can expect from ICC conversions.

For another test, we take an ordinary photo, assign an ICC sRGB profile, then convert it to another profile using two methods: -profile and barymap. Then we compare the two results.

To do this, barymap needs to know the parameters of the sRGB ICC profile (primaries, white point and transfer). See Colorspace gamut. Blah.

Verbose output

Verbose text output is sent to stderr.

%IM7DEV%magick ^
  xc: ^
  -process 'barymap verbose' ^
  NULL: 2>cbc_verb.lis
barymap options:  chromAdap Bradford  verbose
In:
  inChannels xyY 
  inPrim 0.64,0.33,0.3,0.6,0.15,0.06
  inWP 0.3127,0.329
  inTransfer sRGB
  XYZwp 0.3127,0.329
Out:
  outChannels xyY 
  outPrim 0.64,0.33,0.3,0.6,0.15,0.06
  outWP 0.3127,0.329
  outTransfer sRGB
  XYZwp 0.3127,0.329
nAngMults 0

In the following examples, we write verbose output to a file that will be read by a script drPrimWp.bat that draws circles at the primaries and white points.

verbose2 also writes the 3x3 matrices used for conversions.

verbose9 also writes data about each pixel.

Examples

We take a raw image from a camera, convert it to XYZ with dcraw, and from there to xyY.

set SRCRAW=%PICTLIB%20171029\AGA_3443.NEF

%DCRAW% -v -4 -w -W -o 5 -T -O cbc_src.tiff %SRCRAW%

%IM7DEV%magick ^
  cbc_src.tiff ^
  -strip ^
  -set colorspace XYZ ^
  -colorspace xyY ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_xyy.miff

The script xyyHorse.bat takes an image encoded in xyY space, and draws it as a chart, white where any pixels have that xy, otherwise black, superimposing a red horseshoe. It also creates a small sRGB JPG version of the input xyY image. A change in position on the xy chart represents a change in colour in the sRGB image.

call %PICTBAT%xyyHorse ^
  cbc_xyy.miff cbc_xyy.png
cbc_xyy.png cbc_xyy_sm.jpg

Some pixels are outside the horseshoe.

Default options

With the default options, the module does a round-trip of cartesian to barycentric using the input primaries and WP, then barycentric to cartesian using the output primaries and WP. As the input and output primaries and WP are the same, this makes no changes.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx0.miff 2>cbc_txverb0.lis
call %PICTBAT%xyyHorse ^
  cbc_tx0.miff cbc_tx0.png

call %PICTBAT%drPrimWp ^
  cbc_tx0.png cbc_tx0.png ^
  cbc_txverb0.lis
cbc_tx0.png cbc_tx0_sm.jpg

Small red circles show input primaries and WP. Slightly larger green circles show output primaries and WP. The defaults are for sRGB, but there is no conversion so these have no significance. However, they show us that many colours are outside the sRGB triangle.

We might assume that chromaticities shown on this diagram are reasonably accurate, except that colours should not be outside the horseshoe. We assume the conversion from camera sensor values to XYZ is not accurate for this image, and that we don't know what an accurate conversion would be.

If the goal is conversion to sRGB, we need to do something to the chromaticities outside the triangle.

Conversion to sRGB by dcraw

As a point of comparison, we use dcraw to create an sRGB version, then convert that to xyY.

%DCRAW% -v -6 -w -W -o 0 -T -O cbc_dcr.tiff %SRCRAW%

%IM7DEV%magick cbc_dcr.tiff -colorspace xyY cbc_dcr.miff
call %PICTBAT%xyyHorse ^
  cbc_dcr.miff cbc_dcr.png

call %PICTBAT%drPrimWp ^
  cbc_dcr.png cbc_dcr.png ^
  cbc_txverb.lis
cbc_dcr.png cbc_dcr_sm.jpg

We can see that:

Map to new primaries

We push the colours that are beyond blue into the horseshoe. This shifts all colours towards the green and red primaries.

The common colours form an approximate triangle. From the Default options section above, we manually choose coordinates on the locus near the corners of the triangle, and we want to push blue chromaticities inwards, so for the input blue primary we choose coordinates outside the locus, at the extreme of the used chromaticities.

set INPRIM=0.56,0.44,0.35,0.65,0.08,0.06
set OUTPRIM=0.56,0.44,0.35,0.65,0.12,0.07
%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap ign inPrim %INPRIM% outPrim %OUTPRIM% v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx1.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx1.miff cbc_tx1.png

call %PICTBAT%drPrimWp ^
  cbc_tx1.png cbc_tx1.png ^
  cbc_txverb.lis
cbc_tx1.png cbc_tx1_sm.jpg

The circles show we are moving the blue primary towards the WP.

Map to new primaries and WP

As the previous example, but we don't ignore the white point. So this won't shift all the colours: any colours within the WP-green-red triangle will be unchanged. Other colours will be pushed towards the green-WP line, or the red-WP line.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% outPrim %OUTPRIM% v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx2.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx2.miff cbc_tx2.png

call %PICTBAT%drPrimWp ^
  cbc_tx2.png cbc_tx2.png ^
  cbc_txverb.lis
cbc_tx2.png cbc_tx2_sm.jpg

I consider this a satisfactory transformation that makes all colours fit inside the horseshoe. If that is the goal, then we have achieved it.

Map to new primaries, and clamp

As the previous example, but we also clamp to the triangles.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% outPrim %OUTPRIM% clB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx2a.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx2a.miff cbc_tx2a.png

call %PICTBAT%drPrimWp ^
  cbc_tx2a.png cbc_tx2a.png ^
  cbc_txverb.lis
cbc_tx2a.png cbc_tx2a_sm.jpg

Clamping to the triangle has caused many chromaticities that were outside the triangle to be mapped to the same chromaticities on the edge of the triangle, which loses detail.

Map to sRGB

We map the input chromaticities triangle to the triangle of the sRGB primaries.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx3.miff  2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx3.miff cbc_tx3.png

call %PICTBAT%drPrimWp ^
  cbc_tx3.png cbc_tx3.png ^
  cbc_txverb.lis
cbc_tx3.png cbc_tx3_sm.jpg

The green circles (the output primaries) no longer coincide with the red circles (the input primaries). In particular, the red primary has shifted heavily, creating a reddish tint. Chromaticities that were outside the input triangle will also be outside the output triangle. This direct mapping to sRGB has greatly changed xy values, so is probably not satisfactory. But it is interesting, so we will stick with it for now.

Tweak power, map to sRGB

As the previous example, but we also use a power function to decrease chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% pow 1.2 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx4.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx4.miff cbc_tx4.png

call %PICTBAT%drPrimWp ^
  cbc_tx4.png cbc_tx4.png ^
  cbc_txverb.lis
cbc_tx4.png cbc_tx4_sm.jpg

Tweak power, map to sRGB

Just for fun, we go in the opposite direction, using a power function to increase chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% pow 0.8 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx5.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx5.miff cbc_tx5.png

call %PICTBAT%drPrimWp ^
  cbc_tx5.png cbc_tx5.png ^
  cbc_txverb.lis
cbc_tx5.png cbc_tx5_sm.jpg

Tweak gain, map to sRGB

Instead of a power functiom, we use a gain function to decrease chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain 0.8 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx6.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx6.miff cbc_tx6.png

call %PICTBAT%drPrimWp ^
  cbc_tx6.png cbc_tx6.png ^
  cbc_txverb.lis
cbc_tx6.png cbc_tx6_sm.jpg

Tweak gain2, map to sRGB

We use a gain2 function to decrease chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain2 -0.9 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx6a.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx6a.miff cbc_tx6a.png

call %PICTBAT%drPrimWp ^
  cbc_tx6a.png cbc_tx6a.png ^
  cbc_txverb.lis
cbc_tx6a.png cbc_tx6a_sm.jpg

Tweak both gains, map to sRGB

We use a function with both gain2 and gain to decrease chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain2 -0.8335 gain 1.08335 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx6b.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx6b.miff cbc_tx6b.png

call %PICTBAT%drPrimWp ^
  cbc_tx6b.png cbc_tx6b.png ^
  cbc_txverb.lis
cbc_tx6b.png cbc_tx6b_sm.jpg

Tweak bias, map to sRGB

We use a bias function to increase chroma.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% bias 0.05 outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx7.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx7.miff cbc_tx7.png

call %PICTBAT%drPrimWp ^
  cbc_tx7.png cbc_tx7.png ^
  cbc_txverb.lis
cbc_tx7.png cbc_tx7_sm.jpg

The hole, centred on the white point, shows we have no neutral colours. The hole would be circular if the three triangles were the same shape, and we were not converting to a different set of primaries.

Two passes

We can run the module multiple times, with the output of the first used as input to the second.

Here, the first pass maps colours from INPRIM to OUTPRIM. The second pass doesn't map between triangles, but simply clamps colours to the sRGB triangle of primaries.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% outPrim %OUTPRIM% v' ^
  -process 'barymap ign inPrim sRGB clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx8.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx8.miff cbc_tx8.png

call %PICTBAT%drPrimWp ^
  cbc_tx8.png cbc_tx8.png ^
  cbc_txverb.lis
cbc_tx8.png cbc_tx8_sm.jpg

The white area has a sharp edge, meaning many colours have been clipped to the triangle.

There were two passes of barymap, so we have two sets of circles. The first set is the smallest, the second set is larger, and so on. Within each set, red marks the input, and green marks the output.

Two passes, shifting white point

In the second pass, we use sRGB primaries for input and output, but shift the white point. As in the previous example, we clamp to the sRGB tiangle.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% outPrim %OUTPRIM% v' ^
  -process 'barymap inPrim sRGB clampBarycentric outPrim sRGB outWP 0.34567,0.3 v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx8a.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx8a.miff cbc_tx8a.png

call %PICTBAT%drPrimWp ^
  cbc_tx8a.png cbc_tx8a.png ^
  cbc_txverb.lis
cbc_tx8a.png cbc_tx8a_sm.jpg

This has slightly reduced clipping, but the image has become more red, most noticably in the sky.

Using gain to reduce clipping

This is as Two passes, but applying a gain in the first pass to reduce clipping.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain 0.85 outPrim %OUTPRIM% v' ^
  -process 'barymap ign inPrim sRGB clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx9.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx9.miff cbc_tx9.png

call %PICTBAT%drPrimWp ^
  cbc_tx9.png cbc_tx9.png ^
  cbc_txverb.lis
cbc_tx9.png cbc_tx9_sm.jpg

Some colours have been clipped to the triangle, but this doesn't seem excessive. We can reduce the clipping near blue by tweaking the output blue primary.

Adjust blue primary

This is as the previous example, but adjusting the output blue primary to be the sRGB coordinates.

set OUTPRIM2=0.56,0.44,0.35,0.65,0.15,0.06

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain 0.85 outPrim %OUTPRIM2% v' ^
  -process 'barymap ign inPrim sRGB clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx10.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx10.miff cbc_tx10.png

call %PICTBAT%drPrimWp ^
  cbc_tx10.png cbc_tx10.png ^
  cbc_txverb.lis
cbc_tx10.png cbc_tx10_sm.jpg

This has pushed colours into purple, which make the sky less realistic. I prefer the previous version. But we can reduce the chroma of purples.

Reduce purple with AngMult.

This is as the previous example, but reducing the chroma of the purples. As we see the diagram after the final pass, setting the parameters is most easily done in that pass.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain 0.8 outPrim %OUTPRIM2% v' ^
  -process 'barymap ign inPrim sRGB am 180,120,0.7 clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_tx11.miff 2>cbc_txverb.lis
call %PICTBAT%xyyHorse ^
  cbc_tx11.miff cbc_tx11.png

call %PICTBAT%drPrimWp ^
  cbc_tx11.png cbc_tx11.png ^
  cbc_txverb.lis
cbc_tx11.png cbc_tx11_sm.jpg

The purples are less saturated.

Blending transformations

We might decide we like the results near the blue primary from Using gain to reduce clipping image cbc_tx9.miff but the green and red primaries from Tweak both gains, map to sRGB image cbc_tx6b.miff blah.

We can composite one xyY image over the other with a mask. We make the mask from one of the xyY versions of the input image, cbc_xyy.miff. The mask is black at the blue primary, fading to white at the green primary. The Y channel is not relevant for making the mask, so we make it zero. Which blue primary? The final one used for cbc_tx9.miff, which is the sRGB blue primary, which is (x,y)=(0.15,0.06).

The difference is 0.0 to 1.0, with the maximum at the opposite corner of the colour cube. but the Y channel doesn't vary so the maximum is sqrt(2)/sqrt(3) which is 0.8265. The distance between the blue and green primaries is hypot (0.15-0.3, 0.06-0.6)/sqrt(3) which is 0.3236. (The distance between the blue and red primaries is similar.)

After compositing the two xyY images with the mask, we use barymap to clamp the chromaticities (xy values) to the sRGB triangle.

%IM7DEV%magick ^
  cbc_xyy.miff ^
  -set colorspace sRGB ^
  -channel B -evaluate set 0 +channel ^
  ( +clone ^
    -fill RGB(15%%,6%%,0) -colorize 100 ^
  ) ^
  -compose Difference -composite ^
  -grayscale RMS ^
  -level 0,32.36%% ^
  -set colorspace sRGB ^
  -write mpr:MASK ^
  +delete ^
  cbc_tx9.miff ^
  cbc_tx6b.miff ^
  mpr:MASK ^
  -alpha off ^
  -compose Over -composite ^
  -process 'barymap ign inPrim sRGB clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_blnd.miff 2>cbc_blnd.lis
call %PICTBAT%xyyHorse ^
  cbc_blnd.miff cbc_blnd.png

call %PICTBAT%drPrimWp ^
  cbc_blnd.png cbc_blnd.png ^
  cbc_blnd.lis
cbc_blnd.png cbc_blnd_sm.jpg

Making and using HALD clut

When we are happy with a transformation, we can create a hald clut (aka 3D clut) from that transformation, and directly use that clut on any image encoded in xyY space. The hald:16 is an image with all 8-bit values in all channels, ie a 256*256*256 cube.

First, make the hald clut:

%IM7DEV%magick ^
  hald:16 ^
  -set colorspace xyY ^
  -process 'barymap inPrim %INPRIM% gain 0.85 outPrim %OUTPRIM% v' ^
  -process 'barymap ign inPrim sRGB clampBarycentric outPrim sRGB v' ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  cbc_hald.miff

Apply the hald clut:

%IM7DEV%magick ^
  cbc_xyy.miff ^
  cbc_hald.miff ^
  -set colorspace xyY ^
  -hald-clut ^
  -depth 32 ^
  -define quantum:format=floating-point ^
  -alpha off ^
  cbc_txh1.miff

Note: this needs "-alpha off", otherwise xyyHorse.bat can't read it.

We are using xyY as the three input and output channels for the hald clut, but the clut doesn't change Y values. We only need a 2D clut, but IM doesn't have that facility.

call %PICTBAT%xyyHorse ^
  cbc_txh1.miff cbc_txh1.png
cbc_txh1.png cbc_txh1_sm.jpg

Performance

The module makes only one pass over the image. The processing per pixel can be complex, depending on the options chosen:

  1. Gain-bias-power takes extra time.
  2. AngleMult takes extra time.
  3. Clamping takes extra time.
  4. IgnoreWP skips the processing to find which sub-triangle contains the colour, so this saves time.

Cleanup

We have created large files we no longer need, so delete them.

del cbc_src.tiff
rem del cbc_*.miff

Future

Gain, bias and power could be automatically chosen.

Software could analyze a xyY histogram to determine bounding primaries. For example, to find the green primary, walk down the rows until no more than X% of pixels are outside the triangle. This would assist in answering the question, "Which colorspace best encodes this image?"

The module provides almost infinite possibilities. In practice, a small number of presets might be designed for common circumstances. An automated system might choose the most appropriate of these according to statistics gathered from the image.

The gain-bias-power options currently shift chroma (the distance of a colour from the white point) the same amount in all directions. This could be split to operate in x and y directions independently.

Metrics might be useful, eg:

As always, we need to decide on trade-offs: accuracy versus clipping versus "looks good".

This process might be better if performed in a more perceptually uniform colorspace, eg on uv channels of Luv.

Scripts

For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.

xyyHorse.bat

rem %1 input image in xyY space.
rem %2 output black/white histogram with horseshoe.
rem Also creates small sRGB version of input image.

%IM7DEV%magick ^
  %1 ^
  -strip ^
  ( +clone ^
    -set colorspace xyY ^
    -resize 512x512 ^
    -colorspace sRGB ^
    +write %~n1_sm.jpg ^
    +delete ^
  ) ^
  -process 'plotrg dim 512 norm verbose' ^
  -flip ^
  -fill White +opaque Black ^
  -set colorspace sRGB ^
  %2

if ERRORLEVEL 1 exit /B 1

call %PICTBAT%cieHorseshoe %2 %2

if ERRORLEVEL 1 exit /B 1

drPrimWp.bat

rem %1 is input chromaticity diagram.
rem %2 is output, with primaries and white point shown.
rem %3 is file of text verbose output from process module barymap.
@rem
@rem This picks up every inPrim etc,
@rem and draws them in different sizes.

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

set INFILE=%1
set OUTFILE=%2
set BARYLIS=%3

set inPrim=
set inWP=
set outPrim=
set outWP=

set NinPrim=0
set NinWP=0
set NoutPrim=0
set NoutWP=0

:: stat is 1 for "In:", 2 for "Out:", 0 for neither
set stat=0

for /F "usebackq tokens=1-2 delims=* " %%A in (%BARYLIS%) do (
  rem echo read [%%A] [%%B] [!stat!]
  if /I "%%A"=="In:" set stat=1
  if /I "%%A"=="Out:" set stat=2
  if /I "%%A"=="nAngMults" set stat=0
  if /I "%%A"=="inPrim" (
    echo inPrim %%B
    set inPrim[!NinPrim!]=%%B
    set /A NinPrim+=1
  )
  if /I "%%A"=="inWP" (
    echo inWP %%B
    set inWP[!NinWP!]=%%B
    set /A NinWP+=1
  )
  if /I "%%A"=="outPrim" (
    echo outPrim %%B
    set outPrim[!NoutPrim!]=%%B
    set /A NoutPrim+=1
  )
  if /I "%%A"=="outWP" (
    echo outWP %%B
    set outWP[!NoutWP!]=%%B
    set /A NoutWP+=1
  )
)

echo %NinPrim% %NinWP% %NoutPrim% %NoutWp%

set /A lastinPrim = NinPrim-1
set /A lastinWP   = NinWP-1
set /A lastoutPrim  = NoutPrim-1
set /A lastoutWP  = NoutWP-1

set sDRAWip=
for /L %%N in (0,1,%lastinPrim%) do (
  set prev=
  set i=0
  for %%A in (!inPrim[%%N]!) do (
    if "!prev!"=="" (
      set prev=%%A
    ) else (
      set /A rad=%%N*6+6
      echo ip !prev! and %%A !rad!
      set sDRAWip!i![%%N]=-draw "translate %%[fx:w*!prev!],%%[fx:h-h*%%A] circle 0,0,0,!rad!"
      set prev=
      set /A i+=1
    )
  )
  set sDRAWip=!sDRAWip! !sDRAWip0[%%N]! !sDRAWip1[%%N]! !sDRAWip2[%%N]!
)

set sDRAWiw=
for /L %%N in (0,1,%lastinWP%) do (
  set prev=
  for %%A in (!inWP[%%N]!) do (
    if "!prev!"=="" (
      set prev=%%A
    ) else (
      set /A rad=%%N*6+6
      echo iw !prev! and %%A !rad!
      set sDRAWiw[%%N]=-draw "translate %%[fx:w*!prev!],%%[fx:h-h*%%A] circle 0,0,0,!rad!"
      set prev=
      set /A i+=1
    )
  )
  set sDRAWiw=!sDRAWiw! !sDRAWiw[%%N]!
)

set sDRAWop=
for /L %%N in (0,1,%lastoutPrim%) do (
  set prev=
  set i=0
  for %%A in (!outPrim[%%N]!) do (
    if "!prev!"=="" (
      set prev=%%A
    ) else (
      set /A rad=%%N*6+8
      echo op !prev! and %%A  !rad!
      set sDRAWop!i![%%N]=-draw "translate %%[fx:w*!prev!],%%[fx:h-h*%%A] circle 0,0,0,!rad!"
      set prev=
      set /A i+=1
    )
  )
  set sDRAWop=!sDRAWop! !sDRAWop0[%%N]! !sDRAWop1[%%N]! !sDRAWop2[%%N]!
)

set sDRAWow=
for /L %%N in (0,1,%lastoutWP%) do (
  set prev=
  for %%A in (!outWP[%%N]!) do (
    if "!prev!"=="" (
      set prev=%%A
    ) else (
      set /A rad=%%N*6+8
      echo ow !prev! and %%A !rad!
      set sDRAWow[%%N]=-draw "translate %%[fx:w*!prev!],%%[fx:h-h*%%A] circle 0,0,0,!rad!"
      set prev=
      set /A i+=1
    )
  )
  set sDRAWow=!sDRAWow! !sDRAWow[%%N]!
)

echo ip = %sDRAWip%
echo op = %sDRAWop%

%IMG7%magick ^
  %INFILE% ^
  -fill None ^
  -stroke #f66 ^
  %sDRAWip% ^
  -stroke #a66 ^
  %sDRAWiw% ^
  -stroke #0d0 ^
  %sDRAWop% ^
  -stroke #6a6 ^
  %sDRAWow% ^
  %OUTFILE%

call echoRestore

@endlocal

All images on this page were created by the commands shown, using:

%IMG7%magick -version
Version: ImageMagick 7.1.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193331630)

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

Source file for this web page is colbarycoord.h1. To re-create this web page, execute "procH1 colbarycoord".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 5-December-2018.

Page created 30-Sep-2022 16:14:54.

Copyright © 2022 Alan Gibson.