def server(registration, board, flip): """Runs camera server process used to provide gateway to captured images and fiducial locations. Serves HTTP API on port 5000, which can be used by `pdserver`. """ from purpledrop.pdcam.server import create_app board_name = board if board_name is not None: board = load_board(board_name) if board is None: raise ValueError(f"No board found with name {board_name}") if registration is not None: registration = load_registration(registration) elif board is not None: registration = board.registration if board is None: layout = None else: layout = board.layout app = create_app(registration, layout, flip) app.run(host="0.0.0.0")
def test_load_misl_v5(): """Test loading `board/misl_v5.json` definition """ board = electrode_board.load_board('misl_v5') assert isinstance(board, electrode_board.Board) assert isinstance(board.layout, electrode_board.Layout) assert board.registration is None
def test_load_misl_v6(): """Test loading `board/misl_v6.json` definition """ board = electrode_board.load_board('misl_v6') assert isinstance(board, electrode_board.Board) assert isinstance(board.layout, electrode_board.Layout) assert len(board.registration.fiducials) == 2 assert len(board.registration.control_points) == 6
def test_pin_polygon(): board = electrode_board.load_board('misl_v6') # Test getting a grid electrode assert np.array_equal( board.layout.pin_polygon(15), [[18.75, 8.25], [18.75, 12.0], [22.5, 12.0], [22.5, 8.25]]) # Test getting a peripheral electrode np.testing.assert_array_equal( board.layout.pin_polygon(76), np.array([[1.25, 0.0], [-1.25, 0.0], [-1.25, 2.5], [0.0, 4.0], [1.25, 2.5], [1.25, 0.0]]) + [60.625, 38.25])
def test_find_grid_transform_v4_1_sample1(): """Load a sample image and try to find the registration """ img = load_image('electrode_board_v4.1_sample1.jpg') board = load_board('misl_v4.1') reference = board.registration transform, fiducials = find_grid_transform(img, reference) print(f"tranform: {transform}") print(f"fiducials: {fiducials}") # There should be 3 fiducials, and they should be labeled with integers # 4, 5, 6 in no particular order assert len(fiducials) == 3 assert sorted([f.label for f in fiducials]) == [4, 5, 6] assert np.allclose(TRANSFORM_V4_1_SAMPLE1, transform)
def main(verbose, board_file, replay_file): """Runs hardware gateway Will auto-connect to any detected purpledrop USB devices, and provides HTTP interfaces for control. Optionally, supports replay mode in which a recorded event stream is played back """ if verbose == 0: console_log_level = logging.WARNING elif verbose == 1: print("Setting stdout logging to INFO") console_log_level = logging.INFO else: print("Setting stdout logging to DEBUG") console_log_level = logging.DEBUG logging.basicConfig( format="%(asctime)s.%(msecs)03d %(levelname)s (%(name)s): %(message)s", datefmt="%H:%M:%S", level=console_log_level) board = load_board(board_file) if board is None: print(f"Could not load board definition for {board_file}") sys.exit(1) video_client = None if replay_file is not None: print(f"Computing seek index for {replay_file}...") index, end_time = index_log(replay_file) start_time = index[0].timestamp print(f"Done. Loaded {end_time - start_time} seconds of data.") print("Launching replay server...") pd_control = PlaybackPurpleDrop(replay_file, index, board) else: pd_dev = PersistentPurpleDropDevice() pd_control = PurpleDropController(pd_dev, board) print("Launching HW server...") # TODO: make video host configurable video_host = "localhost:5000" video_client = VideoClientProtobuf(video_host) server.run_server(pd_control, video_client)
def find_reference_from_fiducials(search_labels: Tuple[int]) -> Optional[Registration]: """Look for a board definition that matches the fiducial tags found A matching board definition must include all of the fiducial labels, and it must be unique. It's possible for multiple boards to match, in which case no reference is returned. Each of the provided fiducials must have a unique label, or no board can be matched. This function is memoized to prevent searching boards on repeated image. This means that the process must be restarted if the board definition files are changed for the new data to take effect. Args: search_labels: Tuple of integer labels for the fiducials to lookup Returns: A Registration object if one is found, or None otherwise """ board_names = list_boards() matched_boards = [] for l in search_labels: if search_labels.count(l) != 1: logger.warn(f"Cannot lookup reference board because of repeated tag in {search_labels}") return None for name in board_names: board = load_board(name) if board is not None and board.registration is not None: board_labels = [f.label for f in board.registration.fiducials] if all([l in board_labels for l in search_labels]): matched_boards.append(board) if len(matched_boards) == 1: return matched_boards[0].registration if len(matched_boards) > 1: logger.warn(f"Found multiple boards matching fiducials {search_labels}") else: logger.warn(f"Found no matching board for fiducials {search_labels}") return None
def test_find_grid_transform_v4_1_sample2(): """Load a sample image and try to find the registration This sample is the same as sample1, but one of the fiducials is covered. We should still be able to find a transform with just two fiducials detected. """ img = load_image('electrode_board_v4.1_sample2.jpg') board = load_board('misl_v4.1') reference = board.registration transform, fiducials = find_grid_transform(img, reference) print(f"tranform: {transform}") print(f"fiducials: {fiducials}") # Only 2 fiducials are visible, 5 and 6, and are returned in no particular # order assert len(fiducials) == 2 assert sorted([f.label for f in fiducials]) == [5, 6] # Allow some tolerance in transform; using a different set of fiducials will # result in a similar but different result assert transform is not None assert_allclose(TRANSFORM_V4_1_SAMPLE1, transform, rtol=0.1, atol=0.6)
def measure(imagefile, outfile, v4, v4_1, v5, layout, points, verbose): """Launch UI to make calibration measurements from an image of an electrode board. """ # TODO: Instead of hardcoding the layouts, load the electrode layout from # board definition files (e.g. with --board option), and auto compute a # good set of control electrodes img = cv2.cvtColor(cv2.imread(imagefile), cv2.COLOR_BGR2RGB) def read_input_tuple(x): try: return tuple(int(n) for n in x.split(',')) except ValueError as ex: raise ValueError(f"Failed parsing point {x}: {ex}") fiducials = find_fiducials(img) for f in fiducials: print(f) mark_fiducial(img, f.corners) electrode_layout = ELECTRODE_LAYOUT_v3 control_electrodes = CONTROL_ELECTRODES_v3 pitch = 1.0 grid_origin = (0.0, 0.0) if layout: if points is None or len(points) < 4: raise ValueError("If providing a custom layout, you must provide at least 4 calibration points with --point") board = load_board(layout) electrode_layout = board.layout.grids[0]['pins'] control_electrodes = [read_input_tuple(p) for p in points] pitch = board.layout.grids[0]['pitch'] origin = board.layout.grids[0]['origin'] if v4: electrode_layout = ELECTRODE_LAYOUT_v4 control_electrodes = CONTROL_ELECTRODES_v4 elif v4_1: electrode_layout = ELECTRODE_LAYOUT_v4_1 control_electrodes = CONTROL_ELECTRODES_v4_1 elif v5: electrode_layout = ELECTRODE_LAYOUT_v5 control_electrodes = CONTROL_ELECTRODES_v5 if verbose: print(f"Using control points: {control_electrodes}") print(f"Using layout: {electrode_layout}") alignment_electrodes = control_electrodes fig = plt.figure() gs = fig.add_gridspec(4, 4) ax1 = fig.add_subplot(gs[:, :-1]) plt.imshow(img) ax1.set_title('Click the top-left corner of the indicated electrode') ax2 = fig.add_subplot(gs[:1, -1]) plot_template(ax2, electrode_layout, [alignment_electrodes[0]]) ax2.invert_yaxis() alignment_points = [] def onclick(event): ix, iy = event.xdata, event.ydata alignment_points.append((ix, iy)) if len(alignment_points) == len(alignment_electrodes): fig.canvas.mpl_disconnect(cid) plt.close(1) else: ax2.clear() plot_template(ax2, electrode_layout, [alignment_electrodes[len(alignment_points)]]) ax2.invert_yaxis() ax1.plot(alignment_points[-1][0], alignment_points[-1][1], 'ro') fig.canvas.draw() cid = fig.canvas.mpl_connect('button_press_event', onclick) fig.tight_layout() plt.ion() plt.show(block=True) if len(alignment_points) != len(alignment_electrodes): print("Figure closed without collecting enough control points. No file will be saved") return for(n, p) in zip(alignment_electrodes, alignment_points): print("%s: (%d, %d)\n" % (n, p[0], p[1])) def map_fiducial(f): def to_tuple(point): return (point[0], point[1]) return [to_tuple(p) for p in f.corners] data = { 'fiducials': [f.to_dict() for f in fiducials], 'control_points': [ {"grid": (n[0]*pitch + origin[0], n[1]*pitch + origin[1]), "image": p} for n,p in zip(alignment_electrodes, alignment_points) ] } print("Storing reference data to %s" % outfile) with open(outfile, 'w') as f: f.write(json.dumps(data))