diff --git a/deepforest/main.py b/deepforest/main.py index 68a714fd..178e540d 100644 --- a/deepforest/main.py +++ b/deepforest/main.py @@ -341,13 +341,13 @@ def predict_image(self, Args: image: a float32 numpy array of a RGB with channels last format path: optional path to read image from disk instead of passing image arg - return_plot: Return image with plotted detections color: color of the bounding box as a tuple of BGR color, e.g. orange annotations is (0, 165, 255) thickness: thickness of the rectangle border line in px Returns: result: A pandas dataframe of predictions (Default) img: The input with predictions overlaid (Optional) """ + # Ensure we are in eval mode self.model.eval() @@ -373,24 +373,20 @@ def predict_image(self, result = predict._predict_image_(model=self.model, image=image, path=path, - nms_thresh=self.config["nms_thresh"], - return_plot=return_plot, - thickness=thickness, - color=color) + nms_thresh=self.config["nms_thresh"]) - if return_plot: - return result + #If there were no predictions, return None + if result is None: + return None else: - #If there were no predictions, return None - if result is None: - return None - else: - result["label"] = result.label.apply( - lambda x: self.numeric_to_label_dict[x]) + result["label"] = result.label.apply( + lambda x: self.numeric_to_label_dict[x]) + + result = utilities.read_file(result) return result - def predict_file(self, csv_file, root_dir, savedir=None, color=None, thickness=1): + def predict_file(self, csv_file, root_dir): """Create a dataset and predict entire annotation file Csv file format is .csv file with the columns "image_path", "xmin","ymin","xmax","ymax" for the image name and bounding box position. Image_path is the @@ -400,12 +396,10 @@ def predict_file(self, csv_file, root_dir, savedir=None, color=None, thickness=1 Args: csv_file: path to csv file root_dir: directory of images. If none, uses "image_dir" in config - savedir: Optional. Directory to save image plots. - color: color of the bounding box as a tuple of BGR color, e.g. orange annotations is (0, 165, 255) - thickness: thickness of the rectangle border line in px Returns: df: pandas dataframe with bounding boxes, label and scores for each image in the csv file """ + df = utilities.read_file(csv_file) ds = dataset.TreeDataset(csv_file=csv_file, root_dir=root_dir, @@ -418,10 +412,7 @@ def predict_file(self, csv_file, root_dir, savedir=None, color=None, thickness=1 annotations=df, dataloader=dataloader, root_dir=root_dir, - nms_thresh=self.config["nms_thresh"], - savedir=savedir, - color=color, - thickness=thickness) + nms_thresh=self.config["nms_thresh"]) return results @@ -431,12 +422,9 @@ def predict_tile(self, patch_size=400, patch_overlap=0.05, iou_threshold=0.15, - return_plot=False, mosaic=True, sigma=0.5, thresh=0.001, - color=None, - thickness=1, crop_model=None, crop_transform=None, crop_augment=False): @@ -453,12 +441,9 @@ def predict_tile(self, iou_threshold: Minimum iou overlap among predictions between windows to be suppressed. Lower values suppress more boxes at edges. - return_plot: Should the image be returned with the predictions drawn? mosaic: Return a single prediction dataframe (True) or a tuple of image crops and predictions (False) sigma: variance of Gaussian function used in Gaussian Soft NMS thresh: the score thresh used to filter bboxes after soft-nms performed - color: color of the bounding box as a tuple of BGR color, e.g. orange annotations is (0, 165, 255) - thickness: thickness of the rectangle border line in px cropModel: a deepforest.model.CropModel object to predict on crops crop_transform: a torchvision.transforms object to apply to crops crop_augment: a boolean to apply augmentations to crops @@ -510,18 +495,6 @@ def predict_tile(self, lambda x: self.numeric_to_label_dict[x]) if raster_path: results["image_path"] = os.path.basename(raster_path) - if return_plot: - # Draw predictions on BGR - if raster_path: - tile = rio.open(raster_path).read() - else: - tile = self.image - drawn_plot = tile[:, :, ::-1] - drawn_plot = visualize.plot_predictions(tile, - results, - color=color, - thickness=thickness) - return drawn_plot else: for df in results: df["label"] = df.label.apply(lambda x: self.numeric_to_label_dict[x]) @@ -543,6 +516,8 @@ def predict_tile(self, transform=crop_transform, augment=crop_augment) + results = utilities.read_file(results) + return results def training_step(self, batch, batch_idx): @@ -732,7 +707,7 @@ def configure_optimizers(self): else: return optimizer - def evaluate(self, csv_file, root_dir, iou_threshold=None, savedir=None): + def evaluate(self, csv_file, root_dir, iou_threshold=None): """Compute intersection-over-union and precision/recall for a given iou_threshold. @@ -740,22 +715,19 @@ def evaluate(self, csv_file, root_dir, iou_threshold=None, savedir=None): csv_file: location of a csv file with columns "name","xmin","ymin","xmax","ymax","label", each box in a row root_dir: location of files in the dataframe 'name' column. iou_threshold: float [0,1] intersection-over-union union between annotation and prediction to be scored true positive - savedir: optional path dir to save evaluation images Returns: results: dict of ("results", "precision", "recall") for a given threshold """ ground_df = utilities.read_file(csv_file) ground_df["label"] = ground_df.label.apply(lambda x: self.label_dict[x]) predictions = self.predict_file(csv_file=csv_file, - root_dir=root_dir, - savedir=savedir) + root_dir=root_dir) results = evaluate_iou.__evaluate_wrapper__( predictions=predictions, ground_df=ground_df, root_dir=root_dir, iou_threshold=iou_threshold, - numeric_to_label_dict=self.numeric_to_label_dict, - savedir=savedir) + numeric_to_label_dict=self.numeric_to_label_dict) return results diff --git a/deepforest/predict.py b/deepforest/predict.py index ef82e2b8..3a68c174 100644 --- a/deepforest/predict.py +++ b/deepforest/predict.py @@ -186,13 +186,6 @@ def _dataloader_wrapper_(model, results = read_file(results, root_dir) - if savedir: - visualize.plot_prediction_dataframe(results, - root_dir=root_dir, - savedir=savedir, - color=color, - thickness=thickness) - return results diff --git a/deepforest/visualize.py b/deepforest/visualize.py index e9d4e987..68f39549 100644 --- a/deepforest/visualize.py +++ b/deepforest/visualize.py @@ -256,38 +256,200 @@ def label_to_color(label): return color_dict[label] - -def convert_to_sv_format(df): +def convert_to_sv_format(df, width=None, height=None): """Convert DeepForest prediction results to a supervision Detections object. Args: df (pd.DataFrame): The results from `predict_image` or `predict_tile`. Expected columns: ['xmin', 'ymin', 'xmax', 'ymax', 'label', 'score', 'image_path']. + width (int): The width of the image in pixels. Required if the geometry type is 'polygon'. + height (int): The height of the image in pixels. Required if the geometry type is 'polygon'. Returns: sv.Detections: A supervision Detections object containing bounding boxes, class IDs, confidence scores, and class names object mapping classes ids to corresponding class names inside of data dictionary. + Example: detections = convert_to_sv_format(result) """ - # Extract bounding boxes as a 2D numpy array with shape (_, 4) - boxes = df[['xmin', 'ymin', 'xmax', 'ymax']].values.astype(np.float32) + geom_type = determine_geometry_type(df) + + if geom_type == "box": + # Extract bounding boxes as a 2D numpy array with shape (_, 4) + boxes = df.geometry.apply(lambda x: (x.bounds[0], x.bounds[1], x.bounds[2], x.bounds[3])).values + boxes = np.stack(boxes) + + label_mapping = {label: idx for idx, label in enumerate(df['label'].unique())} + + # Extract labels as a numpy array + labels = df['label'].map(label_mapping).values.astype(int) + + # Extract scores as a numpy array + try: + scores = np.array(df['score'].tolist()) + except KeyError: + scores = np.ones(len(labels)) + + # Create a reverse mapping from integer to string labels + class_name = {v: k for k, v in label_mapping.items()} + + detections = sv.Detections( + xyxy=boxes, + class_id=labels, + confidence=scores, + data={"class_name": [class_name[class_id] for class_id in labels]}) + + elif geom_type == "polygon": + # Extract bounding boxes as a 2D numpy array with shape (_, 4) + boxes = df.geometry.apply(lambda x: (x.bounds[0], x.bounds[1], x.bounds[2], x.bounds[3])).values + boxes = np.stack(boxes) + + label_mapping = {label: idx for idx, label in enumerate(df['label'].unique())} + + # Extract labels as a numpy array + labels = df['label'].map(label_mapping).values.astype(int) + + # Extract scores as a numpy array + scores = np.array(df['score'].tolist()) + # Create a reverse mapping from integer to string labels + class_name = {v: k for k, v in label_mapping.items()} + + # Create masks + if height is None or width is None: + raise ValueError("height and width of the mask must be provided for polygon predictions") + + polygons = df.geometry.apply(lambda x: np.array(x.exterior.coords)).values + # as integers + polygons = [np.array(p).round().astype(np.int32) for p in polygons] + masks = [ sv.polygon_to_mask(p,(width,height)) for p in polygons ] + masks = np.stack(masks) + + detections = sv.Detections( + xyxy=boxes, + mask=masks, + class_id=labels, + confidence=scores, + data={"class_name": [class_name[class_id] for class_id in labels]}) + + elif geom_type == "point": + points = df.geometry.apply(lambda x: (x.x, x.y)).values + points = np.stack(points) + points = np.expand_dims(points, axis=1) + + label_mapping = {label: idx for idx, label in enumerate(df['label'].unique())} + + # Extract labels as a numpy array + labels = df['label'].map(label_mapping).values.astype(int) + + # Extract scores as a numpy array + scores = np.array(df['score'].tolist()) + scores = np.expand_dims(np.stack(scores), 1) + + # Create a reverse mapping from integer to string labels + class_name = {v: k for k, v in label_mapping.items()} + + detections = sv.KeyPoints( + xy=points, + class_id=labels, + confidence=scores, + data={"class_name": [class_name[class_id] for class_id in labels + ]}) + + return detections + +def plot_results(results, root_dir, ground_truth=None, savedir=None, height=None, width=None, results_color=None, ground_truth_color=None, thickness=2, radius=3): + """Plot the prediction results. + + Args: + df: a pandas dataframe with prediction results + root_dir: the root directory where the images are stored + ground_truth: an optional pandas dataframe with ground truth annotations + savedir: optional path to save the figure. If None (default), the figure will be interactively plotted. + height: height of the image in pixels. Required if the geometry type is 'polygon'. + width: width of the image in pixels. Required if the geometry type is 'polygon'. + results_color (list): color of the results annotations as a tuple of RGB color, e.g. orange annotations is [245, 135, 66] + Returns: + None + """ + # Convert colors, check for multi-class labels + if results_color is None: + sv_color = sv.Color(245, 135, 66) + elif type(results_color) is list: + sv_color = sv.Color(results_color[0], results_color[1], results_color[2]) + else: + sv_color = results_color + + num_labels = len(results.label.unique()) + if num_labels > 1 and results_color is not None: + warnings.warn("Multiple labels detected in the results. Each label will be plotted with a different color using a color ramp, results color argument is ignored.") + if num_labels > 1: + sv_color = sv.ColorPalette.from_matplotlib('viridis', 5) + + # Read images + image_path = os.path.join(root_dir, results.image_path.unique()[0]) + image = np.array(Image.open(image_path)) + + # Plot the results following https://supervision.roboflow.com/annotators/ + fig, ax = plt.subplots() + annotated_scene = _plot_image_with_results(df=results,image=image, sv_color=sv_color, height=height, width=width, thickness=thickness, radius=radius) + + if ground_truth is not None: + if ground_truth_color is None: + sv_color = sv.Color(0, 165, 255) + elif type(ground_truth_color) is list: + sv_color = sv.Color(ground_truth_color[0], ground_truth_color[1], ground_truth_color[2]) + else: + sv_color = ground_truth_color + # Plot the ground truth annotations + annotated_scene = _plot_image_with_results(df=ground_truth,image=annotated_scene, sv_color=sv_color, height=height, width=width, thickness=thickness, radius=radius) + + if savedir: + basename = os.path.splitext(os.path.basename(results.image_path.unique()[0]))[0] + image_name = "{}.png".format(basename) + plt.savefig(os.path.join(savedir, image_name), bbox_inches='tight', pad_inches=0) + else: + # Display the image using Matplotlib + plt.imshow(annotated_scene) + plt.axis('off') # Hide axes for a cleaner look + plt.show() + +def _plot_image_with_results(df, image, sv_color, thickness=1, radius=3, height=None, width=None): + """ + Annotates an image with the given results. - label_mapping = {label: idx for idx, label in enumerate(df['label'].unique())} + Args: + df (pandas.DataFrame): The DataFrame containing the results. + image (numpy.ndarray): The image to annotate. + sv_color (str): The color of the annotations. + thickness (int): The thickness of the annotations. - # Extract labels as a numpy array - labels = df['label'].map(label_mapping).values.astype(int) + Returns: + numpy.ndarray: The annotated image. + """ + # Determine the geometry type + geom_type = determine_geometry_type(df) + detections = convert_to_sv_format(df, height=height, width=width) - # Extract scores as a numpy array - scores = np.array(df['score'].tolist()) - # Create a reverse mapping from integer to string labels - class_name = {v: k for k, v in label_mapping.items()} + if geom_type == "box": + bounding_box_annotator = sv.BoxAnnotator(color=sv_color, thickness=thickness) + annotated_frame = bounding_box_annotator.annotate( + scene=image.copy(), + detections=detections, + ) + elif geom_type == "polygon": - return sv.Detections( - xyxy=boxes, - class_id=labels, - confidence=scores, - data={"class_name": [class_name[class_id] for class_id in labels]}) + polygon_annotator = sv.PolygonAnnotator(color=sv_color, thickness=thickness) + annotated_frame = polygon_annotator.annotate( + scene=image.copy(), + detections=detections, + ) + elif geom_type == "point": + point_annotator = sv.VertexAnnotator(color=sv_color, radius=radius) + annotated_frame = point_annotator.annotate( + scene=image.copy(), + key_points=detections + ) + return annotated_frame diff --git a/docs/advanced_features/visualizations.md b/docs/advanced_features/visualizations.md index 196a7955..10bcdb0e 100644 --- a/docs/advanced_features/visualizations.md +++ b/docs/advanced_features/visualizations.md @@ -1,104 +1,66 @@ - # Visualization -Simple visualization can be done during inference and saved. These are intended as quick ways of looking at data. +To view the results of DeepForest models, we use Roboflow's [supervision](https://supervision.roboflow.com/latest/) library. Thanks to this team for making a nice set of tools. After making predictions, use :func:`deepforest.visualize.plot_results`. -The color and line thickness of boxes can be customized using the `color` and `thickness` arguments. -`color` is the color of the bounding box as a tuple of BGR color, e.g. orange annotations is (0, 165, 255). -`thickness` is the thickness of the rectangle border line in pixels. +### Predict -```python -image_path = get_data("OSBS_029.png") -boxes = model.predict_image(path=image_path, return_plot = True, color=(0, 165, 255), thickness=3) ``` +from deepforest import main, get_data +from deepforest.visualize import plot_results +import os -## Visualizations using Roboflow's supervision package - -The `convert_to_sv_format` function converts DeepForest prediction results into a `supervision.Detections` object. This object contains bounding boxes, class IDs, confidence scores, and class names. It is designed to facilitate the visualization and further processing of object detection results using [supervision](https://supervision.roboflow.com/latest/) library. - -### Arguments - -- `df (pd.DataFrame)`: The DataFrame containing the results from `predict_image` or `predict_tile`. The DataFrame is expected to have the following columns: - - `xmin`: The minimum x-coordinate of the bounding box. - - `ymin`: The minimum y-coordinate of the bounding box. - - `xmax`: The maximum x-coordinate of the bounding box. - - `ymax`: The maximum y-coordinate of the bounding box. - - `label`: The class label of the detected object. - - `score`: The confidence score of the detection. - - `image_path`: The path to the image (optional, not used in this function). - -### Returns - -- `sv.Detections`: A `supervision.Detections` object containing: - - `xyxy (ndarray)`: A 2D numpy array of shape (_, 4) with bounding box coordinates. - - `class_id (ndarray)`: A numpy array of integer class IDs. - - `confidence (ndarray)`: A numpy array of confidence scores. - - `data (Dict[str, List[str]])`: A dictionary containing additional data, including class names. - -### Example 1: Converting Prediction Results and Annotating an Image - -```python -import supervision as sv -from deepforest import main -from deepforest import get_data -from deepforest.visualize import convert_to_sv_format -import matplotlib.pyplot as plt -import cv2 -import numpy as np -import pandas as pd +model = main.deepforest() +model.use_release() -# Initialize the DeepForest model -m = main.deepforest() -m.use_release() +sample_image_path = get_data("OSBS_029.png") +results = model.predict_image(path=sample_image_path) +plot_results(results, root_dir=os.path.dirname(sample_image_path)) +``` +The same works with deepforest.main.predict_tile -# Load image -img_path = get_data("OSBS_029.tif") -image = cv2.imread(img_path) +``` +from deepforest import main, get_data +from deepforest.visualize import plot_results +import os + +model = main.deepforest() +model.use_release() + +img_path = get_data(path="2019_YELL_2_528000_4978000_image_crop2.png") +results = model.predict_tile(img_path, patch_overlap=0, patch_size=400) +# The root dir is the location of the images +root_dir = os.path.dirname(img_path) +plot_results(results, root_dir=root_dir) +``` -# Predict using DeepForest -result = m.predict_image(img_path) +![sample_image](../www/Visualization1.png) +### Customizing outputs -# Convert custom prediction results to supervision format -sv_detections = convert_to_sv_format(result) +The colors and thickness of annotations can be updated. -# You can now use `sv_detections` for further processing or visualization ``` -To show bounding boxes: -```python -# You can visualize predicted bounding boxes - -bounding_box_annotator = sv.BoundingBoxAnnotator() -annotated_frame = bounding_box_annotator.annotate( - scene=image.copy(), - detections=sv_detections -) - +# Orange boxes and thicker lines +plot_results(results, root_dir=root_dir, results_color=[109,50,168], thickness=2) +``` +![sample_image](../www/Visualization2.png) -# Display the image using Matplotlib -plt.imshow(annotated_frame) -plt.axis('off') # Hide axes for a cleaner look -plt.show() +### Overlaying predictions and ground truth +``` +from deepforest.utilities import read_file +ground_truth = read_file(get_data(path="2019_YELL_2_528000_4978000_image_crop2.xml")) +plot_results(results, root_dir, ground_truth) ``` -![Bounding Boxes](../figures/tree_predicted_bounding_boxes.jpeg) +![sample_image](../www/Visualization3.png) -To show labels: -``` python +## Multi-class visualization -label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER) -annotated_frame = label_annotator.annotate( - scene=image.copy(), - detections=sv_detections, - labels=sv_detections['class_name'] -) +For results with more than one predicted class, the plot_results function will detect multiple classes and use a color palette instead of a single class. For control over the color palette see [supervision.draw.color](https://supervision.roboflow.com/draw/color/) -# Display the image using Matplotlib -plt.imshow(annotated_frame) -plt.axis('off') # Hide axes for a cleaner look -plt.show() ``` -![Labels](../figures/tree_predicted_labels.jpeg) ---- +color_palette = sv.ColorPalette.from_matplotlib('viridis', 6) +plot_results(results, root_dir, ground_truth, results_color=color_palette) +`` diff --git a/docs/data_annotation/annotation.md b/docs/data_annotation/annotation.md index ce6365d4..4af95a7c 100644 --- a/docs/data_annotation/annotation.md +++ b/docs/data_annotation/annotation.md @@ -53,14 +53,13 @@ It is often useful to train new training annotations starting from current predi ```python from deepforest import main -from deepforest.visualize import plot_predictions -from deepforest.utilities import boxes_to_shapefile +from deepforest.visualize import plot_results +from deepforest.utilities import read_file, image_to_geo_coordinates import rasterio as rio import geopandas as gpd from glob import glob import os -import matplotlib.pyplot as plt import numpy as np from shapely import geometry @@ -69,9 +68,10 @@ files = glob("{}/*.JPG".format(PATH_TO_DIR)) m = main.deepforest(label_dict={"Bird":0}) m.use_bird_release() for path in files: - #use predict_tile if each object is a orthomosaic + # Use predict_tile if each object is a orthomosaic boxes = m.predict_image(path=path) - #Open each file and get the geospatial information to convert output into a shapefile + + # Open each file and get the geospatial information to convert output into a shapefile rio_src = rio.open(path) image = rio_src.read() @@ -80,16 +80,14 @@ for path in files: continue #View result - image = np.rollaxis(image, 0, 3) - fig = plot_predictions(df=boxes, image=image) - plt.imshow(fig) + plot_results(boxes,root_dir=PATH_TO_DIR) #Create a shapefile, in this case img data was unprojected - shp = boxes_to_shapefile(boxes, root_dir=PATH_TO_DIR, projected=False) + geo_coords = image_to_geo_coordinates(boxes, root_dir=PATH_TO_DIR) #Get name of image and save a .shp in the same folder basename = os.path.splitext(os.path.basename(path))[0] - shp.to_file("{}/{}.shp".format(PATH_TO_DIR,basename)) + geo_coords.to_file("{}/{}.shp".format(PATH_TO_DIR,basename)) ``` ## Reading xml annotations in Pascal VOC diff --git a/docs/examples/Australia.ipynb b/docs/examples/Australia.ipynb index 10dba952..b1716c86 100644 --- a/docs/examples/Australia.ipynb +++ b/docs/examples/Australia.ipynb @@ -23,43 +23,36 @@ ] }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Reading config file: /Users/benweinstein/.conda/envs/test/lib/python3.12/site-packages/deepforest/data/deepforest_config.yml\n" - ] - }, { "name": "stderr", "output_type": "stream", "text": [ - "GPU available: False, used: False\n", - "TPU available: False, using: 0 TPU cores\n", - "IPU available: False, using: 0 IPUs\n", - "HPU available: False, using: 0 HPUs\n" + "/Users/benweinstein/.conda/envs/test/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "No validation file provided. Turning off validation loop\n" + "Reading config file: /Users/benweinstein/.conda/envs/test/lib/python3.10/site-packages/deepforest/data/deepforest_config.yml\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/Users/benweinstein/.conda/envs/test/lib/python3.12/site-packages/pytorch_lightning/trainer/connectors/logger_connector/logger_connector.py:67: Starting from v1.9.0, `tensorboardX` has been removed as a dependency of the `pytorch_lightning` package, due to potential conflicts with other packages in the ML ecosystem. For this reason, `logger=True` will use `CSVLogger` as the default logger, unless the `tensorboard` or `tensorboardX` packages are found. Please `pip install lightning[extra]` or one of them to enable TensorBoard support by default\n" + "GPU available: False, used: False\n", + "TPU available: False, using: 0 TPU cores\n", + "HPU available: False, using: 0 HPUs\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "Model from DeepForest release https://github.com/weecology/DeepForest/releases/tag/1.0.0 was already downloaded. Loading model from file.\n", - "Loading pre-built model: https://github.com/weecology/DeepForest/releases/tag/1.0.0\n", + "Model saved to: /Users/benweinstein/.conda/envs/test/lib/python3.10/site-packages/deepforest/data/NEON.pt\n", + "Loading pre-built model: main\n", "Only three band raster are accepted. Channels should be the final dimension. Input tile has shape (15399, 14775, 4). Check for transparent alpha channel and remove if present\n" ] } @@ -67,12 +60,13 @@ "source": [ "from deepforest import main\n", "from matplotlib import pyplot as plt\n", + "from deepforest.visualize import plot_results\n", "m = main.deepforest()\n", "\n", "m.use_release()\n", "try:\n", - " image = m.predict_tile(raster_path=\"/Users/benweinstein/Downloads/Plot13Ortho.tif\", patch_size=500, patch_overlap=0, return_plot=True)\n", - " plt.imshow(image[:,:,:3])\n", + " image = m.predict_tile(raster_path=\"/Users/benweinstein/Downloads/Plot13Ortho.tif\", patch_size=500, patch_overlap=0)\n", + " plot_results(image)\n", "except Exception as e:\n", " print(e)\n" ] @@ -93,7 +87,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Reading config file: /Users/benweinstein/.conda/envs/test/lib/python3.12/site-packages/deepforest/data/deepforest_config.yml\n" + "Reading config file: /Users/benweinstein/.conda/envs/test/lib/python3.10/site-packages/deepforest/data/deepforest_config.yml\n" ] }, { @@ -102,7 +96,6 @@ "text": [ "GPU available: False, used: False\n", "TPU available: False, using: 0 TPU cores\n", - "IPU available: False, using: 0 IPUs\n", "HPU available: False, using: 0 HPUs\n" ] }, @@ -110,9 +103,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "No validation file provided. Turning off validation loop\n", - "Model from DeepForest release https://github.com/weecology/DeepForest/releases/tag/1.0.0 was already downloaded. Loading model from file.\n", - "Loading pre-built model: https://github.com/weecology/DeepForest/releases/tag/1.0.0\n" + "Model saved to: /Users/benweinstein/.conda/envs/test/lib/python3.10/site-packages/deepforest/data/NEON.pt\n", + "Loading pre-built model: main\n" ] } ], @@ -422,7 +414,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.1" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/docs/examples/simple_load_and_predict.py b/docs/examples/simple_load_and_predict.py index 1ef2369f..20683a90 100644 --- a/docs/examples/simple_load_and_predict.py +++ b/docs/examples/simple_load_and_predict.py @@ -1,8 +1,6 @@ from deepforest import main from deepforest import visualize -import supervision as sv -import cv2 -import matplotlib.pyplot as plt +import os # This script loads an image, performs tree detection using the DeepForest library, and visualizes the detected trees with bounding boxes. @@ -18,20 +16,4 @@ trees = m.predict_tile(image=image, patch_size=3000, patch_overlap=0) # Filter out low-confidence detections trees = trees[trees.score > 0.3] - -# Convert the tree detections to Supervision format for visualization -sv_detections = visualize.convert_to_sv_format(trees) - -# Create a bounding box annotator -bounding_box_annotator = sv.BoxAnnotator() - -# Annotate the image with bounding boxes -annotated_frame = bounding_box_annotator.annotate( - scene=image, - detections=sv_detections -) - -# Display the annotated image using Matplotlib -plt.imshow(annotated_frame) -plt.axis('off') # Hide axes for a cleaner look -plt.show() +visualize.plot_results(trees, root_dir=os.path.dirname(image_path)) \ No newline at end of file diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index 63260707..bf1ec357 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -7,30 +7,41 @@ ## How do I use a pretrained model to predict an image? ```python -from deepforest import main -from deepforest import get_data -import matplotlib.pyplot as plt +from deepforest import main, get_data +from deepforest.visualize import plot_results +import os model = main.deepforest() model.use_release() sample_image_path = get_data("OSBS_029.png") -img = model.predict_image(path=sample_image_path, return_plot=True) - -#predict_image returns plot in BlueGreenRed (opencv style), but matplotlib likes RedGreenBlue, switch the channel order. Many functions in deepforest will automatically perform this flip for you and give a warning. -plt.imshow(img[:,:,::-1]) +results = model.predict_image(path=sample_image_path) +plot_results(results, root_dir=os.path.dirname(sample_image_path)) ``` + + +### Predict a tile + +Large tiles covering wide geographic extents cannot fit into memory during prediction and would yield poor results due to the density of bounding boxes. Often provided as geospatial .tif files, remote sensing data is best suited for the ```predict_tile``` function, which splits the tile into overlapping windows, performs prediction on each of the windows, and then reassembles the resulting annotations. + ![](../../www/getting_started1.png) +Let's show an example with a small image. For larger images, patch_size should be increased. -** please note that this video was made before the deepforest-pytorch -> deepforest name change. ** +```python +raster_path = get_data("OSBS_029.tif") +# Window size of 300px with an overlap of 25% among windows for this small tile. +boxes = model.predict_tile(raster_path, patch_size=300,patch_overlap=0.25) +plot_results(boxes, root_dir=os.path.dirname(raster_path)) +``` + +** Please note the predict tile function is sensitive to patch_size, especially when using the prebuilt model on new data** -
+We encourage users to try out a variety of patch sizes. For 0.1m data, 400-800px per window is appropriate, but it will depend on the density of tree plots. For coarser resolution tiles, >800px patch sizes have been effective, but we welcome feedback from users using a variety of spatial resolutions. -For single images, ```predict_image``` can read an image from memory or file and return predicted bounding boxes. -### Sample data +### Sample package data DeepForest comes with a small set of sample data that can be used to test out the provided examples. The data resides in the DeepForest data directory. Use the `get_data` helper function to locate the path to this directory, if needed. @@ -57,27 +68,4 @@ boxes = model.predict_image(path=image_path, return_plot = False) 4 173.0 0.0 229.0 33.0 Tree 0.738210 OSBS_029.png 5 258.0 198.0 291.0 230.0 Tree 0.716250 OSBS_029.png 6 97.0 305.0 152.0 363.0 Tree 0.711664 OSBS_029.png -7 52.0 72.0 85.0 108.0 Tree 0.698782 OSBS_029.png -``` - -### Predict a tile - -Large tiles covering wide geographic extents cannot fit into memory during prediction and would yield poor results due to the density of bounding boxes. Often provided as geospatial .tif files, remote sensing data is best suited for the ```predict_tile``` function, which splits the tile into overlapping windows, performs prediction on each of the windows, and then reassembles the resulting annotations. - -Let's show an example with a small image. For larger images, patch_size should be increased. - -```python -raster_path = get_data("OSBS_029.tif") -# Window size of 300px with an overlap of 25% among windows for this small tile. -predicted_raster = model.predict_tile(raster_path, return_plot = True, patch_size=300,patch_overlap=0.25) - -# View boxes overlayed when return_plot=True, when False, boxes are returned. -plt.imshow(predicted_raster) -plt.show() -``` - -** Please note the predict tile function is sensitive to patch_size, especially when using the prebuilt model on new data** - -We encourage users to try out a variety of patch sizes. For 0.1m data, 400-800px per window is appropriate, but it will depend on the density of tree plots. For coarser resolution tiles, >800px patch sizes have been effective, but we welcome feedback from users using a variety of spatial resolutions. - - +7 52.0 72.0 85.0 108.0 Tree 0.698782 OSBS_029.png \ No newline at end of file diff --git a/docs/getting_started/sample_test.ipynb b/docs/getting_started/sample_test.ipynb index 2ba7ae43..fec7a3fc 100644 --- a/docs/getting_started/sample_test.ipynb +++ b/docs/getting_started/sample_test.ipynb @@ -24,15 +24,17 @@ "source": [ "from deepforest import main\n", "from deepforest import get_data\n", + "from deepforest.visualize import plot_results\n", "import os\n", - "import matplotlib.pyplot as plt\n", "\n", - "# model = main.deepforest()\n", - "# model.use_release()\n", + "#model = main.deepforest()\n", + "#model.use_release()\n", "\n", - "# img = model.predict_image(path=\"../tests/data/OSBS_029_0.png\",return_plot=True)\n", - "# #predict_image returns plot in BlueGreenRed (opencv style), but matplotlib likes RedGreenBlue, switch the channel order.\n", - "# plt.imshow(img[:,:,::-1])" + "#image_path = get_data('OSBS_029.tif')\n", + "#root_dir = os.path.dirname(image_path)\n", + "\n", + "#results = model.predict_image(path=image_path)\n", + "#plot_results(results, root_dir=root_dir)" ] } ], diff --git a/tests/test_main.py b/tests/test_main.py index 6bcba552..6a1da630 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -209,7 +209,7 @@ def test_predict_image_fromfile(m): assert isinstance(prediction, pd.DataFrame) assert set(prediction.columns) == { - "xmin", "ymin", "xmax", "ymax", "label", "score", "image_path" + "xmin", "ymin", "xmax", "ymax", "label", "score", "image_path","geometry" } @@ -226,15 +226,7 @@ def test_predict_image_fromarray(m): prediction = m.predict_image(image=image) assert isinstance(prediction, pd.DataFrame) - assert set(prediction.columns) == {"xmin", "ymin", "xmax", "ymax", "label", "score"} - - -def test_predict_return_plot(m): - image = get_data(path="2019_YELL_2_528000_4978000_image_crop2.png") - image = np.array(Image.open(image)) - image = image.astype('float32') - plot = m.predict_image(image=image, return_plot=True) - assert isinstance(plot, np.ndarray) + assert set(prediction.columns) == {"xmin", "ymin", "xmax", "ymax", "label", "score","geometry"} def test_predict_big_file(m, tmpdir): @@ -243,26 +235,18 @@ def test_predict_big_file(m, tmpdir): csv_file = big_file() original_file = pd.read_csv(csv_file) df = m.predict_file(csv_file=csv_file, - root_dir=os.path.dirname(csv_file), - savedir=tmpdir) + root_dir=os.path.dirname(csv_file)) assert set(df.columns) == { 'label', 'score', 'image_path', 'geometry', "xmin", "ymin", "xmax", "ymax" } - printed_plots = glob.glob("{}/*.png".format(tmpdir)) - assert len(printed_plots) == len(original_file.image_path.unique()) - - def test_predict_small_file(m, tmpdir): csv_file = get_data("OSBS_029.csv") original_file = pd.read_csv(csv_file) - df = m.predict_file(csv_file, root_dir=os.path.dirname(csv_file), savedir=tmpdir) + df = m.predict_file(csv_file, root_dir=os.path.dirname(csv_file)) assert set(df.columns) == { 'label', 'score', 'image_path', 'geometry', "xmin", "ymin", "xmax", "ymax" } - printed_plots = glob.glob("{}/*.png".format(tmpdir)) - assert len(printed_plots) == len(original_file.image_path.unique()) - @pytest.mark.parametrize("batch_size", [1, 2]) def test_predict_dataloader(m, batch_size, raster_path): @@ -280,8 +264,7 @@ def test_predict_tile(m, raster_path): m.create_trainer() prediction = m.predict_tile(raster_path=raster_path, patch_size=300, - patch_overlap=0.1, - return_plot=False) + patch_overlap=0.1) assert isinstance(prediction, pd.DataFrame) assert set(prediction.columns) == { @@ -298,26 +281,10 @@ def test_predict_tile_from_array(m, patch_overlap, raster_path): m.create_trainer() prediction = m.predict_tile(image=image, patch_size=300, - patch_overlap=patch_overlap, - return_plot=False) + patch_overlap=patch_overlap) assert not prediction.empty -@pytest.mark.parametrize("patch_overlap", [0.1, 0]) -def test_predict_tile_from_array_with_return_plot(m, patch_overlap, raster_path): - # test predict numpy image - image = np.array(Image.open(raster_path)) - m.config["train"]["fast_dev_run"] = False - m.create_trainer() - prediction = m.predict_tile(image=image, - patch_size=300, - patch_overlap=patch_overlap, - return_plot=True, - color=(0, 255, 0)) - assert isinstance(prediction, np.ndarray) - assert prediction.size > 0 - - def test_predict_tile_no_mosaic(m, raster_path): # test no mosaic, return a tuple of crop and prediction m.config["train"]["fast_dev_run"] = False @@ -325,7 +292,6 @@ def test_predict_tile_no_mosaic(m, raster_path): prediction = m.predict_tile(raster_path=raster_path, patch_size=300, patch_overlap=0, - return_plot=False, mosaic=False) assert len(prediction) == 4 assert len(prediction[0]) == 2 @@ -336,7 +302,7 @@ def test_evaluate(m, tmpdir): csv_file = get_data("OSBS_029.csv") root_dir = os.path.dirname(csv_file) - results = m.evaluate(csv_file, root_dir, iou_threshold=0.4, savedir=tmpdir) + results = m.evaluate(csv_file, root_dir, iou_threshold=0.4) # Does this make reasonable predictions, we know the model works. assert np.round(results["box_precision"], 2) > 0.5 @@ -638,7 +604,6 @@ def test_predict_tile_with_crop_model(m, config): patch_size = 400 patch_overlap = 0.05 iou_threshold = 0.15 - return_plot = False mosaic = True # Set up the crop model crop_model = model.CropModel() @@ -650,7 +615,6 @@ def test_predict_tile_with_crop_model(m, config): patch_size=patch_size, patch_overlap=patch_overlap, iou_threshold=iou_threshold, - return_plot=return_plot, mosaic=mosaic, crop_model=crop_model) diff --git a/tests/test_visualize.py b/tests/test_visualize.py index 9d14c83a..1a13bd51 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -1,6 +1,6 @@ # Test visualize from deepforest import visualize -from deepforest import main +from deepforest.utilities import read_file from deepforest import get_data import os import pytest @@ -10,6 +10,7 @@ import pandas as pd import geopandas as gpd from shapely import geometry +import cv2 def test_format_boxes(m): @@ -64,6 +65,12 @@ def test_plot_predictions_and_targets(m, tmpdir): image, prediction, target, image_name=os.path.basename(path), savedir=tmpdir) assert os.path.exists(save_figure_path) +def test_predict_and_plot(m, tmpdir): + sample_image_path = get_data("OSBS_029.png") + results = m.predict_image(path=sample_image_path) + visualize.plot_results(results, root_dir=os.path.dirname(sample_image_path), savedir=tmpdir) + + assert os.path.exists(os.path.join(tmpdir, "OSBS_029.png")) def test_convert_to_sv_format(): # Create a mock DataFrame @@ -72,11 +79,12 @@ def test_convert_to_sv_format(): 'ymin': [0, 20], 'xmax': [5, 15], 'ymax': [5, 25], - 'label': ['tree', 'tree'], + 'label': ['Tree', 'Tree'], 'score': [0.9, 0.8], 'image_path': ['image1.jpg', 'image1.jpg'] } df = pd.DataFrame(data) + df = read_file(df) # Call the function detections = visualize.convert_to_sv_format(df) @@ -90,4 +98,66 @@ def test_convert_to_sv_format(): np.testing.assert_array_equal(detections.xyxy, expected_boxes) np.testing.assert_array_equal(detections.class_id, expected_labels) np.testing.assert_array_equal(detections.confidence, expected_scores) - assert detections['class_name'] == ['tree', 'tree'] + assert detections['class_name'] == ['Tree', 'Tree'] + +def test_plot_results_box(m, tmpdir): + # Create a mock DataFrame with box annotations + data = { + 'xmin': [10, 20], + 'ymin': [10, 20], + 'xmax': [30, 40], + 'ymax': [30, 40], + 'label': ['Tree', 'Tree'], + 'image_path': [get_data("OSBS_029.tif"), get_data("OSBS_029.tif")], + "score": [0.9, 0.8] + } + df = pd.DataFrame(data) + gdf = read_file(df) + + # Call the function + visualize.plot_results(gdf, savedir=tmpdir, root_dir=os.path.dirname(df['image_path'].iloc[0])) + + # Assertions + assert os.path.exists(os.path.join(tmpdir, "OSBS_029.png")) + +def test_plot_results_point(m, tmpdir): + # Create a mock DataFrame with point annotations + data = { + 'x': [15, 25], + 'y': [15, 25], + 'label': ['Tree', 'Tree'], + 'image_path': [get_data("OSBS_029.tif"), get_data("OSBS_029.tif")], + 'score': [0.9, 0.8], + 'label': ['Tree', 'Tree'] + } + df = pd.DataFrame(data) + gdf = read_file(df) + + # Call the function + visualize.plot_results(gdf, savedir=tmpdir, root_dir=os.path.dirname(df['image_path'].iloc[0])) + + # Assertions + assert os.path.exists(os.path.join(tmpdir, "OSBS_029.png")) + + +def test_plot_results_polygon(m, tmpdir): + # Create a mock DataFrame with polygon annotations + data = { + 'geometry': [geometry.Polygon([(10, 10), (20, 10), (20, 20), (10, 20), (15, 25)]), + geometry.Polygon([(30, 30), (40, 30), (40, 40), (30, 40), (35, 35)])], + 'label': ['Tree', 'Tree'], + 'image_path': [get_data("OSBS_029.tif"), get_data("OSBS_029.tif")], + 'score': [0.9, 0.8] + } + gdf = gpd.GeoDataFrame(data) + + #Read in image and get height + image = cv2.imread(get_data("OSBS_029.tif")) + height = image.shape[0] + width = image.shape[1] + + # Call the function + visualize.plot_results(gdf, savedir=tmpdir, root_dir=os.path.dirname(gdf['image_path'].iloc[0]),height=height, width=width) + + # Assertions + assert os.path.exists(os.path.join(tmpdir, "OSBS_029.png")) diff --git a/www/Visualization1.png b/www/Visualization1.png new file mode 100644 index 00000000..a659dead Binary files /dev/null and b/www/Visualization1.png differ diff --git a/www/Visualization2.png b/www/Visualization2.png new file mode 100644 index 00000000..e7dd3ee7 Binary files /dev/null and b/www/Visualization2.png differ diff --git a/www/Visualization3.png b/www/Visualization3.png new file mode 100644 index 00000000..1c074231 Binary files /dev/null and b/www/Visualization3.png differ diff --git a/www/getting_started1.png b/www/getting_started1.png index a778e3b1..cc424210 100644 Binary files a/www/getting_started1.png and b/www/getting_started1.png differ