class ImCanvas(HBox, HasTraits): image_path = Unicode() _image_scale = Float() def __init__(self, width=150, height=150): self._canvas = Canvas(width=width, height=height) super().__init__([self._canvas]) @observe('image_path') def _draw_image(self, change): self._image_scale = draw_img(self._canvas, self.image_path, clear=True) # Add value as a read-only property @property def image_scale(self): return self._image_scale def _clear_image(self): self._canvas.clear() # needed to support voila # https://ipycanvas.readthedocs.io/en/latest/advanced.html#ipycanvas-in-voila def observe_client_ready(self, cb=None): self._canvas.on_client_ready(cb)
def draw_stimulus(include_source, n_background_dots, n_source_dots, source_sd, source_border, width, height): canvas = Canvas(width=425, height=425, layout=widgets.Layout(justify_self='center', grid_area='canvas')) border = 4 stroke = 2 dot_radius = 2 with hold_canvas(canvas): # draw box canvas.clear() canvas.stroke_style = 'black' canvas.stroke_rect(stroke, stroke, width, height) if include_source: bgdots = n_background_dots sourcedots = n_source_dots else: bgdots = n_background_dots + n_source_dots sourcedots = 0 for _ in range(bgdots): canvas.fill_style = npr.choice( sns.color_palette("Paired").as_hex()) x = npr.uniform(border, width - border) y = npr.uniform(border, height - border) canvas.fill_arc(x, y, dot_radius, 0, 2 * math.pi) if sourcedots > 0: x_center = npr.uniform(source_border, width - source_border) y_center = npr.uniform(source_border, width - source_border) x = np.clip(npr.normal(x_center, source_sd, sourcedots), source_border, width - source_border) y = np.clip(npr.normal(y_center, source_sd, sourcedots), source_border, width - source_border) for i in range(n_source_dots): canvas.fill_style = npr.choice( sns.color_palette("Paired").as_hex()) canvas.fill_arc(x[i], y[i], dot_radius, 0, 2 * math.pi) return canvas
class Paper(): """ Base paper class. """ def __init__(self, img): f = open('app/config.json') cfg = json.load(f) self.img = img self.width = 1080 self.height = 770 self.canvas = Canvas( width=self.width, height=self.height, ) self.output = Output( layout=Layout( #border='1px solid cyan', width='1170px', height='770px', min_height='90vh', overflow='hidden hidden'), ) with self.output: display(self.canvas) def put_image(): resized = cv2.resize(img, (self.width, self.height), interpolation=cv2.INTER_AREA) self.canvas.put_image_data(resized, 0, 0) self.canvas.on_client_ready(put_image) def update(self, img, out): resized = cv2.resize(img, (self.width, self.height), interpolation=cv2.INTER_AREA) with hold_canvas(self.canvas): self.canvas.clear() self.canvas.put_image_data(resized, 0, 0)
class ImCanvas(HBox): def __init__(self, width=150, height=150, has_border=False): self.has_border = has_border self._canvas = Canvas(width=width, height=height) super().__init__([self._canvas]) def _draw_image(self, image_path: str): self._image_scale = draw_img( self._canvas, image_path, clear=True, has_border=self.has_border ) def _clear_image(self): self._canvas.clear() # needed to support voila # https://ipycanvas.readthedocs.io/en/latest/advanced.html#ipycanvas-in-voila def observe_client_ready(self, cb=None): self._canvas.on_client_ready(cb)
class Core: # All constants that will be injected into global scope in the user"s cell global_constants = { "pi": pi } # All methods/fields from this class that will be exposed as global in user"s scope global_fields = { "canvas", "size", "width", "height", "mouse_x", "mouse_y", "mouse_is_pressed", "fill_style", "stroke_style", "clear", "background", "rect", "square", "fill_rect", "stroke_rect", "clear_rect", "fill_text", "stroke_text", "text_align", "draw_line", "circle", "fill_circle", "stroke_circle", "fill_arc", "stroke_arc", "print" } # All methods that user will be able to define and override global_methods = { "draw", "setup", "mouse_down", "mouse_up", "mouse_moved" } def __init__(self, globals_dict): self.status_text = display(Code(""), display_id=True) self._globals_dict = globals_dict self._methods = {} self.stop_button = Button(description="Stop") self.stop_button.on_click(self.on_stop_button_clicked) self.canvas = Canvas() self.output_text = "" self.width, self.height = DEFAULT_CANVAS_SIZE self.mouse_x = 0 self.mouse_y = 0 self.mouse_is_pressed = False ### Properties ### @property def mouse_x(self): return self._globals_dict["mouse_x"] @mouse_x.setter def mouse_x(self, val): self._globals_dict["mouse_x"] = val @property def mouse_y(self): return self._globals_dict["mouse_y"] @mouse_y.setter def mouse_y(self, val): self._globals_dict["mouse_y"] = val @property def mouse_is_pressed(self): return self._globals_dict["mouse_is_pressed"] @mouse_is_pressed.setter def mouse_is_pressed(self, val): self._globals_dict["mouse_is_pressed"] = val @property def width(self): return self._globals_dict["width"] @width.setter def width(self, val): self._globals_dict["width"] = val self.canvas.width = val @property def height(self): return self._globals_dict["height"] @height.setter def height(self, val): self._globals_dict["height"] = val self.canvas.height = val ### Library init ### # Updates last activity time @staticmethod def refresh_last_activity(): global _sparkplug_last_activity _sparkplug_last_activity = time.time() # Creates canvas and starts thread def start(self, methods): self._methods = methods draw = self._methods.get("draw", None) if draw: self.print_status("Running...") display(self.stop_button) else: self.print_status("Done drawing") display(self.canvas) self.output_text_code = display(Code(self.output_text), display_id=True) self.canvas.on_mouse_down(self.on_mouse_down) self.canvas.on_mouse_up(self.on_mouse_up) self.canvas.on_mouse_move(self.on_mouse_move) thread = threading.Thread(target=self.loop) thread.start() def stop(self, message="Stopped"): global _sparkplug_running if not _sparkplug_running: return _sparkplug_running = False self.print_status(message) # Assuming we're using IPython to draw the canvas through the display() function. # Commenting this out for now, it throws exception since it does not derive BaseException # raise IpyExit # Loop method that handles drawing and setup def loop(self): global _sparkplug_active_thread_id, _sparkplug_running # Set active thread to this thread. This will stop any other active thread. current_thread_id = threading.current_thread().native_id _sparkplug_active_thread_id = current_thread_id _sparkplug_running = True self.refresh_last_activity() draw = self._methods.get("draw", None) setup = self._methods.get("setup", None) if setup: try: setup() except Exception as e: self.print_status("Error in setup() function: " + str(e)) return while _sparkplug_running: if _sparkplug_active_thread_id != current_thread_id or time.time() - _sparkplug_last_activity > NO_ACTIVITY_THRESHOLD: self.stop("Stopped due to inactivity") return if not draw: return with hold_canvas(self.canvas): try: draw() except Exception as e: self.print_status("Error in draw() function: " + str(e)) return time.sleep(1 / FRAME_RATE) # Prints status to embedded error box def print_status(self, msg): self.status_text.update(Code(msg)) # Prints output to embedded output box def print(self, msg): global _sparkplug_running self.output_text += str(msg) + "\n" if _sparkplug_running: self.output_text_code.update(Code(self.output_text)) # Update mouse_x, mouse_y, and call mouse_down handler def on_mouse_down(self, x, y): self.refresh_last_activity() self.mouse_x, self.mouse_y = int(x), int(y) self.mouse_is_pressed = True mouse_down = self._methods.get("mouse_down", None) if mouse_down: mouse_down() # Update mouse_x, mouse_y, and call mouse_up handler def on_mouse_up(self, x, y): self.refresh_last_activity() self.mouse_x, self.mouse_y = int(x), int(y) self.mouse_is_pressed = False mouse_up = self._methods.get("mouse_up", None) if mouse_up: mouse_up() # Update mouse_x, mouse_y, and call mouse_moved handler def on_mouse_move(self, x, y): self.refresh_last_activity() self.mouse_x, self.mouse_y = int(x), int(y) mouse_moved = self._methods.get("mouse_moved", None) if mouse_moved: mouse_moved() def on_stop_button_clicked(self, button): self.stop() ### Global functions ### # Sets canvas size def size(self, *args): if len(args) == 2: self.width = args[0] self.height = args[1] # Sets fill style # 1 arg: HTML string value # 3 args: r, g, b are int between 0 and 255 # 4 args: r, g, b, a, where r, g, b are ints between 0 and 255, and a (alpha) is a float between 0 and 1.0 def fill_style(self, *args): self.canvas.fill_style = self.parse_color("fill_style", *args) def stroke_style(self, *args): self.canvas.stroke_style = self.parse_color("stroke_style", *args) # Combines fill_rect and stroke_rect into one wrapper function def rect(self, *args): self.check_coords("rect", *args) self.canvas.fill_rect(*args) self.canvas.stroke_rect(*args) # Similar to self.rect wrapper, except only accepts x, y and size def square(self, *args): self.check_coords("square", *args, width_only=True) rect_args = (*args, args[2]) # Copy the width arg into the height self.rect(*rect_args) # Draws filled rect def fill_rect(self, *args): self.check_coords("fill_rect", *args) self.canvas.fill_rect(*args) # Strokes a rect def stroke_rect(self, *args): self.check_coords("stroke_rect", *args) self.canvas.stroke_rect(*args) #Clears a rect def clear_rect(self, *args): self.check_coords('clear_rect', *args) self.canvas.clear_rect(*args) # Draws circle at given coordinates def circle(self, *args): self.check_coords("circle", *args, width_only=True) arc_args = self.arc_args(*args) self.canvas.fill_arc(*arc_args) self.canvas.stroke_arc(*arc_args) # Draws filled circle def fill_circle(self, *args): self.check_coords("fill_circle", *args, width_only=True) arc_args = self.arc_args(*args) self.canvas.fill_arc(*arc_args) # Draws circle stroke def stroke_circle(self, *args): self.check_coords("stroke_circle", *args, width_only=True) arc_args = self.arc_args(*args) self.canvas.stroke_arc(*arc_args) def fill_arc(self, *args): self.canvas.fill_arc(*args) def stroke_arc(self, *args): self.canvas.stroke_arc(*args) def fill_text(self, *args): self.canvas.font = "{px}px sans-serif".format(px = args[4]) self.canvas.fill_text(args[0:3]) self.canvas.font = "12px sans-serif" def stroke_text(self, *args): self.canvas.font = "{px}px sans-serif".format(px = args[4]) self.canvas.stroke_text(args[0:3]) self.canvas.font = "12px sans-serif" def text_align(self, *args): self.canvas.text_align(*args) def draw_line(self, *args): if len(args) == 4: self.canvas.line_width = args[4] else: self.canvas.line_width = 1 self.canvas.begin_path() self.canvas.move_to(args[0],args[1]) self.canvas.line_to(args[2],args[4]) self.canvas.close_path() # Clears canvas def clear(self, *args): self.canvas.clear() # Draws background on canvas def background(self, *args): old_fill = self.canvas.fill_style argc = len(args) if argc == 3: if ((not type(args[0]) is int) or (not type(args[1]) is int) or (not type(args[2]) is int)): raise TypeError("Enter Values between 0 and 255(integers only) for all 3 values") elif (not (args[0] >= 0 and args[0] <= 255) or not (args[1] >= 0 and args[1] <= 255) or not ( args[2] >= 0 and args[2] <= 255)): raise TypeError("Enter Values between 0 and 255(integers only) for all 3 values") self.clear() self.fill_style(args[0], args[1], args[2]) self.fill_rect(0, 0, self.width, self.height) elif argc == 1: if (not type(args[0]) is str): raise TypeError("Enter colour value in Hex i.e #000000 for black and so on") self.clear() self.fill_style(args[0]) self.fill_rect(0, 0, self.width, self.height) elif argc == 4: if ((not type(args[0]) is int) or (not type(args[1]) is int) or (not type(args[2]) is int) or ( not type(args[3]) is float)): raise TypeError("Enter Values between 0 and 255(integers only) for all 3 values") elif (not (args[0] >= 0 and args[0] <= 255) or not (args[1] >= 0 and args[1] <= 255) or not ( args[2] >= 0 and args[2] <= 255) or not (args[3] >= 0.0 and args[3] <= 1.0)): raise TypeError( "Enter Values between 0 and 255(integers only) for all 3 values and a value between 0.0 and 1.0 for opacity(last argument") self.clear() self.fill_style(args[0], args[1], args[2], args[3]) self.fill_rect(0, 0, self.width, self.height) self.canvas.fill_style = old_fill ### Helper Functions ### # Tests if input is numeric # Note: No support for complex numbers def check_type_is_num(self, n, func_name=None): if not isinstance(n, (int, float)): msg = "Expected {} to be a number".format(n) if func_name: msg = "{} expected {} to be a number".format(func_name, self.quote_if_string(n)) raise TypeError(msg) # Tests if input is an int def check_type_is_int(self, n, func_name=None): if type(n) is not int: msg = "Expected {} to be an int".format(n) if func_name: msg = "{} expected {} to be an int".format(func_name, self.quote_if_string(n)) raise TypeError(msg) # Tests if input is a float # allow_int: Set to True to allow ints as a float. Defaults to True. def check_type_is_float(self, n, func_name=None, allow_int=True): if type(n) is not float: if not allow_int or type(n) is not int: msg = "Expected {} to be a float".format(n) if func_name: msg = "{} expected {} to be a float".format(func_name, self.quote_if_string(n)) raise TypeError(msg) @staticmethod def quote_if_string(val): if type(val) is str: return "\"{}\"".format(val) else: return val # Parse a string, rgb or rgba input into an HTML color string def parse_color(self, func_name, *args): argc = len(args) if argc == 1: return args[0] elif argc == 3 or argc == 4: color_args = args[:3] for col in color_args: self.check_type_is_int(col, func_name) color_args = np.clip(color_args, 0, 255) if argc == 3: return "rgb({}, {}, {})".format(*color_args) else: # Clip alpha between 0 and 1 alpha_arg = args[3] self.check_type_is_float(alpha_arg, func_name) alpha_arg = np.clip(alpha_arg, 0, 1.0) return "rgba({}, {}, {}, {})".format(*color_args, alpha_arg) else: raise TypeError("{} expected {}, {} or {} arguments, got {}".format(func_name, 1, 3, 4, argc)) # Check a set of 4 args are valid coordinates # x, y, w, h def check_coords(self, func_name, *args, width_only=False): argc = len(args) if argc != 4 and not width_only: raise TypeError("{} expected {} arguments for x, y, w, h, got {} arguments".format(func_name, 4, argc)) elif argc != 3 and width_only: raise TypeError("{} expected {} arguments for x, y, size, got {} arguments".format(func_name, 3, argc)) for arg in args: self.check_type_is_float(arg, func_name) # Convert a tuple of circle args into arc args def arc_args(self, *args): return (args[0], args[1], args[2] / 2, 0, 2 * pi)
class Turtle: direction = 0 color = "black" drawing = True def __init__(self, x_max=400, y_max=400): self.canvas = Canvas(width=x_max, height=y_max) self.x_max = x_max self.x_pos = x_max / 2 self.y_max = y_max self.y_pos = y_max / 2 def forward(self, steps=50): x_end, y_end = self.calculate_endpoint(steps) if self.is_pen_down(): self.canvas.stroke_line(self.x_pos, self.y_pos, x_end, y_end) self.x_pos = x_end self.y_pos = y_end def calculate_endpoint(self, steps): new_x = cos(radians(self.direction)) * steps + self.x_pos new_y = sin(radians(self.direction)) * steps + self.y_pos return (new_x, new_y) def turn(self, degree): self.direction = (self.direction + degree) % 360 def reset(self): self.x_pos = 200 self.y_pos = 200 self.direction = 0 self.set_color("black") def clear(self): self.canvas.clear() def show(self): self.canvas.fill_style = "green" self.canvas.fill_rect(self.x_pos - 2, self.y_pos - 2, 4, 4) self.canvas.fill_style = self.color self.canvas.stroke_style = self.color return self.canvas def set_color(self, color): self.color = color self.canvas.fill_style = self.color self.canvas.stroke_style = self.color def is_pen_down(self): return self.drawing def pen_up(self): self.drawing = False def pen_down(self): self.drawing = True def is_x_inside_boundries(self): return self.x_pos >= 0 and self.x_pos < self.x_max def is_y_inside_boundries(self): return self.y_pos >= 0 and self.y_pos < self.y_max def is_inside_boundries(self): return self.is_x_inside_boundries() and self.is_y_inside_boundries() def get_x_pos(self): return self.x_pos def get_y_pos(self): return self.y_pos def set_canvas_size(self, width, height): self.canvas = Canvas()
class Detection_Experiment(): def __init__(self, subj_num): self.subject_num = subj_num self.output_file = 'sdt-' + str(subj_num) + '.csv' # create two buttons with these names, randomize the position if npr.random() < 0.5: self.labels = [[ 'Present', widgets.ButtonStyle(button_color='darkseagreen') ], ['Absent', widgets.ButtonStyle(button_color='salmon')]] self.position = 'left' else: self.labels = [[ 'Absent', widgets.ButtonStyle(button_color='salmon') ], ['Present', widgets.ButtonStyle(button_color='darkseagreen')]] self.position = 'right' self.buttons = [ widgets.Button(description=desc[0], layout=widgets.Layout(width='auto', grid_area=f'button{idx}'), value=idx, style=desc[1]) # create button for idx, desc in enumerate(self.labels) ] # puts buttons into a list self.canvas = Canvas(width=425, height=425, layout=widgets.Layout(justify_self='center', grid_area='canvas')) # create output widget for displaying feedback/reward self.out = widgets.Output(layout=widgets.Layout( width='auto', object_position='center', grid_area='output')) # output widgets wrapped in VBoxes np.random.seed(24) self.create_trials(25, [10, 15, 25, 60]) self.done = False def create_trials(self, ntrials, vals): signal_present = np.array([0] * (ntrials * len(vals)) + [1] * (ntrials * len(vals))) l = [[val] * ntrials for val in vals] l = [item for sublist in l for item in sublist] signal_type = np.array([0] * (ntrials * len(vals)) + l) self.trials = pd.DataFrame({ 'signal_present': signal_present, 'signal_type': signal_type }) self.trials = self.trials.sample(frac=1).reset_index( drop=True) # shuffle self.trials['trial_num'] = np.arange(self.trials.shape[0]) self.trials['button_position'] = self.position self.trials['subject_number'] = self.subject_num self.trial_iterator = self.trials.iterrows() self.responses = [] self.rt = [] self.correct_resp = [] # create function called when button clicked. # the argument to this fuction will be the clicked button # widget instance def on_button_clicked(self, button): # "linking function with output' if not self.done: with self.out: # what happens when we press the button choice = button.description if choice == 'Present': choice_code = 1 elif choice == 'Absent': choice_code = 0 # reminds us what we clicked self.responses.append(choice_code) # record reaction time here? if choice_code == self.current_trial['signal_present']: correct = 1 else: correct = 0 self.correct_resp.append(correct) self.rt.append( (datetime.now() - self.dt).total_seconds() * 1000) self.next_trial() else: clear_output() self.save_data(self.output_file) print("The experiment is finished!") print("Data saved to .csv") print("Also available as exp.trials") print("-------------") print("Thanks so much for your time!") def save_data(self, fn): # create dataframe self.trials['responses'] = self.responses self.trials['rt'] = self.rt self.trials['correct_resp'] = self.correct_resp self.trials.to_csv(self.output_file) def draw_stimulus(self, include_source, n_background_dots, n_source_dots, source_sd, source_border, width, height): border = 4 stroke = 2 dot_radius = 2 with hold_canvas(self.canvas): # draw box self.canvas.clear() self.canvas.stroke_style = 'black' self.canvas.stroke_rect(stroke, stroke, width, height) if include_source: bgdots = n_background_dots sourcedots = n_source_dots else: bgdots = n_background_dots + n_source_dots sourcedots = 0 for _ in range(bgdots): self.canvas.fill_style = npr.choice( sns.color_palette("Paired").as_hex()) x = npr.uniform(border, width - border) y = npr.uniform(border, height - border) self.canvas.fill_arc(x, y, dot_radius, 0, 2 * math.pi) if sourcedots > 0: x_center = npr.uniform(source_border, width - source_border) y_center = npr.uniform(source_border, width - source_border) x = np.clip(npr.normal(x_center, source_sd, sourcedots), source_border, width - source_border) y = np.clip(npr.normal(y_center, source_sd, sourcedots), source_border, width - source_border) for i in range(n_source_dots): self.canvas.fill_style = npr.choice( sns.color_palette("Paired").as_hex()) self.canvas.fill_arc(x[i], y[i], dot_radius, 0, 2 * math.pi) def next_trial(self): try: self.current_trial = next(self.trial_iterator)[1] self.draw_stimulus( include_source=self.current_trial['signal_present'], n_background_dots=30 * 30, n_source_dots=self.current_trial['signal_type'], source_sd=10, source_border=20, width=400, height=400) self.dt = datetime.now() # reset clock except StopIteration: self.done = True def start_experiment(self): # linking button and function together using a button's method [button.on_click(self.on_button_clicked) for button in self.buttons] self.next_trial() return widgets.GridBox(children=self.buttons + [self.canvas], layout=widgets.Layout( width='50%', justify_items='center', grid_template_rows='auto auto', grid_template_columns='10% 40% 40% 10%', grid_template_areas=''' ". canvas canvas ." ". button0 button1 ." "output output output output" '''))