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.video.videosift; 031 032import java.awt.event.KeyEvent; 033import java.awt.event.KeyListener; 034import java.util.ArrayList; 035import java.util.List; 036 037import javax.swing.SwingUtilities; 038 039import org.openimaj.demos.video.utils.PolygonDrawingListener; 040import org.openimaj.demos.video.utils.PolygonExtractionProcessor; 041import org.openimaj.feature.local.list.LocalFeatureList; 042import org.openimaj.feature.local.matcher.FastBasicKeypointMatcher; 043import org.openimaj.feature.local.matcher.MatchingUtilities; 044import org.openimaj.feature.local.matcher.consistent.ConsistentLocalFeatureMatcher2d; 045import org.openimaj.image.FImage; 046import org.openimaj.image.MBFImage; 047import org.openimaj.image.colour.RGBColour; 048import org.openimaj.image.colour.Transforms; 049import org.openimaj.image.feature.local.engine.DoGSIFTEngine; 050import org.openimaj.image.feature.local.keypoints.Keypoint; 051import org.openimaj.image.processing.resize.ResizeProcessor; 052import org.openimaj.image.processing.transform.ProjectionProcessor; 053import org.openimaj.image.renderer.MBFImageRenderer; 054import org.openimaj.math.geometry.point.Point2d; 055import org.openimaj.math.geometry.point.Point2dImpl; 056import org.openimaj.math.geometry.shape.Polygon; 057import org.openimaj.math.geometry.shape.Shape; 058import org.openimaj.math.geometry.transforms.HomographyModel; 059import org.openimaj.math.geometry.transforms.HomographyRefinement; 060import org.openimaj.math.geometry.transforms.MatrixTransformProvider; 061import org.openimaj.math.geometry.transforms.TransformUtilities; 062import org.openimaj.math.geometry.transforms.estimation.RobustHomographyEstimator; 063import org.openimaj.math.geometry.transforms.residuals.SingleImageTransferResidual2d; 064import org.openimaj.math.model.fit.RANSAC; 065import org.openimaj.util.pair.IndependentPair; 066import org.openimaj.video.VideoDisplay; 067import org.openimaj.video.VideoDisplayListener; 068import org.openimaj.video.capture.VideoCapture; 069import org.openimaj.video.tracking.klt.Feature; 070import org.openimaj.video.tracking.klt.FeatureList; 071import org.openimaj.video.tracking.klt.KLTTracker; 072import org.openimaj.video.tracking.klt.TrackingContext; 073 074import Jama.Matrix; 075 076public class VideoKLTSIFT implements KeyListener, VideoDisplayListener<MBFImage> { 077 enum Mode { 078 TRACKING, LOOKING, NONE, START_LOOKING 079 } 080 081 private VideoCapture capture; 082 private VideoDisplay<MBFImage> videoFrame; 083 private KLTTracker tracker; 084 private FeatureList fl; 085 086 private FImage oldFrame; 087 private int frameNumber = 0; 088 private int nFeatures = 50; 089 private int nOriginalFoundFeatures = -1; 090 private DoGSIFTEngine engine; 091 private PolygonDrawingListener polygonListener; 092 private Mode mode = Mode.NONE; 093 private FeatureList initialFeatures; 094 private Polygon initialShape; 095 private ConsistentLocalFeatureMatcher2d<Keypoint> siftMatcher; 096 private MBFImage modelImage; 097 private MBFImage overlayFrame = null; 098 099 public VideoKLTSIFT() throws Exception { 100 capture = new VideoCapture(640, 480); 101 polygonListener = new PolygonDrawingListener(); 102 videoFrame = VideoDisplay.createVideoDisplay(capture); 103 videoFrame.getScreen().addMouseListener(polygonListener); 104 // videoFrame.getScreen().setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 105 videoFrame.addVideoListener(this); 106 SwingUtilities.getRoot(videoFrame.getScreen()).addKeyListener(this); 107 108 reinitTracker(); 109 } 110 111 @Override 112 public void afterUpdate(VideoDisplay<MBFImage> arg0) { 113 // TODO Auto-generated method stub 114 115 } 116 117 @Override 118 public synchronized void beforeUpdate(MBFImage image) { 119 this.polygonListener.drawPoints(image); 120 if (videoFrame.isPaused()) 121 { 122 return; 123 } 124 FImage greyFrame = null; 125 greyFrame = Transforms.calculateIntensityNTSC(image); 126 // If we are in looking mode, Use matcher to find a likely position 127 // every 5th frame 128 if (this.mode == Mode.LOOKING) { 129 130 final Shape shape = findObject(greyFrame); 131 if (shape == null) { 132 this.oldFrame = greyFrame; 133 return; 134 } 135 System.out.println("Object FOUND, switcihg to tracking mode"); 136 // If we find a likely position, init the tracker, we are now 137 // tracking 138 initTracking(greyFrame, shape); 139 this.mode = Mode.TRACKING; 140 } 141 // If we are tracking, attempt to track the points every frame 142 else if (this.mode == Mode.TRACKING) { 143 144 continueTracking(greyFrame); 145 // If we don't track enough points, look again. 146 if (fl.countRemainingFeatures() == 0 || fl.countRemainingFeatures() < nOriginalFoundFeatures * 0.2) 147 { 148 System.out.println("Object LOST, switching to LOOKING mode"); 149 this.mode = Mode.LOOKING; 150 reinitTracker(); 151 } 152 // else if(fl.countRemainingFeatures() < nOriginalFoundFeatures * 153 // 0.8){ 154 // initTracking(greyFrame,polygonToDraw); 155 // } 156 } 157 else if (this.mode == Mode.START_LOOKING) { 158 this.reinitTracker(); 159 final Polygon p = this.polygonListener.getPolygon().clone(); 160 this.polygonListener.reset(); 161 final MBFImage modelImage = capture.getCurrentFrame(); 162 this.initTracking(greyFrame, p); 163 this.initObjectFinder(modelImage, p); 164 this.oldFrame = greyFrame; 165 mode = Mode.LOOKING; 166 return; 167 } 168 169 if (overlayFrame != null) { 170 drawOverlay(image); 171 } 172 else { 173 drawDebug(image, greyFrame); 174 } 175 176 this.oldFrame = greyFrame; 177 178 } 179 180 private void drawOverlay(MBFImage image) { 181 final ProjectionProcessor<Float[], MBFImage> proc = new ProjectionProcessor<Float[], MBFImage>(); 182 image.accumulateWith(proc); 183 final Matrix model = this.estimateModel(); 184 if (model != null) { 185 proc.setMatrix(model); 186 this.overlayFrame.accumulateWith(proc); 187 } 188 image.internalAssign(proc.performProjection()); 189 } 190 191 private void drawDebug(MBFImage image, FImage greyFrame) { 192 this.polygonListener.drawPoints(image); 193 194 final MBFImageRenderer renderer = image.createRenderer(); 195 196 if (this.initialShape != null) { 197 renderer.drawPolygon(initialShape, RGBColour.RED); 198 } 199 if (this.initialFeatures != null) { 200 image.internalAssign(MatchingUtilities.drawMatches(image, this.findAllMatchedPairs(), RGBColour.WHITE)); 201 final Matrix esitmatedModel = this.estimateModel(); 202 if (esitmatedModel != null) 203 { 204 final Polygon newPolygon = initialShape.transform(esitmatedModel); 205 renderer.drawPolygon(newPolygon, RGBColour.GREEN); 206 if (fl.countRemainingFeatures() < nOriginalFoundFeatures * 0.5) { 207 reinitTracker(); 208 initTracking(greyFrame, newPolygon); 209 } 210 } 211 estimateMovement(); 212 } 213 } 214 215 private Shape findObject(FImage capImg) { 216 final float scaleImage = .5f; 217 218 final ResizeProcessor resize = new ResizeProcessor(scaleImage); 219 capImg = capImg.process(resize); 220 Shape sh = null; 221 if (siftMatcher != null && !videoFrame.isPaused() && engine != null) { 222 final LocalFeatureList<Keypoint> kpl = engine.findFeatures(capImg); 223 if (siftMatcher.findMatches(kpl)) { 224 Matrix shTransform = ((MatrixTransformProvider) siftMatcher.getModel()).getTransform().copy(); 225 if (shTransform != null) 226 { 227 try { 228 shTransform = TransformUtilities.scaleMatrix(1f / scaleImage, 1f / scaleImage).times( 229 shTransform.inverse()); 230 sh = modelImage.getBounds().transform(shTransform); 231 } catch (final Exception e) { 232 e.printStackTrace(); 233 } 234 } 235 236 } 237 238 } 239 return sh; 240 241 } 242 243 private void reinitTracker() { 244 final TrackingContext tc = new TrackingContext(); 245 fl = new FeatureList(nFeatures); 246 tracker = new KLTTracker(tc, fl); 247 tracker.setVerbosity(0); 248 249 tc.setSequentialMode(true); 250 tc.setWriteInternalImages(false); 251 tc.setAffineConsistencyCheck(-1); /* 252 * set this to 2 to turn on affine 253 * consistency check 254 */ 255 this.initialFeatures = null; 256 this.initialShape = null; 257 } 258 259 public void initTracking(FImage greyFrame, Shape location) { 260 frameNumber = 0; 261 tracker.getTrackingContext().setTargetArea(location); 262 tracker.selectGoodFeatures(greyFrame); 263 nOriginalFoundFeatures = fl.countRemainingFeatures(); 264 initialFeatures = fl.clone(); 265 initialShape = location.asPolygon().clone(); 266 } 267 268 public void continueTracking(FImage greyFrame) { 269 tracker.trackFeatures(oldFrame, greyFrame); 270 this.frameNumber++; 271 } 272 273 private Matrix estimateModel() { 274 if (this.initialFeatures == null) { 275 return null; 276 } 277 final List<? extends IndependentPair<Point2d, Point2d>> pairs = findAllMatchedPairs(); 278 final HomographyModel model = new HomographyModel(); 279 // model.estimate(pairs); 280 final RANSAC<Point2d, Point2d, HomographyModel> fitter = new RANSAC<Point2d, Point2d, HomographyModel>(model, 281 new SingleImageTransferResidual2d<HomographyModel>(), 282 10.0, 1500, 283 new RANSAC.PercentageInliersStoppingCondition(0.5), false); 284 if (!fitter.fitData(pairs)) 285 return null; 286 287 model.getTransform().print(5, 5); 288 return model.getTransform(); 289 } 290 291 private List<IndependentPair<Point2d, Point2d>> findAllMatchedPairs() { 292 final List<IndependentPair<Point2d, Point2d>> pairs = new ArrayList<IndependentPair<Point2d, Point2d>>(); 293 for (int i = 0; i < this.initialFeatures.features.length; i++) { 294 final Feature oldFeature = this.initialFeatures.features[i].clone(); 295 final Feature newFeature = fl.features[i].clone(); 296 if (oldFeature.val >= 0 && newFeature.val >= 0) { 297 pairs.add(new IndependentPair<Point2d, Point2d>(oldFeature, newFeature)); 298 } 299 } 300 return pairs; 301 } 302 303 public Point2dImpl estimateMovement() { 304 final Feature[] oldFeatures = this.initialFeatures.features; 305 float sumX = 0; 306 float sumY = 0; 307 float total = 0; 308 if (oldFeatures != null) { 309 for (int i = 0; i < oldFeatures.length; i++) { 310 final Feature oldFeature = oldFeatures[i]; 311 final Feature newFeature = fl.features[i]; 312 if (oldFeature.val >= 0 && newFeature.val >= 0) { 313 sumX += newFeature.x - oldFeature.x; 314 sumY += newFeature.y - oldFeature.y; 315 total += 1f; 316 } 317 } 318 sumX /= total; 319 sumY /= total; 320 System.out.println("Average displacement: " + sumX + "," + sumY); 321 } 322 return new Point2dImpl(sumX, sumY); 323 } 324 325 @Override 326 public void keyTyped(KeyEvent e) { 327 328 } 329 330 @Override 331 public synchronized void keyPressed(KeyEvent key) { 332 if (key.getKeyCode() == KeyEvent.VK_SPACE) { 333 this.videoFrame.togglePause(); 334 } else if (key.getKeyChar() == 'c' && this.polygonListener.getPolygon().getVertices().size() > 2) { 335 mode = Mode.START_LOOKING; 336 } else if (key.getKeyChar() == 'd' && this.polygonListener.getPolygon().getVertices().size() > 2) { 337 final Polygon p = this.polygonListener.getPolygon().clone(); 338 this.polygonListener.reset(); 339 overlayFrame = this.capture.getCurrentFrame().process( 340 new PolygonExtractionProcessor<Float[], MBFImage>(p, RGBColour.BLACK)); 341 } 342 else if (key.getKeyChar() == 'r') { 343 this.mode = Mode.NONE; 344 345 } 346 } 347 348 private void initObjectFinder(MBFImage frame, Polygon p) { 349 modelImage = frame.process(new PolygonExtractionProcessor<Float[], MBFImage>(p, RGBColour.BLACK)); 350 351 // configure the matcher 352 siftMatcher = new ConsistentLocalFeatureMatcher2d<Keypoint>(new FastBasicKeypointMatcher<Keypoint>(8)); 353 siftMatcher.setFittingModel(new RobustHomographyEstimator(10.0, 1500, 354 new RANSAC.PercentageInliersStoppingCondition(0.5), HomographyRefinement.NONE)); 355 356 engine = new DoGSIFTEngine(); 357 engine.getOptions().setDoubleInitialImage(true); 358 359 final FImage modelF = Transforms.calculateIntensityNTSC(modelImage); 360 siftMatcher.setModelFeatures(engine.findFeatures(modelF)); 361 } 362 363 @Override 364 public void keyReleased(KeyEvent e) { 365 // TODO Auto-generated method stub 366 367 } 368 369 public static void main(String args[]) throws Exception { 370 new VideoKLTSIFT(); 371 } 372 373}