Sync iccviz engine: GamutVolume, RoundTripDE, neutral-axis inking (#1711)#1712
Conversation
|
2026-07-17 23:19:00 UTC Attempting to Clear the Stale API Data |
There was a problem hiding this comment.
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. |
| CIccXform* xform = CIccXform::Create(pIcc, tag, /*bInput=*/false, | ||
| neutralIntentForSig(sig), icInterpLinear); |
| 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); |
| 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)" ); |
| // 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. |
| // 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
left a comment
There was a problem hiding this comment.
- Can you add CTest(s)
- Can you Sign the Commit(s)
- Review & Resolve Copilot Issues
Thank You
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.
5517ddc to
3024b91
Compare
|
Thanks @xsscx — all three addressed and force-pushed to this branch (rebased on current 1. CTests — added under
2. Signed commits — both commits are SSH-signed (ED25519). 3. Copilot — all 10 resolved: |
…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.
3024b91 to
10e0482
Compare
There was a problem hiding this comment.
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.
| 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"; |
| // 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); |
| set(_labels | ||
| "(Input)|\\(Input\\)" | ||
| "(Output)|\\(Output\\)" | ||
| "(L* (100 to 0))|\\(L\\* \\(100 to 0\\)\\)" | ||
| "(% ink)|\\(% ink\\)" | ||
| ) |
| # 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. | |||
| // 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. |
|
Reopened in-repo as #1716 to clear the fork-PR maintainer automation gate (this fork PR touches |
|
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 |
|
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. |
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.
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.
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.
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
GamutVolumeResultwith adegenerateflag so a collapsed sampling is distinguishable from a genuinelytiny 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 aPCS->device (BToA) table, enumerated per BToA tag.
InkReversalLretired(enum value reserved).
new-metric hardening against malformed input.
Breaking API (internal only)
Enumerate(pIcc, Order)loses itsOrderargument.Enumerate(pIcc)nowreturns 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.cppOrderargument at theEnumeratecall site.Kind::NeutralAxisInkinginto the PDF report (was engine-enumeratedbut silently skipped):
CreateNeutralAxesXobject(L*/%-ink axes frame) +renderNeutralAxisGraph(one polyline per colorant along L* 100->0, distinctCMYK stroke, channel-name legend) + the
processLutsswitch case. One pageper PCS->device table.
Notes for review
the only deltas are the restored ICC license header on
IccVizModel.cppandan 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.
iccProfileVisualize" PDF baseline isintentionally 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.
-Wall -Wextra.CTests for the two new scalar metrics + a pinned-output PDF/TIFF regression to
replace the retired baseline.