001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.demos.image;
031
032import java.awt.Container;
033import java.awt.GridLayout;
034import java.awt.event.ActionEvent;
035import java.awt.event.ActionListener;
036import java.io.File;
037import java.io.IOException;
038import java.util.Hashtable;
039
040import javax.swing.BorderFactory;
041import javax.swing.JFrame;
042import javax.swing.JLabel;
043import javax.swing.JPanel;
044import javax.swing.JSlider;
045import javax.swing.JTextField;
046import javax.swing.SpringLayout;
047import javax.swing.event.ChangeEvent;
048import javax.swing.event.ChangeListener;
049
050import org.openimaj.demos.Demo;
051import org.openimaj.image.DisplayUtilities;
052import org.openimaj.image.ImageUtilities;
053import org.openimaj.image.MBFImage;
054import org.openimaj.image.colour.RGBColour;
055import org.openimaj.math.geometry.line.Line2d;
056import org.openimaj.math.geometry.point.Point2d;
057import org.openimaj.math.geometry.point.Point2dImpl;
058
059/**
060 * Radial distortion demo
061 * 
062 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
063 * 
064 */
065@Demo(author = "Sina Samangooei", description = "Demonstrates the radial distortion correction image "
066                + "processor and allows the parameters to be changed interactively "
067                + "and see the results.", keywords = { "image", "warp", "radial",
068                "distortion" }, title = "Radial Distortion Calibrator", icon = "/org/openimaj/demos/icons/image/radial-icon.png")
069public class RadialDistortionCalibrator {
070        private static final int SLIDER_MAX = 1000;
071        private MBFImage outImage;
072        private MBFImage image;
073        private int midX;
074        private int midY;
075        private float alphaY, betaY;
076        private float alphaX, betaX;
077        private JFrame outFrame;
078        private int origMidX;
079        private int origMidY;
080
081        /**
082         * Construct the demo with the given image
083         * @param image the image
084         */
085        public RadialDistortionCalibrator(MBFImage image) {
086                int padding = 200;
087                this.outImage = image.newInstance(image.getWidth() + padding, image.getHeight() + padding);
088                this.image = image;
089                this.midX = outImage.getWidth() / 2;
090                this.midY = outImage.getHeight() / 2;
091                this.origMidX = image.getWidth() / 2;
092                this.origMidY = image.getHeight() / 2;
093                this.alphaX = 0.02f;
094                this.alphaY = 0.04f;
095                this.betaX = 0.02f;
096                this.betaY = 0.04f;
097                regenAndDisplay();
098                createControlWindow();
099        }
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}