Skip to content

Commit 6f73ae3

Browse files
authored
Merge pull request #38 from DiamondLightSource/I04_1-156_messages
I04 1 156 messages
2 parents c89417a + 9961b11 commit 6f73ae3

26 files changed

+438
-175
lines changed
Lines changed: 17 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
11
from __future__ import division
22

33
import multiprocessing
4-
import time
54
import queue
65

7-
from dls_util.image import Image, Color
8-
from dls_util import Beeper
9-
from dls_barcode.scan import GeometryScanner, SlotScanner, OpenScanner
10-
from dls_barcode.datamatrix import DataMatrix
11-
from .overlay import PlateOverlay, TextOverlay
126
from .capture_worker import CaptureWorker
7+
from .scanner_worker import ScannerWorker
138
from .camera_position import CameraPosition
149
from .stream_action import StreamAction
1510
from .capture_command import CaptureCommand
1611

17-
Q_LIMIT = 1
18-
SCANNED_TAG = "Scan Complete"
19-
NO_PUCK_TIME = 2
20-
21-
EXIT_KEY = 'q'
22-
23-
# Maximum frame rate to sample at (rate will be further limited by speed at which frames can be processed)
24-
MAX_SAMPLE_RATE = 10.0
25-
INTERVAL = 1.0 / MAX_SAMPLE_RATE
26-
2712

2813
class CameraScanner:
2914
""" Manages the continuous scanning mode which takes a live feed from an attached camera and
@@ -33,7 +18,7 @@ class CameraScanner:
3318
Two separate processes are spawned, one to handle capturing and displaying images from the cameras,
3419
and the other to handle processing (scanning) of those images.
3520
"""
36-
def __init__(self, result_queue, view_queue, config):
21+
def __init__(self, result_queue, view_queue, message_queue, config):
3722
""" The task queue is used to store a queue of captured frames to be processed; the overlay
3823
queue stores Overlay objects which are drawn on to the image displayed to the user to highlight
3924
certain features; and the result queue is used to pass on the results of successful scans to
@@ -46,6 +31,7 @@ def __init__(self, result_queue, view_queue, config):
4631
self._scanner_kill_q = multiprocessing.Queue()
4732
self._result_q = result_queue
4833
self._view_q = view_queue
34+
self._message_q = message_queue
4935

5036
self._config = config
5137
self._camera_configs = {CameraPosition.SIDE: self._config.get_side_camera_config(),
@@ -55,7 +41,7 @@ def __init__(self, result_queue, view_queue, config):
5541
self._camera_configs)
5642

5743
# The capture process is always running: we initialise the cameras only once because it's time consuming
58-
self._capture_process = multiprocessing.Process(target=_capture_worker, args=capture_args)
44+
self._capture_process = multiprocessing.Process(target=CameraScanner._capture_worker, args=capture_args)
5945
self._capture_process.start()
6046

6147
self._scanner_process = None
@@ -64,8 +50,8 @@ def start_scan(self, cam_position):
6450
""" Spawn the processes that will continuously capture and process images from the camera.
6551
"""
6652
print("\nMAIN: start triggered")
67-
scanner_args = (self._task_q, self._overlay_q, self._result_q, self._scanner_kill_q, self._config, cam_position)
68-
self._scanner_process = multiprocessing.Process(target=_scanner_worker, args=scanner_args)
53+
scanner_args = (self._task_q, self._overlay_q, self._result_q, self._message_q, self._scanner_kill_q, self._config, cam_position)
54+
self._scanner_process = multiprocessing.Process(target=CameraScanner._scanner_worker, args=scanner_args)
6955

7056
self._capture_command_q.put(CaptureCommand(StreamAction.START, cam_position))
7157
self._scanner_process.start()
@@ -95,7 +81,7 @@ def _terminate_scanner_process(self):
9581
if self._scanner_process is not None:
9682
self._scanner_kill_q.put(None)
9783
print("MAIN: forcing scanner cleanup")
98-
self._process_cleanup(self._scanner_process, [self._result_q, self._overlay_q])
84+
self._process_cleanup(self._scanner_process, [self._result_q, self._overlay_q, self._message_q])
9985
self._scanner_process.join()
10086
self._flush_queue(self._scanner_kill_q)
10187
self._scanner_process = None
@@ -131,90 +117,14 @@ def _process_cleanup(self, process, queues):
131117

132118
print("MAIN: sub-process terminated!")
133119

120+
@staticmethod
121+
def _capture_worker(task_queue, view_queue, overlay_queue, command_queue, kill_queue, camera_configs):
122+
""" Function used as the main loop of a worker process.
123+
"""
124+
CaptureWorker(camera_configs).run(task_queue, view_queue, overlay_queue, command_queue, kill_queue)
134125

135-
def _capture_worker(task_queue, view_queue, overlay_queue, command_queue, kill_queue, camera_configs):
136-
""" Function used as the main loop of a worker process.
137-
"""
138-
worker = CaptureWorker(camera_configs)
139-
worker.run(task_queue, view_queue, overlay_queue, command_queue, kill_queue)
140-
141-
142-
def _scanner_worker(task_queue, overlay_queue, result_queue, kill_queue, config, cam_position):
143-
""" Function used as the main loop of a worker process. Scan images for barcodes,
144-
combining partial scans until a full puck is reached.
145-
146-
Keep the record of the last scan which was at least partially successful (aligned geometry
147-
and some barcodes scanned). For each new frame, we can attempt to merge the results with
148-
this previous plates so that we don't have to re-read any of the previously captured barcodes
149-
(because this is a relatively expensive operation).
150-
"""
151-
print("SCANNER start")
152-
last_plate_time = time.time()
153-
154-
SlotScanner.DEBUG = config.slot_images.value()
155-
SlotScanner.DEBUG_DIR = config.slot_image_directory.value()
156-
157-
if cam_position == CameraPosition.SIDE:
158-
plate_type = "None"
159-
barcode_sizes = DataMatrix.DEFAULT_SIDE_SIZES
160-
else:
161-
plate_type = config.plate_type.value()
162-
barcode_sizes = [config.top_barcode_size.value()]
163-
164-
if plate_type == "None":
165-
scanner = OpenScanner(barcode_sizes)
166-
else:
167-
scanner = GeometryScanner(plate_type, barcode_sizes)
168-
169-
display = True
170-
while kill_queue.empty():
171-
if display:
172-
print("--- scanner inside loop")
173-
display = False
174-
if task_queue.empty():
175-
continue
176-
177-
frame = task_queue.get(True)
178-
179-
# Make grayscale version of image
180-
image = Image(frame)
181-
gray_image = image.to_grayscale()
182-
183-
# If we have an existing partial plate, merge the new plate with it and only try to read the
184-
# barcodes which haven't already been read. This significantly increases efficiency because
185-
# barcode read is expensive.
186-
scan_result = scanner.scan_next_frame(gray_image)
187-
188-
if config.console_frame.value():
189-
scan_result.print_summary()
190-
191-
if scan_result.success():
192-
# Record the time so we can see how long its been since we last saw a plate
193-
last_plate_time = time.time()
194-
195-
plate = scan_result.plate()
196-
197-
if scan_result.already_scanned():
198-
overlay_queue.put(TextOverlay(SCANNED_TAG, Color.Green()))
199-
elif scan_result.any_valid_barcodes():
200-
overlay_queue.put(PlateOverlay(plate, config))
201-
_plate_beep(plate, config.scan_beep.value())
202-
203-
if scan_result.any_new_barcodes():
204-
result_queue.put((plate, image))
205-
else:
206-
time_since_plate = time.time() - last_plate_time
207-
if time_since_plate > NO_PUCK_TIME:
208-
overlay_queue.put(TextOverlay(scan_result.error(), Color.Red()))
209-
210-
print("SCANNER stop & kill")
211-
212-
213-
def _plate_beep(plate, do_beep):
214-
if not do_beep:
215-
return
216-
217-
empty_fraction = (plate.num_slots - plate.num_valid_barcodes()) / plate.num_slots
218-
frequency = int(10000 * empty_fraction + 37)
219-
duration = 200
220-
Beeper.beep(frequency, duration)
126+
@staticmethod
127+
def _scanner_worker(task_queue, overlay_queue, result_queue, message_queue, kill_queue, config, cam_position):
128+
""" Function used as the main loop of a worker process.
129+
"""
130+
ScannerWorker().run(task_queue, overlay_queue, result_queue, message_queue, kill_queue, config, cam_position)

dls_barcode/camera/capture_worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import time
22
import queue
33

4-
from .overlay import Overlay
4+
from dls_util.image import Overlay
55
from .stream_action import StreamAction
66
from dls_util.image import Image
77
from dls_util.cv import CameraStream
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from dls_util.image import Overlay, Image, Color
2+
3+
4+
class PlateOverlay(Overlay):
5+
""" Represents an overlay that can be drawn on top of an image. Used to draw the outline of a plate
6+
on the continuous scanner camera image to highlight to the user which barcodes on the plate have
7+
already been scanned.
8+
"""
9+
def __init__(self, plate, options, lifetime=2):
10+
Overlay.__init__(self, lifetime)
11+
12+
self._plate = plate
13+
self._options = options
14+
15+
def draw_on_image(self, img):
16+
""" Draw the plate highlight to the image.
17+
"""
18+
image = Image(img)
19+
20+
# If the overlay has not expired, draw on the plate highlight and/or the status message
21+
if not self.has_expired():
22+
self._plate.draw_plate(image, Color.Blue())
23+
self._plate.draw_pins(image, self._options)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import time
2+
3+
from dls_util.image import Image
4+
from dls_util.message import MessageType, Message
5+
from dls_util import Beeper
6+
from dls_barcode.scan import GeometryScanner, SlotScanner, OpenScanner
7+
from dls_barcode.datamatrix import DataMatrix
8+
from .camera_position import CameraPosition
9+
from .plate_overlay import PlateOverlay
10+
11+
NO_PUCK_TIME = 2
12+
13+
14+
class ScannerWorker:
15+
""" Scan images for barcodes, combining partial scans until a full puck is reached.
16+
Keep the record of the last scan which was at least partially successful (aligned geometry
17+
and some barcodes scanned). For each new frame, we can attempt to merge the results with
18+
this previous plates so that we don't have to re-read any of the previously captured barcodes
19+
(because this is a relatively expensive operation).
20+
"""
21+
def run(self, task_queue, overlay_queue, result_queue, message_queue, kill_queue, config, cam_position):
22+
print("SCANNER start")
23+
self._last_plate_time = time.time()
24+
25+
SlotScanner.DEBUG = config.slot_images.value()
26+
SlotScanner.DEBUG_DIR = config.slot_image_directory.value()
27+
28+
self._create_scanner(cam_position, config)
29+
30+
display = True
31+
while kill_queue.empty():
32+
if display:
33+
print("--- scanner inside loop")
34+
display = False
35+
36+
if task_queue.empty():
37+
continue
38+
39+
frame = task_queue.get(True)
40+
self._process_frame(frame, config, overlay_queue, result_queue, message_queue)
41+
42+
print("SCANNER stop & kill")
43+
44+
def _process_frame(self, frame, config, overlay_queue, result_queue, message_queue):
45+
image = Image(frame)
46+
gray_image = image.to_grayscale()
47+
48+
# If we have an existing partial plate, merge the new plate with it and only try to read the
49+
# barcodes which haven't already been read. This significantly increases efficiency because
50+
# barcode read is expensive.
51+
scan_result = self._scanner.scan_next_frame(gray_image)
52+
53+
if config.console_frame.value():
54+
scan_result.print_summary()
55+
56+
if scan_result.success():
57+
# Record the time so we can see how long its been since we last saw a plate
58+
self._last_plate_time = time.time()
59+
60+
plate = scan_result.plate()
61+
if scan_result.any_valid_barcodes():
62+
overlay_queue.put(PlateOverlay(plate, config))
63+
self._plate_beep(plate, config.scan_beep.value())
64+
65+
if scan_result.any_new_barcodes():
66+
result_queue.put((plate, image))
67+
elif scan_result.error() is not None and (time.time() - self._last_plate_time > NO_PUCK_TIME):
68+
message_queue.put(Message(MessageType.WARNING, scan_result.error(), lifetime=1))
69+
70+
def _create_scanner(self, cam_position, config):
71+
if cam_position == CameraPosition.SIDE:
72+
plate_type = "None"
73+
barcode_sizes = DataMatrix.DEFAULT_SIDE_SIZES
74+
else:
75+
plate_type = config.plate_type.value()
76+
barcode_sizes = [config.top_barcode_size.value()]
77+
78+
if plate_type == "None":
79+
self._scanner = OpenScanner(barcode_sizes)
80+
else:
81+
self._scanner = GeometryScanner(plate_type, barcode_sizes)
82+
83+
def _plate_beep(self, plate, do_beep):
84+
if not do_beep:
85+
return
86+
87+
empty_fraction = (plate.num_slots - plate.num_valid_barcodes()) / plate.num_slots
88+
frequency = int(10000 * empty_fraction + 37)
89+
duration = 200
90+
Beeper.beep(frequency, duration)

dls_barcode/gui/image_frame.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def _init_ui(self):
2828

2929
self.setLayout(vbox)
3030

31-
def clear_frame(self):
31+
def clear_frame(self, message):
3232
self._frame.clear()
33-
self._frame.setText("No Scan Selected")
33+
self._frame.setText(message)
3434

3535
def display_puck_image(self, image):
3636
""" Called when a new row is selected on the record table. Displays the specified

0 commit comments

Comments
 (0)