def classifyImage(self, imageFilename, classifiedFilename): """ Classify minerals in an AVIRIS image using spectral angle mapper classification and save the results to a file. Args: imageFilename (str): filename of the image to be classified classifiedFilename (str): filename of the classified image Returns: None """ # open the image image = spectral.open_image(imageFilename) if self.inMemory: data = image.load() else: data = image.asarray() M = image.shape[0] N = image.shape[1] # define a resampler # TODO detect and scale units # TODO band resampler should do this resample = spectral.BandResampler([x/1000 for x in image.bands.centers], self.library.bands.centers) # allocate a zero-initialized MxN array for the classified image classified = numpy.zeros(shape=(M,N), dtype=numpy.uint16) # for each pixel in the image for x in range(M): for y in range(N): # read the pixel from the file pixel = data[x,y] # if it is not a no data pixel if not numpy.isclose(pixel[0], -0.005) and not pixel[0]==-50: # resample the pixel ignoring NaNs from target bands that don't overlap # TODO fix spectral library so that bands are in order resampledPixel = numpy.nan_to_num(resample(pixel)) # calculate spectral angles angles = spectral.spectral_angles(resampledPixel[numpy.newaxis, numpy.newaxis, ...], self.library.spectra) # normalize confidence values from [pi,0] to [0,1] for z in range(angles.shape[2]): angles[0,0,z] = 1-angles[0,0,z]/math.pi # get index of class with largest confidence value indexOfMax = numpy.argmax(angles) # classify pixel if confidence above threshold if angles[0,0,indexOfMax] > self.threshold: # index from one (after zero for no data) classified[x,y] = indexOfMax + 1 # save the classified image to a file spectral.io.envi.save_classification( classifiedFilename, classified, class_names=['No data']+self.library.names, metadata={ 'data ignore value': 0, 'description': 'COAL '+pycoal.version+' mineral classified image.', 'map info': image.metadata.get('map info') }) # remove unused classes from the image pycoal.mineral.MineralClassification.filterClasses(classifiedFilename)
def classify_image(self, image_file_name, classified_file_name, classifier_type=SAM): """ Classify minerals in an AVIRIS image using specified classifier_type, either SAM, GML, KMeans, MDC. If none, defaults to spectral angle mapper classification and save the results to a file. Args: image_file_name (str): filename of the image to be classified classified_file_name (str): filename of the classified image classifier_type (str): Classifier type used Returns: None """ start = time.time() logging.info("Starting Mineral Classification for image '%s', saving classified image to '%s'" %(image_file_name, classified_file_name)) # open the image image = spectral.open_image(image_file_name) if self.in_memory: data = image.load() else: data = image.asarray() M = image.shape[0] N = image.shape[1] # define a resampler # TODO detect and scale units # TODO band resampler should do this resample = spectral.BandResampler([x/1000 for x in image.bands.centers], self.library.bands.centers) if classifier_type == 'SAM': # this will run the default value, the Spectral angle mapper. # allocate a zero-initialized MxN array for the classified image classified = numpy.zeros(shape=(M,N), dtype=numpy.uint16) # for each pixel in the image for x in range(M): for y in range(N): # read the pixel from the file pixel = data[x,y] # if it is not a no data pixel if not numpy.isclose(pixel[0], -0.005) and not pixel[0]==-50: # resample the pixel ignoring NaNs from target bands that don't overlap # TODO fix spectral library so that bands are in order resampled_pixel = numpy.nan_to_num(resample(pixel)) # calculate spectral angles angles = spectral.spectral_angles(resampled_pixel[numpy.newaxis, numpy.newaxis, ...], self.library.spectra) # normalize confidence values from [pi,0] to [0,1] for z in range(angles.shape[2]): angles[0,0,z] = 1-angles[0,0,z]/math.pi # get index of class with largest confidence value index_of_max = numpy.argmax(angles) # classify pixel if confidence above threshold if angles[0,0,index_of_max] > self.threshold: # index from one (after zero for no data) classified[x,y] = index_of_max + 1 elif classifier_type == 'GML': # Gaussian Maximum Likelihood # the following is the method layed out in the spectral python documentation, needs to be converted to work with our formatting # create classifier gmlc = GaussianClassifier(classes) # classify our training image and display the resulting classification map. clmap = gmlc.classify_image(image) v = imshow(classes=clmap) # mask out all the pixels not associated with a training class gtresults = clmap * (gt != 0) v = imshow(classes=gtresults) elif classifier_type == 'KMeans': # K-means generates clusters, this will read in pixels and figure out nearest cluster center # Currently only setup to run using default values of 20 iterations and 10 clusters # the following is the method layed out in the spectral python documentation, needs to be converted to work with our formatting for x in range(20) (m, c) = kmeans(imgage, 20, 30) # save the classified image to a file # save the classified image to a file spectral.io.envi.save_classification( classified_file_name, classified, class_names=['No data']+self.library.names, metadata={ 'data ignore value': 0, 'description': 'COAL '+pycoal.version+' mineral classified image.', 'map info': image.metadata.get('map info') }) # remove unused classes from the image pycoal.mineral.MineralClassification.filter_classes(classified_file_name) end = time.time() seconds_elapsed = end - start m, s = divmod(seconds_elapsed, 60) h, m = divmod(m, 60) logging.info("Completed Mineral Classification. Time elapsed: '%d:%02d:%02d'" % (h, m, s)) @staticmethod def filter_classes(classified_file_name): """ Modify a classified image to remove unused classes. Args: classified_file_name (str): file of the classified image Returns: None """ # open the image classified = spectral.open_image(classified_file_name) data = classified.asarray() M = classified.shape[0] N = classified.shape[1] # allocate a copy for reindexed pixels copy = numpy.zeros(shape=(M,N), dtype=numpy.uint16) # find classes actually present in the image classes = sorted(set(classified.asarray().flatten().tolist())) lookup = [classes.index(i) if i in classes else 0 for i in range(int(classified.metadata.get('classes')))] # reindex each pixel for x in range(M): for y in range(N): copy[x,y] = lookup[data[x,y,0]] # overwrite the file spectral.io.envi.save_classification( classified_file_name, copy, force=True, class_names=[classified.metadata.get('class names')[i] for i in classes], metadata=classified.metadata) @staticmethod def to_rgb(image_file_name, rgb_image_file_name, red=680.0, green=532.5, blue=472.5): """ Generate a three-band RGB image from an AVIRIS image and save it to a file. Args: image_file_name (str): filename of the source image rgb_image_file_name (str): filename of the three-band RGB image red (float, optional): wavelength in nanometers of the red band green (float, optional): wavelength in nanometers of the green band blue (float, optional): wavelength in nanometers of the blue band Returns: None """ # find the index of the first element in a list greater than the value start = time.time() logging.info("Starting generation of three-band RGB image from input file: '%s' with following RGB values R: '%s', G: '%s', B: '%s'" %(image_file_name, red, green, blue)) def index_of_greater_than(elements, value): for index,element in enumerate(elements): if element > value: return index # open the image image = spectral.open_image(image_file_name) # load the list of wavelengths as floats wavelength_strings = image.metadata.get('wavelength') wavelength_floats = list(map(float, wavelength_strings)) # find the index of the red, green, and blue bands red_index = index_of_greater_than(wavelength_floats, red) green_index = index_of_greater_than(wavelength_floats, green) blue_index = index_of_greater_than(wavelength_floats, blue) # read the red, green, and blue bands from the image red_band = image[:,:,red_index] green_band = image[:,:,green_index] blue_band = image[:,:,blue_index] # remove no data pixels for band in [red_band, green_band, blue_band]: for x in range(band.shape[0]): for y in range(band.shape[1]): if numpy.isclose(band[x,y,0], -0.005) or band[x,y,0]==-50: band[x,y] = 0 # combine the red, green, and blue bands into a three-band RGB image rgb = numpy.concatenate([red_band,green_band,blue_band], axis=2) # update the metadata rgb_metadata = image.metadata rgb_metadata['description'] = 'COAL '+pycoal.version+' three-band RGB image.' rgb_metadata['data ignore value'] = 0 if wavelength_strings: rgb_metadata['wavelength'] = [ wavelength_strings[red_index], wavelength_strings[green_index], wavelength_strings[blue_index]] if image.metadata.get('correction factors'): rgb_metadata['correction factors'] = [ image.metadata.get('correction factors')[red_index], image.metadata.get('correction factors')[green_index], image.metadata.get('correction factors')[blue_index]] if image.metadata.get('fwhm'): rgb_metadata['fwhm'] = [ image.metadata.get('fwhm')[red_index], image.metadata.get('fwhm')[green_index], image.metadata.get('fwhm')[blue_index]] if image.metadata.get('bbl'): rgb_metadata['bbl'] = [ image.metadata.get('bbl')[red_index], image.metadata.get('bbl')[green_index], image.metadata.get('bbl')[blue_index]] if image.metadata.get('smoothing factors'): rgb_metadata['smoothing factors'] = [ image.metadata.get('smoothing factors')[red_index], image.metadata.get('smoothing factors')[green_index], image.metadata.get('smoothing factors')[blue_index]] # save the three-band RGB image to a file logging.info("Saving RGB image as '%s'" % rgb_image_file_name) spectral.envi.save_image(rgb_image_file_name, rgb, metadata=rgb_metadata) end = time.time() seconds_elapsed = end - start m, s = divmod(seconds_elapsed, 60) h, m = divmod(m, 60) logging.info("Completed RGB image generation. Time elapsed: '%d:%02d:%02d'" % (h, m, s)) @staticmethod def subset_spectral_library(spectral_library, class_names): # adapted from https://git.io/v9ThM """ Create a copy of the spectral library containing only the named classes. Args: spectral_library (SpectralLibrary): ENVI spectral library class_names (str[]): list of names of classes to include Returns: SpectralLibrary: subset of ENVI spectral library """ # empty array for spectra spectra = numpy.empty((len(class_names), len(spectral_library.bands.centers))) # empty list for names names = [] # copy class spectra and names for new_index, class_name in enumerate(class_names): old_index = spectral_library.names.index(class_name) spectra[new_index] = spectral_library.spectra[old_index] names.append(class_name) # copy metadata metadata = {'wavelength units': spectral_library.metadata.get('wavelength units'), 'spectra names': names, 'wavelength': spectral_library.bands.centers } # return new spectral library return spectral.io.envi.SpectralLibrary(spectra, metadata, {})
def selectSpectra(Type, sensor, n=0, bands=None): """Subsets the earthlib spectral endmember library. Selects endmembers from specific class, then resamples the spectra to the wavelengths of a specific satellite sensor. This also performs random spectra selection. Args: Type: the type of spectra to select. sensor: the sensor type to resample wavelengths to. n: the number of random spectra to sample. n=0 returns all spectra. bands: list of bands to use. Accepts 0-based indices or a list of band names (e.g. ["B2", "B3", "B4"]). Returns: spectra: list of spectral endmembers resampled to a specific sensor's wavelengths. """ import spectral from . import Read # get the level of the group selected level = getTypeLevel(Type) if level == 0: LOGGER.warning( f"Invalid group parameter: {Type}. Get valid values from earthlib.listTypes()." ) return None # qc the collection selected if sensor not in listSensors(): LOGGER.warning( f"Invalid sensor parameter: {sensor}. Get valid values from earthlib.listSensors()." ) return None # read the spectral library into memory endmembers = Read.spectralLibrary(_endmember_path) # subset to specific bands, if set if bands is None: bands = range(len(getBands(sensor))) else: if type(bands[0]) is str: bands = getBandIndices(bands, sensor) # create a band resampler for this collection sensor_centers = np.array(collections[sensor]["band_centers"])[bands] sensor_fwhm = np.array(collections[sensor]["band_widths"])[bands] resampler = spectral.BandResampler(endmembers.band_centers, sensor_centers, fwhm2=sensor_fwhm) # select the endmembers from just the type passed key = f"LEVEL_{level}" indices = metadata[key] == Type spectra_raw = endmembers.spectra[indices, :] # subset them further if the n parameter is passed if n > 0: random_indices = np.random.randint(indices.sum(), size=n) spectra_raw = spectra_raw[random_indices, :] # loop through each spectrum and resample to the sensor wavelengths resampled = list() for i in range(spectra_raw.shape[0]): spectrum = resampler(spectra_raw[i, :]) resampled.append(spectrum) return resampled
def SAM(image_file_name, classified_file_name, library_file_name, scores_file_name=None, class_names=None, threshold=0.0, in_memory=False): """ Parameter 'scores_file_name' optionally receives the path to where to save an image that holds all the SAM scores yielded for each pixel of the classified image. No score image is create if not provided. The optional 'threshold' parameter defines a confidence value between zero and one below which SAM classifications will be discarded, otherwise all classifications will be included. In order to improve performance on systems with sufficient memory, enable the optional parameter to load entire images. Args: library_file_name (str): filename of the spectral library image_file_name (str): filename of the image to be classified classified_file_name (str): filename of the classified image scores_file_name (str, optional): filename of the image to hold each pixel's classification score class_names (str[], optional): list of classes' names to include threshold (float, optional): classification threshold in_memory (boolean, optional): enable loading entire image Returns: None """ # load and optionally subset the spectral library library = spectral.open_image(library_file_name) if class_names is not None: library = pycoal.mineral.MineralClassification.subset_spectral_library( library, class_names) # open the image image = spectral.open_image(image_file_name) if in_memory: data = image.load() else: data = image.asarray() M = image.shape[0] N = image.shape[1] # define a resampler # TODO detect and scale units # TODO band resampler should do this resample = spectral.BandResampler([x / 1000 for x in image.bands.centers], library.bands.centers) # allocate a zero-initialized MxN array for the classified image classified = numpy.zeros(shape=(M, N), dtype=numpy.uint16) if scores_file_name is not None: # allocate a zero-initialized MxN array for the scores image scored = numpy.zeros(shape=(M, N), dtype=numpy.float64) # for each pixel in the image for x in range(M): for y in range(N): # read the pixel from the file pixel = data[x, y] # if it is not a no data pixel if not numpy.isclose(pixel[0], -0.005) and not pixel[0] == -50: # resample the pixel ignoring NaNs from target bands that don't overlap # TODO fix spectral library so that bands are in order resampled_pixel = numpy.nan_to_num(resample(pixel)) # calculate spectral angles angles = spectral.spectral_angles( resampled_pixel[numpy.newaxis, numpy.newaxis, ...], library.spectra) # normalize confidence values from [pi,0] to [0,1] for z in range(angles.shape[2]): angles[0, 0, z] = 1 - angles[0, 0, z] / math.pi # get index of class with largest confidence value index_of_max = numpy.argmax(angles) # get confidence value of the classied pixel score = angles[0, 0, index_of_max] # classify pixel if confidence above threshold if score > threshold: # index from one (after zero for no data) classified[x, y] = index_of_max + 1 if scores_file_name is not None: # store score value scored[x, y] = score # save the classified image to a file spectral.io.envi.save_classification( classified_file_name, classified, class_names=['No data'] + library.names, metadata={ 'data ignore value': 0, 'description': 'COAL ' + pycoal.version + ' mineral classified image.', 'map info': image.metadata.get('map info') }) # remove unused classes from the image pycoal.mineral.MineralClassification.filter_classes(classified_file_name) if scores_file_name is not None: # save the scored image to a file spectral.io.envi.save_image( scores_file_name, scored, dtype=numpy.float64, metadata={ 'data ignore value': -50, 'description': 'COAL ' + pycoal.version + ' mineral scored image.', 'map info': image.metadata.get('map info') })
# load library library_filename = '../pycoal/tests/s06av95a_envi.hdr' library = spectral.open_image(library_filename) # open the image image_filename = '../pycoal/tests/images/ang20150422t163638_corr_v1e_img_4000-4010_550-560.hdr' image = spectral.open_image(image_filename) # access a pixel known x = 4 y = 7 pixel = image[x, y] # define a resampler resample = spectral.BandResampler([x / 1000 for x in image.bands.centers], library.bands.centers) # resample the pixel resampled_pixel = numpy.nan_to_num(resample(pixel)) # calculate spectral angles angles = spectral.spectral_angles( resampled_pixel[numpy.newaxis, numpy.newaxis, ...], library.spectra) # normalize confidence values from [pi,0] to [0,1] for z in range(angles.shape[2]): angles[0, 0, z] = 1 - angles[0, 0, z] / math.pi # get angles, classes, and indices angle_list = list(numpy.ndarray.flatten(angles)) angle_class_list = zip(angle_list, library.names, range(0, len(library.names)))
def resample_variable(data, w, sensor): # 08/07/2011: faster way, but gives slightly different results worig = range(400, 2401) (wsensor, fwhm) = read_band_centre_fwhm_data(sensor, worig) resample = spectral.BandResampler(w, wsensor, fwhm2=fwhm) return resample(data)