November 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            










« "AutoCAD 2011 New APIs" webcast recording | Main | May’s Plugin of the Month: Spiro for AutoCAD »

April 30, 2010

Importing Photosynth point clouds into AutoCAD 2011 – Part 4

In the previous posts in this series we introduced a command that downloaded and imported point clouds from Photosynth.net, we introduced a WinForms user interface on top of it and then replaced that UI with one implemented using WPF.

As threatened last time, we’re now going to make some efficiency improvements in the original command implementation.

In our previous implementation we were blindly asking for files, one after the other, and using failure to indicate when we’d reached the end. Which was fine, but it limited us in a few ways: we could not reliably parallelize this otherwise highly parallelizable operation, and we couldn’t report accurate progress back to the user (as we didn’t know when it was all going to end).

Thanks to a note from Nate Lawrence and another look at Christoph Hausner’s Photosynth Point Cloud Exporter project, I was able to work out how to get this information from the Photosynth web service. The beauty of getting this information is that we now know exactly what files we need to download and can fire them off as asynchronous tasks.

The best way I know of managing this kind of activity is by integrating F# into your project, and this is – in my opinion – one of the absolutely compelling benefits of the F# language: it’s just so easy to capture the logic of “Asynchronous Workflows”, such as this, and to leave the F# subsystem to execute them as efficiently as it can. And, as we’ll see, for a task where we’re downloading and processing multiple files there are huge performance benefits versus performing this sequentially.

Before we look at the code, a few notes on connecting to the Photosynth web service. F# projects – at least with the April 2010 CTP I’m using with VS 2008 – do not have IDE support for adding web service references, so I decided to keep this “discovery” activity in C#.

Adding service references to a C# project is easy – we right-click inside the Solution Explorer and select “Add Service Reference…”, copying/pasting the web service URL (http://photosynth.net/photosynthws/PhotosynthService.asmx) into the Address bar:

Add a reference to the Photosynth web service

After giving it a name, we select OK to see some details:

Our Photosynth web service

Here we see the GetCollectionData() method, which is the one we’re going to use in this application.

The latest source project is available here. I’ve included the two files which have either been introduced (F#) or heavily updated (C#) below, but there have been a few other miscellaneous changes to other files in the project.

Let’s start by looking at the C# code in our updated import-photosynth.cs file:

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.Windows.Threading;

using System.Threading;

using System.Text.RegularExpressions;

using System.ServiceModel;

using System.Reflection;

using System.Net;

using System.IO;

using System.Diagnostics;

using System;

using DemandLoading;

using ImportPhotosynth.PhotosynthService;

using Newtonsoft.Json;

using Newtonsoft.Json.Linq;

 

namespace ImportPhotosynth

{

  public class Appl : IExtensionApplication

  {

    public void Initialize()

    {

      try

      {

        RegistryUpdate.RegisterForDemandLoading();

        Commands.CleanupOnStartup();

      }

      catch

      { }

    }

 

    public void Terminate()

    {

      Commands.Cleanup();

    }

  }

 

  public class Commands

  {

    const string exeName = "ADNPlugin-BrowsePhotosynth2";

 

    static Process _p = 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("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 + 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("IMPORTPHOTOSYNTH", CommandFlags.NoHistory)]

    public void ImportPhotosynth()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      HostApplicationServices ha =

        HostApplicationServices.Current;

 

      PromptResult 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.txt";

      string lasPath = localPath + "points.las";

 

      // 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);

 

      string colId = ExtractCollectionId(path);

 

      // 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 to the txt2las tool will be the same as the

      // executing assembly (our DLL)

 

      string exePath =

        Path.GetDirectoryName(

          Assembly.GetExecutingAssembly().Location

        ) + "\\";

 

      if (!File.Exists(exePath + "txt2las.exe"))

      {

        ed.WriteMessage(

          "\nCould not find the txt2las tool: please make sure it " +

          "is in the same folder as the application DLL."

        );

        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

      );

 

      // We're interested in two URLs

 

      string dzcUrl, jsonUrl;

 

      // Perform manual binding, to avoid having to add binding info

      // into acad.exe.config

 

      BasicHttpBinding binding = new BasicHttpBinding();

      EndpointAddress address =

        new EndpointAddress(

          "http://photosynth.net/photosynthws/PhotosynthService.asmx"

        );

 

      // Create our SOAP client

 

      PhotosynthServiceSoapClient soapClient =

        new PhotosynthServiceSoapClient(binding, address);

      using (soapClient)

      {

        try

        {

          // Get the data associated with our Photosynth cloud(s)

 

          CollectionResult colRes =

            soapClient.GetCollectionData(new Guid(colId), false);

          dzcUrl = colRes.DzcUrl;

          jsonUrl = colRes.JsonUrl;

        }

        catch (FormatException fex)

        {

          ed.WriteMessage("\nInvalid URL: {0}", fex.Message);

          return;

        }

        catch (EndpointNotFoundException ex)

        {

          ed.WriteMessage(

            "\nCould not connect to Photosynth web service: {0}",

            ex.Message

          );

          return;

        }

      }

 

      if (jsonUrl == null || dzcUrl == null)

      {

        ed.WriteMessage(

          "\nUnable to find information about this point cloud " +

          "via the Photosynth web service."

        );

        return;

      }

      string jsonData;

 

      // All being well we should now be able to download and process

      // the data about our cloud(s)

 

      using (WebClient webClient = new WebClient())

        jsonData = webClient.DownloadString(jsonUrl);

 

      // Extract our point cloud dimension information, as per:

      //  http://pspcexporter.codeplex.com

 

      JObject jObject = JObject.Parse(jsonData);

      JToken cols = jObject["l"] ?? jObject["collections"];

      JToken col = cols[colId] ?? cols[string.Empty];

      JToken numCoordSystems = col["_num_coord_systems"];

      JToken coordSystems = col["x"] ?? col["coord_systems"];

 

      // Create the array of integers representing these

      // dimensions

 

      int totalClouds = (int)numCoordSystems;

      int[] dims = new int[totalClouds];

 

      // Variables to count the number of files and points

 

      int totalFiles = 0;

      long totalPoints = 0;

 

      // Populate our dimensions list and count the files

 

      for (int i = 0; i < totalClouds; i++)

      {

        JToken cs = coordSystems[Convert.ToString(i)];

        JToken pc = cs["k"] ?? cs["pointcloud"];

        if (pc != null)

        {

          string s = (string)pc[0];

 

          if (!string.IsNullOrEmpty(s))

          {

            JToken binFileCount = pc[1];

            int fileCount = (int)binFileCount;

            dims[i] = fileCount;

            totalFiles += fileCount;

          }

        }

      }

 

      // Report back what we've found, thus far

 

      ed.WriteMessage(

        "\n{0} point cloud{1} found across {2} file{3}.\n",

        totalClouds, totalClouds == 1 ? "" : "s",

        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");

 

        try

        {

          // If the current SynchronizationContext is null

          // (which appears to be the case when not called

          // from the debugger) then create one and set it

 

          // We will need this to coordinate UI update events

          // back with this thread

 

          if (SynchronizationContext.Current == null)

          {

            DispatcherSynchronizationContext context =

              new DispatcherSynchronizationContext(

                Dispatcher.CurrentDispatcher

              );

            SynchronizationContext.SetSynchronizationContext(

              context

            );

          }

 

          // Create our processor object

 

          ProcessPhotosynth.PointCloudProcessor pcp =

            new ProcessPhotosynth.PointCloudProcessor();

 

          // Capture the start time

 

          DateTime start = DateTime.Now;

 

          // When each file is processed, write a message

          // to the command-line and update the progress

 

          pcp.JobCompleted +=

            delegate(object sender, Tuple<string, int> args)

            {

              ed.WriteMessage(

                "\nProcessed {0} containing {1} points.",

                args.Item1, args.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();

          }

 

          // Now we can find out the results

 

          totalPoints = pcp.TotalPoints;

 

          // And calculate/report the elapsed time

 

          TimeSpan elapsed = DateTime.Now - start;

 

          ed.WriteMessage(

            "\nImported {0} points from {1} file{2} in {3}.\n",

            totalPoints, totalFiles, totalFiles == 1 ? "" : "s",

            elapsed

          );

 

        }

        catch (System.Exception ex)

        {

          ed.WriteMessage(

            "\nException occurred: {0}", ex.Message

          );

        }

 

        // Stop the progress meter

 

        pm.Stop();

      }

 

      if (totalPoints > 0)

      {

        // Use the txt2las utility to create a .LAS

        // file from our text file

 

        ProcessStartInfo psi =

          new ProcessStartInfo(

            exePath + "txt2las",

            "-i \"" + txtPath +

            "\" -o \"" + lasPath +

            "\" -parse xyzRGB"

          );

        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(20000);

          }

        }

        catch

        { }

 

        // If there's a problem, we return

 

        if (!File.Exists(lasPath))

        {

          ed.WriteMessage(

            "\nError creating LAS file."

          );

          return;

        }

        File.Delete(txtPath);

 

        ed.WriteMessage(

          "Indexing the LAS and attaching the PCG.\n"

        );

 

        // Index the .LAS file, creating a .PCG

 

        string lasLisp = lasPath.Replace('\\', '/'),

              pcgLisp = pcgPath.Replace('\\', '/');

 

        doc.SendStringToExecute(

          "(command \"_.POINTCLOUDINDEX\" \"" +

          lasLisp + "\" \"" +

          pcgLisp + "\")(princ) ",

          false, false, false

        );

 

        // Attach the .PCG file

 

        doc.SendStringToExecute(

          "_.WAITFORFILE \"" +

          pcgLisp + "\" \"" +

          lasLisp + "\" " +

          "(command \"_.-POINTCLOUDATTACH\" \"" +

          pcgLisp +

          "\" \"0,0\" \"1\" \"0\")(princ) ",

          false, false, false

        );

 

        doc.SendStringToExecute(

          "_.-VISUALSTYLES _C _Conceptual _.ZOOM _E ",

          false, false, false

        );

      }

    }

 

    private string ExtractCollectionId(string path)

    {

      const string synthTag = ".synth_files";

 

      string colId = "";

 

      if (path.Contains(synthTag))

      {

        string start =

          path.Substring(0, path.IndexOf(synthTag));

        if (start.Length > 36)

          colId = start.Substring(start.Length - 36);

      }

      return colId;

    }

 

    // A command which waits for a particular PCG file to exist

 

    [CommandMethod("WAITFORFILE", CommandFlags.NoHistory)]

    public void WaitForFileToExist()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      HostApplicationServices ha =

        HostApplicationServices.Current;

 

      PromptResult pr = ed.GetString("Enter path to PCG: ");

      if (pr.Status != PromptStatus.OK)

        return;

      string pcgPath = pr.StringResult.Replace('/', '\\');

 

      pr = ed.GetString("Enter path to LAS: ");

      if (pr.Status != PromptStatus.OK)

        return;

      string lasPath = pr.StringResult.Replace('/', '\\');

 

      // Check the write time for the PCG file...

      // if it hasn't been written to for at least four seconds,

      // we can continue

 

      const int numSecs = 4;

      TimeSpan span = new TimeSpan(0,0,numSecs);

 

      TimeSpan diff;

      while (true)

      {

        if (File.Exists(pcgPath))

        {

          DateTime dt = File.GetLastWriteTime(pcgPath);

          diff = DateTime.Now - dt;

          if (diff.Ticks > span.Ticks)

            break;

        }

        System.Windows.Forms.Application.DoEvents();

      }

 

      try

      {

        CleanupTmpFiles(lasPath);

      }

      catch

      { }

    }

 

    private 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, "-");

    }

  }

}

A few comments on the code:

  • When running this code a lot in the debugger, I found we were getting lots of hidden instances of our browser executable (as we’re killing it rather than allowing it to exit naturally, due to a bug in the csExWb2 component), so I decided to provide some automatic clean-up functionality on startup of the application
  • We’ve removed a lot of code dealing with the downloading/processing of point data – this is all now taken care of by the F#-implemented PointCloudProcessor object
  • This C# code now depends on an additional library called Json.NET (as did the exporter from which I borrowed the code) to parse JSON-formatted information we download from the Photosynth web service
  • We have a situation where we want to perform asynchronous operations on arbitrary threads, but these operations need to report back to the UI thread in order for us to write text to the command-line and to update AutoCAD’s progress meter. For this we have to make sure we have a valid SynchronizationContext set-up, which will be used from our F# code

Here’s the F# code which takes care of defining and running these asynchronous operations:

module ProcessPhotosynth

 

open System

open System.IO

open System.Net

open System.Text

open System.Threading

 

// We need the SynchronizationContext of the UI thread,

// to allow us to make sure our UI update events get

// processed correctly in the calling application

 

let mutable syncContext : SynchronizationContext = null

 

// Asynchronous Worker courtesy of Don Syme:

//  http://blogs.msdn.com/dsyme/archive/2010/01/10/async-and-

//  parallel-design-patterns-in-f-reporting-progress-with-

//  events-plus-twitter-sample.aspx

 

type Agent<'T> = MailboxProcessor<'T>

 

type SynchronizationContext with

 

  // A standard helper extension method to raise an event on

  // the GUI thread

 

  member syncContext.RaiseEvent (event: Event<_>) args =

    syncContext.Post((fun _ -> event.Trigger args),state=null)

 

type AsyncWorker<'T>(jobs: seq<Async<'T>>) =

 

  // Each of these lines declares an F# event that we can raise

 

  let allCompleted  = new Event<'T[]>()

  let error        = new Event<System.Exception>()

  let canceled      = new Event<System.OperationCanceledException>()

  let jobCompleted  = new Event<int * 'T>()

 

  let cancellationCapability = new CancellationTokenSource()

 

  // Start an instance of the work

 

  member x.Start() =                                                     

 

    // Capture the synchronization context to allow us to raise

    // events back on the GUI thread

 

    if syncContext = null then

      syncContext <- SynchronizationContext.Current

 

    if syncContext = null then

      raise(

        System.NullReferenceException(

          "Synchronization context is null."))     

 

    // Mark up the jobs with numbers

 

    let jobs = jobs |> Seq.mapi (fun i job -> (job,i+1))

 

    let work =

      Async.Parallel

      [ for (job,jobNumber) in jobs ->

          async { let! result = job

                  syncContext.RaiseEvent

                    jobCompleted (jobNumber,result)

                  return result } ]

 

    Async.StartWithContinuations(

      work,

      (fun res -> syncContext.RaiseEvent allCompleted res),

      (fun exn -> syncContext.RaiseEvent error exn),

      (fun exn -> syncContext.RaiseEvent canceled exn ),

      cancellationCapability.Token)

 

  member x.CancelAsync() =

    cancellationCapability.Cancel()

 

  // Raised when a particular job completes

 

  member x.JobCompleted = jobCompleted.Publish

 

  // Raised when all jobs complete

 

  member x.AllCompleted = allCompleted.Publish

 

  // Raised when the composition is cancelled successfully

 

  member x.Canceled = canceled.Publish

 

  // Raised when the composition exhibits an error

 

  member x.Error = error.Publish

 

type PointCloudProcessor() =

 

  // Mutable state to track progress and results

 

  let mutable jobsComplete = 0

  let mutable totalJobs = 0

  let mutable totalPoints = 0

 

  // Event to allow caller to update the UI

 

  let jobCompleted  = new Event<string * int>()

 

  // Function to access a stream asynchronously

 

  let httpAsync(url:string) =

 

    async {

      let req = WebRequest.Create(url)

      let! rsp = req.AsyncGetResponse()

      return rsp.GetResponseStream()

    }

 

  // Functions to read data from our point stream

 

  let rec readCompressedInt (i:int) (br:BinaryReader) =

    let b = br.ReadByte()

    let i = (i <<< 7) ||| ((int)b &&& 127)

    if (int)b < 128 then

      readCompressedInt i br

    else

      i

 

  let readBigEndianFloat (br:BinaryReader) =

    let b = br.ReadBytes(4)

    BitConverter.ToSingle( [| b.[3]; b.[2]; b.[1]; b.[0] |], 0)

 

  let readBigEndianShort (br:BinaryReader) =

    let b1 = br.ReadByte()

    let b2 = br.ReadByte()

    ((uint16)b2 ||| ((uint16)b1 <<< 8))

 

  // Recursive function to read n points from our stream

  // (We use an accumulator variable to enable tail-call

  // optimization)

 

  let rec readPoints acc n br =

 

    if n <= 0 then

      acc

    else

 

      // Read our coordinates

 

      let x = readBigEndianFloat br

      let y = readBigEndianFloat br

      let z = readBigEndianFloat br

 

      // Read and extract our RGB values

 

      let rgb = readBigEndianShort br

 

      let r = (rgb >>> 11) * 255us / 31us

      let g = ((rgb >>> 5) &&& 63us) * 255us / 63us

      let b = (rgb &&& 31us) * 255us / 31us

 

      readPoints ((x,y,z,r,g,b) :: acc) (n-1) br

 

  // Function to extract the various point information

  // from a stream cooresponding to a single poinr file

 

  let extractPoints br =

 

    // First information is the file version

    // (for now we support version 1.0 only)

 

    let majVer = readBigEndianShort br

    let minVer = readBigEndianShort br

 

    if (int)majVer <> 1 || (int)minVer <> 0 then

      []

    else

 

      // Clear some header bytes we don't care about

 

      let n = readCompressedInt 0 br

      for i in 0..(int)n-1 do

        let m = readCompressedInt 0 br

        for j in 0..(int)m-1 do

          readCompressedInt 0 br |> ignore

          readCompressedInt 0 br |> ignore

 

      // Find out the number of points in the file

 

      let npts = readCompressedInt 0 br

 

      // Read and return the points

 

      readPoints [] npts br

 

  // Recursive function to create a string from a list

  // of points. Our accumulator is a StringBuilder,

  // which is the most efficient way to collate a

  // string

 

  let rec pointsToString (acc : StringBuilder) pts =

    match pts with

    | [] -> acc.ToString()

    | (x,y,z,r,g,b) :: t ->       

      acc.AppendFormat("{0},{1},{2},{3},{4},{5}\n", x, y, z, r, g, b)

        |> ignore

      pointsToString acc t

 

  // Recursive function to write a list of points to file

 

  let rec addPointsToFile (sw : StreamWriter) pts =

    match pts with

    | [] -> ()

    | (x,y,z,r,g,b) :: t ->

      sw.WriteLine("{0},{1},{2},{3},{4},{5}", x, y, z, r, g, b)

      addPointsToFile sw t

 

  // Expose an event that's subscribable from C#/VB

 

  [<CLIEvent>]

  member x.JobCompleted = jobCompleted.Publish

 

  // Property to indicate that we're done

 

  member x.IsComplete = (jobsComplete = totalJobs)

 

  // Property to return the results

 

  member x.TotalPoints = totalPoints

 

  // Our main function to download and process the point

  // cloud(s) associated with a particular Photosynth

 

  member x.ProcessPointCloud baseUrl dims txtPath =

 

    // A local function to add the URL prefix to each file

 

    let getLocalFilename file = baseUrl + file

 

    // Generate our list of files from the list of dimensions

    // of the various point clouds

 

    // Each entry in dims corresponds to the number of files:

    //  dims[0] = 5 means "points_0_0.bin" .. "points_0_4.bin"

    //  dims[6] = 3 means "points_6_0.bin" .. "points_6_2.bin"

 

    let files =

      Array.mapi

        (fun i d ->

          Array.map (fun j -> sprintf "%d_%d.bin" i j) [| 0..d-1 |]

        )

        dims

        |> Array.concat

        |> List.ofArray

 

    // Set/reset mutable state

 

    totalJobs <- files.Length

    jobsComplete <- 0

 

    // Open the local, temporary text file to hold our points

 

    let t = new FileInfo(txtPath)

    let sw = t.Create()

 

    // An agent to store our points in the file...

    // Loops and receives messages, so that we ensure we don't

    // have a conflict of simultaneous writes

 

    let fileAgent =

      Agent.Start(fun inbox ->

        async { while true do

                  let! (msg : string) = inbox.Receive()

                  do! sw.AsyncWrite(Encoding.ASCII.GetBytes(msg)) })

 

    // Our basic asynchronous task to process a file, returning

    // the number of points

 

    let processFile (file:string) =

      async {

        let! stream = httpAsync file

        use reader = new BinaryReader(stream)

        let pts = extractPoints reader

        pointsToString (new StringBuilder()) pts |> fileAgent.Post

        return file, pts.Length

      }

 

    // Our jobs are a set of tasks, one for each file

 

    let jobs =

      [for file in files ->

        getLocalFilename file

          |> processFile

      ]

 

    // Create our AsyncWorker for our jobs

 

    let worker = new AsyncWorker<_>(jobs)

 

    // Raise an event when each file is processed and update

    // our internal state

 

    worker.JobCompleted.Add(fun (jobNumber, (url , ptnum)) ->

      let file = url.Substring(url.LastIndexOf('/')+1)

      jobsComplete <- jobsComplete + 1

      syncContext.RaiseEvent jobCompleted (file, ptnum)

 

      // If the last job, close our temporary file

 

      if x.IsComplete then

        sw.Close()

        sw.Dispose()

    )

 

    // Once we're all done, set the results as state to be

    // accessed by our calling routine

 

    worker.AllCompleted.Add(fun results ->

        totalPoints <- Array.sumBy snd results )

 

    // Now start the work

 

    worker.Start()

Some comments on this code:

  • A big chunk of this implementation has been copied verbatim from Don Syme’s excellent post on reporting progress from asynchronous tasks. The main change to Don’s AsyncWorker implementation is our need to fail should we be unable to get a valid SynchronizationContext for our main thread: if this doesn’t work then we need to fail (gracefully), as we will not be able to report our progress via a new context executing in the .NET thread pool
  • Otherwise we have a number of tail-recursive functions (a topic I’ve discussed previously), to avoid both iterative code and stack overflows
  • We also expose an event which is subscribed to in our C# calling code. This means we don’t need any dependency on AutoCAD libraries in the F# project: to update the command-line and progress meter we simply need to fire that event from the UI thread and let the code execute from the project with the appropriate assembly references
  • We’re using another asynchronous concept to manage our writing to the local text file for our point data: we’re using an agent to manage this

That’s really about it in terms of the changes. Let’s take it all for a spin.

We’ll start by running our code against the largest, most detailed Photosynth I’ve seen, another suggestion from Nate Lawrence: Mark Willis’ Tres Yonis synth. This is an impressively detailed synth: its point cloud contains 1.1M points and it was defined by 534 high-resolution photos.

The epic Another Tres Yonis Synth

Downloading and processing these files sequentially takes around 5 minutes (I just measure it at 4:48, but have also see it taking around five and a half).

When we perform the same operation using our new, improved application, it now takes a hair over 35 seconds! That’s less than 1/8th of the time. Here’s this magnificent Photosynth’s point cloud inside AutoCAD:

The epic Another Tres Yonis point cloud inside AutoCAD 2011

Well, that’s it for today. I have a few other places I want to invest time working with (or should that be “playing with”? :-) this technology. I want to go through the process of using Photosynth to capture a real-world model and then work on it inside AutoCAD, modelling the captured geometry. I also want to compare the results of this approach with that of working with point clouds generated by a 3D laser scanner (our friends at FARO are hopefully providing one sometime in the next few weeks, which I’m very excited about). This is a really exciting area, and you can expect me to spend more time on it over the coming months (although I’ll continue to address other areas, too, for those that find this stuff boring :-).

blog comments powered by Disqus

Feed/Share

10 Random Posts