001/******************************************************************************* 002 * Permission to use and distribute this software is granted under the BSD 2-Clause 003 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 004 * Copyright (c) 2016-2023 Wilhelm Burger. All rights reserved. 005 * Visit https://imagingbook.com for additional details. 006 ******************************************************************************/ 007package Calibration_Plugins_1; 008 009import ij.IJ; 010import ij.ImagePlus; 011import ij.gui.GenericDialog; 012import ij.plugin.PlugIn; 013import imagingbook.calibration.zhang.Calibrator; 014import imagingbook.calibration.zhang.Calibrator.Parameters; 015import imagingbook.calibration.zhang.Camera; 016import imagingbook.calibration.zhang.ViewTransform; 017import imagingbook.calibration.zhang.data.CalibrationImage; 018import imagingbook.calibration.zhang.data.ZhangData; 019import imagingbook.common.color.sets.BasicAwtColor; 020import imagingbook.common.geometry.basic.Pnt2d; 021import imagingbook.common.ij.overlay.ColoredStroke; 022import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 023import imagingbook.core.jdoc.JavaDocHelp; 024import imagingbook.core.resource.ImageResource; 025 026import java.awt.Shape; 027import java.awt.geom.Ellipse2D; 028import java.awt.geom.Path2D; 029import java.util.ArrayList; 030import java.util.List; 031 032import static imagingbook.common.ij.DialogUtils.formatText; 033 034/** 035 * This plugin performs Zhang's camera calibration on the pre-calculated corner point data for the M given target views. 036 * Based on the estimated intrinsic and extrinsic (view) parameters, the corner points of the 3D target model are then 037 * projected onto the corresponding calibration images (a stack). All drawing is done by non-destructive graphic 038 * overlays. 039 * 040 * @author W. Burger 041 * @version 2022/04/14 042 */ 043public class Do_Calibration implements PlugIn, JavaDocHelp { 044 045 private static ImageResource resource = CalibrationImage.CalibImageStack; 046 047 private static boolean ListCameraIntrinsics = true; 048 private static boolean ListCameraViews = true; 049 050 private static boolean ShowProjectedModel = true; 051 private static boolean MarkCornerPoints = true; 052 053 private static BasicAwtColor ProjectedModelColor = BasicAwtColor.Red; 054 private static BasicAwtColor CornerMarkColor = BasicAwtColor.Blue; 055 private static double CornerMarkRadius = 2.0; 056 private static double StrokeWidth = 0.5; 057 058 public void run(String arg0) { 059 ImagePlus testIm = resource.getImagePlus(); 060 if (testIm == null) { 061 IJ.error("Could not open calibration images!"); 062 return; 063 } 064 065 int M = testIm.getNSlices(); // number of views 066 if (M < 2) { 067 IJ.error("Image must be a stack with 2+ images!"); 068 return; 069 } 070 testIm.show(); 071 072 if (!runDialog()) { 073 return; 074 } 075 076 Pnt2d[] modelPoints = ZhangData.getModelPoints(); 077 Camera camReference = ZhangData.getCameraIntrinsics(); 078 Pnt2d[][] obsPoints = ZhangData.getAllObservedPoints(); 079 080 // Set up the calibrator ------------------------------------------ 081 082 Parameters params = new Calibrator.Parameters(); 083 params.normalizePointCoordinates = true; 084 params.lensDistortionKoeffients = 2; 085 params.useNumericJacobian = true; 086 params.debug = false; 087 088 Calibrator zcalib = new Calibrator(params, modelPoints); 089 for (int i = 0; i < M; i++) { 090 zcalib.addView(obsPoints[i]); 091 } 092 093 // Perform calibration ------------------------------------------ 094 095 Camera camFinal = zcalib.calibrate(); 096 if (camFinal == null) { 097 IJ.error("Calibration failed"); 098 return; 099 } 100 ViewTransform[] finalViews = zcalib.getFinalViews(); 101 102 // Show results ------------------------------------------ 103 104 if (ListCameraIntrinsics) { 105 IJ.log("\n**** Intrinsic camera parameters (common to all views): ****"); 106 IJ.log("Final estimate:\n " + camFinal.toString()); 107 IJ.log("Reference (from EasyCalib):\n " + camReference.toString()); 108 } 109 110 if (ListCameraViews) { 111 IJ.log("\n**** Camera view parameters (3D rotation and translation): ****"); 112 for (int i = 0; i < M; i++) { 113 IJ.log("View " + i + ":\n" + finalViews[i].toString()); 114 } 115 IJ.log(String.format("\nSquared projection error: %.3f\n", 116 zcalib.getProjectionError(camFinal, finalViews, obsPoints))); 117 } 118 119 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 120 121 // draw the projected model (squares) given the camera/view parameters: 122 if (ShowProjectedModel) { 123 ola.setStroke(new ColoredStroke(StrokeWidth, ProjectedModelColor.getColor())); 124 for (int i = 0; i < M; i++) { 125 int sliceNo = i + 1; 126 ola.setStackPosition(sliceNo); 127 Pnt2d[] projPnts = camFinal.project(finalViews[i], modelPoints); 128 for (Shape s : makeQuads(projPnts)) { 129 ola.addShape(s); 130 } 131 } 132 } 133 134 // draw the (pre-calculated) image corner points used for calibration: 135 if (MarkCornerPoints) { 136 ola.setStroke(new ColoredStroke(StrokeWidth, CornerMarkColor.getColor())); 137 // Pnt2d[][] obsPoints = ZhangData.getAllObservedPoints(); 138 for (int i = 0; i < obsPoints.length; i++) { 139 int sliceNo = i + 1; 140 ola.setStackPosition(sliceNo); 141 for (Shape s : makeCircleShapes(obsPoints[i])) { 142 ola.addShape(s); 143 } 144 } 145 } 146 147 testIm.setOverlay(ola.getOverlay()); 148 } 149 150 private List<Shape> makeQuads(Pnt2d[] pnts) { 151 List<Shape> shapes = new ArrayList<>(pnts.length); 152 // 4 successive points make a quad (projected rectangle) 153 for (int i = 0; i < pnts.length; i += 4) { 154 Path2D path = new Path2D.Double(); 155 path.moveTo(pnts[i].getX(), pnts[i].getY()); 156 for (int j = 1; j < 4; j++) { 157 Pnt2d p = pnts[i + j]; 158 path.lineTo(p.getX(), p.getY()); 159 } 160 path.closePath(); 161 shapes.add(path); 162 } 163 return shapes; 164 } 165 166 private List<Shape> makeCircleShapes(Pnt2d[] pnts) { // , Color lineCol 167 final double r = CornerMarkRadius; 168 final double ofs = 0.5; // pixel offset (elements to be placed at pixel centers) 169 List<Shape> shapes = new ArrayList<>(pnts.length); 170 for (int j = 0; j < pnts.length; j++) { 171 double x = pnts[j].getX(); 172 double y = pnts[j].getY(); 173 // OvalRoi circle = new OvalRoi(x - r + ofs, y - r + ofs, 2 * r, 2 * r) ; 174 Shape circle = new Ellipse2D.Double(x - r, y - r, 2 * r, 2 * r); 175 shapes.add(circle); 176 } 177 return shapes; 178 } 179 180 // ------------------------------------------------------------------------- 181 182 private boolean runDialog() { 183 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 184 gd.addHelp(getJavaDocUrl()); 185 gd.setInsets(0, 0, 0); 186 gd.addMessage(formatText(40, 187 "This plugin performs calibration on the supplied test images.", 188 "Note that pre-calculated image corner coordinates are used, i.e.,", 189 "no corner detection is performed.")); 190 191 gd.addCheckbox("List camera intrinsics", ListCameraIntrinsics); 192 gd.addCheckbox("List camera views", ListCameraViews); 193 194 gd.addCheckbox("Show projected model", ShowProjectedModel); 195 gd.addCheckbox("Mark corner points", MarkCornerPoints); 196 197 gd.addEnumChoice("Projected model color", ProjectedModelColor); 198 gd.addEnumChoice("Corner mark color", CornerMarkColor); 199 gd.addNumericField("Corner mark radius", CornerMarkRadius, 1); 200 201 gd.showDialog(); 202 if (gd.wasCanceled()) 203 return false; 204 205 ListCameraIntrinsics = gd.getNextBoolean(); 206 ListCameraViews = gd.getNextBoolean(); 207 208 ShowProjectedModel = gd.getNextBoolean(); 209 MarkCornerPoints = gd.getNextBoolean(); 210 211 ProjectedModelColor = gd.getNextEnumChoice(BasicAwtColor.class); 212 CornerMarkColor = gd.getNextEnumChoice(BasicAwtColor.class); 213 CornerMarkRadius = gd.getNextNumber(); 214 return true; 215 } 216 217}