Source code for aitlas.datasets.spacenet6

"""
.. note:: Based on the implementation at: https://github.com/SpaceNetChallenge/SpaceNet_SAR_Buildings_Solutions/blob/master/1-zbigniewwojna/main.py#L412

"""
import glob
import math
import os
import warnings
from functools import partial
from multiprocessing import Pool

import cv2

try:
    import gdal
except ModuleNotFoundError as err:
    from osgeo import gdal

import numpy as np
import pandas as pd
import torch
from shapely.wkt import loads
from skimage import io, measure
from skimage.morphology import dilation, erosion, square
from skimage.segmentation import watershed
from tqdm import tqdm

from ..base import BaseDataset
from ..datasets.schemas import SpaceNet6DatasetSchema
from ..utils import parse_img_id


# Ignore the "low-contrast" warnings
warnings.filterwarnings("ignore")


[docs]def polygon_to_mask(poly, image_size): image_mask = np.zeros(image_size, np.uint8) def integer_coordinates(x): return np.array(x).round().astype(np.int32) exteriors = [integer_coordinates(poly.exterior.coords)] interiors = [integer_coordinates(pi.coords) for pi in poly.interiors] cv2.fillPoly(image_mask, exteriors, 1) cv2.fillPoly(image_mask, interiors, 0) return image_mask
[docs]def process_image( image_path, segmentation_directory, edge_width, contact_width, gt_buildings_csv ): """ Creates and saves the target (ground-truth) segmentation mask for the input image. :param image_path: path to the source image :type image_path: str :param segmentation_directory: path to the destination directory for the segmentation masks :type segmentation_directory: str :param edge_width: the width of the edge :type edge_width: int :param contact_width: the width of the contact :type contact_width: int :param gt_buildings_csv: path to the source ground-truth-buildings csv :type gt_buildings_csv: str """ gt_buildings = pd.read_csv(gt_buildings_csv) image_name = os.path.basename(image_path) values = gt_buildings[ (gt_buildings["ImageId"] == "_".join(image_name.split("_")[-4:])[:-4]) ][["TileBuildingId", "PolygonWKT_Pix", "Mean_Building_Height"]].values labels = np.zeros((900, 900), dtype="uint16") heights = np.zeros((900, 900), dtype="float") cur_lbl = 0 for i in range(values.shape[0]): poly = loads(values[i, 1]) if not poly.is_empty: cur_lbl += 1 msk = polygon_to_mask(poly, (900, 900)) labels[msk > 0] = cur_lbl if values[i, 2] == values[i, 2]: heights[msk > 0] = values[i, 2] msk = np.zeros((900, 900, 3), dtype="uint8") if cur_lbl > 0: footprint_msk = labels > 0 border_msk = np.zeros_like(labels, dtype="bool") for l in range(1, labels.max() + 1): tmp_lbl = labels == l _k = square(edge_width) tmp = erosion(tmp_lbl, _k) tmp = tmp ^ tmp_lbl border_msk = border_msk | tmp tmp = dilation(labels > 0, square(contact_width)) tmp2 = watershed(tmp, labels, mask=tmp, watershed_line=True) > 0 tmp = tmp ^ tmp2 tmp = tmp | border_msk tmp = dilation(tmp, square(contact_width)) contact_msk = np.zeros_like(labels, dtype="bool") for y0 in range(labels.shape[0]): for x0 in range(labels.shape[1]): if not tmp[y0, x0]: continue if labels[y0, x0] == 0: sz = 3 else: sz = 1 unique = np.unique( labels[ max(0, y0 - sz) : min(labels.shape[0], y0 + sz + 1), max(0, x0 - sz) : min(labels.shape[1], x0 + sz + 1), ] ) if len(unique[unique > 0]) > 1: contact_msk[y0, x0] = True msk = np.stack( (255 * footprint_msk, 255 * border_msk, 255 * contact_msk) ).astype("uint8") msk = np.rollaxis(msk, 0, 3) io.imsave(os.path.join(segmentation_directory, image_name), msk)
[docs]class SpaceNet6Dataset(BaseDataset): """SpaceNet6 dataset.""" schema = SpaceNet6DatasetSchema def __init__(self, config): super().__init__(config) self.image_paths = list() self.mask_paths = list() self.orients = pd.read_csv(config.orients, index_col=0) self.orients["val"] = list(range(len(self.orients.index))) def __getitem__(self, index): """ Loads the dataset item at the specified index. Applies the transformations to the item before returning it. :param index : index :type index: int """ # Get image paths image_path = self.image_paths[index] # Read image image = io.imread(image_path) mask = None # placeholder, ignores the "might be referenced before assignment" warning # Calculate min/max x/y for the black parts m = np.where((image.sum(axis=2) > 0).any(1)) y_min, y_max = np.amin(m), np.amax(m) + 1 m = np.where((image.sum(axis=2) > 0).any(0)) x_min, x_max = np.amin(m), np.amax(m) + 1 # Remove black parts image = image[y_min:y_max, x_min:x_max] # Apply transformations, (should be available only for training data) if self.config.transforms: # Get mask path mask_path = self.mask_paths[index] # Read mask mask = io.imread(mask_path) # Remove black parts mask = mask[y_min:y_max, x_min:x_max] image, mask = self.transform({"image": image, "mask": mask}) # Extract direction, strip and coordinates from image direction, strip, coordinate = parse_img_id(image_path, self.orients) if direction.item(): image = np.fliplr(np.flipud(image)) if self.config.transforms: mask = np.fliplr(np.flipud(mask)) image = ( image - np.array([28.62501827, 36.09922463, 33.84483687, 26.21196667]) ) / np.array([8.41487376, 8.26645475, 8.32328472, 8.63668993]) # Transpose image image = torch.from_numpy(image.transpose((2, 0, 1)).copy()).float() # Reorder bands image = image[[0, 3, 1, 2]] if self.config.transforms: weights = np.ones_like(mask[:, :, :1], dtype=float) region_labels, region_count = measure.label( mask[:, :, 0], background=0, connectivity=1, return_num=True ) region_properties = measure.regionprops(region_labels) for bl in range(region_count): weights[region_labels == bl + 1] = 1024.0 / region_properties[bl].area mask[:, :, :3] = (mask[:, :, :3] > 1) * 1 weights = torch.from_numpy(weights.transpose((2, 0, 1)).copy()).float() mask = torch.from_numpy(mask.transpose((2, 0, 1)).copy()).float() rgb = torch.Tensor([0]) else: mask = rgb = weights = region_count = torch.Tensor([0]) return { "image": image, "mask": mask, "rgb": rgb, "strip": strip, "direction": direction, "coordinate": coordinate, "image_path": image_path, "ymin": y_min, "xmin": x_min, "b_count": region_count, "weights": weights, } def __len__(self): return len(self.image_paths)
[docs] def load_directory(self): """Loads the *.tif images from the specified directory.""" self.image_paths = glob.glob(os.path.join(self.config.test_directory, "*.tif")) self.mask_paths = None
[docs] def load_other_folds(self, fold): """Loads all images (and masks) except the ones from this fold.""" df = pd.read_csv(self.config.folds_path) self.image_paths = [ os.path.join( self.config.root_directory, "SAR-Intensity", os.path.basename(x) ) for x in df[ np.logical_or( df["fold"] > (fold % 10) + 1, df["fold"] < (fold % 10) - 1 ) ]["sar"].values ] self.mask_paths = [ os.path.join(self.config.segmentation_directory, os.path.basename(x)) for x in df[ np.logical_or( df["fold"] > (fold % 10) + 1, df["fold"] < (fold % 10) - 1 ) ]["segm"].values ]
[docs] def load_fold(self, fold): """Loads the images from this fold.""" df = pd.read_csv(self.config.folds_path) self.image_paths = [ os.path.join( self.config.root_directory, "SAR-Intensity", os.path.basename(x) ) for x in df[df["fold"] == (fold % 10)]["sar"].values ] self.mask_paths = None
[docs] def labels(self): pass
[docs] def prepare(self): """ Prepares the SpaceNet6 data set for model training and validation by: 1. Creating training segmentation masks from the geojson files 2. Splitting the data set by location, which was shown to be very important for model learning, see: https://github.com/SpaceNetChallenge/SpaceNet_SAR_Buildings_Solutions/blob/master/1-zbigniewwojna/README.md Creates 10 splits of the data set. Each split consists of 10 folds (i.e. further splits) of which 9 are used for training and one for validation/testing (in essence, a cross validation procedure). """ # Create destination directories if they don't exist os.makedirs(self.config.segmentation_directory, exist_ok=True) os.makedirs(self.config.folds_dir, exist_ok=True) # Path to the ground-truth buildings csv file gt_buildings_csv_filepath = os.path.join( self.config.root_directory, "SummaryData/SN6_Train_AOI_11_Rotterdam_Buildings.csv", ) # Read gt building csv file gt_buildings = pd.read_csv(gt_buildings_csv_filepath) # Walk the raw data directory with the SAR images and save the filenames in it sar_image_paths = glob.glob( os.path.join(self.config.root_directory, "SAR-Intensity", "*.tif") ) # Process each SAR image with Pool(self.config.num_threads) as pool: for _ in tqdm( pool.imap_unordered( partial( process_image, segmentation_directory=self.config.segmentation_directory, edge_width=self.config.edge_width, contact_width=self.config.contact_width, gt_buildings_csv=gt_buildings_csv_filepath, ), sar_image_paths, ) ): pass orientations = pd.read_csv( filepath_or_buffer=self.config.orients, sep=" ", index_col=0, names=["strip", "direction"], header=None, ) df_fold = pd.DataFrame( columns=["ImageId", "sar", "segm", "rotation", "x", "y", "fold"] ) l_edge = 591640 r_edge = 596160 orientations["sum_y"] = 0.0 orientations["ctr_y"] = 0.0 for sar_path in tqdm(sar_image_paths): image_id = "_".join( os.path.splitext(os.path.basename(sar_path))[0].split("_")[-4:] ) strip_name = "_".join(image_id.split("_")[-4:-2]) rotation = orientations.loc[strip_name]["direction"].squeeze() tr = gdal.Open(sar_path).GetGeoTransform() orientations.loc[strip_name, "sum_y"] += tr[3] orientations.loc[strip_name, "ctr_y"] += 1 fold_no = min( self.config.num_folds - 1, max( 0, math.floor( (tr[0] - l_edge) / (r_edge - l_edge) * self.config.num_folds ), ), ) segmentation_path = os.path.join( self.config.segmentation_directory, os.path.basename(sar_path) ) df_fold = df_fold.append( { "ImageId": image_id, "sar": sar_path, "segm": segmentation_path, "rotation": rotation, "x": tr[0], "y": tr[3], "fold": fold_no, }, ignore_index=True, ) df_fold.to_csv(os.path.join(self.config.folds_dir, "folds.csv"), index=False) for i in range(self.config.num_folds): img_ids = df_fold[df_fold["fold"] == i]["ImageId"].values gt_buildings[gt_buildings.ImageId.isin(img_ids)].to_csv( os.path.join(self.config.folds_dir, "gt_fold{}.csv").format(i), index=False, ) orientations["mean_y"] = orientations["sum_y"] / orientations["ctr_y"] orientations["coord_y"] = ( (orientations["mean_y"] - 5746153.106161971) / 11000 ) + 0.2 orientations.to_csv(self.config.orients_output, index=True)