diff --git a/src/mintpy/cli/plot_coherence_matrix.py b/src/mintpy/cli/plot_coherence_matrix.py index b7e6f9fda..9817b7ba5 100755 --- a/src/mintpy/cli/plot_coherence_matrix.py +++ b/src/mintpy/cli/plot_coherence_matrix.py @@ -56,10 +56,12 @@ def create_parser(subparsers=None): 'Default: view.py img_file --wrap --noverbose') # aux files - parser.add_argument('--tcoh', dest='tcoh_file', default='temporalCoherence.h5', + parser.add_argument('--tcoh', dest='tcoh_file', help='temporal coherence file.') parser.add_argument('-t','--template', dest='template_file', help='temporal file.') + parser.add_argument('--time-axis', dest='time_axis', action='store_true', + help='Use continuous time axis instead of date indices for coherence matrix') parser.add_argument('--save', dest='save_fig', action='store_true', help='save the figure') @@ -84,6 +86,8 @@ def cmd_line_parse(iargs=None): mintpy_dir = os.path.dirname(os.path.dirname(inps.ifgram_file)) if not inps.img_file: inps.img_file = os.path.join(mintpy_dir, 'velocity.h5') + if not inps.tcoh_file: + inps.tcoh_file = os.path.join(mintpy_dir, 'temporalCoherence.h5') if not inps.template_file: inps.template_file = os.path.join(mintpy_dir, 'smallbaselineApp.cfg') @@ -114,7 +118,7 @@ def main(iargs=None): obj = coherenceMatrixViewer(inps) obj.open() obj.plot() - obj.fig.canvas.mpl_disconnect(obj.cid) + #obj.fig_img.canvas.mpl_disconnect(obj.cid_img) ############################################################ diff --git a/src/mintpy/plot_coherence_matrix.py b/src/mintpy/plot_coherence_matrix.py index 4e2386269..d41f204aa 100644 --- a/src/mintpy/plot_coherence_matrix.py +++ b/src/mintpy/plot_coherence_matrix.py @@ -1,7 +1,7 @@ ############################################################ # Program is part of MintPy # # Copyright (c) 2013, Zhang Yunjun, Heresh Fattahi # -# Author: Zhang Yunjun, Nov 2018 # +# Author: Zhang Yunjun, Changyang Hu, Nov 2018 # ############################################################ @@ -62,16 +62,24 @@ class coherenceMatrixViewer(): def __init__(self, inps): # figure variables - self.figname = 'Coherence matrix' - self.fig_size = None - self.fig = None + self.figname_img = 'Image' + self.figsize_img = None + self.fig_img = None self.ax_img = None + + self.figname_mat = 'Coherence Matrix' + self.figsize_mat = None + self.fig_mat = None self.ax_mat = None + self.time_axis = getattr(inps, 'time_axis', False) + # copy inps to self object for key, value in inps.__dict__.items(): setattr(self, key, value) + self._marker_artist = None + def open(self): global vprint @@ -89,11 +97,14 @@ def open(self): self = read_network_info(self) # auto figure size - if not self.fig_size: + if not self.figsize_img: ds_shape = readfile.read(self.img_file)[0].shape - fig_size = pp.auto_figure_size(ds_shape, disp_cbar=True, scale=0.7) - self.fig_size = [fig_size[0]+fig_size[1], fig_size[1]] - vprint(f'create figure in size of {self.fig_size} inches') + self.figsize_img = pp.auto_figure_size(ds_shape, disp_cbar=True, scale=0.7) + vprint(f'create image figure in size of {self.figsize_img} inches') + + if not self.figsize_mat: + self.figsize_mat = [8, 6] + vprint(f'create matrix figure in size of {self.figsize_mat} inches') # read aux data # 1. temporal coherence value @@ -111,48 +122,109 @@ def open(self): def plot(self): - # Figure 1 - self.fig = plt.figure(self.figname, figsize=self.fig_size) + # Figure 1 - Image + self.fig_img, self.ax_img = plt.subplots(num=self.figname_img, figsize=self.figsize_img) + self.plot_init_image() + + # Figure 2 - Coherence Matrix + self.fig_mat, self.ax_mat = plt.subplots(num=self.figname_mat, figsize=self.figsize_mat) + self.colormap = pp.ColormapExt(self.cmap_name, vlist=self.cmap_vlist).colormap + if all(i is not None for i in self.yx): + self.plot_coherence_matrix4pixel(self.yx) - # Axes 1 - Image - self.ax_img = self.fig.add_axes([0.05, 0.1, 0.4, 0.8]) + # Link the canvas to the plots. + self.cid_img = self.fig_img.canvas.mpl_connect('button_press_event', self.update_coherence_matrix) + + if self.disp_fig: + plt.show() + return + + def plot_init_image(self): + """Plot the initial image.""" view_cmd = self.view_cmd.format(self.img_file) d_img, atr, view_inps = view.prep_slice(view_cmd) self.coord = ut.coordinate(atr) - if all(i is not None for i in self.yx): - view_inps.pts_marker = 'r^' - view_inps.pts_yx = np.array(self.yx).reshape(-1, 2) - - # point yx --> lalo for geocoded product - if 'Y_FIRST' in atr.keys(): - view_inps.pts_lalo = np.array( - self.coord.radar2geo( - self.yx[0], - self.yx[1], - )[0:2], - ).reshape(-1,2) - view_inps.print_msg = self.print_msg self.ax_img = view.plot_slice(self.ax_img, d_img, atr, view_inps)[0] self.fig_coord = view_inps.fig_coord - # Axes 2 - coherence matrix - self.ax_mat = self.fig.add_axes([0.55, 0.125, 0.40, 0.75]) - self.colormap = pp.ColormapExt(self.cmap_name, vlist=self.cmap_vlist).colormap - if all(i is not None for i in self.yx): - self.plot_coherence_matrix4pixel(self.yx) + self.fig_img.canvas.manager.set_window_title(self.figname_img) + self.fig_img.tight_layout() + return - # Link the canvas to the plots. - self.cid = self.fig.canvas.mpl_connect('button_press_event', self.update_coherence_matrix) - if self.disp_fig: - plt.show() + def plot_coherence_matrix4pixel_time_axis(self, yx): + """Plot coherence matrix with continuous time axis for one pixel.""" + self.ax_mat.cla() + + box = (yx[1], yx[0], yx[1]+1, yx[0]+1) + coh = readfile.read(self.ifgram_file, datasetName='coherence', box=box)[0] + + ex_date12_list = self.ex_date12_list[:] + if self.min_coh_used > 0.: + ex_date12_list += np.array(self.date12_list)[coh < self.min_coh_used].tolist() + ex_date12_list = sorted(list(set(ex_date12_list))) + + plotDict = {} + plotDict['fig_title'] = f'Y = {yx[0]}, X = {yx[1]}' + if self.tcoh is not None: + tcoh = self.tcoh[yx[0], yx[1]] + plotDict['fig_title'] += f', tcoh = {tcoh:.2f}' + plotDict['colormap'] = self.colormap + if len(self.cmap_vlist) >= 2: + plotDict['vlim'] = [self.cmap_vlist[0], self.cmap_vlist[-1]] + else: + plotDict['vlim'] = [0.0, 1.0] + plotDict['cbar_label'] = 'Coherence' + plotDict['disp_legend'] = False + + pp.plot_coherence_matrix_time_axis( + self.ax_mat, + date12List=self.date12_list, + cohList=coh.tolist(), + date12List_drop=ex_date12_list, + p_dict=plotDict, + ) + + self.ax_mat.annotate('ifgrams\navailable', xy=(0.05, 0.05), xycoords='axes fraction', fontsize=12) + self.ax_mat.annotate('ifgrams\nused', ha='right', xy=(0.95, 0.85), xycoords='axes fraction', fontsize=12) + + msg = f'pixel in yx = {tuple(yx)}, ' + msg += f'min/max spatial coherence: {np.nanmin(coh):.2f} / {np.nanmax(coh):.2f}, ' + if self.tcoh is not None: + msg += f'temporal coherence: {tcoh:.2f}' + vprint(msg) + + self.fig_mat.canvas.manager.set_window_title(self.figname_mat) + if not hasattr(self, "_mat_tight_layout_done"): + self.fig_mat.tight_layout() + self._mat_tight_layout_done = True + + self.fig_mat.canvas.draw_idle() + self.fig_mat.canvas.flush_events() + + # plot/update marker on the image window + if self.fig_coord == 'geo': + lat, lon = self.coord.yx2lalo(yx[0], yx[1]) + mx = lon if self.fig_coord == 'geo' else yx[1] + my = lat if self.fig_coord == 'geo' else yx[0] + if self._marker_artist is None: + self._marker_artist = self.ax_img.plot( + mx, my, 'r^', markersize=6, markeredgecolor='black' + )[0] + else: + self._marker_artist.set_data([mx], [my]) + + self.fig_img.canvas.draw_idle() return def plot_coherence_matrix4pixel(self, yx): """Plot coherence matrix for one pixel Parameters: yx : list of 2 int """ + if self.time_axis: + return self.plot_coherence_matrix4pixel_time_axis(yx) + self.ax_mat.cla() # read coherence @@ -169,7 +241,7 @@ def plot_coherence_matrix4pixel(self, yx): plotDict = {} plotDict['fig_title'] = f'Y = {yx[0]}, X = {yx[1]}' # display temporal coherence value of the pixel - if self.tcoh_file: + if self.tcoh is not None: tcoh = self.tcoh[yx[0], yx[1]] plotDict['fig_title'] += f', tcoh = {tcoh:.2f}' plotDict['colormap'] = self.colormap @@ -198,17 +270,41 @@ def format_coord(x, y): # info msg = f'pixel in yx = {tuple(yx)}, ' + if self.fig_coord == 'geo': + lat, lon = self.coord.yx2lalo(yx[0], yx[1]) + msg += f'lat/lon = ({lat:.8f}, {lon:.8f}), ' msg += f'min/max spatial coherence: {np.min(coh):.2f} / {np.max(coh):.2f}, ' - if self.tcoh_file: + if self.tcoh is not None: msg += f'temporal coherence: {tcoh:.2f}' vprint(msg) + self.fig_mat.canvas.manager.set_window_title(self.figname_mat) + + # call tight_layout only once to avoid jitter and repeated work + if not hasattr(self, "_mat_tight_layout_done"): + self.fig_mat.tight_layout() + self._mat_tight_layout_done = True + # update figure - self.fig.canvas.draw_idle() - self.fig.canvas.flush_events() + self.fig_mat.canvas.draw_idle() + self.fig_mat.canvas.flush_events() + + # plot/update marker on the image window + mx = lon if self.fig_coord == 'geo' else yx[1] + my = lat if self.fig_coord == 'geo' else yx[0] + if self._marker_artist is None: + self._marker_artist = self.ax_img.plot( + mx, my, 'r^', markersize=6, markeredgecolor='black' + )[0] + else: + self._marker_artist.set_data([mx], [my]) + + self.fig_img.canvas.draw_idle() + return def update_coherence_matrix(self, event): + """Update coherence matrix when clicking on either window.""" if event.inaxes == self.ax_img: if self.fig_coord == 'geo': yx = self.coord.lalo2yx(event.ydata, event.xdata) diff --git a/src/mintpy/plot_network.py b/src/mintpy/plot_network.py index 31911f7bb..a0df3831b 100644 --- a/src/mintpy/plot_network.py +++ b/src/mintpy/plot_network.py @@ -173,7 +173,7 @@ def plot_network(inps): # figure names ext = 'Ion.pdf' if os.path.basename(inps.file).startswith('ion') else '.pdf' fig_names = { - 'coherence' : [i+ext for i in ['pbaseHistory', 'coherenceHistory', 'coherenceMatrix', 'network']], + 'coherence' : [i+ext for i in ['pbaseHistory', 'coherenceHistory', 'coherenceMatrix', 'coherenceMatrixTimeAxis', 'network']], 'offsetSNR' : [i+ext for i in ['pbaseHistory', 'SNRHistory', 'SNRMatrix', 'network']], 'tbase' : [i+ext for i in ['pbaseHistory', 'tbaseHistory', 'tbaseMatrix', 'network']], 'pbase' : [i+ext for i in ['pbaseHistory', 'pbaseRangeHistory', 'pbaseMatrix', 'network']], @@ -204,7 +204,7 @@ def plot_network(inps): ) if inps.save_fig: fig.savefig(fig_names[1], **kwargs) - print(f'save figure to {fig_names[2]}') + print(f'save figure to {fig_names[1]}') # Fig 3 - Coherence Matrix fig_size3 = np.mean(inps.fig_size) @@ -218,9 +218,25 @@ def plot_network(inps): )[0] if inps.save_fig: fig.savefig(fig_names[2], **kwargs) - print(f'save figure to {fig_names[1]}') + print(f'save figure to {fig_names[2]}') + + # Fig 4 - Coherence Matrix with Time Axis + fig_size4 = np.mean(inps.fig_size) + fig, ax = plt.subplots(figsize=[fig_size4, fig_size4]) + ax = pp.plot_coherence_matrix_time_axis( + ax, + inps.date12List, + inps.cohList, + inps.date12List_drop, + p_dict=vars(inps), + )[0] + fig.tight_layout() + if inps.save_fig: + fig.savefig(fig_names[3], **kwargs) + print(f'save figure to {fig_names[3]}') - # Fig 4 - Interferogram Network + # Fig 5 - Interferogram Network (or Fig 4 if cohList is None) + fig_idx = 4 if inps.cohList is not None else 3 fig, ax = plt.subplots(figsize=inps.fig_size) ax = pp.plot_network( ax, @@ -231,8 +247,8 @@ def plot_network(inps): inps.date12List_drop, ) if inps.save_fig: - fig.savefig(fig_names[3], **kwargs) - print(f'save figure to {fig_names[3]}') + fig.savefig(fig_names[fig_idx], **kwargs) + print(f'save figure to {fig_names[fig_idx]}') if inps.disp_fig: print('showing ...') diff --git a/src/mintpy/utils/plot.py b/src/mintpy/utils/plot.py index 201d20d49..1446ac2c6 100644 --- a/src/mintpy/utils/plot.py +++ b/src/mintpy/utils/plot.py @@ -947,7 +947,7 @@ def plot_coherence_matrix(ax, date12List, cohList, date12List_drop=[], p_dict={} if p_dict['disp_cbar']: divider = make_axes_locatable(ax) cax = divider.append_axes("right", "3%", pad="3%") - cbar = plt.colorbar(im, cax=cax) + cbar = ax.figure.colorbar(im, cax=cax) cbar.set_label(p_dict['cbar_label'], fontsize=p_dict['fontsize']) # Legend @@ -959,6 +959,172 @@ def plot_coherence_matrix(ax, date12List, cohList, date12List_drop=[], p_dict={} return ax, coh_mat, im +def plot_coherence_matrix_time_axis(ax, date12List, cohList, date12List_drop=[], p_dict={}): + """Plot Coherence Matrix with continuous time axis + Parameters: ax : matplotlib.pyplot.Axes, + date12List : list of date12 in YYYYMMDD_YYYYMMDD format + cohList : list of float, coherence value + date12List_drop : list of date12 for date12 marked as dropped + p_dict : dict of plot setting + Returns: ax : matplotlib.pyplot.Axes + coh_mat : 2D np.array in size of [num_date, num_date] + mesh : matplotlib.collections.QuadMesh object + """ + # Figure Setting + if 'ds_name' not in p_dict.keys(): p_dict['ds_name'] = 'Coherence' + if 'fontsize' not in p_dict.keys(): p_dict['fontsize'] = 12 + if 'disp_title' not in p_dict.keys(): p_dict['disp_title'] = True + if 'fig_title' not in p_dict.keys(): p_dict['fig_title'] = '{} Matrix'.format(p_dict['ds_name']) + if 'colormap' not in p_dict.keys(): p_dict['colormap'] = 'RdBu_truncate' + if 'cbar_label' not in p_dict.keys(): p_dict['cbar_label'] = p_dict['ds_name'] + if 'vlim' not in p_dict.keys(): p_dict['vlim'] = (0.2, 1.0) + if 'disp_cbar' not in p_dict.keys(): p_dict['disp_cbar'] = True + if 'legend_loc' not in p_dict.keys(): p_dict['legend_loc'] = 'best' + if 'disp_legend' not in p_dict.keys(): p_dict['disp_legend'] = True + + # support input colormap: string for colormap name, or colormap object directly + if isinstance(p_dict['colormap'], str): + cmap = ColormapExt(p_dict['colormap']).colormap + elif isinstance(p_dict['colormap'], mpl.colors.LinearSegmentedColormap): + cmap = p_dict['colormap'] + else: + raise ValueError('unrecognized colormap input: {}'.format(p_dict['colormap'])) + + date12List = ptime.yyyymmdd_date12(date12List) + coh_mat = pnet.coherence_matrix(date12List, cohList) + + m_dates = [i.split('_')[0] for i in date12List] + s_dates = [i.split('_')[1] for i in date12List] + dateList = ptime.yyyymmdd(sorted(list(set(m_dates + s_dates)))) + dateList_dt = [dt.datetime.strptime(i, '%Y%m%d') for i in dateList] + + if date12List_drop: + date12List_drop = ptime.yyyymmdd_date12(date12List_drop) + for date12 in date12List_drop: + idx1, idx2 = (dateList.index(i) for i in date12.split('_')) + coh_mat[idx1, idx2] = np.nan + + if len(dateList_dt) == 1: + half_width = dt.timedelta(days=15) + grid_points = [dateList_dt[0] - half_width, dateList_dt[0] + half_width] + else: + grid_points = [dateList_dt[0] - (dateList_dt[1] - dateList_dt[0])] + for date1, date2 in zip(dateList_dt[:-1], dateList_dt[1:]): + grid_points.append(date1 + (date2 - date1) / 2) + grid_points.append(dateList_dt[-1] + (dateList_dt[-1] - dateList_dt[-2])) + + grid_nums = mdates.date2num(grid_points) + X, Y = np.meshgrid(grid_nums, grid_nums) + + # Show diagonal value as black, to be distinguished from un-selected interferograms + diag_mat = np.diag(np.ones(coh_mat.shape[0])) + diag_mat[diag_mat == 0.] = np.nan + ax.pcolormesh(X, Y, diag_mat, cmap='gray_r', vmin=0.0, vmax=1.0, shading='auto', zorder=1) + + cmap_plot = cmap.copy() + cmap_plot.set_bad('white') + mesh = ax.pcolormesh( + X, Y, coh_mat, + cmap=cmap_plot, + vmin=p_dict['vlim'][0], + vmax=p_dict['vlim'][1], + shading='auto', + zorder=0, + ) + + # numeric month/year axis labels + fs = p_dict['fontsize'] + min_date, max_date = min(dateList_dt), max(dateList_dt) + if min_date.day > 1: + if min_date.month == 12: + tick_start = min_date.replace(year=min_date.year + 1, month=1, day=1) + else: + tick_start = min_date.replace(month=min_date.month + 1, day=1) + else: + tick_start = min_date.replace(day=1) + + tick_dates = [] + current_date = tick_start + while current_date <= max_date: + tick_dates.append(current_date) + if current_date.month == 12: + current_date = current_date.replace(year=current_date.year + 1, month=1) + else: + current_date = current_date.replace(month=current_date.month + 1) + + tick_positions = mdates.date2num(tick_dates) + major_ticks = [p for p, d in zip(tick_positions, tick_dates) if d.month == 1] + minor_ticks = [p for p, d in zip(tick_positions, tick_dates) if d.month != 1] + ax.set_xticks(major_ticks) + ax.set_xticks(minor_ticks, minor=True) + ax.set_yticks(major_ticks) + ax.set_yticks(minor_ticks, minor=True) + ax.set_xticklabels([''] * len(major_ticks)) + ax.set_xticklabels([''] * len(minor_ticks), minor=True) + ax.set_yticklabels([''] * len(major_ticks)) + ax.set_yticklabels([''] * len(minor_ticks), minor=True) + ax.tick_params(which='major', direction='out', length=6, width=1.1, + bottom=True, top=True, left=True, right=True) + ax.tick_params(which='minor', direction='out', length=3, width=1, + bottom=True, top=True, left=True, right=True) + + offset = (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.03 + year_offset = offset * 2.2 + for i in range(len(tick_dates) - 1): + if tick_dates[i].month % 2 == 1: + pos = (tick_positions[i] + tick_positions[i + 1]) / 2 + label = str(tick_dates[i].month) + ax.text(pos, ax.get_ylim()[1] + offset, label, ha='center', va='top', fontsize=fs) + ax.text(ax.get_xlim()[0] - offset, pos, label, ha='right', va='center', fontsize=fs) + + year_groups = {} + for i, date in enumerate(tick_dates): + year_groups.setdefault(date.year, []).append(tick_positions[i]) + for year, positions in sorted(year_groups.items()): + pos = (positions[0] + positions[-1]) / 2 + ax.text(pos, ax.get_ylim()[1] + year_offset, str(year), ha='center', va='top', fontsize=fs) + ax.text(ax.get_xlim()[0] - year_offset, pos, str(year), + ha='right', va='center', fontsize=fs, rotation=90) + + ax.set_xlabel('Time', fontsize=fs, labelpad=18) + ax.set_ylabel('Time', fontsize=fs, labelpad=18) + ax.set_aspect('equal', adjustable='box') + ax.invert_yaxis() + + if p_dict['disp_title']: + ax.set_title(p_dict['fig_title'], fontsize=p_dict['fontsize']) + + # Colorbar + if p_dict['disp_cbar']: + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", "3%", pad="3%") + cbar = ax.figure.colorbar(mesh, cax=cax) + cbar.set_label(p_dict['cbar_label'], fontsize=p_dict['fontsize']) + + # Legend + if date12List_drop and p_dict['disp_legend']: + ax.plot([], [], label='Upper: Ifgrams used') + ax.plot([], [], label='Lower: Ifgrams all') + ax.legend(loc=p_dict['legend_loc'], handlelength=0) + + # Status bar + def format_coord(x, y): + col = np.searchsorted(grid_nums, x, side='right') - 1 + row = np.searchsorted(grid_nums, y, side='right') - 1 + if 0 <= row < len(dateList_dt) and 0 <= col < len(dateList_dt): + date1 = dateList_dt[col].strftime('%Y-%m-%d') + date2 = dateList_dt[row].strftime('%Y-%m-%d') + coh_val = coh_mat[row, col] + if not np.isnan(coh_val): + return f'x={date1}, y={date2}, v={coh_val:.3f}' + return f'x={date1}, y={date2}, v=NaN' + return '' + + ax.format_coord = format_coord + + return ax, coh_mat, mesh + + def plot_num_triplet_with_nonzero_integer_ambiguity(fname, disp_fig=False, font_size=12, fig_size=[9,3]): """Plot the histogram for the number of triplets with non-zero integer ambiguity.