Skip to content

Instantly share code, notes, and snippets.

@pavel-kirienko
Last active March 6, 2025 12:22
Show Gist options
  • Save pavel-kirienko/0fcd509cd1d7c6dc2651981510badb99 to your computer and use it in GitHub Desktop.
Save pavel-kirienko/0fcd509cd1d7c6dc2651981510badb99 to your computer and use it in GitHub Desktop.
A simple script for extracting numerical data from plots: load an image and click on it to record point coordinates. The first click sets the origin, the second sets the unit scale, the subsequent clicks sample the data points.
#!/usr/bin/env python3
"""
A simple script for extracting numerical data from plots: load an image and click on it to record point coordinates.
Usage example:
./trace_image.py my_plot.jpg
Copyright (C) 2025 Pavel Kirienko <[email protected]>
"""
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
THRESHOLD = 10
origin = None # Will hold (x0, y0) in pixel coords
scale_ref = None # Will hold (dx_pixels, dy_pixels) to define 1 unit in each axis
points = [] # List of collected points in the new coordinate system
fig, ax = plt.subplots()
# Load and display the image
img = mpimg.imread(sys.argv[1])
ax.imshow(img)
ax.set_title("Click to set the origin")
def onclick(event):
"""Handle mouse click events."""
global origin, scale_ref, points
if event.xdata is None or event.ydata is None:
return
if origin is None:
origin = (event.xdata, event.ydata)
ax.plot(origin[0], origin[1], 'k+')
elif scale_ref is None or (scale_ref[0] is None or scale_ref[1] is None):
ax.plot(event.xdata, event.ydata, 'g+')
if scale_ref is None:
dx = event.xdata - origin[0]
dy = event.ydata - origin[1]
if abs(dx) < THRESHOLD and abs(dy) < THRESHOLD:
print("Scale reference click too close to the origin in both axes, try again.", file=sys.stderr)
elif abs(dx) < THRESHOLD and abs(dy) >= THRESHOLD:
scale_ref = (None, dy)
elif abs(dy) < THRESHOLD and abs(dx) >= THRESHOLD:
scale_ref = (dx, None)
else:
scale_ref = (dx, dy)
else:
if scale_ref[0] is None:
dx = event.xdata - origin[0]
if abs(dx) < THRESHOLD:
print("X scale reference too close to the origin, click again for X scale.", file=sys.stderr)
else:
scale_ref = (dx, scale_ref[1])
elif scale_ref[1] is None:
dy = event.ydata - origin[1]
if abs(dy) < THRESHOLD:
print("Y scale reference too close to the origin, click again for Y scale.", file=sys.stderr)
else:
scale_ref = (scale_ref[0], dy)
else:
pixel_x = event.xdata
pixel_y = event.ydata
ax.plot(pixel_x, pixel_y, 'rx')
dx_from_origin = pixel_x - origin[0]
dy_from_origin = pixel_y - origin[1]
new_x = dx_from_origin / scale_ref[0]
new_y = dy_from_origin / scale_ref[1]
points.append((new_x, new_y))
print(f"{new_x:+012.6f}\t{new_y:+012.6f}")
title = ""
if scale_ref is None:
title = "Click a unit distance away from the origin on one or both axes to set the scale"
elif scale_ref[0] is None or scale_ref[1] is None:
axis = "X" if scale_ref[0] is None else "Y"
title = f"Click a unit distance away from the origin along the {axis} axis to set the {axis} scale"
elif not points:
title = "Click to record data points"
else:
title = "Add more data points or close the window if done"
ax.set_title(title)
plt.draw()
fig.canvas.mpl_connect("button_press_event", onclick)
plt.tight_layout()
try:
plt.show()
except KeyboardInterrupt:
pass
if points:
out_file = "points.tab"
print("Saving the data into", out_file, file=sys.stderr)
Path(out_file).write_text("\n".join(f"{x:+012.6f}\t{y:+012.6f}" for x, y in points))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment