All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.examples.reconstruction.ExampleLoopClosure Maven / Gradle / Ivy

/*
 * Copyright (c) 2022, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.examples.reconstruction;

import boofcv.abst.feature.detdesc.DetectDescribePoint;
import boofcv.abst.scene.FeatureSceneRecognition;
import boofcv.abst.scene.SceneRecognition;
import boofcv.abst.scene.nister2006.ConfigRecognitionNister2006;
import boofcv.factory.feature.associate.ConfigAssociateGreedy;
import boofcv.factory.feature.associate.FactoryAssociation;
import boofcv.factory.feature.detdesc.FactoryDetectDescribe;
import boofcv.factory.scene.FactorySceneRecognition;
import boofcv.io.UtilIO;
import boofcv.io.image.UtilImageIO;
import boofcv.struct.feature.TupleDesc_F64;
import boofcv.struct.image.GrayU8;
import georegression.struct.point.Point2D_F64;
import org.ddogleg.struct.DogArray;
import org.ddogleg.struct.FastAccess;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static boofcv.io.image.UtilImageIO.videoToImages;

/**
 * Shows how you can detect if two images are of the same scene. This is known as loop closure and is done in
 * robotic mapping, e.g. SLAM. Here will use a fast recognition approach that takes only a few milliseconds to find
 * the most likely candidate images using image features alone. After that we perform feature matching to reduce
 * false positives. A complete solution would involve a geometric check, i.e. Fundamental matrix.
 *
 * Using scene recognition drastically reduces computational time as it eliminates most bad matches. As a result
 * this can run in a real-time or near real-time environment.
 *
 * @author Peter Abeles
 */
public class ExampleLoopClosure {
	public static void main( String[] args ) {
		System.out.println("Finding Images");
		String pathImages = "loop_closure";
		videoToImages(UtilIO.pathExample("mvs/stone_sign.mp4"), pathImages);
		List imagePaths = UtilIO.listSmart(String.format("glob:%s/*.png", pathImages), true, ( f ) -> true);

		// Create the feature detector. Default settings are often not the best configuration for recognition.
		// Finding the best settings is left as an exercise for the reader.
		DetectDescribePoint detector =
				FactoryDetectDescribe.surfFast(null, null, null, GrayU8.class);

		// Detect features in all the images
		var descriptions = new ArrayList>();
		var locations = new ArrayList>();

		System.out.println("Feature Detection");
		for (int pathIdx = 0; pathIdx < imagePaths.size(); pathIdx++) {
			// Print out the progress
			System.out.print("*");
			if (pathIdx%80 == 79)
				System.out.println();

			// Load the image and detect features
			String path = imagePaths.get(pathIdx);
			GrayU8 gray = UtilImageIO.loadImage(path, GrayU8.class);

			detector.detect(gray);

			// Copy all the features into lists for this image
			var imageDescriptions = new DogArray<>(detector::createDescription);
			var imageLocations = new DogArray<>(Point2D_F64::new);

			for (int i = 0; i < detector.getNumberOfFeatures(); i++) {
				imageDescriptions.grow().setTo(detector.getDescription(i));
				imageLocations.grow().setTo(detector.getLocation(i));
			}
			descriptions.add(imageDescriptions);
			locations.add(imageLocations);
		}
		System.out.println();

		// Put feature information into a format scene recognition understands
		var listRecFeat = new ArrayList>();
		for (int i = 0; i < descriptions.size(); i++) {
			FastAccess pixels = locations.get(i);
			FastAccess descs = descriptions.get(i);
			listRecFeat.add(new FeatureSceneRecognition.Features<>() {
				@Override public Point2D_F64 getPixel( int index ) {return pixels.get(index);}

				@Override public TupleDesc_F64 getDescription( int index ) {return descs.get(index);}

				@Override public int size() {return pixels.size();}
			});
		}

		System.out.println("Learning model. Can take a minute. You can save and reload this model.");

		var config = new ConfigRecognitionNister2006();
		config.learningMinimumPointsForChildren.setFixed(20);
		FeatureSceneRecognition recognizer =
				FactorySceneRecognition.createSceneNister2006(config, detector::createDescription);

		// Pass image information in as an iterator that it understands.
		recognizer.learnModel(new Iterator<>() {
			int imageIndex = 0;

			@Override public boolean hasNext() {return imageIndex < descriptions.size();}

			@Override public FeatureSceneRecognition.Features next() {
				return listRecFeat.get(imageIndex++);
			}
		});

		// To find functions for saving and loading these models look at RecognitionIO

		System.out.println("Creating database");
		for (int imageIdx = 0; imageIdx < descriptions.size(); imageIdx++) {
			// Note that image are assigned a name equal to their index
			recognizer.addImage(imageIdx + "", listRecFeat.get(imageIdx));
		}

		System.out.println("Scoring likely loop closures");

		// Have a strict requirement for matching to reduce false positives
		var configAssociate = new ConfigAssociateGreedy();
		configAssociate.forwardsBackwards = true;
		configAssociate.scoreRatioThreshold = 0.9;

		var scorer = FactoryAssociation.scoreEuclidean(detector.getDescriptionType(), true);
		var associate = FactoryAssociation.greedy(configAssociate, scorer);

		// Go through all the images and use scene recongition to greatly reduce the number of images that need
		// to be considered. Scene recognition is very fast, while feature matching is slow, and geometric
		// checks are even slower.
		var matches = new DogArray<>(SceneRecognition.Match::new);
		for (int imageIdx = 0; imageIdx < descriptions.size(); imageIdx++) {
			// Query results to find the best matches.
			// We are going to pass in a filter that will remove all the most recent frames since we don't care
			// about those. This way we know all the returned results are potential loop closures.
			int _imageIdx = imageIdx;
			recognizer.query(
					/*query*/ listRecFeat.get(imageIdx),
					/*filter*/ ( id ) -> Math.abs(_imageIdx - Integer.parseInt(id)) > 20,
					/*limit*/ 5, /*found matches*/ matches);

			// Set up association
			associate.setSource(descriptions.get(imageIdx));
			int numFeatures = descriptions.get(imageIdx).size;

			System.out.printf("Image[%3d]\n", imageIdx);
			for (var m : matches.toList()) {
				// Note how earlier it assigned the image name to be the index value as a string
				int imageDstIdx = Integer.parseInt(m.id);

				// Perform association
				associate.setDestination(descriptions.get(imageDstIdx));
				associate.associate();

				// Compute and print quality of fit metrics
				double matchFraction = associate.getMatches().size/(double)numFeatures;
				System.out.printf("  %4s error=%.2f matches=%.2f\n", m.id, m.error, matchFraction);

				// A loop closure will have a large number of matching features. When the fraction goes
				// over 30% in this example, you probably have a good match.

				// Typically a geometric check is done next, such as estimating a fundamental matrix or PNP.
				// With a geometric check the odds of a false positive are low.
			}
		}
		System.out.println("Done!");
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy