Merge branch 'main' of https://github.com/jonathanseele/WeCanopy
Browse files
generate_tree_images/.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
/detected_trees
|
2 |
+
/training_images
|
generate_tree_images/generate_tree_images.ipynb
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fa9e3f0c51e56d3590954647541e3860ec62d517bed85842d641605276a4dee1
|
3 |
+
size 13104
|
generate_tree_images/generate_tree_images.py
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import rasterio
|
3 |
+
import geopandas as gpd
|
4 |
+
from shapely.geometry import box
|
5 |
+
from rasterio.mask import mask
|
6 |
+
from PIL import Image
|
7 |
+
import numpy as np
|
8 |
+
import warnings
|
9 |
+
from rasterio.errors import NodataShadowWarning
|
10 |
+
import sys
|
11 |
+
|
12 |
+
warnings.filterwarnings("ignore", category=NodataShadowWarning)
|
13 |
+
|
14 |
+
def cut_trees(output_dir, geojson_path, tif_path):
|
15 |
+
# create output directory if it doesnt exist
|
16 |
+
if not os.path.exists(output_dir):
|
17 |
+
os.makedirs(output_dir)
|
18 |
+
|
19 |
+
# Load the GeoDataFrame
|
20 |
+
gdf = gpd.read_file(geojson_path)
|
21 |
+
|
22 |
+
# Clear the terminal screen
|
23 |
+
os.system('cls' if os.name == 'nt' else 'clear')
|
24 |
+
|
25 |
+
# Open the .tif file
|
26 |
+
with rasterio.open(tif_path) as src:
|
27 |
+
# Get the bounds of the .tif image
|
28 |
+
tif_bounds = box(*src.bounds)
|
29 |
+
|
30 |
+
# Get the CRS (Coordinate Reference System) of the .tif image
|
31 |
+
tif_crs = src.crs
|
32 |
+
|
33 |
+
# Reproject the GeoDataFrame to the CRS of the .tif file
|
34 |
+
gdf = gdf.to_crs(tif_crs)
|
35 |
+
|
36 |
+
# Loop through each polygon in the GeoDataFrame
|
37 |
+
N = len(gdf)
|
38 |
+
n = int(N/10)
|
39 |
+
image_counter = 0
|
40 |
+
for idx, row in gdf.iterrows():
|
41 |
+
if idx % n == 0:
|
42 |
+
progress = f"{round(idx/N*100)} % complete --> {idx}/{N}"
|
43 |
+
sys.stdout.write('\r' + progress)
|
44 |
+
sys.stdout.flush()
|
45 |
+
|
46 |
+
# Extract the geometry (polygon)
|
47 |
+
geom = row['geometry']
|
48 |
+
name = row['id']
|
49 |
+
|
50 |
+
# Check if the polygon intersects the image bounds
|
51 |
+
if geom.intersects(tif_bounds):
|
52 |
+
# Create a mask for the current polygon
|
53 |
+
out_image, out_transform = mask(src, [geom], crop=True)
|
54 |
+
|
55 |
+
# Convert the masked image to a numpy array
|
56 |
+
out_image = out_image.transpose(1, 2, 0) # rearrange dimensions for PIL (H, W, C)
|
57 |
+
|
58 |
+
# Ensure the array is not empty
|
59 |
+
if out_image.size == 0:
|
60 |
+
message = f"{round(idx/N*100)} % complete --> {idx}/{N} | Polygon {idx} resulted in an empty image and will be skipped."
|
61 |
+
sys.stdout.write('\r' + message)
|
62 |
+
sys.stdout.flush()
|
63 |
+
continue
|
64 |
+
|
65 |
+
# Remove the zero-padded areas (optional)
|
66 |
+
mask_array = (out_image[:, :, 0] != src.nodata)
|
67 |
+
non_zero_rows = np.any(mask_array, axis=1)
|
68 |
+
non_zero_cols = np.any(mask_array, axis=0)
|
69 |
+
|
70 |
+
# Ensure there are non-zero rows and columns
|
71 |
+
if not np.any(non_zero_rows) or not np.any(non_zero_cols):
|
72 |
+
message = f"{round(idx/N*100)} % complete --> {idx}/{N} | Polygon {idx} resulted in an invalid image area and will be skipped."
|
73 |
+
sys.stdout.write('\r' + message)
|
74 |
+
sys.stdout.flush()
|
75 |
+
continue
|
76 |
+
|
77 |
+
out_image = out_image[non_zero_rows][:, non_zero_cols]
|
78 |
+
|
79 |
+
# Convert to a PIL Image and save as PNG
|
80 |
+
out_image = Image.fromarray(out_image.astype(np.uint8)) # Ensure correct type for PIL
|
81 |
+
output_path = os.path.join(output_dir, f'tree_{name}.png')
|
82 |
+
out_image.save(output_path)
|
83 |
+
image_counter += 1
|
84 |
+
else:
|
85 |
+
message = f"{round(idx/N*100)} % complete --> {idx}/{N} | Polygon {idx} is outside the image bounds and will be skipped."
|
86 |
+
sys.stdout.write('\r' + message)
|
87 |
+
sys.stdout.flush()
|
88 |
+
|
89 |
+
print(f'\n {image_counter}/{N} Tree images have been successfully saved in the "detected_trees" folder.')
|
90 |
+
|
91 |
+
|
92 |
+
def resize_images(input_folder, output_folder, target_size):
|
93 |
+
# Create the output folder if it doesn't exist
|
94 |
+
if not os.path.exists(output_folder):
|
95 |
+
os.makedirs(output_folder)
|
96 |
+
|
97 |
+
counter = 0
|
98 |
+
# Loop through all files in the input folder
|
99 |
+
for filename in os.listdir(input_folder):
|
100 |
+
if filename.endswith('.png'): # Check for PNG files
|
101 |
+
# Open image
|
102 |
+
with Image.open(os.path.join(input_folder, filename)) as img:
|
103 |
+
# Resize image while preserving aspect ratio
|
104 |
+
img.thumbnail(target_size, Image.LANCZOS)
|
105 |
+
# Calculate paste position to center image in canvas
|
106 |
+
paste_pos = ((target_size[0] - img.size[0]) // 2, (target_size[1] - img.size[1]) // 2)
|
107 |
+
# Create a new blank canvas with the target size and black background
|
108 |
+
new_img = Image.new("RGBA", target_size, (0, 0, 0, 255))
|
109 |
+
# Paste resized image onto the canvas
|
110 |
+
new_img.paste(img, paste_pos, img)
|
111 |
+
# Convert to RGB to remove transparency by merging with black background
|
112 |
+
new_img = new_img.convert("RGB")
|
113 |
+
# Save resized image to output folder
|
114 |
+
new_img.save(os.path.join(output_folder, filename))
|
115 |
+
|
116 |
+
counter += 1
|
117 |
+
# Display the counter
|
118 |
+
if counter % 50 == 0:
|
119 |
+
message = f"Processed {counter} images"
|
120 |
+
print(message, end='\r')
|
121 |
+
|
122 |
+
# Final message after processing all images
|
123 |
+
print(f"Processed a total of {counter} images.")
|
124 |
+
|
125 |
+
|
126 |
+
# THIS IS THE FUNCTION TO IMPORT
|
127 |
+
def generate_tree_images(geojson_path, tif_path, target_size = (224, 224)):
|
128 |
+
"""
|
129 |
+
INPUT: geojson path, tif_path that contain the trees, optional target_size of the resulting images
|
130 |
+
|
131 |
+
RETURNS: nothing
|
132 |
+
|
133 |
+
Action: It creates two folders: + "detected trees" --> the cut tree images
|
134 |
+
+ "tree_images" --> the processed cut tree images, ready to use for species recognition
|
135 |
+
"""
|
136 |
+
|
137 |
+
|
138 |
+
# Set input and output folders
|
139 |
+
folder_cut_trees = "detected_trees"
|
140 |
+
folder_finished_images = "tree_images"
|
141 |
+
# Set target size (width, height)
|
142 |
+
cut_trees(geojson_path = geojson_path, tif_path = tif_path, output_dir = folder_cut_trees)
|
143 |
+
resize_images(input_folder = folder_cut_trees, output_folder = folder_finished_images, target_size = target_size)
|
144 |
+
|
145 |
+
|
generate_tree_images/readme.md
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
## Use the generate_tree_images.py for the pipeline
|
2 |
+
|
3 |
+
--> use the function *generate_tree_images*
|