View Javadoc

1   /**
2    * Copyright (c) 2011, The University of Southampton and the individual contributors.
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    *
8    *   * 	Redistributions of source code must retain the above copyright notice,
9    * 	this list of conditions and the following disclaimer.
10   *
11   *   *	Redistributions in binary form must reproduce the above copyright notice,
12   * 	this list of conditions and the following disclaimer in the documentation
13   * 	and/or other materials provided with the distribution.
14   *
15   *   *	Neither the name of the University of Southampton nor the names of its
16   * 	contributors may be used to endorse or promote products derived from this
17   * 	software without specific prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.openimaj.demos.image;
31  
32  import java.awt.Container;
33  import java.awt.GridLayout;
34  import java.awt.event.ActionEvent;
35  import java.awt.event.ActionListener;
36  import java.io.File;
37  import java.io.IOException;
38  import java.util.Hashtable;
39  
40  import javax.swing.BorderFactory;
41  import javax.swing.JFrame;
42  import javax.swing.JLabel;
43  import javax.swing.JPanel;
44  import javax.swing.JSlider;
45  import javax.swing.JTextField;
46  import javax.swing.SpringLayout;
47  import javax.swing.event.ChangeEvent;
48  import javax.swing.event.ChangeListener;
49  
50  import org.openimaj.demos.Demo;
51  import org.openimaj.image.DisplayUtilities;
52  import org.openimaj.image.ImageUtilities;
53  import org.openimaj.image.MBFImage;
54  import org.openimaj.image.colour.RGBColour;
55  import org.openimaj.math.geometry.line.Line2d;
56  import org.openimaj.math.geometry.point.Point2d;
57  import org.openimaj.math.geometry.point.Point2dImpl;
58  
59  /**
60   * Radial distortion demo
61   * 
62   * @author Sina Samangooei (ss@ecs.soton.ac.uk)
63   * 
64   */
65  @Demo(author = "Sina Samangooei", description = "Demonstrates the radial distortion correction image "
66  		+ "processor and allows the parameters to be changed interactively "
67  		+ "and see the results.", keywords = { "image", "warp", "radial",
68  		"distortion" }, title = "Radial Distortion Calibrator", icon = "/org/openimaj/demos/icons/image/radial-icon.png")
69  public class RadialDistortionCalibrator {
70  	private static final int SLIDER_MAX = 1000;
71  	private MBFImage outImage;
72  	private MBFImage image;
73  	private int midX;
74  	private int midY;
75  	private float alphaY, betaY;
76  	private float alphaX, betaX;
77  	private JFrame outFrame;
78  	private int origMidX;
79  	private int origMidY;
80  
81  	/**
82  	 * Construct the demo with the given image
83  	 * @param image the image
84  	 */
85  	public RadialDistortionCalibrator(MBFImage image) {
86  		int padding = 200;
87  		this.outImage = image.newInstance(image.getWidth() + padding, image.getHeight() + padding);
88  		this.image = image;
89  		this.midX = outImage.getWidth() / 2;
90  		this.midY = outImage.getHeight() / 2;
91  		this.origMidX = image.getWidth() / 2;
92  		this.origMidY = image.getHeight() / 2;
93  		this.alphaX = 0.02f;
94  		this.alphaY = 0.04f;
95  		this.betaX = 0.02f;
96  		this.betaY = 0.04f;
97  		regenAndDisplay();
98  		createControlWindow();
99  	}
100 
101 	private void createControlWindow() {
102 		JFrame control = new JFrame();
103 		control.setBounds(this.outFrame.getWidth(), 0, 700, 400);
104 		Container cpane = control.getContentPane();
105 		cpane.setLayout(new GridLayout(4, 1));
106 		Container alphaXSlider = createSlider(new AlphaXChanger());
107 		Container alphaYSlider = createSlider(new AlphaYChanger());
108 		Container betaXSlider = createSlider(new BetaXChanger());
109 		Container betaYSlider = createSlider(new BetaYChanger());
110 		cpane.add(alphaXSlider);
111 		cpane.add(betaXSlider);
112 		cpane.add(alphaYSlider);
113 		cpane.add(betaYSlider);
114 
115 		control.setVisible(true);
116 	}
117 
118 	abstract class Changer implements ChangeListener, ActionListener {
119 		JTextField text = null;
120 		JSlider slider = null;
121 
122 		public abstract String getName();
123 
124 		public abstract boolean setNewValue(float value);
125 
126 		public float min() {
127 			return -1f;
128 		}
129 
130 		public float max() {
131 			return 1f;
132 		}
133 
134 		public float range() {
135 			return max() - min();
136 		}
137 
138 		public abstract float def();
139 
140 		@Override
141 		public void stateChanged(ChangeEvent e) {
142 			JSlider source = (JSlider) e.getSource();
143 			if (!source.getValueIsAdjusting()) {
144 				int val = (int) source.getValue();
145 				float prop = (float) val / (float) SLIDER_MAX;
146 				float toSet = min() + range() * prop;
147 				if (setNewValue(toSet)) {
148 					text.setText(toSet + "");
149 				}
150 			}
151 		}
152 
153 		@Override
154 		public void actionPerformed(ActionEvent e) {
155 			JTextField field = (JTextField) e.getSource();
156 			float toSet = Float.parseFloat(field.getText());
157 			if (setNewValue(toSet)) {
158 				slider.setValue((int) (SLIDER_MAX * (toSet - min()) / range()));
159 			}
160 		}
161 
162 	}
163 
164 	class AlphaXChanger extends Changer {
165 		@Override
166 		public String getName() {
167 			return "alpha X";
168 		}
169 
170 		@Override
171 		public float def() {
172 			return alphaX;
173 		}
174 
175 		@Override
176 		public boolean setNewValue(float value) {
177 			boolean change = value != alphaX;
178 			if (change) {
179 				alphaX = value;
180 				regenAndDisplay();
181 			}
182 			return change;
183 		}
184 
185 	}
186 
187 	class AlphaYChanger extends Changer {
188 		@Override
189 		public String getName() {
190 			return "alpha Y";
191 		}
192 
193 		@Override
194 		public float def() {
195 			return alphaY;
196 		}
197 
198 		@Override
199 		public boolean setNewValue(float value) {
200 			boolean change = value != alphaY;
201 			if (change) {
202 				alphaY = value;
203 				regenAndDisplay();
204 			}
205 			return change;
206 		}
207 	}
208 
209 	class BetaXChanger extends Changer {
210 		@Override
211 		public String getName() {
212 			return "beta X";
213 		}
214 
215 		@Override
216 		public float def() {
217 			return betaX;
218 		}
219 
220 		@Override
221 		public boolean setNewValue(float value) {
222 			boolean change = value != betaX;
223 			if (change) {
224 				betaX = value;
225 				regenAndDisplay();
226 			}
227 			return change;
228 		}
229 
230 	}
231 
232 	class BetaYChanger extends Changer {
233 		@Override
234 		public String getName() {
235 			return "beta Y";
236 		}
237 
238 		@Override
239 		public float def() {
240 			return betaY;
241 		}
242 
243 		@Override
244 		public boolean setNewValue(float value) {
245 			boolean change = value != betaY;
246 			if (change) {
247 				betaY = value;
248 				regenAndDisplay();
249 			}
250 			return change;
251 		}
252 	}
253 
254 	private Container createSlider(Changer changer) {
255 		JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, SLIDER_MAX,
256 				(int) (SLIDER_MAX * ((changer.def() - changer.min()) / changer
257 						.range())));
258 		Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>();
259 		labelTable.put(new Integer(0), new JLabel("" + changer.min()));
260 		labelTable.put(new Integer(SLIDER_MAX), new JLabel("" + changer.max()));
261 		for (float i = 1; i < 10f; i++) {
262 			float prop = (i / 10f);
263 			String s = String.format("%.2f", (changer.min() + changer.range()
264 					* prop));
265 			labelTable.put(new Integer((int) (prop * SLIDER_MAX)),
266 					new JLabel(s));
267 		}
268 		slider.setLabelTable(labelTable);
269 		slider.setPaintLabels(true);
270 		slider.setBorder(BorderFactory.createTitledBorder(changer.getName()));
271 		slider.addChangeListener(changer);
272 		JPanel sliderHolder = new JPanel();
273 		SpringLayout layout = new SpringLayout();
274 		sliderHolder.setLayout(layout);
275 		sliderHolder.add(slider);
276 		JTextField text = new JTextField("" + changer.def(), 10);
277 		text.addActionListener(changer);
278 		sliderHolder.add(text);
279 		layout.putConstraint(SpringLayout.WEST, slider, 5, SpringLayout.WEST,
280 				sliderHolder);
281 		layout.putConstraint(SpringLayout.WEST, text, 5, SpringLayout.EAST,
282 				slider);
283 		layout.putConstraint(SpringLayout.EAST, sliderHolder, 5,
284 				SpringLayout.EAST, text);
285 		layout.putConstraint(SpringLayout.WEST, sliderHolder, 10,
286 				SpringLayout.WEST, slider);
287 		changer.slider = slider;
288 		changer.text = text;
289 		return sliderHolder;
290 	}
291 
292 	private Point2d getDistortedPoint(Point2d point) {
293 		// this pixel relative to the padding
294 		float paddingX = point.getX();
295 		float paddingY = point.getY();
296 		// Normalise x and y such that they are in a -1 to 1 range
297 		float normX = (paddingX - midX) / (image.getWidth() / 2.0f);
298 		float normY = (paddingY - midY) / (image.getHeight() / 2.0f);
299 
300 		float radius2 = normX * normX + normY * normY;
301 		float radius4 = radius2 * radius2;
302 
303 		float xRatio = normX / (1 - alphaX * radius2 - betaX * radius4);
304 		float yRatio = normY / (1 - alphaY * radius2 - betaY * radius4);
305 
306 		float radiusRatio2 = xRatio * xRatio + yRatio * yRatio;
307 		float radiusRatio4 = radiusRatio2 * radiusRatio2;
308 
309 		float normDistortedX = normX
310 				/ (1 - alphaX * radiusRatio2 - betaX * radiusRatio4);
311 		float normDistortedY = normY
312 				/ (1 - alphaY * radiusRatio2 - betaY * radiusRatio4);
313 
314 		float distortedX = ((1 + normDistortedX) / 2) * image.getWidth();
315 		float distortedY = ((1 + normDistortedY) / 2) * image.getHeight();
316 		return new Point2dImpl(distortedX, distortedY);
317 	}
318 
319 	/**
320 	 * Compute the transformed point for a given point
321 	 * @param point the input point
322 	 * @return the transformed point
323 	 */
324 	public Point2d getUndistortedPoint(Point2d point) {
325 		// this pixel relative to the padding
326 		float x = point.getX();
327 		float y = point.getY();
328 		// Normalise x and y such that they are in a -1 to 1 range
329 		float normX = (x - origMidX) / (outImage.getWidth() / 2.0f);
330 		float normY = (y - origMidY) / (outImage.getHeight() / 2.0f);
331 
332 		float radius2 = normX * normX + normY * normY;
333 		float radius4 = radius2 * radius2;
334 
335 		float normundistortedX = normX - alphaX * normX * radius2 - betaX
336 				* normX * radius4;
337 		float normundistortedY = normY - alphaY * normY * radius2 - betaY
338 				* normY * radius4;
339 
340 		float undistortedX = ((1 + normundistortedX) / 2) * outImage.getWidth();
341 		float undistortedY = ((1 + normundistortedY) / 2)
342 				* outImage.getHeight();
343 		return new Point2dImpl(undistortedX, undistortedY);
344 	}
345 
346 	private void regenAndDisplay() {
347 		double sumDistance = 0;
348 		for (float y = 0; y < outImage.getHeight(); y++) {
349 			for (float x = 0; x < outImage.getWidth(); x++) {
350 				Point2dImpl point = new Point2dImpl(x, y);
351 				Point2d distorted = getDistortedPoint(point);
352 
353 				if (image.getBounds().isInside(distorted)) {
354 					Point2d undistorted = getUndistortedPoint(distorted);
355 					sumDistance += new Line2d(point, undistorted)
356 							.calculateLength();
357 				}
358 
359 				outImage.setPixel((int) x, (int) y, image.getPixelInterp(
360 						distorted.getX(), distorted.getY(), RGBColour.BLACK));
361 			}
362 		}
363 		System.out.println("Sum difference: " + sumDistance);
364 
365 		if (this.outFrame == null) {
366 			outFrame = DisplayUtilities.display(outImage);
367 		} else {
368 			DisplayUtilities.display(outImage, outFrame);
369 		}
370 	}
371 
372 	/**
373 	 * The main method.
374 	 * @param args path to an image; or empty
375 	 * @throws IOException
376 	 */
377 	public static void main(String args[]) throws IOException {
378 		if (args.length == 0)
379 			new RadialDistortionCalibrator(
380 					ImageUtilities.readMBF(RadialDistortionCalibrator.class
381 							.getResourceAsStream("/org/openimaj/demos/image/35smm_original.jpg")));
382 		else
383 			new RadialDistortionCalibrator(ImageUtilities.readMBF(new File(
384 					args[0])));
385 	}
386 }