In the last post we looked at a command to allow importing of Photosynth point clouds into AutoCAD. In this post we’ll put a GUI on the front end, to avoid people having to sniff network traffic to determine the location of the appropriate files on the Photosynth servers.
The application is actually relative simple: it hosts a browser control that gets pointed at the Photosynth web-site, allowing the user to browse through Photosynths. As point clouds are detected (as the browser has some handy events notifying of the HTTP traffic generated by the embedded Photosynth application, and we know that the first point cloud file is always named “points_0_0.bin”), they get added to a list on the right-hand side of the form. They initially get added with just the title (the URL is stored elsewhere) and we then start a timer which will fire each second and – for any items that don’t yet have one – download an image from the server at an appropriate level of detail which we use for a thumbnail. We don’t download the image directly from the HTTP event, as that causes re-entrancy issues (the event will get fired again, which causes the prior event to get cancelled).
Over time the list builds up. When the user feels like it, they can click (or right-click, as they prefer) an item from the list to import it into AutoCAD. This will then cause the command we saw previously to get launched with the URL and the title.
When I started this application I created an in-process form for the browser. I found a really cool control, called csExWb2, which provided the HTTP events for which I was looking. While pretty extensive, there are two main problems with this control: firstly, it’s 32-bit only, so it can’t be hosted inside an AutoCAD plugin on a 64-bit system. Secondly, under certain circumstances it seems to have trouble exiting (and I’m not the only person to hit this, by all accounts).
Thankfully the same approach appears to address both problems: hosting the browser in a separate executable should allow it to run as a 32-bit process on 64-bit systems, and it will allow us to kill the process from our command-implementation should it choose not to exit cleanly. There are ancillary benefits to this approach related to per-process memory consumption (the browser can quickly consume 100+ Mb of memory) and the ability to rebuild the browser without restarting AutoCAD, but those really are of secondary importance.
As we’re using a separate executable, there are clearly some Inter-Process Communication (IPC) issues to deal with. One option would have been to use COM for this, but I decided to go old school and just launch a process for the browser (passing in the handle of the AutoCAD instance as a command-line (string) parameter) and then use the SendMessage() Win32 API to communicate back to AutoCAD.
A couple of extra points to note… I ended up using WinForms for this UI (along with a fun OwnerDraw implementation to make the custom UI look consistent with Photosynth’s) but I could very easily imagine using WPF for this (and I’m sure it would look much better, too). That may be for version 2. I also found the WinForms ListView control to behave quite strangely: I had to jump through some hoops to get the items to centre, for instance, and I’m still not happy that hovering/selecting on the left-hand side of an item doesn’t cause it to be selected. Again, something a WPF version would resolve, I expect.
Now for some code. As there’s now some complexity to the project (including a dialog), you can get the complete source project here.
The C# code for the browser application is very simple:
using System;
using System.Windows.Forms;
using BrowsePhotosynth;
namespace Browser
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
string[] args = Environment.GetCommandLineArgs();
// Extract the handle passed as an argument.
// This is AutoCAD's main window, and we'll use it
// to pump messages. If not handle, set it to 0,
// which means "standalone mode"
int hwnd =
(args.Length > 1 ? int.Parse(args[1]) : 0);
Application.Run(
new BrowserForm("http://photosynth.net", hwnd)
);
}
}
}
Most of the heavy lifting is done by the main implementation file behind the BrowserForm class:
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using System;
namespace BrowsePhotosynth
{
public partial class BrowserForm : Form
{
// A Win32 function we'll use to send messages to AutoCAD
[DllImport("user32.dll")]
private static extern IntPtr SendMessageW(
IntPtr hWnd, int Msg, IntPtr wParam,
ref COPYDATASTRUCT lParam
);
// And the structure we'll require to do so
private struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
// A class containing the browsing information about each
// point-cloud. This is made serializable to XML, to
// allow easy persistence
[XmlRoot("Synth")]
public class SynthInfo
{
// The name of our Photosynth
private string _name;
[XmlAttribute("Name")]
public string Name
{
get { return _name; }
set { _name = value; }
}
// Its URL
private string _url;
[XmlElement( "Url" )]
public string Url
{
get { return _url; }
set { _url = value; }
}
// The location of its image file
private string _image;
[XmlElement( "Image" )]
public string Image
{
get { return _image; }
set { _image = value; }
}
}
// We store a central list of these SynthInfo objects.
private List<SynthInfo> _synths = new List<SynthInfo>();
// A timer to call back into the code to download images
// (to avoid rentrancy)
private Timer _timer = new Timer();
// Public property for the URL loaded into the browser
// (this is the URL of the browser, which is typically
// "http://photosynth.net")
private string _url = null;
public string Url
{
set { _url = value; }
get { return _url; }
}
// Public property for the handle of the AutoCAD application
// we're connected to
private int _hwnd = 0;
public int HWnd
{
set { _hwnd = value; }
get { return _hwnd; }
}
// Internal constants
const string pointsName = "points_0_0.bin";
const string suffix = " - Photosynth";
const string historyXml = "BrowsingHistory.xml";
const int imgWidth = 128;
const int imgHeight = imgWidth;
// Form constructor
public BrowserForm(string url, int hwnd)
{
InitializeComponent();
_url = url;
_hwnd = hwnd;
// Handlers for our various events
// Form events for loading/closing
Load += new EventHandler(BrowserForm_Load);
FormClosing +=
new FormClosingEventHandler(BrowserForm_FormClosing);
// Our main browser event, telling us when a URL is being
// accessed (this allows us to detect when point clouds
// are being accessed by the Photosynth application)
_browser.ProtocolHandlerBeginTransaction +=
new csExWB.ProtocolHandlerBeginTransactionEventHandler(
cEXWB1_ProtocolHandlerBeginTransaction
);
// Events for selection of items from the list (and
// our owner-draw implementation)
_cloudList.MouseClick +=
new MouseEventHandler(cloudList_MouseClick);
_cloudList.DrawItem +=
new DrawListViewItemEventHandler(cloudList_DrawItem);
// Event for selection from the the right-click menu
_cloudsMenu.ItemClicked +=
new ToolStripItemClickedEventHandler(
ContextMenuStrip_ItemClicked
);
// Set the appropriate list display properties
_cloudImages.ImageSize = new Size(imgWidth, imgHeight);
_cloudList.OwnerDraw = true;
}
private void BrowserForm_Load(object sender, EventArgs e)
{
// Navigate to the provided URL, if it's non-null and
// not the one we're already pointed at
if (!String.IsNullOrEmpty(_url) &&
_browser.LocationUrl != _url)
_browser.Navigate(_url);
// Load our browsing history from the XML file.
_synths = DeserializeHistory();
// For each member in the history, re-create
// entries in the list view
foreach (SynthInfo sinf in _synths)
{
// Create a list view item with appropriate indentation
ListViewItem lvi = new ListViewItem(sinf.Name);
lvi.IndentCount = 10;
_cloudList.Items.Add(lvi);
// If we have a valid image file, load it into the list
// (if one doesn't exist then it should be downloaded
// from the server when the timer fires)
if (!String.IsNullOrEmpty(sinf.Image))
{
string imgFile = GetOutputLocation() + sinf.Image;
if (File.Exists(imgFile))
lvi.ImageIndex =
_cloudImages.Images.Add(
System.Drawing.Image.FromFile(imgFile),
Color.Black
);
}
}
}
void BrowserForm_FormClosing(
object sender, FormClosingEventArgs e
)
{
// Store our browsing history to XML
SerializeHistory(_synths);
}
private void StartTimer()
{
// Create a timer which will fire every second
// (we use this to check for images to download and
// add to our dialog, as we get re-entrancy problems
// if we do so from our http monitoring callback)
_timer.Interval = 100;
_timer.Tick += new EventHandler(OnTick);
_timer.Start();
}
private void StopTimer()
{
// Stop our timer
_timer.Stop();
_timer.Tick -= new EventHandler(OnTick);
}
private void cEXWB1_ProtocolHandlerBeginTransaction(
object sender,
csExWB.ProtocolHandlerBeginTransactionEventArgs e
)
{
// If we detect the first point cloud file in a series...
if (e.URL.Contains(pointsName))
{
csExWB.cEXWB wb = (csExWB.cEXWB)sender;
// Get the page's title and extract the URL
string title = wb.GetTitle(true);
// If the point cloud was embedded in the main page,
// let's extract its actual title from the HTML content
// (this will change as the Photosynth page structure
// changes, but if it doesn't find the relevant entries
// then we just use the overall title)
if (title.StartsWith("Photosynth"))
{
string src = wb.DocumentSource;
if (src.Contains("title-block"))
{
src = src.Substring(src.IndexOf("title-block"));
if (src.Contains("A href="))
{
src = src.Substring(src.IndexOf("A href="));
if (src.Contains(">"))
{
src = src.Substring(src.IndexOf(">"));
if (src.Contains("<"))
{
int endPos = src.IndexOf("<");
if (endPos > 1)
{
title = src.Substring(1, endPos - 1);
}
}
}
}
}
}
else if (title.EndsWith(suffix))
{
// Strip off the common suffix, if it's there
title =
title.Substring(0, title.Length - suffix.Length);
}
// Extract the base URL, without the initial point-cloud
// name
string baseUrl =
e.URL.Substring(0, e.URL.Length - pointsName.Length);
// Use this info to create a new entry in our list
// and start the timer to get the related image
AddToPointCloudList(title, baseUrl);
StartTimer();
}
}
private void OnTick(object sender, EventArgs e)
{
// When the timer fires, check if there are images to add...
if (_cloudImages.Images.Count < _synths.Count)
{
Cursor old = this.Cursor;
this.Cursor = Cursors.WaitCursor;
// Stop the timer, just during processing
StopTimer();
// Add images for any items which don't yet have them
// (realistically this will usually be just one image,
// as we check every second and browsing takes time)
for (
int i = _cloudImages.Images.Count; i < _synths.Count; i++
)
{
// This should be redundant, as _synths should have the
// same number of items as _cloudList.Items, but anyway
if (i < _cloudList.Items.Count)
{
// Get the information on the synth for which we need
// to download the image
SynthInfo sinf = _synths[i];
string baseUrl = sinf.Url;
// Get the list view item
ListViewItem lvi = _cloudList.Items[i];
// Transform our base URL to get the URL
// to an appropriate image on the server
if (baseUrl.Contains(".synth_files"))
{
string imageUrl =
baseUrl.Substring(
0, baseUrl.LastIndexOf(".synth_files")
)
+ "_files/6/0_0.jpg";
// Create a web client to download the image
WebClient wc = new WebClient();
using (wc)
{
string locFile =
MakeValidFileName(lvi.Text) + ".jpg";
string locImage = GetOutputLocation() + locFile;
// Try to download our image file
try
{
wc.DownloadFile(imageUrl, locImage);
}
catch
{ }
// If we were successful, load and add it
if (File.Exists(locImage))
{
lvi.ImageIndex =
_cloudImages.Images.Add(
System.Drawing.Image.FromFile(locImage),
Color.Black
);
// Make sure our browsing history reflects
// the existence of the downloaded image
sinf.Image = locFile;
_synths[i] = sinf;
}
}
}
}
}
this.Cursor = old;
}
}
private void cloudList_DrawItem(
object sender,
DrawListViewItemEventArgs e
)
{
// Restrict the bounds to no greater than the
// visible width
Rectangle bounds = e.Bounds;
if (bounds.Width > _cloudList.ClientSize.Width)
bounds.Width = _cloudList.ClientSize.Width;
if ((e.State &
(ListViewItemStates.Hot | ListViewItemStates.Selected)
) != 0)
{
// Draw the background for a selected or hovered item
if ((e.State & ListViewItemStates.Hot) != 0)
e.Item.Selected = true;
// Create a linear gradient brush going between
// "Photosynth green" and black, then draw the background
LinearGradientBrush brush =
new LinearGradientBrush(
bounds,
Color.FromArgb(255, 166, 203, 2),
Color.Black,
LinearGradientMode.Vertical
);
using (brush)
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
}
else
{
// Draw a black background for an unselected item
e.Graphics.FillRectangle(Brushes.Black, e.Bounds);
}
// Draw the item text for views other than "Details"
if (_cloudList.View != View.Details)
{
// Restrict the item bounds to no greater than the
// visible width
Rectangle itemSize = e.Item.Bounds;
if (itemSize.Width > _cloudList.ClientSize.Width)
itemSize.Width = _cloudList.ClientSize.Width;
// Reduce the bounds further to the image size we want
itemSize.Inflate(
(itemSize.Width - imgWidth) / -2,
(itemSize.Height - imgHeight) / -2
);
// Get the image from the list, if it's there, otherwise
// create a blank image
System.Drawing.Image img =
(_cloudImages.Images.Count > e.ItemIndex ?
_cloudImages.Images[e.ItemIndex] :
new System.Drawing.Bitmap(imgWidth, imgHeight)
);
// Draw the image and the text
e.Graphics.DrawImage(img, itemSize);
e.DrawText(
TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter
);
}
}
private void cloudList_MouseClick(
object sender,
MouseEventArgs e
)
{
// If an item in the list is clicked, then either import
// the associated point cloud directly or show the menu
// (if the right mouse button was used)
if (_cloudList.SelectedItems.Count == 1)
{
if (e.Button == MouseButtons.Left)
ImportPointCloud();
else if (e.Button == MouseButtons.Right)
_cloudsMenu.Show(MousePosition);
}
}
private void ContextMenuStrip_ItemClicked(
object sender,
ToolStripItemClickedEventArgs e
)
{
// If the right-click menu item's "import" item
// was used, import the selected point cloud
if (e.ClickedItem.Name == "_importMenuItem")
ImportPointCloud();
}
private void AddToPointCloudList(string title, string baseUrl)
{
// Add a point cloud with a certain title and URL to our list
ListViewItem lvi;
bool found = false;
// First we check that it's not already in the list
for (int idx = 0; idx < _cloudList.Items.Count; idx++)
{
if (_synths.Count > idx)
{
lvi = _cloudList.Items[idx];
if (lvi.Text == title && _synths[idx].Url == baseUrl)
{
found = true;
break;
}
}
}
// If it isn't add it to the list and to our browsing history
if (!found)
{
lvi = new ListViewItem(title);
lvi.IndentCount = 10;
_cloudList.Items.Add(lvi);
SynthInfo sinf = new SynthInfo();
sinf.Url = baseUrl;
sinf.Name = title;
_synths.Add(sinf);
}
}
private void ImportPointCloud()
{
// If we're not connected to an AutoCAD session (via
// the handle we received as a command-line argument),
// then we show a message and continue.
if (_hwnd == 0)
{
MessageBox.Show(
"This browser is not connected to an instance of " +
"AutoCAD. Relaunch from AutoCAD to import Point " +
"Clouds from your browsing history.",
"Browse Photosynth",
MessageBoxButtons.OK,
MessageBoxIcon.Information
);
}
else
{
// Get the selected items from the list
ListView.SelectedListViewItemCollection sel =
_cloudList.SelectedItems;
// Get the index of the first (and only) selected item
int idx = sel[0].Index;
// Assume the item in the _synths list is at the same
// location
SynthInfo sinf = _synths[idx];
string title = sinf.Name;
string firstUrl = sinf.Url + pointsName;
// Hide the form and stop the browsing operation
Visible = false;
_browser.NavToBlank();
_browser.Stop();
// Fire off our command to AutoCAD
SendCommandToAutoCAD(
"_.IMPORTPHOTOSYNTH \"" + firstUrl + "\" \"" +
title + "\" "
);
// Exit the application
Application.Exit();
}
}
// Clear the point cloud history from the browser and
// delete the XMl history file.
private void ClearHistory_Click(object sender, EventArgs e)
{
_synths.Clear();
_cloudImages.Images.Clear();
_cloudList.Items.Clear();
string histFile = GetOutputLocation() + historyXml;
if (File.Exists(histFile))
File.Delete(histFile);
}
// The location of our various output files (the PCGs, JPGs
// and the browsing history)
private string GetOutputLocation()
{
return
Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments
) + "\\Photosynth Point Clouds\\";
}
// Save the current state of our browsing history
// to an XML file.
private void SerializeHistory(List<SynthInfo> synths)
{
string outputPath = GetOutputLocation();
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
if (_synths.Count > 0)
{
XmlSerializer xs =
new XmlSerializer(typeof(List<SynthInfo>));
XmlTextWriter xw =
new XmlTextWriter(outputPath + historyXml, Encoding.UTF8);
xs.Serialize(xw, synths);
xw.Close();
}
}
// Read and return the previous browsing history from
// our stored XML file.
private List<SynthInfo> DeserializeHistory()
{
string histFile = GetOutputLocation() + historyXml;
if (File.Exists(histFile))
{
XmlSerializer xs =
new XmlSerializer(typeof(List<SynthInfo>));
XmlTextReader xr = new XmlTextReader(histFile);
if (xs.CanDeserialize(xr))
{
List<SynthInfo> synths =
(List<SynthInfo>)xs.Deserialize(xr);
xr.Close();
return synths;
}
}
return new List<SynthInfo>();
}
// Just use the Win32 API to communicate with AutoCAD.
// We simply need to send a command string, so this
// approach avoids a dependency on AutoCAD's COM
// interface.
private void SendCommandToAutoCAD(string toSend)
{
const int WM_COPYDATA = 0x4A;
COPYDATASTRUCT cds = new COPYDATASTRUCT();
cds.dwData = new IntPtr(1);
string data = toSend + "\0";
cds.cbData = data.Length * Marshal.SystemDefaultCharSize;
cds.lpData = Marshal.StringToCoTaskMemAuto(data);
SendMessageW(
new IntPtr(_hwnd), WM_COPYDATA, this.Handle, ref cds
);
Marshal.FreeCoTaskMem(cds.lpData);
}
// Function to create a valid filename from a string.
// This has been duplicated from the plugin project.
private static string MakeValidFileName(string name)
{
string invChars =
Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invRegEx = string.Format(@"[{0}]", invChars + ".");
return Regex.Replace(name, invRegEx, "-");
}
}
}
Here is the code we’ve added to the original plugin code to define our new BP command:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Reflection;
using System.IO;
using System.Net;
using System;
using DemandLoading;
namespace ImportPhotosynth
{
public class Appl : IExtensionApplication
{
public void Initialize()
{
try
{
RegistryUpdate.RegisterForDemandLoading();
}
catch
{ }
}
public void Terminate()
{
Commands.Cleanup();
}
}
public class Commands
{
static Process _p = null;
static public void Cleanup()
{
if (_p != null)
{
if (!_p.HasExited)
_p.Kill();
_p.Dispose();
_p = null;
}
}
[CommandMethod("BP", CommandFlags.Session)]
public void BrowsePhotosynth()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Cleanup();
string exePath =
Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location
) + "\\";
if (!File.Exists(exePath + "ADNPlugin-BrowsePhotosynth.exe"))
{
ed.WriteMessage(
"\nCould not find the ADNPlugin-BrowsePhotosynth " +
"tool: please make sure it is in the same folder " +
"as the application DLL."
);
return;
}
// Launch our browser window with the AutoCAD's handle
// so that we can receive back command strings
ProcessStartInfo psi =
new ProcessStartInfo(
exePath + "ADNPlugin-BrowsePhotosynth",
" " + Application.MainWindow.Handle
);
_p = Process.Start(psi);
}
[CommandMethod("IMPORTPHOTOSYNTH", CommandFlags.NoHistory)]
public void ImportPhotosynth()
{
…
The rest of the code in this source file is identical to that from the previous post.
A few comments on getting the application to work…
- Pre-built versions of the files you need can be found in the project’s bin folder. txt2las.exe is the original one provided on the lastools website, csExWB.dll is built exactly from the application source, and ComUtilities.dll is the pre-built module provided along with it.
- You will need to extract the various files into a single folder on your hard drive (I recommend a folder under your main AutoCAD 2011 program files folder – I would normally put them straight into the program files folder, but as some of them have generic names that aren’t prefixed by an RDS, this is a little risky).
- You will need to use regsvr32 to register the ComUtilities.dll file: you can either open a command-prompt window and browse to the folder, entering “regsvr32 ComUtilities.dll” or – and this is the approach I tend to use – create a desktop shortcut to the regsvr32.exe file in your Windows\System32 folder and drag & drop the ComUtilities.dll file from Windows Explorer onto that shortcut.
- I haven’t tried this out on a 64-bit system, but I believe it will work (if it doesn’t please let me know).
The best way to run the application is directly from inside AutoCAD: you NETLOAD the ADNPlugins-ImportPhotosynth.dll into AutoCAD 2011, which should create demand-loading entries for future, automatic loading. You can then use the BP command to launch the browser dialog. [It’s also possible to launch the executable application directly, but this will only allow you to populate the browsing history for later use inside an AutoCAD session – without being “connected” to AutoCAD, it won’t do anything more).
When the application loads, if there’s an embedded Photosynth in the main page, this should get added to the history (bear in mind that Photosynth now hosts panoramas, so not all items have a point cloud behind):
And as you browse (there’s not yet a “back” button on the form, so you will need to right-click on a non-Silverlight part of the page to get access to this via a context menu) you will see additional Photosynths get added to our history:
And then when you find a Photosynth that interests you, hovering over the item in the right-hand side of the dialog should select it:
And, if you’re inside AutoCAD, you can then left- or right-click it to import it:
If you’re interested in getting at the data generated by this application, open up the “Photosynth Point Clouds” folder under your “My Documents”. In here you’ll find JPGs and PCGs for the various point clouds, as well as an XML file containing your browsing history:
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfSynthInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SynthInfo Name="Point Wilson Lighthouse">
<Url>http://mslabs-354.vo.llnwd.net/d7/photosynth/M6/collections/3d/7d/fe/3d7dfec4-e184-4c96-831f-c4667ed88ed9.synth_files/</Url>
<Image>Point Wilson Lighthouse.jpg</Image>
</SynthInfo>
<SynthInfo Name="Девичья башня">
<Url>http://mslabs-361.vo.llnwd.net/d4/photosynth/m6/collections/89/19/b0/8919b044-95c5-4bce-8147-2149d1d5342e.synth_files/</Url>
<Image>Девичья башня.jpg</Image>
</SynthInfo>
<SynthInfo Name="Cincinnati Art Museum Exterior - West Wing">
<Url>http://mslabs-606.vo.llnwd.net/d7/photosynth/M6/collections/f7/5d/71/f75d71e8-7624-4ff8-8037-0106ee59eec4.synth_files/</Url>
<Image>Cincinnati Art Museum Exterior - West Wing.jpg</Image>
</SynthInfo>
<SynthInfo Name="Moskvich">
<Url>http://mslabs-866.vo.llnwd.net/d4/photosynth/m6/collections/93/20/cb/9320cb03-6d22-48d7-b697-b897d75d2435.synth_files/</Url>
<Image>Moskvich.jpg</Image>
</SynthInfo>
<SynthInfo Name="Mini Cooper 2009 rubensanchez">
<Url>http://mslabs-488.vo.llnwd.net/d3/photosynth/M6/collections/5b/93/87/5b93877f-e15a-4310-a9f4-11100e101e88.synth_files/</Url>
<Image>Mini Cooper 2009 rubensanchez.jpg</Image>
</SynthInfo>
</ArrayOfSynthInfo>
There are some really cool Photosynths out there – knock yourselves out! :-)
In a future post I’m going to show a little more on the “front-end”: to describe an attempt at capturing a 3D point cloud from a set of 2D images. If you’re interested in seeing a first try at this, search Photosynth for “kean notebook”: the first item in the list should be a relatively simple Photosynth of a notebook PC my manager (Jim Quanci) captured using the camera in his Blackberry while we were in the Tokyo office a couple of weeks ago. Nothing very exciting, just the early results of a very basic test.