-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmacroscope.py
More file actions
296 lines (259 loc) · 10.6 KB
/
Copy pathmacroscope.py
File metadata and controls
296 lines (259 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import time
import picamera
import numpy as np
import os
import copy
from pynput import mouse, keyboard
from threading import Timer
import traceback
import sys, select
import subprocess
def create_circular_mask(h, w, center=None, radius=None):
if center is None: # use the middle of the image
center = (int(w/2), int(h/2))
if radius is None: # use the smallest distance between the center and image walls
radius = min(center[0], center[1], w-center[0], h-center[1])
Y, X = np.ogrid[:h, :w]
dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2)
mask = dist_from_center <= radius
return mask
def timeout_input(prompt, timeout=3, default=""):
print(prompt, end=': ', flush=True)
inputs, outputs, errors = select.select([sys.stdin], [], [], timeout)
print()
return (0, sys.stdin.readline().strip()) if inputs else (-1, default)
class Macroscope:
def __init__(self, recording_path, recording_session, cursor_size=12, min_roi_size=100, recording_duration=600):
# Constants
self.recording_path = recording_path
self.recording_session = recording_session
self.cursor_size = cursor_size # in pixels
self.min_roi_size = min_roi_size # in pixels
self.recording_duration = recording_duration # in seconds
# Variables
self.preview = False
self.recording = False
self.recording_number = 0
self.image_number = 0
self.roi = np.zeros(4)
self.roi_changing = False
self.mouse_pos = (0, 0)
self.update = False
self.exp = 0
# Pynput mouse listeners
def on_mouse_move(self, x, y):
if not self.preview:
return
if x < 0: x = 0
if x >= self.resolution[0]: x = self.resolution[0] - 1
if y < 0: y = 0
if y >= self.resolution[1]: y = self.resolution[1] - 1
self.mouse_pos = (x, y)
self.update = True
if self.roi_changing:
roi = self.roi
roi[2] = x - roi[0]
roi[3] = y - roi[1]
x_y = roi[2]*self.resolution[1]/self.resolution[0]
y_x = roi[3]*self.resolution[0]/self.resolution[1]
if roi[2] > y_x:
roi[2] = y_x
elif roi[2] < -y_x:
roi[2] = -y_x
elif roi[3] > x_y:
roi[3] = x_y
else:
roi[3] = -x_y
def on_mouse_click(self, x, y, button, pressed):
if not self.preview:
return
if self.record_mask[y,x] and pressed:
self.toggle_recording()
elif self.record_mask[y,x]:
pass
elif pressed:
self.roi[0] = x
self.roi[1] = y
self.roi[2] = 0
self.roi[3] = 0
self.roi_changing = True
else:
self.roi_changing = False
self.change_roi()
self.roi = np.zeros(4)
def on_mouse_scroll(self, x, y, dx, dy):
if not self.preview:
return
if dy > 0 and self.camera.exposure_compensation < 25:
self.camera.exposure_compensation += 1
elif dy < 0 and self.camera.exposure_compensation > -25:
self.camera.exposure_compensation -= 1
def on_keypress(self, key):
if key == keyboard.Key.esc:
return False
elif hasattr(key, 'char') and key.char == "p": # preview
if self.preview:
self.camera.stop_preview()
self.camera.remove_overlay(self.overlay)
self.preview = False
else:
a = np.zeros((self.resolution[1], self.resolution[0], 4), dtype=np.uint8)
self.overlay = self.camera.add_overlay(a.tobytes(), layer=3)
self.camera.start_preview()
self.preview = True
self.update = True
elif key == keyboard.Key.space: # recording
self.toggle_recording()
elif key == keyboard.Key.enter:
self.take_still()
elif hasattr(key, 'char') and key.char == "r": # rotate
self.camera.rotation = self.camera.rotation + 90
elif hasattr(key, 'char') and key.char == "f": # flip
self.camera.hflip = not self.camera.hflip
elif key == keyboard.Key.up and self.preview and \
self.camera.exposure_compensation < 25:
self.camera.exposure_compensation += 1
elif key == keyboard.Key.down and self.preview and \
self.camera.exposure_compensation > -25:
self.camera.exposure_compensation -= 1
def draw_overlay(self):
a = np.zeros((self.resolution[1], self.resolution[0], 4), dtype=np.uint8)
# Recording
a[self.record_mask,0] = 0xff
if self.recording:
a[self.record_mask,3] = 0xff
else:
a[self.record_mask,3] = 0x40
# ROI
if self.roi_changing:
roi = copy.deepcopy(self.roi)
if roi[2] < 0:
roi[0] = roi[0] + roi[2]
roi[2] = -roi[2]
if roi[3] < 0:
roi[1] = roi[1] + roi[3]
roi[3] = -roi[3]
a[int(roi[1]):int(roi[1]+roi[3]), int(roi[0]):int(roi[0]+roi[2]), :] = 0xff
a[int(roi[1]):int(roi[1]+roi[3]), int(roi[0]):int(roi[0]+roi[2]), 3] = 0x10
# Mouse
x, y = self.mouse_pos
a[y, int(x-self.cursor_size/2):int(x+self.cursor_size/2), :] = 0xff
a[int(y-self.cursor_size/2):int(y+self.cursor_size/2), x, :] = 0xff
self.overlay.update(a.tobytes())
# ROI update
def change_roi(self):
roi = self.roi
if roi[2] < 0:
roi[0] = roi[0] + roi[2]
roi[2] = -roi[2]
if roi[3] < 0:
roi[1] = roi[1] + roi[3]
roi[3] = -roi[3]
if roi[2] < self.min_roi_size or roi[3] < self.min_roi_size:
self.camera.zoom = (0.0, 0.0, 1.0, 1.0)
roi = [0, 0, self.resolution[0], self.resolution[1]]
else:
self.camera.zoom = (roi[0]/self.resolution[0], roi[1]/self.resolution[1],
roi[2]/self.resolution[0], roi[3]/self.resolution[1])
def toggle_recording(self):
recording = not self.recording
if recording:
while os.path.exists(self.get_recording_filename()):
self.recording_number += 1
self.camera.start_recording(self.get_recording_filename(), splitter_port=2)
print('Started recording %s ' % self.get_recording_filename())
self.recording_start_time = time.time()
else:
self.camera.stop_recording(splitter_port=2)
print('Stopped recording.')
self.recording = recording
self.update = True
def take_still(self):
filename = os.path.join(self.recording_path, "%s-still%03d.jpg" % (self.recording_session, self.image_number))
while os.path.exists(filename):
self.image_number += 1
filename = os.path.join(self.recording_path, "%s-still%03d.jpg" % (self.recording_session, self.image_number))
self.camera.capture(filename)
print("Captured image %s" % filename)
# Main loop
def run(self, stream=False):
# Init camera
camera = picamera.PiCamera()
camera.framerate = 30
self.camera = camera
self.resolution = camera.resolution
self.record_mask = create_circular_mask(self.resolution[1], self.resolution[0], (25, 25), 11)
# Listen to mouse and keyboard events
l_mouse = mouse.Listener(
on_move=lambda x,y: self.on_mouse_move(x,y),
on_click=lambda x,y,button,pressed: self.on_mouse_click(x,y,button,pressed),
on_scroll=lambda x,y,dx,dy: self.on_mouse_scroll(x,y,dx,dy))
l_mouse.start()
l_keyboard = keyboard.Listener(on_press=lambda key: self.on_keypress(key))
l_keyboard.start()
# Start streaming or start the preview
if stream:
vlc = subprocess.Popen([
'cvlc', 'stream:///dev/stdin',
#'--sout', '#rtp{sdp=rtsp://:8555/',
':demux=h264',
], stdin=subprocess.PIPE)
camera.start_recording(vlc.stdin, format='h264', quality=20, resize=(640, 480))
else:
self.on_keypress(keyboard.KeyCode.from_char('p'))
# Loop until escape key is pressed
while l_keyboard.running:
if self.update:
self.draw_overlay()
self.update = False
time.sleep(1/60) # try not to update the overlays super fast
# Split the recording up if necessary
if self.recording and time.time() - self.recording_start_time > self.recording_duration:
self.recording_number += 1
self.camera.split_recording(self.get_recording_filename(), splitter_port=2)
self.recording_start_time = time.time()
print('... %s ' % self.get_recording_filename())
# Cleanup
if self.preview:
self.camera.remove_overlay(self.overlay)
camera.stop_preview()
if self.recording:
camera.stop_recording(splitter_port=2)
l_mouse.stop()
if stream:
camera.stop_recording()
vlc.kill()
camera.close()
print("Bye!")
time.sleep(1)
# Path helper
def get_recording_filename(self):
return os.path.join(self.recording_path, "%s%03d.h264" % (self.recording_session,self.recording_number))
if __name__ == "__main__":
recording_dir = '/home/pi/Videos'
filename = 'macroscope'
# Ask for filename
prompt = 'Session name? (default is "%s") ' % filename
ans, value = timeout_input(prompt, timeout=30, default=filename)
if ans == -1 or value == "":
print('Using default...')
else:
filename = value
print('--------------------------------------------------')
print('Press "P" to start/stop preview')
print('Press Space to start/stop recording')
print('Press Enter to take a still image')
print('Press "R" to rotate camera')
print('Use scroll wheel to adjust brightness')
print('Click and drag to set ROI. Click anywhere to reset')
print('Press "Esc" to exit')
print('--------------------------------------------------')
print('')
time.sleep(1)
try:
scope = Macroscope(recording_dir, filename)
scope.run()
except Exception as e:
print(e)
traceback.print_tb(e.__traceback__)
timeout_input("Press any key to exit", timeout=60)