OpenDroneMap-WebODM/coreplugins/changedetection/compare.py

140 wiersze
5.2 KiB
Python
Czysty Zwykły widok Historia

2020-04-25 16:49:27 +00:00
import rasterio as rio
from rasterio import warp, transform
import numpy as np
import cv2
import json
from geojson import Feature, FeatureCollection, dumps, Polygon
from rasteralign import align, align_altitudes
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]