def __init__(self, alg_name, alpha_deg, beta_deg, info=None): self.alpha_deg = alpha_deg self.beta_deg = beta_deg self.alg_name = alg_name self.info = info # customize data_format based on which algorithm type if self.alg_name.startswith('matlab'): self.data_format = dataformats.get_format('AlgorithmOutputMatlab') else: self.data_format = dataformats.get_format(self.class_name)
def __init__(self, alg_name, alpha_deg, beta_deg, info=None): self.alpha_deg = alpha_deg self.beta_deg = beta_deg self.alg_name = alg_name self.info = info # customize data_format based on which algorithm type if self.alg_name.startswith('matlab'): self.data_format = dataformats.get_format('AlgorithmOutputMatlab') else: self.data_format = dataformats.get_format(self.class_name)
def check_input(h5group, data_format): """ Input HDF5 group should have 'obj_type' attribute which matches a class we know. """ if ext_data_format is not None: # for testing: don't use dataformats.get_format() return ext_data_format else: if 'obj_type' not in h5group.attrs: if h5group == h5group.file: raise InterfaceError( 'Looks like you supplied the HDF5 file object ' + 'instead of the HDF5 group representing the object...') else: raise InterfaceError( 'HDF5 object should have an attribute, obj_type') obj_type = h5group.attrs['obj_type'] data_format = dataformats.get_format(obj_type) return data_format
def check_input(h5group, data_format): """ Input HDF5 group should have 'obj_type' attribute which matches a class we know. """ if ext_data_format is not None: # for testing: don't use dataformats.get_format() return ext_data_format else: if 'obj_type' not in h5group.attrs: if h5group == h5group.file: raise InterfaceError( 'Looks like you supplied the HDF5 file object ' + 'instead of the HDF5 group representing the object...') else: raise InterfaceError( 'HDF5 object should have an attribute, obj_type') obj_type = h5group.attrs['obj_type'] data_format = dataformats.get_format(obj_type) return data_format
def test_io(tracks=None): """ """ import etrack.reconstruction.test_moments as tm if tracks is None: tracks = tm.get_tracklist(n_files=1) mom = tm.momentlist_from_tracklist(tracks, fill_nans=False) cl = tm.classifierlist_from_tracklist(tracks, mom) filename = 'testcl.h5' print('Writing classifiers to {}...'.format(filename)) trackio.write_object_list_to_hdf5(filename, cl, prefix='cl_') print('Reading classifiers back...') clread = trackio.read_object_list_from_hdf5( filename, Classifier.from_hdf5, prefix='cl_') print('Checking attributes...') attrs = df.get_format('Classifier') error_ind = [] error_list = [] for i, c in enumerate(clread): if hasattr(c, 'error'): if c.error: error_ind.append(i) error_list.append(c.error) # if there's a track error, which fields are filled, are wonky continue for attr in attrs: if attr.name == 'g4track': continue elif getattr(cl[i], attr.name) is not None: assert getattr(cl[i], attr.name) == getattr(c, attr.name) return error_ind, error_list
def test_io(tracks=None): """ """ import etrack.reconstruction.test_moments as tm if tracks is None: tracks = tm.get_tracklist(n_files=1) mom = tm.momentlist_from_tracklist(tracks, fill_nans=False) cl = tm.classifierlist_from_tracklist(tracks, mom) filename = 'testcl.h5' print('Writing classifiers to {}...'.format(filename)) trackio.write_object_list_to_hdf5(filename, cl, prefix='cl_') print('Reading classifiers back...') clread = trackio.read_object_list_from_hdf5( filename, Classifier.from_hdf5, prefix='cl_') print('Checking attributes...') attrs = df.get_format('Classifier') error_ind = [] error_list = [] for i, c in enumerate(clread): if hasattr(c, 'error'): if c.error: error_ind.append(i) error_list.append(c.error) # if there's a track error, which fields are filled, are wonky continue for attr in attrs: if attr.name == 'g4track': continue elif getattr(cl[i], attr.name) is not None: assert getattr(cl[i], attr.name) == getattr(c, attr.name) return error_ind, error_list
class MatlabAlgorithmInfo(object): """ An empty container to store attributes loaded from the Matlab algorithm. """ __version__ = '0.1' class_name = 'MatlabAlgorithmInfo' data_format = dataformats.get_format(class_name) attr_list = ( 'Tind', 'lt', 'n_ends', 'Eend', 'alpha', 'beta', 'dalpha', 'dbeta', 'edgesegments_energies_kev', 'edgesegments_coordinates_pix', 'edgesegments_chosen_index', 'edgesegments_start_coordinates_pix', 'edgesegments_start_direction_indices', 'edgesegments_low_threshold_used', 'dedx_ref', 'dedx_meas', 'measurement_start_ind', 'measurement_end_ind', ) data_list = ( 'thinned_img', 'x', 'y', 'w', 'a0', 'dE', ) def __init__(self, **kwargs): """ Shouldn't need to call this -- use from_h5pixnoise or from_pydict """ for attr in kwargs: setattr(self, attr, kwargs[attr]) @classmethod def from_h5pixnoise(cls, pixnoise): """ Initialize MatlabAlgorithmInfo with all the attributes from a successful HybridTrack algorithm. pixnoise is the h5 group. This goes with the _h5matlab format. """ kwargs = {} for attr in cls.attr_list: if attr in pixnoise.attrs: kwargs[attr] = pixnoise.attrs[attr] else: raise InputError('Missing h5 attribute: {} in {}'.format( attr, str(pixnoise))) for attr in cls.data_list: if attr in pixnoise: kwargs[attr] = pixnoise[attr] else: raise InputError('Missing h5 dataset: {} in {}'.format( attr, str(pixnoise))) return cls(**kwargs) @classmethod def from_pydict(cls, read_dict, pydict_to_pyobj=None): """ Initialize a MatlabAlgorithmInfo object from the dictionary returned by trackio.read_object_from_hdf5(). """ if pydict_to_pyobj is None: pydict_to_pyobj = {} if id(read_dict) in pydict_to_pyobj: return pydict_to_pyobj[id(read_dict)] all_attrs = cls.attr_list kwargs = {} for attr in all_attrs: kwargs[attr] = read_dict.get(attr) # read_dict.get() defaults to None, although this actually # shouldn't be needed since read_object_from_hdf5 adds Nones constructed_object = cls(**kwargs) # add entry to pydict_to_pyobj pydict_to_pyobj[id(read_dict)] = constructed_object return constructed_object @classmethod def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None): """ Initialize a MatlabAlgorithmInfo object from an HDF5 group. """ if h5_to_pydict is None: h5_to_pydict = {} if pydict_to_pyobj is None: pydict_to_pyobj = {} read_dict = trackio.read_object_from_hdf5(h5group, h5_to_pydict=h5_to_pydict) constructed_object = cls.from_pydict(read_dict, pydict_to_pyobj=pydict_to_pyobj) return constructed_object
class Track(object): """ Electron track, from modeling or from experiment. """ __version__ = '0.1' class_name = 'Track' data_format = dataformats.get_format(class_name) attr_list = ( 'is_modeled', 'is_measured', 'pixel_size_um', 'noise_ev', 'g4track', 'energy_kev', 'x_offset_pix', 'y_offset_pix', 'timestamp', 'shutter_ind', 'label', ) attr_types = { 'is_modeled': bool, 'is_measured': bool, 'pixel_size_um': float, 'noise_ev': float, 'g4track': object, 'energy_kev': float, 'x_offset_pix': float, 'y_offset_pix': float, 'timestamp': object, 'shutter_ind': int, 'label': str, } def __init__(self, image, **kwargs): """ Construct a track object. Required input: image Either is_modeled or is_measured is required. Keyword inputs: is_modeled (bool) is_measured (bool) pixel_size_um (float) noise_ev (float) g4track (G4Track object) energy_kev (float) x_offset_pix (int) y_offset_pix (int) timestamp (datetime object) shutter_ind (int) label (string) """ self.input_handling(image, **kwargs) self.algorithms = {} def input_handling(self, image, is_modeled=None, is_measured=None, pixel_size_um=None, noise_ev=None, g4track=None, energy_kev=None, x_offset_pix=None, y_offset_pix=None, timestamp=None, shutter_ind=None, label=None): if is_modeled is None and is_measured is None: raise InputError('Please specify modeled or measured!') elif is_modeled is True and is_measured is True: raise InputError('Track cannot be both modeled and measured!') elif is_modeled is False and is_measured is False: raise InputError('Track must be either modeled or measured!') elif is_measured is not None: self.is_measured = bool(is_measured) self.is_modeled = not bool(is_measured) elif is_modeled is not None: self.is_modeled = bool(is_modeled) self.is_measured = not bool(is_modeled) if g4track is not None and not isinstance(g4track, G4Track): raise InputError('g4track input must be a G4Track!') # or handle e.g. a g4 matrix input self.g4track = g4track self.image = np.array(image) if pixel_size_um is not None: pixel_size_um = np.float(pixel_size_um) self.pixel_size_um = pixel_size_um if noise_ev is not None: noise_ev = np.float(noise_ev) self.noise_ev = noise_ev if energy_kev is not None: energy_kev = np.float(energy_kev) self.energy_kev = energy_kev if x_offset_pix is not None: np.testing.assert_almost_equal( x_offset_pix, np.round(x_offset_pix), decimal=3, err_msg='x_offset_pix must be an integer') x_offset_pix = int(np.round(x_offset_pix)) self.x_offset_pix = x_offset_pix if y_offset_pix is not None: np.testing.assert_almost_equal( y_offset_pix, np.round(y_offset_pix), decimal=3, err_msg='y_offset_pix must be an integer') y_offset_pix = int(np.round(y_offset_pix)) self.y_offset_pix = y_offset_pix if shutter_ind is not None: np.testing.assert_almost_equal( shutter_ind, np.round(shutter_ind), decimal=3, err_msg='shutter_ind must be an integer') shutter_ind = int(np.round(shutter_ind)) self.shutter_ind = shutter_ind if (timestamp is not None and timestamp != 'None' and not isinstance(timestamp, datetime.datetime)): raise InputError('timestamp should be a datetime object') self.timestamp = str(timestamp) if label is not None: label = str(label) self.label = label @classmethod def from_h5matlab(cls, pixnoise, g4track=None): """ Construct a Track object from one pixelsize/noise of an event in an HDF5 file. HDF5 file is the December 2015 format from MATLAB. """ try: errorcode = pixnoise.attrs['errorcode'] except KeyError: # found this in HTbatch01_h5m/MultiAngle_HT_100_12.h5 track 127 errorcode = 97 if errorcode != 0: # for now, only accept tracks without errors return errorcode # algorithm_error = (errorcode == 4 or errorcode == 5) # good_algorithm = (errorcode == 0) # has_algorithm = good_algorithm or algorithm_error # has_ridge = good_algorithm # has_measurement = good_algorithm # has_multiple_tracks = (errorcode == 2 or errorcode == 6) # # do_pixnoise = true # check_pixsize = good_algorithm or has_multiple_tracks # check_noise = has_multiple_tracks # do_img = has_algorithm # # do_EtotTind = has_algorithm # do_nends = good_algorithm or errorcode == 4 # # do_T = has_multiple_tracks # do_edgesegments = good_algorithm # do_ridge = has_ridge # do_measurement = has_measurement # track info (not algorithm info) try: data = pixnoise['img'] except KeyError: errorcode = 96 return errorcode img = np.zeros(data.shape) data.read_direct(img) kwargs = {} kwargs['is_modeled'] = True kwargs['g4track'] = g4track try: kwargs['pixel_size_um'] = pixnoise.attrs['pixel_size_um'] kwargs['noise_ev'] = pixnoise.attrs['noise_ev'] kwargs['energy_kev'] = pixnoise.attrs['Etot'] except KeyError: # hypothetical errorcode = 98 return errorcode track = Track(img, **kwargs) # algorithm info try: alpha = pixnoise.attrs['alpha'] beta = pixnoise.attrs['beta'] except KeyError: # found this in HTbatch01_h5m/MultiAngle_100_3.h5 track 00097 # yes, with errorcode 0. maybe something crashed? errorcode = 99 return errorcode try: info = MatlabAlgorithmInfo.from_h5pixnoise(pixnoise) except InputError: errorcode = 88 return errorcode track.add_algorithm('matlab HT v1.5', alpha_deg=alpha, beta_deg=beta, info=info) return track @classmethod def from_dth5(cls, pixnoise, g4track=None): """ Construct a Track object from one pixelsize/noise of an event in an HDF5 file. The format is that of files from DT_to_hdf5.m / write_DT_hdf5.m. """ # adapted from from_h5matlab() try: errorcode = int(pixnoise.attrs['errorcode']) except KeyError: # found this in HTbatch01_h5m/MultiAngle_HT_100_12.h5 track 127 errorcode = 97 if errorcode != 0: # for now, only accept tracks without errors return errorcode # see logbook p. 132 (2016-oct-11) for other error codes # since this is data directly from DT, # we need to handle segmentation issues. if 'T1' in pixnoise.keys(): # multiple tracks. not dealing with these. # assume real multiplicity events (multiple scattering) were # already removed after the G4Track. errorcode = 2 # "Segmentation divided the track" return errorcode if 'T0' not in pixnoise.keys(): # no track errorcode = 3 # "No segmented image to use" return errorcode # now, the track info. kwargs = {} kwargs['is_modeled'] = True kwargs['g4track'] = g4track try: data = pixnoise['T0']['img'] except KeyError: errorcode = 76 return errorcode img = np.zeros(data.shape) data.read_direct(img) try: kwargs['pixel_size_um'] = float(pixnoise.attrs['pixel_size_um']) kwargs['noise_ev'] = int(pixnoise.attrs['noise_ev']) kwargs['energy_kev'] = float(pixnoise.attrs['E']) pix_thresh = int(pixnoise.attrs['pixel_threshold']) seg_thresh_kev = float(pixnoise.attrs['segment_threshold']) edgeflag = pixnoise['T0'].attrs['edgeflag'] kwargs['x_offset_pix'] = int(pixnoise['T0'].attrs['x']) kwargs['y_offset_pix'] = int(pixnoise['T0'].attrs['y']) except KeyError: # hypothetical errorcode = 78 return errorcode # put extra info in the "label" attribute label_text = 'pix_thresh={}, seg_thresh_kev={}, edgeflag={}'.format( int(pix_thresh), float(seg_thresh_kev), int(edgeflag)) kwargs['label'] = label_text track = Track(img, **kwargs) return track @classmethod def from_pydict(cls, read_dict, pydict_to_pyobj=None): """ Initialize a Track object from the dictionary returned by trackio.read_object_from_hdf5(). """ if pydict_to_pyobj is None: pydict_to_pyobj = {} if id(read_dict) in pydict_to_pyobj: return pydict_to_pyobj[id(read_dict)] other_attrs = ('image', 'algorithms') all_attrs = other_attrs + cls.attr_list kwargs = {} # keep algorithms in a separate dict because they do not go in __init__ algorithms = {} for attr in all_attrs: if attr == 'g4track' and read_dict.get(attr) is not None: kwargs[attr] = G4Track.from_pydict( read_dict[attr], pydict_to_pyobj=pydict_to_pyobj) # if g4track *is* None, then it gets assigned in "else" below elif attr == 'algorithms': if read_dict.get(attr) is not None: for key, val in read_dict[attr].iteritems(): algorithms[key] = AlgorithmOutput.from_pydict( read_dict[attr][key], pydict_to_pyobj=pydict_to_pyobj) # else, algorithms is still {} as it should be else: kwargs[attr] = read_dict.get(attr) # read_dict.get() defaults to None, although this actually # shouldn't be needed since read_object_from_hdf5 adds Nones image = kwargs.pop('image') constructed_object = cls(image, **kwargs) for key, algoutput in algorithms.iteritems(): alpha = algoutput.alpha_deg beta = algoutput.beta_deg info = algoutput.info constructed_object.add_algorithm(key, alpha, beta, info) # add entry to pydict_to_pyobj pydict_to_pyobj[id(read_dict)] = constructed_object return constructed_object @classmethod def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None): """ Initialize a Track instance from an HDF5 group. """ if h5_to_pydict is None: h5_to_pydict = {} if pydict_to_pyobj is None: pydict_to_pyobj = {} read_dict = trackio.read_object_from_hdf5(h5group, h5_to_pydict=h5_to_pydict) constructed_object = cls.from_pydict(read_dict, pydict_to_pyobj=pydict_to_pyobj) return constructed_object @classmethod def generate_random(cls, size=(10, 10), alg_name=None, a_fwhm=30, a_f=0.7, b_rms=20, b_f=0.5): """ Generate a random image. is_modeled = True. size: 2x1 tuple indicating dimensions of image alg_name: if this is a string, generate a random alpha and beta and assign as algorithm results to this alg_name. The distribution of dalpha and dbeta are controlled by a_fwhm, a_f, b_rms, b_f, as in evaluation.generate_random_alg_results """ img = np.random.random(size=size) a0 = np.random.uniform(-180.0, 180.0) b0 = np.random.uniform(-90.0, 90.0) g4t = G4Track(alpha_deg=a0, beta_deg=b0) t = Track(img, is_modeled=True, g4track=g4t) if alg_name is not None and isinstance(alg_name, str): # alpha if np.random.random() < a_f: a1 = a0 + np.random.normal(loc=0.0, scale=(a_fwhm) / 2.355) else: a1 = np.random.uniform(-180.0, 180.0) # beta if np.random.random() < b_f: b1 = b0 + np.random.normal(loc=0.0, scale=b_rms) if b1 < 0: b1 = 0 elif b1 > 90: b1 = 89.9 else: b1 = 0 t.add_algorithm(alg_name, a1, b1) return t def add_algorithm(self, alg_name, alpha_deg, beta_deg, info=None): """ """ if alg_name in self.algorithms: raise InputError(alg_name + " already in algorithms") self.algorithms[alg_name] = AlgorithmOutput(alg_name, alpha_deg, beta_deg, info=info) def list_algorithms(self): """ List AlgorithmOutput objects attached to this track. Like dict.keys() """ return self.algorithms.keys() def keys(self): """ Allow dictionary-like behavior on Track object for its attached algorithms. """ return self.list_algorithms() def __getitem__(self, key): # Map dictionary lookup to algorithms dictionary. return self.algorithms[key] def __contains__(self, item): # Map dictionary lookup to algorithms dictionary. return item in self.algorithms
class G4Track(object): """ Electron track from Geant4. """ __version__ = '0.2' class_name = 'G4Track' data_format = dataformats.get_format(class_name) # a lot more attributes could be added here... attr_list = ( 'x', 'dE', 'x0', 'alpha_deg', 'beta_deg', 'first_step_vector', 'energy_tot_kev', 'energy_dep_kev', 'energy_esc_kev', 'energy_xray_kev', 'energy_brems_kev', 'depth_um', 'is_contained', ) def __init__(self, matrix=None, **kwargs): """ Construct G4Track object. If matrix is supplied and other quantities are not, then the other quantities will be calculated using the matrix (not implemented yet). matrix (see attr_list class variable) """ self.matrix = matrix for attr in self.attr_list: if attr in kwargs: setattr(self, attr, kwargs[attr]) else: setattr(self, attr, None) if matrix is not None and ('x' not in kwargs or 'dE' not in kwargs or 'energy_tot_kev' not in kwargs or 'energy_dep_kev' not in kwargs or 'energy_esc_kev' not in kwargs or 'depth_um' not in kwargs or 'is_contained' not in kwargs): pass # self.measure_quantities() @classmethod def from_h5matlab(cls, evt): """ Construct a G4Track instance from an event in an HDF5 file. The format of the HDF5 file is 'matlab', a.k.a. the more complete mess that Brian made in December 2015. """ if evt.attrs['multiplicity'] > 1: # at this time, I am not handling multiple-scattered photons return None data = evt['trackM'] matrix = np.zeros(data.shape) data.read_direct(matrix) matrix = np.array(matrix) cheat = evt['cheat']['0'] try: # h5 attributes kwargs = { 'energy_tot_kev': float(cheat.attrs['Etot']), 'energy_dep_kev': float(cheat.attrs['Edep']), 'energy_esc_kev': float(evt.attrs['Eesc']), 'energy_xray_kev': float(cheat.attrs['Exray']), 'energy_brems_kev': float(cheat.attrs['Ebrems']), 'x0': cheat.attrs['x0'], 'first_step_vector': cheat.attrs['firstStepVector'], 'alpha_deg': float(cheat.attrs['alpha']), 'beta_deg': float(cheat.attrs['beta']) } except KeyError: # found this in HTbatch01_h5m/MultiAngle_100_6.h5 track 117 print('Unexpected missing attributes in ' + evt.name + '!') return None try: # h5 datasets data = cheat['x'] x = np.zeros(data.shape) data.read_direct(x) kwargs['x'] = x data = cheat['dE'] dE = np.zeros(data.shape) data.read_direct(dE) kwargs['dE'] = dE except KeyError: # hypothetical print('Unexpected missing dataset in ' + evt.name + '!') return None g4track = G4Track(matrix=matrix, **kwargs) return g4track @classmethod def from_dth5(cls, evt): """ Construct a G4Track instance from an event in an HDF5 file. The format is that of files from DT_to_hdf5.m / write_DT_hdf5.m. evt is the h5py group corresponding to an event. """ # G4Track comes from the 'cheat' subgroup, which is unchanged from # h5matlab. return cls.from_h5matlab(evt) @classmethod def from_pydict(cls, read_dict, pydict_to_pyobj=None): """ Initialize a G4Track object from the dictionary returned by trackio.read_object_from_hdf5(). """ if pydict_to_pyobj is None: pydict_to_pyobj = {} if id(read_dict) in pydict_to_pyobj: return pydict_to_pyobj[id(read_dict)] other_attrs = ('matrix', ) all_attrs = other_attrs + cls.attr_list kwargs = {} for attr in all_attrs: kwargs[attr] = read_dict.get(attr) # read_dict.get() defaults to None, although this actually # shouldn't be needed since read_object_from_hdf5 adds Nones constructed_object = cls(**kwargs) # add entry to pydict_to_pyobj pydict_to_pyobj[id(read_dict)] = constructed_object return constructed_object @classmethod def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None): """ Initialize a G4Track instance from an HDF5 group. """ if h5_to_pydict is None: h5_to_pydict = {} if pydict_to_pyobj is None: pydict_to_pyobj = {} read_dict = trackio.read_object_from_hdf5(h5group, h5_to_pydict=h5_to_pydict) constructed_object = cls.from_pydict(read_dict, pydict_to_pyobj=pydict_to_pyobj) return constructed_object def measure_quantities(self): """ Measure the following using the Geant4 matrix. alpha beta energy_tot energy_dep energy_esc x dE depth_um is_contained """ # TODO raise NotImplementedError("haven't written this yet") if self.matrix is None: raise DataError('measure_quantities needs a geant4 matrix')
class Classifier(object): """ Object to handle auto classifying (ground truth) a Monte Carlo track """ class_name = 'Classifier' data_format = df.get_format(class_name) def __init__(self, g4track, suppress_check=False): assert isinstance(g4track, G4Track), "Not a G4Track object" assert g4track.x.shape[0] == 3, "x has funny shape" self.x = np.copy(g4track.x) self.E = np.copy(g4track.dE.flatten()) self.g4track = g4track # avoid IO errors in case of only mc_classify and not end_classify # or for errors in test_moments.classifierlist_from_tracklist self.scatterlen_um = None self.overlapdist_um = None self.scatter_type = None self.use2d_angle = None self.use2d_dist = None self.angle_threshold_deg = None self.escaped = None self.early_scatter = None self.total_scatter_angle = None self.overlap = None self.wrong_end = None self.n_ends = None self.max_end_energy = None self.min_end_energy = None self.error = None @classmethod def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None, reconstruct=False): """ Initialize a Classifier object from an HDF5 group. """ if h5_to_pydict is None: h5_to_pydict = {} if pydict_to_pyobj is None: pydict_to_pyobj = {} read_dict = trackio.read_object_from_hdf5( h5group, h5_to_pydict=h5_to_pydict) constructed_object = cls.from_pydict( read_dict, pydict_to_pyobj=pydict_to_pyobj, reconstruct=reconstruct) return constructed_object @classmethod def from_pydict(cls, read_dict, pydict_to_pyobj=None, reconstruct=False): """ Initialize a Classifier object from a pydict. """ if pydict_to_pyobj is None: pydict_to_pyobj = {} if id(read_dict) in pydict_to_pyobj: return pydict_to_pyobj[id(read_dict)] # first, reconstruct g4track (if needed) if isinstance(read_dict['g4track'], G4Track): # g4track is already a G4Track object (not sure how) g4track = read_dict['g4track'] elif (isinstance(read_dict['g4track'], dict) and id(read_dict['g4track']) in pydict_to_pyobj): # g4track is in the pydict table g4track = pydict_to_pyobj[id(read_dict['g4track'])] elif isinstance(read_dict['g4track'], dict): # g4track not in the pydict table. create and add it g4track = G4Track.from_pydict( read_dict['g4track'], pydict_to_pyobj=pydict_to_pyobj) pydict_to_pyobj[id(read_dict['g4track'])] = g4track else: raise Exception("Unexpected or missing 'g4track' in Classifier") new_obj = cls(g4track) # add entry to pydict_to_pyobj pydict_to_pyobj[id(read_dict)] = new_obj # fill in outputs if read_dict['error'] is not None: new_obj.error = read_dict['error'] if read_dict['scatterlen_um'] is not None: # mc_classify ran new_obj.scatterlen_um = read_dict['scatterlen_um'] new_obj.overlapdist_um = read_dict['overlapdist_um'] new_obj.escaped = read_dict['escaped'] new_obj.scatter_type = read_dict['scatter_type'] new_obj.use2d_angle = read_dict['use2d_angle'] new_obj.use2d_dist = read_dict['use2d_dist'] new_obj.angle_threshold_deg = read_dict['angle_threshold_deg'] if read_dict['early_scatter'] is not None: # no TrackTooShortError new_obj.early_scatter = read_dict['early_scatter'] new_obj.total_scatter_angle = read_dict['total_scatter_angle'] new_obj.overlap = read_dict['overlap'] if read_dict['wrong_end'] is not None: # end_classify ran new_obj.wrong_end = read_dict['wrong_end'] new_obj.n_ends = read_dict['n_ends'] new_obj.max_end_energy = read_dict['max_end_energy'] new_obj.min_end_energy = read_dict['min_end_energy'] if reconstruct: new_obj.mc_classify() print( 'Reconstructing Classifier.end_classify() not implemented yet') return new_obj def mc_classify(self, scatterlen_um=25, overlapdist_um=40, verbose=False): """ Classify the Monte Carlo track as either: 'good', early scatter 'scatter', overlapping the initial end 'overlap', or no result, None. Optional input args: scatterlen_um: length from initial end, in um, to look for high-angle scatter. (default 50 um) overlapdist_um: if points are """ self.scatterlen_um = scatterlen_um self.overlapdist_um = overlapdist_um self.check_escape() self.flag_newparticle() self.check_early_scatter(v=verbose) self.check_overlap() def end_classify(self, track, mom=None, HT=None): """ Look at the end segment selection algorithm results, and classify. """ self.get_end_info(track, mom=mom, HT=HT) self.check_wrong_end(track, mom=mom, HT=HT) def get_end_info(self, track, mom=None, HT=None): """ Record number of ends, minimum end energy (this is the chosen end), maximum end energy (may indicate escape). """ if mom is not None: ends_energy = mom.info.ends_energy elif HT is not None: ends_energy = HT.info.ends_energy self.n_ends = len(ends_energy) if self.n_ends == 0: raise NoEnds('Cannot get max and min end energy') self.max_end_energy = np.max(ends_energy) self.min_end_energy = np.min(ends_energy) def check_wrong_end(self, track, mom=None, HT=None, maxdist=3): """ See if the algorithm's end segment was correct or not. """ if mom is None and HT is None: raise ValueError( 'Requires either a moments object or a HybridTrack object') g4xfull, g4yfull = tp.get_image_xy(track) g4x, g4y = g4xfull[0], g4yfull[0] # could throw an AttributeError if there were errors in the algorithm if mom: algx, algy = mom.start_coordinates elif HT: algx, algy = HT.start_coordinates else: raise ValueError('bad value in moments or HybridTrack object') dist = np.sqrt((algx - g4x)**2 + (algy - g4y)**2) self.end_distance = dist if dist > maxdist: self.wrong_end = True else: self.wrong_end = False self.g4xy = g4x, g4y self.algxy = algx, algy def check_escape(self): """ Check the Etot and Edep to see if track escaped. (>2 keV difference) Unfortunately the g4track.is_contained flag is None in this dataset. """ energy_diff_kev = ( self.g4track.energy_tot_kev - self.g4track.energy_dep_kev) self.escaped = (energy_diff_kev > 2.0) def flag_newparticle(self): """ Look for particle transitions - jumping to a new electron ID in Geant4. Marked by >1.5um step. """ if not hasattr(self, 'd'): self.dx = self.x[:, 1:] - self.x[:, :-1] self.d = np.linalg.norm(self.dx, axis=0) self.dx_newparticle_flag = (self.d > BIG_STEP_UM * 1.5) def check_early_scatter(self, v=False, scatter_type='total', use2d_angle=True, use2d_dist=True, angle_threshold_deg=30): """ look for a >30 degree direction change within the first scatterlen_um of track. v: verbosity (True: print "Early scatter!") scatter_type: 'total': compare direction at end of segment, to beginning of segment 'discrete': look for a single scattering of more than angle angle_threshold_deg: threshold angle. scattering through more than this angle is flagged. use2d_angle: flag for looking at the scatter angle in the 2D plane use2d_dist: flag for measuring scatterlen along the track projection """ angle_threshold_rad = np.float(angle_threshold_deg) / 180 * np.pi angle_threshold_cos = np.cos(angle_threshold_rad) self.scatter_type = scatter_type self.use2d_angle = use2d_angle self.use2d_dist = use2d_dist self.angle_threshold_deg = angle_threshold_deg # use only every other point, so as to bypass the zigzag issue. # x2: positions (every other point) self.x2 = self.x[:, ::2] # dx2: delta-position between each entry in x2 self.dx2 = self.x2[:, 1:] - self.x2[:, :-1] # integrate the path length, either in 2D or 3D, to determine cutoff # for scatterlen # d2: dx2 integrated to get path length # d2_2d: dx2 integrated to get path length, in 2D if use2d_dist: self.d2_2d = np.linalg.norm(self.dx2[:2, :], axis=0) integrated_dist = np.cumsum(self.d2_2d) else: self.d2 = np.linalg.norm(self.dx2, axis=0) integrated_dist = np.cumsum(self.d2) # ind2: the index where path length (2D or 3D) exceeds scatterlen # short tracks raise an IndexError here try: ind2 = np.nonzero(integrated_dist >= self.scatterlen_um)[0][0] - 1 except IndexError: raise TrackTooShortError() # trim x2 and dx2 self.x2 = self.x2[:, :ind2] self.dx2 = self.dx2[:, :ind2] # dx2norm: unit vectors of dx2 self.dx2norm = self.normalize_steps(self.dx2) # ddir2: dot product of consecutive dx2norm's. =cos(theta) self.ddir2 = np.sum( self.dx2norm[:, 1:] * self.dx2norm[:, :-1], axis=0) # dx2norm_2d: 2D unit vectors of dx2 self.dx2norm_2d = self.dx2[:2, :] / np.linalg.norm( self.dx2[:2, :], axis=0) # ddir2_2d: dot product of consecutive dx2norm_2d's. =cos(theta) self.ddir2_2d = np.sum( self.dx2norm_2d[:, 1:] * self.dx2norm_2d[:, :-1], axis=0) # for discrete scatters, look for large angles in dx2norm or dx2norm_2d # for total scatter angle, dot the unit vector with the initial # direction if scatter_type.lower() == 'total': # ddir: the unit vector at each step, # dotted with the initial unit vector, to get angle of deviation # take the maximum from along scatterlen if use2d_angle: ddir = np.array([ np.sum(self.dx2norm_2d[:, 0] * self.dx2norm_2d[:, i]) for i in xrange(1, self.dx2norm_2d.shape[1])]) else: ddir = np.array([ np.sum(self.dx2norm[:, 0] * self.dx2norm[:, i]) for i in xrange(1, self.dx2norm.shape[1])]) self.early_scatter = (np.min(ddir) < angle_threshold_cos) self.total_scatter_angle = np.arccos(np.min(ddir)) elif scatter_type.lower() == 'discrete': if use2d_angle: self.early_scatter = np.any(self.ddir2 < angle_threshold_cos) else: self.early_scatter = np.any(self.ddir2 < angle_threshold_cos) else: raise ValueError( 'scatter_type {} not recognized!'.format(scatter_type)) if self.early_scatter and v: print('Early scatter!') def check_overlap(self): """ look for a section of track overlapping with the initial scatterlen_um of track. """ self.overlap = False # until shown otherwise self.scatterlen_steps = self.scatterlen_um / BIG_STEP_UM * 2 # see which points are within overlapdist_um of these points # distance matrix: distance of all points from each of the first 50 # arrays of dimensions (self.x.shape[1], self.numsteps) all_x, init_x = np.meshgrid( self.x[0, :self.scatterlen_steps], self.x[0, :]) all_y, init_y = np.meshgrid( self.x[1, :self.scatterlen_steps], self.x[1, :]) dist_matrix = (all_x - init_x)**2 + (all_y - init_y)**2 dist_vector = np.min(dist_matrix, axis=1) # don't bother with sqrt dist_threshold = self.overlapdist_um**2 too_close = (dist_vector < dist_threshold) + 0 # as an int # the initial segment, and some points after it, are obviously # going to be too close. # first try: if at least overlapdist of consecutive points are # too_close, then classify the track as overlapping # i.e. at least overlapdist consecutive points are within overlapdist # of the initial scatterlen segment. # look for transitions from too_close to not too_close, and back dclose = too_close[1:] - too_close[:-1] # get indices of transitions. # ignore first transition away from initial segment going_out = np.nonzero(dclose == -1)[0] # too_close to not too_close going_out = going_out[1:] going_in = np.nonzero(dclose == 1)[0] # not too_close to too_close # get the lengths of too_close segments segment_len_threshold = self.overlapdist_um / BIG_STEP_UM * 2 for i, in_ind in enumerate(going_in): try: out_ind = going_out[i] except IndexError: out_ind = len(dclose) if out_ind - in_ind > segment_len_threshold: self.overlap = True break def normalize_steps(self, dx, d=None): """ normalize steps into unit vectors dx.shape should be (3, n) """ assert dx.shape[0] == 3, "dx has funny shape in normalize_steps" if d is None: d = np.linalg.norm(dx, axis=0) norm_dx = dx / d return norm_dx def round_steplength(self, d): """ round to nearest half-big-step (nearest 0.5 um) """ return np.round(d / (BIG_STEP_UM / 2)) * (BIG_STEP_UM / 2)
class MomentsReconstruction(object): class_name = 'MomentsReconstruction' data_format = df.get_format(class_name) def __init__(self, original_image_kev, pixel_size_um=10.5, starting_distance_um=63): """ Init: Load options only """ self.original_image_kev = original_image_kev self.pixel_size_um = pixel_size_um self.options = hybridtrack.ReconstructionOptions(pixel_size_um) # increase walking distance from 4 pixels to 6 pixels - for now # hybridtrack default: 40 um # initial testing before 6/21/16: 63 um self.starting_distance_um = starting_distance_um self.options.ridge_starting_distance_from_track_end_um = ( starting_distance_um) self.info = hybridtrack.ReconstructionInfo() # for writing to file - these attributes must exist self.edge_pixel_count = None self.edge_pixel_segments = None self.phi = None self.R = None self.alpha = None self.x0 = None self.error = None @classmethod def from_hdf5(cls, h5group, h5_to_pydict=None, pydict_to_pyobj=None, reconstruct=False): """ Initialize a MomentsReconstruction object from an HDF5 group. """ if h5_to_pydict is None: h5_to_pydict = {} if pydict_to_pyobj is None: pydict_to_pyobj = {} read_dict = trackio.read_object_from_hdf5(h5group, h5_to_pydict=h5_to_pydict) constructed_object = cls.from_pydict(read_dict, pydict_to_pyobj=pydict_to_pyobj, reconstruct=reconstruct) return constructed_object @classmethod def from_pydict(cls, read_dict, pydict_to_pyobj=None, reconstruct=False): """ Initialize a MomentsReconstruction object from a pydict. """ if pydict_to_pyobj is None: pydict_to_pyobj = {} if id(read_dict) in pydict_to_pyobj: return pydict_to_pyobj[id(read_dict)] new_obj = cls(read_dict['original_image_kev'], pixel_size_um=read_dict['pixel_size_um'], starting_distance_um=read_dict['starting_distance_um']) # fill in outputs new_obj.error = read_dict['error'] if read_dict['ends_energy'] is not None: # (actually required by the data_formats class) new_obj.ends_energy = read_dict['ends_energy'] new_obj.rough_est = read_dict['rough_est'] new_obj.box_x = read_dict['box_x'] new_obj.box_y = read_dict['box_y'] if read_dict['edge_pixel_count'] is not None: # successfully got a segment image new_obj.edge_pixel_count = read_dict['edge_pixel_count'] new_obj.edge_pixel_segments = read_dict['edge_pixel_segments'] if read_dict['alpha'] is not None: # actually, even if calculation is pathological, these are np.nan # so they still get assigned. new_obj.phi = read_dict['phi'] new_obj.R = read_dict['R'] new_obj.alpha = read_dict['alpha'] new_obj.x0 = read_dict['x0'] # add entry to pydict_to_pyobj pydict_to_pyobj[id(read_dict)] = new_obj if reconstruct: new_obj.reconstruct() return new_obj def reconstruct(self): hybridtrack.choose_initial_end(self.original_image_kev, self.options, self.info) try: self.segment_initial_end() # get a sub-image containing the initial end # also need a rough estimate of the electron direction # (using thinned) except CheckSegmentBoxError: self.error = 'CheckSegmentBoxError' return except RuntimeError: self.error = 'what the heck happened?' return # 1. self.get_coordlist() self.compute_first_moments() # 2ab. self.compute_central_moments() # 3ab. try: self.compute_optimal_rotation_angle() except MomentsError: self.error = 'Rotation angle conditions not met' return # 4. self.compute_arc_parameters() self.compute_direction() self.compute_pathology() @classmethod def from_end_segment(cls, end_segment_image, rough_est): """ Run the moments on a (handpicked) track section. """ mom = cls(None) mom.end_segment_image = end_segment_image mom.rough_est = rough_est mom.get_coordlist() mom.compute_first_moments() mom.compute_central_moments() mom.compute_optimal_rotation_angle() mom.compute_arc_parameters() mom.compute_direction() mom.compute_pathology() return mom @classmethod def reconstruct_arc(cls, clist, rough_est): """ Run the moments on a CoordinatesList object, e.g. from generate_arc(). Need to also provide a rough_est """ mom = cls(None) mom.clist0 = clist mom.rough_est = rough_est mom.compute_first_moments() mom.compute_central_moments() mom.compute_optimal_rotation_angle() mom.compute_arc_parameters() mom.compute_direction() return mom def get_segment_initial_values(self): """ Get start_coordinates, end_coordinates, and rough_est for segmenting. """ # copied from hybridtrack.get_starting_point() self.ends_energy = self.info.ends_energy min_index = self.ends_energy.argmin() self.end_energy = self.ends_energy[min_index] self.start_coordinates = self.info.ends_xy[min_index] # start_coordinates are the end (extremity) of the thinned track self.end_coordinates = self.info.start_coordinates # end_coordinates are after walking up the track 40 um # angle from start to end dcoord = self.end_coordinates - self.start_coordinates self.rough_est = np.arctan2(dcoord[1], dcoord[0]) def get_segment_box(self): """ Get the x,y coordinates of the box containing the initial segment. """ segwid = 10 # pixels seglen = 11 # pixels mod = self.rough_est % (np.pi / 2) if mod < np.pi / 6 or mod > np.pi / 3: # close enough to orthogonal general_dir = np.round(self.rough_est / (np.pi / 2)) * (np.pi / 2) self.is45 = False else: # use a diagonal box (aligned to 45 degrees) general_dir = np.round(self.rough_est / (np.pi / 4)) * (np.pi / 4) self.is45 = True # make box and rotate box_dx = np.array([0, -seglen, -seglen, 0, 0]) box_dy = np.array( [-segwid / 2, -segwid / 2, segwid / 2, segwid / 2, -segwid / 2]) box_dx_rot = (np.round(box_dx * np.cos(general_dir)) - np.round(box_dy * np.sin(general_dir))).astype(int) box_dy_rot = (np.round(box_dx * np.sin(general_dir)) + np.round(box_dy * np.cos(general_dir))).astype(int) self.box_x = self.end_coordinates[0] + box_dx_rot self.box_y = self.end_coordinates[1] + box_dy_rot def check_segment_box(self): """ Check whether the box intersects too many hot pixels. That would indicate that we should draw a new box. """ problem_length = 60 # microns # xmesh, ymesh get used in get_pixlist, also. so save into self. img_shape = self.original_image_kev.shape self.xmesh, self.ymesh = np.meshgrid(range(img_shape[0]), range(img_shape[1]), indexing='ij') # get the pixels along the line segment that passes through the track, # by walking along from one endpoint toward the other. xcheck = [self.box_x[-2]] ycheck = [self.box_y[-2]] dx = np.sign(self.box_x[-1] - self.box_x[-2]) dy = np.sign(self.box_y[-1] - self.box_y[-2]) while xcheck[-1] != self.box_x[-1] or ycheck[-1] != self.box_y[-1]: xcheck.append(xcheck[-1] + dx) ycheck.append(ycheck[-1] + dy) xcheck.append(self.box_x[-1]) ycheck.append(self.box_y[-1]) xcheck = np.array(xcheck) ycheck = np.array(ycheck) lgbad = ((xcheck < 0) | (xcheck >= self.original_image_kev.shape[0]) | (ycheck < 0) | (ycheck >= self.original_image_kev.shape[1])) xcheck = xcheck[np.logical_not(lgbad)] ycheck = ycheck[np.logical_not(lgbad)] # threshold from HybridTrack options low_threshold_kev = self.options.low_threshold_kev # see what pixels are over the threshold. over_thresh = np.array([ self.original_image_kev[xcheck[i], ycheck[i]] > low_threshold_kev for i in xrange(len(xcheck)) ]) # in order to avoid counting pixels from a separate segment, # start from end_coordinates and count outward until you hit a 0. over_thresh_pix = 1 start_ind = np.nonzero((xcheck == self.end_coordinates[0]) & (ycheck == self.end_coordinates[1]))[0][0] # +dx, +dy side (start_ind+1 --> end): for i in xrange(start_ind + 1, len(xcheck), 1): if over_thresh[i]: over_thresh_pix += 1 else: break # -dx, -dy side (start_ind-1 --> 0): for i in xrange(start_ind - 1, -1, -1): if over_thresh[i]: over_thresh_pix += 1 else: break over_thresh_length = (over_thresh_pix * self.options.pixel_size_um * np.sqrt(dx**2 + dy**2)) if over_thresh_length > problem_length: # have we done this too much already? if self.options.ridge_starting_distance_from_track_end_um < 30: raise CheckSegmentBoxError("Couldn't get a clean end segment") # try again, with a shorter track segment self.options.ridge_starting_distance_from_track_end_um -= 10.5 # now, repeat what we've done so far hybridtrack.choose_initial_end(self.original_image_kev, self.options, self.info) self.get_segment_initial_values() self.get_segment_box() self.check_segment_box() # recurse until ridge_starting_dist... < 30 def get_pixlist(self): """ get list of pixels that are inside the box (and within image bounds) """ # logical array representing pixels in the segment if not self.is45: segment_lg = ((self.xmesh >= np.min(self.box_x)) & (self.xmesh <= np.max(self.box_x)) & (self.ymesh >= np.min(self.box_y)) & (self.ymesh <= np.max(self.box_y))) else: # need to compose the lines which form bounding box pairs = ((0, 1), (1, 2), (2, 3), (3, 0)) m = np.zeros(4) b = np.zeros(4) for i in xrange(len(pairs)): # generate the m, b for y = mx+b for line connecting this # pair of points m[i] = ((self.box_y[pairs[i][1]] - self.box_y[pairs[i][0]]) / (self.box_x[pairs[i][1]] - self.box_x[pairs[i][0]])) b[i] = self.box_y[pairs[i][0]] - m[i] * self.box_x[pairs[i][0]] # m should be alternating sign... (this is for testing) assert m[0] * m[1] == -1 assert m[1] * m[2] == -1 assert m[2] * m[3] == -1 assert m[3] * m[0] == -1 min_ind = np.zeros(2) max_ind = np.zeros(2) if b[0] < b[2]: min_ind[0] = 0 max_ind[0] = 2 else: min_ind[0] = 2 max_ind[0] = 0 if b[1] < b[3]: min_ind[1] = 1 max_ind[1] = 3 else: min_ind[1] = 3 max_ind[1] = 1 segment_lg = ( (self.ymesh >= m[min_ind[0]] * self.xmesh + b[min_ind[0]]) & (self.ymesh >= m[min_ind[1]] * self.xmesh + b[min_ind[1]]) & (self.ymesh <= m[max_ind[0]] * self.xmesh + b[max_ind[0]]) & (self.ymesh <= m[max_ind[1]] * self.xmesh + b[max_ind[1]])) xpix = self.xmesh[segment_lg] ypix = self.ymesh[segment_lg] self.xpix = xpix self.ypix = ypix self.segment_lg = segment_lg def get_segment_image(self): """ Produce the actual segment image using xpix and ypix """ # initialize segment image min_x = np.min(self.xpix) max_x = np.max(self.xpix) min_y = np.min(self.ypix) max_y = np.max(self.ypix) seg_img = np.zeros((max_x - min_x + 1, max_y - min_y + 1)) # fill segment image for i in xrange(len(self.xpix)): xi = self.xpix[i] - min_x yi = self.ypix[i] - min_y seg_img[xi, yi] = self.original_image_kev[self.xpix[i], self.ypix[i]] self.end_segment_image = seg_img self.end_segment_offsets = np.array([min_x, min_y]) def separate_segments(self): """ Perform image segmentation on the "segment image", and remove any segments that aren't the right part of the track. """ # binary image binary_segment_image = (self.end_segment_image > self.options.low_threshold_kev) # segmentation: labeled regions, 8-connectivity labels = morph.label(binary_segment_image, connectivity=2) x1 = self.end_coordinates[0] - self.end_segment_offsets[0] y1 = self.end_coordinates[1] - self.end_segment_offsets[1] x2 = self.start_coordinates[0] - self.end_segment_offsets[0] y2 = self.start_coordinates[1] - self.end_segment_offsets[1] chosen_label = labels[x1, y1] if labels[x2, y2] != chosen_label: # this happens with 4-connectivity. need to use 8-connectivity raise RuntimeError('What the heck happened?') binary_again = (labels == chosen_label) # dilate this region, in order to capture information below threshold # (it won't include the other regions, because there must be a gap # between) pix_to_keep = morph.binary_dilation(binary_again) self.end_segment_image[np.logical_not(pix_to_keep)] = 0 def check_segment_indicators(self): """ Check the number of pixels above threshold along the edge of the segment image. Also measure number of separate segments of pixels along the edge of the segment image. """ x1 = self.end_segment_offsets[0] x2 = x1 + self.end_segment_image.shape[0] y1 = self.end_segment_offsets[1] y2 = y1 + self.end_segment_image.shape[1] segment_lg = self.segment_lg[x1:x2, y1:y2] # in end segment coords edge_pixels = segment_lg - morph.binary_erosion(segment_lg) # edge_pixel_count and edge_pixel_segments edge_pixels_over_thresh_image = np.zeros_like(self.end_segment_image) edge_pixels_over_thresh_image[edge_pixels] = ( self.end_segment_image[edge_pixels] > self.options.low_threshold_kev) self.edge_pixel_count = np.sum(edge_pixels_over_thresh_image).astype( int) _, self.edge_pixel_segments = morph.label( edge_pixels_over_thresh_image, connectivity=2, return_num=True) # edge_avg_dist edge_values_image = np.zeros_like(self.end_segment_image) edge_values_image[edge_pixels] = self.end_segment_image[edge_pixels] max_edge_ind_flat = np.argmax(edge_values_image) max_coords = np.unravel_index(max_edge_ind_flat, edge_values_image.shape) # debug assert edge_values_image[max_coords[0], max_coords[1]] == np.max(edge_values_image) dist_sum = 0 x, y = np.nonzero(edge_pixels) for i in xrange(len(x)): dist_sum += (self.end_segment_image[x[i], y[i]] * np.sqrt((max_coords[0] - x[i])**2 + (max_coords[1] - y[i])**2)) self.edge_avg_dist = dist_sum / np.sum(edge_values_image) def segment_initial_end(self): """ Get the image segment to use for moments, and the rough direction estimate. Calls get_segment_box(), get_pixlist(), get_segment_image(). """ self.get_segment_initial_values() self.get_segment_box() self.check_segment_box() self.get_pixlist() self.get_segment_image() self.separate_segments() self.check_segment_indicators() def end_segment_coords_to_full_image_coords(xy): """ Convert x,y from the coordinate frame of the end segment image to the coordinate frame of the full image """ x, y = xy_split(xy) return np.array([ x + self.end_segment_offsets[0], y + self.end_segment_offsets[1] ]) self.segment_to_full = end_segment_coords_to_full_image_coords @classmethod def get_base_diagonal_pixlist(cls, diag_hw, diag_len): """ Get the diagonal pixel list for 45 degrees. (To be rotated to other angles) """ xlist = [] ylist = [] # major diagonals xy0 = np.array([0, 0]) xy1 = np.array([diag_len, diag_len]) for i in range(-diag_hw, diag_hw): offset = np.array([i, -i]) xt, yt = cls.list_diagonal_pixels(xy0 + offset, xy1 + offset) xlist += xt ylist += yt # minor diagonals xy0 = np.array([0, 1]) xy1 = np.array([diag_len - 1, diag_len]) for i in range(-diag_hw + 1, diag_hw): offset = np.array([i, -i]) xt, yt = cls.list_diagonal_pixels(xy0 + offset, xy1 + offset) xlist += xt ylist += yt return np.array(xlist), np.array(ylist) @classmethod def list_diagonal_pixels(cls, xy0, xy1): """ Return xlist, ylist, which list all the pixels on the 45-degree diagonal between xy0 and xy1. """ dxy = [np.sign(xy1[0] - xy0[0]), np.sign(xy1[1] - xy0[1])] xlist = range(xy0[0], xy1[0], dxy[0]) ylist = range(xy0[1], xy1[1], dxy[1]) return xlist, ylist def get_coordlist(self): self.clist0 = CoordinatesList.from_image(self.end_segment_image) def compute_first_moments(self): self.first_moments = get_moments(self.clist0, maxmoment=1) def compute_central_moments(self): self.xoffset = self.first_moments[1, 0] / self.first_moments[0, 0] self.yoffset = self.first_moments[0, 1] / self.first_moments[0, 0] self.clist1 = CoordinatesList.from_clist(self.clist0, xoffset=self.xoffset, yoffset=self.yoffset) self.central_moments = get_moments(self.clist1, maxmoment=3) def central_coords_to_end_segment_coords(xy): """ Convert x,y from the coordinate frame of the central moments to the coordinate frame of the end segment image """ x, y = xy_split(xy) return np.array([x + self.xoffset, y + self.yoffset]) self.central_to_segment = central_coords_to_end_segment_coords def central_coords_to_full_image_coords(xy): """ Convert x,y from the coordinate frame of the central moments to the coordinate frame of the end segment image """ return self.segment_to_full(self.central_to_segment(xy)) self.central_to_full = central_coords_to_full_image_coords def compute_optimal_rotation_angle(self): numerator = 2 * self.central_moments[1, 1] denominator = self.central_moments[2, 0] - self.central_moments[0, 2] theta0 = 0.5 * np.arctan(numerator / denominator) # four possible quadrants theta = np.array([0, np.pi / 2, np.pi, 3 * np.pi / 2]) + theta0 rotated_clists = [ CoordinatesList.from_clist(self.clist1, rotation_rad=t) for t in theta ] rotated_moments = [ get_moments(this_clist, maxmoment=3) for this_clist in rotated_clists ] # condition A: x-axis is longer than y-axis condA = np.array([m[2, 0] - m[0, 2] > 0 for m in rotated_moments]) # condition B: direction of rough estimate dtheta = theta - self.rough_est dtheta[dtheta > np.pi] -= 2 * np.pi dtheta[dtheta < -np.pi] += 2 * np.pi condB = np.abs(dtheta) <= np.pi / 2 # choose cond_both = condA & condB if not np.any(cond_both): raise MomentsError('Rotation quadrant conditions not met') elif np.sum(cond_both) > 1: pass # should throw out this event. # raise MomentsError( # 'Rotation quadrant conditions met more than once') chosen_ind = np.nonzero(cond_both)[0][0] # 1st dim, 1st entry self.rotation_angle = theta[chosen_ind] self.clist2 = rotated_clists[chosen_ind] self.rotated_moments = rotated_moments[chosen_ind] def rotated_coords_to_central_coords(xy): """ Convert x,y from the coordinate frame of the rotated moments to the coordinate frame of the central moments """ x, y = xy_split(xy) t = self.rotation_angle # rotate "forward" because CoordList rotates "backward" x1 = x * np.cos(t) - y * np.sin(t) y1 = x * np.sin(t) + y * np.cos(t) return np.array([x1, y1]) self.rotated_to_central = rotated_coords_to_central_coords def rotated_coords_to_end_segment_coords(xy): """ Convert x,y from the coordinate frame of the rotated moments to the coordinate frame of the end segment image. """ return self.central_to_segment(self.rotated_to_central(xy)) self.rotated_to_segment = rotated_coords_to_end_segment_coords def rotated_coords_to_full_image_coords(xy): """ Convert x,y from the coordinate frame of the rotated moments to the coordinate frame of the full image. """ return self.segment_to_full(self.rotated_to_segment(xy)) self.rotated_to_full = rotated_coords_to_full_image_coords def compute_arc_parameters(self): C_fit = -8.5467 T = self.rotated_moments phi = C_fit * ((np.sqrt(T[0, 0]) * T[2, 1]) / (T[2, 0] - T[0, 2])**1.5) q1 = np.sqrt(2 - 2 * np.cos(phi) - phi * np.sin(phi)) q2 = np.sqrt((T[2, 0] - T[0, 2]) / T[0, 0]) q3 = np.sqrt(T[0, 0]**3 / (T[2, 0] - T[0, 2])) self.R = (phi / q1 * q2) self.Rphi = self.R * phi self.eta0 = (q1 / phi**2) * q3 self.phi = phi self.arc_center = np.array([T[1, 0], T[0, 1]]) / T[0, 0] def compute_direction(self): self.alpha = self.phi / 2 + self.rotation_angle e_theta = np.array( [np.cos(self.rotation_angle), np.sin(self.rotation_angle)]) e2 = np.array([ np.cos(self.rotation_angle + np.pi / 2), np.sin(self.rotation_angle + np.pi / 2) ]) q1 = self.R * np.sin(self.phi / 2) * e_theta # np.sinc is a NORMALIZED sinc function - sin(pi*x)/(pi*x) q2 = self.R * (np.cos(self.phi / 2) - np.sin(self.phi) / self.phi) * e2 self.x0 = self.arc_center - q1 + q2 def compute_pathology(self): # Test 1: The total arc-length should be longer than 3(?) pixels self.arclength = self.Rphi # Test 2: Radius should be much greater than arc-length # self.phi # Test 3: Certain moments are expected to ... be significantly smaller self.pathology_ratio_3a = (self.rotated_moments[1, 2] / self.rotated_moments[2, 1]) self.pathology_ratio_3b = (self.rotated_moments[3, 0] / self.rotated_moments[0, 3])