Skip to content

Commit 1f399ed

Browse files
committed
Rework to use the new screenshot/video generation script
1 parent 6794a5d commit 1f399ed

File tree

5 files changed

+294
-147
lines changed

5 files changed

+294
-147
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Autogenerate screenshots and videos from pre-recorded napari interactions
2+
3+
This folder contains scripts designed to automate the generation of screenshots
4+
and videos from pre-recorded interactions with napari. It uses `pyautogui` and
5+
`pynput` to record the screen and mouse interactions, exports the results to a
6+
json file, and then generates screenshots and videos based on this data.
7+
8+
## Usage
9+
10+
To use these scripts, follow these steps:
11+
1. **Install Dependencies**: Ensure you have the required Python packages installed. You can do this by running:
12+
```bash
13+
pip install pyautogui pynput
14+
```
15+
2. **Launch napari**: Start napari in a separate terminal or environment. Ensure it is running and ready to accept interactions.
16+
3. **Record Interactions**: Use the `record.py` script to record your interactions with napari. This will create a JSON file containing the recorded mouse and keyboard events.
17+
4. **Convert to Screenshots**: Use the `convert.py` script to convert the recorded interactions into screenshots. This will generate a series of PNG files in the `screenshots` directory.
18+
5. The conversion will be saved as `play.py`. Run python play.py to play back the actions
19+
20+
## Attribution
21+
22+
These scripts are inspired by the code from https://github.com/shedloadofcode/mouse-and-keyboard-recorder
23+
and its accompanying blog post at https://www.shedloadofcode.com/blog/record-mouse-and-keyboard-for-automation-scripts-with-python.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Converts the recording.json file to a Python script
3+
'play.py' to use with PyAutoGUI.
4+
5+
The 'play.py' script may require editing and adapting
6+
before use.
7+
8+
Always review 'play.py' before running with PyAutoGUI!
9+
"""
10+
11+
import json
12+
13+
key_mappings = {
14+
"cmd": "win",
15+
"alt_l": "alt",
16+
"alt_r": "alt",
17+
"ctrl_l": "ctrl",
18+
"ctrl_r": "ctrl",
19+
}
20+
21+
22+
def read_json_file():
23+
"""
24+
Takes the JSON output 'recording.json'
25+
26+
Excludes released and scrolling events to
27+
keep things simple.
28+
"""
29+
with open("recording.json") as f:
30+
recording = json.load(f)
31+
32+
def excluded_actions(object):
33+
return "released" not in object["action"] and "scroll" not in object["action"]
34+
35+
recording = list(filter(excluded_actions, recording))
36+
37+
return recording
38+
39+
40+
def convert_to_pyautogui_script(recording):
41+
"""
42+
Converts to a Python template script 'play.py' to
43+
use with PyAutoGUI.
44+
45+
Converts the:
46+
47+
- Mouse clicks
48+
- Keyboard input
49+
- Time between actions calculated
50+
"""
51+
if not recording:
52+
return
53+
54+
output = open("play.py", "w")
55+
output.write("import time\n")
56+
output.write("import pyautogui\n\n")
57+
58+
for i, step in enumerate(recording):
59+
print(step)
60+
61+
not_first_element = (i - 1) > 0
62+
if not_first_element:
63+
## compare time to previous time for the 'sleep' with a 10% buffer
64+
pause_in_seconds = (step["_time"] - recording[i - 1]["_time"]) * 1.1
65+
66+
output.write(f"time.sleep({pause_in_seconds})\n\n")
67+
else:
68+
output.write("time.sleep(1)\n\n")
69+
70+
if step["action"] == "pressed_key":
71+
key = (
72+
step["key"].replace("Key.", "")
73+
if "Key." in step["key"]
74+
else step["key"]
75+
)
76+
77+
if key in key_mappings.keys():
78+
key = key_mappings[key]
79+
80+
output.write(f"pyautogui.press('{key}')\n")
81+
82+
if step["action"] == "clicked":
83+
output.write(f"pyautogui.moveTo({step['x']}, {step['y']})\n")
84+
85+
if step["button"] == "Button.right":
86+
output.write("pyautogui.mouseDown(button='right')\n")
87+
else:
88+
output.write("pyautogui.mouseDown()\n")
89+
90+
if step["action"] == "unclicked":
91+
output.write(f"pyautogui.moveTo({step['x']}, {step['y']})\n")
92+
93+
if step["button"] == "Button.right":
94+
output.write("pyautogui.mouseUp(button='right')\n")
95+
else:
96+
output.write("pyautogui.mouseUp()\n")
97+
98+
print("Recording converted. Saved to 'play.py'")
99+
100+
101+
if __name__ == "__main__":
102+
recording = read_json_file()
103+
convert_to_pyautogui_script(recording)

docs/_scripts/generate_screenshots/coordinates.py

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""
2+
Opens a napari window and records mouse and keyboard interactions to a JSON file
3+
4+
To begin recording:
5+
- Run `python record.py`
6+
7+
To end recording:
8+
- Hold right click for 2 seconds then release to end the recording for mouse.
9+
- Press 'ESC' to end the recording for keyboard.
10+
- Both are needed to finish recording.
11+
"""
12+
13+
import time
14+
import json
15+
import napari
16+
import argparse
17+
from pynput import mouse, keyboard
18+
19+
20+
class InteractionRecorder:
21+
def __init__(self, output_file="recording.json"):
22+
self.recording = []
23+
self.output_file = output_file
24+
self.keyboard_listener = None
25+
self.mouse_listener = None
26+
self.viewer = None
27+
28+
def on_press(self, key):
29+
try:
30+
json_object = {"action": "pressed_key", "key": key.char, "_time": time.time()}
31+
except AttributeError:
32+
if key == keyboard.Key.esc:
33+
print("Keyboard recording ended.")
34+
self.stop_recording()
35+
return False
36+
37+
json_object = {"action": "pressed_key", "key": str(key), "_time": time.time()}
38+
39+
self.recording.append(json_object)
40+
41+
def on_release(self, key):
42+
try:
43+
json_object = {"action": "released_key", "key": key.char, "_time": time.time()}
44+
except AttributeError:
45+
json_object = {"action": "released_key", "key": str(key), "_time": time.time()}
46+
47+
self.recording.append(json_object)
48+
49+
def on_move(self, x, y):
50+
if len(self.recording) >= 1:
51+
if (
52+
self.recording[-1]["action"] == "pressed"
53+
and self.recording[-1]["button"] == "Button.left"
54+
) or (
55+
self.recording[-1]["action"] == "moved"
56+
and time.time() - self.recording[-1]["_time"] > 0.02
57+
):
58+
json_object = {"action": "moved", "x": x, "y": y, "_time": time.time()}
59+
self.recording.append(json_object)
60+
61+
def on_click(self, x, y, button, pressed):
62+
json_object = {
63+
"action": "clicked" if pressed else "unclicked",
64+
"button": str(button),
65+
"x": x,
66+
"y": y,
67+
"_time": time.time(),
68+
}
69+
70+
self.recording.append(json_object)
71+
72+
if len(self.recording) > 1:
73+
if (
74+
self.recording[-1]["action"] == "unclicked"
75+
and self.recording[-1]["button"] == "Button.right"
76+
and self.recording[-1]["_time"] - self.recording[-2]["_time"] > 2
77+
):
78+
self.save_recording()
79+
print("Mouse recording ended.")
80+
self.stop_recording()
81+
return False
82+
83+
def on_scroll(self, x, y, dx, dy):
84+
json_object = {
85+
"action": "scroll",
86+
"vertical_direction": int(dy),
87+
"horizontal_direction": int(dx),
88+
"x": x,
89+
"y": y,
90+
"_time": time.time(),
91+
}
92+
93+
self.recording.append(json_object)
94+
95+
def save_recording(self):
96+
"""Save the recorded interactions to a JSON file."""
97+
with open(self.output_file, "w") as f:
98+
json.dump(self.recording, f)
99+
print(f"Recording saved to {self.output_file}")
100+
101+
def start_recording(self):
102+
"""Creates a napari window and starts recording mouse and keyboard interactions."""
103+
napari.Viewer.close_all()
104+
# Open napari with standard window size (800x600)
105+
self.viewer = napari.Viewer()
106+
self.viewer.window._qt_window.setGeometry(0, 0, 800, 600)
107+
108+
# Reset recording for this session
109+
self.recording = []
110+
111+
print("Press 'ESC' to end the keyboard recording")
112+
print("Hold right click for 2 seconds then release to end the mouse recording")
113+
114+
self.keyboard_listener = keyboard.Listener(
115+
on_press=self.on_press,
116+
on_release=self.on_release
117+
)
118+
self.mouse_listener = mouse.Listener(
119+
on_click=self.on_click,
120+
on_scroll=self.on_scroll,
121+
on_move=self.on_move
122+
)
123+
124+
self.keyboard_listener.start()
125+
self.mouse_listener.start()
126+
127+
# Show the napari window
128+
napari.run()
129+
130+
# Wait for listeners to finish
131+
self.keyboard_listener.join()
132+
self.mouse_listener.join()
133+
134+
def stop_recording(self):
135+
"""Stop the recording and save the data."""
136+
if self.keyboard_listener:
137+
self.keyboard_listener.stop()
138+
if self.mouse_listener:
139+
self.mouse_listener.stop()
140+
self.save_recording()
141+
142+
if self.viewer:
143+
self.viewer.close()
144+
145+
146+
def start_recording(output: str = "recording.json"):
147+
"""Creates a napari window and starts recording mouse and keyboard interactions.
148+
149+
Args:
150+
output (str): The name of the output JSON file to save the recording.
151+
"""
152+
recorder = InteractionRecorder(output)
153+
recorder.start_recording()
154+
155+
156+
if __name__ == "__main__":
157+
parser = argparse.ArgumentParser(
158+
description="Record mouse and keyboard interactions in a napari window."
159+
)
160+
parser.add_argument(
161+
"output",
162+
type=str,
163+
nargs='?',
164+
default="recording.json",
165+
help="Output JSON file name"
166+
)
167+
args = parser.parse_args()
168+
start_recording(args.output)

0 commit comments

Comments
 (0)