trace-digitiser 1.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trace_digitiser-2.0.0/CHANGELOG.md +109 -0
- trace_digitiser-2.0.0/MANIFEST.in +7 -0
- trace_digitiser-2.0.0/PKG-INFO +296 -0
- trace_digitiser-2.0.0/README.md +256 -0
- trace_digitiser-2.0.0/RELEASING.md +68 -0
- trace_digitiser-2.0.0/examples/README.md +37 -0
- trace_digitiser-2.0.0/examples/digitise_published_figure.py +47 -0
- trace_digitiser-2.0.0/examples/lopatina2014_fig4.jpg +0 -0
- trace_digitiser-2.0.0/examples/validate_with_known_data.py +44 -0
- trace_digitiser-2.0.0/examples/walkthrough.ipynb +669 -0
- trace_digitiser-2.0.0/pyproject.toml +87 -0
- trace_digitiser-2.0.0/src/trace_digitiser/__init__.py +625 -0
- trace_digitiser-2.0.0/src/trace_digitiser/axis_labels.py +1290 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/calibration.py +201 -11
- trace_digitiser-2.0.0/src/trace_digitiser/cli.py +394 -0
- trace_digitiser-2.0.0/src/trace_digitiser/diagnostics.py +428 -0
- trace_digitiser-2.0.0/src/trace_digitiser/digitise.py +112 -0
- trace_digitiser-2.0.0/src/trace_digitiser/frames.py +538 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/io.py +36 -5
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/line_detection.py +0 -1
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/models.py +88 -15
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/ocr.py +7 -8
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/panel_detection.py +221 -115
- trace_digitiser-2.0.0/src/trace_digitiser/provenance.py +162 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/resampling.py +0 -1
- trace_digitiser-2.0.0/src/trace_digitiser/straighten.py +95 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/synthetic.py +0 -1
- trace_digitiser-2.0.0/src/trace_digitiser/trace_detection.py +646 -0
- trace_digitiser-2.0.0/src/trace_digitiser/validation.py +1244 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/x_calibration.py +99 -9
- trace_digitiser-2.0.0/src/trace_digitiser.egg-info/PKG-INFO +296 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser.egg-info/SOURCES.txt +26 -1
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser.egg-info/requires.txt +3 -0
- trace_digitiser-2.0.0/tests/__init__.py +0 -0
- trace_digitiser-2.0.0/tests/conftest.py +125 -0
- trace_digitiser-2.0.0/tests/data/lopatina2014_fig4_reference.csv +697 -0
- trace_digitiser-2.0.0/tests/test_axis_labels.py +440 -0
- trace_digitiser-2.0.0/tests/test_calibration.py +116 -0
- trace_digitiser-2.0.0/tests/test_cli.py +132 -0
- trace_digitiser-2.0.0/tests/test_digitise.py +304 -0
- trace_digitiser-2.0.0/tests/test_frames.py +208 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/tests/test_integration.py +49 -1
- trace_digitiser-2.0.0/tests/test_io.py +61 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/tests/test_panel_detection.py +0 -2
- trace_digitiser-2.0.0/tests/test_published_figure.py +101 -0
- trace_digitiser-2.0.0/tests/test_straighten.py +149 -0
- trace_digitiser-2.0.0/tests/test_trace_detection.py +169 -0
- trace_digitiser-2.0.0/tests/test_validation.py +253 -0
- trace_digitiser-1.2.0/PKG-INFO +0 -178
- trace_digitiser-1.2.0/README.md +0 -157
- trace_digitiser-1.2.0/pyproject.toml +0 -43
- trace_digitiser-1.2.0/src/trace_digitiser/__init__.py +0 -276
- trace_digitiser-1.2.0/src/trace_digitiser/cli.py +0 -179
- trace_digitiser-1.2.0/src/trace_digitiser/diagnostics.py +0 -283
- trace_digitiser-1.2.0/src/trace_digitiser/digitise.py +0 -85
- trace_digitiser-1.2.0/src/trace_digitiser/trace_detection.py +0 -431
- trace_digitiser-1.2.0/src/trace_digitiser.egg-info/PKG-INFO +0 -178
- trace_digitiser-1.2.0/tests/test_trace_detection.py +0 -35
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/LICENSE +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/setup.cfg +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/geometry.py +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser/summarise.py +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser.egg-info/dependency_links.txt +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser.egg-info/entry_points.txt +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/src/trace_digitiser.egg-info/top_level.txt +0 -0
- {trace_digitiser-1.2.0 → trace_digitiser-2.0.0}/tests/test_geometry.py +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to trace-digitiser are recorded here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
|
+
and the project uses [semantic versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [2.0.0] - 2026-11-14
|
|
8
|
+
|
|
9
|
+
Every call and command-line option of 1.2.0 still works, with the same
|
|
10
|
+
meaning. The version number changes because the **results** change: figures
|
|
11
|
+
are analysed in a new way, and the values that come out differ from those of
|
|
12
|
+
1.2.0. By the package's own tests against known data they are much closer to
|
|
13
|
+
what was drawn (55 of 57 test traces recovered to within 2 % with nothing
|
|
14
|
+
supplied, against 20; see the README). Anything digitised with 1.x that
|
|
15
|
+
matters should be digitised again.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Plot areas are found without being told the layout.** Axis lines are
|
|
20
|
+
located to a fraction of a pixel and assembled into plot areas: any number
|
|
21
|
+
of panels, boxed or L-shaped axes, with or without gridlines and tick
|
|
22
|
+
marks. `layout_mode`, `expected_rows` and the rest remain as hints and
|
|
23
|
+
checks.
|
|
24
|
+
- **Tilted scans are straightened.** The tilt of the figure is measured and
|
|
25
|
+
undone first (`straighten="auto"`, `--straighten`), so a page that lay
|
|
26
|
+
askew on the scanner is digitised as accurately as one that did not.
|
|
27
|
+
- **Each automatic step can be replaced by the answer.** `plot_area` for the
|
|
28
|
+
position of the axes, `y_ref` / `x_ref` for reference points on an axis,
|
|
29
|
+
`y_scale` / `x_scale` for logarithmic axes, `trace_colours` for the
|
|
30
|
+
colours to follow, `exclude` for legends and annotations, `achromatic` for
|
|
31
|
+
black and grey traces, `ocr=False` to do without Tesseract.
|
|
32
|
+
- **The x axis can be calibrated** from `x_min` / `x_max`, from reference
|
|
33
|
+
points, or from numeric tick labels (`x_value` in the output).
|
|
34
|
+
- **Axis scales read from tick labels come with a confidence rating**, and a
|
|
35
|
+
scale that too few labels agree on is declined rather than guessed. The
|
|
36
|
+
rating, and the labels the scale rests on, are in the panel metadata.
|
|
37
|
+
- **Messages about anything that needs checking**: `result.warnings`,
|
|
38
|
+
`DigitiserWarning`, and `CHECK:` lines on the command line.
|
|
39
|
+
- **An overlay image** of the result drawn over the figure (`save_overlay`,
|
|
40
|
+
`--overlay`, `result.overlay()`): the quickest way to see whether a
|
|
41
|
+
digitisation is right.
|
|
42
|
+
- **A provenance file** for every run: version, SHA-256 of the image, all
|
|
43
|
+
settings, what was detected, and all messages.
|
|
44
|
+
- **Validation tools** (`trace_digitiser.validation`): `round_trip` tests the
|
|
45
|
+
digitiser on your own data drawn at the size of your figure;
|
|
46
|
+
`run_validation` (`--validate`) and `run_axis_validation` test it on 44
|
|
47
|
+
figures and 750 axes drawn from known data.
|
|
48
|
+
- New columns in the trace file: `n_pixels`, `interpolated`, `x_value`. New
|
|
49
|
+
columns at the end of the panel metadata: the sub-pixel plot area and how
|
|
50
|
+
each axis was calibrated. The 1.2.0 columns keep their names and order.
|
|
51
|
+
- Image arrays are accepted as well as file names; transparent images are
|
|
52
|
+
placed on white, and photographs are turned according to their EXIF data.
|
|
53
|
+
- `--version`; exit status 1 if any image could not be processed.
|
|
54
|
+
- Examples: two scripts, a notebook, and a published figure (CC BY) that is
|
|
55
|
+
also used in the tests.
|
|
56
|
+
- The repository itself: continuous testing on Python 3.10 to 3.14, and a
|
|
57
|
+
release workflow that publishes to PyPI through trusted publishing (see
|
|
58
|
+
`RELEASING.md`).
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Values differ from 1.2.0**, chiefly because:
|
|
63
|
+
- the plot area is now bounded by the centres of the axis lines; 1.2.0 put
|
|
64
|
+
its edges up to 13 pixels outside them when tick marks pointed outwards,
|
|
65
|
+
which shifted and rescaled every value read with `y_min` / `y_max`;
|
|
66
|
+
- traces are followed down onto the axis lines when their colour allows,
|
|
67
|
+
and thin, pale, grey and black traces are no longer missed;
|
|
68
|
+
- gridlines, text and compression fringes are no longer reported as
|
|
69
|
+
extra traces (1.2.0 did so in about half of single-trace test figures).
|
|
70
|
+
- Axis labels are read in a new way (located geometrically, read by
|
|
71
|
+
Tesseract in several ways, and fitted on the principle that tick values
|
|
72
|
+
step by exactly the same amount). On the test axes it never gave a wrong
|
|
73
|
+
scale with high confidence; 1.2.0 misread decimal and logarithmic axes by
|
|
74
|
+
large factors without warning. It takes about half a second a figure
|
|
75
|
+
rather than twenty.
|
|
76
|
+
- Tesseract is now optional: without it, give the axis limits.
|
|
77
|
+
- Labels under 8 pixels high are read but never trusted outright; the
|
|
78
|
+
result carries a message asking for the values to be checked.
|
|
79
|
+
- `mean_estimate` and `center_estimate` are documented for what they are
|
|
80
|
+
(statistics of the trace's pixels in a column), with the consequence for
|
|
81
|
+
dense records spelt out in the README.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- A one-pixel shift in the trace mask (from a 2×2 morphological opening)
|
|
86
|
+
that erased one-pixel lines and produced all-white "traces".
|
|
87
|
+
- `tests/conftest.py` was missing from the 1.2.0 source distribution, so its
|
|
88
|
+
tests could not be run from it.
|
|
89
|
+
- `use_lab=True` needed scikit-learn, and one helper needed SciPy, without
|
|
90
|
+
either being declared. scikit-learn is now the optional extra
|
|
91
|
+
`trace-digitiser[lab]`; SciPy is no longer used.
|
|
92
|
+
- `eval` of text read back from a data frame replaced by `ast.literal_eval`.
|
|
93
|
+
- The trace-accuracy test that failed in 1.2.0 (RMSE 102 against a limit of
|
|
94
|
+
50) passes, and a new one holds all eight example figures to 7.5.
|
|
95
|
+
|
|
96
|
+
### Removed
|
|
97
|
+
|
|
98
|
+
- The slow "exhaustive" reading of y labels is no longer tried by default
|
|
99
|
+
(`calibrate_y(..., exhaustive_fallback=True)` still does it). On axes that
|
|
100
|
+
the new method declined, it was right in none of twenty trials.
|
|
101
|
+
|
|
102
|
+
## [1.2.0] - 2026-06-07
|
|
103
|
+
|
|
104
|
+
First version tracked in this repository; identical to the 1.2.0 files on PyPI.
|
|
105
|
+
Releases 0.1.0 to 1.0.0 (6-7 June 2026) were uploaded to PyPI directly and are
|
|
106
|
+
not recorded here.
|
|
107
|
+
|
|
108
|
+
[2.0.0]: https://github.com/comparativechrono/trace-digitiser/compare/1d34f75...v2.0.0
|
|
109
|
+
[1.2.0]: https://pypi.org/project/trace-digitiser/1.2.0/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Ship the complete test suite and the examples in the source distribution.
|
|
2
|
+
# (Without this, setuptools includes tests/test_*.py but not conftest.py or
|
|
3
|
+
# the reference data, so the tests in the sdist cannot run.)
|
|
4
|
+
recursive-include tests *.py *.csv
|
|
5
|
+
recursive-include examples *.py *.md *.jpg *.ipynb
|
|
6
|
+
prune examples/output
|
|
7
|
+
include CHANGELOG.md RELEASING.md
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trace-digitiser
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Recover the data from line plots in figure images, on your own computer: no uploads, no AI model.
|
|
5
|
+
Author: Trace Digitiser Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/comparativechrono/trace-digitiser
|
|
8
|
+
Project-URL: Repository, https://github.com/comparativechrono/trace-digitiser
|
|
9
|
+
Project-URL: Issues, https://github.com/comparativechrono/trace-digitiser/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: plot digitizer,graph digitiser,data extraction,data rescue,computer vision,time series,circadian,actigraphy
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: opencv-python-headless>=4.8
|
|
28
|
+
Requires-Dist: pytesseract>=0.3.10
|
|
29
|
+
Requires-Dist: Pillow>=10.0
|
|
30
|
+
Requires-Dist: pandas>=2.0
|
|
31
|
+
Requires-Dist: numpy>=1.24
|
|
32
|
+
Requires-Dist: matplotlib>=3.7
|
|
33
|
+
Provides-Extra: lab
|
|
34
|
+
Requires-Dist: scikit-learn>=1.2; extra == "lab"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# trace-digitiser
|
|
42
|
+
|
|
43
|
+
Get the numbers back out of a line plot when the figure is all that survives — on your own computer.
|
|
44
|
+
|
|
45
|
+
`trace-digitiser` takes an image of a figure (a scan of an old paper, a figure saved from a PDF),
|
|
46
|
+
finds the plot area, follows each trace, and writes the values to a CSV file. It is ordinary image
|
|
47
|
+
processing, written to be read and checked.
|
|
48
|
+
|
|
49
|
+
**Nothing is uploaded and no AI model is involved.** The only other program it runs is
|
|
50
|
+
[Tesseract](https://github.com/tesseract-ocr/tesseract), also on your machine and optional, to read
|
|
51
|
+
the numbers printed beside the axes. A figure that you are not free to pass to an online service
|
|
52
|
+
can therefore still be digitised. (Whether the values in a published figure may be *reused* is a
|
|
53
|
+
matter for the licence of the source and the law where you work; no tool can settle that for you.)
|
|
54
|
+
|
|
55
|
+
A worked example with pictures is in
|
|
56
|
+
[`examples/walkthrough.ipynb`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/walkthrough.ipynb).
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install trace-digitiser
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Python 3.10 or later. To have axis values read from the figure, also install Tesseract
|
|
65
|
+
(`sudo apt-get install tesseract-ocr`, `brew install tesseract`, or the
|
|
66
|
+
[Windows installer](https://github.com/UB-Mannheim/tesseract/wiki)). Without it everything else
|
|
67
|
+
works, and you type in the axis limits yourself — which is the better practice anyway.
|
|
68
|
+
|
|
69
|
+
## Use
|
|
70
|
+
|
|
71
|
+
From the command line:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
trace-digitiser figure.png --y-min 0 --y-max 300 --x-min 0 --x-max 7 --overlay -o results/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
or from Python:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from trace_digitiser import digitise
|
|
81
|
+
|
|
82
|
+
result = digitise("figure.png", y_min=0, y_max=300, x_min=0, x_max=7,
|
|
83
|
+
output_dir="results", save_overlay=True)
|
|
84
|
+
|
|
85
|
+
result.trace_data # one row per pixel column of each trace
|
|
86
|
+
result.warnings # anything you should check
|
|
87
|
+
result.overlay_path # the digitised traces drawn over the figure
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`y_min` and `y_max` are the values at the bottom and top axis lines, `x_min` and `x_max` those at
|
|
91
|
+
the left and right. Nothing else is needed for most figures: the plot areas (any number of panels)
|
|
92
|
+
and the traces (coloured, black or grey) are found automatically, and a scan that lies a few
|
|
93
|
+
degrees askew is straightened first.
|
|
94
|
+
|
|
95
|
+
Leave the limits out and the scale is read from the tick labels instead:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
trace-digitiser figure.png --overlay -o results/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Panels: 2, Traces: 2
|
|
103
|
+
panel 1: y 0.31 to 299, read from tick labels (medium confidence); x not calibrated
|
|
104
|
+
CHECK: Panel 1: the y-axis scale was read from 7 of 7 tick labels (300, 250, 200, 150, 100, 50, 0);
|
|
105
|
+
the print is only 6 pixels high. Confidence in it is medium: check those values against
|
|
106
|
+
the figure, or give y_min and y_max.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A scale read from labels always comes with a statement of how far to trust it and, unless that is
|
|
110
|
+
"high", of the labels it rests on. If too few labels can be read to agree on a scale, the axis is
|
|
111
|
+
left uncalibrated (values from 0 to 1) and you are told so: the aim is never to pass off a wrong
|
|
112
|
+
scale as a good one ([how well that is met](#how-accurate-is-it)).
|
|
113
|
+
|
|
114
|
+
## Check the result
|
|
115
|
+
|
|
116
|
+
1. **Look at the overlay** (`--overlay`). It shows the plot area that was found, the centre and
|
|
117
|
+
envelope of each digitised trace drawn over the original, and the positions of the axis values.
|
|
118
|
+
If the lines sit on the traces, the digitisation is right; if they do not, you can see where.
|
|
119
|
+
2. **Read the messages.** Everything the program is unsure of is printed as a `CHECK:` line
|
|
120
|
+
(in Python: `result.warnings`, also raised as `DigitiserWarning`). A clean run prints none.
|
|
121
|
+
3. **Keep the provenance file.** `<name>_automated_provenance.json` records the version, the
|
|
122
|
+
SHA-256 of the image, every setting, what was detected and all messages, so that a digitisation
|
|
123
|
+
can be reported and repeated exactly.
|
|
124
|
+
|
|
125
|
+
## Output
|
|
126
|
+
|
|
127
|
+
| File | Contents |
|
|
128
|
+
|------|----------|
|
|
129
|
+
| `<name>_automated_digitised_trace_by_column.csv` | One row per pixel column per trace (below). |
|
|
130
|
+
| `<name>_automated_panel_metadata.csv` | Where each plot area is, its axis calibrations and how they were obtained. |
|
|
131
|
+
| `<name>_automated_digitised_summary_by_label.csv` | Means per labelled interval, when the x axis carries interval names (`1N`, `1D`, …). |
|
|
132
|
+
| `<name>_automated_provenance.json` | Everything needed to report and repeat the run. |
|
|
133
|
+
| `<name>_automated_overlay.png` | With `--overlay`: the result drawn over the figure. |
|
|
134
|
+
| `<name>_automated_straightened.png` | Only if the figure was tilted: the image turned upright, which is what pixel positions in the other files refer to. |
|
|
135
|
+
|
|
136
|
+
Columns of the trace file:
|
|
137
|
+
|
|
138
|
+
| Column | Meaning |
|
|
139
|
+
|--------|---------|
|
|
140
|
+
| `panel_id`, `trace_id` | Panels are numbered row by row from the top left; traces within a panel from 1. |
|
|
141
|
+
| `x_pixel`, `x_norm_0_to_1` | The pixel column, and its position between the left (0) and right (1) axis lines. |
|
|
142
|
+
| `x_value` | The x value, if the x axis is calibrated. |
|
|
143
|
+
| `center_estimate` | Middle of the trace's ink in this column (median of its pixels). |
|
|
144
|
+
| `mean_estimate` | Mean of the trace's pixels in this column. |
|
|
145
|
+
| `upper_envelope`, `lower_envelope` | Top and bottom of the trace's ink in this column. |
|
|
146
|
+
| `n_pixels`, `interpolated` | How many pixels the trace has in this column; `True` if it has none and the values were interpolated from its neighbours. |
|
|
147
|
+
| `scale_type` | Where the y values come from: `user_supplied`, `ocr_value`, `normalised_0_to_1`, … |
|
|
148
|
+
| `trace_median_rgb`, `trace_pixel_count` | The trace's colour and size, for identifying it. |
|
|
149
|
+
|
|
150
|
+
`result.resample(...)` and `--sampling-interval` put the trace on an even grid in x.
|
|
151
|
+
|
|
152
|
+
## What the values are — and are not
|
|
153
|
+
|
|
154
|
+
A digitiser recovers what was **drawn**, not what was measured.
|
|
155
|
+
|
|
156
|
+
* For an ordinary curve — a thin line with at most a few samples per pixel column —
|
|
157
|
+
`center_estimate` is the plotted value, to within a fraction of a pixel.
|
|
158
|
+
* For a **dense record**, where many samples share each pixel column and the trace is drawn as a
|
|
159
|
+
solid band (an activity record of several days in a small figure, for instance), the drawing
|
|
160
|
+
shows only the *range* of the samples in each column. `upper_envelope` and `lower_envelope`
|
|
161
|
+
recover that range; `center_estimate` is its middle. **None of them is the mean of the samples**,
|
|
162
|
+
and for spiky data the middle of the range can be far from the mean. The timing of the rhythm
|
|
163
|
+
survives; the level does not.
|
|
164
|
+
|
|
165
|
+
How much this matters for a particular kind of data and size of figure can be measured rather than
|
|
166
|
+
guessed: see [Test it on data you know](#test-it-on-data-you-know).
|
|
167
|
+
|
|
168
|
+
Positions cannot be better than the image. In a plot area 350 pixels wide showing seven days, one
|
|
169
|
+
pixel is half an hour.
|
|
170
|
+
|
|
171
|
+
## When something is not found
|
|
172
|
+
|
|
173
|
+
Each automatic step can be replaced by telling the program the answer. Pixel positions count from
|
|
174
|
+
the top-left corner of the image, starting at 0; any image viewer shows them.
|
|
175
|
+
|
|
176
|
+
| Problem | Option | Python |
|
|
177
|
+
|---------|--------|--------|
|
|
178
|
+
| The figure is tilted and was not straightened, or was turned when it should not have been | `--straighten DEGREES` (its anticlockwise tilt), `--straighten never` | `straighten=1.3`, `straighten=False` |
|
|
179
|
+
| The plot area is wrong or not found | `--plot-area LEFT,TOP,RIGHT,BOTTOM` (repeat per panel) | `plot_area=(l, t, r, b)` |
|
|
180
|
+
| The wrong number of panels | `--layout`, `--rows`, `--cols`, `--panels` | `layout_mode`, `expected_rows`, … |
|
|
181
|
+
| The axis continues beyond its last tick, or has no line at the top or right | `--y-ref ROW:VALUE` twice or more (also `--x-ref`) | `y_ref=[(row, value), …]` |
|
|
182
|
+
| A logarithmic axis, with limits given | `--y-scale log` | `y_scale="log"` |
|
|
183
|
+
| A trace is missed, or two are merged | `--trace-colour RRGGBB` (repeat per trace) | `trace_colours=["#578dd2", …]` |
|
|
184
|
+
| A legend or annotation is taken for a trace | `--exclude LEFT,TOP,RIGHT,BOTTOM` | `exclude=(l, t, r, b)` |
|
|
185
|
+
| A black trace is invented or ignored | `--achromatic never` / `always` | `achromatic="never"` |
|
|
186
|
+
| No Tesseract, or it should not be used | `--no-ocr` | `ocr=False` |
|
|
187
|
+
|
|
188
|
+
## How accurate is it?
|
|
189
|
+
|
|
190
|
+
The package carries its own tests against known answers, and you can rerun them.
|
|
191
|
+
|
|
192
|
+
**Traces.** `trace-digitiser --validate results/` draws 44 figures (57 traces) from known data —
|
|
193
|
+
different axis styles, twelve colours including black and grey, 60 to 300 dpi, JPEG compression,
|
|
194
|
+
noise and blur, line widths, multi-panel layouts, several traces per panel, a logarithmic axis,
|
|
195
|
+
jagged and dense records, tilted scans — digitises them, and compares. Errors are in percent of the
|
|
196
|
+
height of the plot. A trace counts as *recovered* if its panel and curve were found with no
|
|
197
|
+
spurious extra trace, x positions are within 1 % and the RMS error in y is within 2 %.
|
|
198
|
+
|
|
199
|
+
| | found | recovered | median error | time per figure¹ |
|
|
200
|
+
|---|---|---|---|---|
|
|
201
|
+
| **2.0, nothing supplied** | 57 of 57 | 55 | 0.13 % | 0.5 s |
|
|
202
|
+
| **2.0, axis limits supplied** | 57 of 57 | 55 | 0.09 % | 0.3 s |
|
|
203
|
+
| 1.2.0, panel layout supplied | 48 of 57 | 20 | 0.49 % | 20 s |
|
|
204
|
+
| 1.2.0, panel layout and axis limits supplied | 48 of 57 | 13 | 1.39 % | 0.3 s |
|
|
205
|
+
|
|
206
|
+
¹ On a two-core machine.
|
|
207
|
+
|
|
208
|
+
The two traces that 2.0 does not recover are dense records in low-resolution figures (errors of
|
|
209
|
+
2.3 % and 4.3 %), for the reason given [above](#what-the-values-are--and-are-not). Of the traces
|
|
210
|
+
that 1.2.0 found but did not recover, most came with a spurious second trace.
|
|
211
|
+
|
|
212
|
+
**Axis scales.** Reading a scale from tick labels is the one step that depends on OCR, and OCR of
|
|
213
|
+
small print is unreliable, so it is tested separately:
|
|
214
|
+
`python -m trace_digitiser.validation results/ --axes` reads 750 axes (whole numbers, decimals,
|
|
215
|
+
negative values, thousands, clock and angle steps, logarithmic; print from 5 to 24 pixels high;
|
|
216
|
+
three typefaces; JPEG compression and blur).
|
|
217
|
+
|
|
218
|
+
| Height of the labels | axes | read correctly | declined | read wrongly, with a warning | read wrongly, no warning |
|
|
219
|
+
|---|---|---|---|---|---|
|
|
220
|
+
| 8 pixels or more | 477 | 464 | 13 | 0 | 0 |
|
|
221
|
+
| under 8 pixels | 273 | 257 (all with a warning to check them) | 15 | 1 | 0 |
|
|
222
|
+
|
|
223
|
+
*Declined* means that no scale was offered. All of those at 8 pixels or more are logarithmic axes
|
|
224
|
+
whose raised exponents are too small to read, though the rest of the label is not. The outcome for small
|
|
225
|
+
print depends on how the text was rasterised: with the test figures drawn by matplotlib 3.7, which
|
|
226
|
+
renders small print more softly than current versions, 236 of 292 axes under 8 pixels were read
|
|
227
|
+
correctly, 46 declined and 10 read wrongly with a warning — and still none without.
|
|
228
|
+
|
|
229
|
+
Labels under 8 pixels high are never given high confidence, however well they agree, because print
|
|
230
|
+
that small is misread in consistent ways — the decimal point dropped from every label, say. If
|
|
231
|
+
your figure has such labels, enlarging the image does not help; typing in two numbers does.
|
|
232
|
+
|
|
233
|
+
### Test it on data you know
|
|
234
|
+
|
|
235
|
+
The best test of a digitisation is one on data like yours. `round_trip` draws a known series as a
|
|
236
|
+
figure of whatever size and style you choose, digitises the drawing, and reports the errors:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from trace_digitiser.validation import round_trip
|
|
240
|
+
|
|
241
|
+
check = round_trip(t, y, "check/", xlim=(0, 7), ylim=(0, 300),
|
|
242
|
+
panel_px=(347, 137), dpi=72, linewidth=0.8, fmt="jpg")
|
|
243
|
+
check["metrics"] # errors of centre and envelopes, correlation with the mean per column, …
|
|
244
|
+
check["overlay"] # the picture to look at
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
[`examples/validate_with_known_data.py`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/validate_with_known_data.py)
|
|
248
|
+
does this for a week of minute-by-minute activity drawn at the size of a small journal figure.
|
|
249
|
+
|
|
250
|
+
## Limits
|
|
251
|
+
|
|
252
|
+
* The plot must have at least a left and a bottom axis line, drawn in black or grey. A figure
|
|
253
|
+
that is askew is straightened automatically if the tilt is under five degrees; beyond that,
|
|
254
|
+
or for a photograph taken at an angle (where the axes are no longer parallel), correct the
|
|
255
|
+
image first.
|
|
256
|
+
* One trace per colour per panel. Two traces of the same colour, dashed or dotted lines, and
|
|
257
|
+
traces that are only markers are not separated or joined up.
|
|
258
|
+
* Where traces cross or overlap, the one drawn underneath is hidden and its values there are
|
|
259
|
+
interpolated (`interpolated` is `True`).
|
|
260
|
+
* Axis scales are read only from numeric labels on linear or logarithmic axes; dates, broken axes
|
|
261
|
+
and a second y axis are not understood. Limits typed in are assumed to be at the axis lines
|
|
262
|
+
unless `y_ref` / `x_ref` say otherwise.
|
|
263
|
+
* Error bars, shaded bands, bars and scatter plots are not digitised.
|
|
264
|
+
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
git clone https://github.com/comparativechrono/trace-digitiser.git
|
|
269
|
+
cd trace-digitiser
|
|
270
|
+
pip install -e ".[dev]"
|
|
271
|
+
pytest # the tests that need Tesseract are skipped without it
|
|
272
|
+
ruff check src tests
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`src/trace_digitiser/` in the order the work is done:
|
|
276
|
+
|
|
277
|
+
| Module | Does |
|
|
278
|
+
|--------|------|
|
|
279
|
+
| `straighten.py` | Measures the tilt of a scan and turns it upright. |
|
|
280
|
+
| `frames.py`, `panel_detection.py` | Finds axis lines and assembles them into plot areas. |
|
|
281
|
+
| `axis_labels.py`, `calibration.py`, `x_calibration.py` | Locates and reads tick labels, fits the scale, rates its confidence; or takes the limits given. |
|
|
282
|
+
| `trace_detection.py` | Separates the traces from gridlines, axes and text. |
|
|
283
|
+
| `digitise.py` | Converts each trace to values, column by column. |
|
|
284
|
+
| `ocr.py`, `summarise.py` | Interval labels along the x axis, and means per interval. |
|
|
285
|
+
| `diagnostics.py`, `provenance.py`, `io.py` | Overlay, record of the run, files. |
|
|
286
|
+
| `validation.py`, `synthetic.py` | Figures drawn from known data, and the comparisons. |
|
|
287
|
+
| `cli.py` | The command line. |
|
|
288
|
+
|
|
289
|
+
Changes are listed in [CHANGELOG.md](https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md);
|
|
290
|
+
making a release is described in
|
|
291
|
+
[RELEASING.md](https://github.com/comparativechrono/trace-digitiser/blob/main/RELEASING.md).
|
|
292
|
+
|
|
293
|
+
## Licence
|
|
294
|
+
|
|
295
|
+
MIT; see `LICENSE`. The example figure has its own licence and attribution, given in
|
|
296
|
+
[`examples/README.md`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/README.md).
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# trace-digitiser
|
|
2
|
+
|
|
3
|
+
Get the numbers back out of a line plot when the figure is all that survives — on your own computer.
|
|
4
|
+
|
|
5
|
+
`trace-digitiser` takes an image of a figure (a scan of an old paper, a figure saved from a PDF),
|
|
6
|
+
finds the plot area, follows each trace, and writes the values to a CSV file. It is ordinary image
|
|
7
|
+
processing, written to be read and checked.
|
|
8
|
+
|
|
9
|
+
**Nothing is uploaded and no AI model is involved.** The only other program it runs is
|
|
10
|
+
[Tesseract](https://github.com/tesseract-ocr/tesseract), also on your machine and optional, to read
|
|
11
|
+
the numbers printed beside the axes. A figure that you are not free to pass to an online service
|
|
12
|
+
can therefore still be digitised. (Whether the values in a published figure may be *reused* is a
|
|
13
|
+
matter for the licence of the source and the law where you work; no tool can settle that for you.)
|
|
14
|
+
|
|
15
|
+
A worked example with pictures is in
|
|
16
|
+
[`examples/walkthrough.ipynb`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/walkthrough.ipynb).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install trace-digitiser
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Python 3.10 or later. To have axis values read from the figure, also install Tesseract
|
|
25
|
+
(`sudo apt-get install tesseract-ocr`, `brew install tesseract`, or the
|
|
26
|
+
[Windows installer](https://github.com/UB-Mannheim/tesseract/wiki)). Without it everything else
|
|
27
|
+
works, and you type in the axis limits yourself — which is the better practice anyway.
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
From the command line:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
trace-digitiser figure.png --y-min 0 --y-max 300 --x-min 0 --x-max 7 --overlay -o results/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
or from Python:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from trace_digitiser import digitise
|
|
41
|
+
|
|
42
|
+
result = digitise("figure.png", y_min=0, y_max=300, x_min=0, x_max=7,
|
|
43
|
+
output_dir="results", save_overlay=True)
|
|
44
|
+
|
|
45
|
+
result.trace_data # one row per pixel column of each trace
|
|
46
|
+
result.warnings # anything you should check
|
|
47
|
+
result.overlay_path # the digitised traces drawn over the figure
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`y_min` and `y_max` are the values at the bottom and top axis lines, `x_min` and `x_max` those at
|
|
51
|
+
the left and right. Nothing else is needed for most figures: the plot areas (any number of panels)
|
|
52
|
+
and the traces (coloured, black or grey) are found automatically, and a scan that lies a few
|
|
53
|
+
degrees askew is straightened first.
|
|
54
|
+
|
|
55
|
+
Leave the limits out and the scale is read from the tick labels instead:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
trace-digitiser figure.png --overlay -o results/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Panels: 2, Traces: 2
|
|
63
|
+
panel 1: y 0.31 to 299, read from tick labels (medium confidence); x not calibrated
|
|
64
|
+
CHECK: Panel 1: the y-axis scale was read from 7 of 7 tick labels (300, 250, 200, 150, 100, 50, 0);
|
|
65
|
+
the print is only 6 pixels high. Confidence in it is medium: check those values against
|
|
66
|
+
the figure, or give y_min and y_max.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A scale read from labels always comes with a statement of how far to trust it and, unless that is
|
|
70
|
+
"high", of the labels it rests on. If too few labels can be read to agree on a scale, the axis is
|
|
71
|
+
left uncalibrated (values from 0 to 1) and you are told so: the aim is never to pass off a wrong
|
|
72
|
+
scale as a good one ([how well that is met](#how-accurate-is-it)).
|
|
73
|
+
|
|
74
|
+
## Check the result
|
|
75
|
+
|
|
76
|
+
1. **Look at the overlay** (`--overlay`). It shows the plot area that was found, the centre and
|
|
77
|
+
envelope of each digitised trace drawn over the original, and the positions of the axis values.
|
|
78
|
+
If the lines sit on the traces, the digitisation is right; if they do not, you can see where.
|
|
79
|
+
2. **Read the messages.** Everything the program is unsure of is printed as a `CHECK:` line
|
|
80
|
+
(in Python: `result.warnings`, also raised as `DigitiserWarning`). A clean run prints none.
|
|
81
|
+
3. **Keep the provenance file.** `<name>_automated_provenance.json` records the version, the
|
|
82
|
+
SHA-256 of the image, every setting, what was detected and all messages, so that a digitisation
|
|
83
|
+
can be reported and repeated exactly.
|
|
84
|
+
|
|
85
|
+
## Output
|
|
86
|
+
|
|
87
|
+
| File | Contents |
|
|
88
|
+
|------|----------|
|
|
89
|
+
| `<name>_automated_digitised_trace_by_column.csv` | One row per pixel column per trace (below). |
|
|
90
|
+
| `<name>_automated_panel_metadata.csv` | Where each plot area is, its axis calibrations and how they were obtained. |
|
|
91
|
+
| `<name>_automated_digitised_summary_by_label.csv` | Means per labelled interval, when the x axis carries interval names (`1N`, `1D`, …). |
|
|
92
|
+
| `<name>_automated_provenance.json` | Everything needed to report and repeat the run. |
|
|
93
|
+
| `<name>_automated_overlay.png` | With `--overlay`: the result drawn over the figure. |
|
|
94
|
+
| `<name>_automated_straightened.png` | Only if the figure was tilted: the image turned upright, which is what pixel positions in the other files refer to. |
|
|
95
|
+
|
|
96
|
+
Columns of the trace file:
|
|
97
|
+
|
|
98
|
+
| Column | Meaning |
|
|
99
|
+
|--------|---------|
|
|
100
|
+
| `panel_id`, `trace_id` | Panels are numbered row by row from the top left; traces within a panel from 1. |
|
|
101
|
+
| `x_pixel`, `x_norm_0_to_1` | The pixel column, and its position between the left (0) and right (1) axis lines. |
|
|
102
|
+
| `x_value` | The x value, if the x axis is calibrated. |
|
|
103
|
+
| `center_estimate` | Middle of the trace's ink in this column (median of its pixels). |
|
|
104
|
+
| `mean_estimate` | Mean of the trace's pixels in this column. |
|
|
105
|
+
| `upper_envelope`, `lower_envelope` | Top and bottom of the trace's ink in this column. |
|
|
106
|
+
| `n_pixels`, `interpolated` | How many pixels the trace has in this column; `True` if it has none and the values were interpolated from its neighbours. |
|
|
107
|
+
| `scale_type` | Where the y values come from: `user_supplied`, `ocr_value`, `normalised_0_to_1`, … |
|
|
108
|
+
| `trace_median_rgb`, `trace_pixel_count` | The trace's colour and size, for identifying it. |
|
|
109
|
+
|
|
110
|
+
`result.resample(...)` and `--sampling-interval` put the trace on an even grid in x.
|
|
111
|
+
|
|
112
|
+
## What the values are — and are not
|
|
113
|
+
|
|
114
|
+
A digitiser recovers what was **drawn**, not what was measured.
|
|
115
|
+
|
|
116
|
+
* For an ordinary curve — a thin line with at most a few samples per pixel column —
|
|
117
|
+
`center_estimate` is the plotted value, to within a fraction of a pixel.
|
|
118
|
+
* For a **dense record**, where many samples share each pixel column and the trace is drawn as a
|
|
119
|
+
solid band (an activity record of several days in a small figure, for instance), the drawing
|
|
120
|
+
shows only the *range* of the samples in each column. `upper_envelope` and `lower_envelope`
|
|
121
|
+
recover that range; `center_estimate` is its middle. **None of them is the mean of the samples**,
|
|
122
|
+
and for spiky data the middle of the range can be far from the mean. The timing of the rhythm
|
|
123
|
+
survives; the level does not.
|
|
124
|
+
|
|
125
|
+
How much this matters for a particular kind of data and size of figure can be measured rather than
|
|
126
|
+
guessed: see [Test it on data you know](#test-it-on-data-you-know).
|
|
127
|
+
|
|
128
|
+
Positions cannot be better than the image. In a plot area 350 pixels wide showing seven days, one
|
|
129
|
+
pixel is half an hour.
|
|
130
|
+
|
|
131
|
+
## When something is not found
|
|
132
|
+
|
|
133
|
+
Each automatic step can be replaced by telling the program the answer. Pixel positions count from
|
|
134
|
+
the top-left corner of the image, starting at 0; any image viewer shows them.
|
|
135
|
+
|
|
136
|
+
| Problem | Option | Python |
|
|
137
|
+
|---------|--------|--------|
|
|
138
|
+
| The figure is tilted and was not straightened, or was turned when it should not have been | `--straighten DEGREES` (its anticlockwise tilt), `--straighten never` | `straighten=1.3`, `straighten=False` |
|
|
139
|
+
| The plot area is wrong or not found | `--plot-area LEFT,TOP,RIGHT,BOTTOM` (repeat per panel) | `plot_area=(l, t, r, b)` |
|
|
140
|
+
| The wrong number of panels | `--layout`, `--rows`, `--cols`, `--panels` | `layout_mode`, `expected_rows`, … |
|
|
141
|
+
| The axis continues beyond its last tick, or has no line at the top or right | `--y-ref ROW:VALUE` twice or more (also `--x-ref`) | `y_ref=[(row, value), …]` |
|
|
142
|
+
| A logarithmic axis, with limits given | `--y-scale log` | `y_scale="log"` |
|
|
143
|
+
| A trace is missed, or two are merged | `--trace-colour RRGGBB` (repeat per trace) | `trace_colours=["#578dd2", …]` |
|
|
144
|
+
| A legend or annotation is taken for a trace | `--exclude LEFT,TOP,RIGHT,BOTTOM` | `exclude=(l, t, r, b)` |
|
|
145
|
+
| A black trace is invented or ignored | `--achromatic never` / `always` | `achromatic="never"` |
|
|
146
|
+
| No Tesseract, or it should not be used | `--no-ocr` | `ocr=False` |
|
|
147
|
+
|
|
148
|
+
## How accurate is it?
|
|
149
|
+
|
|
150
|
+
The package carries its own tests against known answers, and you can rerun them.
|
|
151
|
+
|
|
152
|
+
**Traces.** `trace-digitiser --validate results/` draws 44 figures (57 traces) from known data —
|
|
153
|
+
different axis styles, twelve colours including black and grey, 60 to 300 dpi, JPEG compression,
|
|
154
|
+
noise and blur, line widths, multi-panel layouts, several traces per panel, a logarithmic axis,
|
|
155
|
+
jagged and dense records, tilted scans — digitises them, and compares. Errors are in percent of the
|
|
156
|
+
height of the plot. A trace counts as *recovered* if its panel and curve were found with no
|
|
157
|
+
spurious extra trace, x positions are within 1 % and the RMS error in y is within 2 %.
|
|
158
|
+
|
|
159
|
+
| | found | recovered | median error | time per figure¹ |
|
|
160
|
+
|---|---|---|---|---|
|
|
161
|
+
| **2.0, nothing supplied** | 57 of 57 | 55 | 0.13 % | 0.5 s |
|
|
162
|
+
| **2.0, axis limits supplied** | 57 of 57 | 55 | 0.09 % | 0.3 s |
|
|
163
|
+
| 1.2.0, panel layout supplied | 48 of 57 | 20 | 0.49 % | 20 s |
|
|
164
|
+
| 1.2.0, panel layout and axis limits supplied | 48 of 57 | 13 | 1.39 % | 0.3 s |
|
|
165
|
+
|
|
166
|
+
¹ On a two-core machine.
|
|
167
|
+
|
|
168
|
+
The two traces that 2.0 does not recover are dense records in low-resolution figures (errors of
|
|
169
|
+
2.3 % and 4.3 %), for the reason given [above](#what-the-values-are--and-are-not). Of the traces
|
|
170
|
+
that 1.2.0 found but did not recover, most came with a spurious second trace.
|
|
171
|
+
|
|
172
|
+
**Axis scales.** Reading a scale from tick labels is the one step that depends on OCR, and OCR of
|
|
173
|
+
small print is unreliable, so it is tested separately:
|
|
174
|
+
`python -m trace_digitiser.validation results/ --axes` reads 750 axes (whole numbers, decimals,
|
|
175
|
+
negative values, thousands, clock and angle steps, logarithmic; print from 5 to 24 pixels high;
|
|
176
|
+
three typefaces; JPEG compression and blur).
|
|
177
|
+
|
|
178
|
+
| Height of the labels | axes | read correctly | declined | read wrongly, with a warning | read wrongly, no warning |
|
|
179
|
+
|---|---|---|---|---|---|
|
|
180
|
+
| 8 pixels or more | 477 | 464 | 13 | 0 | 0 |
|
|
181
|
+
| under 8 pixels | 273 | 257 (all with a warning to check them) | 15 | 1 | 0 |
|
|
182
|
+
|
|
183
|
+
*Declined* means that no scale was offered. All of those at 8 pixels or more are logarithmic axes
|
|
184
|
+
whose raised exponents are too small to read, though the rest of the label is not. The outcome for small
|
|
185
|
+
print depends on how the text was rasterised: with the test figures drawn by matplotlib 3.7, which
|
|
186
|
+
renders small print more softly than current versions, 236 of 292 axes under 8 pixels were read
|
|
187
|
+
correctly, 46 declined and 10 read wrongly with a warning — and still none without.
|
|
188
|
+
|
|
189
|
+
Labels under 8 pixels high are never given high confidence, however well they agree, because print
|
|
190
|
+
that small is misread in consistent ways — the decimal point dropped from every label, say. If
|
|
191
|
+
your figure has such labels, enlarging the image does not help; typing in two numbers does.
|
|
192
|
+
|
|
193
|
+
### Test it on data you know
|
|
194
|
+
|
|
195
|
+
The best test of a digitisation is one on data like yours. `round_trip` draws a known series as a
|
|
196
|
+
figure of whatever size and style you choose, digitises the drawing, and reports the errors:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from trace_digitiser.validation import round_trip
|
|
200
|
+
|
|
201
|
+
check = round_trip(t, y, "check/", xlim=(0, 7), ylim=(0, 300),
|
|
202
|
+
panel_px=(347, 137), dpi=72, linewidth=0.8, fmt="jpg")
|
|
203
|
+
check["metrics"] # errors of centre and envelopes, correlation with the mean per column, …
|
|
204
|
+
check["overlay"] # the picture to look at
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
[`examples/validate_with_known_data.py`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/validate_with_known_data.py)
|
|
208
|
+
does this for a week of minute-by-minute activity drawn at the size of a small journal figure.
|
|
209
|
+
|
|
210
|
+
## Limits
|
|
211
|
+
|
|
212
|
+
* The plot must have at least a left and a bottom axis line, drawn in black or grey. A figure
|
|
213
|
+
that is askew is straightened automatically if the tilt is under five degrees; beyond that,
|
|
214
|
+
or for a photograph taken at an angle (where the axes are no longer parallel), correct the
|
|
215
|
+
image first.
|
|
216
|
+
* One trace per colour per panel. Two traces of the same colour, dashed or dotted lines, and
|
|
217
|
+
traces that are only markers are not separated or joined up.
|
|
218
|
+
* Where traces cross or overlap, the one drawn underneath is hidden and its values there are
|
|
219
|
+
interpolated (`interpolated` is `True`).
|
|
220
|
+
* Axis scales are read only from numeric labels on linear or logarithmic axes; dates, broken axes
|
|
221
|
+
and a second y axis are not understood. Limits typed in are assumed to be at the axis lines
|
|
222
|
+
unless `y_ref` / `x_ref` say otherwise.
|
|
223
|
+
* Error bars, shaded bands, bars and scatter plots are not digitised.
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
git clone https://github.com/comparativechrono/trace-digitiser.git
|
|
229
|
+
cd trace-digitiser
|
|
230
|
+
pip install -e ".[dev]"
|
|
231
|
+
pytest # the tests that need Tesseract are skipped without it
|
|
232
|
+
ruff check src tests
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`src/trace_digitiser/` in the order the work is done:
|
|
236
|
+
|
|
237
|
+
| Module | Does |
|
|
238
|
+
|--------|------|
|
|
239
|
+
| `straighten.py` | Measures the tilt of a scan and turns it upright. |
|
|
240
|
+
| `frames.py`, `panel_detection.py` | Finds axis lines and assembles them into plot areas. |
|
|
241
|
+
| `axis_labels.py`, `calibration.py`, `x_calibration.py` | Locates and reads tick labels, fits the scale, rates its confidence; or takes the limits given. |
|
|
242
|
+
| `trace_detection.py` | Separates the traces from gridlines, axes and text. |
|
|
243
|
+
| `digitise.py` | Converts each trace to values, column by column. |
|
|
244
|
+
| `ocr.py`, `summarise.py` | Interval labels along the x axis, and means per interval. |
|
|
245
|
+
| `diagnostics.py`, `provenance.py`, `io.py` | Overlay, record of the run, files. |
|
|
246
|
+
| `validation.py`, `synthetic.py` | Figures drawn from known data, and the comparisons. |
|
|
247
|
+
| `cli.py` | The command line. |
|
|
248
|
+
|
|
249
|
+
Changes are listed in [CHANGELOG.md](https://github.com/comparativechrono/trace-digitiser/blob/main/CHANGELOG.md);
|
|
250
|
+
making a release is described in
|
|
251
|
+
[RELEASING.md](https://github.com/comparativechrono/trace-digitiser/blob/main/RELEASING.md).
|
|
252
|
+
|
|
253
|
+
## Licence
|
|
254
|
+
|
|
255
|
+
MIT; see `LICENSE`. The example figure has its own licence and attribution, given in
|
|
256
|
+
[`examples/README.md`](https://github.com/comparativechrono/trace-digitiser/blob/main/examples/README.md).
|