def test_OpenImage(self): """Testing ROI open functionality""" tempdir = mkdtemp() # crop the test image (size=1388x1040) to be bottom right tile of the test image # tiled with 6 cols and 5 rows => ROI= 231x208+1155+832 orig_big = join( pychrm_test_dir, 'lymphoma_eosin_channel_MCL_test_img_sj-05-3362-R2_001_E.tif') orig_cropped = join( pychrm_test_dir, 'lymphoma_eosin_channel_MCL_test_img_sj-05-3362-R2_001_E-t6x5_5_4-l.tiff' ) test_cropped_path = join(tempdir, 'TEST_OpenImageROI.tif') x = 1155 y = 832 w = 231 h = 208 from wndcharm import rect bb = rect() bb.x = x bb.y = y bb.w = w bb.h = h # setting mean = 0 is flag to not use mean in ImageMatrix::OpenImage() downsample = 0 #bb = None mean = 0.0 stddev = 0.0 try: cropped_on_load_im = PyImageMatrix() if 1 != cropped_on_load_im.OpenImage(orig_big, downsample, bb, mean, stddev): self.fail('Could not build an ImageMatrix from ' + orig_big) cropped_on_load_im.SaveTiff(test_cropped_path) orig_pixels = plt.imread(orig_cropped) cropped_pixels = plt.imread(test_cropped_path) assert_equal(orig_pixels, cropped_pixels) finally: rmtree(tempdir)
def GenerateFeatures( self, write_to_disk=True, quiet=True ): """@brief Loads precalculated features, or calculates new ones, based on which instance attributes have been set, and what their values are. write_to_disk (bool) - save features to text file which by convention has extension ".sig" Returns self for convenience.""" # 0: What features does the user want? # 1: are there features already calculated somewhere? # 2: if so are they current/complete/expected/correct? # 3: if not, what's left to calculate? # 4: Calculate the rest # 5: Reduce the features down to what the user asked for if self.values is not None and len( self.values ) != 0: return self # Make sure Feature Vector version string is correct, etc: #self.Update() partial_load = False try: self.LoadSigFile( quiet=quiet ) # FIXME: Here's where you'd calculate a small subset of features # and see if they match what was loaded from file. The file could be corrupted # incomplete, or calculated with different options, e.g., -S1441 return self except IOError: # File doesn't exist pass except WrongFeatureSetVersionError: # File has different feature version than desired pass except IncompleteFeatureSetError: # LoadSigFile should create a FeatureComputationPlan if not quiet: print 'Loaded {0} features from disk for sample "{1}"'.format( len( self.temp_names ), self.name ) partial_load = True pass # All hope is lost, calculate features. # Use user-assigned feature computation plan, if provided: if self.feature_computation_plan != None: comp_plan = self.feature_computation_plan # I Commented the following out because the computation plan may only reflect # the subset of features that haven't been calculated yet: # comp_plan.feature_vec_type seems to only contain the minor version # i.e., number after the '.'. Assume major version is the latest. #self.feature_set_version = '{0}.{1}'.format( # feature_vector_major_version, comp_plan.feature_vec_type ) else: major, minor = self.feature_set_version.split('.') if minor == '0': comp_plan = GenerateFeatureComputationPlan( self.feature_names ) elif minor == '1': comp_plan = wndcharm.StdFeatureComputationPlans.getFeatureSet() elif minor == '2': comp_plan = wndcharm.StdFeatureComputationPlans.getFeatureSetLong() elif minor == '3': comp_plan = wndcharm.StdFeatureComputationPlans.getFeatureSetColor() elif minor == '4': comp_plan = wndcharm.StdFeatureComputationPlans.getFeatureSetColorLong() else: raise ValueError( "Not sure which features you want." ) self.feature_computation_plan = comp_plan # Here are the ImageMatrix API calls: # void normalize(double min, double max, long range, double mean, double stddev); # int OpenImage(char *image_file_name, int downsample, rect *bounding_rect, double mean, double stddev); # void Rotate (const ImageMatrix &matrix_IN, double angle); if self.rot is not None: raise NotImplementedError( "FIXME: Implement rotations." ) if self.x is not None and self.y is not None and self.w is not None and self.h is not None: bb = wndcharm.rect() bb.x = self.x bb.y = self.y bb.w = self.w bb.h = self.h else: bb = None if self.pixel_intensity_mean: mean = self.pixel_intensity_mean # stddev arg only used in ImageMatrix::OpenImage() if mean is set stddev = self.pixel_intensity_stddev else: # setting mean = 0 is flag to not use mean in ImageMatrix::OpenImage() mean = 0 stddev = 0 from .PyImageMatrix import PyImageMatrix if isinstance( self.source_filepath, str ): the_tiff = PyImageMatrix() if 1 != the_tiff.OpenImage( self.source_filepath, self.downsample, bb, mean, stddev ): raise ValueError( 'Could not build an ImageMatrix from {0}, check the path.'.\ format( self.source_filepath ) ) elif isinstance( self.source_filepath, wndcharm.ImageMatrix ): if self.downsample or mean: raise NotImplementedError( 'still need to implement modifying open pixel plane with downsample, mean or stddev' ) if not bb: the_tiff = self.source_filepath else: # API calls for copying desired pixels into empty ImageMatrix instance: # the_tiff is garbage collected on return the_tiff = PyImageMatrix() # bb only used when calling OpenImage # ImageMatrix::submatrix() has a funky signature: # void ImageMatrix::submatrix (const ImageMatrix &matrix, const unsigned int x1, const unsigned int y1, const unsigned int x2, const unsigned int y2); # where x2 and y2 are INCLUSIVE, i.e., must subtract 1 from both x1 = self.x y1 = self.y x2 = x1 + self.w - 1 y2 = y1 + self.h - 1 if 1 != the_tiff.submatrix( self.source_filepath, x1, y1, x2, y2 ): raise ValueError( 'Could not crop bounding box ({0},{1}),({2},{3}) from image "{4}"'.\ format( x1, y1, x2, y2, self.source_filepath.source ) ) else: raise ValueError("image parameter 'image_path_or_mat' is not a string or a wndcharm.ImageMatrix") # pre-allocate space where the features will be stored (C++ std::vector<double>) tmp_vec = wndcharm.DoubleVector( comp_plan.n_features ) # Get an executor for this plan and run it plan_exec = wndcharm.FeatureComputationPlanExecutor( comp_plan ) plan_exec.run( the_tiff, tmp_vec, 0 ) # get the feature names from the plan comp_names = [ comp_plan.getFeatureNameByIndex(i) for i in xrange( comp_plan.n_features ) ] # convert std::vector<double> to native python list of floats comp_vals = list( tmp_vec ) # Feature Reduction/Reorder step: # Feature computation may give more features than are asked for by user, or out of order. if self.feature_names: if self.feature_names != comp_names: if partial_load: # If we're here, we've already loaded some but not all of the features # we need. Take what we've already loaded and slap it at the end # of what was calculated. Doesn't matter if some of the features are # redundant, because the .index() method returns the first item it finds. # FIXME: if there is overlap between what was loaded and what was # calculated, check to see that they match. comp_names.extend( self.temp_names ) comp_vals.extend( self.temp_values ) del self.temp_names del self.temp_values self.values = np.array( [ comp_vals[ comp_names.index( name ) ] for name in self.feature_names ] ) else: self.feature_names = comp_names self.values = comp_vals if not quiet: if len( comp_vals ) != len( self ): print "CALCULATED {0} TOTAL FEATURES, REDUCED TO: {1}".format( len( comp_vals ), self ) else: print "CALCULATED: " + str( self ) # FIXME: maybe write to disk BEFORE feature reduce? Provide flag to let user decide? if write_to_disk: self.ToSigFile( quiet=quiet ) # Feature names need to be modified for their sampling options. # Base case is that channel goes in the innermost parentheses, but really it's not # just channel, but all sampling options. # For now, let the FeatureSpace constructor code handle the modification of feature names # for its own self.feature_names return self