def get_predicts(num: Union[int, list], bands: tuple, predicted_values: list, results: dict) -> list: """ Return the model prediction values in the time series for a particular band or bands Args: num: Returns: A list of segment models """ # Check for type int, create list if true if isinstance(num, int): num = [num] try: _predicts = [ predicted_values[m * len(bands) + n] for n in num for m in range(len(results["change_models"])) ] except (IndexError, TypeError) as e: log.error('Exception: %s' % e, exc_info=True) _predicts = [] return _predicts
def export_data(self): """ Export the currently plotted data to an output CSV that will be saved in the specified working directory. """ data = dict() for item in self.item_list: if item in aliases.keys(): group = aliases[item] for key in group: data[key] = self.ard_observations.pixel_ard[key] try: data['qa'] = self.ard_observations.pixel_ard['qas'] data['dates'] = self.ard_observations.pixel_ard['dates'] except KeyError as _e: log.error('Exception: %s' % _e, exc_info=True) data = pd.DataFrame(data).sort_values('dates').reset_index(drop=True) data['dates'] = data['dates'].apply( lambda x: dt.datetime.fromordinal(x)) data.to_csv( os.path.join( self.working_directory, f'{get_time()}_{self.geo_info.pixel_coord_ul.x}_{self.geo_info.pixel_coord_ul.y}.csv' )) return None
def show_model_params(self, results, geo): """ Print the model results out to the GUI QPlainTextEdit widget Args: results: Class instance containing change model results and parameters geo: Class instance containing geographic info """ log.info( "Plotting for tile H{:02}V{:02} at point ({}, {}) meters".format( geo.H, geo.V, geo.coord.x, geo.coord.y)) try: self.ui.PlainTextEdit_results.appendPlainText( "\n\nBegin Date: {}".format(results.begin)) log.info("Begin Date: {}".format(results.begin)) self.ui.PlainTextEdit_results.appendPlainText( "End Date: {}\n".format(results.end)) log.info("End Date: {}".format(results.end)) for num, result in enumerate(results.results["change_models"]): self.ui.PlainTextEdit_results.appendPlainText( "Result: {}".format(num + 1)) log.info("Result: {}".format(num + 1)) self.ui.PlainTextEdit_results.appendPlainText( "Start Date: {}".format( dt.datetime.fromordinal(result["start_day"]))) log.info("Start Date: {}".format( dt.datetime.fromordinal(result["start_day"]))) self.ui.PlainTextEdit_results.appendPlainText( "End Date: {}".format( dt.datetime.fromordinal(result["end_day"]))) log.info("End Date: {}".format( dt.datetime.fromordinal(result["end_day"]))) self.ui.PlainTextEdit_results.appendPlainText( "Break Date: {}".format( dt.datetime.fromordinal(result["break_day"]))) log.info("Break Date: {}".format( dt.datetime.fromordinal(result["break_day"]))) self.ui.PlainTextEdit_results.appendPlainText("QA: {}".format( result["curve_qa"])) log.info("QA: {}".format(result["curve_qa"])) self.ui.PlainTextEdit_results.appendPlainText( "Change prob: {}\n".format(result["change_probability"])) log.info("Change prob: {}".format( result["change_probability"])) except (ValueError, TypeError) as _e: log.error('Exception: %s' % _e, exc_info=True) pass
def close_ard(self): """ If plotting for a new HV tile, close the previous ARD Viewer window if one was previously opened Returns: None """ try: self.ard.exit() except AttributeError as _e: log.error('Exception: %s' % _e, exc_info=True)
def show_ard(self, clicked_item): """ Display the ARD image clicked on the plot Args: clicked_item: <QListWidgetItem> Passed automatically by the itemClicked method of the QListWidget Returns: None """ match = re.search(r'\d{4}-\D{3}-\d{2}', clicked_item.text()).group() date = dt.datetime.strptime(match, '%Y-%b-%d') try: if self.ard: # Update with the previously selected color channels so they are displayed in the new ARD viewer self.store_r = self.ard.r self.store_g = self.ard.g self.store_b = self.ard.b self.close_ard() self.ard = ChipsViewerX(x=self.geo_info.coord.x, y=self.geo_info.coord.y, date=date, url=self.merlin_url, subplot=self.plot_window.b, geo=self.geo_info, r=self.store_r, g=self.store_g, b=self.store_b, outdir=self.working_directory) self.ard.update_plot_signal.connect(self.update_plot) except (AttributeError, IndexError) as _e: log.error("Display ARD raised an exception: %s" % _e, exc_info=True)
def get_modeled_index(ard, results, predicted_values): """ Calculate the model-predicted index curves Returns: """ bands = ('blue', 'green', 'red', 'nir', 'swir1', 'swir2', 'thermal') indices = ('ndvi', 'msavi', 'evi', 'savi', 'ndmi', 'nbr', 'nbr2') modeled = dict() for key in ard.keys(): if key in indices: new_key = f'{key}-modeled' modeled[new_key] = list() call = index_functions[key]['func'] inds = index_functions[key]['inds'] try: for m in range(len(results['change_models'])): args = tuple([ predicted_values[m * len(bands) + ind] for ind in inds ]) modeled[new_key].append(call(*args)) except (AttributeError, TypeError) as e: log.error('Exception: %s' % e, exc_info=True) modeled[new_key].append([]) return modeled
def save_fig(self): """ Save the current matplotlib figure to a PNG file """ if not os.path.exists(self.ui.LineEdit_outputDir.text()): os.makedirs(self.ui.LineEdit_outputDir.text()) fname = self.fname_generator() # Overwrite the .png if it already exists if os.path.exists(fname): try: os.remove(fname) except (IOError, PermissionError) as _e: log.error('Exception: %s' % _e, exc_info=True) # Make sure the timeseries plot is set as the current figure plt.figure(f'timeseries_figure_{self.fig_num}') plt.savefig(fname, bbox_inches="tight", dpi=150) log.debug("Plot figure saved to file {}".format(fname))
def plot(self): """ TODO: Add descriptive information """ if self.plot_window: self.plot_window.close() self.fig_num += 1 # <list> The bands and/or indices selected for plotting self.item_list = [ str(i.text()) for i in self.ui.ListWidget_items.selectedItems() ] self.geo_info = GeoInfo(x=self.ui.LineEdit_x1.text(), y=self.ui.LineEdit_y1.text(), units=UNITS[self.selected_units]["unit"]) self.cache_data = read_cache(self.geo_info, self.cache_data) self.ard_observations = ARDData(geo=self.geo_info, url=self.merlin_url, items=self.item_list, cache=self.cache_data) self.cache_data = update_cache(self.cache_data, self.ard_observations.cache, self.ard_observations.key) try: self.ccd_results = CCDReader( tile=self.geo_info.tile, chip_coord=self.geo_info.chip_coord_ul, pixel_coord=self.geo_info.pixel_coord_ul, json_dir=self.ccd_directory) except (IndexError, AttributeError, TypeError, ValueError, FileNotFoundError) as _e: log.error('Exception: %s' % _e, exc_info=True) self.ccd_results = None try: self.class_results = SegmentClasses( chip_coord_ul=self.geo_info.chip_coord_ul, class_dir=self.class_directory, rc=self.geo_info.chip_pixel_rowcol, tile=self.geo_info.tile) except (IndexError, AttributeError, TypeError, ValueError, FileNotFoundError) as _e: log.error('Exception: %s' % _e, exc_info=True) self.class_results = None self.plot_specs = PlotSpecs(ard=self.ard_observations.pixel_ard, change=self.ccd_results, segs=self.class_results, items=self.item_list, begin=self.begin, end=self.end) self.ui.PushButton_export.setEnabled(True) # Display change model information for the entered coordinates self.show_model_params(results=self.plot_specs, geo=self.geo_info) """ fig <matplotlib.figure> Matplotlib figure object containing all of the artists artist_map <dict> mapping of each specific PathCollection artist to it's underlying dataset lines_map <dict> mapping of artist lines and points to the legend lines axes <ndarray> 2D array of matplotlib.axes.Axes objects """ self.fig, self.artist_map, self.lines_map, self.axes = make_plots.draw_figure( data=self.plot_specs, items=self.item_list, fig_num=self.fig_num, config=self.plotconfig.opts) if not os.path.exists(self.ui.LineEdit_outputDir.text()): os.makedirs(self.ui.LineEdit_outputDir.text()) # Generate the ESRI point shapefile temp_shp = self.fname_generator(ext=".shp") root, name = os.path.split(temp_shp) root = root + os.sep + "shp" self.get_shp(coords=self.geo_info.coord, out_shp="{}{}{}".format(root, os.sep, name)) # Show the figure in an interactive window self.plot_window = PlotWindow(fig=self.fig, axes=self.axes, artist_map=self.artist_map, lines_map=self.lines_map) self.plot_window.selected_obs.connect(self.connect_plot_selection) self.plot_window.change_symbology.connect(self.change_symbology) # Make these buttons available once a figure has been created self.ui.PushButton_clear.setEnabled(True) self.ui.PushButton_saveFigure.setEnabled(True)
import numpy as np from osgeo import ogr, osr from PyQt5.QtWidgets import QMainWindow, QFileDialog from PyQt5 import QtCore # Tell matplotlib to use the QT5Agg Backend matplotlib.use('Qt5Agg') sys.excepthook = exc_handler try: CONFIG = yaml.load( open(pkg_resources.resource_filename('lcmap_tap', 'config.yaml'))) except FileNotFoundError as e: log.error('Exception: %s' % e, exc_info=True) log.critical( 'Configuration file not present, TAP Tool will not be able to retrieve data. Exiting' ) sys.exit(1) def get_time(): """ Return the current time stamp Returns: A formatted string containing the current date and time
def save_img(self): """ Returns: """ date = self.date.strftime('%Y%m%d') r = self.ui.ComboBox_red.currentText().lower() g = self.ui.ComboBox_green.currentText().lower() b = self.ui.ComboBox_blue.currentText().lower() try: outdir = self.working_dir if r == b and r == g: outfile = os.path.join(outdir, f'{r}_{date}_{get_time()}.png') else: outfile = os.path.join(outdir, f'{r}_{g}_{b}_{date}_{get_time()}.png') fig, ax = plt.subplots(figsize=(10, 10), num=f'ard_figure_{self.fig_num}') # Make sure that the ARD figure is active plt.figure(f'ard_figure_{self.fig_num}') plt.axis('off') ax.grid(False) center = self.chips.tile_geo.chip_coord_ul _date = dt.datetime.fromordinal( self.chips.grid[center]['data'][0][1]['dates'][ self.chips.grid[center]['ind']]).strftime('%Y-%m-%d') title = f'Date: {_date}' text = f'X: {self.x}\nY: {self.y}' ax.set_title(title) ax.imshow(self.chips.rgb, interpolation='nearest') ax.scatter(x=self.col, y=self.row, marker='s', facecolor='none', color='yellow', s=15, linewidth=1) ax.text(0, -.01, text, horizontalalignment='left', verticalalignment='top', transform=ax.transAxes) plt.savefig(outfile, bbox_inches='tight', dpi=200) log.debug("Plot figure saved to file {}".format(outfile)) plt.gcf().clear() self.fig_num += 1 except (TypeError, ValueError) as e: log.error('ARD save_img raised exception: %s' % e, exc_info=True)
def draw_figure(data: PlotSpecs, items: list, fig_num: int, config: dict) -> \ Tuple[matplotlib.figure.Figure, dict, dict, ndarray]: """ Args: data: an instance of the PlotSpecs class, contains all of the plotting attributes and data items: A list of strings representing the subplots to be plotted fig_num: Will be used as a figure identifier config: Plot symbology settings Returns: fig: A matplotlib.figure.Figure object artist_map: A dictionary mapping the data series to plot artists, used for referencing and interactivity lines_map: A dictionary mapping the legend lines to the plot artists they represent, used for interactivity axes: Using squeeze=False, returns a 2D numpy array of matplotlib.axes.Axes objects """ def get_legend_handle(**kwargs): """ A helper function to generate legend handles Args: **kwargs: Line2D keyword arguments Returns: """ return mlines.Line2D([], [], **kwargs) plt.style.use('ggplot') leg_config = config['LEG_DEFAULTS'] m_config = config['DEFAULTS'] # get year values for labeling plots year1 = str(dt.datetime.fromordinal(data.dates[-1]))[:4] year2 = str(dt.datetime.fromordinal(data.dates[0]))[:4] # List of years in time series years = range(int(year1), int(year2) + 2, 1) # list of datetime objects with YYYY-MM-dd pattern using January 1 for month and day datetimes = [dt.datetime(yx, 1, 1) for yx in years] total_mask = data.qa_mask fill_out = data.fill_mask[~data.date_mask] if data.segment_classes is not None: class_results = dict() if not isinstance(data.segment_classes, list): data.segment_classes = [data.segment_classes] for ind, result in enumerate(data.segment_classes): if len(result['class_probs']) == 9: class_ind = np.argmax(result['class_probs']) else: # The older classification results have an additional class '0' so indices are off by 1 class_ind = np.argmax(result['class_probs']) + 1 class_label = NAMES[class_ind] if class_label not in class_results: class_results[class_label] = { 'starts': [result['start_day']], 'ends': [result['end_day']] } else: class_results[class_label]['starts'].append( result['start_day']) class_results[class_label]['ends'].append(result['end_day']) else: class_results = None """ plot_data is a dict whose keys are band names, index names, or a combination of both plot_data[key][0] contains the observed values plot_data[key][1] contains the model predicted values """ plot_data = get_plot_items(data=data, items=items) """ Create an empty dict to contain the mapping of data series to artists artist_map[key][0] contains the x-series artist_map[key][1] contains the y-series artist_map[key][2] contains the subplot name The keys that are subplot names will contain an empty point used for displaying which point is selected on the plot All other keys are the PathCollections returned by matplotlib.axes.Axes.scatter """ artist_map = {} """ Create an empty dict to contain the mapping of legend lines to plot artists """ lines_map = {} """ squeeze=False allows for plt.subplots to have a single subplot, must specify the column index as well when referencing a subplot because will always return a 2D array e.g. axes[num, 0] for subplot 'num'""" fig, axes = plt.subplots(nrows=len(plot_data), ncols=1, figsize=(18, len(plot_data) * 5), dpi=65, squeeze=False, sharex='all', sharey='none', num=f'timeseries_figure_{fig_num}') """ Define list objects that will contain the matplotlib artist objects within all subplots """ end_lines = list() break_lines = list() start_lines = list() model_lines = list() date_lines = list() all_obs_points = list() all_out_points = list() all_mask_points = list() all_empty_points = list() class_lines = dict() class_handles = None for num, b in enumerate(plot_data.keys()): """ Make lists to contain references to the specific artist objects for the current subplot. """ obs_points = list() out_points = list() mask_points = list() empty_point = list() class_handles = list() # Give each subplot a title axes[num, 0].set_title('{}'.format(b)) """ Create an empty plot to use for displaying which point is clicked later on """ empty_point.append(axes[num, 0].plot([], [], **m_config['highlight_pick'])) """ Store a reference to the empty point which will be used to display clicked points on the plot """ artist_map[b] = empty_point[0] all_empty_points.append(empty_point[0][0]) """ Plot the observed values within the PyCCD time range """ obs = axes[num, 0].scatter( x=data.dates_in[total_mask[data.date_mask]], y=plot_data[b][0][data.date_mask][total_mask[data.date_mask]], **m_config['clear_obs']) obs_points.append(obs) all_obs_points.append(obs) """ There's only ever one item in the *_points lists-a PathCollection artist-but it makes it easier to use with the 2D Lines because those are lists too. See the plotwindow.py module. """ artist_map[obs_points[0]] = [ data.dates_in[total_mask[data.date_mask]], plot_data[b][0][data.date_mask][total_mask[data.date_mask]], b ] """ Observed values outside of the PyCCD time range """ out = axes[num, 0].scatter(x=data.dates_out[fill_out], y=plot_data[b][0][~data.date_mask][fill_out], **m_config['out_obs']) out_points.append(out) all_out_points.append(out) artist_map[out_points[0]] = [ data.dates_out[fill_out], plot_data[b][0][~data.date_mask][fill_out], b ] """ Plot the observed values masked out by PyCCD """ mask = axes[num, 0].scatter( x=data.dates_in[~total_mask[data.date_mask]], y=plot_data[b][0][data.date_mask][~total_mask[data.date_mask]], **m_config['mask_obs']) mask_points.append(mask) all_mask_points.append(mask) artist_map[mask_points[0]] = [ data.dates_in[~total_mask[data.date_mask]], plot_data[b][0][data.date_mask][~total_mask[data.date_mask]], b ] """ plot the model start, end, and break dates """ for ind, s in enumerate(data.start_dates): lines1 = axes[num, 0].axvline(s, **m_config['start_lines']) end_lines.append(lines1) for ind, e in enumerate(data.end_dates): lines3 = axes[num, 0].axvline(e, **m_config['end_lines']) start_lines.append(lines3) for ind, br in enumerate(data.break_dates): lines2 = axes[num, 0].axvline(br, **m_config['break_lines']) break_lines.append(lines2) """ Draw the predicted curves """ if data.results is not None: for c in range(0, len(data.results["change_models"])): lines5, = axes[num, 0].plot( data.prediction_dates[c * len(data.bands)], plot_data[b][1][c], **m_config['model_lines']) model_lines.append(lines5) """ Draw horizontal color bars representing class assignments """ if class_results is not None: for key in class_results.keys(): if key not in class_lines: class_lines[key] = list() for ind, item in enumerate(class_results[key]['starts']): lines6 = axes[num, 0].hlines( y=0, xmin=item, xmax=class_results[key]['ends'][ind], linewidth=6, colors=COLORS[key]) class_lines[key].append(lines6) class_handles.append( get_legend_handle(linewidth=6, color=COLORS[key], label=key)) """ Set values for the y-axis limits """ if b in data.index_lookup.keys(): # Potential dynamic range values # ymin = min(plot_data[b][0][data.date_mask][total_mask]) - 0.15 # ymax = max(plot_data[b][0][data.date_mask][total_mask]) + 0.1 # Preferred static range values ymin = -1.01 # Band ratio or index min ymax = 1.01 # Band ratio or index max elif b == "Thermal": ymin = -2500 ymax = 6500 else: # Spectral bands # Potential dynamic range values # ymin = min(plot_data[b][0][data.date_mask][total_mask]) - 700 # ymax = max(plot_data[b][0][data.date_mask][total_mask]) + 500 # Preferred static range values ymin = -100 ymax = 6500 # Set the y-axis limits axes[num, 0].set_ylim([ymin, ymax]) """ Display the x and y values where the cursor is placed on a subplot """ axes[ num, 0].format_coord = lambda xcoord, ycoord: "({0:%Y-%m-%d}, ".format( dt.datetime.fromordinal(int(xcoord))) + "{0:f})".format(ycoord) """ Plot a vertical line at January 1 of each year on the time series """ for date in datetimes: lines7 = axes[num, 0].axvline(date, visible=False, **m_config['date_lines']) date_lines.append(lines7) # Set the axis face color axes[num, 0].patch.set_facecolor(m_config['background']['color']) # With sharex=True, set all x-axis tick labels to visible axes[num, 0].tick_params(axis='both', which='both', labelsize=12, labelbottom=True) """ Create custom legend handles """ empty_leg = get_legend_handle(**leg_config['highlight_pick']) obs_leg = get_legend_handle(**leg_config['clear_obs']) mask_leg = get_legend_handle(**leg_config['mask_obs']) out_leg = get_legend_handle(**leg_config['out_obs']) end_leg = get_legend_handle(**leg_config['end_lines']) break_leg = get_legend_handle(**leg_config['break_lines']) start_leg = get_legend_handle(**leg_config['start_lines']) model_leg = get_legend_handle(**leg_config['model_lines']) date_leg = get_legend_handle(**leg_config['date_lines']) handles = [ empty_leg, obs_leg, mask_leg, out_leg, end_leg, break_leg, start_leg, model_leg, date_leg ] labels = [ "Selected", "Clear", "Masked", "Unused", "End Date", "Break Date", "Start Date", "Model Fit", "Datelines" ] lines = [ all_empty_points, all_obs_points, all_mask_points, all_out_points, end_lines, break_lines, start_lines, model_lines, date_lines ] """ Add whichever land cover classes are present to the legend handles and labels """ if len(class_handles) > 0: for c in class_handles: handles.append(c) if class_results is not None: for cl in class_results.keys(): labels.append(cl) leg = axes[0, 0].legend(handles=handles, labels=labels, ncol=1, loc="upper left", bbox_to_anchor=(1.00, 1.00), borderaxespad=0.) if len(class_lines) > 0: for key in class_lines.keys(): lines.append(class_lines[key]) for legline, origline in zip(leg.get_lines(), lines): # Set a tolerance of 5 pixels legline.set_picker(5) # Map the artist to the corresponding legend line lines_map[legline] = origline try: # Fill in the figure canvas fig.tight_layout() except ValueError as e: log.error('Exception: %s' % e, exc_info=True) # Make room for the legend fig.subplots_adjust(right=0.9) return fig, artist_map, lines_map, axes