Kean Walmsley


  • About the Author
    Kean on Google+

July 2014

Sun Mon Tue Wed Thu Fri Sat
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    








« The latest on Windows Azure | Main | Integrating Kinect with AutoCAD 2013 »

June 18, 2012

Indexing point clouds programmatically in AutoCAD 2013

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:

  1. Generate a set of points (whether from the Kinect sensor or the Photosynth web-service).
  2. Save them to a text file.
  3. Use the txt2las tool to generate a .LAS file from the text file.
  4. Call POINTCLOUDINDEX to index the .LAS into a .PCG.
  5. Wait for the results of POINTCLOUDINDEX (as it runs in the background).
  6. 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:

  1. Generate a set of points (whether from the Kinect sensor or the Photosynth web-service).
  2. Save them to a text file.
  3. Use the AdPointCloudIndexer tool to generate a .PCG from the text file.
  4. 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:

BrowsePhotosynth in action

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):

Our Swiss synth in AutoCAD 2013

In the next post, we’ll apply the same technique to the Kinect integration for AutoCAD.

blog comments powered by Disqus

10 Random Posts