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

using System;
using System.IO;
using System.Threading;
using System.Globalization;
using System.Collections;
using GLib;
using Gtk;
using GtkSharp;
using Glade;

public class Autopanog
{
	// Widgets
	[Glade.Widget]
	Gtk.Window autopanogWin;

	[Glade.Widget]
	Gtk.Window computeWin;

	[Glade.Widget]
	Gtk.TreeView fileTree;

	// The preview image shown below the image list
	[Glade.Widget]
	Gtk.Image previewImage;
	Gdk.Pixbuf previewImageVanilla;
	Gdk.Pixbuf previewImageVanillaScaled;

	[Glade.Widget]
	Gtk.Frame previewFrame;

	// Displayed text
	[Glade.Widget]
	Gtk.Label imageSelectFrame;

	// Action buttons
	[Glade.Widget]
	Gtk.Button computeButton;

	// Filter and output options
	[Glade.Widget]
	Gtk.CheckButton useRANSAC;

	[Glade.Widget]
	Gtk.CheckButton useAreafilter;

	[Glade.Widget]
	Gtk.CheckButton useSubpixel;

	[Glade.Widget]
	Gtk.CheckButton useAbsolutePathnames;

	// Alignment options
	[Glade.Widget]
	Gtk.CheckButton useAlignment;
	[Glade.Widget]
	Gtk.Frame alignOptionsFrame;
	[Glade.Widget]
	Gtk.CheckButton generateHorizon;

	// Refinement options
	[Glade.Widget]
	Gtk.CheckButton refineCheck;
	[Glade.Widget]
	Gtk.Frame refineOptionFrame;
	[Glade.Widget]
	Gtk.CheckButton refineKeepUnrefinable;
	bool refineMiddle = true;

	// Image buttons
	[Glade.Widget]
	Gtk.RadioButton bottomLeftButton;
	[Glade.Widget]
	Gtk.RadioButton bottomRightButton;

	// Disable if generateHorizon is false
	[Glade.Widget]
	Gtk.Combo horizonLineCombo;
	[Glade.Widget]
	Gtk.Entry horizonLineCount;
	public void HorizonLineCountVerifier (object obj, FocusOutEventArgs args)
	{
		int test = HorizonLineCount;
	}
	public int HorizonLineCount {
		get {
			int linecount;
			switch (horizonLineCount.Text) {
				case ("unlimited"):
					return (0);
				default:
					try {
						linecount = Int32.Parse (horizonLineCount.Text);
						if (linecount < 0)
							throw (new Exception ("less than zero is invalid"));
					} catch (Exception ex) {
						MessageDialog md = new MessageDialog (autopanogWin, 
							DialogFlags.DestroyWithParent, MessageType.Error,
							ButtonsType.Ok,
							"Invalid horizon line count given.\nPlease use positive integer numbers.");

						md.Run ();
						md.Destroy();

						horizonLineCount.Text = "6";
						return (HorizonLineCount);
					}

					return (linecount);
			}
		}
	}

	[Glade.Widget]
	Gtk.Image bottomIsLeftImage;
	[Glade.Widget]
	Gtk.Image bottomIsRightImage;


	[Glade.Widget]
	Gtk.Entry downscaleResolution;
	public void DownscaleResolutionVerifier (object obj, FocusOutEventArgs args)
	{
		int test = DownscaleResolution;
	}
	public int DownscaleResolution {
		get {
			int down;
			switch (downscaleResolution.Text) {
				case ("keep input size"):
					return (0);
				case ("double input size"):
					return (-1);
				default:
					try {
						down = Int32.Parse (downscaleResolution.Text);
						if (down < -1)
							throw (new Exception ("less than -1 is invalid"));
					} catch (Exception ex) {
						MessageDialog md = new MessageDialog (autopanogWin, 
							DialogFlags.DestroyWithParent, MessageType.Error,
							ButtonsType.Ok,
							"Invalid downscale resolution given.\nPlease use positive integer numbers.");

						md.Run ();
						md.Destroy();

						downscaleResolution.Text = "700";
						return (DownscaleResolution);
					}

					return (down);
			}
		}
	}

	[Glade.Widget]
	Gtk.Entry maxPairKeypoints;
	public void MaxPairKeypointsVerifier (object obj, FocusOutEventArgs args)
	{
		int test = MaxPairKeypoints;
	}
	public int MaxPairKeypoints {
		get {
			if (String.Compare (maxPairKeypoints.Text, "unlimited") == 0)
				return (0);

			try {
				int max = Int32.Parse (maxPairKeypoints.Text);

				if (max < 0)
					throw (new Exception ("Negative number"));

				return (max);
			} catch (Exception ex) {
				MessageDialog md = new MessageDialog (autopanogWin, 
					DialogFlags.DestroyWithParent, MessageType.Error,
					ButtonsType.Ok,
					"Invalid maximum number of keypoints per pair given.\nPlease use a positive integer number.");

				md.Run ();
				md.Destroy();

				maxPairKeypoints.Text = "12";
				return (MaxPairKeypoints);
			}
		}
	}

	[Glade.Widget]
	Gtk.Entry savePositionPrefix;
	public string SavePositionPrefix {
		get {
			return (savePositionPrefix.Text);
		}
	}

	[Glade.Widget]
	Gtk.Entry ptoFilename;
	string ptoPathname = null;
	public string PTOFilename {
		get {
			if (ptoPathname == null)
				return (ptoFilename.Text);

			return (ptoPathname);
		}
		set {
			ptoPathname = value;
			ptoFilename.Text = ptoPathname;
		}
	}

	[Glade.Widget]
	Gtk.ProgressBar generationProgress;
	int imageDone;

	[Glade.Widget]
	Gtk.ProgressBar matchingProgress;

	[Glade.Widget]
	Gtk.ProgressBar refiningProgress;

	[Glade.Widget]
	Gtk.Button progressQuitButton;

	// Data
	private enum KeypointsTarget {
		File,
		FileCompressed,
		Memory,
	}
	KeypointsTarget target = KeypointsTarget.File;
	string outputFile;

	TreeStore store;
	ArrayList memoryKeypointsList;
	ArrayList keypointFilesList;

	public static void Main (string[] args)
	{
		/* Make culture invariant to produce always floating point output
		 * hugin can understand.
		 */
		System.Threading.Thread.CurrentThread.CurrentCulture =
			CultureInfo.InvariantCulture;

		Application.Init ();

		Autopanog ap = new Autopanog ();
		ap.Init ();

		int optionN = 0;
		int optionCount = 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, "--output") == 0) {
				Console.WriteLine ("output: {0}", args[optionN + 1]);
				ap.PTOFilename = args[optionN + 1];
				optionN += 2;
			} else if (String.Compare (optionStr, "--absolute") == 0) {
				ap.useAbsolutePathnames.Active = true;
				optionN += 1;
			} else if (String.Compare (optionStr, "--imagelist") == 0) {
				StreamReader inputFiles = new StreamReader (args[optionN + 1]);
				optionN += 2;

				string iLine;
				while ((iLine = inputFiles.ReadLine ()) != null) {
					Console.WriteLine ("adding: {0}", iLine);
					ap.AddSingleImage (iLine);
				}
			} else {
				Usage ();
				Environment.Exit (1);
			}
		}
		optionCount = optionN;

		// The remaining image files
		if ((args.Length - optionCount) > 0) {
			Console.WriteLine ("B");
			string[] imgFiles = new string[args.Length - optionCount];
			Array.Copy (args, optionCount, imgFiles, 0, args.Length - optionCount);

			foreach (string imageFile in imgFiles)
				ap.AddSingleImage (imageFile);
		}

		Application.Run ();
	}

	private static void Usage ()
	{
		Console.WriteLine ("usage: autopanog.exe [options] [imagefile1 [imagefile2 [..]]]\n");

		Console.WriteLine ("Options");
		Console.WriteLine ("  --output <filepath>     Pre-set the output PTO pathname");
		Console.WriteLine ("  --imagelist <file.txt>  Take a list of input images from a file");
		Console.WriteLine ("  --absolute              Use absolute pathnames in PTO file");
		Console.WriteLine ("\nNote:");
		Console.WriteLine ("  It is possible to use the --imagelist option multiple times.");
		Console.WriteLine ("  It is also possible to combine it with the command line image list.");
		Console.WriteLine ("");
	}

	private void Init ()
	{
		//Glade.XML gxml = new Glade.XML ("autopanog.glade", "autopanogWin", null);
		Glade.XML gxml = new Glade.XML (null, "autopanog.glade", null, null);
		gxml.Autoconnect (this);

		/*gxml = new Glade.XML ("autopanog.glade", "computeWin", null);
		gxml.Autoconnect (this);*/
		downscaleResolution.FocusOutEvent +=
			new FocusOutEventHandler (DownscaleResolutionVerifier);
		maxPairKeypoints.FocusOutEvent +=
			new FocusOutEventHandler (MaxPairKeypointsVerifier);
		horizonLineCount.FocusOutEvent +=
			new FocusOutEventHandler (HorizonLineCountVerifier);

		// Format: full filepath, filename, info
		store = new TreeStore (typeof (string), typeof (string), typeof (string));
		fileTree.Model = store;
		fileTree.HeadersVisible = true;
		fileTree.AppendColumn ("Filename", new CellRendererText (), "text", 1);
		fileTree.AppendColumn ("Information", new CellRendererText (), "text", 2);
		fileTree.Selection.Mode = SelectionMode.Multiple;

		fileTree.CursorChanged += new EventHandler (FileTreeCursorChange);

		// Setup panel
		horizonLineCombo.Sensitive = false;
		alignOptionsFrame.Sensitive = false;
		refineOptionFrame.Sensitive = false;

		Gdk.Pixbuf left = new Gdk.Pixbuf (null, "image-bottom-left.png");
		bottomIsLeftImage.FromPixbuf = left.ScaleSimple
			((int) (left.Width * 0.4), (int) (left.Width * 0.4), Gdk.InterpType.Bilinear);
		Gdk.Pixbuf right = new Gdk.Pixbuf (null, "image-bottom-right.png");
		bottomIsRightImage.FromPixbuf = right.ScaleSimple
			((int) (right.Width * 0.4), (int) (right.Width * 0.4), Gdk.InterpType.Bilinear);

		previewImageVanilla = new Gdk.Pixbuf (null, "image-vanilla.png");
		previewImageVanillaScaled = previewImageVanilla.ScaleSimple
			((int) (previewImageVanilla.Width * 0.4),
			(int) (previewImageVanilla.Height * 0.4), Gdk.InterpType.Bilinear);
		previewImage.FromPixbuf = previewImageVanillaScaled;
	}

	public void FileTreeCursorChange (object obj, EventArgs args)
	{
		if (fileTree.Selection.CountSelectedRows () >= 1) {

			TreeModel model;
			TreePath[] removals = fileTree.Selection.GetSelectedRows (out model);
			TreePath path = removals[0];
			TreeStore st = (TreeStore) model;
			TreeIter iter;

			st.GetIter (out iter, path);
			//Console.WriteLine ("iterstring: {0}", st.GetStringFromIter (iter));
			string filename = (string) model.GetValue (iter, 0);
			//Console.WriteLine ("file: {0}", filename);

			// Update preview image into GUI
			Gdk.Pixbuf preview = new Gdk.Pixbuf (filename);
			double scaleFactorX, scaleFactorY;
			scaleFactorX = 256.0 / ((double) preview.Width);
			scaleFactorY = 256.0 / ((double) preview.Height);

			if (scaleFactorX >= 1.0 && scaleFactorY >= 1.0) {
				previewImage.FromPixbuf = preview;
				return;
			}
			double smallerScale = (scaleFactorX < scaleFactorY) ?
				scaleFactorX : scaleFactorY;
			preview = preview.ScaleSimple ((int) (preview.Width * smallerScale),
				(int) (preview.Height * smallerScale), Gdk.InterpType.Nearest);
			previewImage.FromPixbuf = preview;
		}
	}

	public void OnautopanogWinDelete (object obj, DeleteEventArgs args)
	{
		Application.Quit ();

		args.RetVal = true;
	}

	public void OnRefineMiddle (object obj, EventArgs ev)
	{
		refineMiddle = true;
	}
	public void OnRefineMean (object obj, EventArgs ev)
	{
		refineMiddle = false;
	}

	public void OnKeypoints2File (object obj, EventArgs ev)
	{
		savePositionPrefix.Sensitive = true;
		target = KeypointsTarget.File;
	}

	public void OnKeypoints2FileCompressed (object obj, EventArgs ev)
	{
		savePositionPrefix.Sensitive = true;
		target = KeypointsTarget.FileCompressed;
	}

	public void OnKeypoints2Mem (object obj, EventArgs ev)
	{
		savePositionPrefix.Sensitive = false;
		target = KeypointsTarget.Memory;
	}

	public void OnUseAlignmentToggled (object obj, EventArgs ev)
	{
		alignOptionsFrame.Sensitive = (alignOptionsFrame.Sensitive == false);
	}
	public void OnRefineCheckToggled (object obj, EventArgs ev)
	{
		refineOptionFrame.Sensitive = (refineOptionFrame.Sensitive == false);
	}

	public void OnGenerateHorizonToggled (object obj, EventArgs ev)
	{
		horizonLineCombo.Sensitive = (horizonLineCombo.Sensitive == false);
	}

	public void OnPTOSelectClicked (object obj, EventArgs ev)
	{
		FileSelection fs = new FileSelection ("PTO output file");
		fs.SelectMultiple = false;
		fs.Run ();

		ptoFilename.Text = fs.SelectionEntry.Text;
		ptoPathname = fs.Filename;

		// Enable the absolute image pathname option automatically in case the
		// PTO file is _not_ saved into the image file directory.
		string ptoDir = Path.GetDirectoryName (Path.GetFullPath (ptoPathname));
		if (ImageDirectoriesDifferent () ||
			(ImageDirectory () != null &&
				String.Compare (ImageDirectory (), ptoDir) != 0))
		{
			useAbsolutePathnames.Active = true;
		}

		fs.Hide ();
	}

	private void OnAddImageCancel (object obj, EventArgs ev)
	{
		imageAddCancelled = true;
	}
	private bool imageAddCancelled;
	string imagesDirectory = null;

	public void OnAddImageClicked (object obj, EventArgs ev)
	{
		FileSelection fs = new FileSelection ("Add image files");

		fs.SelectMultiple = true;
		imageAddCancelled = false;
		fs.CancelButton.Clicked += OnAddImageCancel;
		fs.Run ();

		if (imageAddCancelled) {
			fs.Hide ();

			return;
		}
		fs.Hide ();

		foreach (string filename in fs.Selections) {
			//Console.WriteLine ("adding: {0}", filename);
			AddSingleImage (filename);
		}

		// Check if the image file directory is different for some of the
		// image files and in case it is, enable the absolute pathname option.
		if (ImageDirectoriesDifferent ())
			useAbsolutePathnames.Active = true;

	}

	private void AddSingleImage (string filename)
	{
		Gdk.Pixbuf pbuf;
		try {
			pbuf = new Gdk.Pixbuf (filename);
		} catch (GLib.GException gex) {
			Console.Error.WriteLine ("{0}: {1}", Path.GetFileName (filename),
				gex.Message);
			return;
		}
		store.AppendValues (filename, Path.GetFileName (filename),
			String.Format ("{0} x {1}", pbuf.Width, pbuf.Height));

		pbuf = null;
		GC.Collect ();

		// Update top-text in image frame
		imageSelectFrame.Text = String.Format ("Source images ({0} images)",
			ImageCount ());
		if (ImageCount () >= 2)
			computeButton.Sensitive = true;
		else
			computeButton.Sensitive = false;

		// Update the GUI so the user has some visible feedback the GUI is
		// doing something.  Maybe a progressbar would be better.
		while (Application.EventsPending ())
			Application.RunIteration (false);
	}

	public void OnRemoveImageClicked (object obj, EventArgs ev)
	{
		//Console.WriteLine ("selected {0} rows", fileTree.Selection.CountSelectedRows ());

		while (fileTree.Selection.CountSelectedRows () > 0) {
			TreeModel model;
			TreePath[] removals = fileTree.Selection.GetSelectedRows (out model);
			TreePath path = removals[0];
			TreeStore st = (TreeStore) model;
			TreeIter iter;

			st.GetIter (out iter, path);
			//Console.WriteLine ("iterstring: {0}", st.GetStringFromIter (iter));

			st.Remove (ref iter);

			// Decrease/update top-text in image frame
			imageSelectFrame.Text = String.Format ("Source images ({0} images)",
				ImageCount ());

			if (ImageCount () >= 2)
				computeButton.Sensitive = true;
			else
				computeButton.Sensitive = false;
		}

		// If no images are left, set the imageDirectory to null and uncheck
		// the absolute pathname option
		if (ImageCount () == 0) {
			imageSelectFrame.Text = "Source images";
			imagesDirectory = null;
			useAbsolutePathnames.Active = false;
		}

		previewImage.FromPixbuf = previewImageVanillaScaled;
	}

	private int ImageCount ()
	{
		imageCount = 0;
		imageListIsEmpty = true;
		imagePathNameTest = null;
		differentPathes = false;
		store.Foreach (new TreeModelForeachFunc (CountImages));

		return (imageCount);
	}

	private string ImageDirectory ()
	{
		if (ImageCount () == 0)
			return (null);

		return (imagePathNameTest);
	}

	private bool ImageDirectoriesDifferent ()
	{
		imageCount = 0;
		imageListIsEmpty = true;
		imagePathNameTest = null;
		differentPathes = false;
		store.Foreach (new TreeModelForeachFunc (CountImages));

		return (differentPathes);
	}

	bool imageListIsEmpty = true;
	string imagePathNameTest;
	int imageCount;
	bool differentPathes;
	private bool CountImages (TreeModel model, TreePath path, TreeIter iter)
	{
		imageCount += 1;
		imageListIsEmpty = false;

		string filename = (string) model.GetValue (iter, 0);
		string dirName = Path.GetDirectoryName (Path.GetFullPath (filename));
		if (imagePathNameTest == null) {
			imagePathNameTest = dirName;

			return (false);
		}

		if (String.Compare (imagePathNameTest, dirName) == 0)
			return (false);

		// Images are in different directories
		differentPathes = true;

		return (false);
	}

	public void OnComputeClicked (object obj, EventArgs ev)
	{
		TreeIter iter;
		if (store.GetIterFirst (out iter) == false) {
			Console.Error.WriteLine ("No images in tree.");
			return;
		}

		// Get horizon count
		int horizonCount = 0;
		if (String.Compare (horizonLineCount.Text, "unlimited") == 0) {
			horizonCount = 1024;
		} else {
			try {
				horizonCount = Int32.Parse (horizonLineCount.Text);
			} catch (Exception ex) {
				MessageDialog md = new MessageDialog (autopanogWin, 
					DialogFlags.DestroyWithParent, MessageType.Error,
					ButtonsType.Ok, "Invalid number of horizon points specified, we do not generate horizon lines now.\nPlease use positive integer numbers.");

				md.Run ();
				md.Destroy();

				return;
			}
		}


		imageCount = 1;
		while (store.IterNext (ref iter))
			imageCount += 1;

		// TODO: possibly silent console output from LoweDetector class
		autopanogWin.Hide ();

		//generationProgress.BarStyle = ProgressBarStyle.Discrete;
		generationProgress.DiscreteBlocks = (uint) imageCount;
		generationProgress.Fraction = 0.0;
		imageDone = 0;
		generationProgress.Text = String.Format ("{0} / {1}", imageDone, imageCount);

		computeWin.ShowAll ();
		while (Application.EventsPending ())
			Application.RunIteration (false);

		memoryKeypointsList = new ArrayList ();
		keypointFilesList = new ArrayList ();
		store.Foreach (new TreeModelForeachFunc (GenerateKeypointForImage));

		matchingProgress.Text = "Generating kd-tree...";
		while (Application.EventsPending ())
			Application.RunIteration (false);

		MultiMatch mm = new MultiMatch ();
		if (target == KeypointsTarget.Memory) {
			mm.LoadKeysetsFromMemory (memoryKeypointsList);
		} else {
			mm.LoadKeysets ((string[]) keypointFilesList.ToArray (typeof (string)));
		}

		mm.MatchKeypoint += new MultiMatch.MatchKeypointEventHandler (UpdateMatchProgress);
		ArrayList matches = mm.LocateMatchSets (3, MaxPairKeypoints,
			useRANSAC.Active, useAreafilter.Active);

		// Component check.
		ArrayList components = mm.ComponentCheck (matches);

		// BondBall algorithm
		int bottomDefault = -1;
		if (bottomLeftButton.Active)
			bottomDefault = 0;
		else if (bottomRightButton.Active)
			bottomDefault = 1;

		BondBall bb = null;
		if (useAlignment.Active) {
			bb = mm.BuildBondBall (matches, bottomDefault);

			if (bb == null) {
				MessageDialog md = new MessageDialog (computeWin, 
					DialogFlags.DestroyWithParent, MessageType.Error,
					ButtonsType.Ok,
					String.Format ("Creating the bondball for automatic image alignment failed.{0}",
						(bottomDefault == -1) ?
						"\n\nIf your input images are not in normal position, try giving the image orientation manually using the picture icons :-)" : ""));

				md.Run ();
				md.Destroy();
			}
		}

		// Refine
		if (refineCheck.Active) {
			matchingProgress.Fraction = 1.0;
			matchingProgress.Text = "Done.";
			refiningProgress.Text = "Refining keypoints...";
			while (Application.EventsPending ())
				Application.RunIteration (false);

			Autopano.RefineKeypoints (matches, refineMiddle,
				refineKeepUnrefinable.Active,
				new Autopano.RefineKeypointEventHandler (UpdateRefineProgress));
		}

		// Write PTO file
		TextWriter pto = new StreamWriter (PTOFilename, false);
		Autopano.WritePTOFile (pto, mm, matches, bb,
			horizonCount, useSubpixel.Active == false, useAbsolutePathnames.Active);
		pto.Close ();

		matchingProgress.Fraction = 1.0;
		if (refineCheck.Active) {
			refiningProgress.Text = "Finished. PTO file saved.";
		} else {
			matchingProgress.Text = "Finished. PTO file saved.";
		}
		progressQuitButton.Sensitive = true;

		if (components.Count > 1) {
			string componentDesc = "";
			int compN = 1;
			foreach (MultiMatch.Component comp in components)
				componentDesc += String.Format ("component {0}: {1}\n\n", compN++, comp);

			MessageDialog md = new MessageDialog (computeWin, 
				DialogFlags.DestroyWithParent, MessageType.Warning,
				ButtonsType.Ok, String.Format
				("A connected-component check has identified {0} components among the image matches. This means, there are images that have no connection with other images. The following components have been identified:\n\n{1}", components.Count, componentDesc));
		     
			md.Run ();
			md.Destroy();
		}
	}

	private void UpdateRefineProgress (int index, int total)
	{
		refiningProgress.Fraction = ((double) index) / ((double) total);
		refiningProgress.Text = String.Format ("{0} / {1}", index, total);
		while (Application.EventsPending ())
			Application.RunIteration (false);
	}

	private void UpdateMatchProgress (int index, int total)
	{
		matchingProgress.Fraction = ((double) index) / ((double) total);
		matchingProgress.Text = String.Format ("{0} / {1}", index, total);
		while (Application.EventsPending ())
			Application.RunIteration (false);
	}

	private bool GenerateKeypointForImage (TreeModel model, TreePath path, TreeIter iter)
	{
		string filename = (string) model.GetValue (iter, 0);
		//Console.WriteLine ("file: {0}", filename);

		DisplayImage disp = new DisplayImage (filename);
		int pW = disp.Width;
		int pH = disp.Height;
		double startScale = 1.0;
		bool doubleInputSize = false;
		switch (DownscaleResolution) {
			// Double input image size
			case (-1):
				doubleInputSize = true;
				break;
			// Keep input image size
			case (0):
				break;
			// Downscale to fit within a quadratic box of DownscaleResolution
			// pixels
			default:
				startScale = disp.ScaleWithin (DownscaleResolution);
				break;
		}

		ImageMap picMap = disp.ConvertToImageMap (null);
		disp = null;
		GC.Collect ();

		// Depending on whether we should double the picture size or use the
		// downscaled/original image, we call the different detector methods
		// now.
		LoweFeatureDetector lf = new LoweFeatureDetector ();
		if (doubleInputSize)
			lf.DetectFeatures (picMap);
		else
			lf.DetectFeaturesDownscaled (picMap, 0, 1.0 / startScale);

		Console.WriteLine ("found {0} global keypoints",
			lf.GlobalNaturalKeypoints.Count);

		imageDone += 1;
		generationProgress.Fraction = ((double) imageDone) / ((double) imageCount);
		generationProgress.Text = String.Format ("{0} / {1}", imageDone, imageCount);
		//generationProgress.QueueDraw ();
		while (Application.EventsPending ())
			Application.RunIteration (false);

		if (target == KeypointsTarget.Memory) {
			memoryKeypointsList.Add (new KeypointXMLList (filename,
				pW, pH, lf.GlobalNaturalKeypoints));
		} else {
			string targetfile = SavePositionPrefix +
				Path.GetFileName (filename) + ".xml";
			if (target == KeypointsTarget.FileCompressed)
				targetfile = targetfile + ".gz";

			keypointFilesList.Add (targetfile);

			KeypointXMLWriter.WriteComplete (filename, pW, pH, targetfile,
				lf.GlobalNaturalKeypoints);
		}

		return (false);
	}

	public void OnProgressQuitButtonClicked (object obj, EventArgs ev)
	{
		Application.Quit ();
	}
}


