
/* 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).
 */

/* Autopano.cs
 *
 * Keypoint file correlation and hugin panorama file creation utility.
 *
 * (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.Threading;
using System.Globalization;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;


public class Autopano
{
	public static void Usage ()
	{
		Console.WriteLine ("usage: autopano.exe [options] output.pto keys1.xml keys2.xml [..]\n");
		Console.WriteLine ("Options");
		Console.WriteLine ("  --ransac <on|off|1|0>   Switch RANSAC filtration on or off (default: on)");
		Console.WriteLine ("  --maxmatches <matches>  Use no more than the given number of matches");
		Console.WriteLine ("                          (default: 16, use zero for unlimited)");
		Console.WriteLine ("  --disable-areafilter    Do not use max-area filtration, which is default.");
		Console.WriteLine ("                          See manpage for details.");
		Console.WriteLine ("  --integer-coordinates   Truncate match coordinates to integer numbers.");
		Console.WriteLine ("  --absolute-pathnames <on|off|1|0>   Use the absolute pathname of the image");
		Console.WriteLine ("                          file in the PTO output file. Disabled by default.");
		Console.WriteLine ("");

		Console.WriteLine ("Alignment options");
		Console.WriteLine ("  --align                 Automatically pre-align images in PTO file.");
		Console.WriteLine ("  --bottom-is-left");
		Console.WriteLine ("  --bottom-is-right       Use in case the automatic algorithm fails.");
		Console.WriteLine ("  --generate-horizon <c>  Generate up to 'c' horizon lines.");
		Console.WriteLine ("");

		Console.WriteLine ("Refinement options");
		Console.WriteLine ("  --refine                Refine the found control points using the");
		Console.WriteLine ("                          original images.");
		Console.WriteLine ("  --refine-by-middle      Use the best middle point to refine (default).");
		Console.WriteLine ("  --refine-by-mean        Use the mean of the patches control points.");
		Console.WriteLine ("  --keep-unrefinable <on|off|1|0>");
		Console.WriteLine ("                          Keep unrefinable matches (default: on).");

		Console.WriteLine ("output.pto: The output PTO panorama project file.");
		Console.WriteLine ("    The filename can be \"-\", then stdout is used");
		Console.WriteLine ("key<n>.xml: The keypoint input files.");
		Console.WriteLine ("    The input files can be gzip compressed, but require the \".gz\" extension\n    then.");
		Console.WriteLine ("");
		Console.WriteLine ("Notice: for the aligning to work, the input images shall be");
		Console.WriteLine ("  1. All of the same dimension and scale");
		Console.WriteLine ("  2. The first images must be an ordered row. See manpage.");
		Console.WriteLine ("");
	}

	// The maximum radius to consider around a keypoint that is refined.  That
	// is, at most a patch of a maximum size of twice this value in both
	// horizontal and vertical direction is extracted.
	public const int RefinementRadiusMaximum = 96;

	public static void Main (string[] args)
	{
		Console.WriteLine ("autopano-sift, Automatic panorama generation program\n");

		if (args.Length < 3) {
			Usage ();
			Environment.Exit (1);
		}

		/* Make culture invariant to produce always floating point output
		 * hugin can understand.
		 */
		Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

		// Automatic pre-aligning of images
		bool preAlign = false;
		int bottomDefault = -1;
		int generateHorizon = 0;

		// Use RANSAC algorithm match filtration.
		bool useRansac = true;

		// Use area based weighting for final match selection.
		bool useAreaFiltration = true;

		// Truncate match coordinates to integer numbers.
		bool useIntegerCoordinates = false;

		// Use the absolute pathname of the image files in the output PTO
		// file.
		bool useAbsolutePathnames = false;

		// Use "keep-best" filtration, keep the maxMatches best.
		int maxMatches = 16;	// default: 16

		// Refinement options
		bool refine = false;
		bool refineMiddle = true;
		bool keepUnrefinable = true;

		int optionCount = 0;
		int optionN = 0;
		while (optionN < args.Length &&
			args[optionN].Length >= 2 && args[optionN][0] == '-')
		{
			string optionStr = args[optionN];

			if (args[optionN][1] != '-') {
				Usage ();
				Environment.Exit (1);
			}

			if (String.Compare (optionStr, "--ransac") == 0) {
				useRansac = YesNoOption ("--ransac", args[optionN + 1]);
				optionN += 2;
			} else if (String.Compare (optionStr, "--maxmatches") == 0) {
				try {
					maxMatches = Int32.Parse (args[optionN + 1]);
				} catch (Exception ex) {
					Console.WriteLine ("Parameter to maxmatches option invalid. See the usage help.");
					System.Environment.Exit (1);
				}
				if (maxMatches < 0) {
					Console.WriteLine ("Maximum number of matches must be positive or zero (unlimited).");
					System.Environment.Exit (1);
				}
				optionN += 2;
			} else if (String.Compare (optionStr, "--disable-areafilter") == 0) {
				useAreaFiltration = false;
				optionN += 1;
			} else if (String.Compare (optionStr, "--integer-coordinates") == 0) {
				useIntegerCoordinates = true;
				optionN += 1;
			} else if (String.Compare (optionStr, "--absolute-pathnames") == 0) {
				useAbsolutePathnames = YesNoOption ("--absolute-pathnames",
					args[optionN + 1]);;
				optionN += 2;
			} else if (String.Compare (optionStr, "--align") == 0) {
				preAlign = true;
				optionN += 1;
			} else if (String.Compare (optionStr, "--bottom-is-left") == 0) {
				bottomDefault = 0;
				optionN += 1;
			} else if (String.Compare (optionStr, "--bottom-is-right") == 0) {
				bottomDefault = 1;
				optionN += 1;
			} else if (String.Compare (optionStr, "--generate-horizon") == 0) {
				try {
					generateHorizon = Int32.Parse (args[optionN + 1]);
				} catch (Exception ex) {
					Console.WriteLine ("Parameter to generate-horizon option invalid. See the usage help.");
					System.Environment.Exit (1);
				}
				if (generateHorizon < 0) {
					Console.WriteLine ("The number of horizon lines to generate must be positive.");
					System.Environment.Exit (1);
				}

				optionN += 2;
			} else if (String.Compare (optionStr, "--refine") == 0) {
				refine = true;
				optionN += 1;
			} else if (String.Compare (optionStr, "--refine-by-middle") == 0) {
				refineMiddle = true;
				optionN += 1;
			} else if (String.Compare (optionStr, "--refine-by-mean") == 0) {
				refineMiddle = false;
				optionN += 1;
			} else if (String.Compare (optionStr, "--keep-unrefinable") == 0) {
				keepUnrefinable = YesNoOption ("--keep-unrefinable", args[optionN + 1]);
				optionN += 2;
			} else {
				Console.WriteLine ("Usage error. Run \"autopano.exe\" without arguments for help.");
				System.Environment.Exit (1);
			}
		}
		optionCount = optionN;

		if (bottomDefault != -1 && preAlign == false) {
			Console.WriteLine ("Please enable automatic alignment (\"--align\") before using the");
			Console.WriteLine ("--bottom-is-* options. Thank you. Run \"autopano.exe\" without");
			Console.WriteLine ("arguments for usage help.");

			System.Environment.Exit (1);
		}

		if (generateHorizon > 0 && preAlign == false) {
			Console.WriteLine ("Please enable automatic alignment (\"--align\") before using the");
			Console.WriteLine ("--generate-horizon option. Thank you. Run \"autopano.exe\" without");
			Console.WriteLine ("arguments for usage help.");

			System.Environment.Exit (1);
		}

		MultiMatch mm = new MultiMatch ();
		string[] keyfiles = new string[args.Length - 1 - optionCount];
		Array.Copy (args, 1 + optionCount, keyfiles, 0, args.Length - 1 - optionCount);

		Console.WriteLine ("Loading keyfiles");
		mm.LoadKeysets (keyfiles);

		Console.WriteLine ("\nMatching...{0}", useRansac == true ? " RANSAC enabled" : "");
		ArrayList msList = mm.LocateMatchSets (3, maxMatches,
			useRansac, useAreaFiltration);

		// Connected component check
		Console.WriteLine ("\nConnected component check...");
		ArrayList components = mm.ComponentCheck (msList);
		Console.WriteLine ("Connected component identification resulted in {0} component{1}:",
			components.Count, components.Count > 1 ? "s" : "");

		int compN = 1;
		foreach (MultiMatch.Component comp in components)
			Console.WriteLine ("component {0}: {1}", compN++, comp);

		if (components.Count > 1) {
			Console.WriteLine ("");
			Console.WriteLine ("Warning: There is one or more components that are not connected through control");
			Console.WriteLine ("         points. An optimization of the resulting PTO will not be possible");
			Console.WriteLine ("         without prior adding of control points between the components listed");
			Console.WriteLine ("         above. Please see the manual page for autopano(1) for details.");
			Console.WriteLine ("");
		} else
			Console.WriteLine ("");

		// BondBall algorithm
		BondBall bb = null;
		if (preAlign) {
			bb = mm.BuildBondBall (msList, bottomDefault);

			if (bb == null) {
				Console.WriteLine ("WARNING: Failed to build bondball as requested. No pre-aligning of images");
				Console.WriteLine ("         takes place.\n");
			}
		}

		if (refine) {
			Console.WriteLine ("Refining keypoints");
			RefineKeypoints (msList, refineMiddle, keepUnrefinable, null);
		}

		TextWriter pto;
		if (args[optionCount] == "-") {
			pto = Console.Out;
		} else {
			Console.WriteLine ("Creating output file \"{0}\"", args[optionCount]);
			pto = new StreamWriter (args[optionCount], false);
		}

		WritePTOFile (pto, mm, msList, bb, generateHorizon, useIntegerCoordinates,
			useAbsolutePathnames);

		if (args[optionCount] != "-")
			pto.Close ();
	}

	public delegate void RefineKeypointEventHandler (int index, int total);

	// selectMiddlePoint: if true, select the middle point in the patch,
	//   otherwise build the mean
	// neverLosePoints: if true, and if we cannot do the refinement, still use
	//   the control point.
	public static void RefineKeypoints (ArrayList msList,
		bool selectMiddlePoint, bool neverLosePoints,
		RefineKeypointEventHandler refineHandler)
	{
		DisplayImage pic1 = null;
		DisplayImage pic2 = null;
		string pic1Name = null;
		string pic2Name = null;

		/* Keep stats for the refineHandler delegate
		 */
		int totalRefines = 0;
		int doneRefines = 0;
		foreach (MultiMatch.MatchSet ms in msList)
			foreach (Match m in ms.Matches)
				totalRefines += 1;

		foreach (MultiMatch.MatchSet ms in msList) {
			Console.WriteLine ("  between \"{0}\" and \"{1}\"",
				ms.File1, ms.File2);

			if (pic1Name != ms.File1) {
				pic1Name = ms.File1;
				pic1 = new DisplayImage (ms.File1);
			}
			if (pic2Name != ms.File2) {
				pic2Name = ms.File2;
				pic2 = new DisplayImage (ms.File2);
			}
			/*Console.WriteLine ("pair: {0}, {1}, {2} keypoint matches",
				ms.File1, ms.File2, ms.Matches.Count);*/

			ArrayList refinedMatches = new ArrayList ();

			foreach (Match m in ms.Matches) {
				int p1x = (int) (m.Kp1.X + 0.5);
				int p1y = (int) (m.Kp1.Y + 0.5);
				int p1radius;
				DisplayImage patch1 = ExtractPatch (pic1, p1x, p1y,
					m.Kp1.Scale, out p1radius);

				int p2x = (int) (m.Kp2.X + 0.5);
				int p2y = (int) (m.Kp2.Y + 0.5);
				int p2radius;
				DisplayImage patch2 = ExtractPatch (pic2, p2x, p2y,
					m.Kp2.Scale, out p2radius);

				/* Call the refine handler delegate in case there is one to
				 * inform the callee of a single refining step (for progress
				 * bar displays and such).
				 */
				doneRefines += 1;
				if (refineHandler != null)
					refineHandler (doneRefines, totalRefines);

				// Skip over keypoint matches we cannot refine as part of the
				// image lies outside.
				if (patch1 == null || patch2 == null) {
					if (neverLosePoints)
						refinedMatches.Add (m);

					continue;
				}

				// Otherwise, run the SIFT algorithm on both small patches.
				ArrayList p1kp = ExtractKeypoints (patch1);
				ArrayList p2kp = ExtractKeypoints (patch2);
				/*Console.WriteLine ("p1kp = {0}, p2kp = {1}", p1kp.Count,
					p2kp.Count);*/

				// Apply the matching, RANSAC enabled.
				MultiMatch mm = new MultiMatch ();
				mm.Verbose = false;

				ArrayList matches = null;
				try {
					matches = mm.TwoPatchMatch (p1kp,
						patch1.Width, patch1.Height, p2kp, patch2.Width,
						patch2.Height, true);
				} catch (MultiMatch.SparseKeypointsException spex) {
					/* In case there are less than three keypoints in the
					 * two patches, we ignore them all.
					 */
					matches = null;
				} catch (Exception ex) {
					/* Rethrow the exception we cannot handle.
					 */
					throw (ex);
				}

				if (matches == null || matches.Count != 1) {
					if (neverLosePoints)
						refinedMatches.Add (m);

					continue;
				}

				MultiMatch.MatchSet pSet = (MultiMatch.MatchSet) matches[0];

				// Now get the real new control point coordinates from the
				// patches.  We have two options and assume all points are
				// equal quality-wise:
				//   a) Select the one that is most in the middle
				//      (selectMiddlePoint == true)
				//   b) Build the mean of all the control point matches in the
				//      patches (selectMiddlePoint == false).
				double kp1X = 0.0;
				double kp1Y = 0.0;
				double kp2X = 0.0;
				double kp2Y = 0.0;
				double kpMidDist = Double.MaxValue;

				foreach (Match pM in pSet.Matches) {
					if (selectMiddlePoint) {
						double dist = Math.Sqrt (
							Math.Pow (pM.Kp1.X - p1radius, 2.0) +
							Math.Pow (pM.Kp1.Y - p1radius, 2.0));

						if (dist < kpMidDist) {
							kpMidDist = dist;

							kp1X = pM.Kp1.X;
							kp1Y = pM.Kp1.Y;

							kp2X = pM.Kp2.X;
							kp2Y = pM.Kp2.Y;
						}
					} else {
						kp1X += pM.Kp1.X;
						kp1Y += pM.Kp1.Y;

						kp2X += pM.Kp2.X;
						kp2Y += pM.Kp2.Y;
					}

					/*Console.WriteLine ("({0}, {1}) matches ({2}, {3})",
						pM.Kp1.X, pM.Kp1.Y, pM.Kp2.X, pM.Kp2.Y);*/
				}

				if (selectMiddlePoint == false) {
					kp1X /= (double) pSet.Matches.Count;
					kp1Y /= (double) pSet.Matches.Count;
					kp2X /= (double) pSet.Matches.Count;
					kp2Y /= (double) pSet.Matches.Count;
				}

				kp1X += p1x - p1radius;
				kp1Y += p1y - p1radius;

				kp2X += p2x - p2radius;
				kp2Y += p2y - p2radius;

				Match mn = (Match) m.Clone ();

				// Adjust the original keypoints location to be the mean of
				// all the highly precise superresolution points.
				mn.Kp1.X = kp1X;
				mn.Kp1.Y = kp1Y;

				mn.Kp2.X = kp2X;
				mn.Kp2.Y = kp2Y;

				/*Console.WriteLine ("MASTER POINT MATCH: ({0},{1}) to ({2},{3})",
					kp1X, kp1Y, kp2X, kp2Y);*/

				refinedMatches.Add (mn);
				/*
				patch1.Save ("patch-1.jpg");
				patch2.Save ("patch-2.jpg");
				System.Environment.Exit (0);
				*/
			}

			ms.Matches = refinedMatches;
		}
	}

	/** Extract a small image patch from a larger image, centered at the given
	 * coordinates.
	 */
	private static DisplayImage ExtractPatch (DisplayImage large,
		int px, int py, double scale, out int radius)
	{
		radius = (int) (9.0 * scale + 0.5);
		if (radius > RefinementRadiusMaximum)
			radius = RefinementRadiusMaximum;

		/*Console.WriteLine ("patch centered at ({0},{1}), scale {2}, radius = {3}",
			px, py, scale, radius);*/

		int pxe = px + radius;
		int pye = py + radius;
		px -= radius;
		py -= radius;

		if (px < 0 || py < 0 || pxe >= large.Width || pye >= large.Height) {
			/*Console.WriteLine ("   ({0},{1})-({2},{3}) out of (0,0)-({4},{5})",
				px, py, pxe, pye, large.Width, large.Height);*/

			return (null);
		} else {
			//Console.WriteLine ("   extracting patch");
		}
		DisplayImage patch = large.Carve (px, py, 2*radius, 2*radius);

		return (patch);
	}

	/** Produce keypoints for a small image patch.
	 */
	private static ArrayList ExtractKeypoints (DisplayImage pic)
	{
		ImageMap picMap = pic.ConvertToImageMap (null);

		LoweFeatureDetector lf = new LoweFeatureDetector ();
		lf.PrintWarning = false;
		lf.Verbose = false;
		lf.DetectFeatures (picMap);

		return (lf.GlobalNaturalKeypoints);
	}

	private static bool YesNoOption (string optionName, string val)
	{
		if (String.Compare (val, "1") == 0 || String.Compare (val, "on") == 0)
			return (true);
		else if (String.Compare (val, "0") == 0 || String.Compare (val, "off") == 0)
			return (false);

		throw (new ArgumentException (String.Format
			("'{0}' is no valid truth value for option '{1}'. Please see manpage for help.",
				val, optionName)));
	}

	public static void WritePTOFile (TextWriter pto, MultiMatch mm,
		ArrayList msList, BondBall bb, int generateHorizon, bool integerCoordinates,
		bool useAbsolutePathnames)
	{
		pto.WriteLine ("# Hugin project file");
		pto.WriteLine ("# automatically generated by autopano-sift, available at");
		pto.WriteLine ("# http://cs.tu-berlin.de/~nowozin/autopano-sift/\n");
		pto.WriteLine ("p f2 w3000 h1500 v360  n\"JPEG q90\"");
		pto.WriteLine ("m g1 i0\n");

		int imageIndex = 0;
		Hashtable imageNameTab = new Hashtable ();
		ArrayList resolutions = new ArrayList ();
		foreach (KeypointXMLList kx in mm.Keysets) {
			imageNameTab[kx.ImageFile] = imageIndex;
			resolutions.Add (new Resolution (kx.XDim, kx.YDim));

			string imageFile = kx.ImageFile;
			if (useAbsolutePathnames)
				imageFile = Path.GetFullPath (imageFile);
			else
				imageFile = Path.GetFileName (imageFile);

			// If the resolution was already there, use the first image with
			// the exact same resolution as reference for camera-related
			// values.
			int refIdx;
			for (refIdx = 0 ; refIdx < (resolutions.Count - 1) ; ++refIdx) {
				if (new Resolution (kx.XDim, kx.YDim).CompareTo (resolutions[refIdx]) == 0)
					break;
			}
			if (refIdx == (resolutions.Count - 1))
				refIdx = -1;

			BondBall.Position pos = bb == null ? null :
				(BondBall.Position) bb.Positions[imageFile];
			/*
			if (pos != null) {
				Console.WriteLine ("yaw {0}, pitch {1}, rotation {2}",
					pos.Yaw, pos.Pitch, pos.Rotation);
			}*/

			double yaw = 0.0, pitch = 0.0, rotation = 0.0;
			if (pos != null) {
				yaw = pos.Yaw;
				pitch = pos.Pitch;
				rotation = pos.Rotation;
			}

			if (imageIndex == 0 || refIdx == -1) {
				pto.WriteLine ("i w{0} h{1} f0 a0 b-0.01 c0 d0 e0 p{2} r{3} v180 y{4}  u10 n\"{5}\"",
					kx.XDim, kx.YDim, pitch, rotation, yaw, imageFile);
			} else {
				pto.WriteLine ("i w{0} h{1} f0 a={2} b={2} c={2} d0 e0 p{3} r{4} v={2} y{5}  u10 n\"{6}\"",
					kx.XDim, kx.YDim, refIdx, pitch, rotation, yaw, imageFile);
			}
			imageIndex += 1;
		}

		pto.WriteLine ("\nv p1 r1 y1\n");

		pto.WriteLine ("# match list automatically generated");
		foreach (MultiMatch.MatchSet ms in msList) {
			foreach (Match m in ms.Matches) {
				if (integerCoordinates == false) {
					pto.WriteLine ("c n{0} N{1} x{2:F6} y{3:F6} X{4:F6} Y{5:F6} t0",
						imageNameTab[ms.File1], imageNameTab[ms.File2],
						m.Kp1.X, m.Kp1.Y, m.Kp2.X, m.Kp2.Y);
				} else {
					pto.WriteLine ("c n{0} N{1} x{2} y{3} X{4} Y{5} t0",
						imageNameTab[ms.File1], imageNameTab[ms.File2],
						(int) (m.Kp1.X + 0.5), (int) (m.Kp1.Y + 0.5),
						(int) (m.Kp2.X + 0.5), (int) (m.Kp2.Y + 0.5));
				}
			}
		}

		// Generate horizon if we should
		if (bb != null && generateHorizon > 0) {
			Console.WriteLine ("Creating horizon...");

			int kMain = 2;
			int hPoints = generateHorizon;
			int horizonPointsMade = 0;

			bool hasGood = true;
			while (hPoints > 0 && hasGood) {
				hasGood = false;
				int kStep = 2 * kMain;

				for (int p = 0 ; hPoints > 0 && p < kMain ; ++p) {
					double stepSize = ((double) bb.FirstRow.Count) / ((double) kStep);
					double beginIndex = p * stepSize;
					double endIndex = (((double) bb.FirstRow.Count) / (double) kMain) +
						p * stepSize;

					// Round to next integer and check if their image distance
					// is larger than 1. If its not, we skip over this useless
					// horizon point.
					int bi = (int) (beginIndex + 0.5);
					int ei = (int) (endIndex + 0.5);
					if ((ei - bi) <= 1)
						continue;

					hasGood = true;

					bi %= bb.FirstRow.Count;
					ei %= bb.FirstRow.Count;
					pto.WriteLine ("c n{0} N{1} x{2} y{3} X{4} Y{5} t2",
						imageNameTab[(string) bb.FirstRow[bi]],
						imageNameTab[(string) bb.FirstRow[ei]],
						((Resolution) resolutions[bi]).X / 2,
						((Resolution) resolutions[bi]).Y / 2,
						((Resolution) resolutions[ei]).X / 2,
						((Resolution) resolutions[ei]).Y / 2);

					horizonPointsMade += 1;
					hPoints -= 1;
				}

				// Increase density for next generation lines
				kMain *= 2;
			}
			Console.WriteLine ("  made {0} horizon lines.\n", horizonPointsMade);
		}

		pto.WriteLine ("\n# :-)\n");
		pto.Flush ();
		//pto.Close ();

		Console.WriteLine ("\nYou can now load the output file into hugin.");
		Console.WriteLine ("Notice: You absolutely must adjust the field-of-view value for the images");
	}
}


class Resolution : IComparable
{
	int x, y;
	public Resolution (int x, int y)
	{
		this.x = x;
		this.y = y;
	}

	public int X {
		get {
			return (x);
		}
	}

	public int Y {
		get {
			return (y);
		}
	}

	public int CompareTo (object obj)
	{
		Resolution res = (Resolution) obj;
		if (res.x == x && res.y == y)
			return (0);

		return (1);
	}
}


