I had the same issue a couple of years ago. I have a solution that does not require filtered LAS data or other ancillary data. If you have access to LiDAR data, and can generate DEMs/DSMs/DHMs (DEM hereafter, I'm not debating the semantics of surface model nomenclature) from different returns, the following script may be useful.
The arcpy script ingests 3 DEMs and spits out a forest polygon and tree point shapefiles. The 3 DEMs should have the same spatial resolution (i.e. 1 meter) and extents, and represent first returns, last returns, and bare earth. I had very specific parameters for veg extraction, but the parameters can be altered to suit other needs. I am sure the process can be improved, as this was my first serious attempt at python scripting.
# Name: Veg_Extractor.py
# Date: 2013-07-16
# Usage: ArcMap 10.0; Spatial Analyst
# Input: 1 meter DEMs for first returns (DEM1), last returns (DEM2), and bare earth (BE)
# Output: forest polygon (veg with height > 4m) shapefile with holes > 500m removed;
# tree point (veg with height > 4m, crown radius of 9 cells) shapefile
# Notes: Raises error if input raster cell sizes differ
import arcpy, os
from arcpy import env
from arcpy.sa import *
# Check out any necessary licenses
arcpy.CheckOutExtension("spatial")
# Script arguments
dem1 = arcpy.GetParameterAsText(0) #input Raster Layer, First Return DEM
dem2 = arcpy.GetParameterAsText(1) #input Raster Layer, Last Return DEM
bare_earth = arcpy.GetParameterAsText(2) #input Raster Layer, Bare Earth DEM
outForest = arcpy.GetParameterAsText(3) #shapefile
outTree = arcpy.GetParameterAsText(4) #shapefile
# Make sure cell size of input rasters are same
arcpy.AddMessage("Checking cell sizes...")
dem1Xresult = arcpy.GetRasterProperties_management(dem1, "CELLSIZEX")
dem1Yresult = arcpy.GetRasterProperties_management(dem1, "CELLSIZEY")
dem2Xresult = arcpy.GetRasterProperties_management(dem2, "CELLSIZEX")
dem2Yresult = arcpy.GetRasterProperties_management(dem2, "CELLSIZEY")
beXresult = arcpy.GetRasterProperties_management(bare_earth, "CELLSIZEX")
beYresult = arcpy.GetRasterProperties_management(bare_earth, "CELLSIZEY")
dem1X = round(float(dem1Xresult.getOutput(0)),4)
dem1Y = round(float(dem1Yresult.getOutput(0)),4)
dem2X = round(float(dem2Xresult.getOutput(0)),4)
dem2Y = round(float(dem2Yresult.getOutput(0)),4)
beX = round(float(beXresult.getOutput(0)),4)
beY = round(float(beYresult.getOutput(0)),4)
if (dem1X == dem1Y == dem2X == dem2Y == beX == beY) == True:
arcpy.AddMessage("Cell sizes match.")
else:
arcpy.AddMessage("Input Raster Cell Sizes:")
arcpy.AddMessage("DEM1: (" + str(dem1X) + "," + str(dem1Y) + ")")
arcpy.AddMessage("DEM2: (" + str(dem2X) + "," + str(dem2Y) + ")")
arcpy.AddMessage(" BE: (" + str(beX) + "," + str(beY) + ")")
raise Exception("Cell sizes do not match.")
# Check map units
dem1_spatial_ref = arcpy.Describe(dem1).spatialReference
dem1_units = dem1_spatial_ref.linearUnitName
dem2_spatial_ref = arcpy.Describe(dem2).spatialReference
dem2_units = dem2_spatial_ref.linearUnitName
bare_earth_spatial_ref = arcpy.Describe(bare_earth).spatialReference
bare_earth_units = bare_earth_spatial_ref.linearUnitName
if (dem1_units == dem2_units == bare_earth_units) == True:
if dem1_units == "Meter":
area = "500 SquareMeters" #Area variable for meter
unit = 1 #meter
elif (dem1_units == "Foot_US") or (dem1_units == "Foot"):
area = "5382 SquareFeet" #Area variable for feet
unit = 3.28084 #feet in meter
else:
raise Exception("Units are not 'Meter', 'Foot_US', or 'Foot'.")
else:
raise Exception("Linear units do not match. Check spatial reference.")
# Local variables:
(workspace, filename) = os.path.split(outForest)
arcpy.env.workspace = workspace
arcpy.env.overwriteOutput = True
dem1 = Raster(dem1)
dem2 = Raster(dem2)
bare_earth = Raster(bare_earth)
nbr1 = NbrRectangle(3, 3, "CELL")
nbr2 = NbrRectangle(5, 5, "CELL")
nbr3 = NbrCircle(5, "CELL")
# Give units and multiplier
arcpy.AddMessage("Linear units are " + dem1_units + ". Using multiplier of " + str(unit) + "...")
arcpy.AddMessage("Processing DEMs...")
# Process: Raster Calculator (DEM1 - BE)
ndsm_dem1 = dem1 - bare_earth
# Process: Raster Calculator (DEM1 - DEM2)
d1_d2 = dem1 - dem2
# Process: Raster Calculator
threshold_d1d2 = (d1_d2 > (0.1 * unit)) & (ndsm_dem1 >= (4.0 * unit))
# Process: Focal Statistics (max 3x3)
focal_max1 = FocalStatistics(threshold_d1d2, nbr1, "MAXIMUM", "DATA")
# Process: Focal Statistics (majority 5x5)
focal_majority = FocalStatistics(focal_max1, nbr2, "MAJORITY", "DATA")
# Process: Con
con_ndsm_dem1 = Con(ndsm_dem1 >= (4.0 * unit), focal_majority, focal_max1)
focal_majority = None
focal_max1 = None
# Process: Focal Statistics (min 3x3)
focal_min1 = FocalStatistics(con_ndsm_dem1, nbr1, "MINIMUM", "DATA")
con_ndsm_dem1 = None
# Process: Focal Statistics (min 3x3)
veg_mask = FocalStatistics(focal_min1, nbr1, "MINIMUM", "DATA")
# Process: Focal Statistics (max R5)
focal_max2 = FocalStatistics(ndsm_dem1, nbr3, "MAXIMUM", "DATA")
arcpy.AddMessage("Calculating tree points...")
# Process: Raster Calculator
tree_points = (veg_mask == 1) & (ndsm_dem1 == focal_max2) & (ndsm_dem1 >= (4.0 * unit))
ndsm_dem1 = None
focal_max2 = None
# Process: Raster Calculator
tree_pick = Pick(tree_points == 1, 1)
tree_points = None
# Process: Raster to Polygon
arcpy.RasterToPolygon_conversion(tree_pick, workspace + "\\tree_poly.shp", "SIMPLIFY", "Value")
tree_pick = None
# Process: Feature To Point
arcpy.AddMessage("Writing tree points...")
arcpy.env.workspace = workspace #reset workspace
arcpy.env.overwriteOutput = True #reset overwrite permission
arcpy.FeatureToPoint_management(workspace + "\\tree_poly.shp", outTree, "CENTROID")
arcpy.AddMessage("Calculating forest polygons...")
# Process: Focal Statistics (max 3x3)
forests = FocalStatistics(veg_mask, nbr1, "MAXIMUM", "DATA")
veg_mask = None
# Process: Raster Calculator
forest_pick = Pick(forests == 1, 1)
# Process: Raster to Polygon
arcpy.RasterToPolygon_conversion(forest_pick, workspace + "\\forest_poly.shp", "SIMPLIFY", "Value")
# Process: Eliminate Holes > 500 sq m (5382 sq ft)
arcpy.AddMessage("Writing forest polygons...")
arcpy.EliminatePolygonPart_management(workspace + "\\forest_poly.shp", outForest, "AREA", area, "0", "CONTAINED_ONLY")
# Clean up
arcpy.AddMessage("Cleaing up...")
arcpy.Delete_management(workspace + "\\tree_poly.shp")
arcpy.Delete_management(workspace + "\\forest_poly.shp")