Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FigureDigitizer (figdig)

Digitize plots from phone photos of conference slides back into SVG + CSV.

A photo of a projected slide is perspective-distorted, noisy, and color-washed. figdig rectifies the slide, finds the plot axes, separates multiple traces (even where they cross, are dashed, or are occluded by scatter points), extracts scatter data, calibrates pixels → data coordinates from the tick marks, and exports a clean vector figure plus the underlying numbers.

Usage

# 1. Rectify + detect plots/ticks; writes a starter config + annotated images
uv run figdig inspect photo.jpg -o out/

# 2. Edit out/config.yaml: set each axis type (linear / log / normalized)
#    and type in the tick values printed on the figure
#    (x ticks left→right, y ticks top→bottom).

# 3. Extract and export
uv run figdig process photo.jpg -c out/config.yaml -o out/

Without a config, process still runs and exports everything in normalized 0–1 axis coordinates.

Outputs (per plot)

File Content
<plot>.svg vector figure: axes, ticks, traces, scatter points, legend
data/<plot>_<trace>.csv trace polylines in data coordinates
data/<plot>_points.csv scatter points in data coordinates
verification_overlay.jpg extraction drawn over the rectified photo
fidelity_misses.jpg red = ink not represented in the export
report.json fidelity metrics + axis-fit residuals

The faithfulness guarantee

Automatic extraction from a photo can never be blindly trusted, so figdig measures its own fidelity instead of assuming it:

  • Axis-fit residuals — the pixel→data mapping is least-squares fitted over all detected tick marks; the RMS/max residual (in px) is reported. A bad tick or a wrong tick-value list shows up immediately as a large residual.
  • Ink recall / precision — the fraction of plot ink covered by the export, and the fraction of exported geometry backed by real ink. process warns when either drops below 90%.
  • fidelity_misses.jpg — every uncovered ink pixel painted red, so you can see at a glance what was left out (usually in-plot label text, which is intentionally not exported as data).
  • verification_overlay.jpg — the extracted vectors on top of the photo for direct visual comparison.

Always look at the two verification images before reusing the data.

Pipeline

  1. Rectify (rectify.py) — the slide is found as the largest bright quadrilateral (Otsu + contour), perspective-warped flat, and denoised with an edge-preserving bilateral filter.
  2. Plot/tick detection (plotbox.py) — long horizontal/vertical ink lines are isolated morphologically; their connected structures give the axes boxes (works for full frames and L-shaped axes). Tick marks are read from thin bands just outside the left/bottom edges.
  3. Extraction (extract.py) —
    • scatter dots via distance-transform peaks, validated by roundness, chroma uniformity and a ring test (≤ 2 line exits), so dots sitting on a curve are still found and curve crossings are not mistaken for dots;
    • label text removed by grouping small glyph components into words (dash sequences are told apart by axis alignment, absence of holes and unbranched skeletons; scatter-dot clusters by circularity);
    • all remaining ink is skeletonized into a chain graph; each chain's color is averaged along its core pixels (robust against washed-out projector colors);
    • chains are stitched through junctions (crossing traces) and across gaps (dashes, occlusions) by tangent continuity + color similarity;
    • finished paths are grouped into named traces by color; fragments of one trace severed by crossings are re-joined by mutual endpoint-tangent matching (with a guarded apex case for curve peaks);
    • each path is smoothed with corner-preserving B-splines (deviation bounded to ≤ 2.5 px of the raw skeleton) and exported as cubic Béziers.
  4. Labels (labels.py + ocr.py, macOS) — in-plot text labels are located from the removed-text regions plus any ink no trace explains, de-rotated via PCA orientation, read with Apple's Vision OCR (rotated and flipped fallbacks), and rendered as SVG <text> at the original position/rotation/color/size. Ink that was read as a label counts as exported in the fidelity metrics. label_fixes: {"lons": "Ions"} in the config corrects OCR quirks; labels: false disables the stage.
  5. Calibration (calibrate.py) — linear / log10 / normalized axis fits from tick positions + user-supplied tick values, with residual reporting.
  6. Export (svgout.py) — SVG (with optional per-trace display names and color overrides from the config), CSVs, overlay, misses image, report.

Config reference

plots:                       # matched to detected boxes, reading order
  - name: my_plot
    title: "Figure title"
    x_axis:
      label: Year
      type: linear           # linear | log | normalized
      ticks: [1900, 1910, ...]   # values printed at the ticks, left→right
      # ticks_px: [...]      # optional: override detected tick pixel positions
    y_axis:
      type: log
      ticks: [1e13, 1e11, ...]   # top→bottom
    points_name: "measured data"
    traces:                  # optional: rename / recolor auto-named traces
      darkred: {name: "Integrated Circuit", color: "#8b1a1a"}
      blue: "Vacuum Tube"    # plain string = rename only
    options:                 # optional extraction tuning
      detect_points: true
      min_points: 8          # fewer detected dots than this → no scatter set
      dot_min_r: 3.2         # px, dot radius range
      dot_max_r: 12.0
      gap_max: 28.0          # px, max dash/occlusion gap to bridge
      color_merge: 6.5       # Lab ΔE for merging paths into one trace
      trace_gap_max: 0.1     # fraction of plot diagonal: max same-trace rejoin gap
      smooth_sigma: 1.4      # px, spline smoothing (0 = polyline output)
      smooth_max_dev: 2.5    # px, max smoothing deviation from raw skeleton
      simplify_eps: 1.2      # px, polyline simplification (smooth_sigma: 0)

Example

examples/slide.jpg + examples/slide_config.yaml reproduce the two plots of the bundled SiQEW-2025 slide ("122 years of Moore's law" and a hype-cycle sketch): 4 + 7 traces, 56 scatter points, 98% ink recall / 100% precision on the quantitative plot, axis-fit residuals ≈ 1.4 px (x) / 0.7 px (y).

Limitations

  • Tick values are typed in by the user (tiny tick labels in phone photos are not reliably OCR-able); calibration correctness is then verified via the fit residuals rather than guessed.
  • Label text overlapping a curve can locally distort the skeleton; check the overlay.
  • Traces drawn in the same color that also cross each other may end up in one trace group (they are still separate paths in the SVG/CSV).
  • Dotted (not dashed) lines would be picked up as scatter points.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages