def __init__(self, name, role, parent, hfw_nomag, **kwargs): model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self.parent = parent # Distance between borders if magnification = 1. It should be found out # via calibration. self._hfw_nomag = hfw_nomag # m self.magnification = model.FloatContinuous( self.parent.GetMagnification(), unit="", readonly=True, range=MAGNIFICATION_RANGE) fov_range = (self._hfw_nomag / MAGNIFICATION_RANGE[1], self._hfw_nomag / MAGNIFICATION_RANGE[0]) self.horizontalFoV = model.FloatContinuous( self._hfw_nomag / self.magnification.value, range=fov_range, unit="m", setter=self._setHorizontalFoV) self.horizontalFoV.subscribe(self._onHorizontalFoV) self.blanker = model.VAEnumerated(self.parent.GetBlankBeam(), choices={True, False}, setter=self._setBlanker) self.external = model.VAEnumerated(self.parent.GetExternal(), choices={True, False}, setter=self._setExternal) #self.probeCurrent = model.FloatContinuous(1e-6, range=PC_RANGE, unit="A", # setter=self._setProbeCurrent) self.accelVoltage = model.FloatContinuous( 0, range=(VOLTAGE_RANGE[0] * 1e3, VOLTAGE_RANGE[1] * 1e3), unit="V", setter=self._setVoltage) # No pixelSize as there is no shape (not a full scanner) # To provide some rough idea of the step size when changing focus # Depends on the pixelSize, so will be updated whenever the HFW changes self.depthOfField = model.FloatContinuous(1e-6, range=(0, 1e3), unit="m", readonly=True) self._updateDepthOfField() # Refresh regularly the values, from the hardware, starting from now self._updateSettings() self._va_poll = util.RepeatingTimer(5, self._updateSettings, "Settings polling") self._va_poll.start()
def __init__(self, name, data): """ name (string) data (model.DataArray(Shadow) of shape (YX) or list of such DataArray(Shadow)). The metadata MD_POS and MD_AR_POLE should be provided """ if not isinstance(data, collections.Iterable): data = [data] # from now it's just a list of DataArray # TODO: support DAS, as a "delayed loading" by only calling .getData() # when the projection for the particular data needs to be computed (or # .raw needs to be accessed?) # Ensure all the data is a DataArray, as we don't handle (yet) DAS data = [d.getData() if isinstance(d, model.DataArrayShadow) else d for d in data] # find positions of each acquisition # tuple of 2 floats -> DataArray: position on SEM -> data self._sempos = {} for d in data: try: self._sempos[d.metadata[MD_POS]] = img.ensure2DImage(d) except KeyError: logging.info("Skipping DataArray without known position") # Cached conversion of the CCD image to polar representation # TODO: automatically fill it in a background thread self._polar = {} # dict tuple 2 floats -> DataArray # SEM position displayed, (None, None) == no point selected self.point = model.VAEnumerated((None, None), choices=frozenset([(None, None)] + list(self._sempos.keys()))) # The background data (typically, an acquisition without ebeam). # It is subtracted from the acquisition data. # If set to None, a simple baseline background value is subtracted. self.background = model.VigilantAttribute(None, setter=self._setBackground) self.background.subscribe(self._onBackground) if self._sempos: # Pick one point, e.g., top-left bbtl = (min(x for x, y in self._sempos.keys() if x is not None), min(y for x, y in self._sempos.keys() if y is not None)) # top-left point is the closest from the bounding-box top-left def dis_bbtl(v): try: return math.hypot(bbtl[0] - v[0], bbtl[1] - v[1]) except TypeError: return float("inf") # for None, None self.point.value = min(self._sempos.keys(), key=dis_bbtl) # no need for init=True, as Stream.__init__ will update the image self.point.subscribe(self._onPoint) super(StaticARStream, self).__init__(name, list(self._sempos.values()))
def _createAutoExternal(self, original_external_va): """ """ self._external = original_external_va self.external = model.VAEnumerated(None, setter=self._setExternal, choices={ True: 'external', False: 'acquisition', None: 'auto' })
def _createAutoBlanker(self, original_blanker): """ """ self._blanker = original_blanker self.blanker = model.VAEnumerated(None, setter=self._setBlanker, choices={ True: 'blanked', False: 'unblanked', None: 'auto' })
def __init__(self, name): Stream.__init__(self, name, None, None, None) # For imitating a FluoStream self.excitation = model.VAEnumerated( (4.2e-07, 4.3e-07, 4.38e-07, 4.45e-07, 4.55e-07), # multiple spectra choices={(4.2e-07, 4.3e-07, 4.38e-07, 4.45e-07, 4.55e-07), (3.75e-07, 3.9e-07, 4e-07, 4.02e-07, 4.05e-07), (5.65e-07, 5.7e-07, 5.75e-07, 5.8e-07, 5.95e-07), (5.25e-07, 5.4e-07, 5.5e-07, 5.55e-07, 5.6e-07), (4.95e-07, 5.05e-07, 5.13e-07, 5.2e-07, 5.3e-07)}, unit="m") self.emission = model.VAEnumerated( (500e-9, 520e-9), # one (fixed) multi-band choices={(100e-9, 150e-9), (500e-9, 520e-9), (600e-9, 650e-9)}, unit="m") default_tint = conversion.wave2rgb(488e-9) self.tint = model.VigilantAttribute(default_tint, unit="RGB") self.histogram._edges = (0, 0)
def __init__(self, microscope, main_app): super(AutomaticOverlayPlugin, self).__init__(microscope, main_app) self.addMenu("Data correction/Add && Align EM...", self.start) self._dlg = None # Projections of the reference and new data self._rem_proj = None self._nem_proj = None # On-the-fly keypoints and matching keypoints computed self._nem_kp = None self._nem_mkp = None self._rem_kp = None self._rem_mkp = None # im_ref.choices contains the streams and their name self.im_ref = model.VAEnumerated(None, choices={None: ""}) self.blur_ref = model.IntContinuous(2, range=(0, 20), unit="px") self.blur = model.IntContinuous(5, range=(0, 20), unit="px") self.crop_top = model.IntContinuous(0, range=(0, 200), unit="px") self.crop_top.clip_on_range = True self.crop_bottom = model.IntContinuous(0, range=(0, 200), unit="px") self.crop_bottom.clip_on_range = True self.crop_left = model.IntContinuous(0, range=(0, 200), unit="px") self.crop_left.clip_on_range = True self.crop_right = model.IntContinuous(0, range=(0, 200), unit="px") self.crop_right.clip_on_range = True # TODO: inverting the values doesn't seem to really affect the keypoints self.invert = model.BooleanVA(False) # TODO: ideally, the flip shouldn't be needed, but it seems the matchers # in OpenCV are not able to handle "negative" scale self.flip_x = model.BooleanVA(False) self.flip_y = model.BooleanVA(False) self.draw_kp = model.BooleanVA(True) # self.wta = model.IntContinuous(2, range=(2, 4)) # self.scaleFactor = model.FloatContinuous(1.2, range=(1.01, 2)) # self.nlevels = model.IntContinuous(8, range=(4, 48)) # self.patchSize = model.IntContinuous(31, range=(4, 256)) # Any change on the VAs should update the stream self.blur_ref.subscribe(self._on_ref_stream) self.blur.subscribe(self._on_new_stream) self.crop_top.subscribe(self._on_new_stream) self.crop_bottom.subscribe(self._on_new_stream) self.crop_left.subscribe(self._on_new_stream) self.crop_right.subscribe(self._on_new_stream) self.invert.subscribe(self._on_new_stream) self.flip_x.subscribe(self._on_new_stream) self.flip_y.subscribe(self._on_new_stream) self.draw_kp.subscribe(self._on_draw_kp)
def __init__(self, name, detector, dataflow, emitter, l2=None, analyzer=None, **kwargs): """ See SpectrumSettingsStream for the standard options l2 (None or Actuator with "x" axis): to move the lens 2 (aka "lens-switch") analyzer (None or Actuator with "pol" axis): the polarization analyzer. It should have at least the 7 "standard" positions """ super(LASpectrumSettingsStream, self).__init__(name, detector, dataflow, emitter, **kwargs) self.l2 = l2 if l2: # Convert the boolean to the actual position. # The actuator is expected to have two positions, named "on" and "off" self._toLens2Pos = None for pos, pos_name in l2.axes["x"].choices.items(): if pos_name == "on": self._toLens2Pos = pos if self._toLens2Pos is None: raise ValueError( "Lens 2 actuator should have an 'on' position, but only %s" % (list(l2.axes["x"].choices.values()), )) # Polarization stored on the stream. # We don't use the standard "global" axes trick, so that it's possible # to have multiple streams, each with a different polarization. self.analyzer = analyzer if analyzer: # Hardcode the 6 pol pos + pass-through positions = set(POL_POSITIONS) | {MD_POL_NONE} # check positions specified in the microscope file are correct for pos in positions: if pos not in analyzer.axes["pol"].choices: raise ValueError( "Polarization analyzer %s misses position '%s'" % (analyzer, pos)) self.polarization = model.VAEnumerated(MD_POL_NONE, choices=positions) # Not used, but the MDStream expects it as well. self.acquireAllPol = model.BooleanVA(False)
def test_enumerated(self): prop = model.StringEnumerated("a", set(["a", "c", "bfds"])) self.assertEqual(prop.value, "a") self.assertEqual(prop.choices, set(["a", "c", "bfds"])) self.called = 0 prop.subscribe(self.callback_test_notify) # now count prop.value = "c" # +1 assert (prop.value == "c") try: prop.value = "wfds" self.fail("Assigning out of bound should not be allowed.") except IndexError: pass # as it should be prop.choices = set(["a", "c", "b", 5]) assert (prop.value == "c") try: prop.choices = set(["a", "b"]) self.fail( "Assigning choices not containing current value should not be allowed." ) except IndexError: pass # as it should be try: prop.value = 5 self.fail("Assigning an int to a string should not be allowed.") except TypeError: pass # as it should be try: prop.choices = 5 self.fail("Choices should be allowed only if it's a set.") except TypeError: pass # as it should be prop.unsubscribe(self.callback_test_notify) self.assertTrue(self.called == 1) # It's also allowed to use dict as choices prop = model.VAEnumerated((1, 2), {(1, 2): "aaa", (3, 5): "doo"}) for v in prop.choices: prop.value = v # they all should work
def test_points_select_overlay(self): # Create stuff cnvs = miccanvas.DblMicroscopeCanvas(self.panel) self.add_control(cnvs, wx.EXPAND, proportion=1, clear=True) tab_mod = self.create_simple_tab_model() view = tab_mod.focussedView.value view.mpp.value = 1e-5 cnvs.setView(view, tab_mod) # Manually add the overlay pol = wol.PointsOverlay(cnvs) cnvs.add_world_overlay(pol) cnvs.current_mode = guimodel.TOOL_POINT pol.activate() test.gui_loop() from itertools import product phys_points = product(xrange(-200, 201, 50), xrange(-200, 201, 50)) phys_points = [(a / 1.0e5, b / 1.0e5) for a, b in phys_points] point = model.VAEnumerated(phys_points[0], choices=frozenset(phys_points)) pol.set_point(point) test.gui_loop() cnvs.update_drawing() test.gui_loop(0.5) point.value = (50 / 1.0e5, 50 / 1.0e5) test.gui_loop(0.5)
def __init__(self, name, data, *args, **kwargs): """ name (string) data (model.DataArray(Shadow) of shape (YX) or list of such DataArray(Shadow)). The metadata MD_POS, MD_AR_POLE and MD_POL_MODE should be provided """ if not isinstance(data, collections.Iterable): data = [data] # from now it's just a list of DataArray # TODO: support DAS, as a "delayed loading" by only calling .getData() # when the projection for the particular data needs to be computed (or # .raw needs to be accessed?) # Ensure all the data is a DataArray, as we don't handle (yet) DAS data = [ d.getData() if isinstance(d, model.DataArrayShadow) else d for d in data ] # find positions of each acquisition # (float, float, str or None)) -> DataArray: position on SEM + polarization -> data self._pos = {} sempositions = set() polpositions = set() for d in data: try: sempos_cur = d.metadata[MD_POS] # When reading data: floating point error (slightly different keys for same ebeam pos) # -> check if there is already a position specified, which is very close by # (and therefore the same ebeam pos) and replace with that ebeam position # (e.g. all polarization positions for the same ebeam positions will have exactly the same ebeam pos) for sempos in sempositions: if almost_equal(sempos_cur[0], sempos[0]) and almost_equal( sempos_cur[1], sempos[1]): sempos_cur = sempos break self._pos[sempos_cur + (d.metadata.get(MD_POL_MODE, None), )] = img.ensure2DImage(d) sempositions.add(sempos_cur) if MD_POL_MODE in d.metadata: polpositions.add(d.metadata.get(MD_POL_MODE)) except KeyError: logging.info("Skipping DataArray without known position") # Cached conversion of the CCD image to polar representation # TODO: automatically fill it in a background thread self._polar = {} # dict tuple (float, float, str or None) -> DataArray # SEM position VA # SEM position displayed, (None, None) == no point selected (x, y) self.point = model.VAEnumerated( (None, None), choices=frozenset([(None, None)] + list(sempositions))) if self._pos: # Pick one point, e.g., top-left bbtl = (min(x for x, y in sempositions if x is not None), min(y for x, y in sempositions if y is not None)) # top-left point is the closest from the bounding-box top-left def dis_bbtl(v): try: return math.hypot(bbtl[0] - v[0], bbtl[1] - v[1]) except TypeError: return float("inf") # for None, None self.point.value = min(sempositions, key=dis_bbtl) # no need for init=True, as Stream.__init__ will update the image self.point.subscribe(self._onPoint) # polarization VA # check if any polarization analyzer data, set([]) == no analyzer data (pol) if polpositions: # use first entry in acquisition to populate VA (acq could have 1 or 6 pol pos) self.polarization = model.VAEnumerated(list(polpositions)[0], choices=polpositions) self.polarization.subscribe(self._onPolarization) if "acq_type" not in kwargs: kwargs["acq_type"] = model.MD_AT_AR super(StaticARStream, self).__init__(name, list(self._pos.values()), *args, **kwargs)
def __init__(self, name, role, parent, image, spectrograph=None, daemon=None, **kwargs): """ Initializes a fake readout camera. :parameter name: (str) as in Odemis :parameter role: (str) as in Odemis :parameter parent: class streakcamera :parameter image: fake input image """ # TODO image focus and operate mode # get the fake images try: image_filename = str(image) # ensure relative path is from this file if not os.path.isabs(image): image_filename = os.path.join(os.path.dirname(__file__), image) converter = dataio.find_fittest_converter(image_filename, mode=os.O_RDONLY) self._img_list = [] img_list = converter.read_data(image_filename) for img in img_list: if img.ndim > 3: # remove dims of length 1 img = numpy.squeeze(img) self._img_list.append(img) # can be RGB or greyscale except Exception: raise ValueError("Fake image does not fit requirements for temporal spectrum acquisition.") super(ReadoutCamera, self).__init__(name, role, parent=parent, daemon=daemon, **kwargs) # init HwComponent self.parent = parent self._metadata[model.MD_HW_VERSION] = 'Simulated readout camera OrcaFlash 4.0 V3, ' \ 'Product number: C13440-20C, Serial number: 301730' self._metadata[model.MD_SW_VERSION] = 'Firmware: 4.20.B, Version: 4.20.B03-A19-B02-4.02' self._metadata[model.MD_DET_TYPE] = model.MD_DT_INTEGRATING # sensor size (resolution) # x (lambda): horizontal, y (time): vertical full_res = (self._img_list[0].shape[1], self._img_list[0].shape[0]) self._metadata[model.MD_SENSOR_SIZE] = full_res # 16-bit depth = 2 ** (self._img_list[0].dtype.itemsize * 8) self._shape = full_res + (depth,) # variable needed to update resolution VA and wavelength list correctly (_updateWavelengthList()) self._binning = (2, 2) # need to be before binning, as it is modified when changing binning resolution = (int(full_res[0]/self._binning[0]), int(full_res[1]/self._binning[1])) self.resolution = model.ResolutionVA(resolution, ((1, 1), full_res), setter=self._setResolution) # variable needed to update wavelength list correctly (_updateWavelengthList()) self._resolution = self.resolution.value choices_bin = {(1, 1), (2, 2), (4, 4)} self.binning = model.VAEnumerated(self._binning, choices_bin, setter=self._setBinning) self._metadata[model.MD_BINNING] = self.binning.value # physical pixel size is 6.5um x 6.5um sensor_pixelsize = (6.5e-06, 6.5e-06) self._metadata[model.MD_SENSOR_PIXEL_SIZE] = sensor_pixelsize # pixelsize VA is the sensor size, it does not include binning or magnification self.pixelSize = model.VigilantAttribute(sensor_pixelsize, unit="m", readonly=True) range_exp = [0.00001, 1] # 10us to 1s self._exp_time = 0.1 # 100 msec self.exposureTime = model.FloatContinuous(self._exp_time, range_exp, unit="s", setter=self._setCamExpTime) self._metadata[model.MD_EXP_TIME] = self.exposureTime.value self.readoutRate = model.VigilantAttribute(425000000, unit="Hz", readonly=True) # MHz self._metadata[model.MD_READOUT_TIME] = 1 / self.readoutRate.value # s # spectrograph VAs after readout camera VAs self._spectrograph = spectrograph if self._spectrograph: logging.debug("Starting streak camera with spectrograph.") self._spectrograph.position.subscribe(self._updateWavelengthList, init=True) else: logging.warning("No spectrograph specified. No wavelength metadata will be attached.") # for synchronized acquisition self._sync_event = None self.softwareTrigger = model.Event() # Simple implementation of the flow: we keep generating images and if # there are subscribers, they'll receive it. self.data = SimpleStreakCameraDataFlow(self._start, self._stop, self._sync) self._generator = None self._img_counter = 0 # initialize the image counter
def __init__(self, name, detector, sed, emitter, spectrograph, lens_switch, bigslit, opm, wl_inverted=False): """ name (string): user-friendly name of this stream detector (Detector): the 2D CCD which get wavelength on the X axis and angles on the Y axis sed (Detector): the se-detector emitter (Emitter): the emitter (eg: ebeam scanner) spectrograph (Actuator): the spectrograph wl_inverted (bool): if True, will swap the wavelength axis of the CCD, in order to support hardware where the highest wavelengths are at the smallest indices. (The MD_WL_LIST is *not* inverted) """ self.name = model.StringVA(name) # Hardware Components self._detector = detector self._sed = sed self._emitter = emitter self._sgr = spectrograph self._opm = opm self._lsw = lens_switch self._bigslit = bigslit self._wl_inverted = wl_inverted wlr = spectrograph.axes["wavelength"].range slitw = spectrograph.axes["slit-in"].range self.centerWavelength = model.FloatContinuous(500e-9, wlr, unit="m") self.slitWidth = model.FloatContinuous(100e-6, slitw, unit="m") # dwell time and exposure time are the same thing in this case self.dwellTime = model.FloatContinuous( 1, range=detector.exposureTime.range, unit="s") self.emtTranslation = model.TupleContinuous( (0, 0), range=self._emitter.translation.range, cls=(int, long, float), unit="px") # Distance between the center of each pixel self.stepsize = model.FloatContinuous(1e-6, (1e-9, 1e-4), unit="m") # Region of acquisition. ROI form is LEFT Top RIGHT Bottom, relative to full field size self.roi = model.TupleContinuous((0, 0, 1, 1), range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) # For drift correction self.dcRegion = model.TupleContinuous(UNDEFINED_ROI, range=((0, 0, 0, 0), (1, 1, 1, 1)), cls=(int, long, float)) self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0], range=emitter.dwellTime.range, unit="s") #self.binning = model.VAEnumerated((1,1), choices=set([(1,1), (2,2), (2,3)])) # separate binning values because it can useful for experiment self.binninghorz = model.VAEnumerated(1, choices={1, 2, 4, 8, 16}) self.binningvert = model.VAEnumerated(1, choices={1, 2, 4, 8, 16}) self.nDC = model.IntContinuous(1, (1, 20)) # For acquisition self.ARspectral_data = None self.ARspectral_data_received = threading.Event() self.sem_data = [] self.sem_data_received = threading.Event() self._hw_settings = None
def __init__(self, name, image): """ name (string) image (model.DataArray(Shadow) of shape (CYX) or (C11YX)). The metadata MD_WL_POLYNOMIAL or MD_WL_LIST should be included in order to associate the C to a wavelength. """ # Spectrum stream has in addition to normal stream: # * information about the current bandwidth displayed (avg. spectrum) # * coordinates of 1st point (1-point, line) # * coordinates of 2nd point (line) # TODO: need to handle DAS properly, in case it's tiled (in XY), to avoid # loading too much data in memory. # Ensure the data is a DataArray, as we don't handle (yet) DAS if isinstance(image, model.DataArrayShadow): image = image.getData() if len(image.shape) == 3: # force 5D image = image[:, numpy.newaxis, numpy.newaxis, :, :] elif len(image.shape) != 5 or image.shape[1:3] != (1, 1): logging.error("Cannot handle data of shape %s", image.shape) raise NotImplementedError("SpectrumStream needs a cube data") # This is for "average spectrum" projection try: # cached list of wavelength for each pixel pos self._wl_px_values = spectrum.get_wavelength_per_pixel(image) except (ValueError, KeyError): # useless polynomial => just show pixels values (ex: -50 -> +50 px) # TODO: try to make them always int? max_bw = image.shape[0] // 2 min_bw = (max_bw - image.shape[0]) + 1 self._wl_px_values = range(min_bw, max_bw + 1) assert(len(self._wl_px_values) == image.shape[0]) unit_bw = "px" cwl = (max_bw + min_bw) // 2 width = image.shape[0] // 12 else: min_bw, max_bw = self._wl_px_values[0], self._wl_px_values[-1] unit_bw = "m" cwl = (max_bw + min_bw) / 2 width = (max_bw - min_bw) / 12 # TODO: allow to pass the calibration data as argument to avoid # recomputing the data just after init? # Spectrum efficiency compensation data: None or a DataArray (cf acq.calibration) self.efficiencyCompensation = model.VigilantAttribute(None, setter=self._setEffComp) # The background data (typically, an acquisition without e-beam). # It is subtracted from the acquisition data. # If set to None, a simple baseline background value is subtracted. self.background = model.VigilantAttribute(None, setter=self._setBackground) # low/high values of the spectrum displayed self.spectrumBandwidth = model.TupleContinuous( (cwl - width, cwl + width), range=((min_bw, min_bw), (max_bw, max_bw)), unit=unit_bw, cls=(int, long, float)) # Whether the (per bandwidth) display should be split intro 3 sub-bands # which are applied to RGB self.fitToRGB = model.BooleanVA(False) # This attribute is used to keep track of any selected pixel within the # data for the display of a spectrum self.selected_pixel = model.TupleVA((None, None)) # int, int # first point, second point in pixels. It must be 2 elements long. self.selected_line = model.ListVA([(None, None), (None, None)], setter=self._setLine) # Peak method index, None if spectrum peak fitting curve is not displayed self.peak_method = model.VAEnumerated("gaussian", {"gaussian", "lorentzian", None}) # The thickness of a point or a line (shared). # A point of width W leads to the average value between all the pixels # which are within W/2 from the center of the point. # A line of width W leads to a 1D spectrum taking into account all the # pixels which fit on an orthogonal line to the selected line at a # distance <= W/2. self.selectionWidth = model.IntContinuous(1, [1, 50], unit="px") self.fitToRGB.subscribe(self.onFitToRGB) self.spectrumBandwidth.subscribe(self.onSpectrumBandwidth) self.efficiencyCompensation.subscribe(self._onCalib) self.background.subscribe(self._onCalib) self.selectionWidth.subscribe(self._onSelectionWidth) self._calibrated = image # the raw data after calibration super(StaticSpectrumStream, self).__init__(name, [image]) # Automatically select point/line if data is small (can only be done # after .raw is set) if image.shape[-2:] == (1, 1): # Only one point => select it immediately self.selected_pixel.value = (0, 0) elif image.shape[-2] == 1: # Horizontal line => select line immediately self.selected_line.value = [(0, 0), (image.shape[-1] - 1, 0)] elif image.shape[-1] == 1: # Vertical line => select line immediately self.selected_line.value = [(0, 0), (0, image.shape[-2] - 1)]
def __init__(self, microscope): """ microscope (model.Microscope or None): the root of the HwComponent tree provided by the back-end. If None, it means the interface is not connected to a microscope (and displays a recorded acquisition). """ self.microscope = microscope self.role = None # These are either HwComponents or None (if not available) self.ccd = None self.stage = None self.focus = None # actuator to change the camera focus self.aligner = None # actuator to align ebeam/ccd self.mirror = None # actuator to change the mirror position (on SPARC) self.light = None self.light_filter = None # emission light filter for SECOM/output filter for SPARC self.lens = None self.ebeam = None self.ebeam_focus = None # change the e-beam focus self.sed = None # secondary electron detector self.bsd = None # back-scatter electron detector self.spectrometer = None # spectrometer self.spectrograph = None # actuator to change the wavelength self.ar_spec_sel = None # actuator to select AR/Spectrometer (SPARC) self.lens_switch = None # actuator to (de)activate the lens (SPARC) self.chamber = None # actuator to control the chamber (pressure) self.ccd_chamber = None # view of inside the chamber self.ccd_overview = None # global view from above the sample # Indicates whether the microscope is acquiring a high quality image self.is_acquiring = model.BooleanVA(False) if microscope: self.role = microscope.role for d in microscope.detectors: if d.role == "ccd": self.ccd = d elif d.role == "se-detector": self.sed = d elif d.role == "bs-detector": self.bsd = d elif d.role == "spectrometer": self.spectrometer = d elif d.role == "ccd-chamber": self.ccd_chamber = d elif d.role == "ccd-overview": self.ccd_overview = d for a in microscope.actuators: if a.role == "stage": self.stage = a # most views move this actuator when moving elif a.role == "focus": self.focus = a elif a.role == "ebeam-focus": self.ebeam_focus = a elif a.role == "mirror": self.mirror = a elif a.role == "align": self.aligner = a elif a.role == "lens-switch": self.lens_switch = a elif a.role == "ar-spec-selector": self.ar_spec_sel = a elif a.role == "chamber": self.chamber = a # Spectrograph is not directly an actuator, but a sub-comp of spectrometer if self.spectrometer: for child in self.spectrometer.children: if child.role == "spectrograph": self.spectrograph = child for e in microscope.emitters: if e.role == "light": self.light = e self._light_power_on = None # None = unknown elif e.role == "filter": self.light_filter = e elif e.role == "lens": self.lens = e elif e.role == "e-beam": self.ebeam = e # Do some typical checks on expectations from an actual microscope if not any((self.ccd, self.sed, self.bsd, self.spectrometer)): raise KeyError("No detector found in the microscope") if not self.light and not self.ebeam: raise KeyError("No emitter found in the microscope") # TODO: all that on/off thing is crazy: # * we cannot do it (for now) # * we'd better turn on/off the hardware when streams need it # * pause and off are the same things but for SEM (blank/off) # * optical on in live view means light on, while in lens align it means light off # => we'd be better with just one global pause button (and pressure) # Handle turning on/off the instruments hw_states = set([STATE_OFF, STATE_ON, STATE_PAUSE]) if self.ccd: # not so nice to hard code it here, but that should do it for now... if self.role == "sparc": self.arState = model.IntEnumerated(STATE_OFF, choices=hw_states) self.arState.subscribe(self.onARState) else: self.opticalState = model.IntEnumerated(STATE_OFF, choices=hw_states) self.opticalState.subscribe(self.onOpticalState) if self.ebeam: self.emState = model.IntEnumerated(STATE_OFF, choices=hw_states) self.emState.subscribe(self.onEMState) if self.spectrometer: self.specState = model.IntEnumerated(STATE_OFF, choices=hw_states) self.specState.subscribe(self.onSpecState) # Used when doing fine alignment, based on the value used by the user # when doing manual alignment. 0.1s is not too bad value if the user # hasn't specified anything (yet). self.fineAlignDwellTime = model.FloatContinuous(0.1, range=[1e-9, 100], unit="s") # TODO: should we put also the configuration related stuff? # Like path/file format # Set to True to request debug info to be displayed self.debug = model.BooleanVA(False) # Current tab (+ all available tabs in choices as a dict tab -> name) # Fully set and managed later by the TabBarController. # Not very beautiful because Tab is not part of the model. # MicroscopyGUIData would be better in theory, but is less convenient # do directly access additional GUI information. self.tab = model.VAEnumerated(None, choices={None: ""})
def __init__(self, name, detector, dataflow, emitter, em_filter, **kwargs): """ name (string): user-friendly name of this stream detector (Detector): the detector which has the dataflow dataflow (Dataflow): the dataflow from which to get the data emitter (Light): the HwComponent to modify the light excitation em_filter (Filter): the HwComponent to modify the emission light filtering """ super(FluoStream, self).__init__(name, detector, dataflow, emitter, **kwargs) self._em_filter = em_filter # Emission and excitation are based on the hardware capacities. # For excitation, compared to the hardware, only one band at a time can # be selected. The difficulty comes to pick the default value. The best # would be to use the current hardware value, but if the light is off # there is no default value. In that case, we pick the emission value # and try to pick a compatible excitation value: the first excitation # wavelength below the emission. However, the emission value might also # be difficult to know if there is a multi-band filter. In that case we # just pick the lowest value. # TODO: once the streams have their own version of the hardware settings # and in particular light.power, it should be possible to turn off the # light just by stopping the power, and so leaving the emissions as is. em_choices = em_filter.axes["band"].choices.copy() # convert any list into tuple, as lists cannot be put in a set for k, v in em_choices.items(): em_choices[k] = conversion.ensure_tuple(v) # invert the dict, to directly convert the emission to the position value self._emission_to_idx = {v: k for k, v in em_choices.items()} cur_pos = em_filter.position.value["band"] current_em = em_choices[cur_pos] if isinstance(current_em[0], collections.Iterable): # if multiband => pick the first one em_band = current_em[0] else: em_band = current_em center_em = fluo.get_center(em_band) exc_choices = set(emitter.spectra.value) current_exc = self._get_current_excitation() if current_exc is None: # pick the closest below the current emission current_exc = min(exc_choices, key=lambda b: b[2]) # default to the smallest for b in exc_choices: # Works because exc_choices only contains 5-float tuples if (b[2] < center_em and center_em - b[2] < center_em - current_exc[2]): current_exc = b logging.debug("Guessed excitation is %s, based on emission %s", current_exc, current_em) self.excitation = model.VAEnumerated(current_exc, choices=exc_choices, unit="m") self.excitation.subscribe(self.onExcitation) # The wavelength band on the out path (set when emission changes) self.emission = model.VAEnumerated(current_em, choices=set(em_choices.values()), unit="m") self.emission.subscribe(self.onEmission) # colouration of the image self.tint.value = conversion.wave2rgb(center_em)
def __init__(self, microscope): """ :param microscope: (model.Microscope or None): the root of the HwComponent tree provided by the back-end. If None, it means the interface is not connected to a microscope (and displays a recorded acquisition). """ self.microscope = microscope self.role = None # The following attributes are either HwComponents or None (if not available) self.ccd = None self.stage = None self.focus = None # actuator to change the camera focus self.aligner = None # actuator to align ebeam/ccd self.mirror = None # actuator to change the mirror position (on SPARC) self.light = None self.light_filter = None # emission light filter for SECOM/output filter for SPARC self.lens = None self.ebeam = None self.ebeam_focus = None # change the e-beam focus self.sed = None # secondary electron detector self.bsd = None # backscattered electron detector self.spectrometer = None # spectrometer self.spectrograph = None # actuator to change the wavelength self.ar_spec_sel = None # actuator to select AR/Spectrometer (SPARC) self.lens_switch = None # actuator to (de)activate the lens (SPARC) self.chamber = None # actuator to control the chamber (has vacuum, pumping etc.) self.chamber_ccd = None # view of inside the chamber self.chamber_light = None # Light illuminating the chamber self.overview_ccd = None # global view from above the sample self.overview_focus = None # focus of the overview CCD self.overview_light = None # light of the overview CCD # Indicates whether the microscope is acquiring a high quality image self.is_acquiring = model.BooleanVA(False) # The microscope object will be probed for common detectors, actuators, emitters etc. if microscope: self.role = microscope.role for c in microscope.children.value: if c.role == "ccd": self.ccd = c elif c.role == "se-detector": self.sed = c elif c.role == "bs-detector": self.bsd = c elif c.role == "spectrometer": self.spectrometer = c elif c.role == "chamber-ccd": self.chamber_ccd = c elif c.role == "overview-ccd": self.overview_ccd = c elif c.role == "stage": self.stage = c # most views move this actuator when moving elif c.role == "focus": self.focus = c elif c.role == "ebeam-focus": self.ebeam_focus = c elif c.role == "overview-focus": self.overview_focus = c elif c.role == "mirror": self.mirror = c elif c.role == "align": self.aligner = c elif c.role == "lens-switch": self.lens_switch = c elif c.role == "ar-spec-selector": self.ar_spec_sel = c elif c.role == "chamber": self.chamber = c elif c.role == "light": self.light = c elif c.role == "filter": self.light_filter = c elif c.role == "lens": self.lens = c elif c.role == "e-beam": self.ebeam = c elif c.role == "chamber-light": self.chamber_light = c elif c.role == "overview-light": self.overview_light = c # Spectrograph is not directly an actuator, but a sub-comp of spectrometer if self.spectrometer: for child in self.spectrometer.children.value: if child.role == "spectrograph": self.spectrograph = child # Check that the components that can be expected to be present on an actual microscope # have been correctly detected. if not any((self.ccd, self.sed, self.bsd, self.spectrometer)): raise KeyError("No detector found in the microscope") if not self.light and not self.ebeam: raise KeyError("No emitter found in the microscope") # Chamber is complex so we provide a "simplified state" # It's managed by the ChamberController. Setting to PUMPING or VENTING # state will request a pressure change. chamber_states = { CHAMBER_UNKNOWN, CHAMBER_VENTED, CHAMBER_PUMPING, CHAMBER_VACUUM, CHAMBER_VENTING } self.chamberState = model.IntEnumerated(CHAMBER_UNKNOWN, chamber_states) # Used when doing fine alignment, based on the value used by the user # when doing manual alignment. 0.1s is not too bad value if the user # hasn't specified anything (yet). self.fineAlignDwellTime = FloatContinuous(0.1, range=[1e-9, 100], unit="s") # TODO: should we put also the configuration related stuff? # Like path/file format # Set to True to request debug info to be displayed self.debug = model.BooleanVA(False) # Current tab (+ all available tabs in choices as a dict tab -> name) # Fully set and managed later by the TabBarController. # Not very beautiful because Tab is not part of the model. # MicroscopyGUIData would be better in theory, but is less convenient # do directly access additional GUI information. self.tab = model.VAEnumerated(None, choices={None: ""})
def __init__(self, name, role, parent, aperture=100e-6, wd=10e-3, **kwargs): """ aperture (0 < float): aperture diameter of the electron lens wd (0 < float): working distance """ # It will set up ._shape and .parent model.Emitter.__init__(self, name, role, parent=parent, **kwargs) self._aperture = aperture self._working_distance = wd fake_img = self.parent.fake_img if parent._drift_period: # half the size, to keep some margin for the drift self._shape = tuple(v // 2 for v in fake_img.shape[::-1]) else: self._shape = fake_img.shape[::-1] # next two values are just to determine the pixel size # Distance between borders if magnification = 1. It should be found out # via calibration. We assume that image is square, i.e., VFV = HFV self._hfw_nomag = 0.25 # m # pixelSize is the same as MD_PIXEL_SIZE, with scale == 1 # == smallest size/ between two different ebeam positions pxs = fake_img.metadata[model.MD_PIXEL_SIZE] self.pixelSize = model.VigilantAttribute(pxs, unit="m", readonly=True) # the horizontalFoV VA indicates that it's possible to control the zoom hfv = pxs[0] * self._shape[0] self.horizontalFoV = model.FloatContinuous(hfv, range=[10e-9, 10e-3], unit="m") self.magnification = model.VigilantAttribute(self._hfw_nomag / hfv, unit="", readonly=True) self.horizontalFoV.subscribe(self._onHFV) # To provide some rough idea of the step size when changing focus # Depends on the pixelSize, so will be updated whenever the HFW changes self.depthOfField = model.FloatContinuous(1e-6, range=(0, 1e9), unit="m", readonly=True) self._updateDepthOfField() # needs .pixelSize # (.resolution), .translation, .rotation, and .scaling are used to # define the conversion from coordinates to a region of interest. # (float, float) in m => physically moves the e-beam. shift_rng = ((-50e-06, -50e-06), (50e-06, 50e-06)) self.shift = model.TupleContinuous((0, 0), shift_rng, cls=(int, long, float), unit="m") # (float, float) in m => moves center of acquisition by this amount # independent of scale and rotation. tran_rng = [(-self._shape[0] / 2, -self._shape[1] / 2), (self._shape[0] / 2, self._shape[1] / 2)] self.translation = model.TupleContinuous((0, 0), tran_rng, cls=(int, long, float), unit="px", setter=self._setTranslation) # .resolution is the number of pixels actually scanned. If it's less than # the whole possible area, it's centered. resolution = (self._shape[0] // 4, self._shape[1] // 4) self.resolution = model.ResolutionVA(resolution, [(1, 1), self._shape], setter=self._setResolution) self._resolution = resolution # (float, float) as a ratio => how big is a pixel, compared to pixelSize # it basically works the same as binning, but can be float # (Default to scan the whole area) self._scale = (self._shape[0] / resolution[0], self._shape[1] / resolution[1]) self.scale = model.TupleContinuous(self._scale, [(1, 1), self._shape], cls=(int, long, float), unit="", setter=self._setScale) self.scale.subscribe(self._onScale, init=True) # to update metadata # (float) in rad => rotation of the image compared to the original axes self.rotation = model.FloatContinuous(0, [0, 2 * math.pi], unit="rad") self.dwellTime = model.FloatContinuous(1e-06, (1e-06, 1000), unit="s") # VAs to control the ebeam, purely fake self.probeCurrent = model.FloatEnumerated(1.3e-9, {0.1e-9, 1.3e-9, 2.6e-9, 3.4e-9, 11.564e-9, 23e-9}, unit="A") self.accelVoltage = model.FloatContinuous(10e3, (1e3, 30e3), unit="V") # Pretend it's ready to acquire an image self.power = model.BooleanVA(True) # Blanker has a None = "auto" mode which automatically blanks when not scanning self.blanker = model.VAEnumerated(None, choices={True: 'blanked', False: 'unblanked', None: 'auto'})
def __init__(self, name, data, *args, **kwargs): """ :param name: (string) :param data: (model.DataArray(Shadow) of shape (YX) or list of such DataArray(Shadow)). The metadata MD_POS, MD_AR_POLE and MD_POL_MODE should be provided """ if not isinstance(data, collections.Iterable): data = [data] # from now it's just a list of DataArray # TODO: support DAS, as a "delayed loading" by only calling .getData() # when the projection for the particular data needs to be computed (or # .raw needs to be accessed?) # Ensure all the data is a DataArray, as we don't handle (yet) DAS data = [d.getData() if isinstance(d, model.DataArrayShadow) else d for d in data] # find positions of each acquisition # (float, float, str or None)) -> DataArray: position on SEM + polarization -> data self._pos = {} sempositions = set() polpositions = set() for d in data: try: sempos_cur = d.metadata[MD_POS] # When reading data: floating point error (slightly different keys for same ebeam pos) # -> check if there is already a position specified, which is very close by # (and therefore the same ebeam pos) and replace with that ebeam position # (e.g. all polarization positions for the same ebeam positions will have exactly the same ebeam pos) for sempos in sempositions: if almost_equal(sempos_cur[0], sempos[0]) and almost_equal(sempos_cur[1], sempos[1]): sempos_cur = sempos break self._pos[sempos_cur + (d.metadata.get(MD_POL_MODE, None),)] = img.ensure2DImage(d) sempositions.add(sempos_cur) if MD_POL_MODE in d.metadata: polpositions.add(d.metadata[MD_POL_MODE]) except KeyError: logging.info("Skipping DataArray without known position") # SEM position VA # SEM position displayed, (None, None) == no point selected (x, y) self.point = model.VAEnumerated((None, None), choices=frozenset([(None, None)] + list(sempositions))) if self._pos: # Pick one point, e.g., top-left bbtl = (min(x for x, y in sempositions if x is not None), min(y for x, y in sempositions if y is not None)) # top-left point is the closest from the bounding-box top-left def dis_bbtl(v): try: return math.hypot(bbtl[0] - v[0], bbtl[1] - v[1]) except TypeError: return float("inf") # for None, None self.point.value = min(sempositions, key=dis_bbtl) # check if any polarization analyzer data, (None) == no analyzer data (pol) if polpositions: # Check that for every position, all the polarizations are available, # as the GUI expects all the combinations possible, and weird errors # will happen when one is missing. for pos in sempositions: for pol in polpositions: if pos + (pol,) not in self._pos: logging.warning("Polarization data is not complete: missing %s,%s/%s", pos[0], pos[1], pol) # use first entry in acquisition to populate VA (acq could have 1 or 6 pol pos) current_pol = util.sorted_according_to(polpositions, POL_POSITIONS)[0] self.polarization = model.VAEnumerated(current_pol, choices=polpositions) # Add a polarimetry VA containing the polarimetry image results. # Note: Polarimetry analysis are only possible if all 6 images per ebeam pos exist. # Also check if arpolarimetry package can be imported as might not be installed. if polpositions >= set(POL_POSITIONS) and arpolarimetry: self.polarimetry = model.VAEnumerated(MD_POL_S0, choices=set(POL_POSITIONS_RESULTS)) if "acq_type" not in kwargs: kwargs["acq_type"] = model.MD_AT_AR super(StaticARStream, self).__init__(name, list(self._pos.values()), *args, **kwargs)
def test_enumerated(self): prop = model.StringEnumerated("a", {"a", "c", "bfds"}) self.assertEqual(prop.value, "a") self.assertEqual(prop.choices, {"a", "c", "bfds"}) self.called = 0 prop.subscribe(self.callback_test_notify) # now count prop.value = "c" # +1 assert (prop.value == "c") try: prop.value = "wfds" self.fail("Assigning out of bound should not be allowed.") except IndexError: pass # as it should be prop.choices = {"a", "c", "b", 5} assert (prop.value == "c") try: prop.choices = {"a", "b"} self.fail( "Assigning choices not containing current value should not be allowed." ) except IndexError: pass # as it should be try: prop.value = 5 self.fail("Assigning an int to a string should not be allowed.") except TypeError: pass # as it should be try: prop.choices = 5 self.fail("Choices should be allowed only if it's a set.") except TypeError: pass # as it should be prop.unsubscribe(self.callback_test_notify) self.assertTrue(self.called == 1) # It's also allowed to use dict as choices prop = model.VAEnumerated((1, 2), {(1, 2): "aaa", (3, 5): "doo"}) for v in prop.choices: prop.value = v # they all should work # It's also allowed to use tuples as choices, which contain no numbers prop = model.VAEnumerated((1, 1), {(1, 1), (4, 4), (5, "aaa")}) prop.value = prop.clip((3, 3)) # should find the closest value self.assertEqual(prop.value, (4, 4)) for v in prop.choices: prop.value = v # they all should work prop = model.VAEnumerated((1, "aaa"), {(1, "aaa"), (3, "bbb"), (4, "ccc")}) prev_value = prop.value prop.value = prop.clip((3, 3)) # should not find a closest value, but return old value self.assertEqual(prop.value, prev_value) for v in prop.choices: prop.value = v # they all should work prop = model.VAEnumerated((1, "aaa"), {(1, "aaa"), (3, "bbb"), (4, "ccc")}) prev_value = prop.value prop.value = prop.clip((5, "ddd")) # should not find a closest value as not all values in tuple are numbers, but return old value self.assertEqual(prop.value, prev_value) for v in prop.choices: prop.value = v # they all should work
def __init__(self, name, image, *args, **kwargs): """ name (string) image (model.DataArray(Shadow) of shape (CYX), (C11YX), (CTYX), (CT1YX), (1T1YX)). The metadata MD_WL_POLYNOMIAL or MD_WL_LIST should be included in order to associate the C to a wavelength. The metadata MD_TIME_LIST should be included to associate the T to a timestamp .background is a DataArray of shape (CT111), where C & T have the same length as in the data. .efficiencyCompensation is always DataArray of shape C1111. """ # Spectrum stream has in addition to normal stream: # * information about the current bandwidth displayed (avg. spectrum) # * coordinates of 1st point (1-point, line) # * coordinates of 2nd point (line) # TODO: need to handle DAS properly, in case it's tiled (in XY), to avoid # loading too much data in memory. # Ensure the data is a DataArray, as we don't handle (yet) DAS if isinstance(image, model.DataArrayShadow): image = image.getData() if len(image.shape) == 3: # force 5D for CYX image = image[:, numpy.newaxis, numpy.newaxis, :, :] elif len(image.shape) == 4: # force 5D for CTYX image = image[:, :, numpy.newaxis, :, :] elif len(image.shape) != 5 or image.shape[2] != 1: logging.error("Cannot handle data of shape %s", image.shape) raise NotImplementedError( "StaticSpectrumStream needs 3D or 4D data") # This is for "average spectrum" projection # cached list of wavelength for each pixel pos self._wl_px_values, unit_bw = spectrum.get_spectrum_range(image) min_bw, max_bw = self._wl_px_values[0], self._wl_px_values[-1] cwl = (max_bw + min_bw) / 2 width = (max_bw - min_bw) / 12 # The selected wavelength for a temporal spectrum display self.selected_wavelength = model.FloatContinuous( self._wl_px_values[0], range=(min_bw, max_bw), unit=unit_bw, setter=self._setWavelength) # Is there time data? if image.shape[1] > 1: # cached list of timestamps for each position in the time dimension self._tl_px_values, unit_t = spectrum.get_time_range(image) min_t, max_t = self._tl_px_values[0], self._tl_px_values[-1] # Allow the select the time as any value within the range, and the # setter will automatically "snap" it to the closest existing timestamp self.selected_time = model.FloatContinuous(self._tl_px_values[0], range=(min_t, max_t), unit=unit_t, setter=self._setTime) # This attribute is used to keep track of any selected pixel within the # data for the display of a spectrum self.selected_pixel = model.TupleVA((None, None)) # int, int # first point, second point in pixels. It must be 2 elements long. self.selected_line = model.ListVA([(None, None), (None, None)], setter=self._setLine) # The thickness of a point or a line (shared). # A point of width W leads to the average value between all the pixels # which are within W/2 from the center of the point. # A line of width W leads to a 1D spectrum taking into account all the # pixels which fit on an orthogonal line to the selected line at a # distance <= W/2. self.selectionWidth = model.IntContinuous(1, [1, 50], unit="px") self.selectionWidth.subscribe(self._onSelectionWidth) # Peak method index, None if spectrum peak fitting curve is not displayed self.peak_method = model.VAEnumerated("gaussian", {"gaussian", "lorentzian", None}) # TODO: allow to pass the calibration data as argument to avoid # recomputing the data just after init? # Spectrum efficiency compensation data: None or a DataArray (cf acq.calibration) self.efficiencyCompensation = model.VigilantAttribute( None, setter=self._setEffComp) self.efficiencyCompensation.subscribe(self._onCalib) # Is there spectrum data? if image.shape[0] > 1: # low/high values of the spectrum displayed self.spectrumBandwidth = model.TupleContinuous( (cwl - width, cwl + width), range=((min_bw, min_bw), (max_bw, max_bw)), unit=unit_bw, cls=(int, long, float)) self.spectrumBandwidth.subscribe(self.onSpectrumBandwidth) # Whether the (per bandwidth) display should be split intro 3 sub-bands # which are applied to RGB self.fitToRGB = model.BooleanVA(False) self.fitToRGB.subscribe(self.onFitToRGB) # the raw data after calibration self.calibrated = model.VigilantAttribute(image) if "acq_type" not in kwargs: if image.shape[0] > 1 and image.shape[1] > 1: kwargs["acq_type"] = model.MD_AT_TEMPSPECTRUM elif image.shape[0] > 1: kwargs["acq_type"] = model.MD_AT_SPECTRUM elif image.shape[1] > 1: kwargs["acq_type"] = model.MD_AT_TEMPORAL else: logging.warning( "SpectrumStream data has no spectrum or time dimension, shape = %s", image.shape) super(StaticSpectrumStream, self).__init__(name, [image], *args, **kwargs) # Automatically select point/line if data is small (can only be done # after .raw is set) if image.shape[-2:] == (1, 1): # Only one point => select it immediately self.selected_pixel.value = (0, 0) elif image.shape[ -2] == 1: # Horizontal line => select line immediately self.selected_line.value = [(0, 0), (image.shape[-1] - 1, 0)] elif image.shape[-1] == 1: # Vertical line => select line immediately self.selected_line.value = [(0, 0), (0, image.shape[-2] - 1)]