From 1210e482b88c7a4e7dd0f6d88fb311941ede59da Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 9 Jul 2024 16:42:30 +0000 Subject: [PATCH 01/63] parallelise tiling --- detectree2/preprocessing/tiling.py | 468 +++++++++++++---------------- 1 file changed, 204 insertions(+), 264 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 2ceeb8c4..3a93bb20 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -4,6 +4,7 @@ of models and making landscape predictions. """ +import concurrent.futures import json import os import random @@ -47,6 +48,82 @@ def get_features(gdf: gpd.GeoDataFrame): return [json.loads(gdf.to_json())["features"][0]["geometry"]] +def process_tile( + data: DatasetReader, + out_dir: str, + buffer: int, + tile_width: int, + tile_height: int, + dtype_bool: bool, + minx, + miny, + crs, + tilename +) -> None: + """Process a single tile for making predictions. + + Args: + data: Orthomosaic as a rasterio object in a UTM type projection + out_dir: Output directory + buffer: Overlapping buffer of tiles in meters (UTM) + tile_width: Tile width in meters + tile_height: Tile height in meters + dtype_bool: Flag to edit dtype to prevent black tiles + minx: Minimum x coordinate of tile + miny: Minimum y coordinate of tile + crs: Coordinate reference system + tilename: Name of the tile + + Returns: + None + """ + out_path = Path(out_dir) + out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" + bbox = box(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer) + geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) + coords = get_features(geo) + out_img, out_transform = mask(data, shapes=coords, crop=True) + + out_sumbands = np.sum(out_img, 0) + zero_mask = np.where(out_sumbands == 0, 1, 0) + nan_mask = np.where(out_sumbands == 765, 1, 0) + sumzero = zero_mask.sum() + sumnan = nan_mask.sum() + totalpix = out_img.shape[1] * out_img.shape[2] + + # If the tile is mostly empty or mostly nan, don't save it + if sumzero > 0.25 * totalpix or sumnan > 0.25 * totalpix: + return + + out_meta = data.meta.copy() + out_meta.update({ + "driver": "GTiff", + "height": out_img.shape[1], + "width": out_img.shape[2], + "transform": out_transform, + "nodata": None, + }) + if dtype_bool: + out_meta.update({"dtype": "uint8"}) + + out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") + with rasterio.open(out_tif, "w", **out_meta) as dest: + dest.write(out_img) + + clipped = rasterio.open(out_tif) + arr = clipped.read() + r, g, b = arr[0], arr[1], arr[2] + rgb = np.dstack((b, g, r)) + + # Rescale to 0-255 if necessary + if np.max(g) > 255: + rgb_rescaled = 255 * rgb / 65535 + else: + rgb_rescaled = rgb + + cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) + + def tile_data( data: DatasetReader, out_dir: str, @@ -73,117 +150,131 @@ def tile_data( """ out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) - crs = CRS.from_string(data.crs.wkt) - crs = crs.to_epsg() + crs = CRS.from_string(data.crs.wkt).to_epsg() tilename = Path(data.name).stem + total_tiles = int(((data.bounds[2] - data.bounds[0]) / tile_width) * ((data.bounds[3] - data.bounds[1]) / tile_height)) - total_tiles = int( - ((data.bounds[2] - data.bounds[0]) / tile_width) * ((data.bounds[3] - data.bounds[1]) / tile_height) - ) + tile_args = [ + (data, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename) + for minx in np.arange(data.bounds[0], data.bounds[2] - tile_width, tile_width, int) + for miny in np.arange(data.bounds[1], data.bounds[3] - tile_height, tile_height, int) + ] - tile_count = 0 - print(f"Tiling to {total_tiles} total tiles") - - for minx in np.arange(data.bounds[0], data.bounds[2] - tile_width, - tile_width, int): - for miny in np.arange(data.bounds[1], data.bounds[3] - tile_height, - tile_height, int): - - tile_count += 1 - # Naming conventions - out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" - # new tiling bbox including the buffer - bbox = box( - minx - buffer, - miny - buffer, - minx + tile_width + buffer, - miny + tile_height + buffer, - ) - # define the bounding box of the tile, excluding the buffer - # (hence selecting just the central part of the tile) - # bbox_central = box(minx, miny, minx + tile_width, miny + tile_height) - - # turn the bounding boxes into geopandas DataFrames - geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) - # geo_central = gpd.GeoDataFrame( - # {"geometry": bbox_central}, index=[0], crs=from_epsg(4326) - # ) # 3182 - # overlapping_crowns = sjoin(crowns, geo_central, how="inner") - - # here we are cropping the tiff to the bounding box of the tile we want - coords = get_features(geo) - # print("Coords:", coords) - - # define the tile as a mask of the whole tiff with just the bounding box - out_img, out_transform = mask(data, shapes=coords, crop=True) - - # Discard scenes with many out-of-range pixels - out_sumbands = np.sum(out_img, 0) - zero_mask = np.where(out_sumbands == 0, 1, 0) - nan_mask = np.where(out_sumbands == 765, 1, 0) - sumzero = zero_mask.sum() - sumnan = nan_mask.sum() - totalpix = out_img.shape[1] * out_img.shape[2] - if sumzero > 0.25 * totalpix: - continue - elif sumnan > 0.25 * totalpix: - continue - - out_meta = data.meta.copy() - out_meta.update({ - "driver": "GTiff", - "height": out_img.shape[1], - "width": out_img.shape[2], - "transform": out_transform, - "nodata": None, - }) - # dtype needs to be unchanged for some data and set to uint8 for others - if dtype_bool: - out_meta.update({"dtype": "uint8"}) - # print("Out Meta:",out_meta) - - # Saving the tile as a new tiff, named by the origin of the tile. - # If tile appears blank in folder can show the image here and may - # need to fix RGB data or the dtype - # show(out_img) - out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") - with rasterio.open(out_tif, "w", **out_meta) as dest: - dest.write(out_img) - - # read in the tile we have just saved - clipped = rasterio.open(out_tif) - # read it as an array - # show(clipped) - arr = clipped.read() - - # each band of the tiled tiff is a colour! - r = arr[0] - g = arr[1] - b = arr[2] - - # stack up the bands in an order appropriate for saving with cv2, - # then rescale to the correct 0-255 range for cv2 - - rgb = np.dstack((b, g, r)) # BGR for cv2 - - if np.max(g) > 255: - rgb_rescaled = 255 * rgb / 65535 - else: - rgb_rescaled = rgb # scale to image - # print("rgb rescaled", rgb_rescaled) - - # save this as jpg or png...we are going for png...again, named with the origin of the specific tile - # here as a naughty method - cv2.imwrite( - str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), - rgb_rescaled, - ) - if tile_count % 50 == 0: - print(f"Processed {tile_count} tiles of {total_tiles} tiles") + with concurrent.futures.ThreadPoolExecutor() as executor: + list(executor.map(lambda args: process_tile(*args), tile_args)) print("Tiling complete") +def process_tile_train(data, + out_dir: str, + buffer, + tile_width, + tile_height, + dtype_bool, + minx, + miny, + crs, + tilename, + crowns, + threshold, + nan_threshold +) -> None: + """Process a single tile for training data. + + Args: + data: Orthomosaic as a rasterio object in a UTM type projection + out_dir: Output directory + buffer: Overlapping buffer of tiles in meters (UTM) + tile_width: Tile width in meters + tile_height: Tile height in meters + dtype_bool: Flag to edit dtype to prevent black tiles + minx: Minimum x coordinate of tile + miny: Minimum y coordinate of tile + crs: Coordinate reference system + tilename: Name of the tile + crowns: Crown polygons as a geopandas dataframe + threshold: Min proportion of the tile covered by crowns to be accepted {0,1} + nan_theshold: Max proportion of tile covered by nans + + Returns: + None + """ + + out_path = Path(out_dir) + out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" + + minx_buffered = minx - buffer + miny_buffered = miny - buffer + maxx_buffered = minx + tile_width + buffer + maxy_buffered = miny + tile_height + buffer + + bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) + geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) + coords = get_features(geo) + + overlapping_crowns = gpd.clip(crowns, geo) + if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: + return + + out_img, out_transform = mask(data, shapes=coords, crop=True) + out_sumbands = np.sum(out_img, 0) + zero_mask = np.where(out_sumbands == 0, 1, 0) + nan_mask = np.where(out_sumbands == 765, 1, 0) + sumzero = zero_mask.sum() + sumnan = nan_mask.sum() + totalpix = out_img.shape[1] * out_img.shape[2] + # If the tile is mostly empty or mostly nan, don't save it + if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: + return + + out_meta = data.meta.copy() + out_meta.update({ + "driver": "GTiff", + "height": out_img.shape[1], + "width": out_img.shape[2], + "transform": out_transform, + "nodata": None, + }) + if dtype_bool: + out_meta.update({"dtype": "uint8"}) + + out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") + with rasterio.open(out_tif, "w", **out_meta) as dest: + dest.write(out_img) + + clipped = rasterio.open(out_tif) + arr = clipped.read() + r, g, b = arr[0], arr[1], arr[2] + rgb = np.dstack((b, g, r)) + + # Rescale if necessary + if np.max(g) > 255: + rgb_rescaled = 255 * rgb / 65535 + else: + rgb_rescaled = rgb + + cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) + + moved = overlapping_crowns.translate(-minx + buffer, -miny + buffer) + scalingx = 1 / (data.transform[0]) + scalingy = -1 / (data.transform[4]) + moved_scaled = moved.scale(scalingx, scalingy, origin=(0, 0)) + impath = {"imagePath": out_path_root.with_suffix(out_path_root.suffix + ".png").as_posix()} + + try: + filename = out_path_root.with_suffix(out_path_root.suffix + ".geojson") + moved_scaled = overlapping_crowns.set_geometry(moved_scaled) + moved_scaled.to_file(driver="GeoJSON", filename=filename) + with open(filename, "r") as f: + shp = json.load(f) + shp.update(impath) + with open(filename, "w") as f: + json.dump(shp, f) + except ValueError: + print("Cannot write empty DataFrame to file.") + return + def tile_data_train( # noqa: C901 data: DatasetReader, out_dir: str, @@ -220,167 +311,16 @@ def tile_data_train( # noqa: C901 out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) tilename = Path(data.name).stem - crs = CRS.from_string(data.crs.wkt) - crs = crs.to_epsg() - # out_img, out_transform = mask(data, shapes=crowns.buffer(buffer), crop=True) - # Should start from data.bounds[0] + buffer, data.bounds[1] + buffer to avoid later complications - for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int): - for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int): - - out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" - - # Calculate the buffered tile dimensions - # tile_width_buffered = tile_width + 2 * buffer - # tile_height_buffered = tile_height + 2 * buffer - - # Calculate the bounding box coordinates with buffer - minx_buffered = minx - buffer - miny_buffered = miny - buffer - maxx_buffered = minx + tile_width + buffer - maxy_buffered = miny + tile_height + buffer - - # Create the affine transformation matrix for the tile - # transform = from_bounds(minx_buffered, miny_buffered, maxx_buffered, - # maxy_buffered, tile_width_buffered, tile_height_buffered) - - bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) - geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) - coords = get_features(geo) - - # Skip if insufficient coverage of crowns - good to have early on to save on unnecessary processing - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - # Warning: - # _crs_mismatch_warn - overlapping_crowns = gpd.clip(crowns, geo) - - # Ignore tiles with no crowns - if overlapping_crowns.empty: - continue - - # Discard tiles that do not have a sufficient coverage of training crowns - if (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: - continue - - # define the tile as a mask of the whole tiff with just the bounding box - out_img, out_transform = mask(data, shapes=coords, crop=True) - - # Discard scenes with many out-of-range pixels - out_sumbands = np.sum(out_img, 0) - zero_mask = np.where(out_sumbands == 0, 1, 0) - nan_mask = np.where(out_sumbands == 765, 1, 0) - sumzero = zero_mask.sum() - sumnan = nan_mask.sum() - totalpix = out_img.shape[1] * out_img.shape[2] - if sumzero > nan_threshold * totalpix: # reject tiles with many 0 cells - continue - elif sumnan > nan_threshold * totalpix: # reject tiles with many NaN cells - continue - - out_meta = data.meta.copy() - out_meta.update({ - "driver": "GTiff", - "height": out_img.shape[1], - "width": out_img.shape[2], - "transform": out_transform, - "nodata": None, - }) - # dtype needs to be unchanged for some data and set to uint8 for others to deal with black tiles - if dtype_bool: - out_meta.update({"dtype": "uint8"}) - - # Saving the tile as a new tiff, named by the origin of the tile. If tile appears blank in folder can show - # the image here and may need to fix RGB data or the dtype - out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") - with rasterio.open(out_tif, "w", **out_meta) as dest: - dest.write(out_img) - - # read in the tile we have just saved - clipped = rasterio.open(out_tif) - - # read it as an array - arr = clipped.read() - - # each band of the tiled tiff is a colour! - r = arr[0] - g = arr[1] - b = arr[2] - - # stack up the bands in an order appropriate for saving with cv2, then rescale to the correct 0-255 range - # for cv2. BGR ordering is correct for cv2 (and detectron2) - rgb = np.dstack((b, g, r)) - - # Some rasters need to have values rescaled to 0-255 - # TODO: more robust check - if np.max(g) > 255: - rgb_rescaled = 255 * rgb / 65535 - else: - # scale to image - rgb_rescaled = rgb - - # save this as png, named with the origin of the specific tile - # potentially bad practice - cv2.imwrite( - str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), - rgb_rescaled, - ) + crs = CRS.from_string(data.crs.wkt).to_epsg() + + tile_args = [ + (data, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) + for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) + ] - # select the crowns that intersect the non-buffered central - # section of the tile using the inner join - # TODO: A better solution would be to clip crowns to tile extent - # overlapping_crowns = sjoin(crowns, geo_central, how="inner") - # Maybe left join to keep information of crowns? - - overlapping_crowns = overlapping_crowns.explode(index_parts=True) - - # Translate to 0,0 to overlay on png - moved = overlapping_crowns.translate(-minx + buffer, -miny + buffer) - - # scale to deal with the resolution - scalingx = 1 / (data.transform[0]) - scalingy = -1 / (data.transform[4]) - moved_scaled = moved.scale(scalingx, scalingy, origin=(0, 0)) - - impath = {"imagePath": out_path_root.with_suffix(out_path_root.suffix + ".png").as_posix()} - - # Save as a geojson, a format compatible with detectron2, again named by the origin of the tile. - # If the box selected from the image is outside of the mapped region due to the image being on a slant - # then the shp file will have no info on the crowns and hence will create an empty gpd Dataframe. - # this causes an error so skip creating geojson. The training code will also ignore png so no problem. - try: - filename = out_path_root.with_suffix(out_path_root.suffix + ".geojson") - moved_scaled = overlapping_crowns.set_geometry(moved_scaled) - moved_scaled.to_file( - driver="GeoJSON", - filename=filename, - ) - with open(filename, "r") as f: - shp = json.load(f) - shp.update(impath) - with open(filename, "w") as f: - json.dump(shp, f) - except ValueError: - print("Cannot write empty DataFrame to file.") - continue - # Repeat and want to save crowns before being moved as overlap with lidar data to get the heights - # can try clean up the code here as lots of reprojecting and resaving but just going to get to - # work for now - out_geo_file = out_path_root.parts[-1] + "_geo" - out_path_geo = out_path / Path(out_geo_file) - try: - filename_unmoved = out_path_geo.with_suffix(out_path_geo.suffix + ".geojson") - overlapping_crowns.to_file( - driver="GeoJSON", - filename=filename_unmoved, - ) - with open(filename_unmoved, "r") as f: - shp = json.load(f) - shp.update(impath) - with open(filename_unmoved, "w") as f: - json.dump(shp, f) - except ValueError: - print("Cannot write empty DataFrame to file.") - continue + with concurrent.futures.ThreadPoolExecutor() as executor: + list(executor.map(lambda args: process_tile_train(*args), tile_args)) print("Tiling complete") From 2eda758be860f0eac0c8a74b79a87a19a32bff0b Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Wed, 10 Jul 2024 12:21:16 +0000 Subject: [PATCH 02/63] parallelise tiling --- detectree2/preprocessing/tiling.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 3a93bb20..321f0b09 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -166,12 +166,13 @@ def tile_data( print("Tiling complete") -def process_tile_train(data, +def process_tile_train( + data: DatasetReader, out_dir: str, - buffer, - tile_width, - tile_height, - dtype_bool, + buffer: int, + tile_width: int, + tile_height: int, + dtype_bool: bool, minx, miny, crs, From 7f2a6c7c2d240195f021cfd923b2343cc009e897 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Wed, 10 Jul 2024 12:23:34 +0000 Subject: [PATCH 03/63] update readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 2f07f770..e10c2e83 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,6 @@ Detectree2是一个基于Mask R-CNN的自动树冠检测与分割的Python包。 | | Code developed by James Ball, Seb Hickman, Thomas Koay, Oscar Jiang, Luran Wang, Panagiotis Ioannou, James Hinton and Matthew Archer in the [Forest Ecology and Conservation Group](https://coomeslab.org/) at the University of Cambridge. The Forest Ecology and Conservation Group is led by Professor David Coomes and is part of the University of Cambridge [Conservation Research Institute](https://www.conservation.cam.ac.uk/). | | :---: | :--- | -

-> [!NOTE] -> To save bandwidth, trained models have been moved to [Zenodo](https://zenodo.org/records/10522461). Download models directly with `wget` or equivalent. - ## Citation From 4e10abe92201bd3d46b4660c14166dc8dc89b8c2 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 14:54:32 +0000 Subject: [PATCH 04/63] test partial reading of raster --- detectree2/preprocessing/tiling.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 321f0b09..53af0671 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -6,6 +6,7 @@ import concurrent.futures import json +import logging import os import random import shutil @@ -21,6 +22,7 @@ from rasterio.crs import CRS from rasterio.io import DatasetReader from rasterio.mask import mask +from rasterio.windows import from_bounds from shapely.geometry import box # class img_data(DatasetReader): @@ -82,7 +84,18 @@ def process_tile( bbox = box(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer) geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) coords = get_features(geo) - out_img, out_transform = mask(data, shapes=coords, crop=True) + + # Create a window corresponding to the bounding box + window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) + + try: + out_img = data.read(window=window) + out_transform = data.window_transform(window) + except RasterioIOError as e: + logger.error(f"RasterioIOError while reading window {window}: {e}") + return + + #out_img, out_transform = mask(data, shapes=coords, crop=True) out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) @@ -218,7 +231,19 @@ def process_tile_train( if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: return - out_img, out_transform = mask(data, shapes=coords, crop=True) + # Create a window corresponding to the bounding box + window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) + + try: + out_img = data.read(window=window) + out_transform = data.window_transform(window) + except RasterioIOError as e: + logger.error(f"RasterioIOError while reading window {window}: {e}") + return + + + #out_img, out_transform = mask(data, shapes=coords, crop=True) + out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) nan_mask = np.where(out_sumbands == 765, 1, 0) From 1ded0d856a6c40506e5db9e65d50532708a47e25 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 15:03:22 +0000 Subject: [PATCH 05/63] test partial reading of raster --- detectree2/preprocessing/tiling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 53af0671..7d9a3da5 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -20,6 +20,7 @@ import rasterio from fiona.crs import from_epsg # noqa: F401 from rasterio.crs import CRS +from rasterio.errors import RasterioIOError from rasterio.io import DatasetReader from rasterio.mask import mask from rasterio.windows import from_bounds From 7c421407fca5d1f9e379e6bddd211732e4b8ac4a Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 15:11:42 +0000 Subject: [PATCH 06/63] add logger for tiling --- detectree2/preprocessing/tiling.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 7d9a3da5..eb043066 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -26,6 +26,10 @@ from rasterio.windows import from_bounds from shapely.geometry import box +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + # class img_data(DatasetReader): # """ # Class for image data to be processed for tiling From aa8ab37d622389924394d1f61b0c5007c872fdcc Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 16:23:50 +0000 Subject: [PATCH 07/63] change back to masking --- detectree2/preprocessing/tiling.py | 35 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index eb043066..94263d80 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -91,16 +91,16 @@ def process_tile( coords = get_features(geo) # Create a window corresponding to the bounding box - window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) + #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - try: - out_img = data.read(window=window) - out_transform = data.window_transform(window) - except RasterioIOError as e: - logger.error(f"RasterioIOError while reading window {window}: {e}") - return + #try: + # out_img = data.read(window=window) + # out_transform = data.window_transform(window) + #except RasterioIOError as e: + # logger.error(f"RasterioIOError while reading window {window}: {e}") + # return - #out_img, out_transform = mask(data, shapes=coords, crop=True) + out_img, out_transform = mask(data, shapes=coords, crop=True) out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) @@ -237,17 +237,16 @@ def process_tile_train( return # Create a window corresponding to the bounding box - window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) + #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - try: - out_img = data.read(window=window) - out_transform = data.window_transform(window) - except RasterioIOError as e: - logger.error(f"RasterioIOError while reading window {window}: {e}") - return - - - #out_img, out_transform = mask(data, shapes=coords, crop=True) + #try: + # out_img = data.read(window=window) + # out_transform = data.window_transform(window) + #except RasterioIOError as e: + # logger.error(f"RasterioIOError while reading window {window}: {e}") + # return + + out_img, out_transform = mask(data, shapes=coords, crop=True) out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) From 5320a50c8648adb978cc68edab64173aa11a9b28 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 16:38:06 +0000 Subject: [PATCH 08/63] change back to masking --- detectree2/preprocessing/tiling.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 94263d80..55ce0d9d 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -93,14 +93,13 @@ def process_tile( # Create a window corresponding to the bounding box #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - #try: - # out_img = data.read(window=window) - # out_transform = data.window_transform(window) - #except RasterioIOError as e: - # logger.error(f"RasterioIOError while reading window {window}: {e}") - # return + try: + out_img, out_transform = mask(data, shapes=coords, crop=True) + except RasterioIOError as e: + logger.error(f"RasterioIOError while reading window {window}: {e}") + return - out_img, out_transform = mask(data, shapes=coords, crop=True) + #out_img, out_transform = mask(data, shapes=coords, crop=True) out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) @@ -239,14 +238,13 @@ def process_tile_train( # Create a window corresponding to the bounding box #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - #try: - # out_img = data.read(window=window) - # out_transform = data.window_transform(window) - #except RasterioIOError as e: - # logger.error(f"RasterioIOError while reading window {window}: {e}") - # return - - out_img, out_transform = mask(data, shapes=coords, crop=True) + try: + out_img, out_transform = mask(data, shapes=coords, crop=True) + except RasterioIOError as e: + logger.error(f"RasterioIOError while reading window {window}: {e}") + return + + #out_img, out_transform = mask(data, shapes=coords, crop=True) out_sumbands = np.sum(out_img, 0) zero_mask = np.where(out_sumbands == 0, 1, 0) From 8e5c68ac1665a11f753893502908886f28e76224 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 16:48:37 +0000 Subject: [PATCH 09/63] change back to masking --- detectree2/preprocessing/tiling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 55ce0d9d..2a00cd25 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -96,7 +96,7 @@ def process_tile( try: out_img, out_transform = mask(data, shapes=coords, crop=True) except RasterioIOError as e: - logger.error(f"RasterioIOError while reading window {window}: {e}") + logger.error(f"RasterioIOError: {e}") return #out_img, out_transform = mask(data, shapes=coords, crop=True) @@ -241,7 +241,7 @@ def process_tile_train( try: out_img, out_transform = mask(data, shapes=coords, crop=True) except RasterioIOError as e: - logger.error(f"RasterioIOError while reading window {window}: {e}") + logger.error(f"RasterioIOError: {e}") return #out_img, out_transform = mask(data, shapes=coords, crop=True) From 3fd57995c61ac4f68c99fcf9ab6dd3c649a0c847 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 17:24:27 +0000 Subject: [PATCH 10/63] img_path instead of reader for multithread --- detectree2/preprocessing/tiling.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 2a00cd25..f89a195a 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -96,7 +96,7 @@ def process_tile( try: out_img, out_transform = mask(data, shapes=coords, crop=True) except RasterioIOError as e: - logger.error(f"RasterioIOError: {e}") + logger.error(f"RasterioIOError while applying mask {coords}: {e}") return #out_img, out_transform = mask(data, shapes=coords, crop=True) @@ -184,7 +184,7 @@ def tile_data( def process_tile_train( - data: DatasetReader, + img_path: str, out_dir: str, buffer: int, tile_width: int, @@ -218,6 +218,7 @@ def process_tile_train( Returns: None """ + data = rasterio.open(img_path) out_path = Path(out_dir) out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" @@ -241,7 +242,7 @@ def process_tile_train( try: out_img, out_transform = mask(data, shapes=coords, crop=True) except RasterioIOError as e: - logger.error(f"RasterioIOError: {e}") + logger.error(f"RasterioIOError while applying mask {coords}: {e}") return #out_img, out_transform = mask(data, shapes=coords, crop=True) @@ -304,7 +305,7 @@ def process_tile_train( return def tile_data_train( # noqa: C901 - data: DatasetReader, + img_path: str, out_dir: str, buffer: int = 30, tile_width: int = 200, @@ -342,7 +343,7 @@ def tile_data_train( # noqa: C901 crs = CRS.from_string(data.crs.wkt).to_epsg() tile_args = [ - (data, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) ] From 93f2c1802f213423a2bd3d405dd886b30913efc8 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 17:27:32 +0000 Subject: [PATCH 11/63] img_path instead of reader for multithread --- detectree2/preprocessing/tiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index f89a195a..5479570f 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -339,7 +339,7 @@ def tile_data_train( # noqa: C901 # TODO: Tighten up epsg handling out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) - tilename = Path(data.name).stem + tilename = Path(img_path).stem crs = CRS.from_string(data.crs.wkt).to_epsg() tile_args = [ From 617b733f7d35ac3a8f7e9281897bea000c2c1cbb Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 17:33:11 +0000 Subject: [PATCH 12/63] img_path instead of reader for multithread --- detectree2/preprocessing/tiling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 5479570f..271e066c 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -340,6 +340,7 @@ def tile_data_train( # noqa: C901 out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) tilename = Path(img_path).stem + data = rasterio.open(img_path) crs = CRS.from_string(data.crs.wkt).to_epsg() tile_args = [ From 24b5d74438d9e0ac4675b3c773b05a0a84497ec8 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 17:52:26 +0000 Subject: [PATCH 13/63] explode reintroduced --- detectree2/preprocessing/tiling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 271e066c..35b0f71e 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -285,6 +285,7 @@ def process_tile_train( cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) + overlapping_crowns = overlapping_crowns.explode(index_parts=True) moved = overlapping_crowns.translate(-minx + buffer, -miny + buffer) scalingx = 1 / (data.transform[0]) scalingy = -1 / (data.transform[4]) From bb63e3c04f6c8b2019d3e28cdae0489c1cd6b29e Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Tue, 16 Jul 2024 18:43:19 +0000 Subject: [PATCH 14/63] refactor tile processing --- detectree2/preprocessing/tiling.py | 101 ++++++++--------------------- 1 file changed, 26 insertions(+), 75 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 35b0f71e..c51a1f7d 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -56,7 +56,7 @@ def get_features(gdf: gpd.GeoDataFrame): def process_tile( - data: DatasetReader, + img_path: str, out_dir: str, buffer: int, tile_width: int, @@ -65,8 +65,11 @@ def process_tile( minx, miny, crs, - tilename -) -> None: + tilename, + crowns: gpd.GeoDataFrame = None, + threshold: float = 0, + nan_threshold: float = 0, +): """Process a single tile for making predictions. Args: @@ -84,15 +87,25 @@ def process_tile( Returns: None """ + data = rasterio.open(img_path) + out_path = Path(out_dir) out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" - bbox = box(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer) + + minx_buffered = minx - buffer + miny_buffered = miny - buffer + maxx_buffered = minx + tile_width + buffer + maxy_buffered = miny + tile_height + buffer + + bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) coords = get_features(geo) - # Create a window corresponding to the bounding box - #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - + if crowns is not None: + overlapping_crowns = gpd.clip(crowns, geo) + if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: + return + try: out_img, out_transform = mask(data, shapes=coords, crop=True) except RasterioIOError as e: @@ -109,7 +122,7 @@ def process_tile( totalpix = out_img.shape[1] * out_img.shape[2] # If the tile is mostly empty or mostly nan, don't save it - if sumzero > 0.25 * totalpix or sumnan > 0.25 * totalpix: + if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: return out_meta = data.meta.copy() @@ -139,6 +152,9 @@ def process_tile( rgb_rescaled = rgb cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) + if overlapping_crowns is not None: + return data, out_path_root, overlapping_crowns, minx, miny, buffer + def tile_data( @@ -194,7 +210,7 @@ def process_tile_train( miny, crs, tilename, - crowns, + crowns: gpd.GeoDataFrame, threshold, nan_threshold ) -> None: @@ -218,72 +234,7 @@ def process_tile_train( Returns: None """ - data = rasterio.open(img_path) - - out_path = Path(out_dir) - out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" - - minx_buffered = minx - buffer - miny_buffered = miny - buffer - maxx_buffered = minx + tile_width + buffer - maxy_buffered = miny + tile_height + buffer - - bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) - geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) - coords = get_features(geo) - - overlapping_crowns = gpd.clip(crowns, geo) - if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: - return - - # Create a window corresponding to the bounding box - #window = from_bounds(minx - buffer, miny - buffer, minx + tile_width + buffer, miny + tile_height + buffer, data.transform) - - try: - out_img, out_transform = mask(data, shapes=coords, crop=True) - except RasterioIOError as e: - logger.error(f"RasterioIOError while applying mask {coords}: {e}") - return - - #out_img, out_transform = mask(data, shapes=coords, crop=True) - - out_sumbands = np.sum(out_img, 0) - zero_mask = np.where(out_sumbands == 0, 1, 0) - nan_mask = np.where(out_sumbands == 765, 1, 0) - sumzero = zero_mask.sum() - sumnan = nan_mask.sum() - totalpix = out_img.shape[1] * out_img.shape[2] - # If the tile is mostly empty or mostly nan, don't save it - if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: - return - - out_meta = data.meta.copy() - out_meta.update({ - "driver": "GTiff", - "height": out_img.shape[1], - "width": out_img.shape[2], - "transform": out_transform, - "nodata": None, - }) - if dtype_bool: - out_meta.update({"dtype": "uint8"}) - - out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") - with rasterio.open(out_tif, "w", **out_meta) as dest: - dest.write(out_img) - - clipped = rasterio.open(out_tif) - arr = clipped.read() - r, g, b = arr[0], arr[1], arr[2] - rgb = np.dstack((b, g, r)) - - # Rescale if necessary - if np.max(g) > 255: - rgb_rescaled = 255 * rgb / 65535 - else: - rgb_rescaled = rgb - - cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) + data, out_path_root, overlapping_crowns, minx, miny, buffer = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) overlapping_crowns = overlapping_crowns.explode(index_parts=True) moved = overlapping_crowns.translate(-minx + buffer, -miny + buffer) From b42291649b0766ac4d374480f1e982d223e6498c Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Wed, 17 Jul 2024 10:34:56 +0000 Subject: [PATCH 15/63] refactor tiling --- detectree2/preprocessing/tiling.py | 96 ++++++++++++++++-------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index c51a1f7d..d8940c7a 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -73,7 +73,7 @@ def process_tile( """Process a single tile for making predictions. Args: - data: Orthomosaic as a rasterio object in a UTM type projection + img_path: Path to the orthomosaic out_dir: Output directory buffer: Overlapping buffer of tiles in meters (UTM) tile_width: Tile width in meters @@ -100,60 +100,64 @@ def process_tile( bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) coords = get_features(geo) - + + overlapping_crowns = None if crowns is not None: overlapping_crowns = gpd.clip(crowns, geo) if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: return try: - out_img, out_transform = mask(data, shapes=coords, crop=True) - except RasterioIOError as e: - logger.error(f"RasterioIOError while applying mask {coords}: {e}") - return - - #out_img, out_transform = mask(data, shapes=coords, crop=True) + with rasterio.open(img_path) as data: + out_img, out_transform = mask(data, shapes=coords, crop=True) + + out_sumbands = np.sum(out_img, axis=0) + zero_mask = np.where(out_sumbands == 0, 1, 0) + nan_mask = np.where(out_sumbands == 765, 1, 0) + sumzero = zero_mask.sum() + sumnan = nan_mask.sum() + totalpix = out_img.shape[1] * out_img.shape[2] + + # If the tile is mostly empty or mostly nan, don't save it + if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: + return None + + out_meta = data.meta.copy() + out_meta.update({ + "driver": "GTiff", + "height": out_img.shape[1], + "width": out_img.shape[2], + "transform": out_transform, + "nodata": None, + }) + if dtype_bool: + out_meta.update({"dtype": "uint8"}) + + out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") + with rasterio.open(out_tif, "w", **out_meta) as dest: + dest.write(out_img) + + with rasterio.open(out_tif) as clipped: + arr = clipped.read() + r, g, b = arr[0], arr[1], arr[2] + rgb = np.dstack((b, g, r)) + + # Rescale to 0-255 if necessary + if np.max(g) > 255: + rgb_rescaled = 255 * rgb / 65535 + else: + rgb_rescaled = rgb - out_sumbands = np.sum(out_img, 0) - zero_mask = np.where(out_sumbands == 0, 1, 0) - nan_mask = np.where(out_sumbands == 765, 1, 0) - sumzero = zero_mask.sum() - sumnan = nan_mask.sum() - totalpix = out_img.shape[1] * out_img.shape[2] + cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) - # If the tile is mostly empty or mostly nan, don't save it - if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: - return + if overlapping_crowns is not None: + return data, out_path_root, overlapping_crowns, minx, miny, buffer + + return data, out_path_root, None, minx, miny, buffer - out_meta = data.meta.copy() - out_meta.update({ - "driver": "GTiff", - "height": out_img.shape[1], - "width": out_img.shape[2], - "transform": out_transform, - "nodata": None, - }) - if dtype_bool: - out_meta.update({"dtype": "uint8"}) - - out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") - with rasterio.open(out_tif, "w", **out_meta) as dest: - dest.write(out_img) - - clipped = rasterio.open(out_tif) - arr = clipped.read() - r, g, b = arr[0], arr[1], arr[2] - rgb = np.dstack((b, g, r)) - - # Rescale to 0-255 if necessary - if np.max(g) > 255: - rgb_rescaled = 255 * rgb / 65535 - else: - rgb_rescaled = rgb - - cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) - if overlapping_crowns is not None: - return data, out_path_root, overlapping_crowns, minx, miny, buffer + except RasterioIOError as e: + logger.error(f"RasterioIOError while applying mask {coords}: {e}") + return None From c2267505f31ca3956d3c1ba17ffcd1faf23076f3 Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Wed, 17 Jul 2024 10:43:34 +0000 Subject: [PATCH 16/63] refactor tiling --- detectree2/preprocessing/tiling.py | 218 ++++++++++++----------------- 1 file changed, 92 insertions(+), 126 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index d8940c7a..646d2fe8 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -87,120 +87,78 @@ def process_tile( Returns: None """ - data = rasterio.open(img_path) - - out_path = Path(out_dir) - out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" + try: + with rasterio.open(img_path) as data: + out_path = Path(out_dir) + out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" - minx_buffered = minx - buffer - miny_buffered = miny - buffer - maxx_buffered = minx + tile_width + buffer - maxy_buffered = miny + tile_height + buffer + minx_buffered = minx - buffer + miny_buffered = miny - buffer + maxx_buffered = minx + tile_width + buffer + maxy_buffered = miny + tile_height + buffer - bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) - geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) - coords = get_features(geo) + bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) + geo = gpd.GeoDataFrame({"geometry": bbox}, index=[0], crs=data.crs) + coords = get_features(geo) - overlapping_crowns = None - if crowns is not None: - overlapping_crowns = gpd.clip(crowns, geo) - if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: - return + overlapping_crowns = None + if crowns is not None: + overlapping_crowns = gpd.clip(crowns, geo) + if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: + return None - try: - with rasterio.open(img_path) as data: out_img, out_transform = mask(data, shapes=coords, crop=True) - out_sumbands = np.sum(out_img, axis=0) - zero_mask = np.where(out_sumbands == 0, 1, 0) - nan_mask = np.where(out_sumbands == 765, 1, 0) - sumzero = zero_mask.sum() - sumnan = nan_mask.sum() - totalpix = out_img.shape[1] * out_img.shape[2] - - # If the tile is mostly empty or mostly nan, don't save it - if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: - return None - - out_meta = data.meta.copy() - out_meta.update({ - "driver": "GTiff", - "height": out_img.shape[1], - "width": out_img.shape[2], - "transform": out_transform, - "nodata": None, - }) - if dtype_bool: - out_meta.update({"dtype": "uint8"}) - - out_tif = out_path_root.with_suffix(out_path_root.suffix + ".tif") - with rasterio.open(out_tif, "w", **out_meta) as dest: - dest.write(out_img) - - with rasterio.open(out_tif) as clipped: - arr = clipped.read() - r, g, b = arr[0], arr[1], arr[2] - rgb = np.dstack((b, g, r)) - - # Rescale to 0-255 if necessary - if np.max(g) > 255: - rgb_rescaled = 255 * rgb / 65535 - else: - rgb_rescaled = rgb - - cv2.imwrite(str(out_path_root.with_suffix(out_path_root.suffix + ".png").resolve()), rgb_rescaled) - - if overlapping_crowns is not None: - return data, out_path_root, overlapping_crowns, minx, miny, buffer - - return data, out_path_root, None, minx, miny, buffer + out_sumbands = np.sum(out_img, axis=0) + zero_mask = np.where(out_sumbands == 0, 1, 0) + nan_mask = np.where(out_sumbands == 765, 1, 0) + sumzero = zero_mask.sum() + sumnan = nan_mask.sum() + totalpix = out_img.shape[1] * out_img.shape[2] + + # If the tile is mostly empty or mostly nan, don't save it + if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: + return None + + out_meta = data.meta.copy() + out_meta.update({ + "driver": "GTiff", + "height": out_img.shape[1], + "width": out_img.shape[2], + "transform": out_transform, + "nodata": None, + }) + if dtype_bool: + out_meta.update({"dtype": "uint8"}) + + out_tif = out_path_root.with_suffix(".tif") + with rasterio.open(out_tif, "w", **out_meta) as dest: + dest.write(out_img) + + with rasterio.open(out_tif) as clipped: + arr = clipped.read() + r, g, b = arr[0], arr[1], arr[2] + rgb = np.dstack((b, g, r)) + + # Rescale to 0-255 if necessary + if np.max(g) > 255: + rgb_rescaled = 255 * rgb / 65535 + else: + rgb_rescaled = rgb + + cv2.imwrite(str(out_path_root.with_suffix(".png").resolve()), rgb_rescaled) + + if overlapping_crowns is not None: + return data, out_path_root, overlapping_crowns, minx, miny, buffer + + return data, out_path_root, None, minx, miny, buffer except RasterioIOError as e: logger.error(f"RasterioIOError while applying mask {coords}: {e}") return None - - - -def tile_data( - data: DatasetReader, - out_dir: str, - buffer: int = 30, - tile_width: int = 200, - tile_height: int = 200, - dtype_bool: bool = False, -) -> None: - """Tiles up orthomosaic for making predictions on. - - Tiles up full othomosaic into managable chunks to make predictions on. Use tile_data_train to generate tiled - training data. A bug exists on some input raster types whereby outputed tiles are completely black - the dtype_bool - argument should be switched if this is the case. - - Args: - data: Orthomosaic as a rasterio object in a UTM type projection - buffer: Overlapping buffer of tiles in meters (UTM) - tile_width: Tile width in meters - tile_height: Tile height in meters - dtype_bool: Flag to edit dtype to prevent black tiles - - Returns: - None - """ - out_path = Path(out_dir) - os.makedirs(out_path, exist_ok=True) - crs = CRS.from_string(data.crs.wkt).to_epsg() - tilename = Path(data.name).stem - total_tiles = int(((data.bounds[2] - data.bounds[0]) / tile_width) * ((data.bounds[3] - data.bounds[1]) / tile_height)) - - tile_args = [ - (data, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename) - for minx in np.arange(data.bounds[0], data.bounds[2] - tile_width, tile_width, int) - for miny in np.arange(data.bounds[1], data.bounds[3] - tile_height, tile_height, int) - ] - - with concurrent.futures.ThreadPoolExecutor() as executor: - list(executor.map(lambda args: process_tile(*args), tile_args)) - - print("Tiling complete") + except Exception as e: + logger.error(f"Error processing tile {tilename} at ({minx}, {miny}): {e}") + return None def process_tile_train( @@ -221,7 +179,7 @@ def process_tile_train( """Process a single tile for training data. Args: - data: Orthomosaic as a rasterio object in a UTM type projection + img_path: Path to the orthomosaic out_dir: Output directory buffer: Overlapping buffer of tiles in meters (UTM) tile_width: Tile width in meters @@ -238,17 +196,23 @@ def process_tile_train( Returns: None """ - data, out_path_root, overlapping_crowns, minx, miny, buffer = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + + if result is None: + #logger.warning(f"Skipping tile at ({minx}, {miny}) due to insufficient data.") + return + + data, out_path_root, overlapping_crowns, minx, miny, buffer = result overlapping_crowns = overlapping_crowns.explode(index_parts=True) moved = overlapping_crowns.translate(-minx + buffer, -miny + buffer) scalingx = 1 / (data.transform[0]) scalingy = -1 / (data.transform[4]) moved_scaled = moved.scale(scalingx, scalingy, origin=(0, 0)) - impath = {"imagePath": out_path_root.with_suffix(out_path_root.suffix + ".png").as_posix()} + impath = {"imagePath": out_path_root.with_suffix(".png").as_posix()} try: - filename = out_path_root.with_suffix(out_path_root.suffix + ".geojson") + filename = out_path_root.with_suffix(".geojson") moved_scaled = overlapping_crowns.set_geometry(moved_scaled) moved_scaled.to_file(driver="GeoJSON", filename=filename) with open(filename, "r") as f: @@ -257,10 +221,14 @@ def process_tile_train( with open(filename, "w") as f: json.dump(shp, f) except ValueError: - print("Cannot write empty DataFrame to file.") + logger.warning("Cannot write empty DataFrame to file.") return -def tile_data_train( # noqa: C901 +# Define a top-level helper function +def process_tile_train_helper(args): + return process_tile_train(*args) + +def tile_data( img_path: str, out_dir: str, buffer: int = 30, @@ -271,13 +239,14 @@ def tile_data_train( # noqa: C901 nan_threshold: float = 0.1, dtype_bool: bool = False, ) -> None: - """Tiles up orthomosaic and corresponding crowns into training tiles. + """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. A threshold can be used to ensure a good coverage of crowns across a tile. Tiles that do not have sufficient coverage are rejected. Args: - data: Orthomosaic as a rasterio object in a UTM type projection + img_path: Path to the orthomosaic + out_dir: Output directory buffer: Overlapping buffer of tiles in meters (UTM) tile_width: Tile width in meters tile_height: Tile height in meters @@ -290,25 +259,22 @@ def tile_data_train( # noqa: C901 None """ - - # TODO: Clip data to crowns straight away to speed things up - # TODO: Tighten up epsg handling out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) tilename = Path(img_path).stem - data = rasterio.open(img_path) - crs = CRS.from_string(data.crs.wkt).to_epsg() + with rasterio.open(img_path) as data: + crs = data.crs.to_string() # Update CRS handling to avoid deprecated syntax - tile_args = [ - (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) - for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) - for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) - ] + tile_args = [ + (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) + for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) + ] - with concurrent.futures.ThreadPoolExecutor() as executor: - list(executor.map(lambda args: process_tile_train(*args), tile_args)) + with concurrent.futures.ProcessPoolExecutor() as executor: # Use ProcessPoolExecutor here + list(executor.map(process_tile_train_helper, tile_args)) - print("Tiling complete") + logger.info("Tiling complete") def image_details(fileroot): @@ -490,5 +456,5 @@ def to_traintest_folders( # noqa: C901 tile_width = 200 tile_height = 200 - tile_data_train(data, out_dir, buffer, tile_width, tile_height, crowns) + tile_data(data, out_dir, buffer, tile_width, tile_height, crowns) to_traintest_folders(folds=5) From eb163bec959fd4458d80fa6866538113ef50655e Mon Sep 17 00:00:00 2001 From: Ball JGC Date: Thu, 8 Aug 2024 12:56:24 +0000 Subject: [PATCH 17/63] ms tiling --- detectree2/preprocessing/tiling.py | 128 +++++++++++++++++++++++++++-- docs/source/tutorial.rst | 31 +++---- 2 files changed, 139 insertions(+), 20 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 646d2fe8..46510cc8 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -160,6 +160,112 @@ def process_tile( logger.error(f"Error processing tile {tilename} at ({minx}, {miny}): {e}") return None +def process_tile_ms( + img_path: str, + out_dir: str, + buffer: int, + tile_width: int, + tile_height: int, + dtype_bool: bool, + minx, + miny, + crs, + tilename, + crowns: gpd.GeoDataFrame = None, + threshold: float = 0, + nan_threshold: float = 0, +): + """Process a single tile for making predictions. + + Args: + img_path: Path to the orthomosaic + out_dir: Output directory + buffer: Overlapping buffer of tiles in meters (UTM) + tile_width: Tile width in meters + tile_height: Tile height in meters + dtype_bool: Flag to edit dtype to prevent black tiles + minx: Minimum x coordinate of tile + miny: Minimum y coordinate of tile + crs: Coordinate reference system + tilename: Name of the tile + + Returns: + None + """ + try: + with rasterio.open(img_path) as data: + out_path = Path(out_dir) + out_path_root = out_path / f"{tilename}_{minx}_{miny}_{tile_width}_{buffer}_{crs}" + + minx_buffered = minx - buffer + miny_buffered = miny - buffer + maxx_buffered = minx + tile_width + buffer + maxy_buffered = miny + tile_height + buffer + + bbox = box(minx_buffered, miny_buffered, maxx_buffered, maxy_buffered) + geo = gpd.GeoDataFrame({"geometry": [bbox]}, index=[0], crs=data.crs) + coords = [geo.geometry[0].__geo_interface__] + + overlapping_crowns = None + if crowns is not None: + overlapping_crowns = gpd.clip(crowns, geo) + if overlapping_crowns.empty or (overlapping_crowns.dissolve().area[0] / geo.area[0]) < threshold: + return None + + out_img, out_transform = mask(data, shapes=coords, crop=True) + + out_sumbands = np.sum(out_img, axis=0) + zero_mask = np.where(out_sumbands == 0, 1, 0) + nan_mask = np.isnan(out_sumbands) + sumzero = zero_mask.sum() + sumnan = nan_mask.sum() + totalpix = out_img.shape[1] * out_img.shape[2] + + # If the tile is mostly empty or mostly nan, don't save it + if sumzero > nan_threshold * totalpix or sumnan > nan_threshold * totalpix: + return None + + out_meta = data.meta.copy() + out_meta.update({ + "driver": "GTiff", + "height": out_img.shape[1], + "width": out_img.shape[2], + "transform": out_transform, + "nodata": None, + }) + if dtype_bool: + out_meta.update({"dtype": "uint8"}) + + out_tif = out_path_root.with_suffix(".tif") + with rasterio.open(out_tif, "w", **out_meta) as dest: + dest.write(out_img) + + # Save all bands as an image if needed (not just the first 3 bands) + band_images = [] + for band_index in range(out_img.shape[0]): + band_image = out_img[band_index, :, :] + if np.max(band_image) > 255: + band_image = 255 * band_image / np.max(band_image) + band_images.append(band_image.astype(np.uint8)) + + # Stack the bands into a single image array + full_image = np.stack(band_images, axis=-1) + + # Save the full image with potentially more than 3 bands + full_image_path = out_path_root.with_suffix(".png") + cv2.imwrite(str(full_image_path.resolve()), full_image) + + if overlapping_crowns is not None: + return data, out_path_root, overlapping_crowns, minx, miny, buffer + + return data, out_path_root, None, minx, miny, buffer + + except RasterioIOError as e: + logger.error(f"RasterioIOError while applying mask {coords}: {e}") + return None + except Exception as e: + logger.error(f"Error processing tile {tilename} at ({minx}, {miny}): {e}") + return None def process_tile_train( img_path: str, @@ -174,7 +280,8 @@ def process_tile_train( tilename, crowns: gpd.GeoDataFrame, threshold, - nan_threshold + nan_threshold, + mode: str = "rgb", ) -> None: """Process a single tile for training data. @@ -196,7 +303,12 @@ def process_tile_train( Returns: None """ - result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + if mode == "rgb": + result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, + crowns, threshold, nan_threshold) + elif mode == "ms": + result = process_tile_ms(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, + crowns, threshold, nan_threshold) if result is None: #logger.warning(f"Skipping tile at ({minx}, {miny}) due to insufficient data.") @@ -241,8 +353,10 @@ def tile_data( ) -> None: """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. - A threshold can be used to ensure a good coverage of crowns across a tile. Tiles that do not have sufficient - coverage are rejected. + Tiles up large rasters into managable tiles for training and prediction. If crowns are not supplied the function + will tile up the entire landscape for prediction. If crowns are supplied the function will tile these with the image + and skip tiles without a minimum coverage of crowns. The 'threshold' can be varied to ensure a good coverage of + crowns across a traing tile. Tiles that do not have sufficient coverage are skipped. Args: img_path: Path to the orthomosaic @@ -266,9 +380,11 @@ def tile_data( crs = data.crs.to_string() # Update CRS handling to avoid deprecated syntax tile_args = [ - (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) + (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, + threshold, nan_threshold) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) - for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) + for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, + int) ] with concurrent.futures.ProcessPoolExecutor() as executor: # Use ProcessPoolExecutor here diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 1af9c89a..5095033f 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -69,7 +69,7 @@ We call functions to from ``detectree2``'s tiling and training modules. .. code-block:: python - from detectree2.preprocessing.tiling import tile_data_train, to_traintest_folders + from detectree2.preprocessing.tiling import tile_data, to_traintest_folders from detectree2.models.train import register_train_data, MyTrainer, setup_cfg import rasterio import geopandas as gpd @@ -112,22 +112,24 @@ The tile size will depend on: The total tile size here is 100 m x 100 m (a 40 m x 40 m core area with a surrounding 30 m buffer that overlaps with surrounding tiles). Including a buffer is recommended as it allows for tiles that include more training crowns. -Next we tile the data. The ``tile_data_train`` function will only retain tiles that contain more than the given -``threshold`` coverage of training data (here 60%). This helps to reduce the chance that the network is trained with -tiles that contain a large number of unlabelled crowns (which would reduce its sensitivity). +Next we tile the data. The ``tile_data`` function, when ``crowns`` is supplied, will only retain tiles that contain more +than the given ``threshold`` coverage of training data (here 60%). This helps to reduce the chance that the network is +trained with tiles that contain a large number of unlabelled crowns (which would reduce its sensitivity). .. code-block:: python - tile_data_train(data, out_dir, buffer, tile_width, tile_height, crowns, threshold) + tile_data(img_path, out_dir, buffer, tile_width, tile_height, crowns, threshold) .. warning:: - If tiles are outputing as blank images set ``dtype_bool = True`` in the ``tile_data_train`` function. This is a bug - and we are working on fixing it. + If tiles are outputing as blank images set ``dtype_bool = True`` in the ``tile_data`` function. This is a bug + and we are working on fixing it. Supplying crown polygons will cause the function to tile for + training (as opposed to landscape prediction which is described below). .. note:: - You will want to relax the ``threshold`` value if your trees are sparsely distributed across your landscape. - Remember, ``detectree2`` was initially designed for dense, closed canopy forests so some of the default assumptions - will reflect that. + You will want to relax the ``threshold`` value if your trees are sparsely distributed across your landscape or if you + want to include non-forest areas (e.g. river, roads). Remember, ``detectree2`` was initially designed for dense, + closed canopy forests so some of the default assumptions will reflect that and parameters will need to be adjusted + for different systems. Send geojsons to train folder (with sub-folders for k-fold cross validation) and test folder. @@ -141,7 +143,7 @@ Send geojsons to train folder (with sub-folders for k-fold cross validation) and that have any overlap with test tiles (including the buffers), ensuring strict spatial separation of the test data. However, this can remove a significant proportion of the data available to train on so if validation accuracy is a sufficient test of model performance ``test_frac`` can be set to ``0`` or set ``strict=False`` (which allows for - some overlap in the buffers between test and train/val tiles). + overlap in the buffers between test and train/val tiles). The data has now been tiled and partitioned for model training, tuning and evaluation. @@ -161,8 +163,8 @@ The data has now been tiled and partitioned for model training, tuning and evalu └── test (test data folder) -It is advisable to do a visual inspection on the tiles to ensure that the tiling has worked as expected and that crowns -and images align. This can be done quickly with the inbuilt ``detectron2`` visualisation tools. +It is recommended to visually inspect the tiles before training to ensure that the tiling has worked as expected and +that crowns and images align. This can be done quickly with the inbuilt ``detectron2`` visualisation tools. .. code-block:: python @@ -325,7 +327,8 @@ can discard partial the crowns predicted at the edge of tiles. .. warning:: If tiles are outputing as blank images set ``dtype_bool = True`` in the ``tile_data`` function. This is a bug - and we are working on fixing it. + and we are working on fixing it. Avoid supplying crown polygons otherwise the function will run as if it is tiling + for training. To download a pre-trained model from the ``model_garden`` you can run ``wget`` on the package repo From 6441ae7eb54e90a6bd960465deda58bbfc362c2f Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 9 Aug 2024 13:51:10 +0100 Subject: [PATCH 18/63] tiling mode added --- detectree2/preprocessing/tiling.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 46510cc8..502982f7 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -138,7 +138,7 @@ def process_tile( with rasterio.open(out_tif) as clipped: arr = clipped.read() r, g, b = arr[0], arr[1], arr[2] - rgb = np.dstack((b, g, r)) + rgb = np.dstack((b, g, r)) # Reorder for cv2 (BGRA) # Rescale to 0-255 if necessary if np.max(g) > 255: @@ -249,6 +249,7 @@ def process_tile_ms( band_images.append(band_image.astype(np.uint8)) # Stack the bands into a single image array + # Does the band order need to be reversed as in the RGB case? full_image = np.stack(band_images, axis=-1) # Save the full image with potentially more than 3 bands @@ -350,6 +351,7 @@ def tile_data( threshold: float = 0, nan_threshold: float = 0.1, dtype_bool: bool = False, + mode: str = "rgb", ) -> None: """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. @@ -381,7 +383,7 @@ def tile_data( tile_args = [ (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, - threshold, nan_threshold) + threshold, nan_threshold, mode) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) From dc8e0a3541a085c3a818d19e9c666ddb2ea65343 Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 9 Aug 2024 14:19:02 +0100 Subject: [PATCH 19/63] remove lfs --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index ec4a626f..e69de29b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +0,0 @@ -*.pth filter=lfs diff=lfs merge=lfs -text From e71135ea746d649bc4bf34590ca4aa1bff95b0d0 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 20 Aug 2024 15:37:48 +0100 Subject: [PATCH 20/63] ms bands --- detectree2/data_loading/custom.py | 73 ++++++++++++++++++++++++++++++ detectree2/models/train.py | 47 +++++++++++++++++-- detectree2/preprocessing/tiling.py | 28 ++++++++---- 3 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 detectree2/data_loading/custom.py diff --git a/detectree2/data_loading/custom.py b/detectree2/data_loading/custom.py new file mode 100644 index 00000000..c1e37c82 --- /dev/null +++ b/detectree2/data_loading/custom.py @@ -0,0 +1,73 @@ +import rasterio +import numpy as np +import torch +from torch.utils.data import Dataset +import detectron2.data.transforms as T +from detectron2.structures import BoxMode, Instances, BitMasks +import cv2 + +class CustomTIFFDataset(Dataset): + def __init__(self, annotations, transforms=None): + """ + Args: + annotations (list): List of dictionaries containing image file paths and annotations. + transforms (callable, optional): Optional transform to be applied on a sample. + """ + self.annotations = annotations + self.transforms = transforms + + def __len__(self): + return len(self.annotations) + + def __getitem__(self, idx): + # Load the TIFF image + img_info = self.annotations[idx] + with rasterio.open(img_info['file_name']) as src: + # Read all bands (assuming they are all needed) + image = src.read() + # Normalize or rescale if necessary + image = image.astype(np.float32) / 255.0 # Example normalization + # If the number of bands is not 3, reduce to 3 or handle accordingly + #if image.shape[0] > 3: + # image = image[:3, :, :] # Taking the first 3 bands (e.g., RGB) + # Convert to HWC format expected by Detectron2 + #image = np.transpose(image, (1, 2, 0)) + + # Prepare annotations (this part needs to be adapted to your specific annotations) + target = { + "image_id": idx, + "annotations": img_info["annotations"], + "width": img_info["width"], + "height": img_info["height"], + } + + if self.transforms is not None: + augmentations = T.AugmentationList(self.transforms) + image, target = augmentations(image, target) + + # Convert to Detectron2-compatible format + image = torch.as_tensor(image.astype("float32").transpose(2, 0, 1)) + instances = self.get_detectron_instances(target) + + return image, instances + + def get_detectron_instances(self, target): + """ + Converts annotations into Detectron2's format. + This example assumes annotations are in COCO format, and you'll need to adapt it for your needs. + """ + boxes = [obj["bbox"] for obj in target["annotations"]] + boxes = torch.as_tensor(boxes, dtype=torch.float32) + boxes = BoxMode.convert(boxes, BoxMode.XYWH_ABS, BoxMode.XYXY_ABS) + + # Create BitMasks from the binary mask data (assuming the mask is a binary numpy array) + masks = [obj["segmentation"] for obj in target["annotations"]] # Replace with actual mask loading + masks = BitMasks(torch.stack([torch.from_numpy(mask) for mask in masks])) + + instances = Instances( + image_size=(target["height"], target["width"]), + gt_boxes=boxes, + gt_classes=torch.tensor([obj["category_id"] for obj in target["annotations"]], dtype=torch.int64), + gt_masks=masks + ) + return instances diff --git a/detectree2/models/train.py b/detectree2/models/train.py index d7606793..e7022618 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -42,6 +42,39 @@ # from PIL import Image +class MultiBandDatasetMapper: + def __init__(self, cfg, is_train=True, augmentations=None): + self.is_train = is_train + self.augmentations = T.AugmentationList(augmentations) if augmentations else None + + def __call__(self, dataset_dict): + dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict + image = utils.read_image(dataset_dict["file_name"], format="BGR") # This reads the image + image = self.load_all_bands(dataset_dict["file_name"]) # Custom method to load all bands + + if self.augmentations: + image, transforms = T.apply_augmentations(self.augmentations, image) + dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) + else: + dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) + + annos = [ + utils.transform_instance_annotations(annotation, transforms, image.shape[:2]) + for annotation in dataset_dict.pop("annotations") + ] + dataset_dict["instances"] = utils.annotations_to_instances(annos, image.shape[:2]) + return dataset_dict + + def load_all_bands(self, image_path): + """Load all bands of the image using rasterio and return as a numpy array.""" + with rasterio.open(image_path) as src: + image = src.read() # This will read all bands + # Normalize the bands if necessary + image = image.astype(np.float32) / 255.0 + # Transpose to HWC format + image = np.transpose(image, (1, 2, 0)) + return image + class LossEvalHook(HookBase): """Do inference and get the loss metric. @@ -244,7 +277,7 @@ def build_hooks(self): build_detection_test_loader( self.cfg, self.cfg.DATASETS.TEST, - DatasetMapper(self.cfg, True) + DatasetMapper(self.cfg, True) # Need to edit this for custom dataset ), self.patience, ), @@ -331,7 +364,13 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = filename = img_anns["imagePath"] # Make sure we have the correct height and width - height, width = cv2.imread(filename).shape[:2] + # If image path ends in .png use cv2 to get height and width + if filename.endswith(".png"): + height, width = cv2.imread(filename).shape[:2] # Need to change this to rasterio + # else if image path ends in .tif use rasterio to get height and width + elif filename.endswith(".tif"): + with rasterio.open(filename) as src: + height, width = src.shape record["file_name"] = filename record["height"] = height @@ -459,9 +498,9 @@ def register_train_data(train_location, def read_data(out_dir): """Function that will read the classes that are recorded during tiling.""" list = [] - out_tif = out_dir + 'classes.txt' + classes_txt = out_dir + 'classes.txt' # open file and read the content in a list - with open(out_tif, 'r') as fp: + with open(classes_txt, 'r') as fp: for line in fp: # remove linebreak from a current name # linebreak is the last character of each line diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 502982f7..73f42cc7 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -240,21 +240,22 @@ def process_tile_ms( with rasterio.open(out_tif, "w", **out_meta) as dest: dest.write(out_img) + # Images withmore than 4 bands are not supported by cv2 # Save all bands as an image if needed (not just the first 3 bands) - band_images = [] - for band_index in range(out_img.shape[0]): - band_image = out_img[band_index, :, :] - if np.max(band_image) > 255: - band_image = 255 * band_image / np.max(band_image) - band_images.append(band_image.astype(np.uint8)) + #band_images = [] + #for band_index in range(out_img.shape[0]): + # band_image = out_img[band_index, :, :] + # if np.max(band_image) > 255: + # band_image = 255 * band_image / np.max(band_image) + # band_images.append(band_image.astype(np.uint8)) # Stack the bands into a single image array # Does the band order need to be reversed as in the RGB case? - full_image = np.stack(band_images, axis=-1) + #full_image = np.stack(band_images, axis=-1) # Save the full image with potentially more than 3 bands - full_image_path = out_path_root.with_suffix(".png") - cv2.imwrite(str(full_image_path.resolve()), full_image) + #full_image_path = out_path_root.with_suffix(".png") + #cv2.imwrite(str(full_image_path.resolve()), full_image) if overlapping_crowns is not None: return data, out_path_root, overlapping_crowns, minx, miny, buffer @@ -322,7 +323,11 @@ def process_tile_train( scalingx = 1 / (data.transform[0]) scalingy = -1 / (data.transform[4]) moved_scaled = moved.scale(scalingx, scalingy, origin=(0, 0)) - impath = {"imagePath": out_path_root.with_suffix(".png").as_posix()} + + if mode == "rgb": + impath = {"imagePath": out_path_root.with_suffix(".png").as_posix()} + elif mode == "ms": + impath = {"imagePath": out_path_root.with_suffix(".tif").as_posix()} try: filename = out_path_root.with_suffix(".geojson") @@ -455,6 +460,9 @@ def record_data(crowns, list_of_classes = crowns[column].unique().tolist() + # Sort the list of classes in alphabetical order + list_of_classes.sort() + print("**The list of classes are:**") print(list_of_classes) print("**The list has been saved to the out_dir**") From a5e7088ba72f4dd0f703e69a51cee55f14f9455e Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 20 Aug 2024 18:23:29 +0100 Subject: [PATCH 21/63] ms bands --- detectree2/models/train.py | 63 ++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index e7022618..034fb2b5 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -17,6 +17,7 @@ import detectron2.data.transforms as T # noqa:N812 import detectron2.utils.comm as comm import numpy as np +import rasterio import torch from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 @@ -28,6 +29,7 @@ build_detection_test_loader, build_detection_train_loader, ) +from detectron2.data import detection_utils as utils from detectron2.engine import DefaultTrainer from detectron2.engine.hooks import HookBase from detectron2.evaluation import COCOEvaluator, verify_results @@ -42,15 +44,28 @@ # from PIL import Image -class MultiBandDatasetMapper: +class FlexibleDatasetMapper: def __init__(self, cfg, is_train=True, augmentations=None): + self.cfg = cfg self.is_train = is_train self.augmentations = T.AugmentationList(augmentations) if augmentations else None + self.rgb_mapper = DatasetMapper(cfg, is_train=is_train, augmentations=self.augmentations) def __call__(self, dataset_dict): + # Determine the number of bands + with rasterio.open(dataset_dict["file_name"]) as src: + num_bands = src.count + + if num_bands == 3: + # Use the standard RGB DatasetMapper + return self.rgb_mapper(dataset_dict) + else: + # Use the custom multi-band mapper + return self.multi_band_mapper(dataset_dict) + + def multi_band_mapper(self, dataset_dict): dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict - image = utils.read_image(dataset_dict["file_name"], format="BGR") # This reads the image - image = self.load_all_bands(dataset_dict["file_name"]) # Custom method to load all bands + image = self.load_image(dataset_dict["file_name"]) # Custom method to load image (multi-band) if self.augmentations: image, transforms = T.apply_augmentations(self.augmentations, image) @@ -58,6 +73,7 @@ def __call__(self, dataset_dict): else: dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) + # Transform annotations annos = [ utils.transform_instance_annotations(annotation, transforms, image.shape[:2]) for annotation in dataset_dict.pop("annotations") @@ -65,14 +81,12 @@ def __call__(self, dataset_dict): dataset_dict["instances"] = utils.annotations_to_instances(annos, image.shape[:2]) return dataset_dict - def load_all_bands(self, image_path): - """Load all bands of the image using rasterio and return as a numpy array.""" + def load_image(self, image_path): + """Load multi-band image.""" with rasterio.open(image_path) as src: - image = src.read() # This will read all bands - # Normalize the bands if necessary - image = image.astype(np.float32) / 255.0 - # Transpose to HWC format - image = np.transpose(image, (1, 2, 0)) + image = src.read() # This will read all bands (multi-band support) + image = image.transpose(1, 2, 0) # Convert to HWC format + image = image.astype(np.float32) # Convert to float32 for consistency return image class LossEvalHook(HookBase): @@ -286,7 +300,7 @@ def build_hooks(self): def build_train_loader(cls, cfg): - """Summary. + """Build the train loader with flexibility for RGB and multi-band images. Args: cfg (_type_): _description_ @@ -307,22 +321,39 @@ def build_train_loader(cls, cfg): if cfg.RESIZE: augmentations.append(T.Resize((1000, 1000))) elif cfg.RESIZE == "random": + size = None for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): location = datas['file_name'] - size = cv2.imread(location).shape[0] + try: + # Try to read with cv2 (for RGB images) + img = cv2.imread(location) + if img is not None: + size = img.shape[0] + else: + # Fall back to rasterio for multi-band images + with rasterio.open(location) as src: + size = src.height # Assuming square images + except Exception as e: + # Handle any errors that occur during loading + print(f"Error loading image {location}: {e}") + continue break - print("ADD RANDOM RESIZE WITH SIZE = ", size) - augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) + + if size: + print("ADD RANDOM RESIZE WITH SIZE = ", size) + augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) + else: + raise ValueError("Failed to determine image size for random resize") + return build_detection_train_loader( cfg, - mapper=DatasetMapper( + mapper=FlexibleDatasetMapper( cfg, is_train=True, augmentations=augmentations, ), ) - def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = None) -> List[Dict]: """Get the tree dictionaries. From 2042c482d7b6695df304ec12cb9e991025a5a45c Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 20 Aug 2024 18:49:02 +0100 Subject: [PATCH 22/63] ms bands --- detectree2/preprocessing/tiling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 73f42cc7..bafd259a 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -509,7 +509,8 @@ def to_traintest_folders( # noqa: C901 Path(out_dir / "train").mkdir(parents=True, exist_ok=True) Path(out_dir / "test").mkdir(parents=True, exist_ok=True) - file_names = tiles_dir.glob("*.png") + #file_names = tiles_dir.glob("*.png") + file_names = tiles_dir.glob("*.geojson") file_roots = [item.stem for item in file_names] num = list(range(0, len(file_roots))) From f3d1c89341a2cfa9648a2f77db4c0cb8da62158e Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 21 Aug 2024 18:53:34 +0100 Subject: [PATCH 23/63] ms bands --- detectree2/models/train.py | 179 +++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 95 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 034fb2b5..85d23e35 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -43,51 +43,42 @@ # from IPython.display import display # from PIL import Image - class FlexibleDatasetMapper: def __init__(self, cfg, is_train=True, augmentations=None): self.cfg = cfg self.is_train = is_train - self.augmentations = T.AugmentationList(augmentations) if augmentations else None - self.rgb_mapper = DatasetMapper(cfg, is_train=is_train, augmentations=self.augmentations) + self.rgb_mapper = DatasetMapper(cfg, is_train=is_train) def __call__(self, dataset_dict): - # Determine the number of bands - with rasterio.open(dataset_dict["file_name"]) as src: - num_bands = src.count - + # First, try to open the image using rasterio to determine the number of bands + try: + with rasterio.open(dataset_dict["file_name"]) as src: + num_bands = src.count + except rasterio.errors.RasterioIOError as e: + raise ValueError(f"Error opening image file: {dataset_dict['file_name']}") from e + + # If the image has 3 bands, use the standard RGB DatasetMapper if num_bands == 3: - # Use the standard RGB DatasetMapper return self.rgb_mapper(dataset_dict) else: - # Use the custom multi-band mapper + # Otherwise, handle the image with the custom multi-band mapper return self.multi_band_mapper(dataset_dict) def multi_band_mapper(self, dataset_dict): dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict - image = self.load_image(dataset_dict["file_name"]) # Custom method to load image (multi-band) - - if self.augmentations: - image, transforms = T.apply_augmentations(self.augmentations, image) - dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) - else: - dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) + with rasterio.open(dataset_dict["file_name"]) as src: + img = src.read() # Read all bands + img = np.transpose(img, (1, 2, 0)) # Convert to HWC format - # Transform annotations - annos = [ - utils.transform_instance_annotations(annotation, transforms, image.shape[:2]) - for annotation in dataset_dict.pop("annotations") - ] - dataset_dict["instances"] = utils.annotations_to_instances(annos, image.shape[:2]) + dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) + + # Ensure ground truth instances (annotations) are included + annos = dataset_dict.pop("annotations", []) + instances = utils.annotations_to_instances(annos, dataset_dict["image"].shape[1:]) + dataset_dict["instances"] = utils.filter_empty_instances(instances) + return dataset_dict - def load_image(self, image_path): - """Load multi-band image.""" - with rasterio.open(image_path) as src: - image = src.read() # This will read all bands (multi-band support) - image = image.transpose(1, 2, 0) # Convert to HWC format - image = image.astype(np.float32) # Convert to float32 for consistency - return image class LossEvalHook(HookBase): """Do inference and get the loss metric. @@ -202,6 +193,9 @@ def after_step(self): self.trainer.storage.put_scalars(timetest=12) def after_train(self): + if not self.trainer.APs: + print("No APs were recorded during training. Skipping model selection.") + return # Select the model with the best AP50 index = self.trainer.APs.index(max(self.trainer.APs)) + 1 # Error in demo: @@ -225,8 +219,9 @@ class MyTrainer(DefaultTrainer): def __init__(self, cfg, patience): # noqa: D107 self.patience = patience - # self.resize = resize super().__init__(cfg) + #self.data_loader = build_train_loader(cfg) # Use custom data loader + def train(self): """Run training. @@ -291,68 +286,71 @@ def build_hooks(self): build_detection_test_loader( self.cfg, self.cfg.DATASETS.TEST, - DatasetMapper(self.cfg, True) # Need to edit this for custom dataset + FlexibleDatasetMapper(self.cfg, True) # Need to edit this for custom dataset ), self.patience, ), ) return hooks + @classmethod + def build_train_loader(cls, cfg): + """Build the train loader with flexibility for RGB and multi-band images. -def build_train_loader(cls, cfg): - """Build the train loader with flexibility for RGB and multi-band images. + Args: + cfg (_type_): _description_ - Args: - cfg (_type_): _description_ + Returns: + _type_: _description_ + """ + augmentations = [ + T.RandomRotation(angle=[90, 90], expand=False), + T.RandomLighting(0.7), + T.RandomFlip(prob=0.4, horizontal=True, vertical=False), + T.RandomFlip(prob=0.4, horizontal=False, vertical=True), + ] - Returns: - _type_: _description_ - """ - augmentations = [ - T.RandomBrightness(0.8, 1.8), - T.RandomContrast(0.6, 1.3), - T.RandomSaturation(0.8, 1.4), - T.RandomRotation(angle=[90, 90], expand=False), - T.RandomLighting(0.7), - T.RandomFlip(prob=0.4, horizontal=True, vertical=False), - T.RandomFlip(prob=0.4, horizontal=False, vertical=True), - ] - - if cfg.RESIZE: - augmentations.append(T.Resize((1000, 1000))) - elif cfg.RESIZE == "random": - size = None - for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): - location = datas['file_name'] - try: - # Try to read with cv2 (for RGB images) - img = cv2.imread(location) - if img is not None: - size = img.shape[0] - else: - # Fall back to rasterio for multi-band images - with rasterio.open(location) as src: - size = src.height # Assuming square images - except Exception as e: - # Handle any errors that occur during loading - print(f"Error loading image {location}: {e}") - continue - break - - if size: - print("ADD RANDOM RESIZE WITH SIZE = ", size) - augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) - else: - raise ValueError("Failed to determine image size for random resize") + # Some augmentations are only applicable to 3-band images + if cfg.IMGMODE == "rgb": + augmentations.append(T.RandomBrightness(0.8, 1.8), + T.RandomContrast(0.6, 1.3), + T.RandomSaturation(0.8, 1.4)) + + if cfg.RESIZE: + augmentations.append(T.Resize((1000, 1000))) + elif cfg.RESIZE == "random": + size = None + for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): + location = datas['file_name'] + try: + # Try to read with cv2 (for RGB images) + img = cv2.imread(location) + if img is not None: + size = img.shape[0] + else: + # Fall back to rasterio for multi-band images + with rasterio.open(location) as src: + size = src.height # Assuming square images + except Exception as e: + # Handle any errors that occur during loading + print(f"Error loading image {location}: {e}") + continue + break + + if size: + print("ADD RANDOM RESIZE WITH SIZE = ", size) + augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) + else: + raise ValueError("Failed to determine image size for random resize") - return build_detection_train_loader( - cfg, - mapper=FlexibleDatasetMapper( + return build_detection_train_loader( cfg, - is_train=True, - augmentations=augmentations, - ), - ) + mapper=FlexibleDatasetMapper( + cfg, + is_train=True, + augmentations=augmentations, + ), + ) def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = None) -> List[Dict]: """Get the tree dictionaries. @@ -366,23 +364,13 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = List of dictionaries corresponding to segmentations of trees. Each dictionary includes bounding box around tree and points tracing a polygon around a tree. """ - # filepath = '/content/drive/MyDrive/forestseg/paracou_data/Panayiotis_Outputs/220303_AllSpLabelled.gpkg' - # datagpd = gpd.read_file(filepath) - # List_Genus = datagpd.Genus_Species.to_list() - # Genus_Species_UniqueList = list(set(List_Genus)) - - # if classes is not None: # list_of_classes = crowns[variable].unique().tolist() classes = classes else: classes = ["tree"] - # classes = Genus_Species_UniqueList #['tree'] # genus_species list + dataset_dicts = [] - # for root, dirs, files in os.walk(train_location): - # for file in files: - # if file.endswith(".geojson"): - # print(os.path.join(root, file)) for filename in [file for file in os.listdir(directory) if file.endswith(".geojson")]: json_file = os.path.join(directory, filename) @@ -395,10 +383,9 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = filename = img_anns["imagePath"] # Make sure we have the correct height and width - # If image path ends in .png use cv2 to get height and width + # If image path ends in .png use cv2 to get height and width else if image path ends in .tif use rasterio if filename.endswith(".png"): - height, width = cv2.imread(filename).shape[:2] # Need to change this to rasterio - # else if image path ends in .tif use rasterio to get height and width + height, width = cv2.imread(filename).shape[:2] elif filename.endswith(".tif"): with rasterio.open(filename) as src: height, width = src.shape @@ -587,6 +574,7 @@ def setup_cfg( eval_period=100, out_dir="./train_outputs", resize=True, + imgmode="rgb", ): """Set up config object # noqa: D417. @@ -636,6 +624,7 @@ def setup_cfg( cfg.TEST.EVAL_PERIOD = eval_period cfg.RESIZE = resize cfg.INPUT.MIN_SIZE_TRAIN = 1000 + cfg.IMGMODE = imgmode return cfg From c8803bd38327e7cc4c8954be7d8fd90c031c5575 Mon Sep 17 00:00:00 2001 From: James Ball Date: Thu, 22 Aug 2024 17:15:28 +0100 Subject: [PATCH 24/63] dataset mapper updates --- detectree2/models/train.py | 169 ++++++++++++++++++++++++----- detectree2/preprocessing/tiling.py | 17 --- 2 files changed, 140 insertions(+), 46 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 85d23e35..3855c876 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -43,42 +43,151 @@ # from IPython.display import display # from PIL import Image -class FlexibleDatasetMapper: +#class FlexibleDatasetMapper: +# def __init__(self, cfg, is_train=True, augmentations=None): +# self.cfg = cfg +# self.is_train = is_train +# # Ensure augmentations is always a list +# if augmentations is None: +# augmentations = [] +# self.augmentations = augmentations +# self.rgb_mapper = DatasetMapper(cfg, is_train=is_train, augmentations=augmentations) +# +# def __call__(self, dataset_dict): +# # First, try to open the image using rasterio to determine the number of bands +# try: +# with rasterio.open(dataset_dict["file_name"]) as src: +# num_bands = src.count +# except rasterio.errors.RasterioIOError as e: +# raise ValueError(f"Error opening image file: {dataset_dict['file_name']}") from e +# +# # If the image has 3 bands, use the standard RGB DatasetMapper +# if num_bands == 3: +# print("Using RGB mapper") +# return self.rgb_mapper(dataset_dict) +# else: +# # Otherwise, handle the image with the custom multi-band mapper +# print("Using multi-band mapper") +# return self.multi_band_mapper(dataset_dict) +# +# def multi_band_mapper(self, dataset_dict): +# dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict +# +# # Load the image using rasterio +# with rasterio.open(dataset_dict["file_name"]) as src: +# img = src.read() # Read all bands +# img = np.transpose(img, (1, 2, 0)) # Convert to HWC format (Height, Width, Channels) +# +# # Convert the image to float32 +# img = img.astype("float32") +# +# # Normalize the image if necessary (optional step) +# # Example: normalize to [0, 1] if the original image has high dynamic range values +# img /= 65535.0 # Assuming the original range is [0, 65535] for 16-bit images +# +# # Apply augmentations if provided +# if self.augmentations: +# aug_input = T.AugInput(img) +# all_transforms = [] +# +# # Apply each augmentation in the list +# for aug in self.augmentations: +# transform = aug(aug_input) +# all_transforms.append(transform) +# img = aug_input.image +# +# # Combine all the transformations into a TransformList +# transforms = T.TransformList(all_transforms) +# +# # If there are annotations, apply the same transforms to them +# if "annotations" in dataset_dict: +# annos = dataset_dict.pop("annotations") +# for anno in annos: +# bbox = anno["bbox"] +# anno["bbox"] = transforms.apply_box(bbox) +# # Ensure the bounding box has the correct shape [N, 4] +# bbox = transforms.apply_box(np.array(bbox).reshape(1, 4)).squeeze(0) # Ensure shape [4] +# anno["bbox"] = bbox.tolist() # Convert back to list +# +# # Transform segmentation masks +# if "segmentation" in anno: +# segm = anno["segmentation"] +# if isinstance(segm, list): # Polygon format +# transformed_segm = [] +# for poly in segm: +# poly = np.array(poly).reshape(-1, 2) # Ensure shape (N, 2) +# transformed_poly = transforms.apply_polygons([poly])[0] # Apply and get the first element +# transformed_segm.append(transformed_poly.tolist()) +# anno["segmentation"] = transformed_segm +# else: +# raise ValueError("Unexpected segmentation format.") +# +# instances = utils.annotations_to_instances(annos, img.shape[:2]) +# dataset_dict["instances"] = utils.filter_empty_instances(instances) +# +# dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) +# +# return dataset_dict + +class FlexibleDatasetMapper(DatasetMapper): def __init__(self, cfg, is_train=True, augmentations=None): + if augmentations is None: + augmentations = [] + + # Wrap the augmentations list in AugmentationList inside the call to the superclass + super().__init__( + is_train=is_train, + augmentations=augmentations, + image_format=cfg.INPUT.FORMAT, + use_instance_mask=cfg.MODEL.MASK_ON, + use_keypoint=cfg.MODEL.KEYPOINT_ON, + instance_mask_format=cfg.INPUT.MASK_FORMAT, + keypoint_hflip_indices=None, + precomputed_proposal_topk=None, + recompute_boxes=False + ) self.cfg = cfg self.is_train = is_train - self.rgb_mapper = DatasetMapper(cfg, is_train=is_train) def __call__(self, dataset_dict): - # First, try to open the image using rasterio to determine the number of bands + if dataset_dict is None: + print("Received None for dataset_dict, skipping this entry.") + return None + try: + # If idx is passed with dataset_dict, capture it + #idx = dataset_dict.get("index", "unknown") + #print(f"Processing entry at index {idx}: {dataset_dict}") + # Handle multi-band image loading with rasterio.open(dataset_dict["file_name"]) as src: - num_bands = src.count - except rasterio.errors.RasterioIOError as e: - raise ValueError(f"Error opening image file: {dataset_dict['file_name']}") from e + img = src.read() + img = np.transpose(img, (1, 2, 0)).astype("float32") - # If the image has 3 bands, use the standard RGB DatasetMapper - if num_bands == 3: - return self.rgb_mapper(dataset_dict) - else: - # Otherwise, handle the image with the custom multi-band mapper - return self.multi_band_mapper(dataset_dict) + # If it's a 3-band image, use the inherited behavior + if img.shape[-1] == 3: + return super().__call__(dataset_dict) - def multi_band_mapper(self, dataset_dict): - dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict - with rasterio.open(dataset_dict["file_name"]) as src: - img = src.read() # Read all bands - img = np.transpose(img, (1, 2, 0)) # Convert to HWC format + # Otherwise, handle custom multi-band logic + aug_input = T.AugInput(img) + transforms = self.augmentations(aug_input) # Apply the augmentations + img = aug_input.image - dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) - - # Ensure ground truth instances (annotations) are included - annos = dataset_dict.pop("annotations", []) - instances = utils.annotations_to_instances(annos, dataset_dict["image"].shape[1:]) - dataset_dict["instances"] = utils.filter_empty_instances(instances) - - return dataset_dict + dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) + if "annotations" in dataset_dict: + # Apply transforms to the annotations in the original dataset_dict + dataset_dict = super()._transform_annotations(dataset_dict, transforms, img.shape[:2]) + + return dataset_dict + + except Exception as e: + # Handle and log the error + if dataset_dict is not None: + file_name = dataset_dict.get('file_name', 'unknown') + print(f"Error processing {file_name}: {e}") + else: + print(f"Error processing an entry: {e}") + return None class LossEvalHook(HookBase): """Do inference and get the loss metric. @@ -222,7 +331,6 @@ def __init__(self, cfg, patience): # noqa: D107 super().__init__(cfg) #self.data_loader = build_train_loader(cfg) # Use custom data loader - def train(self): """Run training. @@ -305,7 +413,6 @@ def build_train_loader(cls, cfg): """ augmentations = [ T.RandomRotation(angle=[90, 90], expand=False), - T.RandomLighting(0.7), T.RandomFlip(prob=0.4, horizontal=True, vertical=False), T.RandomFlip(prob=0.4, horizontal=False, vertical=True), ] @@ -313,6 +420,7 @@ def build_train_loader(cls, cfg): # Some augmentations are only applicable to 3-band images if cfg.IMGMODE == "rgb": augmentations.append(T.RandomBrightness(0.8, 1.8), + T.RandomLighting(0.7), T.RandomContrast(0.6, 1.3), T.RandomSaturation(0.8, 1.4)) @@ -351,6 +459,10 @@ def build_train_loader(cls, cfg): augmentations=augmentations, ), ) + + @classmethod + def build_test_loader(cls, cfg, dataset_name): + return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = None) -> List[Dict]: """Get the tree dictionaries. @@ -413,8 +525,7 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)], "bbox_mode": BoxMode.XYXY_ABS, "segmentation": [poly], - "category_id": classes.index(features["properties"][classes_at]), # id - # "category_id": 0, #id + "category_id": classes.index(features["properties"][classes_at]), "iscrowd": 0, } else: diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index bafd259a..7c20c235 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -240,23 +240,6 @@ def process_tile_ms( with rasterio.open(out_tif, "w", **out_meta) as dest: dest.write(out_img) - # Images withmore than 4 bands are not supported by cv2 - # Save all bands as an image if needed (not just the first 3 bands) - #band_images = [] - #for band_index in range(out_img.shape[0]): - # band_image = out_img[band_index, :, :] - # if np.max(band_image) > 255: - # band_image = 255 * band_image / np.max(band_image) - # band_images.append(band_image.astype(np.uint8)) - - # Stack the bands into a single image array - # Does the band order need to be reversed as in the RGB case? - #full_image = np.stack(band_images, axis=-1) - - # Save the full image with potentially more than 3 bands - #full_image_path = out_path_root.with_suffix(".png") - #cv2.imwrite(str(full_image_path.resolve()), full_image) - if overlapping_crowns is not None: return data, out_path_root, overlapping_crowns, minx, miny, buffer From 8b417e87547781da8c1fb0cca2cb6f3b762fe34f Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 23 Aug 2024 12:40:02 +0100 Subject: [PATCH 25/63] ms training --- detectree2/models/train.py | 43 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 3855c876..3a06f857 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -134,7 +134,7 @@ def __init__(self, cfg, is_train=True, augmentations=None): if augmentations is None: augmentations = [] - # Wrap the augmentations list in AugmentationList inside the call to the superclass + # Pass the raw list of augmentations (do not wrap it in T.AugmentationList here) super().__init__( is_train=is_train, augmentations=augmentations, @@ -148,21 +148,29 @@ def __init__(self, cfg, is_train=True, augmentations=None): ) self.cfg = cfg self.is_train = is_train + self.logger = logging.getLogger(__name__) + mode = "training" if is_train else "inference" + self.logger.info(f"[FlexibleDatasetMapper] Augmentations used in {mode}: {augmentations}") def __call__(self, dataset_dict): if dataset_dict is None: - print("Received None for dataset_dict, skipping this entry.") + self.logger.warning("Received None for dataset_dict, skipping this entry.") return None try: - # If idx is passed with dataset_dict, capture it - #idx = dataset_dict.get("index", "unknown") - #print(f"Processing entry at index {idx}: {dataset_dict}") # Handle multi-band image loading with rasterio.open(dataset_dict["file_name"]) as src: img = src.read() + if img is None: + raise ValueError(f"Image data is None for file: {dataset_dict['file_name']}") img = np.transpose(img, (1, 2, 0)).astype("float32") + # Size check similar to utils.check_image_size + if img.shape[:2] != (dataset_dict.get("height"), dataset_dict.get("width")): + self.logger.warning( + f"Image size {img.shape[:2]} does not match expected size {(dataset_dict.get('height'), dataset_dict.get('width'))}." + ) + # If it's a 3-band image, use the inherited behavior if img.shape[-1] == 3: return super().__call__(dataset_dict) @@ -172,21 +180,22 @@ def __call__(self, dataset_dict): transforms = self.augmentations(aug_input) # Apply the augmentations img = aug_input.image - dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) + dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(img.transpose(2, 0, 1))) + + # Handle semantic segmentation if present + if "sem_seg_file_name" in dataset_dict: + sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) + dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) if "annotations" in dataset_dict: # Apply transforms to the annotations in the original dataset_dict - dataset_dict = super()._transform_annotations(dataset_dict, transforms, img.shape[:2]) + self._transform_annotations(dataset_dict, transforms, img.shape[:2]) return dataset_dict - + except Exception as e: - # Handle and log the error - if dataset_dict is not None: - file_name = dataset_dict.get('file_name', 'unknown') - print(f"Error processing {file_name}: {e}") - else: - print(f"Error processing an entry: {e}") + file_name = dataset_dict.get('file_name', 'unknown') if dataset_dict else 'unknown' + self.logger.error(f"Error processing {file_name}: {e}") return None class LossEvalHook(HookBase): @@ -419,12 +428,12 @@ def build_train_loader(cls, cfg): # Some augmentations are only applicable to 3-band images if cfg.IMGMODE == "rgb": - augmentations.append(T.RandomBrightness(0.8, 1.8), + augmentations.extend(T.RandomBrightness(0.8, 1.8), T.RandomLighting(0.7), T.RandomContrast(0.6, 1.3), T.RandomSaturation(0.8, 1.4)) - if cfg.RESIZE: + if cfg.RESIZE == "fixed": augmentations.append(T.Resize((1000, 1000))) elif cfg.RESIZE == "random": size = None @@ -684,7 +693,7 @@ def setup_cfg( num_classes=1, eval_period=100, out_dir="./train_outputs", - resize=True, + resize="fixed", # fixed or random imgmode="rgb", ): """Set up config object # noqa: D417. From f02fdb05a7c6f1937d5ae874465f47be8f516499 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Aug 2024 18:39:10 +0100 Subject: [PATCH 26/63] inhertited processes --- detectree2/models/train.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 3a06f857..9e10480d 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -183,15 +183,16 @@ def __call__(self, dataset_dict): dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(img.transpose(2, 0, 1))) # Handle semantic segmentation if present - if "sem_seg_file_name" in dataset_dict: - sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) - dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) + # THIS CAN BE HANDLED BY INHERITING FROM DatasetMapper? + #if "sem_seg_file_name" in dataset_dict: + # sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) + # dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) - if "annotations" in dataset_dict: - # Apply transforms to the annotations in the original dataset_dict - self._transform_annotations(dataset_dict, transforms, img.shape[:2]) + #if "annotations" in dataset_dict: + # # Apply transforms to the annotations in the original dataset_dict + # self._transform_annotations(dataset_dict, transforms, img.shape[:2]) - return dataset_dict + return super().__call__(dataset_dict) except Exception as e: file_name = dataset_dict.get('file_name', 'unknown') if dataset_dict else 'unknown' @@ -428,10 +429,12 @@ def build_train_loader(cls, cfg): # Some augmentations are only applicable to 3-band images if cfg.IMGMODE == "rgb": - augmentations.extend(T.RandomBrightness(0.8, 1.8), - T.RandomLighting(0.7), - T.RandomContrast(0.6, 1.3), - T.RandomSaturation(0.8, 1.4)) + augmentations.extend([ + T.RandomBrightness(0.8, 1.8), + T.RandomLighting(0.7), + T.RandomContrast(0.6, 1.3), + T.RandomSaturation(0.8, 1.4) + ]) if cfg.RESIZE == "fixed": augmentations.append(T.Resize((1000, 1000))) From 925c3de51995f147cdf2a317dcf3cf57f66e6d5e Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 26 Aug 2024 19:19:05 +0100 Subject: [PATCH 27/63] inhertited processes --- detectree2/models/train.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 9e10480d..a8088135 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -183,16 +183,22 @@ def __call__(self, dataset_dict): dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(img.transpose(2, 0, 1))) # Handle semantic segmentation if present - # THIS CAN BE HANDLED BY INHERITING FROM DatasetMapper? - #if "sem_seg_file_name" in dataset_dict: - # sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) - # dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) - - #if "annotations" in dataset_dict: - # # Apply transforms to the annotations in the original dataset_dict - # self._transform_annotations(dataset_dict, transforms, img.shape[:2]) - - return super().__call__(dataset_dict) + # Can this be handled by inheriting from DatasetMapper? + if "sem_seg_file_name" in dataset_dict: + sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) + dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) + + if not self.is_train: + # USER: Modify this if you want to keep them for some reason. + dataset_dict.pop("annotations", None) + dataset_dict.pop("sem_seg_file_name", None) + return dataset_dict + + if "annotations" in dataset_dict: + # Apply transforms to the annotations in the original dataset_dict + self._transform_annotations(dataset_dict, transforms, img.shape[:2]) + + return dataset_dict except Exception as e: file_name = dataset_dict.get('file_name', 'unknown') if dataset_dict else 'unknown' From 5027a49ee651831cb03a323ee0860b17495ff532 Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 28 Aug 2024 15:59:58 +0100 Subject: [PATCH 28/63] augmentations scaling --- detectree2/models/train.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index a8088135..2ce98459 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -399,9 +399,27 @@ def build_evaluator(cls, cfg, dataset_name, output_folder=None): def build_hooks(self): hooks = super().build_hooks() - # augmentations = [T.ResizeShortestEdge(short_edge_length=(1000, 1000), - # max_size=1333, - # sample_style='choice')] + if cfg.RESIZE == "random": + size = None + for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): + location = datas['file_name'] + try: + # Try to read with cv2 (for RGB images) + img = cv2.imread(location) + if img is not None: + size = img.shape[0] + else: + # Fall back to rasterio for multi-band images + with rasterio.open(location) as src: + size = src.height # Assuming square images + except Exception as e: + # Handle any errors that occur during loading + print(f"Error loading image {location}: {e}") + continue + break + augmentations = [T.ResizeShortestEdge([size, size], size+300)] + else: + augmentations = [T.ResizeShortestEdge([1000, 1000], 1333)] hooks.insert( -1, LossEvalHook( @@ -410,7 +428,7 @@ def build_hooks(self): build_detection_test_loader( self.cfg, self.cfg.DATASETS.TEST, - FlexibleDatasetMapper(self.cfg, True) # Need to edit this for custom dataset + FlexibleDatasetMapper(self.cfg, True, augmentations=augmentations) ), self.patience, ), @@ -436,14 +454,14 @@ def build_train_loader(cls, cfg): # Some augmentations are only applicable to 3-band images if cfg.IMGMODE == "rgb": augmentations.extend([ - T.RandomBrightness(0.8, 1.8), + T.RandomBrightness(0.7, 1.5), T.RandomLighting(0.7), T.RandomContrast(0.6, 1.3), T.RandomSaturation(0.8, 1.4) ]) if cfg.RESIZE == "fixed": - augmentations.append(T.Resize((1000, 1000))) + augmentations.append(T.ResizeShortestEdge([1000, 1000], 1333)) elif cfg.RESIZE == "random": size = None for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): @@ -465,9 +483,14 @@ def build_train_loader(cls, cfg): if size: print("ADD RANDOM RESIZE WITH SIZE = ", size) + #min_size = int(size * 0.6) + #max_size = int(size * 1.4) + #augmentations.append(T.RandomResize(min_size=(min_size, min_size), max_size=max_size)) augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) else: raise ValueError("Failed to determine image size for random resize") + elif cfg.RESIZE == "rand_fixed": + augmentations.append(T.ResizeScale(0.6, 1.4, 1000, 1000)) return build_detection_train_loader( cfg, From 065ad16a3d2473d25e7289ba84192ecb00d2d495 Mon Sep 17 00:00:00 2001 From: James Ball Date: Wed, 28 Aug 2024 16:31:20 +0100 Subject: [PATCH 29/63] augmentations scaling --- detectree2/models/train.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 2ce98459..c9e712b3 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -399,9 +399,9 @@ def build_evaluator(cls, cfg, dataset_name, output_folder=None): def build_hooks(self): hooks = super().build_hooks() - if cfg.RESIZE == "random": + if self.cfg.RESIZE == "random": size = None - for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): + for i, datas in enumerate(DatasetCatalog.get(self.cfg.DATASETS.TRAIN[0])): location = datas['file_name'] try: # Try to read with cv2 (for RGB images) From 5682e7581cc651202dac77f81280b70ae82523a4 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 2 Sep 2024 17:48:41 +0100 Subject: [PATCH 30/63] ms functions --- detectree2/models/train.py | 441 +++++++++++++++++++---------- detectree2/preprocessing/tiling.py | 4 +- 2 files changed, 298 insertions(+), 147 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index c9e712b3..d4e1a82e 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -9,6 +9,7 @@ import logging import os import random +import re import time from pathlib import Path from typing import Any, Dict, List @@ -19,6 +20,7 @@ import numpy as np import rasterio import torch +import torch.nn as nn from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 from detectron2.config import get_cfg @@ -40,101 +42,31 @@ from detectron2.utils.logger import log_every_n_seconds from detectron2.utils.visualizer import ColorMode, Visualizer -# from IPython.display import display -# from PIL import Image - -#class FlexibleDatasetMapper: -# def __init__(self, cfg, is_train=True, augmentations=None): -# self.cfg = cfg -# self.is_train = is_train -# # Ensure augmentations is always a list -# if augmentations is None: -# augmentations = [] -# self.augmentations = augmentations -# self.rgb_mapper = DatasetMapper(cfg, is_train=is_train, augmentations=augmentations) -# -# def __call__(self, dataset_dict): -# # First, try to open the image using rasterio to determine the number of bands -# try: -# with rasterio.open(dataset_dict["file_name"]) as src: -# num_bands = src.count -# except rasterio.errors.RasterioIOError as e: -# raise ValueError(f"Error opening image file: {dataset_dict['file_name']}") from e -# -# # If the image has 3 bands, use the standard RGB DatasetMapper -# if num_bands == 3: -# print("Using RGB mapper") -# return self.rgb_mapper(dataset_dict) -# else: -# # Otherwise, handle the image with the custom multi-band mapper -# print("Using multi-band mapper") -# return self.multi_band_mapper(dataset_dict) -# -# def multi_band_mapper(self, dataset_dict): -# dataset_dict = dataset_dict.copy() # Make a copy of the dataset dict -# -# # Load the image using rasterio -# with rasterio.open(dataset_dict["file_name"]) as src: -# img = src.read() # Read all bands -# img = np.transpose(img, (1, 2, 0)) # Convert to HWC format (Height, Width, Channels) -# -# # Convert the image to float32 -# img = img.astype("float32") -# -# # Normalize the image if necessary (optional step) -# # Example: normalize to [0, 1] if the original image has high dynamic range values -# img /= 65535.0 # Assuming the original range is [0, 65535] for 16-bit images -# -# # Apply augmentations if provided -# if self.augmentations: -# aug_input = T.AugInput(img) -# all_transforms = [] -# -# # Apply each augmentation in the list -# for aug in self.augmentations: -# transform = aug(aug_input) -# all_transforms.append(transform) -# img = aug_input.image -# -# # Combine all the transformations into a TransformList -# transforms = T.TransformList(all_transforms) -# -# # If there are annotations, apply the same transforms to them -# if "annotations" in dataset_dict: -# annos = dataset_dict.pop("annotations") -# for anno in annos: -# bbox = anno["bbox"] -# anno["bbox"] = transforms.apply_box(bbox) -# # Ensure the bounding box has the correct shape [N, 4] -# bbox = transforms.apply_box(np.array(bbox).reshape(1, 4)).squeeze(0) # Ensure shape [4] -# anno["bbox"] = bbox.tolist() # Convert back to list -# -# # Transform segmentation masks -# if "segmentation" in anno: -# segm = anno["segmentation"] -# if isinstance(segm, list): # Polygon format -# transformed_segm = [] -# for poly in segm: -# poly = np.array(poly).reshape(-1, 2) # Ensure shape (N, 2) -# transformed_poly = transforms.apply_polygons([poly])[0] # Apply and get the first element -# transformed_segm.append(transformed_poly.tolist()) -# anno["segmentation"] = transformed_segm -# else: -# raise ValueError("Unexpected segmentation format.") -# -# instances = utils.annotations_to_instances(annos, img.shape[:2]) -# dataset_dict["instances"] = utils.filter_empty_instances(instances) -# -# dataset_dict["image"] = torch.as_tensor(img.transpose(2, 0, 1).astype("float32")) -# -# return dataset_dict class FlexibleDatasetMapper(DatasetMapper): + """ + A flexible dataset mapper that extends the standard DatasetMapper to handle multi-band images + and custom augmentations. + + This class is designed to work with datasets that may contain images with more than three channels + (e.g., multispectral images) and allows for custom augmentations to be applied. It also handles + semantic segmentation data if provided in the dataset. + + Args: + cfg (CfgNode): Configuration object containing dataset and model configurations. + is_train (bool): Flag indicating whether the mapper is being used for training. Default is True. + augmentations (list, optional): List of augmentations to be applied. Default is an empty list. + + Attributes: + cfg (CfgNode): Stores the configuration object for later use. + is_train (bool): Indicates whether the mapper is in training mode. + logger (Logger): Logger instance for logging messages. + """ def __init__(self, cfg, is_train=True, augmentations=None): if augmentations is None: augmentations = [] - # Pass the raw list of augmentations (do not wrap it in T.AugmentationList here) + # Initialize the base DatasetMapper class with provided parameters super().__init__( is_train=is_train, augmentations=augmentations, @@ -153,16 +85,27 @@ def __init__(self, cfg, is_train=True, augmentations=None): self.logger.info(f"[FlexibleDatasetMapper] Augmentations used in {mode}: {augmentations}") def __call__(self, dataset_dict): + """ + Process a single dataset dictionary, applying the necessary transformations and augmentations. + + Args: + dataset_dict (dict): A dictionary containing data for a single dataset item, including + file names and metadata. + + Returns: + dict: The processed dataset dictionary, or None if there was an error. + """ if dataset_dict is None: self.logger.warning("Received None for dataset_dict, skipping this entry.") return None try: - # Handle multi-band image loading + # Handle multi-band image loading using rasterio with rasterio.open(dataset_dict["file_name"]) as src: img = src.read() if img is None: raise ValueError(f"Image data is None for file: {dataset_dict['file_name']}") + # Transpose image dimensions to match expected format (H, W, C) img = np.transpose(img, (1, 2, 0)).astype("float32") # Size check similar to utils.check_image_size @@ -171,7 +114,7 @@ def __call__(self, dataset_dict): f"Image size {img.shape[:2]} does not match expected size {(dataset_dict.get('height'), dataset_dict.get('width'))}." ) - # If it's a 3-band image, use the inherited behavior + # If it's a 3-band image, delegate processing to the parent class if img.shape[-1] == 3: return super().__call__(dataset_dict) @@ -183,19 +126,18 @@ def __call__(self, dataset_dict): dataset_dict["image"] = torch.as_tensor(np.ascontiguousarray(img.transpose(2, 0, 1))) # Handle semantic segmentation if present - # Can this be handled by inheriting from DatasetMapper? if "sem_seg_file_name" in dataset_dict: sem_seg_gt = utils.read_image(dataset_dict.pop("sem_seg_file_name"), "L").squeeze(2) dataset_dict["sem_seg"] = torch.as_tensor(sem_seg_gt.astype("long")) if not self.is_train: - # USER: Modify this if you want to keep them for some reason. + # If not in training mode, remove annotations and segmentation file names dataset_dict.pop("annotations", None) dataset_dict.pop("sem_seg_file_name", None) return dataset_dict if "annotations" in dataset_dict: - # Apply transforms to the annotations in the original dataset_dict + # Apply the transformations to the annotations self._transform_annotations(dataset_dict, transforms, img.shape[:2]) return dataset_dict @@ -206,24 +148,34 @@ def __call__(self, dataset_dict): return None class LossEvalHook(HookBase): - """Do inference and get the loss metric. + """ + A custom hook for evaluating loss during training and managing model checkpoints based on evaluation metrics. - Class to: - - Do inference of dataset like an Evaluator does - - Get the loss metric like the trainer does - https://github.com/facebookresearch/detectron2/blob/master/detectron2/evaluation/evaluator.py - https://github.com/facebookresearch/detectron2/blob/master/detectron2/engine/train_loop.py - See https://gist.github.com/ortegatron/c0dad15e49c2b74de8bb09a5615d9f6b + This hook is designed to: + - Perform inference on a dataset similarly to an Evaluator. + - Calculate and log the loss metric during training. + - Save the best model checkpoint based on a specified evaluation metric (e.g., AP50). + - Implement early stopping if the evaluation metric does not improve over a specified number of evaluations. Attributes: - model: model to train - period: number of iterations between evaluations - data_loader: data loader to use for evaluation - patience: number of evaluation periods to wait for improvement + _model: The model to evaluate. + _period: Number of iterations between evaluations. + _data_loader: The data loader used for evaluation. + patience: Number of evaluation periods to wait before early stopping. + iter: Tracks the number of evaluations since the last improvement in the evaluation metric. + max_ap: The best evaluation metric (e.g., AP50) achieved during training. + best_iter: The iteration at which the best evaluation metric was achieved. """ - def __init__(self, eval_period, model, data_loader, patience): - """Inits LossEvalHook.""" + """ + Initialize the LossEvalHook. + + Args: + eval_period (int): The number of iterations between evaluations. + model (torch.nn.Module): The model to evaluate. + data_loader (torch.utils.data.DataLoader): The data loader for evaluation. + patience (int): The number of evaluation periods to wait for improvement before early stopping. + """ self._model = model self._period = eval_period self._data_loader = data_loader @@ -233,10 +185,14 @@ def __init__(self, eval_period, model, data_loader, patience): self.best_iter = 0 def _do_loss_eval(self): - """Copying inference_on_dataset from evaluator.py. + """ + Perform inference on the dataset and calculate the average loss. + + This method is adapted from `inference_on_dataset` in Detectron2's evaluator. + It also calculates and logs the AP50 metric and updates the best model checkpoint if needed. Returns: - _type_: _description_ + list: A list of loss values for each batch in the dataset. """ total = len(self._data_loader) num_warmup = min(5, total - 1) @@ -246,6 +202,7 @@ def _do_loss_eval(self): losses = [] for idx, inputs in enumerate(self._data_loader): if idx == num_warmup: + # Reset the start time after the warm-up phase start_time = time.perf_counter() total_compute_time = 0 start_compute_time = time.perf_counter() @@ -255,6 +212,7 @@ def _do_loss_eval(self): iters_after_start = idx + 1 - num_warmup * int(idx >= num_warmup) seconds_per_img = total_compute_time / iters_after_start if idx >= num_warmup * 2 or seconds_per_img > 5: + # Log progress and estimated time remaining total_seconds_per_img = (time.perf_counter() - start_time) / iters_after_start eta = datetime.timedelta(seconds=int(total_seconds_per_img * (total - idx - 1))) log_every_n_seconds( @@ -263,11 +221,13 @@ def _do_loss_eval(self): str(eta)), n=5, ) + # Calculate loss for the current batch loss_batch = self._get_loss(inputs) losses.append(loss_batch) + mean_loss = np.mean(losses) - # print(self.trainer.cfg.DATASETS.TEST) - # Combine the AP50s of the different datasets + + # Calculate the average AP50 across datasets if multiple datasets are used for testing if len(self.trainer.cfg.DATASETS.TEST) > 1: APs = [] for dataset in self.trainer.cfg.DATASETS.TEST: @@ -275,7 +235,10 @@ def _do_loss_eval(self): AP = sum(APs) / len(APs) else: AP = self.trainer.test(self.trainer.cfg, self.trainer.model)["segm"]["AP50"] - print("Av. AP50 =", AP) + + print("Av. segm AP50 =", AP) + + # Store the calculated loss and AP50 in the trainer's storage self.trainer.APs.append(AP) self.trainer.storage.put_scalar("validation_loss", mean_loss) self.trainer.storage.put_scalar("validation_ap", AP) @@ -284,15 +247,17 @@ def _do_loss_eval(self): return losses def _get_loss(self, data): - """Calculate loss in train_loop. + """ + Compute the loss for a given batch of data. Args: - data (_type_): _description_ + data (dict): A batch of input data. Returns: - _type_: _description_ + float: The total loss for the batch. """ metrics_dict = self._model(data) + # Detach and move to CPU for logging metrics_dict = { k: v.detach().cpu().item() if isinstance(v, torch.Tensor) else float(v) for k, v in metrics_dict.items() @@ -301,60 +266,74 @@ def _get_loss(self, data): return total_losses_reduced def after_step(self): + """ + Hook to be called after each training iteration to evaluate the model and manage checkpoints. + + - Evaluates the model at regular intervals. + - Saves the best model checkpoint based on the AP50 metric. + - Implements early stopping if the AP50 does not improve after a set number of evaluations. + """ next_iter = self.trainer.iter + 1 is_final = next_iter == self.trainer.max_iter if is_final or (self._period > 0 and next_iter % self._period == 0): self._do_loss_eval() + # Check if the current AP50 is the best so far if self.max_ap < self.trainer.APs[-1]: self.iter = 0 self.max_ap = self.trainer.APs[-1] + # Save the current best model self.trainer.checkpointer.save("model_" + str(len(self.trainer.APs))) self.best_iter = self.trainer.iter else: self.iter += 1 if self.iter == self.patience: + # Early stopping condition met self.trainer.early_stop = True print("Early stopping occurs in iter {}, max ap is {}".format(self.best_iter, self.max_ap)) self.trainer.storage.put_scalars(timetest=12) def after_train(self): + """ + Hook to be called after training is complete to load the best model checkpoint based on AP50. + + - Selects and loads the model checkpoint with the best AP50. + """ if not self.trainer.APs: print("No APs were recorded during training. Skipping model selection.") return # Select the model with the best AP50 index = self.trainer.APs.index(max(self.trainer.APs)) + 1 - # Error in demo: - # AssertionError: Checkpoint /__w/detectree2/detectree2/detectree2-data/paracou-out/train_outputs-1/model_1.pth - # not found! - # Therefore sleep is attempt to allow CI to pass, but it often still fails. + # Error handling for checkpoint loading, with a sleep to ensure file availability in CI environments time.sleep(15) self.trainer.checkpointer.load(self.trainer.cfg.OUTPUT_DIR + '/model_' + str(index) + '.pth') # See https://jss367.github.io/data-augmentation-in-detectron2.html for data augmentation advice class MyTrainer(DefaultTrainer): - """Summary. + """ + Custom Trainer class that extends the DefaultTrainer. - Args: - DefaultTrainer (_type_): _description_ + This trainer adds flexibility for handling different image types (e.g., RGB and multi-band images) + and custom training behavior, such as early stopping and specialized data augmentation strategies. - Returns: - _type_: _description_ + Args: + cfg (CfgNode): Configuration object containing the model and dataset configurations. + patience (int): Number of evaluation periods to wait for improvement before early stopping. """ def __init__(self, cfg, patience): # noqa: D107 self.patience = patience super().__init__(cfg) - #self.data_loader = build_train_loader(cfg) # Use custom data loader def train(self): - """Run training. + """ + Run the training loop. - Args: - start_iter, max_iter (int): See docs above + This method overrides the DefaultTrainer's train method to include early stopping and + custom logging of Average Precision (AP) metrics. Returns: - OrderedDict of results, if evaluation is enabled. Otherwise None. + OrderedDict: Results from evaluation, if evaluation is enabled. Otherwise, None. """ start_iter = self.start_iter @@ -385,6 +364,7 @@ def train(self): raise finally: self.after_train() + # Verify the results if testing is enabled and this is the main process if len(self.cfg.TEST.EXPECTED_RESULTS) and comm.is_main_process(): assert hasattr(self, "_last_eval_results"), "No evaluation results obtained during training!" verify_results(self.cfg, self._last_eval_results) @@ -392,19 +372,42 @@ def train(self): @classmethod def build_evaluator(cls, cfg, dataset_name, output_folder=None): + """ + Build the evaluator for the model. + + Args: + cfg (CfgNode): Configuration object. + dataset_name (str): Name of the dataset to evaluate. + output_folder (str, optional): Directory to save evaluation results. Defaults to "eval". + + Returns: + COCOEvaluator: An evaluator for COCO-style datasets. + """ if output_folder is None: os.makedirs("eval", exist_ok=True) output_folder = "eval" return COCOEvaluator(dataset_name, cfg, True, output_folder) def build_hooks(self): + """ + Build the training hooks, including the custom LossEvalHook. + + This method adds a custom hook for evaluating the model's loss during training, with support for + early stopping based on the AP50 metric. + + Returns: + list: A list of hooks to be used during training. + """ hooks = super().build_hooks() + + # Determine the appropriate resize strategy based on the configuration if self.cfg.RESIZE == "random": size = None + # Attempt to determine the image size from the training dataset for i, datas in enumerate(DatasetCatalog.get(self.cfg.DATASETS.TRAIN[0])): location = datas['file_name'] try: - # Try to read with cv2 (for RGB images) + # Attempt to read the image with OpenCV (for RGB images) img = cv2.imread(location) if img is not None: size = img.shape[0] @@ -417,9 +420,13 @@ def build_hooks(self): print(f"Error loading image {location}: {e}") continue break + # Define augmentation based on the determined size augmentations = [T.ResizeShortestEdge([size, size], size+300)] else: + # Use fixed size resizing as a default augmentations = [T.ResizeShortestEdge([1000, 1000], 1333)] + + # Insert the custom LossEvalHook before the last hook (typically the evaluation hook) hooks.insert( -1, LossEvalHook( @@ -437,21 +444,27 @@ def build_hooks(self): @classmethod def build_train_loader(cls, cfg): - """Build the train loader with flexibility for RGB and multi-band images. + """ + Build the training data loader with support for custom augmentations and image types. + + This method configures the data loader to apply specific augmentations depending on the image mode + (RGB or multi-band) and resize strategy defined in the configuration. Args: - cfg (_type_): _description_ + cfg (CfgNode): Configuration object. Returns: - _type_: _description_ + DataLoader: A data loader for the training dataset. """ + + # Define basic augmentations including rotation and flipping augmentations = [ T.RandomRotation(angle=[90, 90], expand=False), T.RandomFlip(prob=0.4, horizontal=True, vertical=False), T.RandomFlip(prob=0.4, horizontal=False, vertical=True), ] - # Some augmentations are only applicable to 3-band images + # Additional augmentations for RGB images if cfg.IMGMODE == "rgb": augmentations.extend([ T.RandomBrightness(0.7, 1.5), @@ -460,6 +473,7 @@ def build_train_loader(cls, cfg): T.RandomSaturation(0.8, 1.4) ]) + # Add resizing augmentations based on the resize strategy if cfg.RESIZE == "fixed": augmentations.append(T.ResizeShortestEdge([1000, 1000], 1333)) elif cfg.RESIZE == "random": @@ -503,6 +517,19 @@ def build_train_loader(cls, cfg): @classmethod def build_test_loader(cls, cfg, dataset_name): + """ + Build the test data loader. + + This method configures the data loader for evaluation, using the FlexibleDatasetMapper + to handle custom augmentations and image types. + + Args: + cfg (CfgNode): Configuration object. + dataset_name (str): Name of the dataset to load for testing. + + Returns: + DataLoader: A data loader for the test dataset. + """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = None) -> List[Dict]: @@ -590,24 +617,42 @@ def combine_dicts(root_dir: str, mode: str = "train", classes: List[str] = None, classes_at: str = None) -> List[Dict]: - """Join tree dicts from different directories. + """ + Combine dictionaries from different directories based on the specified mode. + + This function aggregates tree dictionaries from multiple directories within a root directory. + Depending on the mode, it either combines dictionaries from all directories, + all except a specified validation directory, or only from the validation directory. Args: - root_dir: - val_dir: + root_dir (str): The root directory containing subdirectories with tree dictionaries. + val_dir (int): The index (1-based) of the validation directory to exclude or use depending on the mode. + mode (str, optional): The mode of operation. Can be "train", "val", or "full". + "train" excludes the validation directory, + "val" includes only the validation directory, + and "full" includes all directories. Defaults to "train". + classes (List[str], optional): A list of classes to filter the dictionaries by. Defaults to None. + classes_at (str, optional): A key to specify where the classes are located within the dictionary structure. Defaults to None. Returns: - Concatenated array of dictionaries over all directories + List[Dict]: A list of combined dictionaries from the specified directories. """ + # Get a list of all directories within the root directory train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] + + # Handle the different modes for combining dictionaries if mode == "train": + # Exclude the validation directory from the list of directories del train_dirs[(val_dir - 1)] tree_dicts = [] for d in train_dirs: + # Combine dictionaries from all directories except the validation directory tree_dicts += get_tree_dicts(d, classes=classes, classes_at=classes_at) elif mode == "val": + # Use only the validation directory tree_dicts = get_tree_dicts(train_dirs[(val_dir - 1)], classes=classes, classes_at=classes_at) elif mode == "full": + # Combine dictionaries from all directories, including the validation directory tree_dicts = [] for d in train_dirs: tree_dicts += get_tree_dicts(d, classes=classes, classes_at=classes_at) @@ -645,6 +690,8 @@ def register_train_data(train_location, name: string to name data val_fold: fold assigned for validation and tuning. If not given, will take place on all folds. + classes: list of classes to include + classes_at: column name for classes """ if val_fold is not None: for d in ["train", "val"]: @@ -665,8 +712,15 @@ def register_train_data(train_location, MetadataCatalog.get(name + "_" + "full").set(thing_classes=classes) -def read_data(out_dir): - """Function that will read the classes that are recorded during tiling.""" +def get_classes(out_dir): + """Function that will read the classes that are recorded during tiling. + + Args: + out_dir: directory where classes.txt is located + + Returns: + list of classes + """ list = [] classes_txt = out_dir + 'classes.txt' # open file and read the content in a list @@ -692,14 +746,23 @@ def remove_registered_data(name="tree"): def register_test_data(test_location, name="tree"): - """Register data for testing.""" + """Register data for testing. + + Args: + test_location: directory containing test data + name: string to name data + """ d = "test" DatasetCatalog.register(name + "_" + d, lambda d=d: get_tree_dicts(test_location)) MetadataCatalog.get(name + "_" + d).set(thing_classes=["tree"]) def load_json_arr(json_path): - """Load json array.""" + """Load json array. + + Args: + json_path: path to json file + """ lines = [] with open(json_path, "r") as f: for line in f: @@ -725,8 +788,9 @@ def setup_cfg( num_classes=1, eval_period=100, out_dir="./train_outputs", - resize="fixed", # fixed or random + resize="fixed", # fixed or random or rand_fixed imgmode="rgb", + num_bands=3, ): """Set up config object # noqa: D417. @@ -749,6 +813,11 @@ def setup_cfg( eval_period: number of iterations between evaluations out_dir: directory to save outputs """ + + # Validate the resize parameter + if resize not in {"fixed", "random", "rand_fixed"}: + raise ValueError(f"Invalid resize option '{resize}'. Must be 'fixed', 'random', or 'rand_fixed'.") + cfg = get_cfg() cfg.merge_from_file(model_zoo.get_config_file(base_model)) cfg.DATASETS.TRAIN = trains @@ -776,7 +845,16 @@ def setup_cfg( cfg.TEST.EVAL_PERIOD = eval_period cfg.RESIZE = resize cfg.INPUT.MIN_SIZE_TRAIN = 1000 - cfg.IMGMODE = imgmode + cfg.IMGMODE = imgmode # rgb or multispectral + if num_bands > 3: + # Adjust PIXEL_MEAN and PIXEL_STD for the number of bands + default_pixel_mean = cfg.MODEL.PIXEL_MEAN + default_pixel_std = cfg.MODEL.PIXEL_STD + # Extend or truncate the PIXEL_MEAN and PIXEL_STD based on num_bands + cfg.MODEL.PIXEL_MEAN = (default_pixel_mean * (num_bands // len(default_pixel_mean)) + + default_pixel_mean[:num_bands % len(default_pixel_mean)]) + cfg.MODEL.PIXEL_STD = (default_pixel_std * (num_bands // len(default_pixel_std)) + + default_pixel_std[:num_bands % len(default_pixel_std)]) return cfg @@ -787,7 +865,17 @@ def predictions_on_data(directory=None, scale=1, geos_exist=True, num_predictions=0): - """Prediction produced from a test folder and outputted to predictions folder.""" + """Prediction produced from a test folder and outputted to predictions folder. + + Args: + directory: directory containing test data + predictor: predictor object + trees_metadata: metadata for trees + save: boolean to save predictions + scale: scale of image + geos_exist: boolean to determine if geojson files exist + num_predictions: number of predictions to make + """ test_location = directory + "/test" pred_dir = test_location + "/predictions" @@ -833,6 +921,69 @@ def predictions_on_data(directory=None, with open(output_file, "w") as dest: json.dump(evaluations, dest) +def modify_conv1_weights(model, num_input_channels): + """ + Modify the weights of the first convolutional layer (conv1) to accommodate a different number of input channels. + + This function adjusts the weights of the `conv1` layer in the model's backbone to support a custom number + of input channels. It creates a new weight tensor with the desired number of input channels, + and initializes it by repeating the weights of the original channels. + + Args: + model (torch.nn.Module): The model containing the convolutional layer to modify. + num_input_channels (int): The number of input channels for the new conv1 layer. + + """ + with torch.no_grad(): + # Retrieve the original weights of the conv1 layer + old_weights = model.backbone.bottom_up.stem.conv1.weight + + # Create a new weight tensor with the desired number of input channels + # The shape is (out_channels, in_channels, height, width) + new_weights = torch.zeros((old_weights.size(0), num_input_channels, *old_weights.shape[2:])) + + # Initialize the new weights by repeating the original weights across the new channels + # This example repeats the first 3 channels if num_input_channels > 3 + for i in range(num_input_channels): + new_weights[:, i, :, :] = old_weights[:, i % 3, :, :] + + # Create a new conv1 layer with the updated number of input channels + model.backbone.bottom_up.stem.conv1 = nn.Conv2d( + num_input_channels, old_weights.size(0), kernel_size=7, stride=2, padding=3, bias=False + ) + + # Copy the modified weights into the new conv1 layer + model.backbone.bottom_up.stem.conv1.weight.copy_(new_weights) + + +def get_latest_model_path(output_dir: str) -> str: + """ + Find the model file with the highest index in the specified output directory. + + Args: + output_dir (str): The directory where the model files are stored. + + Returns: + str: The path to the model file with the highest index. + """ + # Regular expression to match model files with the pattern "model_X.pth" + model_pattern = re.compile(r"model_(\d+)\.pth") + + # List all files in the output directory + files = os.listdir(output_dir) + + # Find all files that match the pattern and extract their indices + model_files = [(f, int(model_pattern.search(f).group(1))) for f in files if model_pattern.search(f)] + + if not model_files: + raise FileNotFoundError(f"No model files found in the directory {output_dir}") + + # Sort the files by index in descending order and select the highest one + latest_model_file = max(model_files, key=lambda x: x[1])[0] + + # Return the full path to the latest model file + return os.path.join(output_dir, latest_model_file) + if __name__ == "__main__": train_location = "/content/drive/Shareddrives/detectree2/data/Paracou/tiles/train/" diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 7c20c235..3f9df300 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -427,10 +427,10 @@ def is_overlapping_box(test_boxes_array, train_box): return False -def record_data(crowns, +def record_classes(crowns, out_dir, column='status'): - """Function that will record a list of classes into a file that can be readed during training. + """Function that will record a list of classes into a file that can be read during training. Args: crowns: gpd dataframe with the crowns From b5f621625d1bb50bb90c2b440def8fb983237fc8 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 3 Sep 2024 18:24:09 +0100 Subject: [PATCH 31/63] tutorial updates --- README.md | 4 +- docs/source/tutorial.rst | 260 +++++++++++++++++++++++++++--- report/figures/train_val_loss.png | Bin 0 -> 44808 bytes 3 files changed, 242 insertions(+), 22 deletions(-) create mode 100644 report/figures/train_val_loss.png diff --git a/README.md b/README.md index e10c2e83..0958d783 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ -Python package for automatic tree crown delineation based on Mask R-CNN. Pre-trained models can be picked in the [`model_garden`](https://github.com/PatBall1/detectree2/tree/master/model_garden). -A tutorial on how to prepare data, train models and make predictions is available [here](https://patball1.github.io/detectree2/tutorial.html). For questions, collaboration proposals and requests for data email [James Ball](mailto:ball.jgc@gmail.com). Some example data is available for download [here](https://doi.org/10.5281/zenodo.8136161). +Python package for automatic tree crown delineation in aerial RGB and multispectral imagery based on Mask R-CNN. Pre-trained models can be picked in the [`model_garden`](https://github.com/PatBall1/detectree2/tree/master/model_garden). +A tutorial on how to prepare data, train models and make predictions is available [here](https://patball1.github.io/detectree2/tutorial.html). For questions, collaboration proposals and requests for data email [James Ball](mailto:ball.jgc@gmail.com). Some example data is available to download [here](https://doi.org/10.5281/zenodo.8136161). Detectree2是一个基于Mask R-CNN的自动树冠检测与分割的Python包。您可以在[`model_garden`](https://github.com/PatBall1/detectree2/tree/master/model_garden)中选择预训练模型。[这里](https://patball1.github.io/detectree2/tutorial.html)提供了如何准备数据、训练模型和进行预测的教程。如果有任何问题,合作提案或者需要样例数据,可以邮件联系[James Ball](mailto:ball.jgc@gmail.com)。一些示例数据可以在[这里](https://doi.org/10.5281/zenodo.8136161)下载。 diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 5095033f..0cf09d86 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -2,9 +2,10 @@ Tutorial ======== This tutorial goes through the steps of single class (tree) detection and -delineation. A guide to multiclass prediction (e.g. species mapping, -disease mapping) is coming soon. Example data that can be used in -this tutorial is available `here `_. +delineation from RGB and multispectral data. A guide to multiclass prediction +(e.g. species mapping, disease mapping) is coming soon. Example data that can +be used in this tutorial is available +`here `_. The key steps are: @@ -40,9 +41,15 @@ If you would just like to make predictions on an orthomosaic with a pre-trained model from the ``model_garden``, skip to part 4 (Generating landscape predictions). +The data preparation and training process for both RGB and multispectral data +is presented here. The process is similar for both data types but there are +some key differences that are highlighted. Training a single model on both RGB +and multispectral data at the same time is not currently supported. Stick to +one data type per model (or stack the RGB bands with the multispectral bands +and treat as in the case of multispectral data). -Preparing data --------------- +Preparing data (RGB and multispectral) +-------------------------------------- An example of the recommended file structure when training a new model is as follows: @@ -58,6 +65,8 @@ An example of the recommended file structure when training a new model is as fol ├── rgb │ ├── Paracou_RGB_2016_10cm.tif (RGB orthomosaic in local UTM CRS) │ └── Paracou_RGB_2019.tif (RGB orthomosaic in local UTM CRS) + ├── ms + │ └── Paracou_MS_2016.tif (Multispectral orthomosaic in local UTM CRS) └── crowns └── UpdatedCrowns8.gpkg (Crown polygons readable by geopandas e.g. Geopackage, shapefile) @@ -65,6 +74,11 @@ Here we have two sites available to train on (Danum and Paracou). Several site d included in the training and testing phase (but only a single site directory is required). If available, several RGB orthomosaics can be included in a single site directory (see e.g ``Paracou -> RGB``). +For Paracou, we also have a multispectral scan available (5-bands). For this data, the ``mode`` parameter in the +``tile_data`` function should be set to ``"ms"``. This calls a different routine for tiling the data that retains the +``.tif`` format instead of converting to ``.png`` as in the case of ``rgb``. This comes at a slight expense of speed +later on but is necessary to retain all the multispectral information. + We call functions to from ``detectree2``'s tiling and training modules. .. code-block:: python @@ -83,9 +97,6 @@ Set up the paths to the orthomosaic and corresponding manual crown data. img_path = site_path + "/rgb/2016/Paracou_RGB_2016_10cm.tif" crown_path = site_path + "/crowns/220619_AllSpLabelled.gpkg" - # Read in the tiff file - data = rasterio.open(img_path) - # Read in crowns (then filter by an attribute if required) crowns = gpd.read_file(crown_path) crowns = crowns.to_crs(data.crs.data) # making sure CRS match @@ -98,6 +109,8 @@ The tile size will depend on: * Available computational resources. * The detail required on the crown outline. * If using a pre-trained model, the tile size used in training should roughly match the tile size of predictions. +* The ``mode`` depends on whether you are tiling 3-band RGB (``mode="rgb"``) data of multispectral data of 4 or more +bands (``mode="ms"``). .. code-block:: python @@ -114,11 +127,13 @@ surrounding tiles). Including a buffer is recommended as it allows for tiles tha Next we tile the data. The ``tile_data`` function, when ``crowns`` is supplied, will only retain tiles that contain more than the given ``threshold`` coverage of training data (here 60%). This helps to reduce the chance that the network is -trained with tiles that contain a large number of unlabelled crowns (which would reduce its sensitivity). +trained with tiles that contain a large number of unlabelled crowns (which would reduce its sensitivity). This value +should be adjusted depending on the density of crowns in the landscape (e.g. 10% may be more appropriate for savannah +type systems or urban environments). .. code-block:: python - tile_data(img_path, out_dir, buffer, tile_width, tile_height, crowns, threshold) + tile_data(img_path, out_dir, buffer, tile_width, tile_height, crowns, threshold, mode="rgb") .. warning:: If tiles are outputing as blank images set ``dtype_bool = True`` in the ``tile_data`` function. This is a bug @@ -131,7 +146,7 @@ trained with tiles that contain a large number of unlabelled crowns (which would closed canopy forests so some of the default assumptions will reflect that and parameters will need to be adjusted for different systems. -Send geojsons to train folder (with sub-folders for k-fold cross validation) and test folder. +Send geojsons to train folder (with sub-folders for k-fold cross validation) and a test folder. .. code-block:: python @@ -164,7 +179,8 @@ The data has now been tiled and partitioned for model training, tuning and evalu It is recommended to visually inspect the tiles before training to ensure that the tiling has worked as expected and -that crowns and images align. This can be done quickly with the inbuilt ``detectron2`` visualisation tools. +that crowns and images align. This can be done with the inbuilt ``detectron2`` visualisation tools. For RGB tiles +(``.png``), the following code can be used to visualise the training data. .. code-block:: python @@ -201,8 +217,61 @@ that crowns and images align. This can be done quickly with the inbuilt ``detect | -Training a model ----------------- +Alternatively, with some adaptation the ``detectron2`` visualisation tools can also be used to visualise the +multispectral (``.tif``) tiles. + +.. code-block:: python + + import rasterio + from detectron2.utils.visualizer import Visualizer + from detectree2.models.train import combine_dicts + from detectron2.data import DatasetCatalog, MetadataCatalog + from PIL import Image + import numpy as np + import cv2 + import matplotlib.pyplot as plt + from IPython.display import display + + val_fold = 1 + name = "Paracou" + tiles = "/tilesMS_" + appends + "/train" + train_location = "/content/drive/MyDrive/WORK/detectree2/data/" + name + tiles + dataset_dicts = combine_dicts(train_location, val_fold) + trees_metadata = MetadataCatalog.get(name + "_train") + + # Function to normalize and convert multi-band image to RGB if needed + def prepare_image_for_visualization(image): + if image.shape[2] == 3: + # If the image has 3 bands, assume it's RGB + image = np.stack([ + cv2.normalize(image[:, :, i], None, 0, 255, cv2.NORM_MINMAX) + for i in range(3) + ], axis=-1).astype(np.uint8) + else: + # If the image has more than 3 bands, choose the first 3 for visualization + image = image[:, :, :3] # Or select specific bands + image = np.stack([ + cv2.normalize(image[:, :, i], None, 0, 255, cv2.NORM_MINMAX) + for i in range(3) + ], axis=-1).astype(np.uint8) + + return image + + # Visualize each image in the dataset + for d in dataset_dicts: + with rasterio.open(d["file_name"]) as src: + img = src.read() # Read all bands + img = np.transpose(img, (1, 2, 0)) # Convert to HWC format + img = prepare_image_for_visualization(img) # Normalize and prepare for visualization + + visualizer = Visualizer(img[:, :, ::-1]*10, metadata=trees_metadata, scale=0.5) + out = visualizer.draw_dataset_dict(d) + image = out.get_image()[:, :, ::-1] + display(Image.fromarray(image)) + + +Training a model (RGB) +---------------------- Before training can commence, it is necessary to register the training data. It is possible to set a validation fold for model evaluation (which can be helpful for tuning models). The validation fold can be changed over different training @@ -233,7 +302,7 @@ datasets should be tuples containing strings. If just a single site is being use trains = ("Paracou_train", "Danum_train", "SepilokEast_train", "SepilokWest_train") # Registered train data tests = ("Paracou_val", "Danum_val", "SepilokEast_val", "SepilokWest_val") # Registered validation data - out_dir = "/content/drive/Shareddrives/detectree2/220809_train_outputs" + out_dir = "/content/drive/Shareddrives/detectree2/240809_train_outputs" cfg = setup_cfg(base_model, trains, tests, workers = 4, eval_period=100, max_iter=3000, out_dir=out_dir) # update_model arg can be used to load in trained model @@ -259,7 +328,7 @@ Then set up the configurations as before but with the trained model also supplie trains = ("Paracou_train", "Danum_train", "SepilokEast_train", "SepilokWest_train") # Registered train data tests = ("Paracou_val", "Danum_val", "SepilokEast_val", "SepilokWest_val") # Registered validation data - out_dir = "/content/drive/Shareddrives/detectree2/220809_train_outputs" + out_dir = "/content/drive/Shareddrives/detectree2/240809_train_outputs" cfg = setup_cfg(base_model, trains, tests, trained_model, workers = 4, eval_period=100, max_iter=3000, out_dir=out_dir) # update_model arg used to load in trained model @@ -269,7 +338,11 @@ Then set up the configurations as before but with the trained model also supplie model training will converge given the particularities of the data supplied and computational resources available. Once we are all set up, we can get commence model training. Training will continue until a specified number of -iterations (``max_iter``) or until model performance is no longer improving ("early stopping" via ``patience``). +iterations (``max_iter``) or until model performance is no longer improving ("early stopping" via ``patience``). The +``patience`` parameter sets the number of training epochs to wait for an improvement in validation accuracy before +stopping training. This is useful for preventing overfitting and saving time. Each time an improved model is found it is +saved to the output directory. + Training outputs, including model weights and training metrics, will be stored in ``out_dir``. .. code-block:: @@ -283,6 +356,154 @@ Training outputs, including model weights and training metrics, will be stored i Early stopping is implemented and will be triggered by a sustained failure to improve on the performance of predictions on the validation fold. This is measured as the AP50 score of the validation predictions. +Training a model (multispectral) +-------------------------------- + +The process for training a multispectral model is similar to that for RGB data but there are some key steps that are +different. Data will be read from ``.tif`` files of 4 or more bands instead of the 3-band ``.png`` files. + +Data should be registered as before: + +.. code-block:: python + + from detectree2.models.train import register_train_data, remove_registered_data + val_fold = 5 + appends = "40_30_0.6" + site_path = "/content/drive/SharedDrive/detectree2/data/Paracou" + train_location = site_path + "/tilesMS_" + appends + "/train/" + register_train_data(train_location, "ParacouMS", val_fold) + +The number of bands can be checked with rasterio: + +.. code-block:: python + + import rasterio + import os + import glob + + # Read in geotif and assess mean and sd for each band + #site_path = "/content/drive/MyDrive/WORK/detectree2/data/Paracou" + folder_path = site_path + "/tilesMS_" + appends + "/" + + # Select path of first .tif file + img_paths = glob.glob(folder_path + "*.tif") + img_path = img_paths[0] + + # Open the raster file + with rasterio.open(img_path) as dataset: + # Get the number of bands + num_bands = dataset.count + + # Print the number of bands + print(f'The raster has {num_bands} bands.') + + +Due to the additional bands, we must modify the weights of the first convolutional layer (conv1) to accommodate a +different number of input channels. This is done with the ``modify_conv1_weights`` function. The extension of the +``cfg.MODEL.PIXEL_MEAN`` and ``cfg.MODEL.PIXEL_STD`` lists to include the additional bands happens within the +``setup_cfg`` function when ``num_bands`` is set to a value greater than 3. ``imgmode`` should be set to ``"ms"`` to +ensure the correct training routines are called. + +.. code-block:: python + + from datetime import date + from detectron2.modeling import build_model + import torch.nn as nn + import torch.nn.init as init + from detectron2.modeling.roi_heads.fast_rcnn import FastRCNNOutputLayers + import numpy as np + from detectree2.models.train import modify_conv1_weights, MyTrainer, setup_cfg + + # Good idea to keep track of the date if producing multiple models + today = date.today() + today = today.strftime("%y%m%d") + + names = ["ParacouMS",] + + trains = (names[0] + "_train",) + tests = (names[0] + "_val",) + out_dir = "/content/drive/SharedDrive/detectree2/models/" + today + "_ParacouMS" + + base_model = "COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml" # Path to the model config + + # Set up the configuration + cfg = setup_cfg(base_model, trains, tests, workers = 2, eval_period=50, + base_lr = 0.0003, backbone_freeze=0, gamma = 0.9, + max_iter=500000, out_dir=out_dir, resize = "rand_fixed", imgmode="ms", + num_bands= num_bands) # update_model arg can be used to load in trained model + + # Build the model + model = build_model(cfg) + + # Adjust input layer to accept correct number of channels + modify_conv1_weights(model, num_input_channels=num_bands) + + +With additional bands, more data is being passed through the network per image so it may be neessary to reduce the +number of images per batch. Only do this is you a getting warnings/errors about memory usage (e.g. +``CUDA out of memory``) as it will slow down training. + +.. code-block:: python + + cfg.SOLVER.IMS_PER_BATCH = 1 + + +Training can now commence as before: + +.. code-block:: + + trainer = MyTrainer(cfg, patience = 5) + trainer.resume_or_load(resume=False) + trainer.train() + + +Post-training (check training convergence) +------------------------------------------ + +It is important to check that the model has converged and is not overfitting. This can be done by plotting the training +and validation loss over time. The ``detectron2`` training routine will output a ``metrics.json`` file that can be used +to plot the training and validation loss. The following code can be used to plot the loss: + +.. code-block:: python + + import json + import matplotlib.pyplot as plt + from detectree2.models.train import load_json_arr + + #out_dir = "/content/drive/Shareddrives/detectree2/models/230103_resize_full" + experiment_folder = out_dir + + experiment_metrics = load_json_arr(experiment_folder + '/metrics.json') + + plt.plot( + [x['iteration'] for x in experiment_metrics if 'validation_loss' in x], + [x['validation_loss'] for x in experiment_metrics if 'validation_loss' in x], label='Total Validation Loss', color='red') + plt.plot( + [x['iteration'] for x in experiment_metrics if 'total_loss' in x], + [x['total_loss'] for x in experiment_metrics if 'total_loss' in x], label='Total Training Loss') + + plt.legend(loc='upper right') + plt.title('Comparison of the training and validation loss of detectree2') + plt.ylabel('Total Loss') + plt.xlabel('Number of Iterations') + plt.show() + +.. image:: ../../report/figures/train_val_loss.png + :width: 400 + :alt: Training tile 1 + :align: center + +| +Training loss and validation loss decreased over time. As training continued, the validation loss flattened whereas the +training loss continued to decrease. The ``patience`` mechanism prevented training from continuing after 3000 iterations +preventing overfitting. If validation loss is substantially higher than training loss, the model may be overfitted. + +To understand how the segmentation performance improves through training, it is also possible to plot the AP50 score +over the iterations. This can be done with the following code: + + + + Evaluating model performance ---------------------------- @@ -314,8 +535,7 @@ can discard partial the crowns predicted at the edge of tiles. site_path = "/content/drive/Shareddrives/detectree2/data/BCI_50ha" img_path = site_path + "/rgb/2015.06.10_07cm_ORTHO.tif" tiles_path = site_path + "/tilespred/" - # Read in the geotiff - data = rasterio.open(img_path) + # Location of trained model model_path = "/content/drive/Shareddrives/detectree2/models/220629_ParacouSepilokDanum_JB.pth" @@ -323,7 +543,7 @@ can discard partial the crowns predicted at the edge of tiles. buffer = 30 tile_width = 40 tile_height = 40 - tile_data(data, tiles_path, buffer, tile_width, tile_height, dtype_bool = True) + tile_data(img_path, tiles_path, buffer, tile_width, tile_height, dtype_bool = True) .. warning:: If tiles are outputing as blank images set ``dtype_bool = True`` in the ``tile_data`` function. This is a bug diff --git a/report/figures/train_val_loss.png b/report/figures/train_val_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..d1be3e7df2a6c7ccca8dc0debc8841a5efea01ef GIT binary patch literal 44808 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 Date: Tue, 3 Sep 2024 18:52:21 +0100 Subject: [PATCH 32/63] tutorial updates --- docs/source/tutorial.rst | 21 ++++++++++++++++++++- report/figures/val_AP50.png | Bin 0 -> 22751 bytes 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 report/figures/val_AP50.png diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 0cf09d86..0370663f 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -490,7 +490,7 @@ to plot the training and validation loss. The following code can be used to plot .. image:: ../../report/figures/train_val_loss.png :width: 400 - :alt: Training tile 1 + :alt: Traina and validation loss :align: center | @@ -502,7 +502,26 @@ To understand how the segmentation performance improves through training, it is over the iterations. This can be done with the following code: +.. code-block:: python + + plt.plot( + [x['iteration'] for x in experiment_metrics if 'validation_loss' in x], + [x['validation_loss'] for x in experiment_metrics if 'validation_loss' in x], label='Total Validation Loss', color='red') + plt.plot( + [x['iteration'] for x in experiment_metrics if 'total_loss' in x], + [x['total_loss'] for x in experiment_metrics if 'total_loss' in x], label='Total Training Loss') + + plt.legend(loc='upper right') + plt.title('Comparison of the training and validation loss of detectree2') + plt.ylabel('Total Loss') + plt.xlabel('Number of Iterations') + plt.show() +.. image:: ../../report/figures/AP50.png + :width: 400 + :alt: AP50 score + :align: center +| Evaluating model performance diff --git a/report/figures/val_AP50.png b/report/figures/val_AP50.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5d8ec994124e1746589ed613004c02a2ab5425 GIT binary patch literal 22751 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 Date: Fri, 6 Sep 2024 19:44:11 +0100 Subject: [PATCH 33/63] multiclass --- detectree2/models/train.py | 185 ++++++++++++++--------------- detectree2/preprocessing/tiling.py | 56 ++++++--- docs/source/tutorial.rst | 44 ++++++- report/figures/IoU_AP.png | Bin 0 -> 251986 bytes report/figures/train_val_loss.png | Bin 44808 -> 41469 bytes 5 files changed, 168 insertions(+), 117 deletions(-) create mode 100644 report/figures/IoU_AP.png diff --git a/detectree2/models/train.py b/detectree2/models/train.py index d4e1a82e..b43b3555 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -532,7 +532,7 @@ def build_test_loader(cls, cfg, dataset_name): """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) -def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = None) -> List[Dict]: +def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List[Dict]: """Get the tree dictionaries. Args: @@ -544,22 +544,14 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = List of dictionaries corresponding to segmentations of trees. Each dictionary includes bounding box around tree and points tracing a polygon around a tree. """ - if classes is not None: - # list_of_classes = crowns[variable].unique().tolist() - classes = classes - else: - classes = ["tree"] - dataset_dicts = [] for filename in [file for file in os.listdir(directory) if file.endswith(".geojson")]: json_file = os.path.join(directory, filename) with open(json_file) as f: img_anns = json.load(f) - # Turn off type checking for annotations until we have a better solution - record: Dict[str, Any] = {} - # filename = os.path.join(directory, img_anns["imagePath"]) + record = {} filename = img_anns["imagePath"] # Make sure we have the correct height and width @@ -580,43 +572,37 @@ def get_tree_dicts(directory: str, classes: List[str] = None, classes_at: str = objs = [] for features in img_anns["features"]: anno = features["geometry"] - # pdb.set_trace() - # GenusSpecies = features['properties']['Genus_Species'] px = [a[0] for a in anno["coordinates"][0]] py = [np.array(height) - a[1] for a in anno["coordinates"][0]] - # print("### HERE IS PY ###", py) poly = [(x, y) for x, y in zip(px, py)] poly = [p for x in poly for p in x] - # print("#### HERE ARE SOME POLYS #####", poly) - if classes != ["tree"]: - obj = { - "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)], - "bbox_mode": BoxMode.XYXY_ABS, - "segmentation": [poly], - "category_id": classes.index(features["properties"][classes_at]), - "iscrowd": 0, - } + + # If class mapping is provided, use it; otherwise, default to "tree" + if class_mapping: + category_id = class_mapping[features["properties"]["status"]] else: - obj = { - "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)], - "bbox_mode": BoxMode.XYXY_ABS, - "segmentation": [poly], - "category_id": 0, # id - "iscrowd": 0, - } - # pdb.set_trace() + category_id = 0 # Default to "tree" if no class mapping is provided + + obj = { + "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)], + "bbox_mode": BoxMode.XYXY_ABS, + "segmentation": [poly], + "category_id": category_id, + "iscrowd": 0, + } + objs.append(obj) - # print("#### HERE IS OBJS #####", objs) + record["annotations"] = objs dataset_dicts.append(record) + return dataset_dicts def combine_dicts(root_dir: str, val_dir: int, mode: str = "train", - classes: List[str] = None, - classes_at: str = None) -> List[Dict]: + class_mapping: Dict[str, int] = None) -> List[Dict]: """ Combine dictionaries from different directories based on the specified mode. @@ -631,11 +617,10 @@ def combine_dicts(root_dir: str, "train" excludes the validation directory, "val" includes only the validation directory, and "full" includes all directories. Defaults to "train". - classes (List[str], optional): A list of classes to filter the dictionaries by. Defaults to None. - classes_at (str, optional): A key to specify where the classes are located within the dictionary structure. Defaults to None. + class_mapping: A dictionary mapping class labels to category indices (optional). Returns: - List[Dict]: A list of combined dictionaries from the specified directories. + List of combined dictionaries from the specified directories. """ # Get a list of all directories within the root directory train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] @@ -647,15 +632,15 @@ def combine_dicts(root_dir: str, tree_dicts = [] for d in train_dirs: # Combine dictionaries from all directories except the validation directory - tree_dicts += get_tree_dicts(d, classes=classes, classes_at=classes_at) + tree_dicts += get_tree_dicts(d, class_mapping=class_mapping) elif mode == "val": # Use only the validation directory - tree_dicts = get_tree_dicts(train_dirs[(val_dir - 1)], classes=classes, classes_at=classes_at) + tree_dicts = get_tree_dicts(train_dirs[(val_dir - 1)], class_mapping=class_mapping) elif mode == "full": # Combine dictionaries from all directories, including the validation directory tree_dicts = [] for d in train_dirs: - tree_dicts += get_tree_dicts(d, classes=classes, classes_at=classes_at) + tree_dicts += get_tree_dicts(d, class_mapping=class_mapping) return tree_dicts @@ -681,35 +666,36 @@ def get_filenames(directory: str): def register_train_data(train_location, name: str = "tree", val_fold=None, - classes=None, - classes_at=None): + class_mapping_file=None): """Register data for training and (optionally) validation. Args: - train_location: directory containing training folds - name: string to name data - val_fold: fold assigned for validation and tuning. If not given, - will take place on all folds. - classes: list of classes to include - classes_at: column name for classes + train_location: Directory containing training folds. + name: Name to register the dataset. + val_fold: Validation fold index (optional). + class_mapping_file: Path to the class mapping file (json or pickle). """ + # Load the class mapping from file if provided + if class_mapping_file: + classes = load_class_mapping(class_mapping_file) + classes = list(classes.keys()) # Convert dictionary to list of class names + else: + class_mapping = None + thing_classes = ["tree"] + if val_fold is not None: for d in ["train", "val"]: - DatasetCatalog.register(name + "_" + d, lambda d=d: combine_dicts(train_location, - val_fold, d, - classes=classes, classes_at=classes_at)) - if classes is None: - MetadataCatalog.get(name + "_" + d).set(thing_classes=["tree"]) - else: - MetadataCatalog.get(name + "_" + d).set(thing_classes=classes) + DatasetCatalog.register( + name + "_" + d, + lambda d=d: combine_dicts(train_location, val_fold, d, class_mapping=class_mapping) + ) + MetadataCatalog.get(name + "_" + d).set(thing_classes=thing_classes) else: - DatasetCatalog.register(name + "_" + "full", lambda d=d: combine_dicts(train_location, - 0, "full", - classes=classes, classes_at=classes_at)) - if classes is None: - MetadataCatalog.get(name + "_" + "full").set(thing_classes=["tree"]) - else: - MetadataCatalog.get(name + "_" + "full").set(thing_classes=classes) + DatasetCatalog.register( + name + "_" + "full", + lambda d=d: combine_dicts(train_location, 0, "full", class_mapping=class_mapping) + ) + MetadataCatalog.get(name + "_" + "full").set(thing_classes=thing_classes) def get_classes(out_dir): @@ -734,6 +720,29 @@ def get_classes(out_dir): return (list) +def load_class_mapping(file_path: str): + """Function to load class-to-index mapping from a file. + + Args: + file_path: Path to the file (json or pickle) + + Returns: + class_to_idx: Loaded class-to-index mapping + """ + file_ext = Path(file_path).suffix + + if file_ext == '.json': + with open(file_path, 'r') as f: + class_to_idx = json.load(f) + elif file_ext == '.pkl': + with open(file_path, 'rb') as f: + class_to_idx = pickle.load(f) + else: + raise ValueError("Unsupported file format. Use '.json' or '.pkl'.") + + return class_to_idx + + def remove_registered_data(name="tree"): """Remove registered data from catalog. @@ -986,45 +995,25 @@ def get_latest_model_path(output_dir: str) -> str: if __name__ == "__main__": - train_location = "/content/drive/Shareddrives/detectree2/data/Paracou/tiles/train/" - register_train_data(train_location, "Paracou", 1) # folder, name, validation fold - - name = "Paracou2019" - train_location = "/content/drive/Shareddrives/detectree2/data/Paracou/tiles2019/train/" - dataset_dicts = combine_dicts(train_location, 1) - trees_metadata = MetadataCatalog.get(name + "_train") - # dataset_dicts = get_tree_dicts("./") - for d in dataset_dicts: - img = cv2.imread(d["file_name"]) - visualizer = Visualizer(img[:, :, ::-1], metadata=trees_metadata, scale=0.5) - out = visualizer.draw_dataset_dict(d) - image = cv2.cvtColor(out.get_image()[:, :, ::-1], cv2.COLOR_BGR2RGB) - # display(Image.fromarray(image)) - # Set the base (pre-trained) model from the detectron2 model_zoo - model = "COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml" - # Set the names of the registered train and test sets - # pretrained model? - # trained_model = "/content/drive/Shareddrives/detectree2/models/220629_ParacouSepilokDanum_JB.pth" - trains = ( - "Paracou_train", - "Paracou2019_train", - "ParacouUAV_train", - "Danum_train", - "SepilokEast_train", - "SepilokWest_train", - ) - tests = ( - "Paracou_val", - "Paracou2019_val", - "ParacouUAV_val", - "Danum_val", - "SepilokEast_val", - "SepilokWest_val", + # Define paths to training data and optional class mapping file + train_location = "/path/to/your/train/location" + class_mapping_file = "/path/to/your/class_to_idx.json" # Optional, can be None + + # Register the training and validation datasets using the class mapping + # If class_mapping_file is not provided, defaults to "tree" + register_train_data(train_location, "MyDataset", val_fold=1, class_mapping_file=class_mapping_file) + + # Set up model configuration, using the class mapping to determine the number of classes + cfg = setup_cfg( + base_model="COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml", + trains=("MyDataset_train", ), + tests=("MyDataset_val", ), + max_iter=3000, + out_dir="/path/to/output", + class_mapping_file=class_mapping_file # Optional ) - out_dir = "/content/drive/Shareddrives/detectree2/220703_train_outputs" - # update_model arg can be used to load in trained model - cfg = setup_cfg(model, trains, tests, eval_period=100, max_iter=3000, out_dir=out_dir) + # Train the model trainer = MyTrainer(cfg, patience=4) trainer.resume_or_load(resume=False) trainer.train() diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 3f9df300..c6b70e44 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -267,6 +267,8 @@ def process_tile_train( threshold, nan_threshold, mode: str = "rgb", + class_column: str = 'status', # Allow user to specify class column + class_mapping_file: str = None ) -> None: """Process a single tile for training data. @@ -288,6 +290,9 @@ def process_tile_train( Returns: None """ + # Load the class-to-index mapping + class_to_idx = load_class_mapping(class_mapping_file) if class_mapping_file else None + if mode == "rgb": result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) @@ -315,7 +320,18 @@ def process_tile_train( try: filename = out_path_root.with_suffix(".geojson") moved_scaled = overlapping_crowns.set_geometry(moved_scaled) + + # Ensure we map the selected column to the 'status' field + moved_scaled['status'] = moved_scaled[class_column] + + if class_to_idx: + moved_scaled["category_id"] = moved_scaled["status"].map(class_to_idx) + + # Save the result as GeoJSON, replacing the original class column with 'status' + moved_scaled = moved_scaled[['geometry', 'status']] # Keep only 'status' and geometry moved_scaled.to_file(driver="GeoJSON", filename=filename) + + # Add image path info to the GeoJSON file with open(filename, "r") as f: shp = json.load(f) shp.update(impath) @@ -340,6 +356,8 @@ def tile_data( nan_threshold: float = 0.1, dtype_bool: bool = False, mode: str = "rgb", + class_column: str = "status", # Allow class column to be passed here + class_mapping_file: str = None # Allow optional class mapping ) -> None: """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. @@ -371,7 +389,7 @@ def tile_data( tile_args = [ (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, - threshold, nan_threshold, mode) + threshold, nan_threshold, mode, class_column, class_mapping_file) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) @@ -427,35 +445,41 @@ def is_overlapping_box(test_boxes_array, train_box): return False -def record_classes(crowns, - out_dir, - column='status'): - """Function that will record a list of classes into a file that can be read during training. +def record_classes(crowns: gpd.GeoDataFrame, out_dir: str, column: str = 'status', save_format: str = 'json'): + """Function that records a list of classes into a file that can be read during training. Args: crowns: gpd dataframe with the crowns out_dir: directory to save the file column: column name to get the classes from + save_format: format to save the file ('json' or 'pickle') Returns: None """ - + # Extract unique class names from the specified column list_of_classes = crowns[column].unique().tolist() - + # Sort the list of classes in alphabetical order list_of_classes.sort() - print("**The list of classes are:**") - print(list_of_classes) - print("**The list has been saved to the out_dir**") + # Create a dictionary for class-to-index mapping + class_to_idx = {class_name: idx for idx, class_name in enumerate(list_of_classes)} + + # Save the class-to-index mapping to disk + out_path = Path(out_dir) + os.makedirs(out_path, exist_ok=True) + + if save_format == 'json': + with open(out_path / 'class_to_idx.json', 'w') as f: + json.dump(class_to_idx, f) + elif save_format == 'pickle': + with open(out_path / 'class_to_idx.pkl', 'wb') as f: + pickle.dump(class_to_idx, f) + else: + raise ValueError("Unsupported save format. Use 'json' or 'pickle'.") - # Write it into file "classes.txt" - out_tif = out_dir + 'classes.txt' - f = open(out_tif, "w") - for i in list_of_classes: - f.write("%s\n" % i) - f.close() + print(f"Classes saved as {save_format} file: {class_to_idx}") def to_traintest_folders( # noqa: C901 diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 0370663f..2f2ad214 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -490,7 +490,7 @@ to plot the training and validation loss. The following code can be used to plot .. image:: ../../report/figures/train_val_loss.png :width: 400 - :alt: Traina and validation loss + :alt: Train and validation loss :align: center | @@ -499,7 +499,7 @@ training loss continued to decrease. The ``patience`` mechanism prevented traini preventing overfitting. If validation loss is substantially higher than training loss, the model may be overfitted. To understand how the segmentation performance improves through training, it is also possible to plot the AP50 score -over the iterations. This can be done with the following code: +(see below for definition) over the iterations. This can be done with the following code: .. code-block:: python @@ -517,18 +517,56 @@ over the iterations. This can be done with the following code: plt.xlabel('Number of Iterations') plt.show() -.. image:: ../../report/figures/AP50.png +.. image:: ../../report/figures/val_AP50.png :width: 400 :alt: AP50 score :align: center | +Performance metrics +------------------- + +In instance segmentation, **AP50** refers to the **Average Precision** at an Intersection over Union (IoU) threshold of +**50%**. + +- **Precision**: Precision is the ratio of correctly predicted positive objects (true positives) to all predicted + bjects (both true positives and false positives). + + - Formula: :math:`\text{Precision} = \frac{\text{True Positives}}{\text{True Positives} + \text{False Positives}}` + +- **Recall**: Recall is the ratio of correctly predicted positive objects (true positives) to all actual positive +objects in the ground truth (true positives and false negatives). + + - Formula: :math:`\text{Recall} = \frac{\text{True Positives}}{\text{True Positives} + \text{False Negatives}}` + +- **Average Precision (AP)**: AP is a common metric used to evaluate the performance of object detection and instance +segmentation models. It represents the precision of the model across various recall levels. In simpler terms, it is a +combination of the model's ability to correctly detect objects and how complete those detections are. + +- **IoU (Intersection over Union)**: IoU measures the overlap between the predicted segmentation mask (or bounding box +in object detection) and the ground truth mask. It is calculated as the area of overlap divided by the area of union +between the predicted and true masks. + +- **AP50**: Specifically, **AP50** computes the average precision for all object classes at a threshold of **50% IoU**. +This means that a predicted object is considered correct (a true positive) if the IoU between the predicted and ground +truth masks is greater than or equal to 0.5 (50%). It is a relatively lenient threshold, focusing on whether the +detected objects overlap reasonably with the ground truth, even if the boundaries aren't perfectly aligned. + +In summary, AP50 evaluates how well a model detects objects with a 50% overlap between the predicted and ground truth +masks in instance segmentation tasks. + +.. image:: ../../report/figures/IoU_AP.png + :width: 400 + :alt: IoU and AP illustration + :align: center + Evaluating model performance ---------------------------- Coming soon! See Colab notebook for example routine (``detectree2/notebooks/colab/evaluationJB.ipynb``). + Generating landscape predictions -------------------------------- diff --git a/report/figures/IoU_AP.png b/report/figures/IoU_AP.png new file mode 100644 index 0000000000000000000000000000000000000000..603778b1c02958a38316db332b5748f78bc3cf8f GIT binary patch literal 251986 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

o5N%r;`sk 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 z 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 literal 0 HcmV?d00001 diff --git a/report/figures/train_val_loss.png b/report/figures/train_val_loss.png index d1be3e7df2a6c7ccca8dc0debc8841a5efea01ef..2f9d0d1868415f065e14c7308510c0321a46c281 100644 GIT binary patch literal 41469 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 U(}2Z{HyD7x)78&qol`;+09Bn*uK)l5 literal 44808 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 Date: Fri, 6 Sep 2024 19:48:03 +0100 Subject: [PATCH 34/63] multiclass --- detectree2/preprocessing/tiling.py | 1 - 1 file changed, 1 deletion(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index c6b70e44..f1b8f17d 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -379,7 +379,6 @@ def tile_data( Returns: None - """ out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) From 774a15fa00338304243a11fbe0ae120d59d2ac9b Mon Sep 17 00:00:00 2001 From: James Ball Date: Fri, 6 Sep 2024 20:20:29 +0100 Subject: [PATCH 35/63] multiclass --- detectree2/models/train.py | 24 +------ detectree2/preprocessing/tiling.py | 101 ++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 45 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index b43b3555..919e26a9 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -21,6 +21,7 @@ import rasterio import torch import torch.nn as nn +from detectree2.preprocessing.tiling import load_class_mapping from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 from detectron2.config import get_cfg @@ -720,29 +721,6 @@ def get_classes(out_dir): return (list) -def load_class_mapping(file_path: str): - """Function to load class-to-index mapping from a file. - - Args: - file_path: Path to the file (json or pickle) - - Returns: - class_to_idx: Loaded class-to-index mapping - """ - file_ext = Path(file_path).suffix - - if file_ext == '.json': - with open(file_path, 'r') as f: - class_to_idx = json.load(f) - elif file_ext == '.pkl': - with open(file_path, 'rb') as f: - class_to_idx = pickle.load(f) - else: - raise ValueError("Unsupported file format. Use '.json' or '.pkl'.") - - return class_to_idx - - def remove_registered_data(name="tree"): """Remove registered data from catalog. diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index f1b8f17d..b4acdfe1 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -8,6 +8,7 @@ import json import logging import os +import pickle import random import shutil import warnings @@ -55,6 +56,29 @@ def get_features(gdf: gpd.GeoDataFrame): return [json.loads(gdf.to_json())["features"][0]["geometry"]] +def load_class_mapping(file_path: str): + """Function to load class-to-index mapping from a file. + + Args: + file_path: Path to the file (json or pickle) + + Returns: + class_to_idx: Loaded class-to-index mapping + """ + file_ext = Path(file_path).suffix + + if file_ext == '.json': + with open(file_path, 'r') as f: + class_to_idx = json.load(f) + elif file_ext == '.pkl': + with open(file_path, 'rb') as f: + class_to_idx = pickle.load(f) + else: + raise ValueError("Unsupported file format. Use '.json' or '.pkl'.") + + return class_to_idx + + def process_tile( img_path: str, out_dir: str, @@ -565,29 +589,62 @@ def to_traintest_folders( # noqa: C901 if __name__ == "__main__": - # Right let"s test this first with Sepilok 10cm resolution, then I need to try it with 50cm resolution. - img_path = "/content/drive/Shareddrives/detectreeRGB/benchmark/Ortho2015_benchmark/P4_Ortho_2015.tif" - crown_path = "gdrive/MyDrive/JamesHirst/NY/Buffalo/Buffalo_raw_data/all_crowns.shp" - out_dir = "./" - # Read in the tiff file - # data = img_data.open(img_path) - # Read in crowns - data = rasterio.open(img_path) + # Define paths to the input data + img_path = "/path/to/your/orthomosaic.tif" # Path to your input orthomosaic file + crown_path = "/path/to/your/crown_shapefile.shp" # Path to the shapefile containing crowns + out_dir = "/path/to/output/directory" # Directory where you want to save the tiled output + + # Optional parameters for tiling and processing + buffer = 30 # Overlap between tiles (in meters) + tile_width = 200 # Tile width (in meters) + tile_height = 200 # Tile height (in meters) + nan_threshold = 0.1 # Max proportion of tile that can be NaN before it's discarded + threshold = 0.5 # Minimum crown coverage per tile for it to be kept (0-1) + dtype_bool = False # Change dtype to uint8 to avoid black tiles + mode = "rgb" # Use 'rgb' for regular 3-channel imagery, 'ms' for multispectral + class_column = "species" # Column in the crowns file to use as the class label + + # Read in the crowns crowns = gpd.read_file(crown_path) - print( - "shape =", - data.shape, - ",", - data.bounds, - "and number of bands =", - data.count, - ", crs =", - data.crs, + + # Record the classes and save the class mapping + record_classes( + crowns=crowns, # Geopandas dataframe with crowns + out_dir=out_dir, # Output directory to save class mapping + column=class_column, # Column used for classes + save_format='json' # Choose between 'json' or 'pickle' ) - buffer = 20 - tile_width = 200 - tile_height = 200 + # Load the class-to-index mapping from the recorded classes + class_mapping_file = os.path.join(out_dir, "class_to_idx.json") + with open(class_mapping_file, 'r') as f: + class_to_idx = json.load(f) + + # Perform the tiling, ensuring the selected class column is used + tile_data( + img_path=img_path, + out_dir=out_dir, + buffer=buffer, + tile_width=tile_width, + tile_height=tile_height, + crowns=crowns, + threshold=threshold, + nan_threshold=nan_threshold, + dtype_bool=dtype_bool, + mode=mode, + class_column=class_column, # Use the selected class column (e.g., 'species', 'status') + class_mapping_file=None # We're now passing None since we don't need an external file + ) + + # Split the data into training and validation sets (optional) + # This can be used for train/test folder creation based on the generated tiles + to_traintest_folders( + tiles_folder=out_dir, # Directory where tiles are saved + out_folder="/path/to/final/data/output", # Final directory for train/test data + test_frac=0.2, # Fraction of data to be used for testing + folds=5, # Number of folds (optional, can be set to 1 for no fold splitting) + strict=True, # Ensure no overlap between train/test tiles + seed=42 # Set seed for reproducibility + ) - tile_data(data, out_dir, buffer, tile_width, tile_height, crowns) - to_traintest_folders(folds=5) + logger.info("Tiling process completed successfully!") From 993fedcbc099c3d440ec28f2e2d0ea2876307d84 Mon Sep 17 00:00:00 2001 From: James Ball Date: Sat, 7 Sep 2024 02:01:43 +0100 Subject: [PATCH 36/63] tutorial updates --- detectree2/preprocessing/tiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index b4acdfe1..b05d0148 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -408,7 +408,7 @@ def tile_data( os.makedirs(out_path, exist_ok=True) tilename = Path(img_path).stem with rasterio.open(img_path) as data: - crs = data.crs.to_string() # Update CRS handling to avoid deprecated syntax + crs = data.crs.to_epsg() # Update CRS handling to avoid deprecated syntax tile_args = [ (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, From 65921f99162b2d0a421b98e7485af8d0380405b7 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 9 Sep 2024 13:39:20 +0100 Subject: [PATCH 37/63] classes fix --- detectree2/models/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 919e26a9..aa728521 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -679,7 +679,7 @@ def register_train_data(train_location, # Load the class mapping from file if provided if class_mapping_file: classes = load_class_mapping(class_mapping_file) - classes = list(classes.keys()) # Convert dictionary to list of class names + thing_classes = list(classes.keys()) # Convert dictionary to list of class names else: class_mapping = None thing_classes = ["tree"] From 92b0853508e72139b82379b636eec02750945c5f Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 9 Sep 2024 14:24:21 +0100 Subject: [PATCH 38/63] classes cfg --- detectree2/models/train.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index aa728521..19b96f34 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -772,12 +772,12 @@ def setup_cfg( base_lr=0.0003389, weight_decay=0.001, max_iter=1000, - num_classes=1, eval_period=100, out_dir="./train_outputs", resize="fixed", # fixed or random or rand_fixed imgmode="rgb", num_bands=3, + class_mapping_file=None, ): """Set up config object # noqa: D417. @@ -799,8 +799,19 @@ def setup_cfg( num_classes: number of classes eval_period: number of iterations between evaluations out_dir: directory to save outputs + resize: resize strategy for images + imgmode: image mode (rgb or multispectral) + num_bands: number of bands in the image + class_mapping_file: path to class mapping file """ + # Load the class mapping if provided + if class_mapping_file: + class_mapping = load_class_mapping(class_mapping_file) + num_classes = len(class_mapping) # Set the number of classes based on the mapping + else: + num_classes = 1 # Default to 1 class if no mapping is provided + # Validate the resize parameter if resize not in {"fixed", "random", "rand_fixed"}: raise ValueError(f"Invalid resize option '{resize}'. Must be 'fixed', 'random', or 'rand_fixed'.") From d98f69f26fc3ab0b5df378c13a3ed1227240d1f6 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 9 Sep 2024 16:05:18 +0100 Subject: [PATCH 39/63] class handling --- detectree2/models/train.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 19b96f34..44a1108b 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -100,6 +100,9 @@ def __call__(self, dataset_dict): self.logger.warning("Received None for dataset_dict, skipping this entry.") return None + if cfg.IMGMODE == "rgb": + return super().__call__(dataset_dict) + try: # Handle multi-band image loading using rasterio with rasterio.open(dataset_dict["file_name"]) as src: @@ -116,8 +119,8 @@ def __call__(self, dataset_dict): ) # If it's a 3-band image, delegate processing to the parent class - if img.shape[-1] == 3: - return super().__call__(dataset_dict) + #if img.shape[-1] == 3: + # return super().__call__(dataset_dict) # Otherwise, handle custom multi-band logic aug_input = T.AugInput(img) @@ -624,8 +627,15 @@ def combine_dicts(root_dir: str, List of combined dictionaries from the specified directories. """ # Get a list of all directories within the root directory - train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] + # train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] + + print(class_mapping) + train_dirs = [ + os.path.join(root_dir, dir) + for dir in os.listdir(root_dir) + if os.path.isdir(os.path.join(root_dir, dir)) + ] # Handle the different modes for combining dictionaries if mode == "train": # Exclude the validation directory from the list of directories @@ -677,11 +687,12 @@ def register_train_data(train_location, class_mapping_file: Path to the class mapping file (json or pickle). """ # Load the class mapping from file if provided + class_mapping = None if class_mapping_file: - classes = load_class_mapping(class_mapping_file) - thing_classes = list(classes.keys()) # Convert dictionary to list of class names + class_mapping = load_class_mapping(class_mapping_file) + thing_classes = list(class_mapping.keys()) # Convert dictionary to list of class names + print(f"Class mapping loaded: {class_mapping}") # Debugging step else: - class_mapping = None thing_classes = ["tree"] if val_fold is not None: From 555cf0676595fc7bff8a75227a9f6b886f190c5d Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 9 Sep 2024 16:27:56 +0100 Subject: [PATCH 40/63] class handling --- detectree2/models/train.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 44a1108b..0236f4d1 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -100,7 +100,7 @@ def __call__(self, dataset_dict): self.logger.warning("Received None for dataset_dict, skipping this entry.") return None - if cfg.IMGMODE == "rgb": + if self.cfg.IMGMODE == "rgb": return super().__call__(dataset_dict) try: @@ -548,6 +548,8 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List List of dictionaries corresponding to segmentations of trees. Each dictionary includes bounding box around tree and points tracing a polygon around a tree. """ + #print("get ", class_mapping) + dataset_dicts = [] for filename in [file for file in os.listdir(directory) if file.endswith(".geojson")]: @@ -629,7 +631,7 @@ def combine_dicts(root_dir: str, # Get a list of all directories within the root directory # train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] - print(class_mapping) + #print(class_mapping) train_dirs = [ os.path.join(root_dir, dir) From 7fadcc756cd875d94ac8bc35fe2d1b169811d202 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 9 Sep 2024 17:27:01 +0100 Subject: [PATCH 41/63] test loader --- detectree2/models/train.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 0236f4d1..9e393f70 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -753,8 +753,17 @@ def register_test_data(test_location, name="tree"): name: string to name data """ d = "test" - DatasetCatalog.register(name + "_" + d, lambda d=d: get_tree_dicts(test_location)) - MetadataCatalog.get(name + "_" + d).set(thing_classes=["tree"]) + + class_mapping = None + if class_mapping_file: + class_mapping = load_class_mapping(class_mapping_file) + thing_classes = list(class_mapping.keys()) # Convert dictionary to list of class names + print(f"Class mapping loaded: {class_mapping}") # Debugging step + else: + thing_classes = ["tree"] + + DatasetCatalog.register(name + "_" + d, lambda d=d: get_tree_dicts(test_location, class_mapping)) + MetadataCatalog.get(name + "_" + d).set(thing_classes=thing_classes) def load_json_arr(json_path): From 516d38f7811623b72c81d0cd7397959cc4e28658 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 15:37:17 +0100 Subject: [PATCH 42/63] class columns reduced --- detectree2/preprocessing/tiling.py | 22 +++---- docs/source/index.rst | 1 + docs/source/tutorial_multi.rst | 98 ++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index b05d0148..55aa7897 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -291,8 +291,7 @@ def process_tile_train( threshold, nan_threshold, mode: str = "rgb", - class_column: str = 'status', # Allow user to specify class column - class_mapping_file: str = None + class_column: str = None, # Allow user to specify class column ) -> None: """Process a single tile for training data. @@ -314,9 +313,6 @@ def process_tile_train( Returns: None """ - # Load the class-to-index mapping - class_to_idx = load_class_mapping(class_mapping_file) if class_mapping_file else None - if mode == "rgb": result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold) @@ -345,14 +341,16 @@ def process_tile_train( filename = out_path_root.with_suffix(".geojson") moved_scaled = overlapping_crowns.set_geometry(moved_scaled) - # Ensure we map the selected column to the 'status' field - moved_scaled['status'] = moved_scaled[class_column] - - if class_to_idx: - moved_scaled["category_id"] = moved_scaled["status"].map(class_to_idx) + if class_column is not None: + # Ensure we map the selected column to the 'status' field + moved_scaled['status'] = moved_scaled[class_column] + # Keep only 'status' and geometry + moved_scaled = moved_scaled[['geometry', 'status']] + else: + # Keep only geometry + moved_scaled = moved_scaled[['geometry']] - # Save the result as GeoJSON, replacing the original class column with 'status' - moved_scaled = moved_scaled[['geometry', 'status']] # Keep only 'status' and geometry + # Save the result as GeoJSON moved_scaled.to_file(driver="GeoJSON", filename=filename) # Add image path info to the GeoJSON file diff --git a/docs/source/index.rst b/docs/source/index.rst index 255eefc3..545790ab 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -42,6 +42,7 @@ Accurate delineation of individual tree crowns in tropical forests from aerial R installation tutorial + tutorial_multi contributing using-git .. _notebooks/contributing_guide diff --git a/docs/source/tutorial_multi.rst b/docs/source/tutorial_multi.rst index 21b1fc72..9f860e42 100644 --- a/docs/source/tutorial_multi.rst +++ b/docs/source/tutorial_multi.rst @@ -4,9 +4,10 @@ Tutorial (multiclass) This tutorial goes through the steps of multiclass detection and delineation (e.g. species mapping, disease mapping). A guide to single class prediction is available -`here `_. The multiclass -process is more complicated than single class prediction as the classes need to -be correctly encoded in the data. +`here `_ - this covers +more detail on the fundamentals of training and should be reviewed before this +tutorial. The multiclassprocess is slightly more intricate than single class +prediction as the classes need to be correctly encoded and caried throughout the pipeline. The key steps are: @@ -15,4 +16,93 @@ The key steps are: 3. Evaluating model performance 4. Making landscape level predictions -THE REST OF THIS TUTORIAL IS UNDER CONSTRUCTION + + +Preparing data (RGB and multispectral) +-------------------------------------- + +Data can be prepared in a similar way to the single class case but the classes +and their order (mapping) need to be saved so that they can be accessed +consistently across training and prediction. The classes are saved in a json +file with the class names and their indices. The indices are used to encode +the classes in the training. + +.. code-block:: python + + import rasterio + import geopandas as gpd + + # Load the data + base_dir = "/content/drive/MyDrive/SHARED/detectree2" + + site_path = base_dir + "/data/Danum_lianas" + + # Set the path to the orthomosaic and the crown shapefile + img_path = site_path + "/rgb/2017_50ha_Ortho_reproject.tif" + crown_path = site_path + "/crowns/Danum_lianas_full2017.gpkg" + + # Here, we set the name of the output folder. + # Set tiling parameters + buffer = 30 + tile_width = 40 + tile_height = 40 + threshold = 0.6 + appends = str(tile_width) + "_" + str(buffer) + "_" + str(threshold) + + out_dir = site_path + "/tilesClass_" + appends + "/" + + # Read in the tiff file + data = rasterio.open(img_path) + + # Read in crowns (then filter by an attribute?) + crowns = gpd.read_file(crown_path) + crowns = crowns.to_crs(data.crs.data) + print(crowns.head()) + + class_column = 'status' + + # Record the classes and save the class mapping + record_classes( + crowns=crowns, # Geopandas dataframe with crowns + out_dir=out_dir, # Output directory to save class mapping + column=class_column, # Column to be used for classes + save_format='json' # Choose between 'json' or 'pickle' + ) + + class_mapping_file = os.path.join(out_dir, "class_to_idx.json") + +The class mapping has been saved in the output directory as a json file called +``class_to_idx.json``. This file can now be accessed to encode the classes in +training and prediction steps. + +To tile the data, we call the ``tile_data`` function as we did in the single +class case except now we point to the column name of the classes. + +.. code-block:: python + + # Tile the data + tile_data( + img_path=img_path, # Path to the orthomosaic + out_dir=out_dir, # Output directory to save tiles + buffer=buffer, # Buffer around the crowns + tile_width=tile_width, # Width of the tiles + tile_height=tile_height, # Height of the tiles + crowns=crowns, # Geopandas dataframe with crowns + threshold=threshold, # Threshold for the buffer + class_column=class_column, # Column to be used for classes + ) + + # Split the data into training and validation sets + to_traintest_folders( + tiles_folder=out_dir, # Directory where tiles are saved + out_folder=out_dir, # Final directory for train/test data + test_frac=0, # Fraction of data to be used for testing + folds=5, # Number of folds (optional, can be set to 1 for no fold splitting) + strict=False, # Ensure no overlap between train/test tiles + seed=42 # Set seed for reproducibility + ) + + +Training models +--------------- + From 09ae19a6dd3344681c6019cc6b9aa491fe275511 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 15:38:38 +0100 Subject: [PATCH 43/63] class columns reduced --- detectree2/preprocessing/tiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 55aa7897..0eacabcf 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -347,7 +347,7 @@ def process_tile_train( # Keep only 'status' and geometry moved_scaled = moved_scaled[['geometry', 'status']] else: - # Keep only geometry + # Keep only geometry to reduce file size moved_scaled = moved_scaled[['geometry']] # Save the result as GeoJSON From db5c4b80d9b63df4c0a835d33c596158175d15a5 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 15:48:33 +0100 Subject: [PATCH 44/63] class columns reduced --- detectree2/preprocessing/tiling.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 0eacabcf..9e88f8e7 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -378,8 +378,7 @@ def tile_data( nan_threshold: float = 0.1, dtype_bool: bool = False, mode: str = "rgb", - class_column: str = "status", # Allow class column to be passed here - class_mapping_file: str = None # Allow optional class mapping + class_column: str = None, # Allow class column to be passed here ) -> None: """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. @@ -410,7 +409,7 @@ def tile_data( tile_args = [ (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, - threshold, nan_threshold, mode, class_column, class_mapping_file) + threshold, nan_threshold, mode, class_column) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) From acc90e37e5335e724ea482a613740724c8bbff34 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 17:53:37 +0100 Subject: [PATCH 45/63] tutorial updates --- docs/source/tutorial.rst | 88 ++++++++++++++++++++++++++++++++++ docs/source/tutorial_multi.rst | 71 ++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 2f2ad214..f8863763 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -457,6 +457,94 @@ Training can now commence as before: trainer.train() +Data augmentation +----------------- + +Data augmentation is a technique used to artificially increase the size of the training dataset by applying random +transformations to the input data. This can help improve the generalization of the model and reduce overfitting. The +``detectron2`` library provides a range of data augmentation options that can be used during training. These include +random flipping, scaling, rotation, and color jittering. + +Additionally, resizing of the input data can be applied as an augmentation technique. This can be useful when training +a model that should be flexible with respect to tile size and resolution. + +By default, random rotations and flips will be performed on input images. + +.. code-block:: python + + augmentations = [ + T.RandomRotation(angle=[90, 90], expand=False), + T.RandomFlip(prob=0.4, horizontal=True, vertical=False), + T.RandomFlip(prob=0.4, horizontal=False, vertical=True), + ] + +If the input data is RGB, additional augmentations will be applied to adjust the brightness, contrast, saturation, and +lighting of the images. These augmentations are only available for RGB images and will not be applied to multispectral. + +.. code-block:: python + # Additional augmentations for RGB images + if cfg.IMGMODE == "rgb": + augmentations.extend([ + T.RandomBrightness(0.7, 1.5), + T.RandomLighting(0.7), + T.RandomContrast(0.6, 1.3), + T.RandomSaturation(0.8, 1.4) + ]) + +There are three resizing modes for the input data (1) ``fixed``, (2) ``random``, and (3) ``rand_fixed``. This are set +in the configuration file (``cfg``) with the `setup_cfg` function. + +The ``fixed`` mode will resize the input data to a images width of 1000 pixelwise + +.. code-block:: python + + if cfg.RESIZE == "fixed": + augmentations.append(T.ResizeShortestEdge([1000, 1000], 1333)) + +The ``random`` mode will randomly resize (and resample to change the resolutions) the input data to between 0.6 and 1.4 +times the original height/width. This can help the model learn to detect objects at different scales and from images of +different resolutions (and sensors). + +.. code-block:: python + + elif cfg.RESIZE == "random": + size = None + for i, datas in enumerate(DatasetCatalog.get(cfg.DATASETS.TRAIN[0])): + location = datas['file_name'] + try: + # Try to read with cv2 (for RGB images) + img = cv2.imread(location) + if img is not None: + size = img.shape[0] + else: + # Fall back to rasterio for multi-band images + with rasterio.open(location) as src: + size = src.height # Assuming square images + except Exception as e: + # Handle any errors that occur during loading + print(f"Error loading image {location}: {e}") + continue + break + + if size: + print("ADD RANDOM RESIZE WITH SIZE = ", size) + augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) + +The ``rand_fixed`` mode constrains the random resizing to a fixed pixel width/height range (regardless of the resolution +of the input data). This can help to speed up training if the input tiles are high resolution and pushing up against +available memory limits. It retains the benefits of random resizing but constrains the range of possible sizes. + +.. code-block:: python + + elif cfg.RESIZE == "rand_fixed": + augmentations.append(T.ResizeScale(0.6, 1.4, 1000, 1000)) + +Which resizing option is selected depends on the problem at hand. A more precise delineation can be generated if high +resolution images are retained but this comes at the cost of increased memory usage and slower training times. If the +model is to be used on a range of different resolutions, random resizing can help the model learn to detect objects at +different scales. + + Post-training (check training convergence) ------------------------------------------ diff --git a/docs/source/tutorial_multi.rst b/docs/source/tutorial_multi.rst index 9f860e42..6c157090 100644 --- a/docs/source/tutorial_multi.rst +++ b/docs/source/tutorial_multi.rst @@ -69,7 +69,6 @@ the classes in the training. save_format='json' # Choose between 'json' or 'pickle' ) - class_mapping_file = os.path.join(out_dir, "class_to_idx.json") The class mapping has been saved in the output directory as a json file called ``class_to_idx.json``. This file can now be accessed to encode the classes in @@ -106,3 +105,73 @@ class case except now we point to the column name of the classes. Training models --------------- +To train with multiple classes, we need to ensure that the classes are +registered correctly in the dataset catalogue. This can be done with the class +mapping file that was saved in the previous step. The class mapping file will +set the classes and their indices. + +.. code-block:: python + + from detectree2.models.train import register_train_data, remove_registered_data, setup_cfg, MyTrainer + from detectree2.preprocessing.tiling import load_class_mapping + + # Set validation fold + val_fold = 5 + + site_path = base_dir + "/data/Danum_lianas" + train_dir = site_path + "/tilesClass_40_30_0.6/train" + class_mapping_file = site_path + "/tilesClass_40_30_0.6/" + "/class_to_idx.json" + data_name = "DanumLiana" + + register_train_data(train_dir, data_name, val_fold=val_fold, class_mapping_file=class_mapping_file) + + +Now the data is registered, should generate the configuration (`cfg`) and train +the model. By passing the class mapping file to the configuration set up, the +`cfg` will be register the number of classes. + +.. code-block:: python + + from detectron2.modeling import build_model + from detectron2.modeling.roi_heads.fast_rcnn import FastRCNNOutputLayers + import numpy as np + from datetime import date + + + today = date.today() + today = today.strftime("%y%m%d") + + names = [data_name,] + + trains = (names[0] + "_train",) + tests = (names[0] + "_val",) + out_dir = "/content/drive/MyDrive/WORK/detectree2/models/" + today + "_Danum_lianas" + + base_model = "COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml" # Path to the model config + + # When you increase the number of channels (i.e., the number of filters) in a Convolutional Neural Network (CNN), the general recommendation is to decrease the learning rate + lrs = [0.03, 0.003, 0.0003, 0.00003] + + # Set up model configuration, using the class mapping to determine the number of classes + cfg = setup_cfg( + base_model="COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml", + trains=trains, + tests=tests, + max_iter=500000, + eval_period=50, + base_lr=lrs[0], + out_dir=out_dir, + resize="rand_fixed", + class_mapping_file=class_mapping_file # Optional + ) + + # Train the model + trainer = MyTrainer(cfg, patience=5) + trainer.resume_or_load(resume=False) + trainer.train() + + +Landscape predictions +--------------------- + +COMING SOON \ No newline at end of file From a6ccbb9d13940806021969b7b151558d1af3184d Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 17:57:23 +0100 Subject: [PATCH 46/63] tutorial updates --- docs/source/tutorial.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index f8863763..a0be35d0 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -494,7 +494,8 @@ lighting of the images. These augmentations are only available for RGB images an There are three resizing modes for the input data (1) ``fixed``, (2) ``random``, and (3) ``rand_fixed``. This are set in the configuration file (``cfg``) with the `setup_cfg` function. -The ``fixed`` mode will resize the input data to a images width of 1000 pixelwise +The ``fixed`` mode will resize the input data to a images width/height of 1000 pixels. This is efficient but may not +lead to models that transfer well across scales (e.g. if the model is to be used on a range of different resolutions). .. code-block:: python From f26e2512644fc93a9f93fa102bd77f5008d26e61 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 18:59:40 +0100 Subject: [PATCH 47/63] update test --- detectree2/tests/test_preprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detectree2/tests/test_preprocessing.py b/detectree2/tests/test_preprocessing.py index 6f2eb9cf..49a7d383 100644 --- a/detectree2/tests/test_preprocessing.py +++ b/detectree2/tests/test_preprocessing.py @@ -62,11 +62,11 @@ def test_tiling(self): tile_height = 40 threshold = 0.2 - from detectree2.preprocessing.tiling import tile_data_train + from detectree2.preprocessing.tiling import tile_data out_dir = os.path.join(out_dir, "tiles") - tile_data_train(data, out_dir, buffer, tile_width, tile_height, crowns, threshold) + tile_data(img_path, out_dir, buffer, tile_width, tile_height, crowns, threshold) # TODO: install pytest-depends to automatically order From 0fea6b47c6ff05da5af64bed77fd6c866e5e071a Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 19:31:59 +0100 Subject: [PATCH 48/63] update test --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 176f2b86..3d6173d1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="detectree2", - version="1.0.8", + version="1.1.0", author="James G. C. Ball", author_email="ball.jgc@gmail.com", description="Detectree packaging", @@ -20,7 +20,7 @@ "pypng", "pygeos", "shapely", - "geopandas", + "geopandas>=0.14.4", "rasterio==1.3a3", "fiona", "pycrs", From f9c06de82036f053855af73f0444484283ec1f19 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 19:35:26 +0100 Subject: [PATCH 49/63] update test --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3d6173d1..bd80e31d 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ "pypng", "pygeos", "shapely", - "geopandas>=0.14.4", + "geopandas", "rasterio==1.3a3", - "fiona", + "fiona==1.9.6", "pycrs", "descartes", "detectron2@git+https://github.com/facebookresearch/detectron2.git", From 77b5f2e338b80cfe9594889e9d1bc23d18aa749e Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 21:20:02 +0100 Subject: [PATCH 50/63] mypy --- detectree2/preprocessing/tiling.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 9e88f8e7..b7d0bd9b 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -612,11 +612,6 @@ def to_traintest_folders( # noqa: C901 save_format='json' # Choose between 'json' or 'pickle' ) - # Load the class-to-index mapping from the recorded classes - class_mapping_file = os.path.join(out_dir, "class_to_idx.json") - with open(class_mapping_file, 'r') as f: - class_to_idx = json.load(f) - # Perform the tiling, ensuring the selected class column is used tile_data( img_path=img_path, @@ -629,8 +624,7 @@ def to_traintest_folders( # noqa: C901 nan_threshold=nan_threshold, dtype_bool=dtype_bool, mode=mode, - class_column=class_column, # Use the selected class column (e.g., 'species', 'status') - class_mapping_file=None # We're now passing None since we don't need an external file + class_column=class_column # Use the selected class column (e.g., 'species', 'status') ) # Split the data into training and validation sets (optional) From 31d40006928c8b1446d227958b228ddeab2c507d Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 21:30:21 +0100 Subject: [PATCH 51/63] mypy --- detectree2/models/train.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 9e393f70..55d57f29 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -536,7 +536,7 @@ def build_test_loader(cls, cfg, dataset_name): """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) -def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List[Dict]: +def get_tree_dicts(directory: str, class_mapping = None) -> List[Dict]: """Get the tree dictionaries. Args: @@ -608,7 +608,7 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List def combine_dicts(root_dir: str, val_dir: int, mode: str = "train", - class_mapping: Dict[str, int] = None) -> List[Dict]: + class_mapping = None) -> List[Dict]: """ Combine dictionaries from different directories based on the specified mode. @@ -976,7 +976,7 @@ def modify_conv1_weights(model, num_input_channels): model.backbone.bottom_up.stem.conv1.weight.copy_(new_weights) -def get_latest_model_path(output_dir: str) -> str: +def get_latest_model_path(output_dir) -> str: """ Find the model file with the highest index in the specified output directory. From e753c1aeea9017ce8f1e4954b35a098dd88c18a3 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 21:33:52 +0100 Subject: [PATCH 52/63] mypy --- detectree2/models/train.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 55d57f29..9e393f70 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -536,7 +536,7 @@ def build_test_loader(cls, cfg, dataset_name): """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) -def get_tree_dicts(directory: str, class_mapping = None) -> List[Dict]: +def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List[Dict]: """Get the tree dictionaries. Args: @@ -608,7 +608,7 @@ def get_tree_dicts(directory: str, class_mapping = None) -> List[Dict]: def combine_dicts(root_dir: str, val_dir: int, mode: str = "train", - class_mapping = None) -> List[Dict]: + class_mapping: Dict[str, int] = None) -> List[Dict]: """ Combine dictionaries from different directories based on the specified mode. @@ -976,7 +976,7 @@ def modify_conv1_weights(model, num_input_channels): model.backbone.bottom_up.stem.conv1.weight.copy_(new_weights) -def get_latest_model_path(output_dir) -> str: +def get_latest_model_path(output_dir: str) -> str: """ Find the model file with the highest index in the specified output directory. From bcc73442389867724e5a0a7bde8ac2d844ea4490 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 21:43:45 +0100 Subject: [PATCH 53/63] mypy --- detectree2/models/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 9e393f70..61e5bda8 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -572,7 +572,7 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List record["height"] = height record["width"] = width record["image_id"] = filename[0:400] - record["annotations"] = {} + record["annotations"] = [] # print(filename[0:400]) objs = [] From 534e530634226929e87e78c128ab8d8f22ae2f87 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 21:48:46 +0100 Subject: [PATCH 54/63] mypy --- detectree2/models/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 61e5bda8..6d775d96 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -572,7 +572,7 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List record["height"] = height record["width"] = width record["image_id"] = filename[0:400] - record["annotations"] = [] + # record["annotations"] = {} # print(filename[0:400]) objs = [] From c569c7c237d60f0671c90e01dd8394eb3441f223 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 22:14:53 +0100 Subject: [PATCH 55/63] mypy --- detectree2/models/train.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 6d775d96..fc0cfd57 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -536,7 +536,7 @@ def build_test_loader(cls, cfg, dataset_name): """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) -def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List[Dict]: +def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """Get the tree dictionaries. Args: @@ -572,7 +572,7 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List record["height"] = height record["width"] = width record["image_id"] = filename[0:400] - # record["annotations"] = {} + record["annotations"] = {} # print(filename[0:400]) objs = [] @@ -608,7 +608,7 @@ def get_tree_dicts(directory: str, class_mapping: Dict[str, int] = None) -> List def combine_dicts(root_dir: str, val_dir: int, mode: str = "train", - class_mapping: Dict[str, int] = None) -> List[Dict]: + class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]:`` """ Combine dictionaries from different directories based on the specified mode. From 39d5b1ce1682d9cfd5ae16d139c824ac0ac05c0c Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 22:55:26 +0100 Subject: [PATCH 56/63] mypy --- detectree2/models/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index fc0cfd57..2c692f16 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -608,7 +608,7 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non def combine_dicts(root_dir: str, val_dir: int, mode: str = "train", - class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]:`` + class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """ Combine dictionaries from different directories based on the specified mode. From 7fdf0726a5b4403bcd3da96c579b4a7c0d1b014d Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 22:59:20 +0100 Subject: [PATCH 57/63] mypy --- detectree2/models/train.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 2c692f16..eb5dbcad 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -12,7 +12,7 @@ import re import time from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import cv2 import detectron2.data.transforms as T # noqa:N812 From 9061812912188e1e35ebe587d15c9285f00d6894 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 23:08:49 +0100 Subject: [PATCH 58/63] mypy --- detectree2/models/train.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index eb5dbcad..a8f9a3a1 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -557,7 +557,7 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non with open(json_file) as f: img_anns = json.load(f) - record = {} + record: Dict[str, Any] = {} filename = img_anns["imagePath"] # Make sure we have the correct height and width @@ -599,7 +599,7 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non objs.append(obj) - record["annotations"] = objs + record["annotations"] = objs if objs else [] dataset_dicts.append(record) return dataset_dicts From c9c67655448d9c5b44782e430a49396a5e3e5281 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 23:21:13 +0100 Subject: [PATCH 59/63] mypy --- detectree2/models/train.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index a8f9a3a1..e11fd47c 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -993,7 +993,11 @@ def get_latest_model_path(output_dir: str) -> str: files = os.listdir(output_dir) # Find all files that match the pattern and extract their indices - model_files = [(f, int(model_pattern.search(f).group(1))) for f in files if model_pattern.search(f)] + model_files = [] + for f in files: + match = model_pattern.search(f) + if match: + model_files.append((f, int(match.group(1)))) if not model_files: raise FileNotFoundError(f"No model files found in the directory {output_dir}") From fc1cfe0e85377b8aa3b592acb468517490c096d0 Mon Sep 17 00:00:00 2001 From: James Ball Date: Mon, 16 Sep 2024 23:25:47 +0100 Subject: [PATCH 60/63] isort --- detectree2/data_loading/custom.py | 9 +++++---- detectree2/models/train.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/detectree2/data_loading/custom.py b/detectree2/data_loading/custom.py index c1e37c82..81a1becb 100644 --- a/detectree2/data_loading/custom.py +++ b/detectree2/data_loading/custom.py @@ -1,10 +1,11 @@ -import rasterio +import cv2 +import detectron2.data.transforms as T import numpy as np +import rasterio import torch +from detectron2.structures import BitMasks, BoxMode, Instances from torch.utils.data import Dataset -import detectron2.data.transforms as T -from detectron2.structures import BoxMode, Instances, BitMasks -import cv2 + class CustomTIFFDataset(Dataset): def __init__(self, annotations, transforms=None): diff --git a/detectree2/models/train.py b/detectree2/models/train.py index e11fd47c..faeb472e 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -21,7 +21,6 @@ import rasterio import torch import torch.nn as nn -from detectree2.preprocessing.tiling import load_class_mapping from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 from detectron2.config import get_cfg @@ -43,6 +42,8 @@ from detectron2.utils.logger import log_every_n_seconds from detectron2.utils.visualizer import ColorMode, Visualizer +from detectree2.preprocessing.tiling import load_class_mapping + class FlexibleDatasetMapper(DatasetMapper): """ From dfe68e29b287fe97e458c42814dd60fd07893be3 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 17 Sep 2024 11:43:40 +0100 Subject: [PATCH 61/63] flake8 --- detectree2/models/train.py | 81 ++++++++++++++---------------- detectree2/preprocessing/tiling.py | 54 +++++++++++--------- 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index faeb472e..db8bf1e1 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -47,12 +47,13 @@ class FlexibleDatasetMapper(DatasetMapper): """ - A flexible dataset mapper that extends the standard DatasetMapper to handle multi-band images - and custom augmentations. + A flexible dataset mapper that extends the standard DatasetMapper to handle + multi-band images and custom augmentations. - This class is designed to work with datasets that may contain images with more than three channels - (e.g., multispectral images) and allows for custom augmentations to be applied. It also handles - semantic segmentation data if provided in the dataset. + This class is designed to work with datasets that may contain images with + more than three channels (e.g., multispectral images) and allows for custom + augmentations to be applied. It also handles semantic segmentation data if + provided in the dataset. Args: cfg (CfgNode): Configuration object containing dataset and model configurations. @@ -91,7 +92,7 @@ def __call__(self, dataset_dict): Process a single dataset dictionary, applying the necessary transformations and augmentations. Args: - dataset_dict (dict): A dictionary containing data for a single dataset item, including + dataset_dict (dict): A dictionary containing data for a single dataset item, including file names and metadata. Returns: @@ -116,13 +117,10 @@ def __call__(self, dataset_dict): # Size check similar to utils.check_image_size if img.shape[:2] != (dataset_dict.get("height"), dataset_dict.get("width")): self.logger.warning( - f"Image size {img.shape[:2]} does not match expected size {(dataset_dict.get('height'), dataset_dict.get('width'))}." + f"""Image size {img.shape[:2]} does not match expected size {(dataset_dict.get('height'), + dataset_dict.get('width'))}.""" ) - # If it's a 3-band image, delegate processing to the parent class - #if img.shape[-1] == 3: - # return super().__call__(dataset_dict) - # Otherwise, handle custom multi-band logic aug_input = T.AugInput(img) transforms = self.augmentations(aug_input) # Apply the augmentations @@ -152,6 +150,7 @@ def __call__(self, dataset_dict): self.logger.error(f"Error processing {file_name}: {e}") return None + class LossEvalHook(HookBase): """ A custom hook for evaluating loss during training and managing model checkpoints based on evaluation metrics. @@ -229,7 +228,7 @@ def _do_loss_eval(self): # Calculate loss for the current batch loss_batch = self._get_loss(inputs) losses.append(loss_batch) - + mean_loss = np.mean(losses) # Calculate the average AP50 across datasets if multiple datasets are used for testing @@ -318,7 +317,7 @@ class MyTrainer(DefaultTrainer): """ Custom Trainer class that extends the DefaultTrainer. - This trainer adds flexibility for handling different image types (e.g., RGB and multi-band images) + This trainer adds flexibility for handling different image types (e.g., RGB and multi-band images) and custom training behavior, such as early stopping and specialized data augmentation strategies. Args: @@ -334,7 +333,7 @@ def train(self): """ Run the training loop. - This method overrides the DefaultTrainer's train method to include early stopping and + This method overrides the DefaultTrainer's train method to include early stopping and custom logging of Average Precision (AP) metrics. Returns: @@ -397,7 +396,7 @@ def build_hooks(self): """ Build the training hooks, including the custom LossEvalHook. - This method adds a custom hook for evaluating the model's loss during training, with support for + This method adds a custom hook for evaluating the model's loss during training, with support for early stopping based on the AP50 metric. Returns: @@ -452,7 +451,7 @@ def build_train_loader(cls, cfg): """ Build the training data loader with support for custom augmentations and image types. - This method configures the data loader to apply specific augmentations depending on the image mode + This method configures the data loader to apply specific augmentations depending on the image mode (RGB or multi-band) and resize strategy defined in the configuration. Args: @@ -499,12 +498,9 @@ def build_train_loader(cls, cfg): print(f"Error loading image {location}: {e}") continue break - + if size: print("ADD RANDOM RESIZE WITH SIZE = ", size) - #min_size = int(size * 0.6) - #max_size = int(size * 1.4) - #augmentations.append(T.RandomResize(min_size=(min_size, min_size), max_size=max_size)) augmentations.append(T.ResizeScale(0.6, 1.4, size, size)) else: raise ValueError("Failed to determine image size for random resize") @@ -519,13 +515,13 @@ def build_train_loader(cls, cfg): augmentations=augmentations, ), ) - + @classmethod def build_test_loader(cls, cfg, dataset_name): """ Build the test data loader. - This method configures the data loader for evaluation, using the FlexibleDatasetMapper + This method configures the data loader for evaluation, using the FlexibleDatasetMapper to handle custom augmentations and image types. Args: @@ -537,6 +533,7 @@ def build_test_loader(cls, cfg, dataset_name): """ return build_detection_test_loader(cfg, dataset_name, mapper=FlexibleDatasetMapper(cfg, is_train=False)) + def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """Get the tree dictionaries. @@ -549,8 +546,7 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non List of dictionaries corresponding to segmentations of trees. Each dictionary includes bounding box around tree and points tracing a polygon around a tree. """ - #print("get ", class_mapping) - + dataset_dicts = [] for filename in [file for file in os.listdir(directory) if file.endswith(".geojson")]: @@ -614,26 +610,22 @@ def combine_dicts(root_dir: str, Combine dictionaries from different directories based on the specified mode. This function aggregates tree dictionaries from multiple directories within a root directory. - Depending on the mode, it either combines dictionaries from all directories, + Depending on the mode, it either combines dictionaries from all directories, all except a specified validation directory, or only from the validation directory. Args: root_dir (str): The root directory containing subdirectories with tree dictionaries. val_dir (int): The index (1-based) of the validation directory to exclude or use depending on the mode. - mode (str, optional): The mode of operation. Can be "train", "val", or "full". - "train" excludes the validation directory, - "val" includes only the validation directory, + mode (str, optional): The mode of operation. Can be "train", "val", or "full". + "train" excludes the validation directory, + "val" includes only the validation directory, and "full" includes all directories. Defaults to "train". class_mapping: A dictionary mapping class labels to category indices (optional). Returns: List of combined dictionaries from the specified directories. """ - # Get a list of all directories within the root directory - # train_dirs = [os.path.join(root_dir, dir) for dir in os.listdir(root_dir)] - - #print(class_mapping) - + # Get the list of directories within the root directory train_dirs = [ os.path.join(root_dir, dir) for dir in os.listdir(root_dir) @@ -715,7 +707,7 @@ def register_train_data(train_location, def get_classes(out_dir): """Function that will read the classes that are recorded during tiling. - + Args: out_dir: directory where classes.txt is located @@ -748,7 +740,7 @@ def remove_registered_data(name="tree"): def register_test_data(test_location, name="tree"): """Register data for testing. - + Args: test_location: directory containing test data name: string to name data @@ -769,7 +761,7 @@ def register_test_data(test_location, name="tree"): def load_json_arr(json_path): """Load json array. - + Args: json_path: path to json file """ @@ -797,7 +789,7 @@ def setup_cfg( max_iter=1000, eval_period=100, out_dir="./train_outputs", - resize="fixed", # fixed or random or rand_fixed + resize="fixed", # "fixed" or "random" or "rand_fixed" imgmode="rgb", num_bands=3, class_mapping_file=None, @@ -866,15 +858,15 @@ def setup_cfg( cfg.TEST.EVAL_PERIOD = eval_period cfg.RESIZE = resize cfg.INPUT.MIN_SIZE_TRAIN = 1000 - cfg.IMGMODE = imgmode # rgb or multispectral + cfg.IMGMODE = imgmode # "rgb" or "ms" (multispectral) if num_bands > 3: # Adjust PIXEL_MEAN and PIXEL_STD for the number of bands default_pixel_mean = cfg.MODEL.PIXEL_MEAN default_pixel_std = cfg.MODEL.PIXEL_STD # Extend or truncate the PIXEL_MEAN and PIXEL_STD based on num_bands - cfg.MODEL.PIXEL_MEAN = (default_pixel_mean * (num_bands // len(default_pixel_mean)) + + cfg.MODEL.PIXEL_MEAN = (default_pixel_mean * (num_bands // len(default_pixel_mean)) + default_pixel_mean[:num_bands % len(default_pixel_mean)]) - cfg.MODEL.PIXEL_STD = (default_pixel_std * (num_bands // len(default_pixel_std)) + + cfg.MODEL.PIXEL_STD = (default_pixel_std * (num_bands // len(default_pixel_std)) + default_pixel_std[:num_bands % len(default_pixel_std)]) return cfg @@ -887,7 +879,7 @@ def predictions_on_data(directory=None, geos_exist=True, num_predictions=0): """Prediction produced from a test folder and outputted to predictions folder. - + Args: directory: directory containing test data predictor: predictor object @@ -942,12 +934,13 @@ def predictions_on_data(directory=None, with open(output_file, "w") as dest: json.dump(evaluations, dest) + def modify_conv1_weights(model, num_input_channels): """ Modify the weights of the first convolutional layer (conv1) to accommodate a different number of input channels. - This function adjusts the weights of the `conv1` layer in the model's backbone to support a custom number - of input channels. It creates a new weight tensor with the desired number of input channels, + This function adjusts the weights of the `conv1` layer in the model's backbone to support a custom number + of input channels. It creates a new weight tensor with the desired number of input channels, and initializes it by repeating the weights of the original channels. Args: @@ -973,7 +966,7 @@ def modify_conv1_weights(model, num_input_channels): num_input_channels, old_weights.size(0), kernel_size=7, stride=2, padding=3, bias=False ) - # Copy the modified weights into the new conv1 layer + # Copy the modified weights into the new conv1 layer model.backbone.bottom_up.stem.conv1.weight.copy_(new_weights) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index b7d0bd9b..2a9c1d7d 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -11,7 +11,7 @@ import pickle import random import shutil -import warnings +import warnings # noqa: F401 from math import ceil from pathlib import Path @@ -20,11 +20,11 @@ import numpy as np import rasterio from fiona.crs import from_epsg # noqa: F401 -from rasterio.crs import CRS +# from rasterio.crs import CRS from rasterio.errors import RasterioIOError -from rasterio.io import DatasetReader +# from rasterio.io import DatasetReader from rasterio.mask import mask -from rasterio.windows import from_bounds +# from rasterio.windows import from_bounds from shapely.geometry import box # Configure logging @@ -107,7 +107,7 @@ def process_tile( miny: Minimum y coordinate of tile crs: Coordinate reference system tilename: Name of the tile - + Returns: None """ @@ -132,7 +132,7 @@ def process_tile( return None out_img, out_transform = mask(data, shapes=coords, crop=True) - + out_sumbands = np.sum(out_img, axis=0) zero_mask = np.where(out_sumbands == 0, 1, 0) nan_mask = np.where(out_sumbands == 765, 1, 0) @@ -162,7 +162,7 @@ def process_tile( with rasterio.open(out_tif) as clipped: arr = clipped.read() r, g, b = arr[0], arr[1], arr[2] - rgb = np.dstack((b, g, r)) # Reorder for cv2 (BGRA) + rgb = np.dstack((b, g, r)) # Reorder for cv2 (BGRA) # Rescale to 0-255 if necessary if np.max(g) > 255: @@ -174,7 +174,7 @@ def process_tile( if overlapping_crowns is not None: return data, out_path_root, overlapping_crowns, minx, miny, buffer - + return data, out_path_root, None, minx, miny, buffer except RasterioIOError as e: @@ -184,6 +184,7 @@ def process_tile( logger.error(f"Error processing tile {tilename} at ({minx}, {miny}): {e}") return None + def process_tile_ms( img_path: str, out_dir: str, @@ -212,7 +213,7 @@ def process_tile_ms( miny: Minimum y coordinate of tile crs: Coordinate reference system tilename: Name of the tile - + Returns: None """ @@ -266,7 +267,7 @@ def process_tile_ms( if overlapping_crowns is not None: return data, out_path_root, overlapping_crowns, minx, miny, buffer - + return data, out_path_root, None, minx, miny, buffer except RasterioIOError as e: @@ -276,6 +277,7 @@ def process_tile_ms( logger.error(f"Error processing tile {tilename} at ({minx}, {miny}): {e}") return None + def process_tile_train( img_path: str, out_dir: str, @@ -309,19 +311,19 @@ def process_tile_train( crowns: Crown polygons as a geopandas dataframe threshold: Min proportion of the tile covered by crowns to be accepted {0,1} nan_theshold: Max proportion of tile covered by nans - + Returns: None """ if mode == "rgb": - result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, - crowns, threshold, nan_threshold) + result = process_tile(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, + tilename, crowns, threshold, nan_threshold) elif mode == "ms": - result = process_tile_ms(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, - crowns, threshold, nan_threshold) - + result = process_tile_ms(img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, + tilename, crowns, threshold, nan_threshold) + if result is None: - #logger.warning(f"Skipping tile at ({minx}, {miny}) due to insufficient data.") + # logger.warning(f"Skipping tile at ({minx}, {miny}) due to insufficient data.") return data, out_path_root, overlapping_crowns, minx, miny, buffer = result @@ -350,7 +352,7 @@ def process_tile_train( # Keep only geometry to reduce file size moved_scaled = moved_scaled[['geometry']] - # Save the result as GeoJSON + # Save the result as GeoJSON moved_scaled.to_file(driver="GeoJSON", filename=filename) # Add image path info to the GeoJSON file @@ -363,10 +365,12 @@ def process_tile_train( logger.warning("Cannot write empty DataFrame to file.") return + # Define a top-level helper function def process_tile_train_helper(args): return process_tile_train(*args) + def tile_data( img_path: str, out_dir: str, @@ -382,8 +386,8 @@ def tile_data( ) -> None: """Tiles up orthomosaic and corresponding crowns (if supplied) into training/prediction tiles. - Tiles up large rasters into managable tiles for training and prediction. If crowns are not supplied the function - will tile up the entire landscape for prediction. If crowns are supplied the function will tile these with the image + Tiles up large rasters into managable tiles for training and prediction. If crowns are not supplied the function + will tile up the entire landscape for prediction. If crowns are supplied the function will tile these with the image and skip tiles without a minimum coverage of crowns. The 'threshold' can be varied to ensure a good coverage of crowns across a traing tile. Tiles that do not have sufficient coverage are skipped. @@ -408,10 +412,10 @@ def tile_data( crs = data.crs.to_epsg() # Update CRS handling to avoid deprecated syntax tile_args = [ - (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, + (img_path, out_dir, buffer, tile_width, tile_height, dtype_bool, minx, miny, crs, tilename, crowns, threshold, nan_threshold, mode, class_column) for minx in np.arange(ceil(data.bounds[0]) + buffer, data.bounds[2] - tile_width - buffer, tile_width, int) - for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, + for miny in np.arange(ceil(data.bounds[1]) + buffer, data.bounds[3] - tile_height - buffer, tile_height, int) ] @@ -479,13 +483,13 @@ def record_classes(crowns: gpd.GeoDataFrame, out_dir: str, column: str = 'status """ # Extract unique class names from the specified column list_of_classes = crowns[column].unique().tolist() - + # Sort the list of classes in alphabetical order list_of_classes.sort() # Create a dictionary for class-to-index mapping class_to_idx = {class_name: idx for idx, class_name in enumerate(list_of_classes)} - + # Save the class-to-index mapping to disk out_path = Path(out_dir) os.makedirs(out_path, exist_ok=True) @@ -536,7 +540,7 @@ def to_traintest_folders( # noqa: C901 Path(out_dir / "train").mkdir(parents=True, exist_ok=True) Path(out_dir / "test").mkdir(parents=True, exist_ok=True) - #file_names = tiles_dir.glob("*.png") + # file_names = tiles_dir.glob("*.png") file_names = tiles_dir.glob("*.geojson") file_roots = [item.stem for item in file_names] From 7a174ca049e9b41c69923f5279b1f319a4a67112 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 17 Sep 2024 11:52:03 +0100 Subject: [PATCH 62/63] tutorial --- docs/source/tutorial.rst | 1 + docs/source/tutorial_multi.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index a0be35d0..369d71ec 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -1,6 +1,7 @@ Tutorial ======== + This tutorial goes through the steps of single class (tree) detection and delineation from RGB and multispectral data. A guide to multiclass prediction (e.g. species mapping, disease mapping) is coming soon. Example data that can diff --git a/docs/source/tutorial_multi.rst b/docs/source/tutorial_multi.rst index 6c157090..125e06ef 100644 --- a/docs/source/tutorial_multi.rst +++ b/docs/source/tutorial_multi.rst @@ -17,7 +17,6 @@ The key steps are: 4. Making landscape level predictions - Preparing data (RGB and multispectral) -------------------------------------- From 1c436c3305484f15e523d8a171011b8699e69efc Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 17 Sep 2024 12:01:40 +0100 Subject: [PATCH 63/63] pep8 --- detectree2/models/train.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/detectree2/models/train.py b/detectree2/models/train.py index db8bf1e1..5e404e4d 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -425,7 +425,7 @@ def build_hooks(self): continue break # Define augmentation based on the determined size - augmentations = [T.ResizeShortestEdge([size, size], size+300)] + augmentations = [T.ResizeShortestEdge([size, size], size + 300)] else: # Use fixed size resizing as a default augmentations = [T.ResizeShortestEdge([1000, 1000], 1333)] @@ -627,10 +627,10 @@ def combine_dicts(root_dir: str, """ # Get the list of directories within the root directory train_dirs = [ - os.path.join(root_dir, dir) - for dir in os.listdir(root_dir) - if os.path.isdir(os.path.join(root_dir, dir)) - ] + os.path.join(root_dir, dir) + for dir in os.listdir(root_dir) + if os.path.isdir(os.path.join(root_dir, dir)) + ] # Handle the different modes for combining dictionaries if mode == "train": # Exclude the validation directory from the list of directories @@ -864,10 +864,14 @@ def setup_cfg( default_pixel_mean = cfg.MODEL.PIXEL_MEAN default_pixel_std = cfg.MODEL.PIXEL_STD # Extend or truncate the PIXEL_MEAN and PIXEL_STD based on num_bands - cfg.MODEL.PIXEL_MEAN = (default_pixel_mean * (num_bands // len(default_pixel_mean)) + - default_pixel_mean[:num_bands % len(default_pixel_mean)]) - cfg.MODEL.PIXEL_STD = (default_pixel_std * (num_bands // len(default_pixel_std)) + - default_pixel_std[:num_bands % len(default_pixel_std)]) + cfg.MODEL.PIXEL_MEAN = ( + default_pixel_mean * (num_bands // len(default_pixel_mean)) + + default_pixel_mean[:num_bands % len(default_pixel_mean)] + ) + cfg.MODEL.PIXEL_STD = ( + default_pixel_std * (num_bands // len(default_pixel_std)) + + default_pixel_std[:num_bands % len(default_pixel_std)] + ) return cfg