opencropphenotyping 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencropphenotyping/batch_processing.py +128 -0
- opencropphenotyping/cli.py +218 -0
- opencropphenotyping/coco.py +175 -0
- opencropphenotyping/evaluation.py +352 -0
- opencropphenotyping/indices.py +253 -0
- opencropphenotyping/io.py +540 -0
- opencropphenotyping/pipeline.py +393 -0
- opencropphenotyping/segmentation.py +1320 -0
- opencropphenotyping/statistics.py +17 -0
- opencropphenotyping/traits.py +60 -0
- opencropphenotyping/visualization.py +467 -0
- opencropphenotyping-0.2.0.dist-info/METADATA +695 -0
- opencropphenotyping-0.2.0.dist-info/RECORD +17 -0
- opencropphenotyping-0.2.0.dist-info/WHEEL +5 -0
- opencropphenotyping-0.2.0.dist-info/entry_points.txt +2 -0
- opencropphenotyping-0.2.0.dist-info/licenses/LICENSE +21 -0
- opencropphenotyping-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from opencropphenotyping.pipeline import export_results, export_uav_results, process_sentinel2, process_uav_segmentation
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def process_batch(
|
|
7
|
+
input_dir: Path,
|
|
8
|
+
output_dir: Path,
|
|
9
|
+
processed_dir: Path,
|
|
10
|
+
indices: list[str] | None = None,
|
|
11
|
+
ndvi_threshold: float = 0.3,
|
|
12
|
+
resolution: int = 10,
|
|
13
|
+
) -> None:
|
|
14
|
+
product_dirs = [
|
|
15
|
+
path for path in input_dir.iterdir()
|
|
16
|
+
if path.is_dir()
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
for product_dir in product_dirs:
|
|
20
|
+
product_output_dir = output_dir / product_dir.name
|
|
21
|
+
product_processed_dir = processed_dir / product_dir.name
|
|
22
|
+
|
|
23
|
+
print(f"Processing product: {product_dir.name}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
result_product = process_sentinel2(
|
|
27
|
+
input_dir=product_dir,
|
|
28
|
+
output_dir=product_processed_dir,
|
|
29
|
+
indices=indices,
|
|
30
|
+
ndvi_threshold=ndvi_threshold,
|
|
31
|
+
resolution=resolution,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export_results(
|
|
35
|
+
result_product,
|
|
36
|
+
product_output_dir,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
print(f"Successfully processed: {product_dir.name}")
|
|
40
|
+
|
|
41
|
+
except Exception as error:
|
|
42
|
+
print(
|
|
43
|
+
f"Error processing product '{product_dir.name}': {error}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def process_uav_batch(
|
|
47
|
+
input_dir: Path,
|
|
48
|
+
output_dir: Path,
|
|
49
|
+
threshold: float = 25,
|
|
50
|
+
vegetation_fraction_threshold: float = 0.04,
|
|
51
|
+
export_intermediate: bool = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Process multiple UAV image datasets from a parent directory."""
|
|
54
|
+
|
|
55
|
+
dataset_dirs = [
|
|
56
|
+
path for path in input_dir.iterdir()
|
|
57
|
+
if path.is_dir()
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for dataset_dir in dataset_dirs:
|
|
61
|
+
dataset_output_dir = output_dir / dataset_dir.name
|
|
62
|
+
|
|
63
|
+
image_files = [
|
|
64
|
+
path for path in dataset_dir.iterdir()
|
|
65
|
+
if path.suffix.lower() in {".jpg", ".jpeg", ".png"}
|
|
66
|
+
and "georef" not in path.stem.lower()
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
geotiff_files = [
|
|
70
|
+
path for path in dataset_dir.iterdir()
|
|
71
|
+
if path.suffix.lower() in {".tif", ".tiff"}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
geopackage_files = [
|
|
75
|
+
path for path in dataset_dir.iterdir()
|
|
76
|
+
if path.suffix.lower() == ".gpkg"
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
if len(image_files) != 1:
|
|
80
|
+
print(
|
|
81
|
+
f"Error processing '{dataset_dir.name}': "
|
|
82
|
+
f"expected 1 RGB image, found {len(image_files)}."
|
|
83
|
+
)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if len(geotiff_files) != 1:
|
|
87
|
+
print(
|
|
88
|
+
f"Error processing '{dataset_dir.name}': "
|
|
89
|
+
f"expected 1 GeoTIFF, found {len(geotiff_files)}."
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if len(geopackage_files) != 1:
|
|
94
|
+
print(
|
|
95
|
+
f"Error processing '{dataset_dir.name}': "
|
|
96
|
+
f"expected 1 GeoPackage, found {len(geopackage_files)}."
|
|
97
|
+
)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
image_path = image_files[0]
|
|
101
|
+
geotiff_path = geotiff_files[0]
|
|
102
|
+
geopackage_path = geopackage_files[0]
|
|
103
|
+
|
|
104
|
+
print(f"Processing UAV dataset: {dataset_dir.name}")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = process_uav_segmentation(
|
|
108
|
+
image_path=image_path,
|
|
109
|
+
geotiff_path=geotiff_path,
|
|
110
|
+
geopackage_path=geopackage_path,
|
|
111
|
+
threshold=threshold,
|
|
112
|
+
vegetation_fraction_threshold=vegetation_fraction_threshold,
|
|
113
|
+
export_intermediate=export_intermediate,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
export_uav_results(
|
|
117
|
+
result,
|
|
118
|
+
dataset_output_dir,
|
|
119
|
+
export_intermediate=export_intermediate
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
print(f"Successfully processed: {dataset_dir.name}")
|
|
123
|
+
|
|
124
|
+
except Exception as error:
|
|
125
|
+
print(
|
|
126
|
+
f"Error processing UAV dataset "
|
|
127
|
+
f"'{dataset_dir.name}': {error}"
|
|
128
|
+
)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from opencropphenotyping.batch_processing import process_batch, process_uav_batch
|
|
6
|
+
from opencropphenotyping.pipeline import export_results, export_uav_results, process_sentinel2, process_uav_segmentation
|
|
7
|
+
|
|
8
|
+
app = typer.Typer()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
def process(
|
|
13
|
+
input_dir: Path = typer.Option(..., help="Path to the Sentinel-2 product."),
|
|
14
|
+
output_dir: Path = typer.Option(..., help="Directory where results will be saved."),
|
|
15
|
+
processed_dir: Path = typer.Option(..., help="Directory for processed files."),
|
|
16
|
+
indices: list[str] = typer.Option(
|
|
17
|
+
["ndvi"],
|
|
18
|
+
help="Vegetation indices to compute.",
|
|
19
|
+
),
|
|
20
|
+
ndvi_threshold: float = typer.Option(
|
|
21
|
+
0.3,
|
|
22
|
+
help="NDVI threshold used for vegetation masking.",
|
|
23
|
+
),
|
|
24
|
+
resolution: int = typer.Option(
|
|
25
|
+
10,
|
|
26
|
+
help="Target spatial resolution in meters.",
|
|
27
|
+
),
|
|
28
|
+
):
|
|
29
|
+
"""Process a single Sentinel-2 product."""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
result = process_sentinel2(
|
|
33
|
+
input_dir=input_dir,
|
|
34
|
+
output_dir=processed_dir,
|
|
35
|
+
indices=indices,
|
|
36
|
+
ndvi_threshold=ndvi_threshold,
|
|
37
|
+
resolution=resolution,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export_results(
|
|
41
|
+
result,
|
|
42
|
+
output_dir,
|
|
43
|
+
)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
typer.echo(f"Error during processing: {e}", err=True)
|
|
46
|
+
raise typer.Exit(code=1)
|
|
47
|
+
|
|
48
|
+
typer.echo("Processing completed successfully.")
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def batch(
|
|
52
|
+
input_dir: Path = typer.Option(..., help="Path to the directory containing Sentinel-2 products."),
|
|
53
|
+
output_dir: Path = typer.Option(..., help="Directory where results will be saved."),
|
|
54
|
+
processed_dir: Path = typer.Option(..., help="Directory for processed files."),
|
|
55
|
+
indices: list[str] = typer.Option(
|
|
56
|
+
["ndvi"],
|
|
57
|
+
help="Vegetation indices to compute.",
|
|
58
|
+
),
|
|
59
|
+
ndvi_threshold: float = typer.Option(
|
|
60
|
+
0.3,
|
|
61
|
+
help="NDVI threshold used for vegetation masking.",
|
|
62
|
+
),
|
|
63
|
+
resolution: int = typer.Option(
|
|
64
|
+
10,
|
|
65
|
+
help="Target spatial resolution in meters.",
|
|
66
|
+
),
|
|
67
|
+
):
|
|
68
|
+
"""Process multiple Sentinel-2 products from a directory."""
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
process_batch(
|
|
72
|
+
input_dir=input_dir,
|
|
73
|
+
output_dir=output_dir,
|
|
74
|
+
processed_dir=processed_dir,
|
|
75
|
+
indices=indices,
|
|
76
|
+
ndvi_threshold=ndvi_threshold,
|
|
77
|
+
resolution=resolution,
|
|
78
|
+
)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
typer.echo(f"Error during processing: {e}", err=True)
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
|
|
83
|
+
typer.echo("Batch processing completed successfully.")
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
app()
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def segment_uav(
|
|
90
|
+
image: Path = typer.Option(
|
|
91
|
+
...,
|
|
92
|
+
help="Path to the UAV RGB image.",
|
|
93
|
+
),
|
|
94
|
+
geotiff: Path = typer.Option(
|
|
95
|
+
...,
|
|
96
|
+
help="Path to the georeferenced GeoTIFF.",
|
|
97
|
+
),
|
|
98
|
+
geopackage: Path = typer.Option(
|
|
99
|
+
...,
|
|
100
|
+
help="Path to the experimental plot GeoPackage.",
|
|
101
|
+
),
|
|
102
|
+
output_dir: Path = typer.Option(
|
|
103
|
+
...,
|
|
104
|
+
help="Directory where UAV segmentation results will be saved.",
|
|
105
|
+
),
|
|
106
|
+
best_angle: float | None = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
help="Crop-row orientation angle. If omitted, it is estimated automatically.",
|
|
109
|
+
),
|
|
110
|
+
threshold: float = typer.Option(
|
|
111
|
+
25.0,
|
|
112
|
+
help="ExG threshold used for vegetation segmentation.",
|
|
113
|
+
),
|
|
114
|
+
vegetation_fraction_threshold: float = typer.Option(
|
|
115
|
+
0.04,
|
|
116
|
+
help="Vegetation fraction threshold used to classify planting positions.",
|
|
117
|
+
),
|
|
118
|
+
export_intermediate: bool = typer.Option(
|
|
119
|
+
False,
|
|
120
|
+
help="If True, all intermediate results will be exported.",
|
|
121
|
+
)
|
|
122
|
+
):
|
|
123
|
+
"""Process a UAV RGB image and detect crop plants."""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result = process_uav_segmentation(
|
|
127
|
+
image_path=image,
|
|
128
|
+
geotiff_path=geotiff,
|
|
129
|
+
geopackage_path=geopackage,
|
|
130
|
+
best_angle=best_angle,
|
|
131
|
+
threshold=threshold,
|
|
132
|
+
vegetation_fraction_threshold=vegetation_fraction_threshold,
|
|
133
|
+
export_intermediate=export_intermediate,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
export_uav_results(
|
|
137
|
+
result,
|
|
138
|
+
output_dir,
|
|
139
|
+
export_intermediate=export_intermediate
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
typer.echo(
|
|
144
|
+
f"Error during UAV segmentation: {e}",
|
|
145
|
+
err=True,
|
|
146
|
+
)
|
|
147
|
+
raise typer.Exit(code=1)
|
|
148
|
+
|
|
149
|
+
typer.echo(
|
|
150
|
+
"UAV segmentation completed successfully."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if(export_intermediate):
|
|
154
|
+
typer.echo(
|
|
155
|
+
f"Best angle: {result.best_angle:.1f}°"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
typer.echo(
|
|
159
|
+
f"Detected rows: {len(result.row_positions)}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
typer.echo(
|
|
163
|
+
f"Planting positions: {len(result.plants)}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
typer.echo(
|
|
167
|
+
"Occupied positions: "
|
|
168
|
+
f"{(~result.plants['missing_candidate']).sum()}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
typer.echo(
|
|
172
|
+
"Potentially missing positions: "
|
|
173
|
+
f"{result.plants['missing_candidate'].sum()}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@app.command()
|
|
177
|
+
def batch_uav(
|
|
178
|
+
input_dir: Path = typer.Option(
|
|
179
|
+
...,
|
|
180
|
+
help="Parent directory containing UAV datasets.",
|
|
181
|
+
),
|
|
182
|
+
output_dir: Path = typer.Option(
|
|
183
|
+
...,
|
|
184
|
+
help="Directory where UAV results will be saved.",
|
|
185
|
+
),
|
|
186
|
+
threshold: float = typer.Option(
|
|
187
|
+
25,
|
|
188
|
+
help="ExG threshold used for vegetation segmentation.",
|
|
189
|
+
),
|
|
190
|
+
vegetation_fraction_threshold: float = typer.Option(
|
|
191
|
+
0.04,
|
|
192
|
+
help="Threshold used to classify missing plants.",
|
|
193
|
+
),
|
|
194
|
+
export_intermediate: bool = typer.Option(
|
|
195
|
+
False,
|
|
196
|
+
help="If True, all intermediate results will be exported.",
|
|
197
|
+
),
|
|
198
|
+
):
|
|
199
|
+
"""Process multiple UAV datasets from a parent directory."""
|
|
200
|
+
try:
|
|
201
|
+
process_uav_batch(
|
|
202
|
+
input_dir=input_dir,
|
|
203
|
+
output_dir=output_dir,
|
|
204
|
+
threshold=threshold,
|
|
205
|
+
vegetation_fraction_threshold=vegetation_fraction_threshold,
|
|
206
|
+
export_intermediate=export_intermediate,
|
|
207
|
+
)
|
|
208
|
+
except Exception as error:
|
|
209
|
+
typer.echo(
|
|
210
|
+
f"Error during UAV batch processing: {error}",
|
|
211
|
+
err=True,
|
|
212
|
+
)
|
|
213
|
+
raise typer.Exit(code=1)
|
|
214
|
+
|
|
215
|
+
typer.echo("UAV batch processing completed successfully.")
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
app()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_coco_annotations(
|
|
9
|
+
json_paths: list[Path],
|
|
10
|
+
) -> dict[str, dict]:
|
|
11
|
+
"""
|
|
12
|
+
Load COCO annotation files from JSON files.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
json_paths : list[Path]
|
|
17
|
+
Paths to COCO annotation JSON files.
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
dict[str, dict]
|
|
22
|
+
Dictionary mapping each JSON filename to its decoded
|
|
23
|
+
annotation data.
|
|
24
|
+
"""
|
|
25
|
+
annotations = {}
|
|
26
|
+
|
|
27
|
+
for path in json_paths:
|
|
28
|
+
with open(path, encoding="utf-8") as f:
|
|
29
|
+
annotations[path.name] = json.load(f)
|
|
30
|
+
|
|
31
|
+
return annotations
|
|
32
|
+
|
|
33
|
+
def build_image_annotation_index(
|
|
34
|
+
coco_datasets: dict[str, dict],
|
|
35
|
+
) -> dict[str, dict]:
|
|
36
|
+
"""
|
|
37
|
+
Build an index linking each image to its COCO annotations.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
coco_datasets : dict[str, dict]
|
|
42
|
+
COCO datasets loaded from JSON files.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
dict[str, dict]
|
|
47
|
+
Dictionary indexed by image filename. Each entry contains
|
|
48
|
+
the image metadata under ``"image"`` and the corresponding
|
|
49
|
+
annotations under ``"annotations"``.
|
|
50
|
+
"""
|
|
51
|
+
image_annotations = {}
|
|
52
|
+
|
|
53
|
+
for data in coco_datasets.values():
|
|
54
|
+
|
|
55
|
+
for image in data["images"]:
|
|
56
|
+
|
|
57
|
+
image_annotations[image["file_name"]] = {
|
|
58
|
+
"image": image,
|
|
59
|
+
"annotations": [
|
|
60
|
+
annotation
|
|
61
|
+
for annotation in data["annotations"]
|
|
62
|
+
if annotation["image_id"] == image["id"]
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return image_annotations
|
|
67
|
+
|
|
68
|
+
def build_plant_annotations_dataframe(
|
|
69
|
+
image_annotations: dict[str, dict],
|
|
70
|
+
) -> pd.DataFrame:
|
|
71
|
+
"""
|
|
72
|
+
Convert indexed COCO plant annotations into a DataFrame.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
image_annotations : dict[str, dict]
|
|
77
|
+
Image annotation index produced by
|
|
78
|
+
:func:`build_image_annotation_index`.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
pd.DataFrame
|
|
83
|
+
DataFrame containing one row per annotated plant.
|
|
84
|
+
Bounding-box coordinates and plant centre coordinates are
|
|
85
|
+
expressed in the original image coordinate system.
|
|
86
|
+
"""
|
|
87
|
+
plant_annotations = []
|
|
88
|
+
|
|
89
|
+
columns = [
|
|
90
|
+
"image_id",
|
|
91
|
+
"image_name",
|
|
92
|
+
"category_id",
|
|
93
|
+
"bbox_x",
|
|
94
|
+
"bbox_y",
|
|
95
|
+
"bbox_width",
|
|
96
|
+
"bbox_height",
|
|
97
|
+
"center_x",
|
|
98
|
+
"center_y",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for image_name, info in image_annotations.items():
|
|
102
|
+
|
|
103
|
+
image_id = info["image"]["id"]
|
|
104
|
+
|
|
105
|
+
for annotation in info["annotations"]:
|
|
106
|
+
|
|
107
|
+
x, y, width, height = annotation["bbox"]
|
|
108
|
+
|
|
109
|
+
plant_annotations.append(
|
|
110
|
+
{
|
|
111
|
+
"image_id": image_id,
|
|
112
|
+
"image_name": image_name,
|
|
113
|
+
"category_id": annotation["category_id"],
|
|
114
|
+
"bbox_x": x,
|
|
115
|
+
"bbox_y": y,
|
|
116
|
+
"bbox_width": width,
|
|
117
|
+
"bbox_height": height,
|
|
118
|
+
"center_x": x + width / 2,
|
|
119
|
+
"center_y": y + height / 2,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return pd.DataFrame(plant_annotations, columns=columns)
|
|
124
|
+
|
|
125
|
+
def get_image_annotations(
|
|
126
|
+
plant_annotations: pd.DataFrame,
|
|
127
|
+
image_path: Path,
|
|
128
|
+
) -> pd.DataFrame:
|
|
129
|
+
"""
|
|
130
|
+
Return plant annotations associated with a given image.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
plant_annotations : pd.DataFrame
|
|
135
|
+
DataFrame containing plant annotations for all images.
|
|
136
|
+
Must contain an ``image_name`` column.
|
|
137
|
+
image_path : Path
|
|
138
|
+
Path to the image for which annotations are requested.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
pd.DataFrame
|
|
143
|
+
DataFrame containing only the annotations associated with
|
|
144
|
+
``image_path``.
|
|
145
|
+
"""
|
|
146
|
+
return plant_annotations[
|
|
147
|
+
plant_annotations["image_name"] == image_path.name
|
|
148
|
+
].copy()
|
|
149
|
+
|
|
150
|
+
def get_annotation_centers(
|
|
151
|
+
plant_annotations: pd.DataFrame,
|
|
152
|
+
) -> np.ndarray:
|
|
153
|
+
"""
|
|
154
|
+
Extract annotated plant centres as an array of x/y coordinates.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
plant_annotations : pd.DataFrame
|
|
159
|
+
Plant annotations containing ``center_x`` and ``center_y``.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
np.ndarray
|
|
164
|
+
Array of plant centres with shape ``(n_annotations, 2)``.
|
|
165
|
+
Columns correspond to x and y coordinates.
|
|
166
|
+
"""
|
|
167
|
+
if len(plant_annotations) == 0:
|
|
168
|
+
return np.empty(
|
|
169
|
+
(0, 2),
|
|
170
|
+
dtype=float,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return plant_annotations[
|
|
174
|
+
["center_x", "center_y"]
|
|
175
|
+
].to_numpy(dtype=float)
|