phyloforfun commited on
Commit
1f68f21
·
1 Parent(s): 60a5c82
Files changed (2) hide show
  1. app.py +327 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, math, csv
2
+ import streamlit as st
3
+ from streamlit_image_select import image_select
4
+ import cv2
5
+ import numpy as np
6
+ from PIL import Image
7
+ import matplotlib.colors as mcolors
8
+
9
+ class DirectoryManager:
10
+ def __init__(self, output_dir):
11
+ self.dir_output = output_dir
12
+ self.mask_flag = os.path.join(output_dir, "mask_flag")
13
+ self.mask_plant = os.path.join(output_dir, "mask_plant")
14
+ self.mask_plant_plot = os.path.join(output_dir, "mask_plant_plot")
15
+ self.plant_rgb = os.path.join(output_dir, "plant_rgb")
16
+ self.plot_rgb = os.path.join(output_dir, "plot_rgb")
17
+ self.plant_rgb_warp = os.path.join(output_dir, "plant_rgb_warp")
18
+ self.plant_mask_warp = os.path.join(output_dir, "plant_mask_warp")
19
+ self.data = os.path.join(output_dir, "data")
20
+
21
+ def create_directories(self):
22
+ os.makedirs(self.dir_output, exist_ok=True)
23
+ os.makedirs(self.mask_flag, exist_ok=True)
24
+ os.makedirs(self.mask_plant, exist_ok=True)
25
+ os.makedirs(self.mask_plant_plot, exist_ok=True)
26
+ os.makedirs(self.plant_rgb, exist_ok=True)
27
+ os.makedirs(self.plot_rgb, exist_ok=True)
28
+ os.makedirs(self.plant_rgb_warp, exist_ok=True)
29
+ os.makedirs(self.plant_mask_warp, exist_ok=True)
30
+ os.makedirs(self.data, exist_ok=True)
31
+
32
+
33
+
34
+ def hex_to_hsv_bounds(hex_color, sat_value, val_value):
35
+ # Convert RGB hex to color
36
+ rgb_color = mcolors.hex2color(hex_color)
37
+ hsv_color = mcolors.rgb_to_hsv(np.array(rgb_color).reshape(1, 1, 3))
38
+
39
+ # Adjust the saturation and value components based on user's input
40
+ hsv_color[0][0][1] = sat_value / 255.0 # Saturation
41
+ hsv_color[0][0][2] = val_value / 255.0 # Value
42
+
43
+ hsv_bound = tuple((hsv_color * np.array([179, 255, 255])).astype(int)[0][0])
44
+
45
+ return hsv_bound
46
+
47
+ def warp_image(img, vertices):
48
+ # Compute distances between the vertices to determine the size of the target square
49
+ distances = [np.linalg.norm(np.array(vertices[i]) - np.array(vertices[i+1])) for i in range(len(vertices)-1)]
50
+ distances.append(np.linalg.norm(np.array(vertices[-1]) - np.array(vertices[0]))) # Add the distance between the last and first point
51
+ max_distance = max(distances)
52
+
53
+ # Define target vertices for the square
54
+ dst_vertices = np.array([
55
+ [max_distance - 1, 0],
56
+ [0, 0],
57
+ [0, max_distance - 1],
58
+ [max_distance - 1, max_distance - 1]
59
+ ], dtype="float32")
60
+
61
+ # Compute the perspective transform matrix using the provided vertices
62
+ matrix = cv2.getPerspectiveTransform(np.array(vertices, dtype="float32"), dst_vertices)
63
+
64
+ # Warp the image to the square
65
+ warped_img = cv2.warpPerspective(img, matrix, (int(max_distance), int(max_distance)))
66
+
67
+ return warped_img
68
+
69
+ def process_image(image_path, flag_lower, flag_upper, plant_lower, plant_upper):
70
+ img = cv2.imread(image_path)
71
+
72
+ # Check if image is valid
73
+ if img is None:
74
+ print(f"Error reading image from path: {image_path}")
75
+ return None, None, None, None, None, None, None, None, None, None
76
+
77
+ hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # Convert image to HSV
78
+
79
+ # Explicitly ensure bounds are integer tuples
80
+ flag_lower = tuple(int(x) for x in flag_lower)
81
+ flag_upper = tuple(int(x) for x in flag_upper)
82
+ plant_lower = tuple(int(x) for x in plant_lower)
83
+ plant_upper = tuple(int(x) for x in plant_upper)
84
+
85
+ flag_mask = cv2.inRange(hsv_img, flag_lower, flag_upper)
86
+ plant_mask = cv2.inRange(hsv_img, plant_lower, plant_upper)
87
+
88
+ # Find contours
89
+ contours, _ = cv2.findContours(flag_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
90
+
91
+ # Sort contours by area and keep only the largest 4
92
+ sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True)[:4]
93
+
94
+ # If there are not 4 largest contours, return
95
+ if len(sorted_contours) != 4:
96
+ return None, None, None, None, None, None, None, None, None, None
97
+
98
+ # Create a new mask with only the largest 4 contours
99
+ largest_4_flag_mask = np.zeros_like(flag_mask)
100
+ cv2.drawContours(largest_4_flag_mask, sorted_contours, -1, (255), thickness=cv2.FILLED)
101
+
102
+ # Compute the centroid for each contour
103
+ centroids = []
104
+ for contour in sorted_contours:
105
+ M = cv2.moments(contour)
106
+ if M["m00"] != 0:
107
+ cx = int(M["m10"] / M["m00"])
108
+ cy = int(M["m01"] / M["m00"])
109
+ else:
110
+ cx, cy = 0, 0
111
+ centroids.append((cx, cy))
112
+
113
+ # Compute the centroid of the centroids
114
+ centroid_x = sum(x for x, y in centroids) / 4
115
+ centroid_y = sum(y for x, y in centroids) / 4
116
+
117
+ # Sort the centroids
118
+ centroids.sort(key=lambda point: (-math.atan2(point[1] - centroid_y, point[0] - centroid_x)) % (2 * np.pi))
119
+
120
+ # Create a polygon mask using the sorted centroids
121
+ poly_mask = np.zeros_like(flag_mask)
122
+ cv2.fillPoly(poly_mask, [np.array(centroids)], 255)
123
+
124
+ # Mask the plant_mask with poly_mask
125
+ mask_plant_plot = cv2.bitwise_and(plant_mask, plant_mask, mask=poly_mask)
126
+
127
+ # Count the number of black pixels inside the quadrilateral
128
+ total_pixels_in_quad = np.prod(poly_mask.shape)
129
+ white_pixels_in_quad = np.sum(poly_mask == 255)
130
+ black_pixels_in_quad = total_pixels_in_quad - white_pixels_in_quad
131
+
132
+ # Extract the RGB pixels from the original image using the mask_plant_plot
133
+ plant_rgb = cv2.bitwise_and(img, img, mask=mask_plant_plot)
134
+
135
+ # Draw the bounding quadrilateral
136
+ plot_rgb = plant_rgb.copy()
137
+ for i in range(4):
138
+ cv2.line(plot_rgb, centroids[i], centroids[(i+1)%4], (0, 0, 255), 3)
139
+
140
+ # Convert the masks to RGB for visualization
141
+ flag_mask_rgb = cv2.cvtColor(flag_mask, cv2.COLOR_GRAY2RGB)
142
+ orange_color = [255, 165, 0] # RGB value for orange
143
+ flag_mask_rgb[np.any(flag_mask_rgb != [0, 0, 0], axis=-1)] = orange_color
144
+
145
+ plant_mask_rgb = cv2.cvtColor(plant_mask, cv2.COLOR_GRAY2RGB)
146
+ mask_plant_plot_rgb = cv2.cvtColor(mask_plant_plot, cv2.COLOR_GRAY2RGB)
147
+ bright_green_color = [0, 255, 0]
148
+ plant_mask_rgb[np.any(plant_mask_rgb != [0, 0, 0], axis=-1)] = bright_green_color
149
+ mask_plant_plot_rgb[np.any(mask_plant_plot_rgb != [0, 0, 0], axis=-1)] = bright_green_color
150
+
151
+ # Warp the images
152
+ plant_rgb_warp = warp_image(plant_rgb, centroids)
153
+ plant_mask_warp = warp_image(mask_plant_plot_rgb, centroids)
154
+
155
+ return flag_mask_rgb, plant_mask_rgb, mask_plant_plot_rgb, plant_rgb, plot_rgb, plant_rgb_warp, plant_mask_warp, plant_mask, mask_plant_plot, black_pixels_in_quad
156
+
157
+ def calculate_coverage(mask_plant_plot, plant_mask_warp, black_pixels_in_quad):
158
+ # Calculate the percentage of white pixels for mask_plant_plot
159
+ white_pixels_plot = np.sum(mask_plant_plot > 0)
160
+ total_pixels_plot = mask_plant_plot.size
161
+ plot_coverage = (white_pixels_plot / black_pixels_in_quad) * 100
162
+
163
+ # Convert plant_mask_warp to grayscale
164
+ plant_mask_warp_gray = cv2.cvtColor(plant_mask_warp, cv2.COLOR_BGR2GRAY)
165
+
166
+ # Calculate the percentage of white pixels for plant_mask_warp
167
+ white_pixels_warp = np.sum(plant_mask_warp_gray > 0)
168
+ total_pixels_warp = plant_mask_warp_gray.size
169
+ warp_coverage = (white_pixels_warp / total_pixels_warp) * 100
170
+
171
+ # Calculate the area in cm^2 of the mask_plant_plot
172
+ # Given that the real-life size of the square is 2 square meters or 20000 cm^2
173
+ plot_area_cm2 = (white_pixels_warp / total_pixels_warp) * 20000
174
+
175
+ return round(plot_coverage,2), round(warp_coverage,2), round(plot_area_cm2,2)
176
+
177
+ def get_color_parameters():
178
+ # Color pickers for hue component
179
+ FL, FL_S, FL_SS = st.columns([2,4,4])
180
+ with FL:
181
+ flag_lower_hex = st.color_picker("Flag Color Lower Bound Hue", "#33211f")
182
+ with FL_S:
183
+ flag_lower_sat = st.slider("Flag Lower Bound Saturation", 0, 255, 120)
184
+ with FL_SS:
185
+ flag_lower_val = st.slider("Flag Lower Bound Value", 0, 255, 150)
186
+
187
+ FU, FU_S, FU_SS = st.columns([2,4,4])
188
+ with FU:
189
+ flag_upper_hex = st.color_picker("Flag Color Upper Bound Hue", "#ff7700")
190
+ with FU_S:
191
+ flag_upper_sat = st.slider("Flag Upper Bound Saturation", 0, 255, 255)
192
+ with FU_SS:
193
+ flag_upper_val = st.slider("Flag Upper Bound Value", 0, 255, 255)
194
+
195
+ PL, PL_S, PL_SS = st.columns([2,4,4])
196
+ with PL:
197
+ plant_lower_hex = st.color_picker("Plant Color Lower Bound Hue", "#504F49")
198
+ with PL_S:
199
+ plant_lower_sat = st.slider("Plant Lower Bound Saturation", 0, 255, 30)
200
+ with PL_SS:
201
+ plant_lower_val = st.slider("Plant Lower Bound Value", 0, 255, 30)
202
+
203
+ PU, PU_S, PU_SS = st.columns([2,4,4])
204
+ with PU:
205
+ plant_upper_hex = st.color_picker("Plant Color Upper Bound Hue", "#00CFFF")
206
+ with PU_S:
207
+ plant_upper_sat = st.slider("Plant Upper Bound Saturation", 0, 255, 255)
208
+ with PU_SS:
209
+ plant_upper_val = st.slider("Plant Upper Bound Value", 0, 255, 255)
210
+
211
+ # Get HSV bounds using the modified function
212
+ flag_lower_bound = hex_to_hsv_bounds(flag_lower_hex, flag_lower_sat, flag_lower_val)
213
+ flag_upper_bound = hex_to_hsv_bounds(flag_upper_hex, flag_upper_sat, flag_upper_val)
214
+ plant_lower_bound = hex_to_hsv_bounds(plant_lower_hex, plant_lower_sat, plant_lower_val)
215
+ plant_upper_bound = hex_to_hsv_bounds(plant_upper_hex, plant_upper_sat, plant_upper_val)
216
+
217
+ return flag_lower_bound, flag_upper_bound, plant_lower_bound, plant_upper_bound
218
+
219
+ def save_img(directory, base_name, mask):
220
+ mask_name = os.path.join(directory, os.path.basename(base_name))
221
+ cv2.imwrite(mask_name, mask)
222
+
223
+ def main():
224
+
225
+ _, R_coverage, R_plot_area_cm2, R_save = st.columns([5,2,2,2])
226
+ img_gallery, img_main, img_seg, img_green, img_warp = st.columns([1,4,2,2,2])
227
+
228
+ dir_input = st.text_input("Input directory for images:", value="D:\Dropbox\GreenSight\demo")
229
+ dir_output = st.text_input("Output directory:", value="D:\Dropbox\GreenSight\demo_out")
230
+
231
+ directory_manager = DirectoryManager(dir_output)
232
+ directory_manager.create_directories()
233
+
234
+ run_name = st.text_input("Run name:", value="test")
235
+ file_name = os.path.join(directory_manager.data, f"{run_name}.csv")
236
+ headers = ['image',"plant_coverage_uncorrected_percen", "plant_coverage_corrected_percent", "plant_area_corrected_cm2"]
237
+ file_exists = os.path.isfile(file_name)
238
+
239
+ if 'input_list' not in st.session_state:
240
+ input_images = [os.path.join(dir_input, fname) for fname in os.listdir(dir_input) if fname.endswith(('.jpg', '.jpeg', '.png'))]
241
+ st.session_state.input_list = input_images
242
+
243
+ if os.path.exists(dir_input):
244
+
245
+ if len(st.session_state.input_list) == 0 or st.session_state.input_list is None:
246
+ st.balloons()
247
+ else:
248
+ with img_gallery:
249
+ selected_img = image_select("Select an image", st.session_state.input_list, use_container_width=False)
250
+ base_name = os.path.basename(selected_img)
251
+
252
+ if selected_img:
253
+
254
+ selected_img_view = Image.open(selected_img)
255
+ with img_main:
256
+ st.image(selected_img_view, caption="Selected Image", use_column_width='auto')
257
+
258
+ flag_lower_bound, flag_upper_bound, plant_lower_bound, plant_upper_bound = get_color_parameters()
259
+
260
+ flag_mask, plant_mask, mask_plant_plot, plant_rgb, plot_rgb, plant_rgb_warp, plant_mask_warp, plant_mask_bi, mask_plant_plot_bi, black_pixels_in_quad = process_image(selected_img, flag_lower_bound, flag_upper_bound, plant_lower_bound, plant_upper_bound)
261
+
262
+ if plant_mask_warp is not None:
263
+ plot_coverage, warp_coverage, plot_area_cm2 = calculate_coverage(mask_plant_plot_bi, plant_mask_warp, black_pixels_in_quad)
264
+
265
+ with R_coverage:
266
+ st.markdown(f"Uncorrected Plant Coverage: {plot_coverage}%")
267
+ with R_plot_area_cm2:
268
+ st.markdown(f"Corrected Plant Coverage: {warp_coverage}%")
269
+ st.markdown(f"Corrected Plant Area: {plot_area_cm2}cm2")
270
+
271
+ # Display masks in galleries
272
+ with img_seg:
273
+ st.image(plant_mask, caption="Plant Mask", use_column_width=True)
274
+ st.image(flag_mask, caption="Flag Mask", use_column_width=True)
275
+ with img_green:
276
+ st.image(mask_plant_plot, caption="Plant Mask Inside Plot", use_column_width=True)
277
+ st.image(plant_rgb, caption="Plant Material", use_column_width=True)
278
+ with img_warp:
279
+ st.image(plot_rgb, caption="Plant Material Inside Plot", use_column_width=True)
280
+ st.image(plant_rgb_warp, caption="Plant Mask Inside Plot Warped to Square", use_column_width=True)
281
+ # st.image(plot_rgb_warp, caption="Flag Mask", use_column_width=True)
282
+ with R_save:
283
+ if st.button('Save'):
284
+ # Save the masks to their respective folders
285
+ save_img(directory_manager.mask_flag, base_name, flag_mask)
286
+ save_img(directory_manager.mask_plant, base_name, plant_mask)
287
+ save_img(directory_manager.mask_plant_plot, base_name, mask_plant_plot)
288
+ save_img(directory_manager.plant_rgb, base_name, plant_rgb)
289
+ save_img(directory_manager.plot_rgb, base_name, plot_rgb)
290
+ save_img(directory_manager.plant_rgb_warp, base_name, plant_rgb_warp)
291
+ save_img(directory_manager.plant_mask_warp, base_name, plant_mask_warp)
292
+
293
+ # Append the data to the CSV file
294
+ with open(file_name, mode='a', newline='') as file:
295
+ writer = csv.writer(file)
296
+
297
+ # If the file doesn't exist, write the headers
298
+ if not file_exists:
299
+ writer.writerow(headers)
300
+
301
+ # Write the data
302
+ writer.writerow([f"{base_name}",f"{plot_coverage}", f"{warp_coverage}", f"{plot_area_cm2}"])
303
+
304
+ # Remove processed image from the list
305
+ st.session_state.input_list.remove(selected_img)
306
+ st.rerun()
307
+ else:
308
+ with R_save:
309
+ if st.button('Save as Failure'):
310
+ # Append the data to the CSV file
311
+ with open(file_name, mode='a', newline='') as file:
312
+ writer = csv.writer(file)
313
+
314
+ # If the file doesn't exist, write the headers
315
+ if not file_exists:
316
+ writer.writerow(headers)
317
+
318
+ # Write the data
319
+ writer.writerow([f"{base_name}",f"NA", f"NA", f"NA"])
320
+
321
+ # Remove processed image from the list
322
+ st.session_state.input_list.remove(selected_img)
323
+ st.rerun()
324
+
325
+ st.set_page_config(layout="wide", page_title='GreenSight')
326
+ st.title("GreenSight")
327
+ main()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ numpy
2
+ matplotlib
3
+ streamlit
4
+ streamlit_image_select
5
+ opencv-python
6
+ Pillow