Skip to content

Sync iccviz engine: GamutVolume, RoundTripDE, neutral-axis inking (#1711)#1712

Closed
colourbill-ctrl wants to merge 2 commits into
InternationalColorConsortium:masterfrom
colourbill-ctrl:feature/iccviz-sync-gamut-roundtrip
Closed

Sync iccviz engine: GamutVolume, RoundTripDE, neutral-axis inking (#1711)#1712
colourbill-ctrl wants to merge 2 commits into
InternationalColorConsortium:masterfrom
colourbill-ctrl:feature/iccviz-sync-gamut-roundtrip

Conversation

@colourbill-ctrl

Copy link
Copy Markdown
Contributor

Refs #1711.

Sync the data-first visualization engine under Tools/CmdLine/IccProfilePlot/
(IccVizModel, landed in #1517) up to date with three feature-additive changes,
plus the caller updates they require. No refactor of the enumerate/render
surface; no change to IccProfLib, to ChrisCoxArt's Tools/CmdLine/IccProfileVisualize/
(a different directory), or to the Mini{PDF,SVG,TIFF} writers.

Engine (IccVizModel.hpp/.cpp, IccVizMath.hpp)

  • GamutVolume() — gamut volume in dE*ab^3 via boundary voxelisation +
    flood-fill through the AToB transform; scalar GamutVolumeResult with a
    degenerate flag so a collapsed sampling is distinguishable from a genuinely
    tiny gamut. Outside the Enumerate/Render path.
  • RoundTripDE() — B2A->A2B round-trip accuracy (mean/p90/max/std dE*ab)
    seeded from in-gamut device-cube samples; scalar RoundTripResult.
  • Kind::NeutralAxisInking — device colorant along the neutral axis from a
    PCS->device (BToA) table, enumerated per BToA tag. InkReversalL retired
    (enum value reserved).
  • Plus GamutVolume accuracy work (IccProfLib CIELAB reuse) and CLUT-raster /
    new-metric hardening against malformed input.

Breaking API (internal only)

Enumerate(pIcc, Order) loses its Order argument. Enumerate(pIcc) now
returns one canonical order (chromaticity, then per-tag A2B0/1/2, B2A0/1/2,
gamut, then named/colorant). The only in-tree caller is iccProfileVisualize.cpp,
updated here. No public/library consumer outside this tool directory is affected.

iccProfileVisualize.cpp

  • Drop the Order argument at the Enumerate call site.
  • Render Kind::NeutralAxisInking into the PDF report (was engine-enumerated
    but silently skipped): CreateNeutralAxesXobject (L*/%-ink axes frame) +
    renderNeutralAxisGraph (one polyline per colorant along L* 100->0, distinct
    CMYK stroke, channel-name legend) + the processLuts switch case. One page
    per PCS->device table.

Notes for review

  • The engine sources are algorithmically identical to the reference copy;
    the only deltas are the restored ICC license header on IccVizModel.cpp and
    an ASCII punctuation sweep (matching the Add IccProfilePlot: data-first profile visualization API + CLI #1517 strict-build convention), which
    changes five graph-title string literals from an em-dash to a hyphen. Nothing
    else in the rendered output differs from the engine logic.
  • The prior "byte-identical vs original iccProfileVisualize" PDF baseline is
    intentionally superseded by the canonical page order and the chromaticity
    page now drawn for device/CMYK profiles (design decisions on Bring GamutVolume / RoundTripDE / neutral-axis inking to the IccProfilePlot iccviz engine #1711). CLUT
    raster data stayed byte-identical old-vs-new across the trial corpus.
  • All translation units build clean under -Wall -Wextra.
  • Follows as a separate in-repo PR (per the maintainer-owned CTest policy):
    CTests for the two new scalar metrics + a pinned-output PDF/TIFF regression to
    replace the retired baseline.

@xsscx xsscx self-assigned this Jul 17, 2026
@xsscx xsscx closed this Jul 17, 2026
@xsscx xsscx reopened this Jul 17, 2026
@github-actions github-actions Bot added Tools Command-line tool or GUI tool changes Source C or C++ source code changes labels Jul 17, 2026
@xsscx

xsscx commented Jul 17, 2026

Copy link
Copy Markdown
Member

2026-07-17 23:19:00 UTC

Attempting to Clear the Stale API Data

@xsscx xsscx added this to the v2.3.2.3 milestone Jul 17, 2026
@xsscx
xsscx removed their request for review July 17, 2026 23:20
@xsscx
xsscx requested a review from Copilot July 17, 2026 23:20
@xsscx xsscx added the Copilot Copilot use indicated by Maintainer label Jul 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds gamut-volume and round-trip metrics, neutral-axis inking visualization, and canonical enumeration.

Changes:

  • Adds scalar gamut and round-trip analysis APIs.
  • Adds neutral-axis graph generation and PDF rendering.
  • Hardens CLUT raster processing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
IccVizModel.hpp Extends the visualization API.
IccVizModel.cpp Implements metrics, graph generation, and hardening.
IccVizMath.hpp Clarifies math-helper documentation.
iccProfileVisualize.cpp Renders neutral-axis PDF pages.

Comment on lines +1036 to +1037
CIccXform* xform = CIccXform::Create(pIcc, tag, /*bInput=*/false,
neutralIntentForSig(sig), icInterpLinear);
Comment on lines +1084 to +1085
if (CIccXform* fwd = CIccXform::Create(pIcc, fwdTag, /*bInput=*/true,
icRelativeColorimetric, icInterpLinear)) {
if (!pTag) return fail("AToB tag not present");

// Device->PCS transform for this tag (bInput=true = A2B / "input" side).
CIccXform* x = CIccXform::Create(pIcc, pTag, /*bInput=*/true, intent, icInterpLinear);
Comment on lines +1658 to +1659
CIccXform* xA = CIccXform::Create(pIcc, a2bTag, /*bInput=*/true, intent, icInterpLinear);
CIccXform* xB = CIccXform::Create(pIcc, b2aTag, /*bInput=*/false, intent, icInterpLinear);
double boundarySampleCount(int N, int S) {
if (N < 1) N = 1;
const int e = (N >= 2) ? (N - 1) : 0; // free axes per facet
return 2.0 * N * std::pow((double)(S + 1), (double)e);

int S = samplesPerAxis > 0 ? samplesPerAxis : roundTripSteps(N);
if (S < 2) S = 2;
double total = std::pow((double)(S + 1), N);
// enclosed region is at/below the voxel-resolution floor (collapsed toward a
// point/plane). Lets a caller show N/A instead of a misleading tiny number.
const int finitePts = (int)(lab.size() / 3);
r.degenerate = (finitePts * 2 < nPts) || (cells <= 27);
const point2D rangeX( right-2*margin, 0.0f );
const point2D tickLengthX( 0, -tickLength );
const point2D fullLengthX( 0, (top-margin) - (bottom+margin) );
commands += DrawAxisPDF( basepoint, rangeX, tickLengthX, fullLengthX, 12.0f, "L* (100 to 0)" );
Comment on lines +207 to +210
// List every visualization available for the profile, in a stable canonical
// order: chromaticity first, then per-tag in tag-table order - TRC curves, then
// each LUT's A/B/M curves followed by its CLUT image, then a neutral-axis inking
// graph for each PCS->device BToA table, then named/colorant tables as a*b* then xy.
Comment on lines +1343 to +1345
// Enumerate now returns a single canonical order that already follows the
// profile's tag-table page sequence (the Order::TagTable parameter was retired
// upstream), so no ordering argument is passed.
@xsscx
xsscx self-requested a review July 17, 2026 23:27

@xsscx xsscx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can you add CTest(s)
  2. Can you Sign the Commit(s)
  3. Review & Resolve Copilot Issues

Thank You

@xsscx xsscx assigned ChrisCoxArt and unassigned xsscx Jul 18, 2026
colourbill-ctrl added a commit to colourbill-ctrl/iccDEV that referenced this pull request Jul 18, 2026
Adds the regression coverage requested in the InternationalColorConsortium#1712 review (item 1) for the
iccviz engine synced in InternationalColorConsortium#1711.

* iccviz-gamut-roundtrip-metrics: drives GamutVolume and RoundTripDE end-to-end
  on the stock sRGB v4 LUT profile -- positive 3-D gamut volume, not degenerate,
  small in-gamut round-trip dE -- and asserts a coarse-grid GamutVolume still
  flags degenerate through the public API. Compiles IccVizModel.cpp directly
  (the metrics live in tool sources, not IccProfLib).

* iccviz-degenerate-detection: header-only unit test of
  iccvizmath::principalStdDevs on synthetic blob/plane/line clouds; calibrates
  the 0.02 coplanarity threshold (s3/s1 ratio 0.01 flags flat, 0.05 does not).

* iccviz-pdf-axis-labels: runs iccProfileVisualizePlot and asserts the emitted
  LUT PDF carries both DrawAxisPDF label paths -- the default identity axis
  (Input/Output) and the parameterized custom-tick axis (L* (100 to 0) / % ink).
  A content pin (uncompressed-stream label match), not a byte-for-byte golden
  PDF, so a legitimate LUT curve-sampling shift will not false-fail it.

Registered on both the WIN32 and POSIX test paths, gated behind their targets
and labelled issue-1712. Each assertion was mutation-verified against the
shipped implementation.
@colourbill-ctrl
colourbill-ctrl force-pushed the feature/iccviz-sync-gamut-roundtrip branch from 5517ddc to 3024b91 Compare July 18, 2026 02:10
@colourbill-ctrl

Copy link
Copy Markdown
Contributor Author

Thanks @xsscx — all three addressed and force-pushed to this branch (rebased on current master):

1. CTests — added under .github/ci/regression/ + Build/Cmake/Testing/, registered on both the WIN32 and POSIX test paths and labelled issue-1712. Each assertion is mutation-verified; ctest -R iccviz = 3/3 locally.

  • iccviz-gamut-roundtrip-metrics — drives GamutVolume + RoundTripDE end-to-end on Testing/sRGB_v4_ICC_preference.icc (volume ≈ 735,816 dE³, not degenerate; round-trip mean ≈ 0.008 dE), and a coarse-grid call asserting degenerate==true reaches the caller. Compiles IccVizModel.cpp directly.
  • iccviz-degenerate-detection — unit test of iccvizmath::principalStdDevs on synthetic blob/plane/line clouds; calibrates the 0.02 coplanarity threshold (s3/s1 = 0.01 flags flat, 0.05 does not).
  • iccviz-pdf-axis-labels — runs iccProfileVisualizePlot and asserts the emitted LUT PDF carries both DrawAxisPDF label paths: the default identity axis (Input/Output) and the parameterized custom-tick axis (L* (100 to 0) / % ink). A content pin, not a byte-for-byte golden PDF, so a legitimate LUT curve-sampling shift across IccProfLib versions won't false-fail it.

2. Signed commits — both commits are SSH-signed (ED25519).

3. Copilot — all 10 resolved: bOwnsProfile UAF/double-free ×5 (pass bOwnsProfile=false), signed-overflow S+1 ×2 ((double)S + 1.0), doc-comment corrections ×2 (fixed canonical signature order), and the CLUT-raster tile-geometry bound.

@xsscx
xsscx requested review from Copilot and xsscx and removed request for dwtza, maxderhak and xsscx July 18, 2026 02:12
@xsscx
xsscx self-requested a review July 18, 2026 02:12
…ternationalColorConsortium#1711)

Bring the data-first visualization engine under Tools/CmdLine/IccProfilePlot/
(IccVizModel, landed in InternationalColorConsortium#1517) up to date with three feature-additive changes,
plus the caller updates they require. No refactor of the enumerate/render
surface; no change to IccProfLib or to the Mini{PDF,SVG,TIFF} writers.

Engine (IccVizModel.hpp/.cpp, IccVizMath.hpp):
- GamutVolume(): gamut volume in dE*ab^3 via boundary voxelisation + flood-fill
  through the AToB transform; scalar GamutVolumeResult with a `degenerate` flag
  so a collapsed sampling is distinguishable from a genuinely tiny gamut.
- RoundTripDE(): B2A->A2B round-trip accuracy (mean/p90/max/std dE*ab) seeded
  from in-gamut device-cube samples; scalar RoundTripResult.
- Kind::NeutralAxisInking: device colorant along the neutral axis from a
  PCS->device (BToA) table, enumerated per BToA tag. InkReversalL retired
  (enum value reserved). Plus GamutVolume accuracy work (IccProfLib CIELAB
  reuse) and CLUT-raster / new-metric hardening against malformed input.

Breaking API (internal only): Enumerate(pIcc, Order) loses its Order argument.
Enumerate(pIcc) now returns one canonical order (chromaticity, then per-tag
A2B0/1/2, B2A0/1/2, gamut, then named/colorant). Only in-tree caller updated.

iccProfileVisualize.cpp:
- Drop the Order argument at the Enumerate call site.
- Render Kind::NeutralAxisInking into the PDF report (was engine-enumerated but
  silently skipped): CreateNeutralAxesXobject (L*/%-ink axes frame) +
  renderNeutralAxisGraph (one polyline per colorant along L* 100->0, distinct
  CMYK stroke, channel-name legend) + the processLuts switch case. One page per
  PCS->device table.

The engine sources are algorithmically identical to the upstream copy; the only
deltas are the restored ICC license header on IccVizModel.cpp and an ASCII
punctuation sweep (matching the InternationalColorConsortium#1517 strict-build convention), which changes
five graph-title string literals from an em-dash to a hyphen. All translation
units build clean under -Wall -Wextra.

The old "byte-identical vs original iccProfileVisualize" PDF baseline is
intentionally superseded by the canonical page order and the chromaticity page
for device/CMYK profiles; a pinned-output regression + CTests for the two new
scalar metrics follow as a separate in-repo PR per the maintainer-owned CTest
policy. Refs InternationalColorConsortium#1711.
Adds the regression coverage requested in the InternationalColorConsortium#1712 review (item 1) for the
iccviz engine synced in InternationalColorConsortium#1711.

* iccviz-gamut-roundtrip-metrics: drives GamutVolume and RoundTripDE end-to-end
  on the stock sRGB v4 LUT profile -- positive 3-D gamut volume, not degenerate,
  small in-gamut round-trip dE -- and asserts a coarse-grid GamutVolume still
  flags degenerate through the public API. Compiles IccVizModel.cpp directly
  (the metrics live in tool sources, not IccProfLib).

* iccviz-degenerate-detection: header-only unit test of
  iccvizmath::principalStdDevs on synthetic blob/plane/line clouds; calibrates
  the 0.02 coplanarity threshold (s3/s1 ratio 0.01 flags flat, 0.05 does not).

* iccviz-pdf-axis-labels: runs iccProfileVisualizePlot and asserts the emitted
  LUT PDF carries both DrawAxisPDF label paths -- the default identity axis
  (Input/Output) and the parameterized custom-tick axis (L* (100 to 0) / % ink).
  A content pin (uncompressed-stream label match), not a byte-for-byte golden
  PDF, so a legitimate LUT curve-sampling shift will not false-fail it.

Registered on both the WIN32 and POSIX test paths, gated behind their targets
and labelled issue-1712. Each assertion was mutation-verified against the
shipped implementation.
@xsscx
xsscx force-pushed the feature/iccviz-sync-gamut-roundtrip branch from 3024b91 to 10e0482 Compare July 18, 2026 02:13
@github-actions github-actions Bot added Testing CTest, regression, or test coverage Configuration Repository, CMake, YAML, JSON, or tool configuration Build Build system, CMake, compiler, or packaging labels Jul 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Comments suppressed due to low confidence (1)

Tools/CmdLine/IccProfilePlot/iccProfileVisualize.cpp:1357

  • This comment is incorrect: the new implementation explicitly uses a fixed canonical signature order, not the profile's tag-table sequence (IccVizModel.cpp:1350-1355). Keeping this wording will mislead future report-order changes.
  // Enumerate now returns a single canonical order that already follows the
  // profile's tag-table page sequence (the Order::TagTable parameter was retired
  // upstream), so no ordering argument is passed.

Comment on lines +1543 to +1544
CIccTag* pTag = pIcc->FindTag(aToBTag);
if (!pTag) return fail("AToB tag not present");
} else {
continue;
}
if (!std::isfinite(lab[0])) continue;
const iccviz::Series &s = graph.series[c];
if (s.verts.empty())
continue;
commands << kInkStroke[c % 8] << "\n";
Comment on lines +50 to +53
// The exact predicate GamutVolume applies (voxelSize term dropped: these unit
// clouds are dimensionless, so we exercise the coplanarity ratio in isolation).
static bool is_flat_ratio(double s1, double s3) {
return (s1 > 0.0) && (s3 < 0.02 * s1);
Comment on lines +63 to +68
set(_labels
"(Input)|\\(Input\\)"
"(Output)|\\(Output\\)"
"(L* (100 to 0))|\\(L\\* \\(100 to 0\\)\\)"
"(% ink)|\\(% ink\\)"
)
Comment on lines +839 to +840
# Guards the two scalar visualization metrics in the iccviz engine
# (Tools/CmdLine/IccProfilePlot/IccVizModel): GamutVolume and RoundTripDE (#1712).
@@ -0,0 +1,136 @@
// Regression test for the two scalar visualization metrics in the iccviz engine
@@ -0,0 +1,195 @@
// Regression test for the gamut-degeneracy heuristic used by iccviz::GamutVolume
@@ -0,0 +1,80 @@
# RunIccVizPdfAxisTest.cmake - driver for the DrawAxisPDF axis-label regression.
Comment on lines +56 to +59
// Build an M x M grid in x,y over [0,1] with `layers` z-planes evenly spanning
// [0, zThickness]. layers==1 collapses z to a single plane (s3 == 0 exactly).
// The covariance is diagonal by construction, so the principal axes align with
// x/y/z and s3 is the z extent -- a clean, analytically predictable cloud.
@colourbill-ctrl

Copy link
Copy Markdown
Contributor Author

Reopened in-repo as #1716 to clear the fork-PR maintainer automation gate (this fork PR touches .github/ci/regression/** + Build/Cmake/Testing/**, which the gate blocks from the untrusted fork path). #1716 carries the identical sync + CTests plus the Copilot Review 2 fixes (AToB-signature guard, a*/b* finiteness, 15-ink stroke palette, planar-gamut + rotated-cloud + tick-operator test coverage, stale-comment fix). Will close this once #1716 is green — keeping it open for now for traceability.

@colourbill-ctrl

Copy link
Copy Markdown
Contributor Author

Correction: the in-repo continuation is #1717 (I consolidated #1716#1717 under a distinct branch name). Note the CI on #1717 can't run yet — GitHub Actions has been failing every workflow at startup (0s startup_failure) repo-wide since ~02:14 UTC today, independent of branch/PR/event. Once Actions is restored, #1717 will need a re-trigger.

@colourbill-ctrl

Copy link
Copy Markdown
Contributor Author

Closing as superseded by the in-repo PR #1717, which carries the identical sync + CTests plus the Copilot Review 2 fixes. Continuing this work in-repo (no fork) per project convention.

colourbill-ctrl added a commit that referenced this pull request Jul 18, 2026
Engine/tool hardening (Tools/CmdLine/IccProfilePlot):
- GamutVolume: reject non-AToB signatures up front. Because the transform is
  built with bInput=true, GetSrcSpace() reports the profile's device space even
  for a BToA tag, so the existing isPcsSpace() guard could not catch it; a BToA
  tag would run its LUT backwards and return a plausible-but-meaningless volume.
- buildNeutralAxisGraph: require all three Lab components finite before formatting
  the colour hint (a*/b* NaN/Inf could previously reach the snprintf).
- renderNeutralAxisGraph: give every one of the up-to-15 ink channels a distinct
  stroke colour instead of aliasing at 8 (c % 8 -> c % kInkStrokeCount).
- processLuts: correct the stale Enumerate ordering comment (fixed canonical
  signature list, not the profile's tag-table order).

Test coverage (.github/ci/regression, Build/Cmake/Testing):
- gamut-roundtrip-metrics: add a BToA-rejection check and a synthetic planar-gamut
  AToB profile that pins GamutVolume's flat/degenerate predicate end-to-end (voxels
  > 27, so only the s3 < 0.02*s1 term can flag it) -- both red-green verified.
- degenerate-detection: add a rotated anisotropic cloud so principalStdDevs' Smith
  1961 trig branch (p1 > 0) is exercised, not just the diagonal fast-path.
- RunIccVizPdfAxisTest: assert the parameterized tick operators (default 50%/100%
  vs reversed 100/50), not just axis titles, and add the ICC BSD header.
xsscx pushed a commit that referenced this pull request Jul 18, 2026
Adds the regression coverage requested in the #1712 review (item 1) for the
iccviz engine synced in #1711.

* iccviz-gamut-roundtrip-metrics: drives GamutVolume and RoundTripDE end-to-end
  on the stock sRGB v4 LUT profile -- positive 3-D gamut volume, not degenerate,
  small in-gamut round-trip dE -- and asserts a coarse-grid GamutVolume still
  flags degenerate through the public API. Compiles IccVizModel.cpp directly
  (the metrics live in tool sources, not IccProfLib).

* iccviz-degenerate-detection: header-only unit test of
  iccvizmath::principalStdDevs on synthetic blob/plane/line clouds; calibrates
  the 0.02 coplanarity threshold (s3/s1 ratio 0.01 flags flat, 0.05 does not).

* iccviz-pdf-axis-labels: runs iccProfileVisualizePlot and asserts the emitted
  LUT PDF carries both DrawAxisPDF label paths -- the default identity axis
  (Input/Output) and the parameterized custom-tick axis (L* (100 to 0) / % ink).
  A content pin (uncompressed-stream label match), not a byte-for-byte golden
  PDF, so a legitimate LUT curve-sampling shift will not false-fail it.

Registered on both the WIN32 and POSIX test paths, gated behind their targets
and labelled issue-1712. Each assertion was mutation-verified against the
shipped implementation.
xsscx pushed a commit that referenced this pull request Jul 18, 2026
Engine/tool hardening (Tools/CmdLine/IccProfilePlot):
- GamutVolume: reject non-AToB signatures up front. Because the transform is
  built with bInput=true, GetSrcSpace() reports the profile's device space even
  for a BToA tag, so the existing isPcsSpace() guard could not catch it; a BToA
  tag would run its LUT backwards and return a plausible-but-meaningless volume.
- buildNeutralAxisGraph: require all three Lab components finite before formatting
  the colour hint (a*/b* NaN/Inf could previously reach the snprintf).
- renderNeutralAxisGraph: give every one of the up-to-15 ink channels a distinct
  stroke colour instead of aliasing at 8 (c % 8 -> c % kInkStrokeCount).
- processLuts: correct the stale Enumerate ordering comment (fixed canonical
  signature list, not the profile's tag-table order).

Test coverage (.github/ci/regression, Build/Cmake/Testing):
- gamut-roundtrip-metrics: add a BToA-rejection check and a synthetic planar-gamut
  AToB profile that pins GamutVolume's flat/degenerate predicate end-to-end (voxels
  > 27, so only the s3 < 0.02*s1 term can flag it) -- both red-green verified.
- degenerate-detection: add a rotated anisotropic cloud so principalStdDevs' Smith
  1961 trig branch (p1 > 0) is exercised, not just the diagonal fast-path.
- RunIccVizPdfAxisTest: assert the parameterized tick operators (default 50%/100%
  vs reversed 100/50), not just axis titles, and add the ICC BSD header.
xsscx pushed a commit that referenced this pull request Jul 18, 2026
…edes #1712) (#1717)

Signed-off-by: David Hoyt <dhoyt@hoyt.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Build Build system, CMake, compiler, or packaging Configuration Repository, CMake, YAML, JSON, or tool configuration Copilot Copilot use indicated by Maintainer Source C or C++ source code changes Testing CTest, regression, or test coverage Tools Command-line tool or GUI tool changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring GamutVolume / RoundTripDE / neutral-axis inking to the IccProfilePlot iccviz engine

4 participants