def UpdateMagnification(self): # Total magnification mag = self._mpp_screen / self._microscope_view.mpp.value label = u"Mag: × %s" % units.readable_str( units.round_significant(mag, 3)) # Gather all different image mpp values mpps = set() for im, stream in self._microscope_view.stream_tree.getImages(): try: if hasattr(stream, 'mpp'): # im is a tuple of tuple of tiles mpps.add(stream.mpp.min) else: md = im.metadata mpps.add(md[model.MD_PIXEL_SIZE][0]) except KeyError: pass # If there's only one mpp value (i.e. there's only one image, or they # all have the same mpp value), indicate the digital zoom. if len(mpps) == 1: mpp_im = mpps.pop() # mag_im = self._mpp_screen / mpp_im # as if 1 im.px == 1 sc.px mag_dig = mpp_im / self._microscope_view.mpp.value label += u" (Digital: × %s)" % units.readable_str( units.round_significant(mag_dig, 2)) if self.bottom_legend: self.bottom_legend.set_mag_label(label)
def acquire(comp_name, dataflow_names, filename): """ Acquire an image from one (or more) dataflow comp_name (string): name of the detector to find dataflow_names (list of string): name of each dataflow to access filename (unicode): name of the output file (format depends on the extension) """ component = get_detector(comp_name) # check the dataflow exists dataflows = [] for df_name in dataflow_names: try: df = getattr(component, df_name) except AttributeError: raise ValueError("Failed to find data-flow '%s' on component %s" % (df_name, comp_name)) if not isinstance(df, model.DataFlowBase): raise ValueError("%s.%s is not a data-flow" % (comp_name, df_name)) dataflows.append(df) images = [] for df in dataflows: try: # Note: currently, get() uses Pyro, which is not as memory efficient # as .subscribe(), which uses ZMQ. So would need to use # _get_big_image() if very large image is requested. image = df.get() except Exception as exc: raise IOError("Failed to acquire image from component %s: %s" % (comp_name, exc)) logging.info("Acquired an image of dimension %r.", image.shape) images.append(image) try: if model.MD_PIXEL_SIZE in image.metadata: pxs = image.metadata[model.MD_PIXEL_SIZE] dim = (image.shape[0] * pxs[0], image.shape[1] * pxs[1]) logging.info("Physical dimension of image is %s.", units.readable_str(dim, unit="m", sig=3)) else: logging.warning("Physical dimension of image is unknown.") if model.MD_SENSOR_PIXEL_SIZE in image.metadata: spxs = image.metadata[model.MD_SENSOR_PIXEL_SIZE] dim_sens = (image.shape[0] * spxs[0], image.shape[1] * spxs[1]) logging.info("Physical dimension of sensor is %s.", units.readable_str(dim_sens, unit="m", sig=3)) except Exception as exc: logging.exception("Failed to read image information.") exporter = dataio.find_fittest_converter(filename) try: exporter.export(filename, images) except IOError as exc: raise IOError(u"Failed to save to '%s': %s" % (filename, exc))
def draw(self, ctx): # TODO: Both focuses at the same time, or 'snap' to horizontal/vertical on first motion? ctx.set_line_width(10) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(1.0, 1.0, 1.0, 0.8) x, y = self.cnvs.ClientSize # Horizontal if self.shifts[0] is not None: y -= self.margin + (self.line_width // 2) middle = x / 2 # don't display extremely small values, which are due to accumulation # of floating point error shiftm = self.shifts[0] if abs(shiftm) < 1e-12: shiftm = 0 shift = shiftm * self.ppm[0] end_x = middle + (middle * (shift / (x / 2))) end_x = min(max(self.margin, end_x), x - self.margin) ctx.move_to(middle, y) ctx.line_to(end_x, y) ctx.stroke() lbl = "focus %s" % units.readable_str(shiftm, "m", 2) self.focus_label.text = lbl self.focus_label.pos = (end_x, y - 15) self._write_label(ctx, self.focus_label) # Vertical if self.shifts[1] is not None: x -= self.margin + (self.line_width // 2) middle = y / 2 # don't display extremely small values, which are due to accumulation # of floating point error shiftm = self.shifts[1] if abs(shiftm) < 1e-12: shiftm = 0 shift = shiftm * self.ppm[1] end_y = middle - (middle * (shift / (y / 2))) end_y = min(max(self.margin, end_y), y - self.margin) ctx.move_to(x, middle) ctx.line_to(x, end_y) ctx.stroke() lbl = "focus %s" % units.readable_str(shiftm, "m", 2) self.focus_label.text = lbl self.focus_label.pos = (x - 15, end_y) self._write_label(ctx, self.focus_label)
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model self._acq_future = None # To avoid holding the ref in memory self._faf_connector = None try: trans_val, cor_md = future.result() opt_md, sem_md = cor_md # Save the optical correction metadata straight into the CCD main_data.ccd.updateMetadata(opt_md) # The SEM correction metadata goes to the ebeam main_data.ebeam.updateMetadata(sem_md) except CancelledError: self._tab_panel.lbl_fine_align.Label = "Cancelled" except Exception: self._tab_panel.lbl_fine_align.Label = "Failed" else: self._tab_panel.lbl_fine_align.Label = "Successful" self._main_frame.menu_item_reset_finealign.Enable(True) # Temporary info until the GUI can actually rotate the images rot = math.degrees(opt_md.get(model.MD_ROTATION_COR, 0)) shear = sem_md.get(model.MD_SHEAR_COR, 0) scaling_xy = sem_md.get(model.MD_PIXEL_SIZE_COR, (1, 1)) # the worse is the rotation, the longer it's displayed timeout = max(2, min(abs(rot), 10)) popup.show_message( self._tab_panel, u"Rotation applied: %s\nShear applied: %s\nX/Y Scaling applied: %s" % (units.readable_str(rot, unit="°", sig=3), units.readable_str(shear, sig=3), units.readable_str(scaling_xy, sig=3)), timeout=timeout ) logging.warning("Fine alignment computed rotation needed of %f°, " "shear needed of %s, and X/Y scaling needed of %s.", rot, shear, scaling_xy) # As the CCD image might have different pixel size, force to fit self._tab_panel.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._tab_panel.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._tab_panel.btn_fine_align.Label = self._fa_btn_label self._resume() self._tab_panel.lbl_fine_align.Show() self._tab_panel.gauge_fine_align.Hide() self._sizer.Layout()
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model self._acq_future = None # To avoid holding the ref in memory self._faf_connector = None try: trans_val, cor_md = future.result() opt_md, sem_md = cor_md # Save the optical correction metadata straight into the CCD main_data.ccd.updateMetadata(opt_md) # The SEM correction metadata goes to the ebeam main_data.ebeam.updateMetadata(sem_md) except CancelledError: self._tab_panel.lbl_fine_align.Label = "Cancelled" except Exception: self._tab_panel.lbl_fine_align.Label = "Failed" else: self._tab_panel.lbl_fine_align.Label = "Successful" self._main_frame.menu_item_reset_finealign.Enable(True) # Temporary info until the GUI can actually rotate the images rot = math.degrees(opt_md.get(model.MD_ROTATION_COR, 0)) shear = sem_md.get(model.MD_SHEAR_COR, 0) scaling_xy = sem_md.get(model.MD_PIXEL_SIZE_COR, (1, 1)) # the worse is the rotation, the longer it's displayed timeout = max(2, min(abs(rot), 10)) popup.show_message( self._tab_panel, u"Rotation applied: %s\nShear applied: %s\nX/Y Scaling applied: %s" % (units.readable_str( rot, unit="°", sig=3), units.readable_str( shear, sig=3), units.readable_str(scaling_xy, sig=3)), timeout=timeout) logging.warning( "Fine alignment computed rotation needed of %f°, " "shear needed of %s, and X/Y scaling needed of %s.", rot, shear, scaling_xy) # As the CCD image might have different pixel size, force to fit self._tab_panel.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._tab_panel.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._tab_panel.btn_fine_align.Label = self._fa_btn_label self._resume() self._tab_panel.lbl_fine_align.Show() self._tab_panel.gauge_fine_align.Hide() self._sizer.Layout()
def test_readable_str(self): # (input) (expected output) values = [ ((1.0, None), "1"), ((1, None), "1"), ((-1.234, "m"), "-1.234 m"), ((-1234, "g"), "-1.234 kg"), ((160000, None), "160000"), ((16, None), "16"), ((160001, None, 3), "160000"), ((16000000.0, None), "16000000"), ((16.3000000001, "px", 3), "16.3 px"), ((numpy.float64(16.3), "px", 3), "16.3 px"), ((-1600, "N"), "-1.6 kN"), ((-1601, "N", 3), "-1.6 kN"), # sig=3 ((0.0001236, None), "0.0001236"), ((0.0012, "V"), "1.2 mV"), ((999.999, "V", 3), "1 kV"), ((999.5, "V"), "999.5 V"), ((999.5, "V", 3), "1 kV"), ((999.4, "V", 3), "999 V"), ((200e-6, "m"), u"200 µm"), ((0.0, "m"), "0 m"), (([1500, 1200, 150], None), "1500 x 1200 x 150"), (([0.0001236, 0.00014], "m"), u"123.6 x 140 µm"), (([0.0001236, 12.0], "m"), "0.0001236 x 12 m"), (([1200, 1000], "px"), "1200 x 1000 px"), # special non-prefix unit (([-float("inf"), float("NaN")], "m"), u"-∞ x unknown m"), ] for (i, eo) in values: o = units.readable_str(*i) self.assertEqual(o, eo, u"%s is '%s' while expected '%s'" % (i, o, eo))
def test_readable_str(self): # (input) (expected output) values = [((1.0, None), "1"), ((1, None), "1"), ((-1.234, "m"), "-1.234 m"), ((-1234, "g"), "-1.234 kg"), ((160000, None), "160000"), ((16L, None), "16"), ((160001, None, 3), "160000"), ((16000000.0, None), "16000000"), ((-1600, "N"), "-1.6 kN"), ((-1601, "N", 3), "-1.6 kN"), # sig=3 ((0.0001236, None), "0.0001236"), ((0.0012, "V"), "1.2 mV"), ((200e-6, "m"), u"200 µm"), ((0.0, "m"), "0 m"), (([1500, 1200, 150], None), "1500 x 1200 x 150"), (([0.0001236, 0.00014], "m"), u"123.6 x 140 µm"), (([0.0001236, 12.0], "m"), "0.0001236 x 12 m"), (([1200, 1000], "px"), "1200 x 1000 px"), # special non-prefix unit (([-float("inf"), float("NaN")], "m"), u"-∞ x unknown m"), ] for (i, eo) in values: o = units.readable_str(*i) self.assertEquals(o, eo, u"%s is '%s' while expected '%s'" % (i, o, eo))
def Draw(self, ctx): """ Draws the crosshair dc (wx.DC) """ # TODO: handle displaying the focus 0 (horizontally) if self.shifts[1]: ctx.set_line_width(10) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(1.0, 1.0, 1.0, 0.8) x, y = self.cnvs.ClientSize x -= self.margin + (self.line_width // 2) middle = y / 2 shift = self.shifts[1] * 1e6 # typically within µm end_y = middle - (middle * (shift / (y / 2))) end_y = min(max(self.margin, end_y), y - self.margin) ctx.move_to(x, middle) ctx.line_to(x, end_y) ctx.stroke() lbl = "focus %s" % units.readable_str(self.shifts[1], 'm', 2) self.focus_label.text = lbl self.focus_label.pos = (x - 15, end_y) self._write_label(ctx, self.focus_label)
def _display_pretty(self): if self._number_value is None: str_val = u"" elif self._number_value == 0 and self.unit not in units.IGNORE_UNITS: # Special case with 0: readable_str return just "0 unit", without # prefix. This is technically correct, but quite inconvenient and # a little strange when the typical value has a prefix (eg, nm, kV). # => use prefix of key_step (as it's a "small value") if self.key_step: _, prefix = units.get_si_scale(self.key_step) elif self.key_step_min: _, prefix = units.get_si_scale(self.key_step_min) else: prefix = "" str_val = "0 %s%s" % (prefix, self.unit) else: str_val = units.readable_str(self._number_value, self.unit, self.accuracy) # Get the length of the number, without the unit (number and unit are separated by a space) number_length = str_val.find(" ") if number_length < 0: # No space found -> only numbers number_length = len(str_val) wx.TextCtrl.ChangeValue(self, str_val) # Select the number value wx.CallAfter(self.SetSelection, number_length, number_length)
def add_metadata(self, key, value): """ Adds an entry representing specific metadata According to the metadata key, the right representation is used for the value. :param key: (model.MD_*) the metadata key :param value: (depends on the metadata) the value to display """ # By default the key is a nice user-readable string label = str(key) # Convert value to a nice string according to the metadata type try: if key == model.MD_ACQ_DATE: # convert to a date using the user's preferences nice_str = time.strftime("%c", time.localtime(value)) # In Python 2, we still need to convert it to unicode if isinstance(nice_str, bytes): nice_str = nice_str.decode(locale.getpreferredencoding()) else: # Still try to beautify a bit if it's a number if (isinstance(value, (int, long, float)) or (isinstance(value, collections.Iterable) and len(value) > 0 and isinstance(value[0], (int, long, float)))): nice_str = readable_str(value, sig=3) else: nice_str = str(value) except Exception: logging.exception("Trying to convert metadata %s", key) nice_str = "N/A" self.panel.add_readonly_field(label, nice_str)
def add_metadata(self, key, value): """ Adds an entry representing specific metadata According to the metadata key, the right representation is used for the value. :param key: (model.MD_*) the metadata key :param value: (depends on the metadata) the value to display """ # By default the key is a nice user-readable string label = unicode(key) # Convert value to a nice string according to the metadata type try: if key == model.MD_ACQ_DATE: # convert to a date using the user's preferences nice_str = time.strftime(u"%c", time.localtime(value)) else: # Still try to beautify a bit if it's a number if isinstance(value, (int, long, float)) or ( isinstance(value, collections.Iterable) and len(value) > 0 and isinstance(value[0], (int, long, float)) ): nice_str = readable_str(value, sig=3) else: nice_str = unicode(value) except Exception: logging.exception("Trying to convert metadata %s", key) nice_str = "N/A" self.panel.add_readonly_field(label, nice_str)
def Draw(self, ctx, shift=(0, 0), scale=1.0): if self.w_start_pos and self.w_end_pos: ctx.save() # Important: We need to use the world positions, in order to draw everything at the # right scale. offset = self.cnvs.get_half_buffer_size() b_pos = (self.cnvs.world_to_buffer(self.w_start_pos, offset) + self.cnvs.world_to_buffer(self.w_end_pos, offset)) b_pos = self._normalize(b_pos) self.update_from_buffer(b_pos[:2], b_pos[2:4], shift + (scale, )) #logging.warn("%s %s", shift, world_to_buffer_pos(shift)) rect = (b_pos[0] + 0.5, b_pos[1] + 0.5, b_pos[2] - b_pos[0], b_pos[3] - b_pos[1]) # draws a light black background for the rectangle ctx.set_line_width(4) ctx.set_source_rgba(0, 0, 0, 0.5) ctx.rectangle(*rect) ctx.stroke() # draws the dotted line ctx.set_line_width(2) ctx.set_dash([3]) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(*self.colour) ctx.rectangle(*rect) ctx.stroke() # Label if (self.selection_mode in (base.SEL_MODE_EDIT, base.SEL_MODE_CREATE) and self.cnvs.microscope_view): w, h = self.cnvs.selection_to_real_size( self.w_start_pos, self.w_end_pos) w = units.readable_str(w, 'm', sig=2) h = units.readable_str(h, 'm', sig=2) size_lbl = u"{} x {}".format(w, h) pos = (b_pos[2] + 8, b_pos[3] - 10) self.position_label.pos = pos self.position_label.text = size_lbl self._write_labels(ctx) ctx.restore()
def format_axis_choices(name, axis_def): """ Transform the given choices for an axis into an user friendly display name (str): the name of the axis axis_def (Axis): the axis definition returns: choices_formatted (None or list of (value, str): axis value/user-friendly display name (including the unit). None if axis doesn't support choices. """ try: choices = axis_def.choices except AttributeError: return None if not choices: return None unit = axis_def.unit if isinstance(choices, dict): choices_formatted = list(choices.items()) # In this case, normally the values are already formatted, but for # wavelength band, the "formatted" value is still a band info (ie, two # values in m) if name == "band": def to_readable_band(v): if (isinstance(v, (tuple, list)) and len(v) > 1 and all(isinstance(c, numbers.Real) for c in v)): return fluo.to_readable_band(v) else: return v choices_formatted = [(k, to_readable_band(v)) for k, v in choices_formatted] elif len(choices) > 1 and all(isinstance(c, numbers.Real) for c in choices): choices_formatted = None try: choices = sorted(choices) # Can we fit them (more or less) all with the same unit prefix? mn_non0 = min(c for c in choices if c != 0) if abs(choices[-1] / mn_non0) < 1000: fmt, choices_si_prefix = utun.si_scale_list(choices) fmt = [utun.to_string_pretty(c, 3, unit) for c in fmt] choices_formatted = list(zip(choices, fmt)) except Exception: logging.exception("Formatting error for %s", choices) if choices_formatted is None: choices_formatted = [(c, readable_str(c, unit=unit, sig=3)) for c in choices] else: choices_formatted = [(c, u"%s %s" % (choice_to_str(c), unit)) for c in choices] if not isinstance(choices, OrderedDict): # sort 2-tuples = according to first value in tuple choices_formatted = sorted(choices_formatted) return choices_formatted
def format_axis_choices(name, axis_def): """ Transform the given choices for an axis into an user friendly display name (str): the name of the axis axis_def (Axis): the axis definition returns: choices_formatted (None or list of (value, str): axis value/user-friendly display name (including the unit). None if axis doesn't support choices. """ try: choices = axis_def.choices except AttributeError: return None if not choices: return None unit = axis_def.unit if isinstance(choices, dict): choices_formatted = choices.items() # In this case, normally the values are already formatted, but for # wavelength band, the "formatted" value is still a band info (ie, two # values in m) if name == "band": def to_readable_band(v): if (isinstance(v, (tuple, list)) and len(v) > 1 and all(isinstance(c, numbers.Real) for c in v)): return fluo.to_readable_band(v) else: return v choices_formatted = [(k, to_readable_band(v)) for k, v in choices_formatted] elif len(choices) > 1 and all(isinstance(c, numbers.Real) for c in choices): choices_formatted = None try: choices = sorted(choices) # Can we fit them (more or less) all with the same unit prefix? mn_non0 = min(c for c in choices if c != 0) if abs(choices[-1] / mn_non0) < 1000: fmt, choices_si_prefix = utun.si_scale_list(choices) fmt = [utun.to_string_pretty(c, 3, unit) for c in fmt] choices_formatted = zip(choices, fmt) except Exception: logging.exception("Formatting error for %s", choices) if choices_formatted is None: choices_formatted = [(c, readable_str(c, unit=unit, sig=3)) for c in choices] else: choices_formatted = [(c, u"%s %s" % (choice_to_str(c), unit)) for c in choices] if not isinstance(choices, OrderedDict): # sort 2-tuples = according to first value in tuple choices_formatted = sorted(choices_formatted) return choices_formatted
def move(comp_name, moves, check_distance=True, to_radians=False): """ move (relatively) the axis of the given component by the specified amount of µm comp_name (str): name of the component moves (dict str -> str): axis -> distance (as text, and in µm for distances) check_distance (bool): if the axis is in meters, check that the move is not too big. to_radians (bool): will convert from degrees to radians if the axis is in radians, otherwise will fail """ # for safety reason, we use µm instead of meters, as it's harder to type a # huge distance component = get_component(comp_name) act_mv = {} # axis -> value for axis_name, str_distance in moves.items(): try: if axis_name not in component.axes: raise ValueError("Actuator %s has no axis '%s'" % (comp_name, axis_name)) ad = component.axes[axis_name] except (TypeError, AttributeError): raise ValueError("Component %s is not an actuator" % comp_name) if ad.unit == "m": try: # Use convert_to_object() to allow typing negative values with e: # -1e-6 => '!!float -1.0e-6'. It's not very nice, but does work. distance = float(convert_to_object(str_distance)) * 1e-6 # µm -> m except ValueError: raise ValueError("Distance '%s' cannot be converted to a number" % str_distance) if check_distance and abs(distance) > MAX_DISTANCE: raise IOError("Distance of %f m is too big (> %f m), use '--big-distance' to allow the move." % (abs(distance), MAX_DISTANCE)) else: distance = convert_to_object(str_distance) if to_radians: if ad.unit == "rad": distance = math.radians(distance) else: raise ValueError("Axis %s is in %s, doesn't support value in degrees" % (axis_name, ad.unit)) act_mv[axis_name] = distance logging.info(u"Will move %s.%s by %s", comp_name, axis_name, units.readable_str(distance, ad.unit, sig=3)) try: m = component.moveRel(act_mv) try: m.result(120) except KeyboardInterrupt: logging.warning("Cancelling relative move of component %s", comp_name) m.cancel() raise except Exception as exc: raise IOError("Failed to move component %s by %s: %s" % (comp_name, act_mv, exc))
def UpdateHFWLabel(self): """ Physical width of the display""" if not self._microscope_view: return hfw = self._microscope_view.mpp.value * self.GetClientSize()[0] hfw = units.round_significant(hfw, 4) label = u"HFW: %s" % units.readable_str(hfw, "m", sig=3) self.bottom_legend.set_hfw_label(label)
def _update_label(self, pressure_val): """ Set a formatted pressure value as the label of the button """ str_value = units.readable_str( pressure_val, sig=2, unit=self.main_data.chamber.pressure.unit) if self.btn.Label != str_value: self.btn.Label = str_value self.btn.Refresh()
def Draw(self, ctx, shift=(0, 0), scale=1.0): if self.w_start_pos and self.w_end_pos: ctx.save() # Important: We need to use the world positions, in order to draw everything at the # right scale. offset = self.cnvs.get_half_buffer_size() b_pos = (self.cnvs.world_to_buffer(self.w_start_pos, offset) + self.cnvs.world_to_buffer(self.w_end_pos, offset)) b_pos = self._normalize(b_pos) self.update_from_buffer(b_pos[:2], b_pos[2:4], shift + (scale,)) #logging.warn("%s %s", shift, world_to_buffer_pos(shift)) rect = (b_pos[0] + 0.5, b_pos[1] + 0.5, b_pos[2] - b_pos[0], b_pos[3] - b_pos[1]) # draws a light black background for the rectangle ctx.set_line_width(4) ctx.set_source_rgba(0, 0, 0, 0.5) ctx.rectangle(*rect) ctx.stroke() # draws the dotted line ctx.set_line_width(2) ctx.set_dash([3]) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(*self.colour) ctx.rectangle(*rect) ctx.stroke() # Label if (self.selection_mode in (base.SEL_MODE_EDIT, base.SEL_MODE_CREATE) and self.cnvs.microscope_view): w, h = self.cnvs.selection_to_real_size(self.w_start_pos, self.w_end_pos) w = units.readable_str(w, 'm', sig=2) h = units.readable_str(h, 'm', sig=2) size_lbl = u"{} x {}".format(w, h) pos = (b_pos[2] + 8, b_pos[3] - 10) self.position_label.pos = pos self.position_label.text = size_lbl self._write_labels(ctx) ctx.restore()
def Draw(self, ctx): # TODO: Both focuses at the same time, or 'snap' to horizontal/vertical on first motion? ctx.set_line_width(10) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(1.0, 1.0, 1.0, 0.8) x, y = self.cnvs.ClientSize # Horizontal if self.shifts[0]: y -= self.margin + (self.line_width // 2) middle = x / 2 shift = self.shifts[0] * 1e6 # typically within µm end_x = middle + (middle * (shift / (x / 2))) end_x = min(max(self.margin, end_x), x - self.margin) ctx.move_to(middle, y) ctx.line_to(end_x, y) ctx.stroke() lbl = "focus %s" % units.readable_str(self.shifts[0], 'm', 2) self.focus_label.text = lbl self.focus_label.pos = (end_x, y - 15) self._write_label(ctx, self.focus_label) # Vertical if self.shifts[1]: x -= self.margin + (self.line_width // 2) middle = y / 2 shift = self.shifts[1] * 1e6 # typically within µm end_y = middle - (middle * (shift / (y / 2))) end_y = min(max(self.margin, end_y), y - self.margin) ctx.move_to(x, middle) ctx.line_to(x, end_y) ctx.stroke() lbl = "focus %s" % units.readable_str(self.shifts[1], 'm', 2) self.focus_label.text = lbl self.focus_label.pos = (x - 15, end_y) self._write_label(ctx, self.focus_label)
def on_paint(self, _): if self._value_range is None: return # shared function with the export method self._tick_list, self._vtp_ratio = calculate_ticks( self._value_range, self.ClientSize, self._orientation, self._tick_spacing) csize = self.ClientSize # Set Font font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font.GetPointSize()) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) max_width = 0 prev_lpos = 0 if self._orientation == wx.HORIZONTAL else csize.y for i, (pos, val) in enumerate(self._tick_list): label = units.readable_str(val, self.unit, 3) _, _, lbl_width, lbl_height, _, _ = ctx.text_extents(label) if self._orientation == wx.HORIZONTAL: lpos = pos - (lbl_width // 2) lpos = max(min(lpos, csize.x - lbl_width - 2), 2) # print (i, prev_right, lpos) if prev_lpos < lpos: ctx.move_to(lpos, lbl_height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) prev_lpos = lpos + lbl_width else: max_width = max(max_width, lbl_width) lpos = pos + (lbl_height // 2) lpos = max(min(lpos, csize.y), 2) if prev_lpos >= lpos + 20 or i == 0: ctx.move_to(csize.x - lbl_width - 9, lpos) ctx.show_text(label) ctx.move_to(csize.x - 5, pos) ctx.line_to(csize.x, pos) prev_lpos = lpos + lbl_height ctx.stroke() if self._orientation == wx.VERTICAL and max_width != self._max_tick_width: self._max_tick_width = max_width self.SetMinSize((self._max_tick_width + 14, -1)) self.Parent.GetSizer().Layout()
def Draw(self, ctx, shift=(0, 0), scale=1.0): if self.w_start_pos and self.w_end_pos: offset = [v // 2 for v in self.cnvs._bmp_buffer_size] b_pos = (self.cnvs.world_to_buffer(self.w_start_pos, offset) + self.cnvs.world_to_buffer(self.w_end_pos, offset)) b_pos = util.normalize_rect(b_pos) self.update_from_buffer(b_pos[:2], b_pos[2:4], shift + (scale,)) #logging.warn("%s %s", shift, world_to_buffer_pos(shift)) rect = (b_pos[0] + 0.5, b_pos[1] + 0.5, b_pos[2] - b_pos[0], b_pos[3] - b_pos[1]) # draws a light black background for the rectangle ctx.set_line_width(2.5) ctx.set_source_rgba(0, 0, 0, 0.5) ctx.rectangle(*rect) ctx.stroke() # draws the dotted line ctx.set_line_width(2) ctx.set_dash([3,]) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(*self.colour) ctx.rectangle(*rect) ctx.stroke() # Label if (self.dragging or self.edit) and self.cnvs.microscope_view: w, h = self.cnvs.selection_to_real_size( self.w_start_pos, self.w_end_pos ) w = units.readable_str(w, 'm', sig=2) h = units.readable_str(h, 'm', sig=2) size_lbl = u"{} x {}".format(w, h) pos = (b_pos[2] + 10, b_pos[3] + 5) self.position_label.pos = pos self.position_label.text = size_lbl self._write_labels(ctx)
def _display_pretty(self): if self._number_value is None: str_val = u"" else: str_val = units.readable_str(self._number_value, self.unit, self.accuracy) # Get the length of the number (string length, minus the unit length) number_length = len(str_val.rstrip(string.ascii_letters + u" µ")) wx.TextCtrl.ChangeValue(self, str_val) # Select the number value wx.CallAfter(self.SetSelection, number_length, number_length)
def draw(self, ctx): ctx.set_line_width(self.line_width) ctx.set_dash([3]) ctx.set_line_join(cairo.LINE_JOIN_MITER) ctx.set_source_rgba(*self.colour) if self.val.value is not None: val = self.val.value if self.map_y_from_x: # Maps Y and also snap X to the closest X value in the data val = self.cnvs.val_x_to_val(val[0]) v_pos = self.cnvs.val_to_pos(val) self.x_label = units.readable_str(val[0], self.cnvs.unit_x, 3) self.y_label = units.readable_str(val[1], self.cnvs.unit_y, 3) # v_posx, v_posy = self.v_pos.value if self.orientation & self.VERTICAL: ctx.move_to(v_pos[0], 0) ctx.line_to(v_pos[0], self.cnvs.ClientSize.y) ctx.stroke() if self.orientation & self.HORIZONTAL: ctx.move_to(0, v_pos[1]) ctx.line_to(self.cnvs.ClientSize.x, v_pos[1]) ctx.stroke() if self.x_label.text: self.x_label.pos = (v_pos[0] + 5, self.cnvs.ClientSize.y) self._write_label(ctx, self.x_label) if self.y_label.text: yp = max(0, v_pos[1] - 5) # Padding from line # Increase bottom margin if x label is close label_padding = 30 if v_pos[0] < 50 else 0 yn = min(self.view_height - label_padding, yp) self.y_label.pos = (2, yn) self._write_label(ctx, self.y_label) r, g, b, a = conversion.change_brightness(self.colour, -0.2) ctx.set_source_rgba(r, g, b, 0.5) ctx.arc(v_pos[0], v_pos[1], 5.5, 0, 2 * math.pi) ctx.fill()
def on_paint(self, _): if self._value_range is None: return # shared function with the export method self._tick_list, self._vtp_ratio = calculate_ticks(self._value_range, self.ClientSize, self._orientation, self._tick_spacing) csize = self.ClientSize # Set Font font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font.GetPointSize()) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) max_width = 0 prev_lpos = 0 if self._orientation == wx.HORIZONTAL else csize.y for i, (pos, val) in enumerate(self._tick_list): label = units.readable_str(val, self.unit, 3) _, _, lbl_width, lbl_height, _, _ = ctx.text_extents(label) if self._orientation == wx.HORIZONTAL: lpos = pos - (lbl_width // 2) lpos = max(min(lpos, csize.x - lbl_width - 2), 2) # print (i, prev_right, lpos) if prev_lpos < lpos: ctx.move_to(lpos, lbl_height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) prev_lpos = lpos + lbl_width else: max_width = max(max_width, lbl_width) lpos = pos + (lbl_height // 2) lpos = max(min(lpos, csize.y), 2) if prev_lpos >= lpos + 20 or i == 0: ctx.move_to(csize.x - lbl_width - 9, lpos) ctx.show_text(label) ctx.move_to(csize.x - 5, pos) ctx.line_to(csize.x, pos) prev_lpos = lpos + lbl_height ctx.stroke() if self._orientation == wx.VERTICAL and max_width != self._max_tick_width: self._max_tick_width = max_width self.SetMinSize((self._max_tick_width + 14, -1)) self.Parent.GetSizer().Layout()
def cb_set(value, ctrl=value_ctrl, u=unit): for i in range(ctrl.Count): if ctrl.GetClientData(i) == value: logging.debug("Setting combobox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.debug("No existing label found for value %s in combobox ctrl %d", value, id(ctrl)) # entering value as free text txt = readable_str(value, u, sig=accuracy) return ctrl.SetValue(txt)
def cb_set(value, ctrl=value_ctrl, unit=unit): for i in range(ctrl.Count): if ((isinstance(value, float) and util.almost_equal(ctrl.GetClientData(i), value)) or ctrl.GetClientData(i) == value): logging.debug("Setting ComboBox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.warning("No existing label found for value %s", value) # entering value as free text txt = readable_str(value, unit) ctrl.SetValue(txt)
def on_paint(self, evt=None): if not hasattr(self.Parent.canvas, 'has_data') or not self.Parent.canvas.has_data(): self.clear() return ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font.GetPointSize()) # TODO: only put label for some of the ticks if not self.ticks: self.calc_ticks(ctx) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) max_width = 0 for i, (pos, val) in enumerate(self.ticks): label = units.readable_str(val, self.unit, 3) _, _, lbl_width, lbl_height, _, _ = ctx.text_extents(label) if self.orientation == wx.HORIZONTAL: lpos = pos - (lbl_width / 2) lpos = max(min(lpos, self.ClientSize.x - lbl_width - 2), 2) ctx.move_to(lpos, lbl_height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) else: max_width = max(max_width, lbl_width) lpos = pos + (lbl_height / 2) lpos = max(min(lpos, self.ClientSize.y), 2) ctx.move_to(self.ClientSize.x - lbl_width - 9, lpos) ctx.show_text(label) ctx.move_to(self.ClientSize.x - 5, pos) ctx.line_to(self.ClientSize.x, pos) ctx.stroke() if self.orientation == wx.VERTICAL and max_width != self.max_tick_width: self.max_tick_width = max_width self.SetMinSize((self.max_tick_width + 14, -1)) self.Parent.GetSizer().Layout()
def UpdateMagnification(self): # Total magnification mag = self._mpp_screen / self._microscope_view.mpp.value label = u"Mag: × %s" % units.readable_str(units.round_significant(mag, 3)) # Gather all different image mpp values mpps = set() for im in self._microscope_view.stream_tree.getImages(): try: mpps.add(im.metadata[model.MD_PIXEL_SIZE][0]) except KeyError: pass # If there's only one mpp value (i.e. there's only one image, or they # all have the same mpp value), indicate the digital zoom. if len(mpps) == 1: mpp_im = mpps.pop() # mag_im = self._mpp_screen / mpp_im # as if 1 im.px == 1 sc.px mag_dig = mpp_im / self._microscope_view.mpp.value label += u" (Digital: × %s)" % units.readable_str(units.round_significant(mag_dig, 2)) self.bottom_legend.set_mag_label(label)
def move(comp_name, moves, check_distance=True): """ move (relatively) the axis of the given component by the specified amount of µm comp_name (str): name of the component check_distance (bool): if the axis is in meters, check that the move is not too big. moves (dict str -> str): axis -> distance (as text, and in µm for distances) """ # for safety reason, we use µm instead of meters, as it's harder to type a # huge distance component = get_component(comp_name) act_mv = {} # axis -> value for axis_name, str_distance in moves.items(): try: if axis_name not in component.axes: raise ValueError("Actuator %s has no axis '%s'" % (comp_name, axis_name)) ad = component.axes[axis_name] except (TypeError, AttributeError): raise ValueError("Component %s is not an actuator" % comp_name) if ad.unit == "m": try: # Use convert_to_object() to allow typing negative values with e: # -1e-6 => '!!float -1.0e-6'. It's not very nice, but does work. distance = float(convert_to_object(str_distance)) * 1e-6 # µm -> m except ValueError: raise ValueError("Distance '%s' cannot be converted to a number" % str_distance) if check_distance and abs(distance) > MAX_DISTANCE: raise IOError("Distance of %f m is too big (> %f m), use '--big-distance' to allow the move." % (abs(distance), MAX_DISTANCE)) else: distance = convert_to_object(str_distance) act_mv[axis_name] = distance logging.info(u"Will move %s.%s by %s", comp_name, axis_name, units.readable_str(distance, ad.unit, sig=3)) try: m = component.moveRel(act_mv) try: m.result(120) except KeyboardInterrupt: logging.warning("Cancelling relative move of component %s", comp_name) m.cancel() raise except Exception as exc: raise IOError("Failed to move component %s by %s: %s" % (comp_name, act_mv, exc))
def cb_set(value, ctrl=value_ctrl, u=unit, acc=accuracy): for i in range(ctrl.Count): d = ctrl.GetClientData(i) if (d == value or (all(isinstance(v, float) for v in (value, d)) and util.almost_equal(d, value)) ): logging.debug("Setting combobox value to %s", ctrl.Items[i]) ctrl.SetSelection(i) break else: logging.debug("No existing label found for value %s in combobox ctrl %d", value, id(ctrl)) # entering value as free text txt = readable_str(value, u, sig=acc) ctrl.SetValue(txt)
def _display_pretty(self): if self._number_value is None: str_val = u"" elif self._number_value == 0 and self.key_step and self.unit not in units.IGNORE_UNITS: # Special case with 0: readable_str return just "0 unit", without # prefix. This is technically correct, but quite inconvenient and # a little strange when the typical value has a prefix (eg, nm, kV). # => use prefix of key_step (as it's a "small value") _, prefix = units.get_si_scale(self.key_step) str_val = "0 %s%s" % (prefix, self.unit) else: str_val = units.readable_str(self._number_value, self.unit, self.accuracy) # Get the length of the number (string length, minus the unit length) number_length = len(str_val.rstrip(string.ascii_letters + u" µ")) wx.TextCtrl.ChangeValue(self, str_val) # Select the number value wx.CallAfter(self.SetSelection, number_length, number_length)
def on_paint(self, event=None): if not self.Parent.canvas.has_data(): self.clear() return if not self.ticks: self.calc_ticks() ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx.select_font_face( font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL ) ctx.set_font_size(font.GetPointSize()) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) for i, (pos, val) in enumerate(self.ticks): label = units.readable_str(val, self.unit, 3) _, _, width, height, _, _ = ctx.text_extents(label) if self.orientation == self.HORIZONTAL: lpos = pos - (width / 2) lpos = max(min(lpos, self.ClientSize.x - width - 2), 2) ctx.move_to(lpos, height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) else: lpos = pos + (height / 2) lpos = max(min(lpos, self.ClientSize.y - height - 2), 2) ctx.move_to(self.ClientSize.x - width - 9, lpos) ctx.show_text(label) ctx.move_to(self.ClientSize.x - 5, pos) ctx.line_to(self.ClientSize.x, pos) ctx.stroke()
def on_paint(self, event=None): if not self.Parent.canvas.has_data(): self.clear() return if not self.ticks: self.calc_ticks() ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font.GetPointSize()) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) for i, (pos, val) in enumerate(self.ticks): label = units.readable_str(val, self.unit, 3) _, _, width, height, _, _ = ctx.text_extents(label) if self.orientation == self.HORIZONTAL: lpos = pos - (width / 2) lpos = max(min(lpos, self.ClientSize.x - width - 2), 2) ctx.move_to(lpos, height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) else: lpos = pos + (height / 2) lpos = max(min(lpos, self.ClientSize.y - height - 2), 2) ctx.move_to(self.ClientSize.x - width - 9, lpos) ctx.show_text(label) ctx.move_to(self.ClientSize.x - 5, pos) ctx.line_to(self.ClientSize.x, pos) ctx.stroke()
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model try: trans_val, cor_md = future.result() # The magic: save the correction metadata straight into the CCD main_data.ccd.updateMetadata(cor_md) except CancelledError: self._main_frame.lbl_fine_align.Label = "Cancelled" except Exception: logging.warning("Failed to run the fine alignment, a report " "should be available in ~/odemis-overlay-report.") self._main_frame.lbl_fine_align.Label = "Failed" else: self._main_frame.lbl_fine_align.Label = "Successful" self._main_frame.menu_item_reset_finealign.Enable(True) # Temporary info until the GUI can actually rotate the images if model.MD_ROTATION_COR in cor_md: rot = math.degrees(cor_md[model.MD_ROTATION_COR]) # the worse is the rotation, the longer it's displayed timeout = max(2, min(abs(rot), 10)) Message.show_message( self._main_frame, u"Rotation applied: %s" % (units.readable_str(rot, unit="°", sig=3)), timeout=timeout) logging.warning( "Fine alignment computed rotation needed of %f°", rot) # As the CCD image might have different pixel size, force to fit self._main_frame.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._main_frame.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._main_frame.btn_fine_align.Label = self._fa_btn_label self._resume() self._main_frame.lbl_fine_align.Show() self._main_frame.gauge_fine_align.Hide() self._sizer.Layout()
def move(comp_name, axis_name, str_distance): """ move (relatively) the axis of the given component by the specified amount of µm """ # for safety reason, we use µm instead of meters, as it's harder to type a # huge distance component = get_component(comp_name) try: if axis_name not in component.axes: raise ValueError("Actuator %s has no axis '%s'" % (comp_name, axis_name)) ad = component.axes[axis_name] except (TypeError, AttributeError): raise ValueError("Component %s is not an actuator" % comp_name) if ad.unit == "m": try: distance = float(str_distance) * 1e-6 # µm -> m except ValueError: raise ValueError("Distance '%s' cannot be converted to a number" % str_distance) if abs(distance) > MAX_DISTANCE: raise IOError("Distance of %f m is too big (> %f m)" % (abs(distance), MAX_DISTANCE)) else: distance = convertToObject(str_distance) try: logging.info(u"Will move %s.%s by %s", comp_name, axis_name, units.readable_str(distance, ad.unit, sig=3)) except Exception: pass try: m = component.moveRel({axis_name: distance}) m.result() except Exception as exc: raise IOError("Failed to move axis %s of component %s: %s" % (axis_name, comp_name, exc))
def move_abs(comp_name, axis_name, str_position): """ move (in absolute) the axis of the given component to the specified position in µm """ component = get_component(comp_name) try: if axis_name not in component.axes: raise ValueError("Actuator %s has no axis '%s'" % (comp_name, axis_name)) ad = component.axes[axis_name] except (TypeError, AttributeError): raise ValueError("Component %s is not an actuator" % comp_name) if ad.unit == "m": try: position = float(str_position) except ValueError: raise ValueError("Position '%s' cannot be converted to a number" % str_position) # compare to the current position, to see if the new position sounds reasonable cur_pos = component.position.value[axis_name] if abs(cur_pos - position) > MAX_DISTANCE: raise IOError("Distance of %f m is too big (> %f m)" % (abs(cur_pos - position), MAX_DISTANCE)) else: position = convertToObject(str_position) try: logging.info(u"Will move %s.%s to %s", comp_name, axis_name, units.readable_str(position, ad.unit, sig=3)) except Exception: pass try: m = component.moveAbs({axis_name: position}) m.result() except Exception as exc: raise IOError("Failed to move axis %s of component %s: %s" % (axis_name, comp_name, exc))
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model try: trans_val, cor_md = future.result() # The magic: save the correction metadata straight into the CCD main_data.ccd.updateMetadata(cor_md) except CancelledError: self._main_frame.lbl_fine_align.Label = "Cancelled" except Exception: logging.warning("Failed to run the fine alignment, a report " "should be available in ~/odemis-overlay-report.") self._main_frame.lbl_fine_align.Label = "Failed" else: self._main_frame.lbl_fine_align.Label = "Successful" self._main_frame.menu_item_reset_finealign.Enable(True) # Temporary info until the GUI can actually rotate the images if model.MD_ROTATION_COR in cor_md: rot = math.degrees(cor_md[model.MD_ROTATION_COR]) # the worse is the rotation, the longer it's displayed timeout = max(2, min(abs(rot), 10)) Message.show_message( self._main_frame, u"Rotation applied: %s" % (units.readable_str(rot, unit="°", sig=3)), timeout=timeout ) logging.warning("Fine alignment computed rotation needed of %f°", rot) # As the CCD image might have different pixel size, force to fit self._main_frame.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._main_frame.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._main_frame.btn_fine_align.Label = self._fa_btn_label self._resume() self._main_frame.lbl_fine_align.Show() self._main_frame.gauge_fine_align.Hide() self._sizer.Layout()
def Draw(self, dc): # return self.DrawGC(dc) nod = self.nod vmiddle = self.GetClientSize()[1] // 2 background_col = self.Parent.GetBackgroundColour() foreground_col = self.Parent.GetForegroundColour() dc.SetBackgroundMode(wx.BRUSHSTYLE_SOLID) dc.SetBackground(wx.Brush(background_col)) dc.Clear() if not self.mpp: # unknown mpp => blank return dc.SetFont(self.GetFont()) dc.SetTextForeground(foreground_col) dc.SetTextBackground(background_col) length, actual = self.GetLineWidth(dc) # Draw the text below charSize = dc.GetTextExtent("M") height = self.gap + charSize[1] + self.nod main_line_y = vmiddle - (height // 2) + self.nod dc.DrawText(units.readable_str(actual, "m", sig=2), 0, main_line_y + self.gap) # Draw the scale itself pen = wx.Pen(foreground_col, self.line_width) pen.Cap = wx.CAP_PROJECTING # how to draw the border of the lines dc.SetPen(pen) # main line lines = [(0, main_line_y, length, main_line_y)] # nods at each end lines += [(0, main_line_y - nod, 0, main_line_y)] lines += [(length, main_line_y - nod, length, main_line_y)] dc.DrawLineList(lines)
def UpdateMagnification(self): # TODO: shall we use the real density of the screen? # We could use real density but how much important is it? mppScreen = 0.00025 # 0.25 mm/px label = u"Mag: " # three possibilities: # * no image => total mag (using current mpp) # * all images have same mpp => mag instrument * mag digital # * >1 mpp => total mag # get all the mpps mpps = set() for im in self._microscope_view.stream_tree.getImages(): try: mpps.add(im.metadata[model.MD_PIXEL_SIZE][0]) except KeyError: pass if len(mpps) == 1: # two magnifications im_mpp = mpps.pop() magIm = mppScreen / im_mpp # as if 1 im.px == 1 sc.px if magIm >= 1: label += u"×" + units.readable_str( units.round_significant(magIm, 3)) else: label += u"÷" + units.readable_str( units.round_significant(1.0 / magIm, 3)) magDig = im_mpp / self._microscope_view.mpp.value if magDig >= 1: label += u" ×" + units.readable_str( units.round_significant(magDig, 3)) else: label += u" ÷" + units.readable_str( units.round_significant(1.0 / magDig, 3)) else: # one magnification mag = mppScreen / self._microscope_view.mpp.value if mag >= 1: label += u"×" + units.readable_str( units.round_significant(mag, 3)) else: label += u"÷" + units.readable_str( units.round_significant(1.0 / mag, 3)) self.legend.set_mag_label(label)
def test_readable_str(self): # (input) (expected output) values = [ ((1.0, None), "1"), ((1, None), "1"), ((-1.234, "m"), "-1.234 m"), ((-1234, "g"), "-1.234 kg"), ((160000, None), "160000"), ((-1600, "N"), "-1.6 kN"), ((-1601, "N", 3), "-1.6 kN"), # sig=3 ((0.0001236, None), "0.0001236"), ((0.0012, "V"), "1.2 mV"), ((200e-6, "m"), u"200 µm"), ((0.0, "m"), "0 m"), (([1500, 1200, 150], None), "1500 x 1200 x 150"), (([0.0001236, 0.00014], "m"), u"123.6 x 140 µm"), (([0.0001236, 12.0], "m"), "0.0001236 x 12 m"), (([1200, 1000], "px"), "1200 x 1000 px"), # special non-prefix unit ] for (i, eo) in values: o = units.readable_str(*i) self.assertEquals(o, eo, u"%s is '%s' while expected '%s'" % (i, o, eo))
def UpdateMagnification(self): # TODO: shall we use the real density of the screen? # We could use real density but how much important is it? mppScreen = 0.00025 # 0.25 mm/px label = u"Mag: " # three possibilities: # * no image => total mag (using current mpp) # * all images have same mpp => mag instrument * mag digital # * >1 mpp => total mag # get all the mpps mpps = set() for im in self._microscope_view.stream_tree.getImages(): try: mpps.add(im.metadata[model.MD_PIXEL_SIZE][0]) except KeyError: pass if len(mpps) == 1: # two magnifications im_mpp = mpps.pop() magIm = mppScreen / im_mpp # as if 1 im.px == 1 sc.px if magIm >= 1: label += u"×" + units.readable_str(units.round_significant(magIm, 3)) else: label += u"÷" + units.readable_str(units.round_significant(1.0 / magIm, 3)) magDig = im_mpp / self._microscope_view.mpp.value if magDig >= 1: label += u" ×" + units.readable_str(units.round_significant(magDig, 3)) else: label += u" ÷" + units.readable_str(units.round_significant(1.0 / magDig, 3)) else: # one magnification mag = mppScreen / self._microscope_view.mpp.value if mag >= 1: label += u"×" + units.readable_str(units.round_significant(mag, 3)) else: label += u"÷" + units.readable_str(units.round_significant(1.0 / mag, 3)) self.legend.set_mag_label(label)
def _runAcquisition(self, future): self._data = [] self._md = {} wls = self.startWavelength.value wle = self.endWavelength.value res = self.numberOfPixels.value dt = self.dwellTime.value trig = self._detector.softwareTrigger df = self._detector.data # Prepare the hardware self._emitter.resolution.value = (1, 1) # Force one pixel only self._emitter.translation.value = self.emtTranslation.value self._emitter.dwellTime.value = dt df.synchronizedOn(trig) df.subscribe(self._on_mchr_data) wllist = [] if wle == wls: res = 1 if res <= 1: res = 1 wli = 0 else: wli = (wle - wls) / (res - 1) try: for i in range(res): left = (res - i) * (dt + 0.05) future.set_progress(end=time.time() + left) cwl = wls + i * wli # requested value self._sgr.moveAbs({"wavelength": cwl}).result() if future._acq_state == CANCELLED: raise CancelledError() cwl = self._sgr.position.value["wavelength"] # actual value logging.info("Acquiring point %d/%d @ %s", i + 1, res, units.readable_str(cwl, unit="m", sig=3)) self._pt_acq.clear() trig.notify() if not self._pt_acq.wait(dt * 5 + 1): raise IOError("Timeout waiting for the data") if future._acq_state == CANCELLED: raise CancelledError() wllist.append(cwl) # Done df.unsubscribe(self._on_mchr_data) df.synchronizedOn(None) # Convert the sequence of data into one spectrum in a DataArray if wls > wle: # went backward? => sort back the spectrum logging.debug("Inversing spectrum as acquisition went from %g to %g m", wls, wls) self._data.reverse() wllist.reverse() na = numpy.array(self._data) # keeps the dtype na.shape += (1, 1, 1, 1) # make it 5th dim to indicate a channel md = self._md md[model.MD_WL_LIST] = wllist if model.MD_OUT_WL in md: # The MD_OUT_WL on the monochromator contains the current cw, which we don't want del md[model.MD_OUT_WL] # MD_POS should already be at the correct position (from the e-beam metadata) # MD_PIXEL_SIZE is not meaningful but handy for the display in Odemis # (it's the size of the square on top of the SEM survey => BIG!) sempxs = self._emitter.pixelSize.value md[model.MD_PIXEL_SIZE] = (sempxs[0] * 50, sempxs[1] * 50) spec = model.DataArray(na, md) with future._acq_lock: if future._acq_state == CANCELLED: raise CancelledError() future._acq_state = FINISHED return [spec] except CancelledError: raise # Just don't log the exception except Exception: logging.exception("Failure during monochromator scan") finally: # In case it was stopped before the end df.unsubscribe(self._on_mchr_data) df.synchronizedOn(None) future._acq_done.set()
def on_paint(self, _): if self._value_range is None: return # shared function with the export method self._tick_list, self._vtp_ratio = calculate_ticks(self._value_range, self.ClientSize, self._orientation, self._tick_spacing) csize = self.ClientSize if self.lock_va is not None and self.lock_va.value == False: if self._orientation == wx.HORIZONTAL: csize.x -= 24 # If min and max are very close, we need more significant numbers to # ensure the values displayed are different (ex 17999 -> 18003) rng = self._value_range if rng[0] == rng[1]: sig = None else: ratio_rng = max(abs(v) for v in rng) / (max(rng) - min(rng)) sig = max(3, 1 + math.ceil(math.log10(ratio_rng * len(self._tick_list)))) # Set Font font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) ctx = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self)) ctx.select_font_face(font.GetFaceName(), cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font.GetPointSize()) ctx.set_source_rgb(*self.tick_colour) ctx.set_line_width(2) ctx.set_line_join(cairo.LINE_JOIN_MITER) max_width = 0 prev_lpos = 0 if self._orientation == wx.HORIZONTAL else csize.y for i, (pos, val) in enumerate(self._tick_list): label = units.readable_str(val, self.unit, sig) if i == 0 and self._lo_ellipsis: label = u"…" + label if i == len(self._tick_list) - 1 and self._hi_ellipsis: label = label + u"…" _, _, lbl_width, lbl_height, _, _ = ctx.text_extents(label) if self._orientation == wx.HORIZONTAL: lpos = pos - (lbl_width // 2) lpos = max(min(lpos, csize.x - lbl_width - 2), 2) # print (i, prev_right, lpos) if prev_lpos < lpos: ctx.move_to(lpos, lbl_height + 8) ctx.show_text(label) ctx.move_to(pos, 5) ctx.line_to(pos, 0) prev_lpos = lpos + lbl_width else: max_width = max(max_width, lbl_width) lpos = pos + (lbl_height // 2) lpos = max(min(lpos, csize.y), 2) if prev_lpos >= lpos + 20 or i == 0: ctx.move_to(csize.x - lbl_width - 9, lpos) ctx.show_text(label) ctx.move_to(csize.x - 5, pos) ctx.line_to(csize.x, pos) prev_lpos = lpos + lbl_height ctx.stroke() if self._orientation == wx.VERTICAL and max_width != self._max_tick_width: self._max_tick_width = max_width self.SetMinSize((self._max_tick_width + 14, -1)) self.Parent.GetSizer().Layout()
def _runAcquisition(self, future): self._data = [] self._md = {} wls = self.startWavelength.value wle = self.endWavelength.value res = self.numberOfPixels.value dt = self.dwellTime.value trig = self._detector.softwareTrigger df = self._detector.data # Prepare the hardware self._emitter.resolution.value = (1, 1) # Force one pixel only self._emitter.translation.value = self.emtTranslation.value self._emitter.dwellTime.value = dt df.synchronizedOn(trig) df.subscribe(self._on_mchr_data) wllist = [] if wle == wls: res = 1 if res <= 1: res = 1 wli = 0 else: wli = (wle - wls) / (res - 1) try: for i in range(res): left = (res - i) * (dt + 0.05) future.set_progress(end=time.time() + left) cwl = wls + i * wli # requested value self._sgr.moveAbs({"wavelength": cwl}).result() if future._acq_state == CANCELLED: raise CancelledError() cwl = self._sgr.position.value["wavelength"] # actual value logging.info("Acquiring point %d/%d @ %s", i + 1, res, units.readable_str(cwl, unit="m", sig=3)) self._pt_acq.clear() trig.notify() if not self._pt_acq.wait(dt * 5 + 1): raise IOError("Timeout waiting for the data") if future._acq_state == CANCELLED: raise CancelledError() wllist.append(cwl) # Done df.unsubscribe(self._on_mchr_data) df.synchronizedOn(None) # Convert the sequence of data into one spectrum in a DataArray if wls > wle: # went backward? => sort back the spectrum logging.debug( "Inverting spectrum as acquisition went from %g to %g m", wls, wls) self._data.reverse() wllist.reverse() na = numpy.array(self._data) # keeps the dtype na.shape += (1, 1, 1, 1) # make it 5th dim to indicate a channel md = self._md md[model.MD_WL_LIST] = wllist if model.MD_OUT_WL in md: # The MD_OUT_WL on the monochromator contains the current cw, which we don't want del md[model.MD_OUT_WL] # MD_POS should already be at the correct position (from the e-beam metadata) # MD_PIXEL_SIZE is not meaningful but handy for the display in Odemis # (it's the size of the square on top of the SEM survey => BIG!) sempxs = self._emitter.pixelSize.value md[model.MD_PIXEL_SIZE] = (sempxs[0] * 50, sempxs[1] * 50) spec = model.DataArray(na, md) with future._acq_lock: if future._acq_state == CANCELLED: raise CancelledError() future._acq_state = FINISHED return [spec] except CancelledError: raise # Just don't log the exception except Exception: logging.exception("Failure during monochromator scan") finally: # In case it was stopped before the end df.unsubscribe(self._on_mchr_data) df.synchronizedOn(None) future._acq_done.set()
def value_formatter(value, unit=val_unit): value_ctrl.SetValue(readable_str(value, unit, sig=sig))
def move_abs(comp_name, moves, check_distance=True): """ move (in absolute) the axis of the given component to the specified position comp_name (str): name of the component check_distance (bool): if the axis is in meters, check that the move is not too big. moves (dict str -> str): axis -> position (as text) """ component = get_component(comp_name) act_mv = {} # axis -> value for axis_name, str_position in moves.items(): try: if axis_name not in component.axes: raise ValueError("Actuator %s has no axis '%s'" % (comp_name, axis_name)) ad = component.axes[axis_name] except (TypeError, AttributeError): raise ValueError("Component %s is not an actuator" % comp_name) # Allow the user to indicate the position via the user-friendly choice entry position = None if (hasattr(ad, "choices") and isinstance(ad.choices, dict)): for key, value in ad.choices.items(): if value == str_position: logging.info("Converting '%s' into %s", str_position, key) position = key # Even if it's a big distance, we don't complain as it's likely # that all choices are safe break if position is None: if ad.unit == "m": try: position = float(str_position) except ValueError: raise ValueError("Position '%s' cannot be converted to a number" % str_position) # compare to the current position, to see if the new position sounds reasonable cur_pos = component.position.value[axis_name] if check_distance and abs(cur_pos - position) > MAX_DISTANCE: raise IOError("Distance of %f m is too big (> %f m), use '--big-distance' to allow the move." % (abs(cur_pos - position), MAX_DISTANCE)) else: position = convert_to_object(str_position) # If only a couple of positions are possible, and asking for a float, # avoid the rounding error by looking for the closest possible if (isinstance(position, numbers.Real) and hasattr(ad, "choices") and isinstance(ad.choices, collections.Iterable) and position not in ad.choices): closest = util.find_closest(position, ad.choices) if util.almost_equal(closest, position, rtol=1e-3): logging.debug("Adjusting value %.15g to %.15g", position, closest) position = closest act_mv[axis_name] = position logging.info(u"Will move %s.%s to %s", comp_name, axis_name, units.readable_str(position, ad.unit, sig=3)) try: m = component.moveAbs(act_mv) try: m.result(120) except KeyboardInterrupt: logging.warning("Cancelling absolute move of component %s", comp_name) m.cancel() raise except Exception as exc: raise IOError("Failed to move component %s to %s: %s" % (comp_name, act_mv, exc))
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model self._acq_future = None # To avoid holding the ref in memory self._faf_connector = None try: # DEBUG try: trans_val, cor_md = future.result() except Exception: cor_md = {}, {} opt_md, sem_md = cor_md # Save the optical correction metadata straight into the CCD main_data.ccd.updateMetadata(opt_md) # The SEM correction metadata goes to the ebeam main_data.ebeam.updateMetadata(sem_md) except CancelledError: self._tab_panel.lbl_fine_align.Label = "Cancelled" except Exception as ex: logging.warning("Failure during overlay: %s", ex) self._tab_panel.lbl_fine_align.Label = "Failed" else: self._main_frame.menu_item_reset_finealign.Enable(True) # Check whether the values make sense. If not, we still accept them, # but hopefully make it clear enough to the user that the calibration # should not be trusted. rot = opt_md.get(model.MD_ROTATION_COR, 0) rot0 = (rot + math.pi) % (2 * math.pi) - math.pi # between -pi and pi rot_deg = math.degrees(rot0) opt_scale = opt_md.get(model.MD_PIXEL_SIZE_COR, (1, 1))[0] shear = sem_md.get(model.MD_SHEAR_COR, 0) scaling_xy = sem_md.get(model.MD_PIXEL_SIZE_COR, (1, 1)) if (not abs(rot_deg) < 10 or # Rotation < 10° not 0.9 < opt_scale < 1.1 or # Optical mag < 10% not abs(shear) < 0.3 or # Shear < 30% any(not 0.9 < v < 1.1 for v in scaling_xy) # SEM ratio diff < 10% ): # Special warning in case of wrong magnification if not 0.9 < opt_scale < 1.1 and model.hasVA( main_data.lens, "magnification"): lens_mag = main_data.lens.magnification.value measured_mag = lens_mag / opt_scale logging.warning( "The measured optical magnification is %fx, instead of expected %fx. " "Check that the lens magnification and the SEM magnification are correctly set.", measured_mag, lens_mag) else: # Generic warning logging.warning( u"The fine alignment values are very large, try on a different place on the sample. " u"mag correction: %f, rotation: %f°, shear: %f, X/Y scale: %f", opt_scale, rot_deg, shear, scaling_xy) self._tab_panel.lbl_fine_align.Label = "Probably incorrect" else: self._tab_panel.lbl_fine_align.Label = "Successful" # Rotation is compensated in software on the FM image, but the user # can also change the SEM scan rotation, and re-run the alignment, # so show it clearly, for the user to take action. # The worse the rotation, the longer it's displayed. timeout = max(2, min(abs(rot_deg), 10)) popup.show_message( self._tab_panel, u"Rotation applied: %s\nShear applied: %s\nX/Y Scaling applied: %s" % (units.readable_str( rot_deg, unit=u"°", sig=3), units.readable_str( shear, sig=3), units.readable_str(scaling_xy, sig=3)), timeout=timeout) logging.info( u"Fine alignment computed mag correction of %f, rotation of %f°, " u"shear needed of %s, and X/Y scaling needed of %s.", opt_scale, rot, shear, scaling_xy) # As the CCD image might have different pixel size, force to fit self._tab_panel.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._tab_panel.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._tab_panel.btn_fine_align.Label = self._fa_btn_label self._resume() self._tab_panel.lbl_fine_align.Show() self._tab_panel.gauge_fine_align.Hide() self._sizer.Layout()
def _on_fa_done(self, future): logging.debug("End of overlay procedure") main_data = self._main_data_model self._acq_future = None # To avoid holding the ref in memory self._faf_connector = None try: # DEBUG try: trans_val, cor_md = future.result() except Exception: cor_md = {}, {} opt_md, sem_md = cor_md # Save the optical correction metadata straight into the CCD main_data.ccd.updateMetadata(opt_md) # The SEM correction metadata goes to the ebeam main_data.ebeam.updateMetadata(sem_md) except CancelledError: self._tab_panel.lbl_fine_align.Label = "Cancelled" except Exception as ex: logging.warning("Failure during overlay: %s", ex) self._tab_panel.lbl_fine_align.Label = "Failed" else: self._main_frame.menu_item_reset_finealign.Enable(True) # Check whether the values make sense. If not, we still accept them, # but hopefully make it clear enough to the user that the calibration # should not be trusted. rot = opt_md.get(model.MD_ROTATION_COR, 0) rot0 = (rot + math.pi) % (2 * math.pi) - math.pi # between -pi and pi rot_deg = math.degrees(rot0) opt_scale = opt_md.get(model.MD_PIXEL_SIZE_COR, (1, 1))[0] shear = sem_md.get(model.MD_SHEAR_COR, 0) scaling_xy = sem_md.get(model.MD_PIXEL_SIZE_COR, (1, 1)) if (not abs(rot_deg) < 10 or # Rotation < 10° not 0.9 < opt_scale < 1.1 or # Optical mag < 10% not abs(shear) < 0.3 or # Shear < 30% any(not 0.9 < v < 1.1 for v in scaling_xy) # SEM ratio diff < 10% ): # Special warning in case of wrong magnification if not 0.9 < opt_scale < 1.1 and model.hasVA(main_data.lens, "magnification"): lens_mag = main_data.lens.magnification.value measured_mag = lens_mag / opt_scale logging.warning("The measured optical magnification is %fx, instead of expected %fx. " "Check that the lens magnification and the SEM magnification are correctly set.", measured_mag, lens_mag) else: # Generic warning logging.warning(u"The fine alignment values are very large, try on a different place on the sample. " u"mag correction: %f, rotation: %f°, shear: %f, X/Y scale: %f", opt_scale, rot_deg, shear, scaling_xy) self._tab_panel.lbl_fine_align.Label = "Probably incorrect" else: self._tab_panel.lbl_fine_align.Label = "Successful" # Rotation is compensated in software on the FM image, but the user # can also change the SEM scan rotation, and re-run the alignment, # so show it clearly, for the user to take action. # The worse the rotation, the longer it's displayed. timeout = max(2, min(abs(rot_deg), 10)) popup.show_message( self._tab_panel, u"Rotation applied: %s\nShear applied: %s\nX/Y Scaling applied: %s" % (units.readable_str(rot_deg, unit=u"°", sig=3), units.readable_str(shear, sig=3), units.readable_str(scaling_xy, sig=3)), timeout=timeout ) logging.info(u"Fine alignment computed mag correction of %f, rotation of %f°, " u"shear needed of %s, and X/Y scaling needed of %s.", opt_scale, rot, shear, scaling_xy) # As the CCD image might have different pixel size, force to fit self._tab_panel.vp_align_ccd.canvas.fit_view_to_next_image = True main_data.is_acquiring.value = False self._tab_panel.btn_fine_align.Bind(wx.EVT_BUTTON, self._on_fine_align) self._tab_panel.btn_fine_align.Label = self._fa_btn_label self._resume() self._tab_panel.lbl_fine_align.Show() self._tab_panel.gauge_fine_align.Hide() self._sizer.Layout()
def _display_pretty(self): if self._number_value is None: str_val = u"" else: str_val = units.readable_str(self._number_value, sig=self.accuracy) wx.TextCtrl.ChangeValue(self, str_val)