General Plot Digitizer
Goal
Extract calibrated numeric X-Y data from images of experimental spectra (Raman, XRD, UV-Vis, IR, NMR, etc.) using a deterministic "Agent-in-the-Loop" workflow.
The labor is divided between two models:
- Vision-Language Model (Visual Sensor): Reads the image and returns a rich, unstructured narrative description of axes, colors, and visual obstacles. It does not produce JSON.
- Coding Agent (Translator & Executor): Translates the VLM narrative into a precise
metadata.json, runs the CV pipeline, inspects the overlay, and iterates until the curve is correctly isolated.
Instructions
Phase 1: Visual Inspection (VLM)
Do not attempt to generate JSON with the VLM. It acts only as a visual sensor.
- Generate grid overlay:
# Env: base-agent
python .agents/skills/general-plot-digitizer/scripts/plot_utils.py plot.png --draw-grid
This produces plot_grid.png with a labeled pixel grid for precise coordinate reading.
Prompt the VLM to analyze plot_grid.png (not the raw image). Use the built-in vision capabilities or the notify_user VLM inspection tool. Provide the prompt guidelines from scripts/vlm_prompt_template.txt.
Expected VLM output — a natural-language report covering:
- Axis labels, numeric ranges, and directions (is X reversed?).
- Bounding box of the data region in pixels (read from the grid).
- Color and style of each target curve (hex guess from the pixels on the line itself).
- Pixel bounding boxes of all visual obstacles (legends, text annotations, gridlines, tick marks) that overlap the data curves.
- Trace quality hints: thin/needle-like, thick/noisy, anti-aliased, JPEG artifacts.
Phase 2: Metadata Construction (Coding Agent)
Read the VLM narrative and construct metadata.json. Schema: resources/metadata_schema.json.
Required fields:
{
"plot_title": "",
"x_axis_label": "Wavelength (nm)",
"y_axis_label": "Absorbance",
"x_tick_min": 400, "x_tick_max": 800,
"y_tick_min": 0, "y_tick_max": 1,
"x_calibration_points": [
{ "pixel": 70, "value": 400 },
{ "pixel": 450, "value": 800 }
],
"x_scale": "linear", "y_scale": "linear",
"bounding_box": {"x_min": 72, "y_min": 28, "x_max": 452, "y_max": 318},
"x_reversed": false, "y_reversed": false,
"spectrum_type": "UV-Vis",
"curves": [{"label": "sample", "color_hint": "#1f77b4"}],
"text_regions": [{"x_min": 300, "y_min": 50, "x_max": 400, "y_max": 80, "label": "legend"}]
}
x_calibration_points (strongly recommended): anchor the X-axis transform to exact pixel→value pairs read from the grid, rather than assuming axis ticks align perfectly with bbox edges. Pick two well-separated ticks visible on the grid. If provided, these override x_tick_min/max for pixel-to-data mapping.
Translation rules (VLM narrative → metadata fields):
- Obstacles →
text_regions[] and/or mask_regions[] with pixel bounding boxes.
- Curve colors →
curves[].color_hint (from pixels on the plotted line, not from legend swatches).
- "Curve is black / same as axes" →
"cli_hints": {"curve_is_black": true}.
- "Noisy / jagged trace" →
"cli_hints": {"smooth": true}.
- "Thin, needle-like peaks" → do not set smooth; set upscale in Phase 3 instead.
If the VLM color guess is uncertain, run:
# Env: base-agent
python .agents/skills/general-plot-digitizer/scripts/suggest_colors.py plot.png \
--bounding-box x_min,y_min,x_max,y_max
This reports dominant non-background colors in the cropped region. Use the top result as color_hint.
Phase 3: Pipeline Execution
Select CLI flags based on VLM visual cues:
| VLM describes... |
Required CLI flags |
Avoid |
| Thin, needle-like peaks (XRD, FTIR) |
--crop-upscale 4.0 |
--smooth, --cluster-centroid |
| Fuzzy / anti-aliased / JPEG artifacts |
--curve-tolerance 75 (up to 85) |
— |
| Thick, noisy trace / scatter points |
--smooth --smooth-window 5 --smooth-deviation 15.0 |
— |
| Black curve on black axes |
--allow-black (auto-enables --spatial-filter --cluster-centroid) |
— |
| Thin anti-aliased colored line |
--extraction-method edge+color |
--morph-open |
Run the pipeline:
# Env: base-agent
python .agents/skills/general-plot-digitizer/scripts/digitize_pipeline.py \
plot.png \
--full \
--metadata metadata.json \
--output-dir ./output \
--overlay \
--format both
Append the VLM-dictated flags from the table above. The pipeline also reads cli_hints from metadata and auto-applies safe flags (--allow-black, --smooth, --all-curves).
Phase 4: Overlay Inspection & Iteration
- Inspect
*_digitized.overlay.png visually.
- If extraction is wrong, diagnose using this table and re-run Phase 3:
| Symptom |
Fix (metadata or CLI) |
| Wrong curve extracted (e.g. legend ink) |
Set curves[].color_hint from actual line pixels; or run suggest_colors.py on a tight crop |
| Text / labels contaminating trace |
Add bounding boxes to text_regions[] in metadata; add --smooth |
| Black curve picks up axis lines |
--allow-black + add axis regions to mask_regions[] |
| Trace too sparse / broken gaps |
--curve-tolerance 55 or --preset lowres |
| Thin line lost entirely |
--extraction-method edge+color; or --crop-upscale 4.0 for needle peaks |
| Same-color text blobs on thick curve |
--morph-open (caution: destroys thin <3px curves) |
| Low-res image, everything pixelated |
--upscale-strategy force (pre-upscales image, scales metadata bbox) |
| X/Y values shifted or inverted |
Fix x_tick_min/max, x_reversed, y_reversed, or x_calibration_points in metadata |
- Prefer metadata edits (adjusting
color_hint, text_regions, mask_regions, bounding_box) over adding CLI flags. Re-run the same pipeline command after editing metadata.json.
Outputs:
*_digitized.csv — comma-separated with x,y header
*_digitized.xy — space-separated, no header (if --format xy or both)
*_digitized.overlay.png — visual QC
*_digitized.md — summary
Agent Rules
- Do not write ad-hoc NumPy/OpenCV pixel-scanning code. The pipeline already handles HSV masking, spatial filtering, and smoothing internally.
- Do not skip the grid overlay step. The VLM is significantly more accurate on
plot_grid.png than on raw images.
- Do not guess CLI flags without VLM visual evidence. The flag table above is the complete decision tree.
- Always inspect the overlay image after each run before declaring success.
Key Parameters
| Flag |
Use when... |
Default |
--curve-color HEX |
VLM identified a specific curve color |
auto-detect |
--curve-tolerance N |
Trace is sparse or image has JPEG artifacts (increase); or mask bleeds into nearby colors (decrease) |
40 |
--overlay |
Always recommended for QC |
off |
--format {csv,xy,both} |
Downstream tool needs specific format |
csv |
--all-curves |
Multiple curves in metadata curves[] (auto-enabled when >1 curve) |
off |
--allow-black |
Data curve is black (same color as axes/frame) |
off |
--smooth |
Noisy, jagged, or thick trace with outlier points |
off |
--smooth-window N |
Tune smoothing aggressiveness (larger = more smoothing) |
5 |
--smooth-deviation PX |
Pixel distance from local median beyond which a point is rejected as outlier |
15.0 |
--crop-upscale FACTOR |
Thin peaks (XRD, FTIR) need more pixel width to register; or generally low-res crop |
1.0 |
--upscale-strategy {none,auto,force} |
auto (default) pre-upscales when metadata or heuristic says low-res; force always pre-upscales; none skips |
auto |
--upscale-factor FACTOR |
Controls the multiplier used by --upscale-strategy auto|force |
2.0 |
--vlm-metadata-on-upscale |
After pre-upscale, re-run VLM metadata on upscaled image instead of just scaling coordinates. Only if API keys are set |
off |
--extraction-method {color,edge,edge+color} |
color (default) fails on thin anti-aliased lines; try edge+color |
color |
--morph-open |
Same-color text blobs touching a thick curve; erodes then dilates to remove small blobs. Destroys thin (<3px) curves |
off |
--spatial-filter |
Curve matches frame/axis color — keeps only the largest connected line-like component |
off |
--cluster-centroid |
Text or axes bleed into mask — uses largest-cluster centroid instead of full-column median. Auto-enabled with --allow-black |
off |
--preset {lowres,thin-red} |
Quick combos: lowres = upscale 2 + tolerance 55 + edge+color; thin-red = tolerance 50, no morph-open |
none |
--debug |
Save intermediate crops/masks for diagnosing failures |
off |
--json-summary |
Emit machine-readable JSON summary to stdout after completion |
off |
Full flag list: python .agents/skills/general-plot-digitizer/scripts/digitize_pipeline.py --help
Examples
| Scenario |
Directory |
Key Flags |
| Single colored curve |
01-single-curve/ |
--curve-color |
| Multiple curves by color |
02-multi-curve-color/ |
--all-curves |
| Black curve + text masking |
03-black-curve-text-mask/ |
--allow-black --smooth, text_regions |
| Stacked spectra |
04-stacked-spectra/ |
--all-curves, per_curve_normalized |
Constraints
- Bounding box must enclose only the inner data region (inside axis lines, excluding labels/legend/title). VLMs often get this wrong — verify against the grid overlay.
- Multi-curve: Each curve in
curves[] must have a color_hint. Auto-detect is unreliable with multiple traces.
- Per-curve regions: For stacked/offset plots,
curves[].region.y_min/y_max must include generous padding (10-20px) above tallest peaks and below baseline.
- Environments: All scripts require
base-agent conda env.
Resources
- resources/metadata_schema.json — full metadata schema
- scripts/vlm_prompt_template.txt — VLM prompt guidelines
- scripts/suggest_colors.py — dominant color detection for unknown curve colors
Related Skills
References
- Gonzalez & Woods, Digital Image Processing, Pearson, 4th ed., 2018. Standard reference for HSV color-space segmentation, morphological operations, and connected-component analysis used in the extraction pipeline.
- Bradski, G., "The OpenCV Library", Dr. Dobb's Journal of Software Tools, 2000. Core CV library underlying all image manipulation in this skill.
Author: Jesus Diaz Sanchez
Contact: GitHub @jdsanc
1---2name: general-plot-digitizer3description: Extract continuous X-Y data from experimental spectrum images (Raman, XRD, UV-Vis, IR, etc.) via hybrid VLM + CV pipeline and agent-in-the-loop workflow.4---56# General Plot Digitizer78## Goal910Extract calibrated numeric X-Y data from images of experimental spectra (Raman, XRD, UV-Vis, IR, NMR, etc.) using a deterministic "Agent-in-the-Loop" workflow.1112The labor is divided between two models:131. **Vision-Language Model (Visual Sensor)**: Reads the image and returns a rich, unstructured narrative description of axes, colors, and visual obstacles. It does **not** produce JSON.142. **Coding Agent (Translator & Executor)**: Translates the VLM narrative into a precise `metadata.json`, runs the CV pipeline, inspects the overlay, and iterates until the curve is correctly isolated.1516## Instructions1718### Phase 1: Visual Inspection (VLM)1920Do **not** attempt to generate JSON with the VLM. It acts only as a visual sensor.21221. **Generate grid overlay**:23```bash24# Env: base-agent25python .agents/skills/general-plot-digitizer/scripts/plot_utils.py plot.png --draw-grid26```27This produces `plot_grid.png` with a labeled pixel grid for precise coordinate reading.28292. **Prompt the VLM** to analyze `plot_grid.png` (not the raw image). Use the built-in vision capabilities or the `notify_user` VLM inspection tool. Provide the prompt guidelines from [scripts/vlm_prompt_template.txt](scripts/vlm_prompt_template.txt).30313. **Expected VLM output** — a natural-language report covering:32 - Axis labels, numeric ranges, and directions (is X reversed?).33 - Bounding box of the data region in pixels (read from the grid).34 - Color and style of each target curve (hex guess from the pixels on the line itself).35 - Pixel bounding boxes of **all visual obstacles** (legends, text annotations, gridlines, tick marks) that overlap the data curves.36 - Trace quality hints: thin/needle-like, thick/noisy, anti-aliased, JPEG artifacts.3738### Phase 2: Metadata Construction (Coding Agent)3940Read the VLM narrative and construct `metadata.json`. Schema: [resources/metadata_schema.json](resources/metadata_schema.json).4142**Required fields:**43```json44{45 "plot_title": "",46 "x_axis_label": "Wavelength (nm)",47 "y_axis_label": "Absorbance",48 "x_tick_min": 400, "x_tick_max": 800,49 "y_tick_min": 0, "y_tick_max": 1,50 "x_calibration_points": [51 { "pixel": 70, "value": 400 },52 { "pixel": 450, "value": 800 }53 ],54 "x_scale": "linear", "y_scale": "linear",55 "bounding_box": {"x_min": 72, "y_min": 28, "x_max": 452, "y_max": 318},56 "x_reversed": false, "y_reversed": false,57 "spectrum_type": "UV-Vis",58 "curves": [{"label": "sample", "color_hint": "#1f77b4"}],59 "text_regions": [{"x_min": 300, "y_min": 50, "x_max": 400, "y_max": 80, "label": "legend"}]60}61```6263**`x_calibration_points`** (strongly recommended): anchor the X-axis transform to exact pixel→value pairs read from the grid, rather than assuming axis ticks align perfectly with bbox edges. Pick two well-separated ticks visible on the grid. If provided, these override `x_tick_min/max` for pixel-to-data mapping.6465**Translation rules** (VLM narrative → metadata fields):66- Obstacles → `text_regions[]` and/or `mask_regions[]` with pixel bounding boxes.67- Curve colors → `curves[].color_hint` (from **pixels on the plotted line**, not from legend swatches).68- "Curve is black / same as axes" → `"cli_hints": {"curve_is_black": true}`.69- "Noisy / jagged trace" → `"cli_hints": {"smooth": true}`.70- "Thin, needle-like peaks" → **do not** set smooth; set upscale in Phase 3 instead.7172**If the VLM color guess is uncertain**, run:73```bash74# Env: base-agent75python .agents/skills/general-plot-digitizer/scripts/suggest_colors.py plot.png \76 --bounding-box x_min,y_min,x_max,y_max77```78This reports dominant non-background colors in the cropped region. Use the top result as `color_hint`.7980### Phase 3: Pipeline Execution8182**Select CLI flags based on VLM visual cues:**8384| VLM describes... | Required CLI flags | Avoid |85|---|---|---|86| Thin, needle-like peaks (XRD, FTIR) | `--crop-upscale 4.0` | `--smooth`, `--cluster-centroid` |87| Fuzzy / anti-aliased / JPEG artifacts | `--curve-tolerance 75` (up to 85) | — |88| Thick, noisy trace / scatter points | `--smooth --smooth-window 5 --smooth-deviation 15.0` | — |89| Black curve on black axes | `--allow-black` (auto-enables `--spatial-filter --cluster-centroid`) | — |90| Thin anti-aliased colored line | `--extraction-method edge+color` | `--morph-open` |9192**Run the pipeline:**93```bash94# Env: base-agent95python .agents/skills/general-plot-digitizer/scripts/digitize_pipeline.py \96 plot.png \97 --full \98 --metadata metadata.json \99 --output-dir ./output \100 --overlay \101 --format both102```103Append the VLM-dictated flags from the table above. The pipeline also reads `cli_hints` from metadata and auto-applies safe flags (`--allow-black`, `--smooth`, `--all-curves`).104105### Phase 4: Overlay Inspection & Iteration1061071. **Inspect** `*_digitized.overlay.png` visually.1082. **If extraction is wrong**, diagnose using this table and re-run Phase 3:109110| Symptom | Fix (metadata or CLI) |111|---------|----------------------|112| Wrong curve extracted (e.g. legend ink) | Set `curves[].color_hint` from actual line pixels; or run `suggest_colors.py` on a tight crop |113| Text / labels contaminating trace | Add bounding boxes to `text_regions[]` in metadata; add `--smooth` |114| Black curve picks up axis lines | `--allow-black` + add axis regions to `mask_regions[]` |115| Trace too sparse / broken gaps | `--curve-tolerance 55` or `--preset lowres` |116| Thin line lost entirely | `--extraction-method edge+color`; or `--crop-upscale 4.0` for needle peaks |117| Same-color text blobs on thick curve | `--morph-open` (caution: destroys thin <3px curves) |118| Low-res image, everything pixelated | `--upscale-strategy force` (pre-upscales image, scales metadata bbox) |119| X/Y values shifted or inverted | Fix `x_tick_min/max`, `x_reversed`, `y_reversed`, or `x_calibration_points` in metadata |1201213. **Prefer metadata edits** (adjusting `color_hint`, `text_regions`, `mask_regions`, `bounding_box`) over adding CLI flags. Re-run the same pipeline command after editing `metadata.json`.122123**Outputs:**124- `*_digitized.csv` — comma-separated with `x,y` header125- `*_digitized.xy` — space-separated, no header (if `--format xy` or `both`)126- `*_digitized.overlay.png` — visual QC127- `*_digitized.md` — summary128129## Agent Rules130131- **Do not** write ad-hoc NumPy/OpenCV pixel-scanning code. The pipeline already handles HSV masking, spatial filtering, and smoothing internally.132- **Do not** skip the grid overlay step. The VLM is significantly more accurate on `plot_grid.png` than on raw images.133- **Do not** guess CLI flags without VLM visual evidence. The flag table above is the complete decision tree.134- **Always** inspect the overlay image after each run before declaring success.135136## Key Parameters137138| Flag | Use when... | Default |139|------|-------------|---------|140| `--curve-color HEX` | VLM identified a specific curve color | auto-detect |141| `--curve-tolerance N` | Trace is sparse or image has JPEG artifacts (increase); or mask bleeds into nearby colors (decrease) | 40 |142| `--overlay` | Always recommended for QC | off |143| `--format {csv,xy,both}` | Downstream tool needs specific format | csv |144| `--all-curves` | Multiple curves in metadata `curves[]` (auto-enabled when >1 curve) | off |145| `--allow-black` | Data curve is black (same color as axes/frame) | off |146| `--smooth` | Noisy, jagged, or thick trace with outlier points | off |147| `--smooth-window N` | Tune smoothing aggressiveness (larger = more smoothing) | 5 |148| `--smooth-deviation PX` | Pixel distance from local median beyond which a point is rejected as outlier | 15.0 |149| `--crop-upscale FACTOR` | Thin peaks (XRD, FTIR) need more pixel width to register; or generally low-res crop | 1.0 |150| `--upscale-strategy {none,auto,force}` | `auto` (default) pre-upscales when metadata or heuristic says low-res; `force` always pre-upscales; `none` skips | auto |151| `--upscale-factor FACTOR` | Controls the multiplier used by `--upscale-strategy auto\|force` | 2.0 |152| `--vlm-metadata-on-upscale` | After pre-upscale, re-run VLM metadata on upscaled image instead of just scaling coordinates. Only if API keys are set | off |153| `--extraction-method {color,edge,edge+color}` | `color` (default) fails on thin anti-aliased lines; try `edge+color` | color |154| `--morph-open` | Same-color text blobs touching a **thick** curve; erodes then dilates to remove small blobs. **Destroys thin (<3px) curves** | off |155| `--spatial-filter` | Curve matches frame/axis color — keeps only the largest connected line-like component | off |156| `--cluster-centroid` | Text or axes bleed into mask — uses largest-cluster centroid instead of full-column median. Auto-enabled with `--allow-black` | off |157| `--preset {lowres,thin-red}` | Quick combos: `lowres` = upscale 2 + tolerance 55 + edge+color; `thin-red` = tolerance 50, no morph-open | none |158| `--debug` | Save intermediate crops/masks for diagnosing failures | off |159| `--json-summary` | Emit machine-readable JSON summary to stdout after completion | off |160161Full flag list: `python .agents/skills/general-plot-digitizer/scripts/digitize_pipeline.py --help`162163## Examples164165| Scenario | Directory | Key Flags |166|----------|-----------|-----------|167| Single colored curve | [01-single-curve/](examples/01-single-curve/) | `--curve-color` |168| Multiple curves by color | [02-multi-curve-color/](examples/02-multi-curve-color/) | `--all-curves` |169| Black curve + text masking | [03-black-curve-text-mask/](examples/03-black-curve-text-mask/) | `--allow-black --smooth`, `text_regions` |170| Stacked spectra | [04-stacked-spectra/](examples/04-stacked-spectra/) | `--all-curves`, `per_curve_normalized` |171172## Constraints173174- **Bounding box** must enclose only the inner data region (inside axis lines, excluding labels/legend/title). VLMs often get this wrong — verify against the grid overlay.175- **Multi-curve:** Each curve in `curves[]` must have a `color_hint`. Auto-detect is unreliable with multiple traces.176- **Per-curve regions:** For stacked/offset plots, `curves[].region.y_min/y_max` must include generous padding (10-20px) above tallest peaks and below baseline.177- **Environments:** All scripts require `base-agent` conda env.178179## Resources180181- [resources/metadata_schema.json](resources/metadata_schema.json) — full metadata schema182- [scripts/vlm_prompt_template.txt](scripts/vlm_prompt_template.txt) — VLM prompt guidelines183- [scripts/suggest_colors.py](scripts/suggest_colors.py) — dominant color detection for unknown curve colors184185## Related Skills186187- [mat-xrd-digitizer](../mat-xrd-digitizer/SKILL.md): Peak-based XRD digitization for phase matching.188189## References190191- Gonzalez & Woods, *Digital Image Processing*, Pearson, 4th ed., 2018. Standard reference for HSV color-space segmentation, morphological operations, and connected-component analysis used in the extraction pipeline.192- Bradski, G., "The OpenCV Library", *Dr. Dobb's Journal of Software Tools*, 2000. Core CV library underlying all image manipulation in this skill.193194---195196**Author:** Jesus Diaz Sanchez197**Contact:** [GitHub @jdsanc](https://github.com/jdsanc)