
/* autopano-sift, Automatic panorama image creation
 * Copyright (C) 2004 -- Sebastian Nowozin
 *
 * This program is free software released under the GNU General Public
 * License, which is included in this software package (doc/LICENSE).
 */

/* MatchKeys.cs
 *
 * Keypoint file correlation functionality.
 *
 * (C) Copyright 2004 -- Sebastian Nowozin (nowozin@cs.tu-berlin.de)
 *
 * "The University of British Columbia has applied for a patent on the SIFT
 * algorithm in the United States. Commercial applications of this software
 * may require a license from the University of British Columbia."
 * For more information, see the LICENSE file supplied with the distribution.
 */

using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;


public class MatchKeys
{
	public ArrayList FindMatchesBBF (KeypointN[] keys1, KeypointN[] keys2)
	{
		// TODO: swap so smaller list is searched.

		ArrayList al = new ArrayList ();
		foreach (KeypointN kp in keys2)
			al.Add (kp);
		KDTree kd = KDTree.CreateKDTree (al);

		ArrayList matches = new ArrayList ();
		foreach (KeypointN kp in keys1) {
			ArrayList kpNNList = kd.NearestNeighbourListBBF (kp, 2, 40);

			if (kpNNList.Count < 2)
				throw (new Exception ("BUG: less than two neighbours!"));

			KDTree.BestEntry be1 = (KDTree.BestEntry) kpNNList[0];
			KDTree.BestEntry be2 = (KDTree.BestEntry) kpNNList[1];

			if ((be1.Distance / be2.Distance) > 0.6)
				continue;

			KeypointN kpN = (KeypointN) be1.Neighbour;

			matches.Add (new Match (kp, kpN, be1.Distance, be2.Distance));

			/*
			Console.WriteLine ("({0},{1}) ({2},{3}) {4}, 2nd: {5}", (int)(kp.X + 0.5),
				(int)(kp.Y + 0.5), (int)(kpN.X + 0.5),
				(int)(kpN.Y + 0.5), be1.Distance, be2.Distance);
			*/
		}

		return (matches);
	}

	/*
	public ArrayList FindMatches (ArrayList keys1, ArrayList keys2)
	{
		ArrayList matches = new ArrayList ();

		//KDTree kd = KDTree.CreateKDTree (keys2);
		foreach (Keypoint kp in keys1) {
			double distNearest = Double.PositiveInfinity;
			int nearest = -1;
			double dist2Nearest = Double.PositiveInfinity;
			int nearest2 = -1;

			for (int kn = 0 ; kn < keys2.Count ; ++kn) {
				Keypoint kp2 = (Keypoint) keys2[kn];

				double dist = Math.Sqrt (KDTree.DistanceSq (kp, kp2));
				if (dist < distNearest) {
					nearest2 = nearest;
					dist2Nearest = distNearest;

					nearest = kn;
					distNearest = dist;
				}
			}

			if (nearest == -1 || nearest2 == -1)
				continue;

			if ((distNearest / dist2Nearest) > 0.6)
				continue;

			matches.Add (new Match (kp, (Keypoint) keys2[nearest]));

			Console.WriteLine ("({0},{1}) ({2},{3}) {4}", (int)(kp.X + 0.5),
				(int)(kp.Y + 0.5), (int)(((Keypoint) keys2[nearest]).X + 0.5),
				(int)(((Keypoint) keys2[nearest]).Y + 0.5), distNearest);

		}

		return (matches);
	}
	*/

	public static ArrayList FilterJoins (ArrayList matches)
	{
		Hashtable ht = new Hashtable ();

		// Count the references to each keypoint
		foreach (Match m in matches) {
			int lI = (ht[m.Kp1] == null) ? 0 : (int) ht[m.Kp1];
			ht[m.Kp1] = lI + 1;
			int rI = (ht[m.Kp2] == null) ? 0 : (int) ht[m.Kp2];
			ht[m.Kp2] = rI + 1;
		}

		ArrayList survivors = new ArrayList ();
		int removed = 0;
		foreach (Match m in matches) {
			//Console.WriteLine ("match: {0}, {1}", (int) ht[m.Kp1], (int) ht[m.Kp2]);

			if (((int) ht[m.Kp1]) <= 1 && ((int) ht[m.Kp2]) <= 1)
				survivors.Add (m);
			else
				removed += 1;
		}

		return (survivors);
	}

	public static void FilterNBest (ArrayList matches, int bestQ)
	{
		matches.Sort (new Match.MatchWeighter ());
		if (matches.Count > bestQ)
			matches.RemoveRange (bestQ, matches.Count - bestQ);
	}
}


public class Match : ICloneable
{
	KeypointN kp1;
	KeypointN kp2;
	public KeypointN Kp1 {
		get {
			return (kp1);
		}
	}
	public KeypointN Kp2 {
		get {
			return (kp2);
		}
	}

	public object Clone ()
	{
		Match mc = new Match ();

		mc.kp1 = (KeypointN) kp1.Clone ();
		mc.kp2 = (KeypointN) kp2.Clone ();
		mc.dist1 = dist1;
		mc.dist2 = dist2;

		return (mc);
	}

	double dist1;
	public double Dist1 {
		get {
			return (dist1);
		}
	}
	double dist2;
	public double Dist2 {
		get {
			return (dist2);
		}
	}

	private Match ()
	{
	}

	// dist1: distance between kp1/kp2,
	// dist2: distance between kp1 and kp3, where kp3 is the next closest
	//   match
	public Match (KeypointN kp1, KeypointN kp2, double dist1, double dist2)
	{
		this.kp1 = kp1;
		this.kp2 = kp2;
		this.dist1 = dist1;
		this.dist2 = dist2;
	}

	public class MatchWeighter : IComparer
	{
		private double distExp;
		private double quotExp;

		public MatchWeighter ()
			: this (1.0, 1.0)
		{
		}

		// The formula goes like this, with lowest weight being best matches:
		// w(kp) = kp.dist1^{distExp} *
		//     {\frac{1}{kp.dist2 - kp.dist1}}^{quotExp}
		//
		// This means, as both dist1 and dist2 are in [0.0 ; 1.0], that a high
		// distExp exponent (and distExp > quotExp) will make the absolute
		// distance for the best match more important. A high value for
		// quotExp will make the difference between the best and second best
		// match more important (as in "how many other candidates are likely
		// matches?").
		public MatchWeighter (double distExp, double quotExp)
		{
			this.distExp = distExp;
			this.quotExp = quotExp;
		}

		public int Compare (object obj1, object obj2)
		{
			Match m1 = (Match) obj1;
			Match m2 = (Match) obj2;

			double fit1 = OverallFitness (m1);
			double fit2 = OverallFitness (m2);
			if (fit1 < fit2)
				return (-1);
			else if (fit1 > fit2)
				return (1);

			return (0);
		}

		public double OverallFitness (Match m)
		{
			double fitness = Math.Pow (m.Dist1, distExp) *
				Math.Pow (1.0 / (m.Dist2 - m.Dist1), quotExp);

			return (fitness);
		}
	}
}


public class MultiMatch
{
	KeypointXMLList[] keysets;
	public KeypointXMLList[] Keysets {
		get {
			return (keysets);
		}
	}

	// Global k-d tree, containing all keys.
	KDTree globalKeyKD;
	// Global key list, containing Keypoint elements
	ArrayList globalKeys;

	// Global match list containing Match objects
	ArrayList globalMatches;

	// Partitioned matches
	MatchSet[,] matchSets;
	int imageCount;

	ArrayList filteredMatchSets;

	bool verbose = true;
	public bool Verbose {
		set {
			verbose = value;
		}
	}

	public void LoadKeysetsFromMemory (ArrayList memlist)
	{
		imageCount = memlist.Count;
		keysets = (KeypointXMLList[]) memlist.ToArray (typeof (KeypointXMLList));
	}

	public void LoadKeysets (string[] filenames)
	{
		imageCount = filenames.Length;

		keysets = new KeypointXMLList[imageCount];
		for (int n = 0 ; n < imageCount ; ++n) {
			KeypointXMLList keys = KeypointXMLReader.ReadComplete (filenames[n]);
			Console.WriteLine ("Loaded {0} keypoints from {1} for image \"{2}\" ({3}x{4})",
				keys.Arr.Length,
				System.IO.Path.GetFileName (filenames[n]),
				System.IO.Path.GetFileName (keys.ImageFile),
				keys.XDim, keys.YDim);

			keysets[n] = keys;
		}
	}

	// Matches between two images
	public class MatchSet
	{
		string file1;
		public string File1 {
			get {
				return (file1);
			}
		}
		string file2;
		public string File2 {
			get {
				return (file2);
			}
		}
		public int xDim1, yDim1, xDim2, yDim2;

		ArrayList matches;
		public ArrayList Matches {
			get {
				return (matches);
			}
			set {
				matches = value;
			}
		}

		// The best result of the RANSAC matching
		ImageMatchModel bestMatchFit = null;
		public ImageMatchModel BestMatchFit {
			get {
				return (bestMatchFit);
			}
			set {
				bestMatchFit = value;
			}
		}

		KeypointXMLList keys1, keys2;
		public KeypointN[] GetOriginalKeys (int which) {
			if (which == 0)
				return (keys1.Arr);
			else
				return (keys2.Arr);
		}


		private MatchSet ()
		{
		}

		public MatchSet (string file1, int xdim1, int ydim1,
			string file2, int xdim2, int ydim2,
			KeypointXMLList kp1, KeypointXMLList kp2)
		{
			this.file1 = file1;
			this.file2 = file2;
			this.matches = new ArrayList ();

			xDim1 = xdim1;
			yDim1 = ydim1;
			xDim2 = xdim2;
			yDim2 = ydim2;

			keys1 = kp1;
			keys2 = kp2;
		}
	}

	public ArrayList TwoPatchMatch (ArrayList kp1, int kp1maxX, int kp1maxY,
		ArrayList kp2, int kp2maxX, int kp2maxY, bool useRANSAC)
	{
		// Initialize keysets
		ArrayList kl = new ArrayList ();
		kl.Add (new KeypointXMLList (kp1, kp1maxX, kp1maxY));
		kl.Add (new KeypointXMLList (kp2, kp2maxX, kp2maxY));
		LoadKeysetsFromMemory (kl);

		// Build search structure
		BuildGlobalKD ();
		BuildGlobalMatchList ();

		PartitionMatches ();

		filteredMatchSets = new ArrayList ();

		// The only combination between two matches
		MatchSet ms = matchSets[0, 1];
		if (ms == null || ms.Matches == null)
			return (null);

		if (useRANSAC) {
			ArrayList ransacMatches = null;
			int maxX = ms.xDim1 >= ms.xDim2 ? ms.xDim1 : ms.xDim2;
			int maxY = ms.yDim1 >= ms.yDim2 ? ms.yDim1 : ms.yDim2;

			// TODO: 16.0 -> configurable
			ImageMatchModel bestRansac = MatchDriver.FilterMatchSet
				(ms.Matches, 16.0, maxX, maxY);

			ms.BestMatchFit = bestRansac;
			if (bestRansac != null)
				ransacMatches = bestRansac.FittingGround;

			if (ransacMatches == null) {
				if (verbose)
					Console.WriteLine ("RANSAC says: no good matches from " +
						"{0} original matches", ms.Matches.Count);

				return (null);
			} else {
				if (verbose)
					Console.WriteLine ("RANSAC purged: {0} to {1}",
						ms.Matches.Count, ransacMatches.Count);
			}

			// Overwrite matches with RANSAC checked ones.
			ms.Matches = ransacMatches;
		}
		filteredMatchSets.Add (ms);

		return (filteredMatchSets);
	}

	// Find and return a list of MatchSet objects.
	//
	// This is the heart of the matching process.
	//
	// minimumMatches: minimum number of matches required in final results.
	// bestMatches: number of best matches to keep, or zero to keep all.
	// useRANSAC: whether ransac filtering should be used (true recommended
	//     for 2d rigid transformed images, such as panoramas).
	public ArrayList LocateMatchSets (int minimumMatches, int bestMatches,
		bool useRANSAC, bool useAreaFiltration)
	{
		BuildGlobalKD ();
		BuildGlobalMatchList ();

		PartitionMatches ();

		filteredMatchSets = new ArrayList ();

		// Walk all image combinations.
		for (int n0 = 0 ; n0 < imageCount ; ++n0) {
			for (int n1 = n0 + 1 ; n1 < imageCount ; ++n1) {
				MatchSet ms = matchSets[n0, n1];
				if (ms == null || ms.Matches == null)
					continue;

				if (useRANSAC) {
					ArrayList ransacMatches = null;
					int maxX = ms.xDim1 >= ms.xDim2 ? ms.xDim1 : ms.xDim2;
					int maxY = ms.yDim1 >= ms.yDim2 ? ms.yDim1 : ms.yDim2;

					// TODO: 16.0 -> configurable
					ImageMatchModel bestRansac = MatchDriver.FilterMatchSet
						(ms.Matches, 16.0, maxX, maxY);

					ms.BestMatchFit = bestRansac;
					if (bestRansac != null)
						ransacMatches = bestRansac.FittingGround;

					if (ransacMatches == null) {
						if (verbose)
							Console.WriteLine ("RANSAC says: no good matches from " +
								"{0} original matches", ms.Matches.Count);

						continue;
					} else {
						if (verbose)
							Console.WriteLine ("RANSAC purged: {0} to {1}",
								ms.Matches.Count, ransacMatches.Count);
					}

					// Overwrite matches with RANSAC checked ones.
					ms.Matches = ransacMatches;
				}

				// Number of RANSAC-ok matches before count-filtering.
				int beforeBestFilterCount = ms.Matches.Count;

				// TODO: replace with area filtering
				Console.WriteLine ("Filtering... ({0}, {1})",
					System.IO.Path.GetFileName (ms.File1),
					System.IO.Path.GetFileName (ms.File2));

				// First filtration: Join matches
				int beforeJoinFiltering = ms.Matches.Count;
				ms.Matches = MatchKeys.FilterJoins (ms.Matches);
				Console.WriteLine ("   A. Join Filtration: {0} to {1}",
					beforeJoinFiltering, ms.Matches.Count);

#if false
				// Second: Area filtration
				if (useAreaFiltration && bestMatches > 0) {
					int beforeAreaFiltering = ms.Matches.Count;
					double pixelCount = ms.xDim1 * ms.yDim1;
					double areaPixels;
					ms.Matches = FilterMatchesByArea (ms.Matches, bestMatches,
						out areaPixels);

					Console.WriteLine ("   B. Area Filtration: {0} to {1}, covering {2:N2} % of image \"{3}\"",
						beforeAreaFiltering, ms.Matches.Count,
						(areaPixels * 100.0) / pixelCount,
						System.IO.Path.GetFileName (ms.File1));
#endif
#if false
				if (useAreaFiltration && bestMatches > 0) {
					int beforeFiltering = ms.Matches.Count;

					ms.Matches = FilterMatchesByCoverage (ms, ms.Matches, bestMatches);

					Console.WriteLine ("   B. Coverage Filtration: {0} to {1} on image \"{2}\"",
						beforeFiltering, ms.Matches.Count,
						System.IO.Path.GetFileName (ms.File1));
				} else
#endif
				if (bestMatches > 0) {
					int beforeScoreFiltering = ms.Matches.Count;
					ms.Matches = FilterMatches (ms.Matches, bestMatches);

					Console.WriteLine ("   B. Score Filtration: {0} to {1}",
						beforeScoreFiltering, ms.Matches.Count);
				}

				Console.WriteLine ("Filtered partition [{0},{1}] from {2} matches down to {3}",
					n0, n1, beforeBestFilterCount, ms.Matches.Count);

				if (ms.Matches.Count >= minimumMatches)
					filteredMatchSets.Add (ms);
			}
		}

		return (filteredMatchSets);
	}

	private ArrayList CreatePointList (ArrayList matches)
	{
		ArrayList points = new ArrayList ();

		// Create a point list.
		foreach (Match m in matches) {
			FilterPoint p = new FilterPoint ();

			p.x = m.Kp1.X;
			p.y = m.Kp1.Y;
			p.user = m;

			Console.WriteLine ("{0} {1} # CPOINTS", p.x, p.y);
			points.Add (p);
		}

		return (points);
	}

	// This was a planned attempt to use a more complex FMM based filter to
	// prune keypoints in a way to maximize coverage.  Failed, tho.
#if false
	private ArrayList FilterMatchesByCoverage (MatchSet ms,
		ArrayList matches, int bestMatches)
	{
		// Nothing to do.
		if (matches.Count <= bestMatches)
			return (matches);

		ArrayList remainingPoints = CreatePointList (matches);
		foreach (FilterPoint p in remainingPoints)
			Console.WriteLine ("{0} {1} # INPUT", p.x, p.y);

		/* Prune them.
		 */
		while (remainingPoints.Count > bestMatches) {
			CoverageFilter cf = new CoverageFilter (ms, remainingPoints, bestMatches);
			remainingPoints = cf.PruneOne ();
			Console.WriteLine ("Pruned one point, {0}.  Goal: {1}.",
				remainingPoints.Count, bestMatches);
		}

		foreach (FilterPoint p in remainingPoints)
			Console.WriteLine ("{0} {1} # OUTPUT", p.x, p.y);

		/*double[] pruneScores = cf.GetScore ();
		for (int k = 0 ; k < pruneScores.Length ; ++k)
			Console.WriteLine ("{0}: {1}", k, pruneScores[k]);*/

		/* Restore the remaining points to match points.
		 */
		ArrayList filteredMatches = new ArrayList ();
		foreach (FilterPoint p in remainingPoints)
			filteredMatches.Add ((Match) p.user);

		return (filteredMatches);
	}
#endif

	private ArrayList FilterMatchesByArea (ArrayList matches, int bestMatches,
		out double areaPixels)
	{
		areaPixels = 0.0;

		// Trivial case: no more matches available.
		if (matches.Count <= bestMatches)
			return (matches);

		ArrayList points = CreatePointList (matches);

		AreaFilter areaF = new AreaFilter ();
		ArrayList convexHull = areaF.CreateConvexHull (points);
		//Console.WriteLine ("convex hull holds {0} points", convexHull.Count);

		// Case one: we have more points in the convex hull than we want
		// Solution: Iteratively remove the point that reduces the hull area
		//   the least, until we have only bestMatches left.
		//Console.WriteLine ("Polygon area before: {0}", areaF.PolygonArea (convexHull));
		while (convexHull.Count > bestMatches) {
			double maxArea = -1.0;
			int removeIndex = -1;

			// Remove exactly one element from the convex hull, that element
			// which still maximizes the polygon area (ie image coverage).
			for (int n = 0 ; n < convexHull.Count ; ++n) {
				ArrayList convexHullMinusOne = (ArrayList) convexHull.Clone ();
				convexHullMinusOne.RemoveAt (n);

				double remArea = areaF.PolygonArea (convexHullMinusOne);
				if (removeIndex < 0 || remArea > maxArea) {
					removeIndex = n;
					maxArea = remArea;
				}
			}

			//Console.WriteLine ("DEBUG Removing {0}, area still {1}", removeIndex, maxArea);
			convexHull.RemoveAt (removeIndex);
		}

		//Console.WriteLine ("Polygon area after: {0}", areaF.PolygonArea (convexHull));
		areaPixels = areaF.PolygonArea (convexHull);

		// Case two: we have less points in the convex hull than we want.
		// Solution: Add points based on their average distance to all convex
		//   hull points.
		//
		// We know there are enough matches available as
		// matches.Count >= bestMatches.
		while (convexHull.Count < bestMatches)
		{
			double maxDistance = -1.0;
			Match addMatch = null;

			foreach (Match m in matches) {
				bool matchFound = false;
				foreach (FilterPoint p in convexHull) {
					if (p.user != m)
						continue;

					matchFound = true;
					break;
				}

				// Match already in pointlist.
				if (matchFound)
					continue;

				// Now that we have a unique point, calculate its average
				// distance to all points in the convex hull.
				double dist = 0.0;
				int distCount = 0;
				foreach (FilterPoint p in convexHull) {
					dist += Math.Sqrt (Math.Pow (p.x - m.Kp1.X, 2.0) +
						Math.Pow (p.y - m.Kp1.Y, 2.0));
					distCount += 1;
				}

				dist /= (double) distCount;
				//Console.WriteLine ("  max: {0}, this: {1}", maxDistance, dist);
				if (addMatch == null || dist > maxDistance) {
					addMatch = m;
					maxDistance = dist;
				}
			}

			// Add point, although its not in the hull. It just happens to be
			// farthest away from all points in the hull, so the image
			// coverage is improved most.
			FilterPoint pNew = new FilterPoint ();
			pNew.x = addMatch.Kp1.X;
			pNew.y = addMatch.Kp1.Y;
			pNew.user = addMatch;
			convexHull.Add (pNew);
		}

		ArrayList filteredMatches = new ArrayList ();
		foreach (FilterPoint p in convexHull)
			filteredMatches.Add ((Match) p.user);

		return (filteredMatches);
	}

	private ArrayList FilterMatches (ArrayList matches, int bestMatches)
	{
		matches = MatchKeys.FilterJoins (matches);
		MatchKeys.FilterNBest (matches, bestMatches);

		/*
		Match.MatchWeighter mw = new Match.MatchWeighter ();

		foreach (Match m in matches) {
			Console.WriteLine ("    ({0},{1}) ({2},{3}) {4}, 2nd: {5}, weight {6}",
				(int)(m.Kp1.X + 0.5), (int)(m.Kp1.Y + 0.5),
				(int)(m.Kp2.X + 0.5), (int)(m.Kp2.Y + 0.5),
				m.Dist1, m.Dist2, mw.OverallFitness (m));
		}*/

		return (matches);
	}

	private void BuildGlobalKD ()
	{
		globalKeys = new ArrayList ();
		foreach (KeypointXMLList list in keysets)
			foreach (KeypointN kp in list.Arr)
				globalKeys.Add (kp);

		globalKeyKD = KDTree.CreateKDTree (globalKeys);

		if (verbose)
			Console.WriteLine ("Created global k-d tree containing {0} keypoints",
				globalKeys.Count);
	}

	// Partition the matches into image pair groups
	private void PartitionMatches ()
	{
		matchSets = new MatchSet[imageCount, imageCount];
		int createCount = 0;
		int maxL = 0;

		// Create all possible partition combinations, while pruning reverse
		// matches
		foreach (Match m in globalMatches) {
			int l0 = FindOrigin (m.Kp1);
			int l1 = FindOrigin (m.Kp2);

			if (l0 > maxL)
				maxL = l0;
			if (l1 > maxL)
				maxL = l1;
			bool reverseAlreadyIn = false;

			// FIXME: remove/is this correct?
			// FIXME: rewrite this whole crappy function
			if (l0 >= l1)
				continue;

			if (matchSets[l1, l0] != null) {
				MatchSet rev = matchSets[l1, l0];
				if (rev != null) {
					foreach (Match mr in rev.Matches) {
						if (mr.Kp1 == m.Kp2 && mr.Kp2 == m.Kp1) {
							reverseAlreadyIn = true;

							break;
						}
					}
				}
			}
			if (reverseAlreadyIn)
				continue;

			if (matchSets[l0, l1] == null) {
				createCount += 1;
				matchSets[l0, l1] = new MatchSet (keysets[l0].ImageFile,
					keysets[l0].XDim, keysets[l0].YDim,
					keysets[l1].ImageFile, keysets[l1].XDim, keysets[l1].YDim,
					keysets[l0], keysets[l1]);
			}

			matchSets[l0, l1].Matches.Add (m);
		}

		if (verbose) {
			Console.WriteLine ("Created {0} match partitions, max l = {1}",
				createCount, maxL);

			/*
			for (int l0 = 0 ; l0 <= maxL ; ++l0) {
				for (int l1 = 0 ; l1 <= maxL ; ++l1) {
					Console.Write ("({0},{1}: {2}), ", l0, l1,
						matchSets[l0, l1] == null ? "empty" :
						String.Format ("{0}", matchSets[l0, l1].Matches.Count));
				}
			}
			*/

			Console.WriteLine ("");
		}
	}

	private int FindOrigin (KeypointN kp)
	{
		for (int ksn = 0 ; ksn < keysets.Length ; ++ksn) {
			KeypointXMLList list = keysets[ksn];

			int lIdx = Array.IndexOf (list.Arr, kp);
			if (lIdx >= 0)
				return (ksn);
		}

		throw (new Exception ("BUG: keypoint origin unknown"));
	}

	public class SparseKeypointsException : ApplicationException
	{
		public SparseKeypointsException (string message)
			: base (message)
		{
		}
	}

	public delegate void MatchKeypointEventHandler (int index, int total);
	public event MatchKeypointEventHandler MatchKeypoint;

	private void BuildGlobalMatchList ()
	{
		globalMatches = new ArrayList ();

		int count = 0;
		double searchDepth = Math.Max (130.0,
			(Math.Log (globalKeys.Count) / Math.Log (1000.0)) * 130.0);
		int searchDepthI = (int) searchDepth;

		if (verbose)
			Console.WriteLine ("Using a BBF cut search depth of {0}", searchDepthI);

		foreach (KeypointN kp in globalKeys) {
			// Raise event in case an event handler has been registered.
			if (MatchKeypoint != null)
				MatchKeypoint (count, globalKeys.Count);

			if (verbose) {
				if ((count % 25) == 0)
					Console.Write ("\r% {0:N2}, {1}/{2}        ",
						(100 * ((double) count)) / ((double) globalKeys.Count),
						count, globalKeys.Count);
			}
			count++;

			// There should be one exact hit (the keypoint itself, which we
			// ignore) and two real hits, so lets search for the three best
			// hits. But it could be the exact match is not found for, as we
			// use probabilistic bbf matching.
			ArrayList kpNNList =
				globalKeyKD.NearestNeighbourListBBF (kp, 3, searchDepthI);

			if (kpNNList.Count < 3)
				throw (new SparseKeypointsException ("BUG: less than three neighbours!"));

			KDTree.BestEntry be1 = (KDTree.BestEntry) kpNNList[0];
			KDTree.BestEntry be2 = (KDTree.BestEntry) kpNNList[1];

			// If be1 is the same (exact hit), shift one
			if (be1.Neighbour == kp) {
				be1 = be2;
				be2 = (KDTree.BestEntry) kpNNList[2];
			}
			if (be1.Neighbour == kp || be2.Neighbour == kp ||
				be1.Neighbour == be2.Neighbour)
				continue;
				//throw (new Exception ("BUG: wrong keypoints caught"));

			if ((be1.Distance / be2.Distance) > 0.6)
				continue;

			globalMatches.Add (new Match (kp, (KeypointN) be1.Neighbour,
				be1.Distance, be2.Distance));
		}
		if (verbose) {
			Console.Write ("\r% {0:N2}, {1}/{2}        ",
				(100 * ((double) count)) / ((double) globalKeys.Count),
				count, globalKeys.Count);

			Console.WriteLine ("\nGlobal match search yielded {0} matches",
				globalMatches.Count);
		}
	}

	// matches: ArrayList of MatchSet
	// returns: ArrayList of ArrayList of string (filenames)
	public ArrayList ComponentCheck (ArrayList matches)
	{
		ArrayList components = new ArrayList ();

		foreach (MatchSet ms in matches) {
			Component c1 = FindComponent (components, ms.File1);
			Component c2 = FindComponent (components, ms.File2);

			// case: new component
			if (c1 == null && c2 == null) {
				Component comp = new Component ();

				comp.Add (ms.File1);
				comp.Add (ms.File2);

				components.Add (comp);
			// c2 is new in c1-component
			} else if (c1 != null && c2 == null) {
				c1.Add (ms.File2);
			// c1 is new in c2-component
			} else if (c1 == null && c2 != null) {
				c2.Add (ms.File1);
			// same component already, do nothing
			} else if (c1 == c2) {
			// different component: join components
			} else if (c1 != c2) {
				c1.Add (c2);
				components.Remove (c2);
			}
		}

		// Now locate all components with no matches at all and add them to
		// the final result.
		foreach (KeypointXMLList klist in keysets) {
			string filename = klist.ImageFile;

			if (FindComponent (components, filename) != null)
				continue;

			Component isolatedFile = new Component ();
			isolatedFile.Add (filename);
			components.Add (isolatedFile);
		}

		return (components);
	}

	private Component FindComponent (ArrayList components, string filename)
	{
		foreach (Component comp in components) {
			if (comp.IsIncluded (filename))
				return (comp);
		}

		return (null);
	}

	public class Component
	{
		ArrayList files = new ArrayList ();

		public void Add (Component comp) {
			foreach (string filename in comp.files)
				Add (filename);
		}

		public bool IsIncluded (string filename)
		{
			return (files.Contains (filename));
		}

		public void Add (string filename)
		{
			if (IsIncluded (filename))
				return;

			files.Add (filename);
		}

		public override string ToString ()
		{
			StringBuilder sb = new StringBuilder ();

			bool first = true;
			int cCount = 0;
			foreach (string filename in files) {
				if (first) {
					first = false;
				} else {
					sb.Append (", ");
					cCount += 2;
				}

				string justFile = System.IO.Path.GetFileName (filename);
				sb.AppendFormat ("{0}", justFile);
				cCount += justFile.Length;

				if (cCount > 65) {
					sb.Append ("\n  ");
					cCount = 2;
					first = true;
				}
			}

			return (sb.ToString ());
		}
	}

	public BondBall BuildBondBall (ArrayList ransacFiltered, int bottomDefault)
	{
		BondBall bb = null;
		bool first = true;
		string fileNow, fileNext;

		for (int fileN = 0 ; fileN < (keysets.Length - 1) ; ++fileN) {
			fileNow = keysets[fileN].ImageFile;
			fileNext = keysets[fileN + 1].ImageFile;

			Console.WriteLine ("Searching for matches between {0}-{1}",
				fileNow, fileNext);
			// Process only the MatchSet that is build from this exact two
			// image files.
			MatchSet msNext = null;
			foreach (MatchSet ms in ransacFiltered) {
				if (String.Compare (ms.File1, fileNow) == 0 &&
					String.Compare (ms.File2, fileNext) == 0)
				{
					msNext = ms;
					Console.WriteLine ("  found!");
					break;
				}
			}

			// In case no matchset can be found this means we reached the end
			// of the panorama (it could still be a 360 degree one).
			if (msNext == null) {
				Console.WriteLine ("  NOT found!");
				break;
			}

			if (first) {
				Console.WriteLine ("Building bondball");
				if (bottomDefault == -1)
					bb = new BondBall ();
				else
					bb = new BondBall (bottomDefault);

				if (bb.InitiateBond (msNext) == false) {
					Console.WriteLine ("  FAILED");
					break;
				} else {
					Console.WriteLine ("  SUCCESS: {0}", bb);
				}
				first = false;

				continue;
			}

			// Try to add one more image to the row.
			if (bb.AddRowImage (msNext) == true) {
				Console.WriteLine ("Terminating first row creation.");
				break;
			}
		}

		// Case: no bondball has been build, because first pair does not
		// exist.
		if (bb == null)
			return (null);

		ArrayList rowFileNames = new ArrayList ();
		string last = null;
		foreach (MatchSet ms in bb.Sets) {
			rowFileNames.Add (ms.File1);
			last = ms.File2;
		}
		rowFileNames.Add (last);

		Console.WriteLine ("First row is:");
		foreach (string rowFileName in rowFileNames)
			Console.WriteLine ("  {0}", rowFileName);

		bb.FirstRow = rowFileNames;

		// Check if we have a 360 degree panorama, but only if we have more
		// than two images in the first row. This rules out very wide angle
		// lenses (> 180 degrees), which seems ok to me. Heck, is it even
		// possible?
		MatchSet msJoining = null;
		if (bb.Sets.Count > 2) {
			foreach (MatchSet ms in ransacFiltered) {
				if ((String.Compare (ms.File1, bb.Last.File2) == 0 &&
					String.Compare (ms.File2, bb.First.File1) == 0) ||
					(String.Compare (ms.File2, bb.Last.File2) == 0 &&
						String.Compare (ms.File1, bb.First.File1) == 0))
				{
					msJoining = ms;
					break;
				}
			}
		}

		bool is360 = false;
		if (msJoining != null) {
			Console.WriteLine ("Found 360 degree panorama, merging between \"{0}\" and \"{1}\" :-)",
				msJoining.File1, msJoining.File2);

			is360 = true;
		} else {
			Console.WriteLine ("Found no 360 degree boundary, assuming a < 360 degree panorama.");
			Console.WriteLine ("In case this is an error and it is a 360 degree boundary, please");
			Console.WriteLine ("consult the manpage documentation for information how to improve");
			Console.WriteLine ("the detection results. Thank you :-)");

			is360 = false;
		}

		// Align first row.
		bb.StretchImages (is360);

		// Now roughly add all remaining images.
		bool alignGap = true;
		int alignCount = 0;

		Console.WriteLine ("Estimating remaining images' positions");
		while (alignGap == true && alignCount < ransacFiltered.Count) {
			alignGap = false;

			foreach (MatchSet ms in ransacFiltered) {
				// Already aligned.
				if (bb.Positions.Contains (ms.File1) && bb.Positions.Contains (ms.File2))
					continue;

				// Unable to align (yet)
				if (bb.Positions.Contains (ms.File1) == false &&
					bb.Positions.Contains (ms.File2) == false)
				{
					alignGap = true;
					continue;
				}

				BondBall.Position pPos = null;
				string pPosFile = null;

				// Alignable cases: one is part of the aligned set
				if (bb.Positions.Contains (ms.File1) == true &&
					bb.Positions.Contains (ms.File2) == false)
				{
					pPos = bb.EstimateImage (ms);
					bb.Positions.Add (System.IO.Path.GetFileName (ms.File2), pPos);
					pPosFile = ms.File2;
				} else if (bb.Positions.Contains (ms.File1) == false &&
					bb.Positions.Contains (ms.File2) == true)
				{
					pPos = bb.EstimateImage (ms);
					bb.Positions.Add (System.IO.Path.GetFileName (ms.File1), pPos);
					pPosFile = ms.File1;
				}
				Console.WriteLine ("  estimated: \"{0}\": {1}",
					pPosFile, pPos);
			}

			alignCount += 1;
		}

		if (alignGap) {
			Console.WriteLine ("");
			Console.WriteLine ("Warning: Unaligned images remain. This is most likely due to loose");
			Console.WriteLine ("         components, see the manual page documentation for details.");
			Console.WriteLine ("");
		} else
			Console.WriteLine ("Done.\n");

		/*
		foreach (string posFile in bb.Positions.Keys) {
			Console.WriteLine ("have for \"{0}\": {1}", posFile,
				(BondBall.Position) bb.Positions[posFile]);
		}
		*/
		Console.WriteLine ("{0}", bb);
		return (bb);
	}
}

public class FilterPoint 
{
	public double x, y;
	public object user = null;
}

