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_2; 008 009import ij.IJ; 010import ij.ImagePlus; 011import ij.ImageStack; 012import ij.gui.GenericDialog; 013import ij.plugin.PlugIn; 014import ij.process.ByteProcessor; 015import imagingbook.calibration.zhang.Camera; 016import imagingbook.calibration.zhang.ViewTransform; 017import imagingbook.calibration.zhang.data.CalibrationImage; 018import imagingbook.calibration.zhang.data.ZhangData; 019import imagingbook.calibration.zhang.util.MathUtil; 020import imagingbook.common.color.sets.BasicAwtColor; 021import imagingbook.common.geometry.basic.Pnt2d; 022import imagingbook.common.ij.overlay.ColoredStroke; 023import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 024import imagingbook.core.jdoc.JavaDocHelp; 025import imagingbook.core.resource.ImageResource; 026import org.apache.commons.math3.geometry.euclidean.threed.Rotation; 027 028import java.awt.Shape; 029import java.awt.geom.Path2D; 030import java.util.ArrayList; 031import java.util.List; 032 033import static imagingbook.common.ij.DialogUtils.formatText; 034 035/** 036 * This plugin performs interpolation of views, given a sequence of key views. Translations (3D camera positions) are 037 * interpolated linearly. Pairs of rotations are interpolated by linear mixture of the corresponding quaternion 038 * representations (see 039 * http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/#How_do_I_interpolate_between_2_quaternions__). 040 * 041 * @author WB 042 * @version 2022/12/19 043 */ 044public class View_Interpolation_Demo implements PlugIn, JavaDocHelp { 045 046 private static ImageResource resource = CalibrationImage.CalibImageStack; 047 private static int NumberOfInterpolatedFrames = 10; 048 private static double PeakHeightZ = -1.5; 049 private static BasicAwtColor BackGroundColor = BasicAwtColor.White; 050 private static BasicAwtColor LineColor = BasicAwtColor.Black; 051 private static double LineWidth = 1.0; 052 053 054 public void run(String arg0) { 055 ImagePlus testIm = resource.getImagePlus(); 056 if (testIm == null) { 057 IJ.error("Could not open calibration images!"); 058 return; 059 } 060 061 if (!runDialog()) { 062 return; 063 } 064 065 Camera cam = ZhangData.getCameraIntrinsics(); 066 Pnt2d[] modelPoints = ZhangData.getModelPoints(); 067 068 final int w = testIm.getWidth(); 069 final int h = testIm.getHeight(); 070 final int M = testIm.getNSlices(); 071 072 ImageStack animStack = new ImageStack(w, h); 073 ByteProcessor bgIp = new ByteProcessor(w, h); // background image for all stack slices 074 bgIp.setColor(BackGroundColor.getColor()); 075 bgIp.fill(); 076 077 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 078 ola.setStroke(new ColoredStroke(LineWidth, LineColor.getColor())); 079 080 int sliceNo = 1; 081 for (int A = 0; A < M; A++) { 082 int B = (A + 1) % M; 083 ViewTransform viewA = ZhangData.getViewTransform(A); // view A 084 ViewTransform viewB = ZhangData.getViewTransform(B); // view B 085 086 Rotation rA = viewA.getRotation(); 087 Rotation rB = viewB.getRotation(); 088 double[] tA = viewA.getTranslation(); 089 double[] tB = viewB.getTranslation(); 090 091 // interpolation step k for view pair (A,B) 092 for (int k = 0; k < NumberOfInterpolatedFrames; k++) { 093 double alpha = (double) k / NumberOfInterpolatedFrames; 094 Rotation rk = MathUtil.Lerp(rA, rB, alpha); // interpolate rotation 095 double[] tk = MathUtil.Lerp(tA, tB, alpha); // interpolate translation 096 ViewTransform viewK = new ViewTransform(rk, tk); 097 098 String sliceLabel = String.format("frame-%d-%d", A, k); 099 animStack.addSlice(sliceLabel, bgIp); // dummy image with white background 100 ola.setStackPosition(sliceNo++); 101 for (Shape s : makePyramids(cam, viewK, modelPoints)) { 102 ola.addShape(s); 103 } 104 } 105 } 106 ImagePlus animIm = new ImagePlus("Animation", animStack); 107 animIm.setOverlay(ola.getOverlay()); 108 animIm.show(); 109 } 110 111 // ---------------------------------------------------------------------- 112 113 List<Shape> makePyramids(Camera cam, ViewTransform view, Pnt2d[] modelPoints) { 114 List<Shape> shapes = new ArrayList<>(); 115 for (int i = 0; i < modelPoints.length; i += 4) { 116 Pnt2d[] modelSq = new Pnt2d[4]; 117 Pnt2d[] imageSq = new Pnt2d[4]; 118 // 3D points p0,...,p3 define a model square in the Z=0 plane 119 for (int j = 0; j < 4; j++) { 120 modelSq[j] = modelPoints[i + j]; 121 imageSq[j] = MathUtil.toPnt2d(cam.project(view, modelSq[j])); 122 } 123 // make the 3D pyramid peak and project to 2D: 124 double[] modelPeak3d = new double[3]; 125 modelPeak3d[0] = (modelSq[0].getX() + modelSq[2].getX()) / 2; // X 126 modelPeak3d[1] = (modelSq[0].getY() + modelSq[2].getY()) / 2; // Y 127 modelPeak3d[2] = PeakHeightZ; // Z 128 Pnt2d pk = MathUtil.toPnt2d(cam.project(view, modelPeak3d)); 129 // make and add the projected pyramid for this model quad: 130 shapes.add(makePyramidShape(imageSq, pk)); 131 } 132 return shapes; 133 } 134 135 private Shape makePyramidShape(Pnt2d[] pnts, Pnt2d pk) { // takes 4 base points + 1 peak point 136 Path2D path = new Path2D.Double(); 137 // draw the base quad: 138 path.moveTo(pnts[0].getX(), pnts[0].getY()); 139 for (int j = 1; j < 4; j++) { 140 path.lineTo(pnts[j].getX(), pnts[j].getY()); 141 } 142 path.closePath(); 143 // draw lines to pyramid peak 144 for (int j = 0; j < 4; j++) { 145 path.moveTo(pnts[j].getX(), pnts[j].getY()); 146 path.lineTo(pk.getX(), pk.getY()); 147 } 148 return path; 149 } 150 151 // ------------------------------------------------------------------------------------ 152 153 private boolean runDialog() { 154 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 155 gd.addHelp(getJavaDocUrl()); 156 gd.setInsets(0, 0, 0); 157 gd.addMessage(formatText(40, 158 "This plugin performs interpolation of views, given a sequence of key views.")); 159 160 gd.addNumericField("Number of interpolated frames", NumberOfInterpolatedFrames); 161 gd.addNumericField("Pyramid peak height (inches)", PeakHeightZ); 162 163 gd.showDialog(); 164 if (gd.wasCanceled()) 165 return false; 166 167 NumberOfInterpolatedFrames = (int) gd.getNextNumber(); 168 PeakHeightZ = gd.getNextNumber(); 169 return true; 170 } 171 172}