Digitize scientific plots with Python
Frequently, scientific data are published as plots correlating one quantity against another. However, the original raw data are often not available. This is even more common for plots appearing in old books or printed papers. Instead of using a ruler to extract the data with limited accuracy, I will show you how you can use Python to digitize the curves in the plot and extract the data as arrays. You can find the complete code of the curve digitizer tool in the curvedigitizer repository on GitHub.
Let’s assume that you have a plot available as an image in .jpg or .png format. First, we will read and display this image with Python. To do this, we will use Tkinter and Matplotlib.
from tkinter import Tk, filedialog
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
# open the dialog box
# first hide the root window
root = Tk()
root.withdraw()
# open the dialog
filein = filedialog.askopenfilename(
title="Select image to digitize",
filetypes=(
("JPEG files", "*.jpg *.jpeg"),
("PNG files", "*.png"),
("All files", "*.*"))
)
# read and show the image
img = mpimg.imread(filein)
_, ax = plt.subplots()
ax.imshow(img)
ax.axis('off') # clear x-axis and y-axis
Here, I preferred to select the image with a dialog box instead of typing the complete path of the image file.
The loaded image has a “coordinate system” whose origin is the upper left corner as illustrated in Fig. 1. The position of every pixel in the image is given in terms of number of pixel columns to the right x number of pixel rows downwards. Of course, this system is different from the coordinate system of the plot in the image. We must correlate these two systems in order to be able to digitize the plot. To do this, we define two reference lengths in the and directions. From these two lengths, we will calculate a scale factor in each direction that expresses how many units correspond to one pixel in that direction. The units may be anything depending on your chart (e.g. meters, millimeters, etc). The scale factors are calculated from the expressions:
where and are the reference lengths in plot units, is the pixel column and the pixel row of the start and end points of each reference length. The order of the clicks matters. Pixel rows count downwards, while the axis of a plot usually points upwards. Therefore, for the reference length in the direction, click the lower point first and then the upper point, and enter a positive length: comes out negative and increases upwards. If you click in the opposite order, the digitized curve will be upside down. In the direction, click the left point first and then the right point.
In the above scaling method, rotation or skewness of the chart due to bad scanning are not taken into account. These would require extra corrections/manipulations. You should try to get an image where the two axes of the plot are as parallel as possible to its edges.
Since we will calculate the scale factor twice, once for the horizontal and once for the vertical direction, we can write one Python function that we will call two times. The desired direction ( or ) will be given as input to the function. The starting and ending pixels of the reference length will be captured by clicking on the image with the mouse. Depending on the requested direction of the reference length, we will use either the horizontal or the vertical coordinates of the captured pixels. The complete function is the following:
from tkinter import messagebox, simpledialog
def get_reference_length(index):
'''
Get the reference length in the requested direction
USAGE: factor = get_reference_length(index)
index = 0 for x-direction or 1 for y-direction
'''
# define a 'direction' string
direction = 'x' if index == 0 else 'y'
order = 'left point, then the right' if index == 0 else 'lower point, then the upper'
# get the reference length
reply = False
while not reply:
messagebox.showinfo("Select reference length",
f"Use the mouse to select the reference length in {direction} direction. "
f"Click the {order} point of the reference length."
)
coord = plt.ginput(
2,
timeout=0,
show_clicks=True
) # capture only two points
deltaref = coord[1][index] - coord[0][index]
if deltaref == 0:
messagebox.showerror("Error", "The two points must be different.")
continue
# ask for a valid length
valid_length = False
while not valid_length:
reflength = simpledialog.askfloat("Enter reference length",
f"Enter the reference length in {direction} direction")
if isinstance(reflength, float):
valid_length = True
else:
messagebox.showerror("Error", "Please provide a valid length.")
# calculate scale factor
factor = reflength / deltaref
reply = messagebox.askyesno("Length confirmation",
f"You selected {abs(deltaref):4.0f} pixels in {direction} direction "
f"corresponding to {reflength:4.4f} units. Is this correct?"
)
return factor
When using the code, you should aim for a large reference length in both directions. This will reduce the error when we convert the pixel coordinates of the digitized curves into the real coordinates of the plot.
Now that we have a method to calculate the scale factors, we can use the mouse to digitize the curve. The coordinates of the digitized points are measured from the first point you click, not from the axes of the plot. Therefore, the first point should be one whose plot coordinates you know, for example the origin in Fig. 1. The plot coordinates of the points are then given by:
where , are the pixel coordinates (column and row) of point , and , those of the first point. Left click selects a point, right click removes the last one, and a middle click or the Enter key finishes (most laptops have no middle mouse button). At the end of the procedure you will get an -by- matrix (NumPy array) containing the coordinates. The first column is the coordinate and the second is the coordinate. The Python code is the following:
import numpy as np
# get reference length in x direction
xfactor = get_reference_length(0)
# get the reference length in y direction
yfactor = get_reference_length(1)
# plot coordinates of the first point (e.g. the origin)
x0, y0 = 0.0, 0.0
messagebox.showinfo("Digitize curve",
"Please digitize the curve. The first point is the origin. "
"Left click: select point; Right click: undo; "
"Middle click or Enter: finish"
)
# get the curve points
x = plt.ginput(
-1,
timeout=0,
show_clicks=True
)
x = np.asarray(x)
ax.plot(x[:, 0], x[:, 1], 'g', linewidth=1.5)
plt.draw()
# convert the curve points from pixels to coordinates
x[:, 0] = x0 + (x[:, 0] - x[0, 0]) * xfactor
x[:, 1] = y0 + (x[:, 1] - x[0, 1]) * yfactor
# save the curve in a text file
np.savetxt('curve.txt', x, header='x y')
Subsequently, you can process the digitized curve as you like. If your image contains more curves that you wish to digitize, you may repeat the above procedure without redefining the reference lengths.
Logarithmic axes
Many scientific plots have one or both axes in logarithmic scale. Eqs. (1) and (2) still apply, but to the logarithms of the values. Measure the reference length in decades: for example, a reference length from to is . Enter (or ) as the logarithm of the value at the first point, and convert the result back with (or ), e.g. x[:, 0] = 10**x[:, 0].
Accuracy tips
- Use the image with the highest resolution available.
- Zoom in with the Matplotlib toolbar, then turn the zoom tool off again before you click the points.
- Place more points where the curve bends sharply and fewer where it is nearly straight, as in Fig. 1.
You can find the complete curve digitizer Python code in the curvedigitizer repository on GitHub. The program supports the digitization of multiple curves in a plot as well as the storage of the data into text files.
Tags: programmingpython