From 19df4342dfb7cd09352c3ba546a03943aeb20a97 Mon Sep 17 00:00:00 2001 From: Charles Song Date: Wed, 26 Mar 2025 16:26:17 -0400 Subject: [PATCH 1/6] Skip IoU == 0 to prevent error --- detectree2/models/evaluation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/detectree2/models/evaluation.py b/detectree2/models/evaluation.py index e48cee65..fa7a7208 100644 --- a/detectree2/models/evaluation.py +++ b/detectree2/models/evaluation.py @@ -424,6 +424,8 @@ def find_intersections(all_test_feats, all_pred_feats): continue # calculate the IoU + if union_area == 0: + continue IoU = intersection / union_area # update the objects so they only store greatest intersection value From df029c4d651c12c38824271216dd415585be5233 Mon Sep 17 00:00:00 2001 From: Charles Song Date: Thu, 4 Sep 2025 16:08:54 -0400 Subject: [PATCH 2/6] Refactor functions to use pathlib and tqdm - Including predict_on_data(), project_to_geojson(), stitch_crowns(), etc. - Simplify file path: `Path(file_roots[num[i]]` is a string of tile stem, its `.suffix` will return an empty string. Simply tile name stem + .geojson is good. - Update get_tree_dicts and get_filenmes function, accept Path as argument - Explicitly annotate feature as Feature class --- detectree2/models/evaluation.py | 134 +++++++-------- detectree2/models/outputs.py | 264 +++++++++++++---------------- detectree2/models/predict.py | 56 +++--- detectree2/models/train.py | 78 ++++----- detectree2/preprocessing/tiling.py | 16 +- 5 files changed, 252 insertions(+), 296 deletions(-) diff --git a/detectree2/models/evaluation.py b/detectree2/models/evaluation.py index fa7a7208..28f0c772 100644 --- a/detectree2/models/evaluation.py +++ b/detectree2/models/evaluation.py @@ -14,6 +14,7 @@ from shapely import make_valid from shapely.errors import GEOSException from shapely.geometry import Polygon, shape +from tqdm import tqdm # Initialising the parent class so any attributes or functions that are common # to both features should be placed in here @@ -191,27 +192,24 @@ def tree_height(self): # Regular functions now def get_tile_width(file): """Split up the file name to get width and buffer then adding to get overall width.""" - filename = file.replace(".geojson", "") - filename_split = filename.split("_") - - tile_width = (2 * int(filename_split[-2]) + int(filename_split[-3])) - + file = Path(file) + filename_split = file.stem.split("_") + tile_width = 2 * int(filename_split[-2]) + int(filename_split[-3]) return tile_width def get_epsg(file): """Splitting up the file name to get EPSG""" - filename = file.replace(".geojson", "") - filename_split = filename.split("_") - + file = Path(file) + filename_split = file.stem.split("_") epsg = filename_split[-1] return epsg def get_tile_origin(file): """Splitting up the file name to get tile origin""" - filename = file.replace(".geojson", "") - filename_split = filename.split("_") + file = Path(file) + filename_split = file.stem.split("_") buffer = int(filename_split[-2]) # center = int(filename_split[-3]) @@ -343,7 +341,7 @@ def initialise_feats( def initialise_feats2( directory, - file, + filename, lidar_img, area_threshold, conf_threshold, @@ -353,21 +351,21 @@ def initialise_feats2( epsg ): """Creates a list of all the features as objects of the class.""" - with open(directory + "/" + file) as feat_file: + feat_path = Path(directory) / filename + with feat_path.open() as feat_file: feat_json = json.load(feat_file) - feats = feat_json["features"] + feats = feat_json["features"] all_feats = [] count = 0 + for feat in feats: - feat_obj = GeoFeature(file, directory, count, feat, lidar_img, epsg) + feat_obj = GeoFeature(filename, directory, count, feat, lidar_img, epsg) if feat_threshold_tests2(feat_obj, conf_threshold, area_threshold, border_filter, tile_width, tile_origin): all_feats.append(feat_obj) count += 1 - else: - continue return all_feats @@ -601,10 +599,10 @@ def site_f1_score( find_intersections(all_test_feats, all_pred_feats) tps, fps, fns = positives_test(all_test_feats, all_pred_feats, IoU_threshold, height_threshold) - print("tps:", tps) - print("fps:", fps) - print("fns:", fns) - print("") + # print("tps:", tps) + # print("fps:", fps) + # print("fns:", fns) + # print("") total_tps = total_tps + tps total_fps = total_fps + fps @@ -614,7 +612,7 @@ def site_f1_score( prec, rec = prec_recall(total_tps, total_fps, total_fns) f1_score = f1_cal(prec, rec) # noqa: F841 print("Precision ", "Recall ", "F1") - print(prec, rec, f1_score) + print("{:.2f}".format(prec), "{:.2f}".format(rec), "{:.2f}".format(f1_score)) except ZeroDivisionError: print("ZeroDivisionError: Height threshold is too large.") @@ -636,10 +634,10 @@ def site_f1_score2( area of the corresponding polygons. Args: - tile_directory: path to the folder containing all of the tiles - test_directory: path to the folder containing just the test files - pred_directory: path to the folder containing the predictions and the reprojections - lidar_img: path to the lidar image of an entire region + tile_directory (str | Path): path to the folder containing all of the tiles + test_directory (str | Path): path to the folder containing just the test files + pred_directory (str | Path): path to the folder containing the predictions and the reprojections + lidar_img (str | Path): path to the lidar image of an entire region IoU_threshold: minimum value of IoU such that the intersection can be considered a true positive min_height: minimum height of the features to be considered max_height: minimum height of the features to be considered @@ -651,56 +649,57 @@ def site_f1_score2( save: bool to tell program whether the filtered crowns should be saved """ - test_entries = os.listdir(test_directory) - total_tps = 0 - total_fps = 0 - total_fns = 0 + tile_directory = Path(tile_directory) + test_directory = Path(test_directory) + pred_directory = Path(pred_directory) + + total_tps = total_fps = total_fns = 0 heights = [] # total_tests = 0 - for file in test_entries: - if ".geojson" in file: - print(file) + for file in tqdm(test_directory.iterdir(), desc='Calculating eval scores'): + if file.suffix != ".geojson": + continue - # work out the area threshold to ignore these crowns in the tiles - # tile_width = get_tile_width(file) * scaling[0] - # area_threshold = ((tile_width)**2) * area_fraction_limit + # print(file.name) - tile_width = get_tile_width(file) - tile_origin = get_tile_origin(file) - epsg = get_epsg(file) + # work out the area threshold to ignore these crowns in the tiles + # tile_width = get_tile_width(file) * scaling[0] + # area_threshold = ((tile_width)**2) * area_fraction_limit - test_file = file #.replace(".geojson", "_geo.geojson") - all_test_feats = initialise_feats2(tile_directory, test_file, - lidar_img, area_threshold, - conf_threshold, border_filter, - tile_width, tile_origin, epsg) + tile_width = get_tile_width(file) + tile_origin = get_tile_origin(file) + epsg = get_epsg(file) - new_heights = get_heights(all_test_feats, min_height, max_height) - heights.extend(new_heights) + all_test_feats = initialise_feats2(tile_directory, file.name, + lidar_img, area_threshold, + conf_threshold, border_filter, + tile_width, tile_origin, epsg) - pred_file = "Prediction_" + file.replace(".geojson", "_eval.geojson") - all_pred_feats = initialise_feats2(pred_directory, pred_file, - lidar_img, area_threshold, - conf_threshold, border_filter, - tile_width, tile_origin, epsg) + new_heights = get_heights(all_test_feats, min_height, max_height) + heights.extend(new_heights) - if save: - save_feats(tile_directory, all_test_feats) - save_feats(tile_directory, all_pred_feats) + pred_file = "Prediction_" + file.stem + "_eval.geojson" + all_pred_feats = initialise_feats2(pred_directory, pred_file, + lidar_img, area_threshold, + conf_threshold, border_filter, + tile_width, tile_origin, epsg) - find_intersections(all_test_feats, all_pred_feats) - tps, fps, fns = positives_test(all_test_feats, all_pred_feats, - IoU_threshold, min_height, max_height) + if save: + save_feats(tile_directory, all_test_feats) + save_feats(tile_directory, all_pred_feats) - print("tps:", tps) - print("fps:", fps) - print("fns:", fns) - print("") + find_intersections(all_test_feats, all_pred_feats) + tps, fps, fns = positives_test(all_test_feats, all_pred_feats, + IoU_threshold, min_height, max_height) - total_tps += tps - total_fps += fps - total_fns += fns + # print(f"tps: {tps}") + # print(f"fps: {fps}") + # print(f"fns: {fns}\n") + + total_tps += tps + total_fps += fps + total_fns += fns try: prec, rec = prec_recall(total_tps, total_fps, total_fns) @@ -708,12 +707,13 @@ def site_f1_score2( f1_score = f1_cal(prec, rec) # noqa: F841 med_height = median(heights) print("Precision ", "Recall ", "F1") - print(prec, rec, f1_score) - print(" ") - print("Total_trees=", len(heights)) - print("med_height=", med_height) + print("{:.2f}".format(prec), "{:.2f}".format(rec), "{:.2f}".format(f1_score), "\n") + print(f"Total_trees = {len(heights)}") + print(f"med_height = {med_height}") + except ZeroDivisionError: print("ZeroDivisionError: Height threshold is too large.") + return prec, rec, f1_score diff --git a/detectree2/models/outputs.py b/detectree2/models/outputs.py index d81d6e35..432d3fdb 100644 --- a/detectree2/models/outputs.py +++ b/detectree2/models/outputs.py @@ -36,8 +36,7 @@ def polygon_from_mask(masked_arr): https://github.com/hazirbas/coco-json-converter/blob/master/generate_coco_json.py <-- adapted from here """ - contours, _ = cv2.findContours( - masked_arr, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours(masked_arr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) segmentation = [] for contour in contours: @@ -50,7 +49,7 @@ def polygon_from_mask(masked_arr): contour.extend(contour[:2]) # small artifacts due to this? segmentation.append(contour) - [x, y, w, h] = cv2.boundingRect(masked_arr) + # [x, y, w, h] = cv2.boundingRect(masked_arr) if len(segmentation) > 0: return segmentation[0] # , [x, y, w, h], area @@ -64,89 +63,87 @@ def to_eval_geojson(directory=None): # noqa:N803 Reproject the crowns to overlay with the cropped crowns and cropped pngs. Another copy is produced to overlay with pngs. """ + directory = Path(directory) - entries = os.listdir(directory) + for file in directory.iterdir(): + if file.suffix != ".json": + continue - for file in entries: - if ".json" in file: + # create a dictionary for each file to store data used multiple times + img_dict = {"filename": file.name} - # create a dictionary for each file to store data used multiple times - img_dict = {} - img_dict["filename"] = file - - file_mins = file.replace(".json", "") - file_mins_split = file_mins.split("_") - img_dict["minx"] = file_mins_split[-5] - img_dict["miny"] = file_mins_split[-4] - epsg = file_mins_split[-1] - # create a geofile for each tile --> the EPSG value should be done - # automatically - geofile = { - "type": "FeatureCollection", - "crs": { - "type": "name", - "properties": { - "name": "urn:ogc:def:crs:EPSG::" + epsg - }, + file_mins = file.stem + file_mins_split = file_mins.split("_") + img_dict["minx"] = file_mins_split[-5] + img_dict["miny"] = file_mins_split[-4] + epsg = file_mins_split[-1] + # create a geofile for each tile --> the EPSG value should be done + # automatically + geofile = { + "type": "FeatureCollection", + "crs": { + "type": "name", + "properties": { + "name": f"urn:ogc:def:crs:EPSG::{epsg}" }, - "features": [], - } + }, + "features": [], + } - # load the json file we need to convert into a geojson - with open(directory + "/" + img_dict["filename"]) as prediction_file: - datajson = json.load(prediction_file) + # load the json file we need to convert into a geojson + with file.open() as prediction_file: + datajson = json.load(prediction_file) - img_dict["width"] = datajson[0]["segmentation"]["size"][0] - img_dict["height"] = datajson[0]["segmentation"]["size"][1] - # print(img_dict) - - # json file is formated as a list of segmentation polygons so cycle through each one - for crown_data in datajson: - # just a check that the crown image is correct - if img_dict["minx"] + "_" + img_dict["miny"] in crown_data["image_id"]: - crown = crown_data["segmentation"] - confidence_score = crown_data["score"] - - # changing the coords from RLE format so can be read as numbers, here the numbers are - # integers so a bit of info on position is lost - mask_of_coords = mask_util.decode(crown) - crown_coords = polygon_from_mask(mask_of_coords) - if crown_coords == 0: - continue - rescaled_coords = [] - - # coords from json are in a list of [x1, y1, x2, y2,... ] so convert them to [[x1, y1], ...] - # format and at the same time rescale them so they are in the correct position for QGIS - for c in range(0, len(crown_coords), 2): - x_coord = crown_coords[c] - y_coord = crown_coords[c + 1] - # TODO: make flexible to deal with hemispheres - if epsg == "26917": - rescaled_coords.append([x_coord, -y_coord]) - else: - rescaled_coords.append( - [x_coord, -y_coord + int(img_dict["height"])]) - - geofile["features"].append({ - "type": "Feature", - "properties": { - "Confidence_score": confidence_score - }, - "geometry": { - "type": "Polygon", - "coordinates": [rescaled_coords], - }, - }) - - # Check final form is correct - compare to a known geojson file if - # error appears. - # print(geofile) - - output_geo_file = os.path.join( - directory, img_dict["filename"].replace(".json", "_eval.geojson")) - # print(output_geo_file) - with open(output_geo_file, "w") as dest: - json.dump(geofile, dest) + img_dict["width"] = datajson[0]["segmentation"]["size"][0] + img_dict["height"] = datajson[0]["segmentation"]["size"][1] + # print(img_dict) + + # json file is formated as a list of segmentation polygons so cycle through each one + for crown_data in datajson: + # just a check that the crown image is correct + if f"{img_dict['minx']}_{img_dict['miny']}" not in crown_data["image_id"]: + continue + + crown = crown_data["segmentation"] + confidence_score = crown_data["score"] + + # changing the coords from RLE format so can be read as numbers, here the numbers are + # integers so a bit of info on position is lost + mask_of_coords = mask_util.decode(crown) + crown_coords = polygon_from_mask(mask_of_coords) + if crown_coords == 0: + continue + + rescaled_coords = [] + # coords from json are in a list of [x1, y1, x2, y2,... ] so convert them to [[x1, y1], ...] + # format and at the same time rescale them so they are in the correct position for QGIS + for c in range(0, len(crown_coords), 2): + x_coord = crown_coords[c] + y_coord = crown_coords[c + 1] + # TODO: make flexible to deal with hemispheres + if epsg == "26917": + rescaled_coords.append([x_coord, -y_coord]) + else: + rescaled_coords.append([x_coord, -y_coord + int(img_dict["height"])]) + + geofile["features"].append({ + "type": "Feature", + "properties": { + "Confidence_score": confidence_score + }, + "geometry": { + "type": "Polygon", + "coordinates": [rescaled_coords], + }, + }) + + # Check final form is correct - compare to a known geojson file if error appears. + # print(geofile) + + output_geo_file = file.with_name(f"{file.stem}_eval.geojson") + # print(output_geo_file) + with output_geo_file.open("w") as dest: + json.dump(geofile, dest) def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class: bool = False): # noqa:N803 @@ -156,28 +153,25 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class with PNGs. Args: - tiles_path (str): Path to the tiles folder. - pred_fold (str): Path to the predictions folder. - output_fold (str): Path to the output folder. + tiles_path (str | Path): Path to the tiles folder. + pred_fold (str | Path): Path to the predictions folder. + output_fold (str | Path): Path to the output folder. Returns: None """ - Path(output_fold).mkdir(parents=True, exist_ok=True) - entries = list(Path(pred_fold) / file for file in os.listdir(pred_fold) if Path(file).suffix == ".json") - total_files = len(entries) - print(f"Projecting {total_files} files") + output_fold = Path(output_fold) + + output_fold.mkdir(parents=True, exist_ok=True) + entries = [file for file in Path(pred_fold).iterdir() if file.suffix == ".json"] - for idx, filename in enumerate(entries, start=1): - if idx % 50 == 0: - print(f"Projecting file {idx} of {total_files}: {filename}") + for file in tqdm(entries, desc="Projecting files",): - tifpath = Path(tiles_path) / Path(filename.name.replace("Prediction_", "")).with_suffix(".tif") + tifpath = Path(tiles_path) / f"{file.stem.removeprefix('Prediction_')}.tif" - data = rasterio.open(tifpath) - epsg = CRS.from_string(data.crs.wkt) - epsg = epsg.to_epsg() - raster_transform = data.transform + with rasterio.open(tifpath) as data: + epsg = data.crs.to_epsg() + raster_transform = data.transform geofile = { "type": "FeatureCollection", @@ -191,14 +185,14 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class } # type: GeoFile # load the json file we need to convert into a geojson - with open(filename, "r") as prediction_file: + with file.open("r") as prediction_file: datajson = json.load(prediction_file) # json file is formated as a list of segmentation polygons so cycle through each one for crown_data in datajson: if multi_class: category = crown_data["category_id"] - # print(category) + crown = crown_data["segmentation"] confidence_score = crown_data["score"] @@ -206,7 +200,7 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class # integers so a bit of info on position is lost mask_of_coords = mask_util.decode(crown) crown_coords = polygon_from_mask(mask_of_coords) - if crown_coords == 0: + if not crown_coords: continue crown_coords_array = np.array(crown_coords).reshape(-1, 2) @@ -214,47 +208,34 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class rows=crown_coords_array[:, 1], cols=crown_coords_array[:, 0]) moved_coords = list(zip(x_coords, y_coords)) + + feature: Feature = { + "type": "Feature", + "properties": { + "Confidence_score": confidence_score, + }, + "geometry": { + "type": "Polygon", + "coordinates": [moved_coords], + }, + } + if multi_class: - geofile["features"].append({ - "type": "Feature", - "properties": { - "Confidence_score": confidence_score, - "category": category, - }, - "geometry": { - "type": "Polygon", - "coordinates": [moved_coords], - }, - }) - else: - geofile["features"].append({ - "type": "Feature", - "properties": { - "Confidence_score": confidence_score - }, - "geometry": { - "type": "Polygon", - "coordinates": [moved_coords], - }, - }) - # print(geofile["features"]) - - output_geo_file = os.path.join(output_fold, filename.with_suffix(".geojson").name) - - with open(output_geo_file, "w") as dest: + feature["properties"]["category"] = category + + geofile["features"].append(feature) + + output_geo_file = output_fold / file.with_suffix(".geojson").name + + with output_geo_file.open("w") as dest: json.dump(geofile, dest) def filename_geoinfo(filename): """Return geographic info of a tile from its filename.""" - parts = os.path.basename(filename).replace(".geojson", "").split("_") - - parts = [int(part) for part in parts[-5:]] # type: ignore - minx = parts[0] - miny = parts[1] - width = parts[2] - buffer = parts[3] - crs = parts[4] + name = Path(filename).stem + parts = name.split("_")[-5:] + minx, miny, width, buffer, crs = map(int, parts) return (minx, miny, width, buffer, crs) @@ -308,25 +289,18 @@ def stitch_crowns(folder: str, shift: int = 1): gpd.GeoDataFrame: A GeoDataFrame containing all the crowns. """ crowns_path = Path(folder) - files = list(crowns_path.glob("*geojson")) - if len(files) == 0: - raise FileNotFoundError("No geojson files found in folder.") + files = list(crowns_path.glob("*.geojson")) + if not files: + raise FileNotFoundError(f"No geojson files found in {crowns_path}.") _, _, _, _, crs = filename_geoinfo(files[0]) - total_files = len(files) crowns_list = [] - for idx, file in enumerate(files, start=1): - if idx % 50 == 0: - print(f"Stitching file {idx} of {total_files}: {file}") - + for file in tqdm(files, desc="Stitching crowns", unit="file"): crowns_tile = gpd.read_file(file) # This throws a huge amount of warnings fiona closed ring detected - geo = box_filter(file, shift) - crowns_tile = gpd.sjoin(crowns_tile, geo, "inner", "within") - crowns_list.append(crowns_tile) crowns = pd.concat(crowns_list, ignore_index=True) @@ -380,7 +354,7 @@ def clean_crowns( # 2. Use a spatial join to quickly find all candidate overlapping pairs. # The join will pair each crown with any crown whose bounding box intersects. - print("clearn_crowns: Performing spatial join...") + print("clean_crowns: Performing spatial join...") join = gpd.sjoin(crowns, crowns, how="inner", predicate="intersects") # Remove self-joins (where a crown is paired with itself). join = join[join.index != join.index_right] diff --git a/detectree2/models/predict.py b/detectree2/models/predict.py index b87e4422..da0d7148 100644 --- a/detectree2/models/predict.py +++ b/detectree2/models/predict.py @@ -9,9 +9,9 @@ import cv2 import numpy as np import rasterio +from tqdm import tqdm from detectron2.engine import DefaultPredictor from detectron2.evaluation.coco_evaluation import instances_to_coco_json - from detectree2.models.train import get_filenames, get_tree_dicts # Code to convert RLE data from the output instances into Polygons, @@ -20,13 +20,13 @@ def predict_on_data( - directory: str = "./", - out_folder: str = "predictions", + directory: str | Path = "./", + out_folder: str | Path = "predictions", predictor=DefaultPredictor, - eval=False, + eval: bool=False, save: bool = True, num_predictions=0, -): +) -> None: """Make predictions on tiled data. Predicts crowns for all images (.png or .tif) present in a directory and outputs masks as JSON files. @@ -42,35 +42,36 @@ def predict_on_data( Returns: None """ - pred_dir = os.path.join(directory, out_folder) - Path(pred_dir).mkdir(parents=True, exist_ok=True) + directory = Path(directory) + pred_dir = Path(out_folder) + pred_dir.mkdir(parents=True, exist_ok=True) if eval: dataset_dicts = get_tree_dicts(directory) - if len(dataset_dicts) > 0: - sample_file = dataset_dicts[0]["file_name"] - _, mode = get_filenames(os.path.dirname(sample_file)) + if dataset_dicts: + sample_file = Path(dataset_dicts[0]["file_name"]) + _, mode = get_filenames(sample_file.parent) else: mode = None else: dataset_dicts, mode = get_filenames(directory) - total_files = len(dataset_dicts) - num_to_pred = len( - dataset_dicts) if num_predictions == 0 else num_predictions + num_to_pred = len(dataset_dicts) if num_predictions == 0 else num_predictions - print(f"Predicting {num_to_pred} files in mode {mode}") + for d in tqdm( + dataset_dicts[:num_to_pred], + desc=f"Predicting files in mode {mode}", + ): + file_name = Path(d["file_name"]) + file_ext = file_name.suffix.lower() - for i, d in enumerate(dataset_dicts[:num_to_pred], start=1): - file_name = d["file_name"] - file_ext = os.path.splitext(file_name)[1].lower() if file_ext == ".png": # RGB image, read with cv2 - cv_img = cv2.imread(file_name) - if cv_img is None: + img = cv2.imread(str(file_name)) + if img is None: print(f"Failed to read image {file_name} with cv2.") continue - img = np.array(cv_img) # Explicitly convert to numpy array + img = np.array(img) # Explicitly convert to numpy array elif file_ext == ".tif": # Multispectral image, read with rasterio with rasterio.open(file_name) as src: @@ -83,20 +84,13 @@ def predict_on_data( outputs = predictor(img) - # Create the output file name - file_name_only = os.path.basename(file_name) - file_name_json = os.path.splitext(file_name_only)[0] + ".json" - output_file = os.path.join(pred_dir, f"Prediction_{file_name_json}") - if save: # Save predictions to JSON file + output_file = pred_dir / f"Prediction_{file_name.stem}.json" evaluations = instances_to_coco_json(outputs["instances"].to("cpu"), - file_name) - with open(output_file, "w") as dest: - json.dump(evaluations, dest) - - if i % 50 == 0: - print(f"Predicted {i} files of {total_files}") + str(file_name)) + with output_file.open("w") as f: + json.dump(evaluations, f) if __name__ == "__main__": diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 57fe9813..90fa111e 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -26,6 +26,7 @@ import shapely.geometry as geom import torch import torch.nn as nn +from tqdm import tqdm from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 from detectron2.config import get_cfg @@ -453,7 +454,7 @@ def train(self): self.iter = self.start_iter = start_iter self.max_iter = max_iter - self.early_stop = False + self.early_stop = True self.APs = [] with EventStorage(start_iter) as self.storage: @@ -722,7 +723,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]]: +def get_tree_dicts(directory: str | Path, class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: """Get the tree dictionaries. Args: @@ -737,29 +738,28 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non dataset_dicts = [] - for filename in [file for file in os.listdir(directory) if file.endswith(".geojson")]: - json_file = os.path.join(directory, filename) + for json_file in Path(directory).glob("*.geojson"): with open(json_file) as f: img_anns = json.load(f) record: Dict[str, Any] = {} - filename = img_anns["imagePath"] + file_path = Path(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 else if image path ends in .tif use rasterio - if filename.endswith(".png"): - img = cv2.imread(filename) + if file_path.suffix == ".png": + img = cv2.imread(str(file_path)) if img is None: continue height, width = img.shape[:2] - elif filename.endswith(".tif"): - with rasterio.open(filename) as src: + elif file_path.suffix == ".tif": + with rasterio.open(str(file_path)) as src: height, width = src.shape - record["file_name"] = filename + record["file_name"] = str(file_path) record["height"] = height record["width"] = width - record["image_id"] = filename[0:400] + record["image_id"] = str(file_path)[0:400] record["annotations"] = {} # print(filename[0:400]) @@ -767,7 +767,7 @@ def get_tree_dicts(directory: str, class_mapping: Optional[Dict[str, int]] = Non for features in img_anns["features"]: anno = features["geometry"] if anno["type"] != "Polygon" and anno["type"] != "MultiPolygon": - print("Skipping annotation of type", anno["type"], "in file", filename) + print("Skipping annotation of type", anno["type"], "in file", file_path) continue px = [a[0] for a in anno["coordinates"][0]] py = [height - a[1] for a in anno["coordinates"][0]] @@ -824,10 +824,9 @@ def combine_dicts(root_dir: str, Returns: List of combined dictionaries from the specified directories. """ - # Get the list of directories within the root directory, sorted alphabetically - train_dirs = sorted([ - os.path.join(root_dir, dir) for dir in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, dir)) - ]) + # Get the list of directories within the root directory + root_dir = Path(root_dir) + train_dirs = [d for d in root_dir.iterdir() if d.is_dir()] # Handle the different modes for combining dictionaries if mode == "train": # Exclude the validation directory from the list of directories @@ -847,7 +846,7 @@ def combine_dicts(root_dir: str, return tree_dicts -def get_filenames(directory: str): +def get_filenames(directory: str | Path): """Get the file names from the directory, handling both RGB (.png) and multispectral (.tif) images. Args: @@ -858,11 +857,11 @@ def get_filenames(directory: str): - dataset_dicts (list): List of dictionaries with 'file_name' keys. - mode (str): 'rgb' if .png files are used, 'ms' if .tif files are used. """ - dataset_dicts = [] + directory = Path(directory) # Get list of .png and .tif files - png_files = glob.glob(os.path.join(directory, "*.png")) - tif_files = glob.glob(os.path.join(directory, "*.tif")) + png_files = list(directory.glob("*.png")) + tif_files = list(directory.glob("*.tif")) if png_files and tif_files: # Both .png and .tif files are present, select only .png files @@ -881,10 +880,7 @@ def get_filenames(directory: str): files = [] mode = None - for filename in files: - file = {} - file["file_name"] = filename - dataset_dicts.append(file) + dataset_dicts = [{"file_name": str(f)} for f in files] return dataset_dicts, mode @@ -1117,16 +1113,17 @@ def predictions_on_data( Returns: None """ - pred_dir = os.path.join(directory, "predictions") - Path(pred_dir).mkdir(parents=True, exist_ok=True) + directory = Path(directory) + pred_dir = directory / "predictions" + pred_dir.mkdir(parents=True, exist_ok=True) - test_location = os.path.join(directory, "test") + test_location = directory / "test" if geos_exist: dataset_dicts = get_tree_dicts(test_location) if len(dataset_dicts) > 0: - sample_file = dataset_dicts[0]["file_name"] - _, mode = get_filenames(os.path.dirname(sample_file)) + sample_file = Path(dataset_dicts[0]["file_name"]) + _, mode = get_filenames(sample_file.parent) else: mode = None else: @@ -1135,9 +1132,9 @@ def predictions_on_data( # Decide how many items to predict on num_to_pred = len(dataset_dicts) if num_predictions == 0 else num_predictions - for d in random.sample(dataset_dicts, num_to_pred): - file_name = d["file_name"] - file_ext = os.path.splitext(file_name)[1].lower() + for d in tqdm(random.sample(dataset_dicts, num_to_pred), desc='Predicting'): + file_name = Path(d["file_name"]) + file_ext = file_name.suffix.lower() if file_ext == ".png": # RGB image, read with cv2 img = cv2.imread(file_name) @@ -1168,14 +1165,12 @@ def predictions_on_data( v = v.draw_instance_predictions(outputs["instances"].to("cpu")) # Create the output file name - file_name_only = os.path.basename(file_name) - file_name_json = os.path.splitext(file_name_only)[0] + ".json" - output_file = os.path.join(pred_dir, f"Prediction_{file_name_json}") + output_file = pred_dir / f"Prediction_{file_name.stem}.json" if save: # Save predictions to JSON file - evaluations = instances_to_coco_json(outputs["instances"].to("cpu"), file_name) - with open(output_file, "w") as dest: + evaluations = instances_to_coco_json(outputs["instances"].to("cpu"), str(file_name)) + with output_file.open("w") as dest: json.dump(evaluations, dest) @@ -1240,13 +1235,10 @@ def get_latest_model_path(output_dir: str) -> str: # 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 = [] - for f in files: - match = model_pattern.search(f) + for f in Path(output_dir).iterdir(): + match = model_pattern.match(f.name) if match: model_files.append((f, int(match.group(1)))) @@ -1257,7 +1249,7 @@ def get_latest_model_path(output_dir: str) -> str: 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) + return str(latest_model_file) if __name__ == "__main__": diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index d076fd98..202ed8f6 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -885,7 +885,7 @@ def tile_data( if mask_path is not None: mask_gdf = gpd.read_file(mask_path) out_path = Path(out_dir) - os.makedirs(out_path, exist_ok=True) + out_path.mkdir(parents=True, exist_ok=True) tilename = Path(img_path).stem with rasterio.open(img_path) as data: crs = data.crs.to_epsg() # Update CRS handling to avoid deprecated syntax @@ -1302,7 +1302,7 @@ def record_classes(crowns: gpd.GeoDataFrame, out_dir: str, column: str = 'status # Save the class-to-index mapping to disk out_path = Path(out_dir) - os.makedirs(out_path, exist_ok=True) + out_path.mkdir(parents=True, exist_ok=True) if save_format == 'json': with open(out_path / 'class_to_idx.json', 'w') as f: @@ -1340,7 +1340,7 @@ def to_traintest_folders( # noqa: C901 tiles_dir = Path(tiles_folder) out_dir = Path(out_folder) - if not os.path.exists(tiles_dir): + if not tiles_dir.exists(): raise IOError if Path(out_dir / "train").exists() and Path(out_dir / "train").is_dir(): @@ -1367,19 +1367,15 @@ def to_traintest_folders( # noqa: C901 # copy to test if i < len(file_roots) * test_frac: test_boxes.append(image_details(file_roots[num[i]])) - shutil.copy((tiles_dir / file_roots[num[i]]).with_suffix(Path(file_roots[num[i]]).suffix + ".geojson"), - out_dir / "test") + shutil.copy(tiles_dir / f"{file_roots[num[i]]}.geojson", out_dir / "test") else: # copy to train train_box = image_details(file_roots[num[i]]) if strict: # check if there is overlap with test boxes if not is_overlapping_box(test_boxes, train_box): - shutil.copy( - (tiles_dir / file_roots[num[i]]).with_suffix(Path(file_roots[num[i]]).suffix + ".geojson"), - out_dir / "train") + shutil.copy(tiles_dir / f"{file_roots[num[i]]}.geojson", out_dir / "train") else: - shutil.copy((tiles_dir / file_roots[num[i]]).with_suffix(Path(file_roots[num[i]]).suffix + ".geojson"), - out_dir / "train") + shutil.copy(tiles_dir / f"{file_roots[num[i]]}.geojson", out_dir / "train") # COMMENT NECESSARY HERE file_names = (out_dir / "train").glob("*.geojson") From e2d32faa43cbd895485e8084d765ce31f53f7524 Mon Sep 17 00:00:00 2001 From: Charles Song Date: Thu, 20 Nov 2025 11:05:56 -0500 Subject: [PATCH 3/6] Sort imports --- detectree2/models/predict.py | 4 ++-- detectree2/models/train.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/detectree2/models/predict.py b/detectree2/models/predict.py index da0d7148..0fe81109 100644 --- a/detectree2/models/predict.py +++ b/detectree2/models/predict.py @@ -3,15 +3,15 @@ This module contains the code to generate predictions on tiled data. """ import json -import os from pathlib import Path import cv2 import numpy as np import rasterio -from tqdm import tqdm from detectron2.engine import DefaultPredictor from detectron2.evaluation.coco_evaluation import instances_to_coco_json +from tqdm import tqdm + from detectree2.models.train import get_filenames, get_tree_dicts # Code to convert RLE data from the output instances into Polygons, diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 90fa111e..1559726e 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -4,7 +4,6 @@ manual crown data. """ import datetime -import glob import json import logging import os @@ -26,7 +25,6 @@ import shapely.geometry as geom import torch import torch.nn as nn -from tqdm import tqdm from detectron2 import model_zoo from detectron2.checkpoint import DetectionCheckpointer # noqa:F401 from detectron2.config import get_cfg @@ -44,10 +42,13 @@ from detectron2.evaluation.coco_evaluation import instances_to_coco_json from detectron2.layers.wrappers import Conv2d from detectron2.structures import BoxMode -from detectron2.utils.events import get_event_storage # noqa:F401 -from detectron2.utils.events import EventStorage +from detectron2.utils.events import ( # noqa:F401 + EventStorage, + get_event_storage, +) from detectron2.utils.logger import log_every_n_seconds from detectron2.utils.visualizer import ColorMode, Visualizer +from tqdm import tqdm from detectree2.models.outputs import clean_crowns from detectree2.preprocessing.tiling import load_class_mapping From 19301356d98789e241048ca691139a6fcd60040d Mon Sep 17 00:00:00 2001 From: Charles Song Date: Thu, 1 Jan 2026 17:03:11 -0500 Subject: [PATCH 4/6] edits to comply with checks --- 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 1559726e..0a5dc39c 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -802,7 +802,7 @@ def get_tree_dicts(directory: str | Path, class_mapping: Optional[Dict[str, int] return dataset_dicts -def combine_dicts(root_dir: str, +def combine_dicts(root_dir: str | Path, val_dir: int, mode: str = "train", class_mapping: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]: From 805a0fbb539fa72505ec65cfc1c58bbb159a375c Mon Sep 17 00:00:00 2001 From: Charles Song Date: Wed, 7 Jan 2026 16:16:41 -0500 Subject: [PATCH 5/6] Make improvements as requested Jan 07, 2026 --- detectree2/models/outputs.py | 71 +++++++++++++++++++----------------- detectree2/models/predict.py | 4 +- detectree2/models/train.py | 8 ++-- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/detectree2/models/outputs.py b/detectree2/models/outputs.py index 432d3fdb..51b3a394 100644 --- a/detectree2/models/outputs.py +++ b/detectree2/models/outputs.py @@ -217,7 +217,7 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class "geometry": { "type": "Polygon", "coordinates": [moved_coords], - }, + }, } if multi_class: @@ -598,39 +598,42 @@ def combine_and_average_polygons(gdfs, iou=0.9): def clean_predictions(directory, iou_threshold=0.7): """Clean predictions prior to accuracy assessment.""" - pred_fold = directory - entries = os.listdir(pred_fold) - - for file in entries: - if ".json" in file: - print(file) - with open(pred_fold + "/" + file) as prediction_file: - datajson = json.load(prediction_file) - - crowns = gpd.GeoDataFrame() - - for shp in datajson: - crown_coords = polygon_from_mask( - mask_util.decode(shp["segmentation"])) - if crown_coords == 0: - continue - rescaled_coords = [] - # coords from json are in a list of [x1, y1, x2, y2,... ] so convert them to [[x1, y1], ...] - # format and at the same time rescale them so they are in the correct position for QGIS - for c in range(0, len(crown_coords), 2): - x_coord = crown_coords[c] - y_coord = crown_coords[c + 1] - rescaled_coords.append([x_coord, y_coord]) - crowns = pd.concat([crowns, gpd.GeoDataFrame({'Confidence_score': shp['score'], - 'geometry': [Polygon(rescaled_coords)]}, - geometry=[Polygon(rescaled_coords)])]) - - crowns = crowns.reset_index().drop('index', axis=1) - crowns, indices = clean_outputs(crowns, iou_threshold) - datajson_reduced = [datajson[i] for i in indices] - print("data_json:", len(datajson), " ", len(datajson_reduced)) - with open(pred_fold + "/" + file, "w") as dest: - json.dump(datajson_reduced, dest) + pred_fold = Path(directory) + + for file in pred_fold.iterdir(): + if file.suffix != "json": + continue + + print(file.name) + with file.open("r") as prediction_file: + datajson = json.load(prediction_file) + + crowns = gpd.GeoDataFrame() + + for shp in datajson: + crown_coords = polygon_from_mask( + mask_util.decode(shp["segmentation"])) + if crown_coords == 0: + continue + + # convert coords from json [x1, y1, x2, y2,... ] -> [[x1, y1], ...] + # format and at the same time rescale them so they are in the correct position for QGIS + rescaled_coords = [ + [crown_coords[i], crown_coords[i + 1]] + for i in range(0, len(crown_coords), 2) + ] + + crowns = pd.concat([crowns, gpd.GeoDataFrame({'Confidence_score': shp['score'], + 'geometry': [Polygon(rescaled_coords)]}, + geometry='geometry')]) + + crowns = crowns.reset_index(drop=True) + crowns, indices = clean_outputs(crowns, iou_threshold) + datajson_reduced = [datajson[i] for i in indices] + print(f"data_json: {len(datajson)} {len(datajson_reduced)}") + + with file.open("w") as dest: + json.dump(datajson_reduced, dest) def clean_outputs(crowns: gpd.GeoDataFrame, iou_threshold=0.7): diff --git a/detectree2/models/predict.py b/detectree2/models/predict.py index 0fe81109..7ad0ba56 100644 --- a/detectree2/models/predict.py +++ b/detectree2/models/predict.py @@ -32,8 +32,8 @@ def predict_on_data( Predicts crowns for all images (.png or .tif) present in a directory and outputs masks as JSON files. Args: - directory (str): Directory containing the images. - out_folder (str): Output folder for predictions. + directory (str | Path): Directory containing the images. + out_folder (str | Path): Output folder for predictions. predictor (DefaultPredictor): The predictor object. eval (bool): Whether to use evaluation mode. save (bool): Whether to save the predictions. diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 0a5dc39c..21cd4001 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -455,7 +455,7 @@ def train(self): self.iter = self.start_iter = start_iter self.max_iter = max_iter - self.early_stop = True + self.early_stop = False self.APs = [] with EventStorage(start_iter) as self.storage: @@ -814,7 +814,7 @@ def combine_dicts(root_dir: str | Path, all except a specified validation directory, or only from the validation directory. Args: - root_dir (str): The root directory containing subdirectories with tree dictionaries. + root_dir (str | Path): 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, @@ -851,7 +851,7 @@ def get_filenames(directory: str | Path): """Get the file names from the directory, handling both RGB (.png) and multispectral (.tif) images. Args: - directory (str): Directory of images to be predicted on. + directory (str | Path): Directory of images to be predicted on. Returns: tuple: A tuple containing: @@ -1138,7 +1138,7 @@ def predictions_on_data( file_ext = file_name.suffix.lower() if file_ext == ".png": # RGB image, read with cv2 - img = cv2.imread(file_name) + img = cv2.imread(str(file_name)) if img is None: print(f"Failed to read image {file_name} with cv2.") continue From 5160ded4efc43a4523eb0e7b10099ed2a4638dd1 Mon Sep 17 00:00:00 2001 From: Charles Song Date: Thu, 15 Jan 2026 11:08:48 -0500 Subject: [PATCH 6/6] Edit according to review request 2026-01-05 --- detectree2/models/evaluation.py | 18 +++++++++--------- detectree2/models/outputs.py | 20 +++++++++----------- detectree2/models/predict.py | 2 +- detectree2/models/train.py | 23 +++++++++++++---------- detectree2/preprocessing/tiling.py | 28 ++++++++++++++-------------- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/detectree2/models/evaluation.py b/detectree2/models/evaluation.py index 28f0c772..39d8e0ab 100644 --- a/detectree2/models/evaluation.py +++ b/detectree2/models/evaluation.py @@ -423,7 +423,7 @@ def find_intersections(all_test_feats, all_pred_feats): # calculate the IoU if union_area == 0: - continue + continue IoU = intersection / union_area # update the objects so they only store greatest intersection value @@ -672,18 +672,18 @@ def site_f1_score2( epsg = get_epsg(file) all_test_feats = initialise_feats2(tile_directory, file.name, - lidar_img, area_threshold, - conf_threshold, border_filter, - tile_width, tile_origin, epsg) + lidar_img, area_threshold, + conf_threshold, border_filter, + tile_width, tile_origin, epsg) new_heights = get_heights(all_test_feats, min_height, max_height) heights.extend(new_heights) - pred_file = "Prediction_" + file.stem + "_eval.geojson" + pred_file = f"Prediction_{file.stem}_eval.geojson" all_pred_feats = initialise_feats2(pred_directory, pred_file, - lidar_img, area_threshold, - conf_threshold, border_filter, - tile_width, tile_origin, epsg) + lidar_img, area_threshold, + conf_threshold, border_filter, + tile_width, tile_origin, epsg) if save: save_feats(tile_directory, all_test_feats) @@ -691,7 +691,7 @@ def site_f1_score2( find_intersections(all_test_feats, all_pred_feats) tps, fps, fns = positives_test(all_test_feats, all_pred_feats, - IoU_threshold, min_height, max_height) + IoU_threshold, min_height, max_height) # print(f"tps: {tps}") # print(f"fps: {fps}") diff --git a/detectree2/models/outputs.py b/detectree2/models/outputs.py index 51b3a394..9fcc9d51 100644 --- a/detectree2/models/outputs.py +++ b/detectree2/models/outputs.py @@ -5,7 +5,6 @@ """ import glob import json -import os import re from http.client import REQUEST_URI_TOO_LONG # noqa: F401 from pathlib import Path @@ -91,7 +90,7 @@ def to_eval_geojson(directory=None): # noqa:N803 } # load the json file we need to convert into a geojson - with file.open() as prediction_file: + with file.open() as prediction_file: datajson = json.load(prediction_file) img_dict["width"] = datajson[0]["segmentation"]["size"][0] @@ -217,12 +216,12 @@ def project_to_geojson(tiles_path, pred_fold=None, output_fold=None, multi_class "geometry": { "type": "Polygon", "coordinates": [moved_coords], - }, + }, } if multi_class: feature["properties"]["category"] = category - + geofile["features"].append(feature) output_geo_file = output_fold / file.with_suffix(".geojson").name @@ -330,7 +329,7 @@ def clean_crowns( Clean overlapping crowns by first identifying all candidate overlapping pairs via a spatial join, then clustering crowns into connected components (where an edge is added if two crowns have IoU above a threshold), and finally keeping the best crown (by confidence or any given field) in each cluster. - + Args: crowns (gpd.GeoDataFrame): Crowns to be cleaned. iou_threshold (float, optional): IoU threshold that determines whether crowns are overlapping. @@ -409,7 +408,6 @@ def union(x, y): # 7. Assemble the cleaned crowns. cleaned_crowns = crowns.loc[selected_indices].copy() - return gpd.GeoDataFrame(cleaned_crowns, crs=crowns.crs).reset_index(drop=True) @@ -601,7 +599,7 @@ def clean_predictions(directory, iou_threshold=0.7): pred_fold = Path(directory) for file in pred_fold.iterdir(): - if file.suffix != "json": + if file.suffix != ".json": continue print(file.name) @@ -615,17 +613,17 @@ def clean_predictions(directory, iou_threshold=0.7): mask_util.decode(shp["segmentation"])) if crown_coords == 0: continue - + # convert coords from json [x1, y1, x2, y2,... ] -> [[x1, y1], ...] # format and at the same time rescale them so they are in the correct position for QGIS rescaled_coords = [ [crown_coords[i], crown_coords[i + 1]] for i in range(0, len(crown_coords), 2) - ] + ] crowns = pd.concat([crowns, gpd.GeoDataFrame({'Confidence_score': shp['score'], - 'geometry': [Polygon(rescaled_coords)]}, - geometry='geometry')]) + 'geometry': [Polygon(rescaled_coords)]}, + geometry='geometry')]) crowns = crowns.reset_index(drop=True) crowns, indices = clean_outputs(crowns, iou_threshold) diff --git a/detectree2/models/predict.py b/detectree2/models/predict.py index 7ad0ba56..ac27be1a 100644 --- a/detectree2/models/predict.py +++ b/detectree2/models/predict.py @@ -23,7 +23,7 @@ def predict_on_data( directory: str | Path = "./", out_folder: str | Path = "predictions", predictor=DefaultPredictor, - eval: bool=False, + eval: bool = False, save: bool = True, num_predictions=0, ) -> None: diff --git a/detectree2/models/train.py b/detectree2/models/train.py index 21cd4001..75f95fae 100644 --- a/detectree2/models/train.py +++ b/detectree2/models/train.py @@ -378,7 +378,7 @@ def after_step(self): size=output["instances"].image_size).squeeze(0) img = np.transpose(img[:3], (1, 2, 0)) v = Visualizer(img, metadata=MetadataCatalog.get(self.trainer.cfg.DATASETS.TEST[0]), scale=1) - #v = v.draw_instance_predictions(output['instances'][output['instances'].scores > 0.5].to("cpu")) + # v = v.draw_instance_predictions(output['instances'][output['instances'].scores > 0.5].to("cpu")) masks = output["instances"].pred_masks.to("cpu").numpy() scores = output["instances"].scores.to("cpu").numpy() @@ -396,8 +396,8 @@ def after_step(self): "Confidence_score": scores, "indices": list(range(len(scores))) }, - geometry=geoms, - crs="EPSG:3857") + geometry=geoms, + crs="EPSG:3857") gdf = clean_crowns(gdf, iou_threshold=0.3, confidence=0.3, area_threshold=0, verbose=False) @@ -407,7 +407,7 @@ def after_step(self): if self.trainer.cfg.IMGMODE == "rgb": image = np.transpose(image.astype("uint8"), (2, 0, 1)) - else: #ms + else: # ms image = np.transpose(image.astype("uint8"), (2, 0, 1))[[1, 0, 2]] storage.put_image(f"val/prediction/{img_data['file_name'].split('/')[-1]}", image) @@ -546,10 +546,12 @@ def resume_or_load(self, resume=True): logger = logging.getLogger("detectree2") if input_channels_in_checkpoint != 3: logger.warning( - "Input channel modification only works if checkpoint was trained on RGB images (3 channels). The first three channels will be copied and then repeated in the model." + "Input channel modification only works if checkpoint was trained on RGB images (3 channels). " \ + "The first three channels will be copied and then repeated in the model." ) logger.warning( - "Mismatch in input channels in checkpoint and model, meaning fvcommon would not have been able to automatically load them. Adjusting weights for 'backbone.bottom_up.stem.conv1.weight' manually." + "Mismatch in input channels in checkpoint and model, meaning fvcommon would not have been able to automatically load them. " \ + "Adjusting weights for 'backbone.bottom_up.stem.conv1.weight' manually." ) with torch.no_grad(): self.model.backbone.bottom_up.stem.conv1.weight[:, :3] = checkpoint[:, :3] @@ -1078,7 +1080,7 @@ def setup_cfg( 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)]) + 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)]) if visualize_training: @@ -1223,12 +1225,12 @@ def multiply_conv1_weights(model): model.backbone.bottom_up.stem.conv1 = new_conv -def get_latest_model_path(output_dir: str) -> str: +def get_latest_model_path(output_dir: str | Path) -> 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. + output_dir (str | Path): The directory where the model files are stored. Returns: str: The path to the model file with the highest index. @@ -1238,7 +1240,8 @@ def get_latest_model_path(output_dir: str) -> str: # Find all files that match the pattern and extract their indices model_files = [] - for f in Path(output_dir).iterdir(): + output_dir = Path(output_dir) # ADD THIS + for f in output_dir.iterdir(): match = model_pattern.match(f.name) if match: model_files.append((f, int(match.group(1)))) diff --git a/detectree2/preprocessing/tiling.py b/detectree2/preprocessing/tiling.py index 202ed8f6..15f4e6f6 100644 --- a/detectree2/preprocessing/tiling.py +++ b/detectree2/preprocessing/tiling.py @@ -673,36 +673,36 @@ def calculate_image_statistics(file_path, def calc_on_everything(): logger.info("Processing entire image...") band_stats = [] - + # Define chunk size for reading (e.g. 2048 rows) chunk_height = 2048 - + for band_idx in range(1, src.count + 1): if band_idx - 1 in ignore_bands_indices: continue - + # Accumulators for exact stats total_count = 0 total_sum = 0.0 total_sum_sq = 0.0 global_min = float('inf') global_max = float('-inf') - + # Buffer for percentiles percentile_buffer = [] buffer_size = 0 MAX_BUFFER = 5_000_000 # 5 million pixels ~ 40MB - + for row_off in tqdm(range(0, height, chunk_height), desc=f"Calculating stats for band {band_idx}", leave=False): h = min(chunk_height, height - row_off) window = rasterio.windows.Window(0, row_off, width, h) - + band_chunk = src.read(band_idx, window=window).astype(float) - + # Mask out bad values mask = (np.isnan(band_chunk) | np.isin(band_chunk, values_to_ignore)) valid_chunk = band_chunk[~mask] - + if valid_chunk.size > 0: # Update exact stats c_min = np.min(valid_chunk) @@ -710,21 +710,21 @@ def calc_on_everything(): c_sum = np.sum(valid_chunk) c_sum_sq = np.sum(valid_chunk ** 2) c_count = valid_chunk.size - + if c_min < global_min: global_min = c_min if c_max > global_max: global_max = c_max total_sum += c_sum total_sum_sq += c_sum_sq total_count += c_count - + # Update percentile buffer percentile_buffer.append(valid_chunk) buffer_size += c_count - + if buffer_size > MAX_BUFFER: merged = np.concatenate(percentile_buffer) # Downsample to keep memory usage low - merged = merged[::2] + merged = merged[::2] percentile_buffer = [merged] buffer_size = merged.size @@ -892,12 +892,12 @@ def tile_data( tile_coordinates = _calculate_tile_placements(img_path, buffer, tile_width, tile_height, crowns, tile_placement, overlapping_tiles) - + image_statistics = calculate_image_statistics(img_path, values_to_ignore=additional_nodata, mode=mode, ignore_bands_indices=ignore_bands_indices) if mode == "ms" else None # Only needed for multispectral 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, mask_gdf, additional_nodata, image_statistics, ignore_bands_indices,