import rasterio as rio from rasterio import warp, transform import numpy as np import json import sys import os from geojson import Feature, FeatureCollection, dumps, Polygon from rasteralign import align, align_altitudes from webodm import settings sys.path.insert(0, os.path.join(settings.MEDIA_ROOT, "plugins", "changedetection", "site-packages")) import cv2 KERNEL_10_10 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10, 10)) KERNEL_20_20 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (20, 20)) def compare(reference_dsm_path, reference_dtm_path, compare_dsm_path, compare_dtm_path, epsg, resolution, display_type, min_height, min_area): # Read DEMs and align them with rio.open(reference_dsm_path) as reference_dsm, \ rio.open(reference_dtm_path) as reference_dtm, \ rio.open(compare_dsm_path) as compare_dsm, \ rio.open(compare_dtm_path) as compare_dtm: reference_dsm, reference_dtm, compare_dsm, compare_dtm = align(reference_dsm, reference_dtm, compare_dsm, compare_dtm, resolution=resolution) reference_dsm, reference_dtm, compare_dsm, compare_dtm = align_altitudes(reference_dsm, reference_dtm, compare_dsm, compare_dtm) # Get arrays from DEMs reference_dsm_array = reference_dsm.read(1, masked=True) reference_dtm_array = reference_dtm.read(1, masked=True) compare_dsm_array = compare_dsm.read(1, masked=True) compare_dtm_array = compare_dtm.read(1, masked=True) # Calculate CHMs chm_reference = reference_dsm_array - reference_dtm_array chm_compare = compare_dsm_array - compare_dtm_array # Calculate diff between CHMs diff = chm_reference - chm_compare # Add to the mask everything below the min height diff.mask = np.ma.mask_or(diff.mask, diff < min_height) # Copy the diff, and set everything on the mask to 0 process = np.copy(diff) process[diff.mask] = 0 # Apply open filter to filter out noise process = cv2.morphologyEx(process, cv2.MORPH_OPEN, KERNEL_10_10) # Apply close filter to fill little areas process = cv2.morphologyEx(process, cv2.MORPH_CLOSE, KERNEL_20_20) # Transform to uint8 process = process.astype(np.uint8) if display_type == 'contours': return calculate_contours(process, reference_dsm, epsg, min_height, min_area) else: return calculate_heatmap(process, diff.mask, reference_dsm, epsg, min_height) def calculate_contours(diff, reference_dem, epsg, min_height, min_area): # Calculate contours contours, _ = cv2.findContours(diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # Convert contours into features features = [map_contour_to_geojson_feature(contour, diff, epsg, reference_dem, min_height) for contour in contours] # Keep features that meet the threshold features = [feature for feature in features if feature.properties['area'] >= min_area] # Write the GeoJSON to a string return dumps(FeatureCollection(features)) def map_contour_to_geojson_feature(contour, diff_array, epsg, reference_dem, min_height): # Calculate how much area is inside a pixel pixel_area = reference_dem.res[0] * reference_dem.res[1] # Calculate the area of the contour area = cv2.contourArea(contour) * pixel_area # Calculate the indices of the values inside the contour cimg = np.zeros_like(diff_array) cv2.drawContours(cimg, [contour], 0, color=255, thickness=-1) indices = cimg == 255 # Calculate values inside the contour values = diff_array[indices] masked_values = np.ma.masked_array(values, values < min_height) # Calculate properties regarding the difference values avg = float(masked_values.mean()) min = float(masked_values.min()) max = float(masked_values.max()) std = float(masked_values.std()) # Map the contour to pixels pixels = to_pixel_format(contour) rows = [row for (row, _) in pixels] cols = [col for (_, col) in pixels] # Map from pixels to coordinates xs, ys = map_pixels_to_coordinates(reference_dem, epsg, rows, cols) coords = [(x, y) for x, y in zip(xs, ys)] # Build polygon, based on the contour polygon = Polygon([coords]) # Build the feature feature = Feature(geometry = polygon, properties = { 'area': area, 'avg': avg, 'min': min, 'max': max, 'std': std }) return feature def calculate_heatmap(diff, mask, dem, epsg, min_height): # Calculate the pixels of valid values pixels = np.argwhere(~mask) xs = pixels[:, 0] ys = pixels[:, 1] # Map pixels to coordinates coords_xs, coords_ys = map_pixels_to_coordinates(dem, epsg, xs, ys) # Calculate the actual values values = diff[~mask] # Substract the min, so all values are between 0 and max values = values - np.min(values) array = np.column_stack((coords_ys, coords_xs, values)) return json.dumps({ 'values': array.tolist(), 'max': float(max(values)) }) def map_pixels_to_coordinates(reference_tiff, dst_epsg, rows, cols): xs, ys = transform.xy(reference_tiff.transform, rows, cols) dst_crs = rio.crs.CRS.from_epsg(dst_epsg) return map_to_new_crs(reference_tiff.crs, dst_crs, xs, ys) def map_to_new_crs(src_crs, target_crs, xs, ys): """Map the given arrays from one crs to the other""" return warp.transform(src_crs, target_crs, xs, ys) def to_pixel_format(contour): """OpenCV contours have a weird format. We are converting them to (row, col)""" return [(pixel[0][1], pixel[0][0]) for pixel in contour]