Loading src/main/java/com/elphel/imagej/orthomosaic/ComboMatch.java +55 −42 Original line number Diff line number Diff line Loading @@ -150,7 +150,7 @@ public class ComboMatch { int zoom_lev = -3; // 0; // +1 - zoom in twice, -1 - zoom out twice // boolean show_map_stats = false; boolean show_combo = false; // true; boolean create_overlaps = false; // boolean show_combo_mask = false; // generate image mas (where it is defined)_ // boolean use_alt = false; // boolean show_centers = true; Loading Loading @@ -182,7 +182,7 @@ public class ComboMatch { } GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,900); GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,600); gd.addChoice ("Files list/data path (w/o extension):", files_lists_paths, files_lists_paths[default_list_choice]); gd.addCheckbox ("Use saved maps collection", use_saved_collection, "If false - use files list."); gd.addCheckbox ("Save maps collection", save_collection, "Save maps collection to be able to restore."); Loading Loading @@ -219,6 +219,7 @@ public class ComboMatch { // gd.addCheckbox ("Show transformation centers", show_centers, "Mark verticals from the UAS on the ground."); // gd.addCheckbox ("Show statistics for ortho images", show_map_stats, "Generate and show statistics for ortho maps."); gd.addCheckbox ("Show combo maps/stats", show_combo, "Generate/save combo maps and stats."); gd.addCheckbox ("Create overlap pairs", create_overlaps, "Create scene pairs overlaps."); // gd.addCheckbox ("Show combo image mask", show_combo_mask, "Display combo binary image."); // gd.addCheckbox ("Show altitude combo image", use_alt, "Load and process altitude maps."); gd.addNumericField("Remove fraction of worst matches", frac_remove, 3,7,"", "When fitting scenes remove this fraction of worst match."); Loading Loading @@ -270,14 +271,8 @@ public class ComboMatch { OrthoMap.setGPUWidthHeight(gpu_width,gpu_height); // show_centers = gd.getNextBoolean(); // show_map_stats = gd.getNextBoolean(); show_combo = gd.getNextBoolean(); // show_combo_mask = gd.getNextBoolean(); // use_alt = gd.getNextBoolean(); // gpu_spair[0] = gd.getNextString(); // gpu_spair[1] = gd.getNextString(); create_overlaps = gd.getNextBoolean(); frac_remove = gd.getNextNumber(); metric_error= gd.getNextNumber(); if (use_marked_image ) { // will only be used if found and asked Loading Loading @@ -770,6 +765,10 @@ public class ComboMatch { maps_collection.processComboMap(debugLevel); return true; } if (create_overlaps) { boolean ok =maps_collection.getIntersectedPairs(debugLevel); if (!ok) return false; } if (process_correlation || render_match || pattern_match) { // int [] gpu_pair; if (gpu_spair == null) { Loading @@ -792,7 +791,7 @@ public class ComboMatch { String [] choices_all = new String[choices.length+1]; System.arraycopy(choices, 0, choices_all, 0, choices.length); choices_all[choices_all.length-1] = "--- select a single image ---"; GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,400); GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,100); int num_choice_lines = 50; gdc.addChoice("Image pair:", choices_all, Loading @@ -804,25 +803,23 @@ public class ComboMatch { if (pair >= choices.length) { int default_choice = 0; int num_scene_lines = 50; String scene_name = maps_collection.selectOneScene( 0, // int num_scene, default_choice, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name == null) { return false; } if (process_correlation) { // select a second image to match // select a second image and set gpu_spair int default_choice1 = 0; String scene_name1 = maps_collection.selectOneScene( 1, // int num_scene, gpu_spair = maps_collection.selectTwoScenes( default_choice1, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name1 == null) { if (gpu_spair == null) { return false; } } else { String scene_name = maps_collection.selectOneScene( 0, // int num_scene, default_choice, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name == null) { return false; } gpu_spair = new String[] {scene_name, scene_name1}; } else { // pattern match - single image gpu_spair = new String[] {scene_name}; } } else { Loading Loading @@ -913,7 +910,7 @@ public class ComboMatch { num_tries_fit = 0; update_match = false; } debugLevel = 0; // debugLevel = 0; boolean batch_mode = true; // false; // true; boolean ignore_prev_rms = true; Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair() Loading Loading @@ -976,6 +973,9 @@ public class ComboMatch { System.out.println("adjusted affines[1] for a pair: "+gpu_spair[0]+"/"+gpu_spair[1]); System.out.println("[["+affines[1][0][0]+","+affines[1][0][1]+","+affines[1][0][2]+"],"); System.out.println(" ["+affines[1][1][0]+","+affines[1][1][1]+","+affines[1][1][2]+"]]"); if (pmatch != null) { System.out.println("RMSE="+pmatch.rms); } System.out.println(); } } Loading Loading @@ -1040,45 +1040,57 @@ public class ComboMatch { PairwiseOrthoMatch pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch( maps_collection.ortho_maps[gpu_pair[1]].getName()); // boolean has_match = pairwiseOrthoMatch != null; PairwiseOrthoMatch inv_match = maps_collection.ortho_maps[gpu_pair[1]].getMatch( maps_collection.ortho_maps[gpu_pair[0]].getName()); // dialog - ask parameters and if has_match -ask if to use it (then just return true) // if has inv - ask and, if yes, = create inverted as initial boolean use_exixting_pair = false; boolean invert_exixting_pair = false; double search_step = 8.0; // pix double search_range = 50.0; // pix double maximal_rms = 0.25; // int min_overlap = 3000; // do not try to match if there is too small overlap (scaled pixels) int num_iter_lma = 5; boolean use_existing_pair = clt_parameters.imp.ospir_existing; // false boolean invert_exixting_pair = clt_parameters.imp.ospir_invert; // false double search_step = clt_parameters.imp.ospir_step; // 8.0; // pix double search_range = clt_parameters.imp.ospir_range; // 50.0; // pix double good_rms = clt_parameters.imp.ospir_good_rms; // 0.27; // double max_rms = clt_parameters.imp.ospir_max_rms; // 0.35; // int min_overlap = clt_parameters.imp.ospir_overlap; // 3000; // do not try to match if there is too small overlap (scaled pixels) int num_iter_lma = clt_parameters.imp.ospir_num_iter; // 5; boolean ignore_rms = clt_parameters.imp.ospir_ignore_rms; // false int spiral_debug = clt_parameters.imp.ospir_debug; // -3; GenericJTabbedDialog gd = new GenericJTabbedDialog("Setup SpiralMatch",1200,300); if (pairwiseOrthoMatch != null) { gd.addCheckbox ("Use existing image pair", use_exixting_pair, "Use existing affine settings for this pair, do not use spiral search."); gd.addCheckbox ("Use existing image pair", use_existing_pair, "Use existing affine settings for this pair, do not use spiral search."); } else { use_existing_pair = false; } if (inv_match != null) { gd.addCheckbox ("Invert existing image pair", invert_exixting_pair, "Invert existing image pair affine transform, do not use spiral search."); } else { invert_exixting_pair = false; } gd.addNumericField("Spiral search step", search_step, 3,7,"scaled pix", "Distance between spiral search probes, in scaled pixels."); gd.addNumericField("Spiral search radius", search_range, 3,7,"scaled pix", "Maximal radius of the spiral search, in scaled pixels."); gd.addNumericField("Maximal RMSE", maximal_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("RMSE to end search", good_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("Satisfactory RMSE", max_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("Minimal overlap", min_overlap, 0,4,"scaled pix ^ 2","Minimal overlap area in square scaled pixels."); gd.addNumericField("LMA iterations", num_iter_lma, 0,2,"", "Number of LMA iterations."); gd.addCheckbox ("Ignore worsening RMSE", ignore_rms, "Ignore worsening/not improving RMSE during spiral search."); gd.addNumericField("Spiral search debug level",spiral_debug, 0,3,"","Debug level during Spiral search."); gd.showDialog(); if (gd.wasCanceled()) return null; if (pairwiseOrthoMatch != null) { use_exixting_pair = gd.getNextBoolean(); use_existing_pair = gd.getNextBoolean(); } if (inv_match != null) { invert_exixting_pair = gd.getNextBoolean(); } search_step= gd.getNextNumber(); search_range= gd.getNextNumber(); maximal_rms = gd.getNextNumber(); good_rms = gd.getNextNumber(); max_rms = gd.getNextNumber(); min_overlap = (int) gd.getNextNumber(); num_iter_lma = (int) gd.getNextNumber(); if (use_exixting_pair) { ignore_rms = gd.getNextBoolean(); spiral_debug = (int) gd.getNextNumber(); if (use_existing_pair) { if (invert_exixting_pair) { System.out.println("Both direct and inverted matches are selected, using direct match"); } Loading @@ -1095,7 +1107,6 @@ public class ComboMatch { pairwiseOrthoMatch = maps_collection.SpiralMatch ( clt_parameters, // CLTParameters clt_parameters, // PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed frac_remove, // double frac_remove, // = 0.25 metric_error, // double metric_error, gpu_pair, // int [] gpu_pair, Loading @@ -1103,10 +1114,12 @@ public class ComboMatch { initial_zoom, // int zoom_lev, search_step, // double pix_step, search_range, // double pix_range, maximal_rms, // double need_rms, good_rms, // double good_rms, max_rms, // double max_rms, num_iter_lma, // int num_tries, // = 5 min_overlap, // int min_overlap, // 3000 debugLevel); // int debugLevel){ ignore_rms, // boolean ignore_rms, spiral_debug); // int debugLevel){ return pairwiseOrthoMatch; } Loading src/main/java/com/elphel/imagej/orthomosaic/OrthoMapsCollection.java +217 −13 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -987,7 +988,6 @@ public class OrthoMapsCollection implements Serializable{ public PairwiseOrthoMatch SpiralMatch ( CLTParameters clt_parameters, // PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed double frac_remove, // = 0.25 double metric_error, int [] gpu_pair, Loading @@ -995,15 +995,16 @@ public class OrthoMapsCollection implements Serializable{ int zoom_lev, double pix_step, double pix_range, double need_rms, double good_rms, double max_rms, int num_tries, // = 5 int min_overlap, // = 5 boolean ignore_rms, int debugLevel){ double [][] affine1 = new double [][] {affines_init[1][0].clone(),affines_init[1][1].clone()}; double [][][] affines = new double [][][] {affines_init[0],affine1}; boolean show_vf = false; boolean batch_mode = true; boolean ignore_prev_rms = false; // true; double [][] ground_planes = null; // null or double[2] - will return ground planes: double pix_size = OrthoMap.getPixelSizeMeters (zoom_lev); int nx = 0, ny=0; // number of steps (pix_step size) in hor and vert directions (both pos and neg) Loading @@ -1015,7 +1016,8 @@ public class OrthoMapsCollection implements Serializable{ zoom_lev); // int zoom_lev); double max_std = 0.5; // maximal standard deviation to limit center area double min_std_rad = 2.0; // minimal radius of the central area (if less - fail) int best_nx = -1, best_ny = -1; PairwiseOrthoMatch best_pom = null; while ((Math.abs(nx) <= nabs) && (Math.abs(ny) <= nabs)) { for (int i = 0; i < affines_init.length; i++) { System.arraycopy(affines_init[1][i], 0, affine1[i], 0, affine1[i].length); Loading @@ -1032,7 +1034,7 @@ public class OrthoMapsCollection implements Serializable{ min_std_rad, // double min_std_rad, // minimal radius of the central area (if less - fail) frac_remove, // double frac_remove, // = 0.25 metric_error, // double metric_error, ignore_prev_rms, // boolean ignore_prev_rms, ignore_rms, // boolean ignore_prev_rms, num_tries, // = 5int num_tries, // = 5 false, // boolean calc_warp, (will return null if false) batch_mode, // boolean batch_mode, Loading @@ -1042,15 +1044,20 @@ public class OrthoMapsCollection implements Serializable{ zoom_lev, // int zoom_lev, show_vf, // boolean show_vf, ground_planes, // double [][] ground_planes, // null or double[2] - will return ground planes debugLevel); // final int debugLevel) debugLevel-4); // final int debugLevel) pairwiseOrthoMatch.affine = affines[1]; // modified by correlateOrthoPair if (debugLevel > -4) { System.out.println(String.format("SpiralMatch(): nx = %3d, ny=%3d, RMSE=%8.6f", nx,ny,pairwiseOrthoMatch.rms)); } if (pairwiseOrthoMatch.rms < need_rms) { if (pairwiseOrthoMatch.rms < good_rms) { break; } if (!Double.isNaN(pairwiseOrthoMatch.rms) && ((best_pom == null) || !(best_pom.rms <= pairwiseOrthoMatch.rms))) { best_pom = pairwiseOrthoMatch.clone(); best_nx = nx; best_ny = ny; } // update nx, ny if ((nx > ny) && (nx > -ny)){ ny++; Loading @@ -1062,11 +1069,22 @@ public class OrthoMapsCollection implements Serializable{ nx++; } } if (!(pairwiseOrthoMatch.rms < need_rms)) { // failed return null; } if (pairwiseOrthoMatch.rms < good_rms) { // failed return pairwiseOrthoMatch; // pairwiseOrthoMatch.affine will have adjusted affine[1] } if ((best_pom != null) && (best_pom.rms <= max_rms)) { if (debugLevel > -4) { System.out.println("SpiralMatch(): best RMSE="+best_pom.rms+" < "+max_rms+ " for nx = "+best_nx+", ny="+best_ny+", using it."); } return best_pom; } if (debugLevel > -4) { System.out.println("SpiralMatch(): Failed to find a good match candidate. Best RMSE="+ ((best_pom != null)? best_pom.rms:"N/A")+", max_rms= "+max_rms+""); } return null; // pairwiseOrthoMatch.affine will have adjusted affine[1] } Loading Loading @@ -1132,6 +1150,7 @@ public class OrthoMapsCollection implements Serializable{ boolean show_vf, double [][] ground_planes, // null or double[2] - will return ground planes: int debugLevel){ double center_radius_change = 1.5; // if increases by more than this, disregard RMSE worsening boolean show_lma_dbg = !batch_mode && (debugLevel > 1); if (woi == null) { woi = new Rectangle(); Loading Loading @@ -1268,6 +1287,7 @@ public class OrthoMapsCollection implements Serializable{ double [][] jtj = null; double rms = Double.NaN; double last_center_radius = 0; double previous_center_radius = 1.0; for (int ntry = 0; ntry < num_tries; ntry++) { if (!batch_mode) { if (debugLevel>-3) { Loading Loading @@ -1508,6 +1528,13 @@ public class OrthoMapsCollection implements Serializable{ debugLevel); // final int debug_level) last_center_radius = orthoPairLMA.getCenterRadius(); double center_radius_increase = Double.isInfinite(previous_center_radius)? (Double.isInfinite(last_center_radius)?1:0): last_center_radius/previous_center_radius; if ((ntry == 0) && Double.isInfinite(last_center_radius)) { center_radius_increase = 1.0; // if starts with infinity - no increase } if ((min_std_rad >0) && (last_center_radius < 1)) { if (debugLevel>-3) { System.out.println("correlateOrthoPair(): center_radius="+ Loading Loading @@ -1559,10 +1586,18 @@ public class OrthoMapsCollection implements Serializable{ if (debugLevel > -3) { System.out.println("Will continue, as ignore_prev_rms is set"); } } else { if (center_radius_increase > center_radius_change) { if (debugLevel > -3) { System.out.println("Will continue, as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } else { break; } } } affines_gpu[1]=OrthoMap.combineAffine(affines_gpu[1], orthoPairLMA.getAffine()); jtj = orthoPairLMA.getLastJtJ(); if ((prev_rms - rms)/prev_rms < rel_improve) { Loading @@ -1573,11 +1608,32 @@ public class OrthoMapsCollection implements Serializable{ if (debugLevel > -3) { System.out.println("Will continue, as ignore_prev_rms is set"); } } else { if (center_radius_increase > center_radius_change) { if (debugLevel > -3) { System.out.println("Will continue, as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } else { break; } } } if (center_radius_increase > center_radius_change) { num_tries ++; if (debugLevel > -2) { System.out.println("Increasing num_tries to "+num_tries+", as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } prev_rms=rms; previous_center_radius = last_center_radius; } // for (int ntry = 0; ntry < num_tries; ntry++) { if ((min_std_rad > 0) && (last_center_radius < min_std_rad)) { Loading Loading @@ -3728,6 +3784,42 @@ public class OrthoMapsCollection implements Serializable{ return ortho_maps[scene_number].getName(); } public String[] selectTwoScenes( int default_choice, int num_choice_lines) { String [] fs = {"first","second"}; String [] lines = new String [ortho_maps.length]; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz for (int indx = 0; indx < ortho_maps.length; indx++) { OrthoMap map = ortho_maps[indx]; LocalDateTime ldt = map.getLocalDateTime(); ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of("Europe/Kyiv")); String sdt = zonedDateTime.format(formatter); String name = map.getName(); double agl = map.getAGL(); lines[indx]=String.format( "%3d %17s %26s %6.2f",indx, name, sdt, agl); } GenericJTabbedDialog gd = new GenericJTabbedDialog("Select two image scenes",1200,150); for (int num_scene = 0; num_scene < 2; num_scene++) { gd.addChoice(fs[num_scene]+" image:", lines, lines[default_choice], "Select "+fs[num_scene]+" scene", num_choice_lines); } gd.showDialog(); if (gd.wasCanceled()) return null; String [] names = new String[2]; for (int num_scene = 0; num_scene < 2; num_scene++) { int scene_number= gd.getNextChoiceIndex(); names[num_scene] = ortho_maps[scene_number].getName(); } return names; } public String [] getScenesList() { String [] lines = new String [ortho_maps.length]; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz Loading Loading @@ -3806,6 +3898,84 @@ public class OrthoMapsCollection implements Serializable{ return indices; } public boolean getIntersectedPairs( int debugLevel) { int [] indices = getScenesSelection( null, // boolean select_all, " to find intersects"); // String purpose) if (indices == null) { return false; } int zoom_lev = -5; double min_overlap = 0.25; boolean bounds_to_indices = true; GenericJTabbedDialog gd = new GenericJTabbedDialog("Combo map/stats generation",1200,1000); gd.addNumericField("Zoom level", zoom_lev, 0,4,"", "Zoom level: +1 - zoom in twice, -1 - zoom out twice"); gd.addNumericField("Minimal overlap", min_overlap, 3,7,"", "Minimal overlap as a fraction of the smaller image."); gd.showDialog(); if (gd.wasCanceled()) return false; zoom_lev = (int) gd.getNextNumber(); min_overlap = gd.getNextNumber(); int [] wh = new int[2]; int [] origin = new int[2]; double [][] centers =new double [indices.length][]; double [][] dmulti = renderMultiDouble ( null, // double [][] ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt indices, // int [] indices, // null or which indices to use (normally just 2 for pairwise comparison) bounds_to_indices, // boolean bounds_to_indices, null, // affines, // double [][][] affines, // null or [indices.length][2][3] null, // warp, // FineXYCorr warp,, zoom_lev, // int zoom_level, wh, // int [] wh, origin, // int [] origin){ // maps[0] as a reference centers); // double [][] centers) double [] overlaps = getFracOverlaps( dmulti); // final double [][] dmulti) { int num_overlaps_all = 0, num_overlaps_sel = 0; boolean [][] intersects = new boolean [dmulti.length][dmulti.length]; for (int i = 0; i < overlaps.length; i++) if (!Double.isNaN(overlaps[i])){ num_overlaps_all++; if (overlaps[i] >= min_overlap) { num_overlaps_sel++; int row = i / dmulti.length; int col = i % dmulti.length; intersects[row][col] = true; } } int [] groups = new int [dmulti.length]; for (int i = 0; i < groups.length; i++) { groups[i] = i; } for (int i = 0; i< (groups.length-1); i++) { for (int j = i+1; j < groups.length; j++) if (groups[j] != groups[i]){ int g0 = groups[i]; int g1 = groups[j]; for (int k = 0; k < groups.length; k++) { if (groups[k] == g1) { groups[k] = g0; } } } } HashSet<Integer> hs = new HashSet<Integer>(); for (int i:groups) { hs.add(i); } System.out.println("getIntersectedPairs(): num_overlaps_all="+num_overlaps_all+ ", num_overlaps_sel="+num_overlaps_sel+", number of disconnected groups="+hs.size()); if (debugLevel > 0) { ShowDoubleFloatArrays.showArrays( overlaps, "Overlaps"); } return true; } public boolean processComboMap( int debugLevel) { int [] indices = getScenesSelection( Loading Loading @@ -4331,4 +4501,38 @@ public class OrthoMapsCollection implements Serializable{ } } public static double [] getFracOverlaps( final double [][] dmulti) { final double [] overlaps = new double [dmulti.length*dmulti.length]; final Thread[] threads = ImageDtt.newThreadArray(); final AtomicInteger ai = new AtomicInteger(0); for (int ithread = 0; ithread < threads.length; ithread++) { threads[ithread] = new Thread() { public void run() { for (int nscene = ai.getAndIncrement(); nscene < (dmulti.length-1); nscene = ai.getAndIncrement()) { double [] data0 = dmulti[nscene]; int num0 = 0; for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data0[i])) { num0++; } for (int nscene1 = nscene+1; nscene1 < dmulti.length; nscene1++) { double [] data1 = dmulti[nscene1]; int num1 = 0, num_intersect=0; for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data1[i])) { num1++; if (!Double.isNaN(data0[i])) { num_intersect++; } } overlaps[nscene * dmulti.length + nscene1] = (1.0 * num_intersect)/ Math.min(num0, num1); } } } }; } ImageDtt.startAndJoin(threads); return overlaps; } } src/main/java/com/elphel/imagej/orthomosaic/PairwiseOrthoMatch.java +13 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,19 @@ public class PairwiseOrthoMatch implements Serializable { this.jtj = jtj; this.zoom_lev=zoom_lev; } public PairwiseOrthoMatch clone() { double [][] affine = {this.affine[0].clone(),this.affine[1].clone()}; double [][] jtj = new double [this.jtj.length][]; for (int i = 0; i < this.jtj.length; i++) { jtj[i] = this.jtj[i].clone(); } PairwiseOrthoMatch pom = new PairwiseOrthoMatch( affine, jtj, this.rms, this.zoom_lev); return pom; } public PairwiseOrthoMatch getInverse(double [] rd) { double [][] affine = OrthoMap.invertAffine(getAffine()); PairwiseOrthoMatch inverted_match = new PairwiseOrthoMatch( Loading Loading
src/main/java/com/elphel/imagej/orthomosaic/ComboMatch.java +55 −42 Original line number Diff line number Diff line Loading @@ -150,7 +150,7 @@ public class ComboMatch { int zoom_lev = -3; // 0; // +1 - zoom in twice, -1 - zoom out twice // boolean show_map_stats = false; boolean show_combo = false; // true; boolean create_overlaps = false; // boolean show_combo_mask = false; // generate image mas (where it is defined)_ // boolean use_alt = false; // boolean show_centers = true; Loading Loading @@ -182,7 +182,7 @@ public class ComboMatch { } GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,900); GenericJTabbedDialog gd = new GenericJTabbedDialog("Set image pair",1200,600); gd.addChoice ("Files list/data path (w/o extension):", files_lists_paths, files_lists_paths[default_list_choice]); gd.addCheckbox ("Use saved maps collection", use_saved_collection, "If false - use files list."); gd.addCheckbox ("Save maps collection", save_collection, "Save maps collection to be able to restore."); Loading Loading @@ -219,6 +219,7 @@ public class ComboMatch { // gd.addCheckbox ("Show transformation centers", show_centers, "Mark verticals from the UAS on the ground."); // gd.addCheckbox ("Show statistics for ortho images", show_map_stats, "Generate and show statistics for ortho maps."); gd.addCheckbox ("Show combo maps/stats", show_combo, "Generate/save combo maps and stats."); gd.addCheckbox ("Create overlap pairs", create_overlaps, "Create scene pairs overlaps."); // gd.addCheckbox ("Show combo image mask", show_combo_mask, "Display combo binary image."); // gd.addCheckbox ("Show altitude combo image", use_alt, "Load and process altitude maps."); gd.addNumericField("Remove fraction of worst matches", frac_remove, 3,7,"", "When fitting scenes remove this fraction of worst match."); Loading Loading @@ -270,14 +271,8 @@ public class ComboMatch { OrthoMap.setGPUWidthHeight(gpu_width,gpu_height); // show_centers = gd.getNextBoolean(); // show_map_stats = gd.getNextBoolean(); show_combo = gd.getNextBoolean(); // show_combo_mask = gd.getNextBoolean(); // use_alt = gd.getNextBoolean(); // gpu_spair[0] = gd.getNextString(); // gpu_spair[1] = gd.getNextString(); create_overlaps = gd.getNextBoolean(); frac_remove = gd.getNextNumber(); metric_error= gd.getNextNumber(); if (use_marked_image ) { // will only be used if found and asked Loading Loading @@ -770,6 +765,10 @@ public class ComboMatch { maps_collection.processComboMap(debugLevel); return true; } if (create_overlaps) { boolean ok =maps_collection.getIntersectedPairs(debugLevel); if (!ok) return false; } if (process_correlation || render_match || pattern_match) { // int [] gpu_pair; if (gpu_spair == null) { Loading @@ -792,7 +791,7 @@ public class ComboMatch { String [] choices_all = new String[choices.length+1]; System.arraycopy(choices, 0, choices_all, 0, choices.length); choices_all[choices_all.length-1] = "--- select a single image ---"; GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,400); GenericJTabbedDialog gdc = new GenericJTabbedDialog("Select image pair",1200,100); int num_choice_lines = 50; gdc.addChoice("Image pair:", choices_all, Loading @@ -804,25 +803,23 @@ public class ComboMatch { if (pair >= choices.length) { int default_choice = 0; int num_scene_lines = 50; String scene_name = maps_collection.selectOneScene( 0, // int num_scene, default_choice, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name == null) { return false; } if (process_correlation) { // select a second image to match // select a second image and set gpu_spair int default_choice1 = 0; String scene_name1 = maps_collection.selectOneScene( 1, // int num_scene, gpu_spair = maps_collection.selectTwoScenes( default_choice1, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name1 == null) { if (gpu_spair == null) { return false; } } else { String scene_name = maps_collection.selectOneScene( 0, // int num_scene, default_choice, // int default_choice, num_scene_lines); // int num_choice_lines) if (scene_name == null) { return false; } gpu_spair = new String[] {scene_name, scene_name1}; } else { // pattern match - single image gpu_spair = new String[] {scene_name}; } } else { Loading Loading @@ -913,7 +910,7 @@ public class ComboMatch { num_tries_fit = 0; update_match = false; } debugLevel = 0; // debugLevel = 0; boolean batch_mode = true; // false; // true; boolean ignore_prev_rms = true; Rectangle woi = new Rectangle(); // used to return actual woi from correlateOrthoPair() Loading Loading @@ -976,6 +973,9 @@ public class ComboMatch { System.out.println("adjusted affines[1] for a pair: "+gpu_spair[0]+"/"+gpu_spair[1]); System.out.println("[["+affines[1][0][0]+","+affines[1][0][1]+","+affines[1][0][2]+"],"); System.out.println(" ["+affines[1][1][0]+","+affines[1][1][1]+","+affines[1][1][2]+"]]"); if (pmatch != null) { System.out.println("RMSE="+pmatch.rms); } System.out.println(); } } Loading Loading @@ -1040,45 +1040,57 @@ public class ComboMatch { PairwiseOrthoMatch pairwiseOrthoMatch = maps_collection.ortho_maps[gpu_pair[0]].getMatch( maps_collection.ortho_maps[gpu_pair[1]].getName()); // boolean has_match = pairwiseOrthoMatch != null; PairwiseOrthoMatch inv_match = maps_collection.ortho_maps[gpu_pair[1]].getMatch( maps_collection.ortho_maps[gpu_pair[0]].getName()); // dialog - ask parameters and if has_match -ask if to use it (then just return true) // if has inv - ask and, if yes, = create inverted as initial boolean use_exixting_pair = false; boolean invert_exixting_pair = false; double search_step = 8.0; // pix double search_range = 50.0; // pix double maximal_rms = 0.25; // int min_overlap = 3000; // do not try to match if there is too small overlap (scaled pixels) int num_iter_lma = 5; boolean use_existing_pair = clt_parameters.imp.ospir_existing; // false boolean invert_exixting_pair = clt_parameters.imp.ospir_invert; // false double search_step = clt_parameters.imp.ospir_step; // 8.0; // pix double search_range = clt_parameters.imp.ospir_range; // 50.0; // pix double good_rms = clt_parameters.imp.ospir_good_rms; // 0.27; // double max_rms = clt_parameters.imp.ospir_max_rms; // 0.35; // int min_overlap = clt_parameters.imp.ospir_overlap; // 3000; // do not try to match if there is too small overlap (scaled pixels) int num_iter_lma = clt_parameters.imp.ospir_num_iter; // 5; boolean ignore_rms = clt_parameters.imp.ospir_ignore_rms; // false int spiral_debug = clt_parameters.imp.ospir_debug; // -3; GenericJTabbedDialog gd = new GenericJTabbedDialog("Setup SpiralMatch",1200,300); if (pairwiseOrthoMatch != null) { gd.addCheckbox ("Use existing image pair", use_exixting_pair, "Use existing affine settings for this pair, do not use spiral search."); gd.addCheckbox ("Use existing image pair", use_existing_pair, "Use existing affine settings for this pair, do not use spiral search."); } else { use_existing_pair = false; } if (inv_match != null) { gd.addCheckbox ("Invert existing image pair", invert_exixting_pair, "Invert existing image pair affine transform, do not use spiral search."); } else { invert_exixting_pair = false; } gd.addNumericField("Spiral search step", search_step, 3,7,"scaled pix", "Distance between spiral search probes, in scaled pixels."); gd.addNumericField("Spiral search radius", search_range, 3,7,"scaled pix", "Maximal radius of the spiral search, in scaled pixels."); gd.addNumericField("Maximal RMSE", maximal_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("RMSE to end search", good_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("Satisfactory RMSE", max_rms, 3,7,"scaled pix", "Maximal RMSE to consider match, in scaled pixels."); gd.addNumericField("Minimal overlap", min_overlap, 0,4,"scaled pix ^ 2","Minimal overlap area in square scaled pixels."); gd.addNumericField("LMA iterations", num_iter_lma, 0,2,"", "Number of LMA iterations."); gd.addCheckbox ("Ignore worsening RMSE", ignore_rms, "Ignore worsening/not improving RMSE during spiral search."); gd.addNumericField("Spiral search debug level",spiral_debug, 0,3,"","Debug level during Spiral search."); gd.showDialog(); if (gd.wasCanceled()) return null; if (pairwiseOrthoMatch != null) { use_exixting_pair = gd.getNextBoolean(); use_existing_pair = gd.getNextBoolean(); } if (inv_match != null) { invert_exixting_pair = gd.getNextBoolean(); } search_step= gd.getNextNumber(); search_range= gd.getNextNumber(); maximal_rms = gd.getNextNumber(); good_rms = gd.getNextNumber(); max_rms = gd.getNextNumber(); min_overlap = (int) gd.getNextNumber(); num_iter_lma = (int) gd.getNextNumber(); if (use_exixting_pair) { ignore_rms = gd.getNextBoolean(); spiral_debug = (int) gd.getNextNumber(); if (use_existing_pair) { if (invert_exixting_pair) { System.out.println("Both direct and inverted matches are selected, using direct match"); } Loading @@ -1095,7 +1107,6 @@ public class ComboMatch { pairwiseOrthoMatch = maps_collection.SpiralMatch ( clt_parameters, // CLTParameters clt_parameters, // PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed frac_remove, // double frac_remove, // = 0.25 metric_error, // double metric_error, gpu_pair, // int [] gpu_pair, Loading @@ -1103,10 +1114,12 @@ public class ComboMatch { initial_zoom, // int zoom_lev, search_step, // double pix_step, search_range, // double pix_range, maximal_rms, // double need_rms, good_rms, // double good_rms, max_rms, // double max_rms, num_iter_lma, // int num_tries, // = 5 min_overlap, // int min_overlap, // 3000 debugLevel); // int debugLevel){ ignore_rms, // boolean ignore_rms, spiral_debug); // int debugLevel){ return pairwiseOrthoMatch; } Loading
src/main/java/com/elphel/imagej/orthomosaic/OrthoMapsCollection.java +217 −13 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -987,7 +988,6 @@ public class OrthoMapsCollection implements Serializable{ public PairwiseOrthoMatch SpiralMatch ( CLTParameters clt_parameters, // PairwiseOrthoMatch pairwiseOrthoMatch, // will return statistics, may be null if not needed double frac_remove, // = 0.25 double metric_error, int [] gpu_pair, Loading @@ -995,15 +995,16 @@ public class OrthoMapsCollection implements Serializable{ int zoom_lev, double pix_step, double pix_range, double need_rms, double good_rms, double max_rms, int num_tries, // = 5 int min_overlap, // = 5 boolean ignore_rms, int debugLevel){ double [][] affine1 = new double [][] {affines_init[1][0].clone(),affines_init[1][1].clone()}; double [][][] affines = new double [][][] {affines_init[0],affine1}; boolean show_vf = false; boolean batch_mode = true; boolean ignore_prev_rms = false; // true; double [][] ground_planes = null; // null or double[2] - will return ground planes: double pix_size = OrthoMap.getPixelSizeMeters (zoom_lev); int nx = 0, ny=0; // number of steps (pix_step size) in hor and vert directions (both pos and neg) Loading @@ -1015,7 +1016,8 @@ public class OrthoMapsCollection implements Serializable{ zoom_lev); // int zoom_lev); double max_std = 0.5; // maximal standard deviation to limit center area double min_std_rad = 2.0; // minimal radius of the central area (if less - fail) int best_nx = -1, best_ny = -1; PairwiseOrthoMatch best_pom = null; while ((Math.abs(nx) <= nabs) && (Math.abs(ny) <= nabs)) { for (int i = 0; i < affines_init.length; i++) { System.arraycopy(affines_init[1][i], 0, affine1[i], 0, affine1[i].length); Loading @@ -1032,7 +1034,7 @@ public class OrthoMapsCollection implements Serializable{ min_std_rad, // double min_std_rad, // minimal radius of the central area (if less - fail) frac_remove, // double frac_remove, // = 0.25 metric_error, // double metric_error, ignore_prev_rms, // boolean ignore_prev_rms, ignore_rms, // boolean ignore_prev_rms, num_tries, // = 5int num_tries, // = 5 false, // boolean calc_warp, (will return null if false) batch_mode, // boolean batch_mode, Loading @@ -1042,15 +1044,20 @@ public class OrthoMapsCollection implements Serializable{ zoom_lev, // int zoom_lev, show_vf, // boolean show_vf, ground_planes, // double [][] ground_planes, // null or double[2] - will return ground planes debugLevel); // final int debugLevel) debugLevel-4); // final int debugLevel) pairwiseOrthoMatch.affine = affines[1]; // modified by correlateOrthoPair if (debugLevel > -4) { System.out.println(String.format("SpiralMatch(): nx = %3d, ny=%3d, RMSE=%8.6f", nx,ny,pairwiseOrthoMatch.rms)); } if (pairwiseOrthoMatch.rms < need_rms) { if (pairwiseOrthoMatch.rms < good_rms) { break; } if (!Double.isNaN(pairwiseOrthoMatch.rms) && ((best_pom == null) || !(best_pom.rms <= pairwiseOrthoMatch.rms))) { best_pom = pairwiseOrthoMatch.clone(); best_nx = nx; best_ny = ny; } // update nx, ny if ((nx > ny) && (nx > -ny)){ ny++; Loading @@ -1062,11 +1069,22 @@ public class OrthoMapsCollection implements Serializable{ nx++; } } if (!(pairwiseOrthoMatch.rms < need_rms)) { // failed return null; } if (pairwiseOrthoMatch.rms < good_rms) { // failed return pairwiseOrthoMatch; // pairwiseOrthoMatch.affine will have adjusted affine[1] } if ((best_pom != null) && (best_pom.rms <= max_rms)) { if (debugLevel > -4) { System.out.println("SpiralMatch(): best RMSE="+best_pom.rms+" < "+max_rms+ " for nx = "+best_nx+", ny="+best_ny+", using it."); } return best_pom; } if (debugLevel > -4) { System.out.println("SpiralMatch(): Failed to find a good match candidate. Best RMSE="+ ((best_pom != null)? best_pom.rms:"N/A")+", max_rms= "+max_rms+""); } return null; // pairwiseOrthoMatch.affine will have adjusted affine[1] } Loading Loading @@ -1132,6 +1150,7 @@ public class OrthoMapsCollection implements Serializable{ boolean show_vf, double [][] ground_planes, // null or double[2] - will return ground planes: int debugLevel){ double center_radius_change = 1.5; // if increases by more than this, disregard RMSE worsening boolean show_lma_dbg = !batch_mode && (debugLevel > 1); if (woi == null) { woi = new Rectangle(); Loading Loading @@ -1268,6 +1287,7 @@ public class OrthoMapsCollection implements Serializable{ double [][] jtj = null; double rms = Double.NaN; double last_center_radius = 0; double previous_center_radius = 1.0; for (int ntry = 0; ntry < num_tries; ntry++) { if (!batch_mode) { if (debugLevel>-3) { Loading Loading @@ -1508,6 +1528,13 @@ public class OrthoMapsCollection implements Serializable{ debugLevel); // final int debug_level) last_center_radius = orthoPairLMA.getCenterRadius(); double center_radius_increase = Double.isInfinite(previous_center_radius)? (Double.isInfinite(last_center_radius)?1:0): last_center_radius/previous_center_radius; if ((ntry == 0) && Double.isInfinite(last_center_radius)) { center_radius_increase = 1.0; // if starts with infinity - no increase } if ((min_std_rad >0) && (last_center_radius < 1)) { if (debugLevel>-3) { System.out.println("correlateOrthoPair(): center_radius="+ Loading Loading @@ -1559,10 +1586,18 @@ public class OrthoMapsCollection implements Serializable{ if (debugLevel > -3) { System.out.println("Will continue, as ignore_prev_rms is set"); } } else { if (center_radius_increase > center_radius_change) { if (debugLevel > -3) { System.out.println("Will continue, as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } else { break; } } } affines_gpu[1]=OrthoMap.combineAffine(affines_gpu[1], orthoPairLMA.getAffine()); jtj = orthoPairLMA.getLastJtJ(); if ((prev_rms - rms)/prev_rms < rel_improve) { Loading @@ -1573,11 +1608,32 @@ public class OrthoMapsCollection implements Serializable{ if (debugLevel > -3) { System.out.println("Will continue, as ignore_prev_rms is set"); } } else { if (center_radius_increase > center_radius_change) { if (debugLevel > -3) { System.out.println("Will continue, as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } else { break; } } } if (center_radius_increase > center_radius_change) { num_tries ++; if (debugLevel > -2) { System.out.println("Increasing num_tries to "+num_tries+", as center_radius increased from "+ previous_center_radius+" to "+ last_center_radius +" (more than "+ center_radius_change+"x)."); } } prev_rms=rms; previous_center_radius = last_center_radius; } // for (int ntry = 0; ntry < num_tries; ntry++) { if ((min_std_rad > 0) && (last_center_radius < min_std_rad)) { Loading Loading @@ -3728,6 +3784,42 @@ public class OrthoMapsCollection implements Serializable{ return ortho_maps[scene_number].getName(); } public String[] selectTwoScenes( int default_choice, int num_choice_lines) { String [] fs = {"first","second"}; String [] lines = new String [ortho_maps.length]; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz for (int indx = 0; indx < ortho_maps.length; indx++) { OrthoMap map = ortho_maps[indx]; LocalDateTime ldt = map.getLocalDateTime(); ZonedDateTime zonedUTC = ldt.atZone(ZoneId.of("UTC")); // that time is UTC ZonedDateTime zonedDateTime = zonedUTC.withZoneSameInstant(ZoneId.of("Europe/Kyiv")); String sdt = zonedDateTime.format(formatter); String name = map.getName(); double agl = map.getAGL(); lines[indx]=String.format( "%3d %17s %26s %6.2f",indx, name, sdt, agl); } GenericJTabbedDialog gd = new GenericJTabbedDialog("Select two image scenes",1200,150); for (int num_scene = 0; num_scene < 2; num_scene++) { gd.addChoice(fs[num_scene]+" image:", lines, lines[default_choice], "Select "+fs[num_scene]+" scene", num_choice_lines); } gd.showDialog(); if (gd.wasCanceled()) return null; String [] names = new String[2]; for (int num_scene = 0; num_scene < 2; num_scene++) { int scene_number= gd.getNextChoiceIndex(); names[num_scene] = ortho_maps[scene_number].getName(); } return names; } public String [] getScenesList() { String [] lines = new String [ortho_maps.length]; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss.SS zzz");// may be VV instead of zzz Loading Loading @@ -3806,6 +3898,84 @@ public class OrthoMapsCollection implements Serializable{ return indices; } public boolean getIntersectedPairs( int debugLevel) { int [] indices = getScenesSelection( null, // boolean select_all, " to find intersects"); // String purpose) if (indices == null) { return false; } int zoom_lev = -5; double min_overlap = 0.25; boolean bounds_to_indices = true; GenericJTabbedDialog gd = new GenericJTabbedDialog("Combo map/stats generation",1200,1000); gd.addNumericField("Zoom level", zoom_lev, 0,4,"", "Zoom level: +1 - zoom in twice, -1 - zoom out twice"); gd.addNumericField("Minimal overlap", min_overlap, 3,7,"", "Minimal overlap as a fraction of the smaller image."); gd.showDialog(); if (gd.wasCanceled()) return false; zoom_lev = (int) gd.getNextNumber(); min_overlap = gd.getNextNumber(); int [] wh = new int[2]; int [] origin = new int[2]; double [][] centers =new double [indices.length][]; double [][] dmulti = renderMultiDouble ( null, // double [][] ground_planes, // null - images, non-null altitudes. use new double[2][3] for old way alt indices, // int [] indices, // null or which indices to use (normally just 2 for pairwise comparison) bounds_to_indices, // boolean bounds_to_indices, null, // affines, // double [][][] affines, // null or [indices.length][2][3] null, // warp, // FineXYCorr warp,, zoom_lev, // int zoom_level, wh, // int [] wh, origin, // int [] origin){ // maps[0] as a reference centers); // double [][] centers) double [] overlaps = getFracOverlaps( dmulti); // final double [][] dmulti) { int num_overlaps_all = 0, num_overlaps_sel = 0; boolean [][] intersects = new boolean [dmulti.length][dmulti.length]; for (int i = 0; i < overlaps.length; i++) if (!Double.isNaN(overlaps[i])){ num_overlaps_all++; if (overlaps[i] >= min_overlap) { num_overlaps_sel++; int row = i / dmulti.length; int col = i % dmulti.length; intersects[row][col] = true; } } int [] groups = new int [dmulti.length]; for (int i = 0; i < groups.length; i++) { groups[i] = i; } for (int i = 0; i< (groups.length-1); i++) { for (int j = i+1; j < groups.length; j++) if (groups[j] != groups[i]){ int g0 = groups[i]; int g1 = groups[j]; for (int k = 0; k < groups.length; k++) { if (groups[k] == g1) { groups[k] = g0; } } } } HashSet<Integer> hs = new HashSet<Integer>(); for (int i:groups) { hs.add(i); } System.out.println("getIntersectedPairs(): num_overlaps_all="+num_overlaps_all+ ", num_overlaps_sel="+num_overlaps_sel+", number of disconnected groups="+hs.size()); if (debugLevel > 0) { ShowDoubleFloatArrays.showArrays( overlaps, "Overlaps"); } return true; } public boolean processComboMap( int debugLevel) { int [] indices = getScenesSelection( Loading Loading @@ -4331,4 +4501,38 @@ public class OrthoMapsCollection implements Serializable{ } } public static double [] getFracOverlaps( final double [][] dmulti) { final double [] overlaps = new double [dmulti.length*dmulti.length]; final Thread[] threads = ImageDtt.newThreadArray(); final AtomicInteger ai = new AtomicInteger(0); for (int ithread = 0; ithread < threads.length; ithread++) { threads[ithread] = new Thread() { public void run() { for (int nscene = ai.getAndIncrement(); nscene < (dmulti.length-1); nscene = ai.getAndIncrement()) { double [] data0 = dmulti[nscene]; int num0 = 0; for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data0[i])) { num0++; } for (int nscene1 = nscene+1; nscene1 < dmulti.length; nscene1++) { double [] data1 = dmulti[nscene1]; int num1 = 0, num_intersect=0; for (int i = 0; i < data0.length; i++) if (!Double.isNaN(data1[i])) { num1++; if (!Double.isNaN(data0[i])) { num_intersect++; } } overlaps[nscene * dmulti.length + nscene1] = (1.0 * num_intersect)/ Math.min(num0, num1); } } } }; } ImageDtt.startAndJoin(threads); return overlaps; } }
src/main/java/com/elphel/imagej/orthomosaic/PairwiseOrthoMatch.java +13 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,19 @@ public class PairwiseOrthoMatch implements Serializable { this.jtj = jtj; this.zoom_lev=zoom_lev; } public PairwiseOrthoMatch clone() { double [][] affine = {this.affine[0].clone(),this.affine[1].clone()}; double [][] jtj = new double [this.jtj.length][]; for (int i = 0; i < this.jtj.length; i++) { jtj[i] = this.jtj[i].clone(); } PairwiseOrthoMatch pom = new PairwiseOrthoMatch( affine, jtj, this.rms, this.zoom_lev); return pom; } public PairwiseOrthoMatch getInverse(double [] rd) { double [][] affine = OrthoMap.invertAffine(getAffine()); PairwiseOrthoMatch inverted_match = new PairwiseOrthoMatch( Loading