Thanks to RS for raising this issue via a blog comment and to my esteemed colleague, Christer Janson, for suggesting the solution during England’s epic Euro 2012 victory over Sweden on Friday evening (sorry, Christer – I couldn’t help but rub it in just a little ;-). On a slightly more serious note, I find it very painful to watch England play, at the best of times, and while I was happy “we” won, I was sad to see Sweden left with no chance of continuing past the group stage. So it goes.
Those of you who have played around either with the Kinect point cloud import or the BrowsePhotosynth Plugin of the Month will probably know that the approach I use in both projects to bring point clouds into AutoCAD is fairly rudimentary, in that it uses standard AutoCAD commands rather than calling in to lower-level APIs.
You can see more detail of the process in this AU handout (scroll down for the process diagram), but basically we follow this process:
- Generate a set of points (whether from the Kinect sensor or the Photosynth web-service).
- Save them to a text file.
- Use the txt2las tool to generate a .LAS file from the text file.
- Call POINTCLOUDINDEX to index the .LAS into a .PCG.
- Wait for the results of POINTCLOUDINDEX (as it runs in the background).
- Call POINTCLOUDATTACH to bring the .PCG into AutoCAD as a point cloud entity.
The good news with AutoCAD 2013 is its direct support for .TXT and .XYZ files – which means we can remove step 3. The bad news – as RS noticed – is that AutoCAD 2013’s POINTCLOUDINDEX command can’t easily be called programmatically: it respects neither the fact it’s being called from a script (which I typically force by making sure the string I send to the command-line contains an AutoLISP (command) call rather than just the direct command) nor the value of FILEDIA. It always displays a dialog box prompting the user to select the input file.
There is a silver lining to this particular cloud, though: as Christer pointed out, the POINTCLOUDINDEX command’s implementation in AutoCAD 2013 calls through to a standalone indexing executable: AdPointCloudIndexer.exe. This tool resides in AutoCAD’s program folder and uses the codecs contained in the IndexCodecs sub-folder.
Here’s the usage information you get when calling AdPointCloudIndexer inside a command-prompt with no arguments:
c:\Program Files\Autodesk\AutoCAD 2013>AdPointCloudIndexer
Autodesk point cloud indexer tool - import raw scan files to pcg or
isd files
g GUID of Semaphore object for communication between
AutoCAD and AdPointCloudIndexer.exe
p Path of the codec plugins
Default path is "$(AdPointCloudIndexer.exe)\IndexCodecs"
o Output indexed file
i Input raw scan files
Allow multiple input files for merging into one PCG or
ISD file
RGB Index with RGB attribute
Intensity Index with intensity attribute
Normal Index with normal attribute
Custom Index with custom attribute
There’s actually a nice side benefit of calling the executable directly in step 4 rather than the POINTCLOUDINDEX command: we can now do away with step 5 (which added some complexity to the implementation).
Here’s the new, altogether much simpler process for AutoCAD 2013:
- Generate a set of points (whether from the Kinect sensor or the Photosynth web-service).
- Save them to a text file.
- Use the AdPointCloudIndexer tool to generate a .PCG from the text file.
- Call POINTCLOUDATTACH to bring the .PCG into AutoCAD as a point cloud entity.
To implement step 3, we need to fire off a process running a command such as this one:
AdPointCloudIndexer -i "input.xyz" -o "output.pcg" -RGB
It’s worth noting that to get the RGB data to be recognised and added to the .PCG, I had to save the points to a file of type .XYZ rather than just a plain old .TXT.
Let’s see what this means to the BrowsePhotosynth code (which Viru Aithal is currently reworking to update the Plugin on the Month on Autodesk Labs, so this will hopefully save him some effort :-). The only file I ended up having to update was import-photosynth.cs:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Threading;
using System.Windows.Threading;
using System.Text.RegularExpressions;
using System.Reflection;
using System.IO;
using System.Diagnostics;
using System;
namespace PhotosynthImporter
{
public class Commands
{
const string exeName = "ADNPlugin-PhotosynthBrowser";
readonly string[] syncModes = new string[]
{ "C# synchronous", "F# synchronous", "F# asynchronous" };
static Process _p = null;
Logging.EventLog _el = null;
static public void Cleanup()
{
if (_p != null)
{
if (!_p.HasExited)
_p.Kill();
_p.Dispose();
_p = null;
}
}
static public void CleanupOnStartup()
{
bool first = true;
foreach (Process proc in Process.GetProcesses())
{
if (proc.ProcessName.Contains(exeName))
{
if (first)
{
if (System.Windows.Forms.MessageBox.Show(
"Instances of browser executable found running. " +
"Would you like them closed?",
"Import Photosynth",
System.Windows.Forms.MessageBoxButtons.YesNo
) != System.Windows.Forms.DialogResult.Yes)
{
break;
}
first = false;
}
proc.Kill();
}
}
}
[CommandMethod("ADNPLUGINS", "EXITPS", CommandFlags.NoHistory)]
public void CleanupBrowser()
{
Cleanup();
}
[CommandMethod("ADNPLUGINS", "REMOVEPS", CommandFlags.Modal)]
static public void RemoveBrowsePhotosynth()
{
DemandLoading.RegistryUpdate.UnregisterForDemandLoading();
Editor ed =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nThe BrowsePhotosynth plugin will not be loaded" +
" automatically in future editing sessions.");
}
[CommandMethod("ADNPLUGINS", "BROWSEPS", CommandFlags.Session)]
static 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 + exeName + ".exe"))
{
ed.WriteMessage(
"\nCould not find the {0} tool: please make sure " +
"it is in the same folder as the application DLL.",
exeName
);
return;
}
// Launch our browser window with the AutoCAD's handle
// so that we can receive back command strings
ProcessStartInfo psi =
new ProcessStartInfo(
exePath + exeName,
" " + Application.MainWindow.Handle
);
_p = Process.Start(psi);
}
[CommandMethod("ADNPLUGINS", "IMPORTPS", CommandFlags.NoHistory)]
public void ImportPhotosynth()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptResult pr =
ed.GetString(
"Enter Photosynth collection ID: "
);
if (pr.Status != PromptStatus.OK)
return;
string colId = pr.StringResult;
pr =
ed.GetString(
"Enter URL of first Photosynth point cloud: "
);
if (pr.Status != PromptStatus.OK)
return;
string path = pr.StringResult;
pr =
ed.GetString(
"Enter name of Photosynth point cloud: "
);
if (pr.Status != PromptStatus.OK)
return;
string name = pr.StringResult;
// The root path has "points_0_0.bin" on the end.
// Strip off the last 5 characters ("0_0.bin"), so
// that we can compose the sequence of URLs needed
// for each of the point cloud files (usually
// going up to about "points_0_23.bin")
if (path.Length > 5)
path = path.Substring(0, path.Length - 7);
// We'll store most local files in the temp folder.
// We get a temp filename, delete the file and
// use the name for our folder
string localPath = Path.GetTempFileName();
File.Delete(localPath);
Directory.CreateDirectory(localPath);
localPath += "\\";
// Paths for our temporary files
string txtPath = localPath + "points.xyz";
// Our PCG file will be stored under My Documents
string outputPath =
Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments
) + "\\Photosynth Point Clouds\\";
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
_el = new Logging.EventLog(outputPath + "log.txt");
// We'll use the title as a base filename for the PCG,
// but will use an incremented integer to get an unused
// filename
int cnt = 0;
string pcgPath;
do
{
pcgPath =
outputPath + MakeValidFileName(name) +
(cnt == 0 ? "" : cnt.ToString()) + ".pcg";
cnt++;
}
while (File.Exists(pcgPath));
// The path for the AdPointCloudIndexer tool is the same as
// for AutoCAD
string exePath =
Path.GetDirectoryName(
System.Windows.Forms.Application.ExecutablePath
) + "\\";
if (!File.Exists(exePath + "AdPointCloudIndexer.exe"))
{
ed.WriteMessage(
"\nCould not find the AdPointCloudIndexer tool."
);
return;
}
// We now access the Photosynth web service to get the size of
// the cloud(s) we want to download and process
ed.WriteMessage(
"\nAccessing Photosynth web service to get information on " +
"\"{0}\" point cloud(s)...\n", name
);
int totalClouds = 0, totalFiles = 0;
long totalPts = 0;
int[] dims = null;
try
{
DataFromService.PointAndFileCount(
ed, colId, ref totalClouds, ref totalFiles,
ref totalPts, ref dims
);
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nUnable to get data on this Photosynth: ",
ex.Message
);
return;
}
// Report back what we've found, thus far
ed.WriteMessage(
"\n{0} point cloud{1} found. " +
"{2} point cloud (containing {3} file{4}) " +
"will now be imported.\n",
totalClouds, totalClouds == 1 ? "" : "s",
totalClouds > 1 ? "The first" : "This",
totalFiles, totalFiles == 1 ? "" : "s"
);
// Start the progress meter for our processing
// operation
ProgressMeter pm = new ProgressMeter();
using (pm)
{
pm.SetLimit(totalFiles);
pm.Start("Downloading/processing Photosynth points");
short sync, log;
bool foundVars = true;
try
{
sync =
(short)Application.GetSystemVariable("BROWSEPSSYNC");
log =
(short)Application.GetSystemVariable("BROWSEPSLOG");
}
catch
{
sync = 2;
log = 0;
foundVars = false;
}
if (foundVars)
ed.WriteMessage(
"\nDownloading/processing via {0} (BROWSEPSSYNC = {1}).",
syncModes[sync], sync
);
// Capture the start time
DateTime start = DateTime.Now;
try
{
switch (sync)
{
case 2:
{
// Fully asynchronous using F#
// We will need this to coordinate UI update events
// back with this thread
SynchronizationContext sc =
SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext()
);
bool cancelled = false;
PhotosynthProcAsyncFs.PointCloudProcessor pcp =
new PhotosynthProcAsyncFs.PointCloudProcessor();
// When each file is processed, write a message
// to the command-line and update the progress
pcp.JobCompleted +=
(s, a) =>
{
ed.WriteMessage(
"\nProcessed {0} containing {1} points.",
a.Item1, a.Item2
);
pm.MeterProgress();
};
// Process our point cloud(s)
pcp.ProcessPointCloud(path, dims, txtPath);
// The above function launches a set of asynchronous
// tasks and returns. We need to loop while
// processing UI events until the tasks are complete
while (!pcp.IsComplete)
{
System.Windows.Forms.Application.DoEvents();
// If the user has cancelled...
cancelled = CheckEscape(ed);
if (cancelled)
{
pcp.Cancel();
break;
}
}
if (pcp.IsComplete && !cancelled)
{
// Now we can find out the results
totalPts = pcp.TotalPoints;
if (pcp.Failures > 0)
ed.WriteMessage(
"\nFailed on {0} files.", pcp.Failures
);
}
else
totalPts = 0;
// Set the sychronization context back
SynchronizationContext.SetSynchronizationContext(sc);
break;
}
case 1:
{
// Synchronous using F#
PhotosynthProcSyncFs.PointCloudProcessor pcp =
new PhotosynthProcSyncFs.PointCloudProcessor();
pcp.JobCompleted +=
(s, a) =>
{
ed.WriteMessage(
"\nProcessed {0} containing {1} points.",
a.Item1, a.Item2
);
pm.MeterProgress();
};
pcp.CheckForCancel +=
(s, a) =>
{
a.Value = CheckEscape(ed);
};
// Process our point cloud
totalPts =
pcp.ProcessPointCloud(path, dims, txtPath);
break;
}
default:
{
// Synchronous using C#
PhotosynthProcSyncCs.PointCloudProcessor pcp =
new PhotosynthProcSyncCs.PointCloudProcessor(
ed, pm, localPath
);
totalPts =
pcp.ProcessPointCloud(path, dims, txtPath);
break;
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nError processing point cloud: {0}.",
ex.Message
);
}
if (totalPts > 0)
{
// Calculate/report the elapsed time
TimeSpan elapsed = DateTime.Now - start;
ed.WriteMessage(
"\nImported {0} points from {1} file{2} in {3}.\n",
totalPts, totalFiles, totalFiles == 1 ? "" : "s",
elapsed
);
if (log > 0)
{
_el.Log(
String.Format(
"\n{0} using {1}: " +
"{2} points from {3} file{4} in {5}\n",
name, syncModes[sync],
totalPts, totalFiles,
totalFiles == 1 ? "" : "s", elapsed
)
);
}
}
// Stop the progress meter
pm.Stop();
}
if (totalPts <= 0)
{
return;
}
// Use the AdPointCloudIndexer tool to create a .PCG from
// our .XYZ file
ed.WriteMessage(
"\nIndexing the downloaded points.\n"
);
ProcessStartInfo psi =
new ProcessStartInfo(
exePath + "AdPointCloudIndexer",
"-i \"" + txtPath + "\" " +
"-o \"" + pcgPath + "\" -RGB"
);
psi.CreateNoWindow = false;
psi.WindowStyle = ProcessWindowStyle.Hidden;
// Wait up to 20 seconds for the process to exit
try
{
using (Process p = Process.Start(psi))
{
p.WaitForExit();
}
}
catch
{ }
// If there's a problem, we return
if (!File.Exists(pcgPath))
{
ed.WriteMessage(
"\nError indexing points."
);
return;
}
CleanupTmpFiles(txtPath);
string pcgLisp = pcgPath.Replace('\\', '/');
// Attach the .PCG file
doc.SendStringToExecute(
"_.-VISUALSTYLES _C _Conceptual " +
"_.UCSICON _OF " +
"(command \"_.-POINTCLOUDATTACH\" \"" +
pcgLisp + "\" \"0,0\" \"1\" \"0\")(princ) ",
false, false, false
);
Cleanup();
}
static internal bool CheckEscape(Editor ed)
{
System.Windows.Forms.Application.DoEvents();
if (HostApplicationServices.Current.UserBreak())
{
ed.WriteMessage(
"\nOperation canceled."
);
return true;
}
return false;
}
internal static void CleanupTmpFiles(string txtPath)
{
if (File.Exists(txtPath))
File.Delete(txtPath);
Directory.Delete(
Path.GetDirectoryName(txtPath)
);
}
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, "-");
}
}
}
Aside from the use of AdPointCloudIndexer, I also made some other miscellaneous changes to the code:
- I cleaned up the use of anonymous delegates for event handlers, switching them out for lambdas, which are much more elegant especially when dealing with F# interop).
- I added comma to the list of invalid characters in a path: for some reason POINTCLOUDATTACH doesn’t seem to like .PCG files with commas in their name.
- I adjusted the call to the POINTCLOUDATTACH command, to set the visual style and turn the UCS icon off before the command call (as it now zooms to the point cloud object automatically when the attach is completed).
Here’s the BROWSEPS dialog displaying a newish (and coincidentally Swiss) Photosynth I found made from 919 photos:
This results in a cloud of 703K points being brought into AutoCAD (which is still not as big as this 1.1m point synth, but is nonetheless pretty amazing):
In the next post, we’ll apply the same technique to the Kinect integration for AutoCAD.