Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

104 Commits

Folders and files

Repository files navigation

icon-vector-reconstruction

A resolution-independent reconstruction of reference.png — a 1024x1024 dark icon showing two luminous cyan curves pinched around a central light inside a rounded-square frame — as a hand-built, parametric SVG.

reference reconstruction difference (x4)
reference reconstruction difference

Primary deliverable: reconstruction.svg — 56 named layers, 157 KB, no embedded bitmap and no traced outlines. Every mark is a primitive driven by a named parameter in src/params.json: one path for the frame, two cubic-Bézier paths for the luminous curves (reused, offset and clipped, by every glow layer), and blurred ellipses, rects, cones and gradients for the optical effects.

Fidelity

Reconstruction rendered at 1024 px (resvg) against reference.png:

metric value for scale
mean absolute error 1.591 / 255 a flat black canvas scores 17.89
RMSE 2.683
MAE on a 1/2.2 display curve 5.160 weights the dark background as the eye does; black scores 59.7
SSIM (luminance) 0.9775 black scores 0.142
worst single-channel error 73
pixels off by more than 2 / 8 / 24 33.7% / 3.0% / 0.1%
mean bias -0.143

Per region (MAE): frame band 2.50, centre 90 px 4.36, bright pixels 5.58, dark background 1.29, everything else 1.42.

About a quarter of that error is the reference's own JPEG noise: decomposed by scale, the background residual implies an MAE floor of 0.57-0.61 per channel that no reconstruction of the underlying design can go below.

The two regions a whole-image average cannot police, from tools/diagnose.py (full report in out/diagnostics.json):

targeted measurement value
MAE within 110 px of the central light 3.83
worst ring of the flare's radial profile +0.9 code values at r = 20-30
curve glow, rms relative error over 21 signed-distance bins 2.8%
the same, resolved along the curve (71 cells) 5.0%
light in the four interior corners, rms relative error 5.0%
worst single bin of that profile -5.8% at s = -70..-52 px
left lobe, MAE more than 25 px from the ridge 1.34 (bias -0.19)
right lobe, MAE more than 25 px from the ridge 1.31 (bias -0.11)

Cross-engine: the same SVG in resvg and headless Chromium agrees to MAE 2.638 (SSIM 0.9559); see out/validation.md for the resolution sweep.

What is in here

reference.png            the ground truth (unmodified)
reconstruction.svg       the deliverable, generated from src/
src/params.json          every number that defines the artwork
src/build_svg.py         params -> SVG (geometry, gradients, filters, layer stack)
tools/render.py          SVG -> PNG at any size, via resvg or headless Chromium
tools/compare.py         metrics + difference images against the reference
tools/probe_compare.py   geometric probes: measure reference and render the same way
tools/fit_photometry.py  closed-form fit of every layer's light amount
tools/optimize.py        bounded coordinate descent over shapes/tapers/geometry
tools/optimize_all.sh    the full fitting cycle
tools/prune_layers.py    re-fits without each layer to see which ones earn their place
tools/isolate.py         recovers one layer group's own contribution from the reference
tools/diagnose.py        targeted reports for the flare, the lobes and the glow profile
tools/ray_report.py      the four diagonal rays: peak, angle and width against the reference
tools/core_report.py     the flare core's radial falloff, in bands, where a blur would show
tools/chroma_report.py   colour by distance from a curve ridge, where the paleness is
tools/measure_flare.py   the rays' geometry of record, and their calibration against each line's profile
tools/wedge_report.py    angular modulation west of the flare, where a regional mean is blind
tools/arm_report.py      the horizontal arms, scored against a matched null along the ridge
tools/vstreak_report.py  the vertical line through the core, north and south reported apart
tools/fan_report.py      whether the westward fan is too bright or too long -- different faults
tools/line_shape.py      the long line's transverse spread and colour, rather than its height
tools/line_report.py     the three horizontal lines' amplitudes, ridges masked
tools/compare_sheet.py   reference | render | signed difference, by region and by scale
tools/flare_view.py      the flare in RGB, luminance and chroma at three scales, side by side
tools/visual_regression.py  the sixteen recurring visual failures, as ratios to the reference
tools/ray_lines.py       every named ray as a 2D profile on its measured line, reference beside render
tools/flare_parts.py     reference | before | after for each named part of the flare
tools/publish.sh         the one command that produces a reviewable release
tools/regions.py         the measured anchors and region geometry both of those share
tools/test_pipeline.py   regression checks that keep optimisation results meaningful
tools/validate.py        multi-resolution and cross-engine validation report
tools/update_readme.py   writes the measured metrics back into this file
docs/METHOD.md           what the image is made of, and how it is reproduced
docs/DECISIONS.md        decision record: what was chosen, why, what was rejected
docs/stage1_findings.md  the first-pass measurements the rest was built on
out/analysis/            per-component measurement reports
out/diagnostics.json     the targeted flare / profile / corner measurements
out/prune.log            what each layer is worth, as MAE
out/                     renders, difference images, metrics, validation report

Reproducing

Needs Python 3 with numpy, Pillow and resvg-py, pinned in requirements.txt (pip install -r requirements.txt) — and nothing else: tools/test_pipeline.py checks that every shipped module imports only those three plus the standard library, because tools/diagnose.py once needed SciPy that this line did not mention. Headless Chromium is optional and only used for the second opinion in tools/validate.py.

A clean checkout needs one setup step before the regression gate: python3 tools/setup_baseline.py renders the previous accepted release (out/baseline/reconstruction.svg, pinned by digest in its manifest), which the before/after sheet is checked against. It exits 3 on a setup failure, and tools/test_pipeline.py exits 3 — not 1 — if the setup was skipped, so a missing setup step is never reported as a regression. CI and tools/publish.sh both run it.

Renderer. resvg is the acceptance renderer: every number quoted here, and every objective the optimiser minimises, is measured on its output at 1024 px. Chromium is a cross-check only. The two disagree by a known, measured amount (docs/DECISIONS.md D12) and the artwork is not adjusted to suit Chromium.

python3 src/build_svg.py                                    # params -> reconstruction.svg
python3 tools/render.py reconstruction.svg out/r.png --size 1024
python3 tools/compare.py reference.png out/r.png --out-prefix out/diff
python3 tools/validate.py                                   # sizes 256..4096, both engines
python3 tools/validate.py --quick --no-chromium              # resvg only, no browser needed
python3 tools/probe_compare.py out/r.png                     # geometry/alignment probes
python3 tools/diagnose.py out/r.png                          # flare / lobe / profile reports
python3 tools/setup_baseline.py                              # once per clean checkout
python3 tools/test_pipeline.py                               # optimiser correctness checks
sh tools/optimize_all.sh                                     # refit everything from scratch

tools/compare.py writes out/diff_diff.png (absolute difference, x4 gain), out/diff_signed.png (red = reconstruction too bright, blue = too dark) and out/diff_sbs.png (side by side).

How it works, in one page

The reference decomposes into geometry plus smooth optical falloffs, so the SVG is a stack of primitives composited with mix-blend-mode: screen over black:

The counts below are the shipped structure; the parameter values live in src/params.json, which is the single source of truth for them. Prose that restated those numbers drifted out of date twice, so it no longer does.

  1. Background — a flat exterior in three pieces (body, top edge, corners), then the frame shape filled with a base colour, two broad radial gradients and a vertical ramp.
  2. Glow — eight blurred copies of each curve's path, six offset toward the concave side and two toward the convex side. The measured facts they reproduce: the glow is 2.8-3.5x brighter on the concave side, and each component carries its own measured fade along the curve, emitted as the measured stations themselves. One of the eight (arc_bloom_w, D62) is confined to the flare's height: within ~40 px of the core row both curves are whiter on their concave side than the other glow terms draw -- on the left curve, 66 px from the core, as much as on the right -- so that light is the curves' and not the flare's. The narrowest concave term is split into a cyan layer and a white one, and near the flare's rows their fades were measured station by station on each curve (D66): there the reference's glow is white, not cyan, on both curves. The broad inset term (arc_glow2) is split at each curve: its flare-facing part has its own fade, measured on that side, because the concave side's fade let its tail reach too far between the curves (D67). A component closing the gap between 5.8 and 20.6 px was built and measured; it raises what the basis can achieve beside the ridge but did not improve the render, and is not shipped (docs/DECISIONS.md D19, D22).
  3. Central light — thirty-two layers: five radial blooms (the halo with one fitted gap north-east of the core, where the reference's white has a notch; D67), nine horizontal streak components at the three measured line heights (lines A and B each carry their white in a layer of their own, so white and cyan follow different profiles along the line; D64), the vertical diffraction line, fourteen one-sided ray segments on eight measured lines, and three short white arms of the core itself: north-west and south, where the reference's white leans away from the horizontal (D63), and east, where its white core runs on to the right curve (D65). There is no separate glint layer; the brightest pixels come from the streak and bloom stack. The rays are measured rather than assumed, and several do not pass through the core (the upper-left pair misses it by 17 and 26 px). A ray whose profile along its line one gradient cannot hold is drawn as segments on the same line -- an inner and an outer lower-left segment, a bright white inner segment, a narrow line and a soft flank that widens with radius on the lower right (D64; the line fades by r ~130 and the flank carries the light beyond it, D65), a white near-core part and a cyan lobe at 229 degrees -- and each segment's geometry of record, including its absolute origin in canvas pixels and where it fades, is the table in tools/measure_flare.py, which --geometry restores (D61-D65). Their amplitudes are calibrated against each ray's measured profile, jointly per line, not fitted to the whole image. The lower-right line and its flank are calibrated together: past r 88 each band is read with a narrow and a broad template at once, which tells a flank's light from a line's, so the whole-image fits hold the flank and calibration restores it (D65). The westward triangle that two straight-edged flank layers once drew is gone and stays gone (D61).
  4. Curve cores — a hard-edged bright core on each path plus two thin mid-tapered strokes, one on each side, because the measured core widens from about 6.2 px at the tips to about 8 px at mid-height on both sides of its centre-line (the flare-side stroke is D66's). The core itself narrows toward its ends as the reference's does, so it is drawn as a filled outline rather than a constant-width stroke (D67). Past each end the reference has a narrow cyan ridge for another 40-55 px, along the curve's continuation; one more thin stroke draws it, continued along the end cubic's own polynomial, with the cubics of record unchanged (D68). At the two north ends the reference's white also holds further toward the corners than the core's measured fade drew, so three of its stations were re-measured there (D69). These fades are measured along the curves but painted as gradients along y, whose rows cross the curves' oblique ends and tilt the fade toward the lens. Where the reference's light lies across the curve itself, at the core's two north ends and the tip stroke's two south ends, the fade is painted along the curve instead. Six of the right curve's stations over its south outer third were then re-measured (D70).
  5. Rim — two frame-ring strokes, a uniform base and a gradient-painted rim, because the measured rim brightness peaks at the middle of each edge and the top edge is twice as bright as the bottom. Two further layers light the four interior corners, which lie past the curve ends and so get no curve glow.

Three measurements did most of the work:

  • The curves are not conics. Two cubic Beziers joined at the apex with a vertical tangent fit the ridge to 0.083/0.093 px; the best ellipse arc manages 0.331/0.473 px and leaves a systematic 4-5 lobe residual.
  • The frame corners are continuous-curvature corners, three arcs each (blend r 639, main r 163.56, blend r 639): the local radius of curvature runs 250-400 px near the straight edges and 145-175 px through the 45-degree region, and a circle fitted to the corner points cannot even be tangent to the measured straight edges.
  • All the light lies in a narrow non-negative colour cone — white(1, 1, 1), cyan(0, 0.94, 1.00) and blue(0, 0, 1) — so each layer carries three non-negative amounts rather than three free channels, and the cone's R <= G <= B is a constraint the fit cannot escape by inventing a green or magenta layer. All three are needed: over the 946 964 reference pixels above luminance 3, the non-negative fit in this basis is off by 0.010/255, while dropping blue and using white+cyan alone is off by 1.84/255. The reason is the dark field, which is not cyan: across its 620 317 pixels between luminance 3 and 14 the mean colour is (1.8, 7.1, 13.4), G/B = 0.53 against cyan's 0.94.

Because everything is screen-composited, the render has the closed form out = 1 - prod(1 - A_i k_i), so each layer's colour can be fitted analytically from a single white render of that layer. That is why the fitting loop is fast enough to search the geometry.

Known limitations

  • The banding budget is now the binding constraint on the flare. The profile error in the 26 px strip beside each ridge has a ceiling of 5.00%, and this iteration's three measured corrections spend it: the horizontal lines +0.09, the west fan +0.08, the ridge colour trade +0.08. They do not all fit. The combination that is best on structure measures 5.03% and is not shipped; line A's falloff was given back instead, so its error rises from 2.95 to 4.02 (still below the 3.41 it had before). Raising the ceiling to fit the result was declined, for the same reason it was declined in D31. D37 has the table.

  • The flare region's mean absolute error rose while every structural measure in it improved — 7.138 to 7.202 within 110 px of the core, against a wedge modulation of 5.62 to 3.83, three horizontal lines from 12.63 to 6.73, and bright pixels at the core from 13 to 10 where the reference has 10. This is not incidental: the angular and line diagnostics exist because a regional mean cannot see a redistribution, and it will sometimes move against them.

  • The worst single-channel error rose from 104 to 110. It is a handful of pixels on the curve crest beside the flare, where the reference reaches R 254 and the reconstruction 249.

  • The paleness beside each curve is improved but not closed. In the 4-8 px ring the chroma deficit falls from 15.19 to 10.74 code values and the red excess from 6.75 to 3.51, but the ring is also 7% too dark, and a further white-for-cyan trade drives red negative before the chroma closes. That residual is an amplitude problem, not a colour one.

  • Three of the four rays match the reference's edge hardness; the upper-left is the exception, and it is too SOFT, not too hard. Defining the softness index as the 25-75% edge run over FWHM of the mean transverse profile (a Gaussian is 0.385, a blurred slab under 0.35), measured on the stacked, ridge-masked, axis-aligned profile — a pipeline that recovers each rendered ray layer's own exact value to ±0.03 — the reference reads 0.306 / 0.359 / 0.320 / 0.442 for the upper-left, lower-left, upper-right and lower-right rays against the render's 0.340 / 0.339 / 0.326 / 0.343. Only the lower-right reference ray is materially softer than its rendered counterpart. An earlier version of this list reported 0.46-0.48 for the reference's left pair and concluded that all four were softer; that came from a per-radius peak statistic, which is biased upward on an image with the reference's 8×8 blocking — the same numbers can be manufactured by adding matched noise to the render, whose true edges are unchanged. See D38.

  • The reference's JPEG blocking and its low-frequency "smudge" texture are not reproduced, by choice: together they set an MAE floor of ~0.6 per channel. The mottling around the core was classified rather than assumed (D62): its 8-px block structure matches the render recompressed at about JPEG quality 60, and what remains after that is a weak, only partly grid-aligned white texture of ~2 cv RMS at 4-8 px -- real light, probably, but lumps a vector model could only invent, so it is not modelled.

  • Four rays are greener than white/cyan/blue can draw, so six of their layers may use a fourth primary, teal (0, 1, 0.7). The 267-degree ray, upper-left A, the upper-right ray and the 229-degree lobe sit with B below G above their local ramp (B/G 0.73-0.85, against cyan's ~1.06). It is not the compositing: had the reference been composited in linear light, every ray's implied B/G would rise, leaving these green (D63). In a controlled refit of every family's colour, teal and a pure green primary tied. Teal was taken because it reaches the evidence and no further: its floor, B/G 0.7, is the greenest ray measured, while pure green would let a later fit draw rays far greener than anything in the reference. Teal is allowed only on the six layers of record, and every other layer stays in the cone. The permission is the layer's own teal key, not its current amount, so a teal ray refitted from zero can take teal back (D65). Those four rays now read B/G 0.73 / 0.78 / 0.84 / 0.78 against the reference's 0.73 / 0.78 / 0.85 / 0.74 (D65).

  • The white core's directional shape is drawn, not all of it matched. The reference's white leans north-west and reaches south of the core; two short white arms fitted to the core's 2D residual now carry that (D63). North of the core, between the vertical line and the right curve, the reference has a notch in its white where the render was ~11 cv too red. The reference lacks WHITE there, not light, so this is not an occluder. The halo now carries one fitted gap in that sector (D67), which takes the excess to about +7; what remains is the fan's tail and the right curve's white glow, and the gap is at its limit. The core's white leans east toward the right curve in the reference. Its white layer was moved about 1 px east, narrowed north to south and tilted, at the same total light, which halves the east-south-east deficit at r 4-12 (D68). The north arc at r 12-16 and the south-west are now a little darker than the reference's.

  • Just past the right curve at the core's height, part of the remaining green is the flare's own light, compact within ~30 px of the core and shaped like the fan and halo, not like the curve's glow. The curve's part was fixed at the curve (D66); the flare part is left, since dimming the flare there would trade one error for another.

  • The two dark axial wedges between the diverging curves used to be over-predicted by ~4 code values, and this list blamed the additive stack for it: "a screen stack can only add light". That was the wrong diagnosis. The wedges are dark because nothing in the reference puts light there, and the over-prediction was two model errors — a broad glow that was not one-sided and a central bloom with more than twice its measured reach — both fixed in docs/DECISIONS.md D13 and D14.

  • The same SVG is about 2.8 code values brighter in Chromium than in resvg. Chromium composites each dim screen layer 0.26-0.33 counts brighter, and over a stack this deep that accumulates to a near-uniform lift of the dark background; removing the mean offset leaves the two engines agreeing to MAE 1.36. It is a compositing-precision artefact rather than a structural difference, it cannot be merged away (a gradient modulates alpha, and each of these layers carries a different colour), and it is the one measure that got worse as the reconstruction gained elements, so the reconstruction is not changed to suit one engine. docs/DECISIONS.md D12 has the per-layer measurements.

  • The true peak radiance of the cores and the streak is unrecoverable: G and B clip at 255 over those pixels in the reference. Any model that clips in the same places matches them.

  • The coherent cross-curve structure beside each curve is still about 1.8x the reference's in the 26 px strip next to the ridge (1.23x on the right curve), falling to 0.96x by a 12 px scale. Across the whole open lobe it is 0.68-1.04x — at or below the reference — so this is a localised defect at the curve's inner edge, not the whole-lobe banding it was first measured as. What makes it read more strongly than its amplitude suggests is that the reference carries structure of the same size under 0.7-0.9 counts of incoherent grain, and the reconstruction carries it naked; that is an observation about visibility, not an excuse, and it is not a reason to add grain. Softening the one layer responsible (arc_glow1 carries 74%/60% of it) does reduce the ratio, to 1.20x at the widest blur its bounds allow -- and makes the profile error in the same strip worse as it does so, 4.00% to 6.55%, along with MAE and SSIM. So the layer is left alone: a blurred stroke that matches the reference's level there necessarily carries more fine cross-curve structure than the reference does, and separating the two needs a primitive that can be flat-topped, not a different blur.

  • The lobe interior's coherent profile error is several times its own bound. The best the basis can do there, fitted to the lobe profile and nothing else, is 0.89%/0.97% — and it reaches that only by amplitudes that take the global MAE from 1.90 to 8.20 and the flare from 7.5 to 35.2. So the bound is not reachable, and what remains is the price of one basis serving the lobes, the frame, the field and the flare at once. Reducing it needs a different decomposition, not a better fit. docs/DECISIONS.md D19 has the measurements.

  • Each glow layer's cross-curve profile shape is constant along the curve — only its amplitude tapers. The reference's varies. This is the structural reason the reconstruction's contours run further than the reference's, and it is the obvious next thing to change.

docs/METHOD.md has the measurements; docs/DECISIONS.md has the decision record, including what was rejected and why; out/analysis/ has the per-component measurement reports the reconstruction was built from.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages