{ "currentVersion": 10.91, "serviceDescription": "
This layer is a combination of two datasets. One of which is an Normalized Difference Vegetation Index (NDVI) product derived from the LARIAC7 program and the other is a Normalized Digital Surface Model from our County partners at NV5. These two datasets were combined together to produce a new countywide tree canopy layer. The imagery was collected in Summer of 2024 and only includes urban/suburban areas of the County of Los Angeles. <\/SPAN><\/P> <\/P> 1. <\/SPAN>Los Angeles Region Imagery Acquisition Consortium (LARIAC-7) <\/SPAN><\/A>Normalized Difference Vegetation Index (NDVI) tiff tiles. NDVI is a simple numerical indicator that can be used to analyze remote sensing measurements, typically but not necessarily from a space platform, and assess whether the target being observed contains live green vegetation or not. NDVI was developed from the 2024 Color Infrared (CIR) imagery at 3-inch resolution which contains the Red and Near Infrared (NIR) Bands. The 3-inch data was resampled to 1 meter resolution. <\/SPAN>Click here for more information.<\/SPAN><\/A><\/P> <\/P> 2. Normalized Digital Surface Model (NDSM) tiff tiles. NDSM (normalized digital surface model) represents the relative height of features above the surrounding ground surface. It is a valuable elevation product that provides insights into the relative height of features compared to the surrounding ground. In contrast, DSMs (digital surface models) and DTMs (digital terrain models) represent absolute height referenced to a datum, typically the mean sea level.<\/SPAN><\/P> <\/P> Processing Steps:<\/SPAN><\/P> 1. To create NDVI, the following code was used outside of ArcPro to efficiently combine the Red and Infrared bands of the CIR imagery.<\/SPAN><\/P> import os<\/SPAN><\/P> import time<\/SPAN><\/P> import numpy as np<\/SPAN><\/P> import rasterio<\/SPAN><\/P> from tqdm import tqdm # progress bar<\/SPAN><\/P> <\/SPAN><\/P> # =========================<\/SPAN><\/P> # CONFIG SECTION (EDIT ME)<\/SPAN><\/P> # =========================<\/SPAN><\/P> <\/SPAN><\/P> RED_RASTER = r\"D:\\NDVI\\SOURCE\\NDVI_CALC_2024\\NDVI_CALC_2024\\Red.tif\"<\/SPAN><\/P> NIR_RASTER = r\"D:\\NDVI\\SOURCE\\NDVI_CALC_2024\\NDVI_CALC_2024\\Infrared.tif\"<\/SPAN><\/P> OUTPUT_NDVI = r\"D:\\NDVI\\SOURCE\\NDVI_CALC_2024\\NDVI_CALC_2024\\NDVI_2024_V2.tif\"<\/SPAN><\/P> <\/SPAN><\/P> NDVI_NODATA = -9999.0<\/SPAN><\/P> <\/SPAN><\/P> OUTPUT_COMPRESS = \"NONE\" # FASTEST. Switch to DEFLATE later if needed.<\/SPAN><\/P> BLOCK_SIZE = 512 # Good block alignment for most rasters.<\/SPAN><\/P> <\/SPAN><\/P> os.environ.setdefault(\"GDAL_NUM_THREADS\", \"ALL_CPUS\")<\/SPAN><\/P> <\/SPAN><\/P> <\/SPAN><\/P> def compute_ndvi_streaming():<\/SPAN><\/P> t0 = time.time()<\/SPAN><\/P> <\/SPAN><\/P> print(f\"\\nOpening RED raster: {RED_RASTER}\")<\/SPAN><\/P> with rasterio.open(RED_RASTER) as red_ds:<\/SPAN><\/P> w = red_ds.width<\/SPAN><\/P> h = red_ds.height<\/SPAN><\/P> red_nodata = red_ds.nodata<\/SPAN><\/P> print(f\" Size: {w} × {h}\")<\/SPAN><\/P> <\/SPAN><\/P> <\/SPAN><\/P> with rasterio.open(NIR_RASTER) as nir_ds:<\/SPAN><\/P> if (nir_ds.width, nir_ds.height) != (w, h):<\/SPAN><\/P> raise ValueError(\"Input rasters differ in size.\")<\/SPAN><\/P> if nir_ds.transform != red_ds.transform:<\/SPAN><\/P> raise ValueError(\"Input rasters have different transforms.\")<\/SPAN><\/P> if nir_ds.crs != red_ds.crs:<\/SPAN><\/P> raise ValueError(\"Input rasters differ in CRS.\")<\/SPAN><\/P> <\/SPAN><\/P> nir_nodata = nir_ds.nodata<\/SPAN><\/P> <\/SPAN><\/P> profile = red_ds.profile.copy()<\/SPAN><\/P> profile.update(<\/SPAN><\/P> driver=\"GTiff\",<\/SPAN><\/P> dtype=\"float32\",<\/SPAN><\/P> count=1,<\/SPAN><\/P> nodata=NDVI_NODATA,<\/SPAN><\/P> tiled=True,<\/SPAN><\/P> blockxsize=BLOCK_SIZE,<\/SPAN><\/P> blockysize=BLOCK_SIZE,<\/SPAN><\/P> compress=OUTPUT_COMPRESS,<\/SPAN><\/P> BIGTIFF=\"IF_SAFER\",<\/SPAN><\/P> )<\/SPAN><\/P> <\/SPAN><\/P> print(\"\\nCreating output NDVI raster...\")<\/SPAN><\/P> with rasterio.open(OUTPUT_NDVI, \"w\", **profile) as out_ds:<\/SPAN><\/P> <\/SPAN><\/P> # Count blocks for progress bar<\/SPAN><\/P> block_list = list(red_ds.block_windows(1))<\/SPAN><\/P> <\/SPAN><\/P> print(f\"Total blocks to process: {len(block_list)}\")<\/SPAN><\/P> <\/SPAN><\/P> # Progress bar loop<\/SPAN><\/P> for ji, window in tqdm(block_list, desc=\"Computing NDVI\", unit=\"block\"):<\/SPAN><\/P> <\/SPAN><\/P> # Read both windows<\/SPAN><\/P> red = red_ds.read(1, window=window).astype(\"float32\")<\/SPAN><\/P> nir = nir_ds.read(1, window=window).astype(\"float32\")<\/SPAN><\/P> <\/SPAN><\/P> # Nodata mask<\/SPAN><\/P> mask = np.zeros(red.shape, dtype=bool)<\/SPAN><\/P> if red_nodata is not None:<\/SPAN><\/P> mask |= (red == red_nodata)<\/SPAN><\/P> if nir_nodata is not None:<\/SPAN><\/P> mask |= (nir == nir_nodata)<\/SPAN><\/P> <\/SPAN><\/P> denom = nir + red<\/SPAN><\/P> safe = denom != 0<\/SPAN><\/P> valid = safe & (~mask)<\/SPAN><\/P> <\/SPAN><\/P> ndvi_block = np.full(red.shape, NDVI_NODATA, dtype=\"float32\")<\/SPAN><\/P> ndvi_block[valid] = (nir[valid] - red[valid]) / denom[valid]<\/SPAN><\/P> <\/SPAN><\/P> # Clamp<\/SPAN><\/P> np.clip(ndvi_block, -1.0, 1.0, out=ndvi_block)<\/SPAN><\/P> <\/SPAN><\/P> out_ds.write(ndvi_block, 1, window=window)<\/SPAN><\/P> <\/SPAN><\/P> total = (time.time() - t0) / 60<\/SPAN><\/P> print(f\"\\nFinished! Total time: {total:.2f} minutes\")<\/SPAN><\/P> print(f\"NDVI saved to: {OUTPUT_NDVI}\")<\/SPAN><\/P> <\/SPAN><\/P> <\/SPAN><\/P> if __name__ == \"__main__\":<\/SPAN><\/P> compute_ndvi_streaming()<\/SPAN><\/P> 2. All areas where the surface height in the NDSM Dataset was greater than 3.048 meters or 10 feet*, and the NDVI value was <.1 were extracted and assigned the value 1. This is the trees layer. The formula is listed below.<\/SPAN><\/P> <\/P> SetNull(\"Mosaic_Height_Float2\", SetNull(\"NDVI_MERGED_Band_1\",1,\"Value <.1\"),\"Value < 3.048\")<\/SPAN><\/P> <\/P> 3. To provide improved symbolization of trees across the County, and to remove a number of artifacts, a set of additional processes were run. The MajorityFilter command was run twice on the trees layer. For the options, I used 8 neighbors with the majority replacement threshold.<\/SPAN><\/P> *Important Note: There is not one single standard threshold used across the board for defining tree height. A height of a 3 meter threshold which is used in municipalities in other countries and in research, has been the standard for tree height here in LA County PW.<\/SPAN><\/P> <\/P> See a few resources of research and municipalities using the 10ft / 3meter threshold:<\/SPAN><\/P> Lake Oswego, OR<\/SPAN><\/A><\/P> Canberra, Australia<\/SPAN><\/A><\/P> Vancouver, Canada<\/SPAN><\/A><\/P> https://www.sciencedirect.com/science/article/pii/S2667393225000018 <\/SPAN><\/A>(section 2.3.1, includes a few references)<\/SPAN><\/P> https://www.mdpi.com/2072-4292/13/4/767 <\/SPAN><\/A>(section 2.3.2)<\/SPAN><\/P> Here is more info related to the 2016 tree canopy data for the county. They don't have their methodology publicly available, but you might be able to get more info if you reached out to the email listed at the bottom of the page on the second link. I'm pretty sure they also used a 10ft threshold, but without methodology I can't say for certain.<\/SPAN><\/P> https://treepeople.org/project/la-treecanopy-data/<\/SPAN><\/A><\/P>