Last active
March 6, 2025 12:22
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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