X

NV5 Geospatial Blog

Each month, NV5 Geospatial posts new blog content across a variety of categories. Browse our latest posts below to learn about important geospatial information or use the search bar to find a specific topic or author. Stay informed of the latest blog posts, events, and technologies by joining our email list!



From Image to Insight: How GEOINT Automation Is Changing the Speed of Decision-Making

From Image to Insight: How GEOINT Automation Is Changing the Speed of Decision-Making

4/28/2025

When every second counts, the ability to process geospatial data rapidly and accurately isn’t just helpful, it’s critical. Geospatial Intelligence (GEOINT) has always played a pivotal role in defense, security, and disaster response. But in high-tempo operations, traditional workflows are no longer fast enough. Analysts are... Read More >

Thermal Infrared Echoes: Illuminating the Last Gasp of a Dying Star

Thermal Infrared Echoes: Illuminating the Last Gasp of a Dying Star

4/24/2025

This blog was written by Eli Dwek, Emeritus, NASA Goddard Space Flight Center, Greenbelt, MD and Research Fellow, Center for Astrophysics, Harvard & Smithsonian, Cambridge, MA. It is the fifth blog in a series showcasing our IDL® Fellows program which supports passionate retired IDL users who may need support to continue their work... Read More >

A New Era of Hyperspectral Imaging with ENVI® and Wyvern’s Open Data Program

A New Era of Hyperspectral Imaging with ENVI® and Wyvern’s Open Data Program

2/25/2025

This blog was written in collaboration with Adam O’Connor from Wyvern.   As hyperspectral imaging (HSI) continues to grow in importance, access to high-quality satellite data is key to unlocking new insights in environmental monitoring, agriculture, forestry, mining, security, energy infrastructure management, and more.... Read More >

Ensure Mission Success With the Deployable Tactical Analytics Kit (DTAK)

Ensure Mission Success With the Deployable Tactical Analytics Kit (DTAK)

2/11/2025

In today’s fast-evolving world, operational success hinges on real-time geospatial intelligence and data-driven decisions. Whether it’s responding to natural disasters, securing borders, or executing military operations, having the right tools to integrate and analyze data can mean the difference between success and failure.... Read More >

How the COVID-19 Lockdown Improved Air Quality in Ecuador: A Deep Dive Using Satellite Data and ENVI® Software

How the COVID-19 Lockdown Improved Air Quality in Ecuador: A Deep Dive Using Satellite Data and ENVI® Software

1/21/2025

The COVID-19 pandemic drastically altered daily life, leading to unexpected environmental changes, particularly in air quality. Ecuador, like many other countries, experienced significant shifts in pollutant concentrations due to lockdown measures. In collaboration with Geospace Solutions and Universidad de las Fuerzas Armadas ESPE,... Read More >

1345678910Last
«May 2025»
SunMonTueWedThuFriSat
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567
14742 Rate this article:
4.8

Putting a Mountain Of Data In Your Hands - From a Point Cloud to a 3D Printed Model using ENVI LiDAR and IDL

Jim Pendleton

For some of us, it's significantly easier to understand an object in 3-dimensional space if we're holding it in our hands, versus viewing it on a screen or even in a virtual reality environment.

Land-use or military planners, architects, and civil engineers will sometimes rely on scale models of their environment to better understand features of the terrain and the ramifications of their decisions.

This blog will cover one technique, using ENVI LiDAR along with some custom IDL code, to convert publicly-available LiDAR data into a 3-D printed object, a scale model.

The subject of this study is a section of picturesque Mt. Princeton, a 14,196-foot peak that flanks the west side of the Arkansas River Valley near the town of Buena Vista, Colorado. Below is a Google Earth view of the region, for perspective.

LiDAR data for a section of the mountain is available to the public through the US Geological Survey's EarthExplorer online portal. Setting up a free account is simple.

Once online, navigate with the map display to the location of interest.

The initial goal is to determine which data sets are available for an area around the summit of Mt. Princeton. In this example, I have manually drawn a bounding box shown in red. Clicking the "Data Sets" button will bring you to the next page from which you can choose from among many data sources.

In order to generate a printed surface, we will start with the point cloud data associated with a LiDAR collection. Check the inventory of available data in this region of interest.

For each data set, the footprint icon will highlight the ground projection of the associated data in the image display. In this example, I've selected data set number 8, which shows fairly significant variations in elevation.

Clicking the "download" button for a data set will prompt you with a dialog box to choose the format of the output data.

ENVI LiDAR can read compressed data as well as uncompressed data file formats, so I've selected the "LAZ Product" (compressed LiDAR) option to speed the transfer.

Download the resulting file to your local computer. Run ENVI 5.4 and select the downloaded ".laz" file as your input. You will be prompted initially for a coordinate system for the point cloud data because the map projection metadata is not carried with the point cloud in this format.

I know that this data set was collected in WGS84 in UTM zone 13 N coordinates. In reality, for our purposes of printing the data, the coordinate system is not particularly important. Unless you own a printer with an impressively large volume, we will be scaling and re-centering the units in the end, from meters to the scale of a few centimeters.

The original data in this area is approximately 1500 by 400 meters on the ground, with an altitude range of 500 meters.

When the projection is applied, the point data will be displayed in the ENVI LiDAR UI. Initially, this will include points returned from objects such as trees, rocks, hikers and marmots.

Point clouds cannot be printed directly because points do not occupy a physical volume. In this example, we will first create a polygonal mesh that represents a surface. The surface itself is not sufficient for 3-D printing, either, because a surface has no physical depth. But we will a third dimension in a future step.

In order to create the initial surface model, we will use the Process Data option in ENVI LiDAR.

In this example, we will create a terrain surface model in the form of a Triangulated Irregular Network, or TIN. The TIN will provide the vertices and connectivity we need for printing the top of our physical object.

The vertices and connectivity will be written to a ShapeFile-format set of files, by default. Progress during processing is shown in the log window of ENVI LiDAR.

Close the ENVI LiDAR UI and return to the main ENVI UI. Select the ShapeFile you just created and display it.

From within ENVI, this View can be exported to Google Earth as an image that can be draped over the surface.  Here is an associated tour.

In order to feed this surface model into a printer, we must first extract the data from the ShapeFile. The ShapeFile, as produced by ENVI LiDAR only stores the triangulation as one individual entity per triangle. As a result, there is a significant amount of duplication where multiple triangles will share a single vertex. In a later step, we will need to take into account this shared connectivity when locating the perimeter vertices.

The following IDL routine accepts as input the path to the .shp file and produces as output the vertex list and connectivity, removing duplicate vertices.  Feel free to improve its efficiency.

 pro shptopoly, file, v, c os = idlffshape(file) os.getproperty, n_entities = nen c = ulonarr(nen*4) tol = .005^2 ent = os.getentity(0) vc = *ent.vertices c[0] = [3L, 0, 1, 2] xs = reform(vc[0, 0:2]) ys = reform(vc[1, 0:2]) zs = reform(vc[2, 0:2]) vcount = 3LL for x = 1l, nen - 1 do begin ent = os.getentity(x) vc = *ent.vertices c[x*4] = 3 for i = 0l, 2 do begin r2 = min((vc[0, i] - xs)^2 + (vc[1, i] - ys)^2 + (vc[2, i] - zs)^2, mr2) if (r2 gt tol) then begin c[x*4 + i + 1] = vcount xs = [xs, reform(vc[0, i])] ys = [ys, reform(vc[1, i])] zs = [zs, reform(vc[2, i])] vcount++ endif else begin c[x*4 + i + 1] = mr2 endelse endfor endfor v = transpose([[xs], [ys], [zs]]) end

 

We can use this routine to compare our new model with the region of interest in EarthExplorer.

 

IDL> shptopoly, pathto + 'terrainTIN.shp', v, c
IDL> xobjview, idlgrpolygon(v, poly = c, color = !color.gold, style = 2)

 

 

Because a surface, by itself, has no depth it cannot be printed. One approach to adding depth is to create a "skirt" around the perimeter. First, we must locate the exterior vertices. An exterior vertex is one whose connected triangles fail to add to 360 degrees.

 

The following routine returns the indices of the exterior vertices. Feel free to optimize this routine as well.

 function exteriorvertices, v, c compile_opt strictarr on_error, 2 indices = [] c0 = c[1:*:4] c1 = c[2:*:4] c2 = c[3:*:4] tol = 10./180*!dpi twopi = !dpi*2. for i = 0L, v.length/3 - 1 do begin hasvertex = where(c0 eq i or c1 eq i or c2 eq i, n) if (n eq 0) then continue allthetas = 0.d for j = 0L, n - 1 do begin v0 = double((v[*, c0[hasvertex[j]]])[0:1]) v1 = double((v[*, c1[hasvertex[j]]])[0:1]) v2 = double((v[*, c2[hasvertex[j]]])[0:1]) h = hasvertex[j] case 1 of c0[hasvertex[j]] eq i : begin aa = sqrt(total((v2 - v1)^2)) bb = sqrt(total((v0 - v1)^2)) cc = sqrt(total((v2 - v0)^2)) end c1[hasvertex[j]] eq i : begin aa = sqrt(total((v2 - v0)^2)) bb = sqrt(total((v1 - v0)^2)) cc = sqrt(total((v2 - v1)^2)) end c2[hasvertex[j]] eq i : begin aa = sqrt(total((v1 - v0)^2)) bb = sqrt(total((v2 - v1)^2)) cc = sqrt(total((v2 - v0)^2)) end else : endcase theta = acos((bb^2 + cc^2 - aa^2)/(2.*bb*cc)) allthetas += abs(theta) endfor if (abs(twopi - allthetas) gt tol) then indices = [indices, i] endfor return, indices end

 

Pass as input the vertices and connectivity list created earlier.  The vector of indices is the function return value.

 

IDL> e = exteriorvertices(v, c)

(There are some edge cases not addressed in the code, such as edge triangles that are calculated by the TIN process to be perpendicular to the X-Y plane.  How would you approach these?)

 

Given the exterior indices, we can now generate a polygonal mesh skirt for our model by projecting a new set of vertices "down" in the Z dimension to an arbitrary base depth that's lower than the lowest Z in the original data. Then we stitch the upper and lower exterior polygons with triangles to provide depth, and triangulate the base to fill it in as well.

 pro addskirt, v, c, newv, newc, exterior_indices = e compile_opt strictarr newv = !null newc = !null newv = v mins = min(newv, dimension = 2, max = maxs) for i = 0, 1 do begin newv[i, *] -= mins[i] endfor if (e eq !null) then begin triangulate, newv[0, *], newv[1, *], tr, b endif else begin b = e endelse xmid= total(newv[0, b])/b.length ymid = total(newv[1, b])/b.length xx = newv[0, b] - xmid yy = newv[1, b] - ymid theta = atan(yy, xx) s = sort(theta) b = b[s] plots, newv[0, [b, b[0]]], newv[1, [b, b[0]]] oldx = reform(v[0, *]) oldy = reform(v[1, *]) oldz = reform(v[2, *]) oldc = oldx.length newx = [oldx, oldx[[b, b[0]]]] newy = [oldy, oldy[[b, b[0]]]] newz = [oldz, replicate(mins[2] - (maxs[2] - mins[2])/100., b.length + 1)] b = [b, b[0]] newc = c for i = 0, b.length - 2 do begin newc = [newc, 3, oldc + i, oldc + i + 1, b[i]] newc = [newc, 3, oldc + i + 1, b[i + 1], b[i]] endfor t = idlgrtessellator() t.addpolygon, oldx[b], oldy[b] r = t.tessellate(basev, basep) for i = 0, basep.length/4 - 1 do begin newc = [newc, 3, oldc + basep[[i*4 + 1 + lindgen(3)]]] endfor newv = transpose([[newx], [newy], [newz]]) newc mod= newv.length/3 opoly = idlgrpolygon(newv, poly = newc, style = 1, color = !color.red) xobjview, opoly end

 

This routine accepts as input the original vertices and connectivity list, along with the exterior vertex indices, and creates a new set of vertices and connectivity.

 

IDL> addskirt, v, c, newv, newc, exterior_indices = e

For debugging purposes, this call also visualizes the result in IDL.

Now we have a set of vertices and a connectivity list that is acceptable to most printing devices. But we must create a file that contains these coordinates in a format that can be used by a printer, or an interface that converts the data into a format acceptable to a particular printer.

A very simple file format that is accepted by most printer drivers is STL, which is acronym-ish for "STereoLithography". In its simplest form, it is just a list of triangles.

The following code writes an ASCII-format STL file using our updated vertices and connectivity lists.

 Pro MakeSTL, vertices, polygons, outfile On_Error, 2 Catch, ErrorNumber If (ErrorNumber ne 0) then Begin Catch, /Cancel If (LUN ne !null) then Begin Free_LUN, LUN EndIf If (C ne !null) then Begin CD, C Endif Message, /Reissue_Last Endif L = List() L.Add, 'solid mntprinceton' V = vertices For I = 0, 2 do begin V[I, *] -= min(Vertices[I, *]) EndFor Poly = Polygons For I = 0L, Poly.Length/4 - 1 Do Begin V1 = Reform(V[*, Poly[I*4 + 1]]) V2 = Reform(V[*, Poly[I*4 + 2]]) V3 = Reform(V[*, Poly[I*4 + 3]]) uu = v2 - v1 vv = v3 - v1 nx = uu[1]*vv[2] - uu[2]*vv[1] ny = uu[2]*vv[0] - uu[0]*vv[2] nz = uu[0]*vv[1] - uu[1]*vv[0] R = Sqrt(Total([nx,ny,nz]^2)) L.Add, 'facet normal ' + ((String([nx,ny,nz]/R, Format = '(f8.5)')).Trim()).Join(' ') L.Add, ' outer loop' L.Add, ' vertex ' + $ ((String(V1)).Trim()).Join(' ') L.Add, ' vertex ' + $ ((String(V2)).Trim()).Join(' ') L.Add, ' vertex ' + $ ((String(V3)).Trim()).Join(' ') L.Add, ' endloop' L.Add, 'endfacet' EndFor L.Add, 'endsolid mntprinceton' OpenW, LUN, OutFile, /Get_LUN ForEach Line, L Do Begin PrintF, LUN, Line EndForEach Free_LUN, LUN End

 

Call this routine with the new vertices and connectivity lists, along with a path to an output .stl file.

 

IDL> makestl, newv, newc, "path-to-your-output-file.stl"

The output file will be simple ASCII, with contents that resemble the following.

One option for printing the STL files is via the "3D Builder" application, which is available as a freebie in Windows 10.

Select the STL file as input.  This will import the model into the printer driver, which is responsible for centering and scaling the raw data into the printer space.

Now you can print your data, if you have a printer.  The 3D Builder application also has a convenient, if rather pricey, option to export your model to a third-party online service that will print your model remotely and ship it to you.

Here is the final product.  Notice that the thin hiking trail is clearly visible.

If you have a more complicated model in ENVI or IDL and you would like some assistance generating a solid rendition, beyond what has been covered here, please contact NV5 Geospatial's Custom Solutions Group. We'd be happy to help!

Please login or register to post comments.