jeffrey1963 commited on
Commit
d3560c6
Β·
verified Β·
1 Parent(s): a6ce784

Upload 4 files

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. Carson_map.png +3 -0
  3. Sim_Setup_Fcns.py +83 -0
  4. requirements.txt +13 -0
  5. zone_utils.py +432 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Carson_map.png filter=lfs diff=lfs merge=lfs -text
Carson_map.png ADDED

Git LFS Details

  • SHA256: 529fde5e9033c127a09e3c5f547d7941c77774548f2a300d0244b0e6323d1e5f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
Sim_Setup_Fcns.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+
3
+ def load_and_crop_image(path="Carson_map.png", crop_box=(15, 15, 1000, 950)):
4
+ img = Image.open(path).convert("RGB")
5
+ cropped_img = img.crop(crop_box)
6
+ return cropped_img
7
+
8
+
9
+ from sklearn.cluster import KMeans
10
+ import numpy as np
11
+
12
+ def cluster_image(cropped_img, n_clusters=6):
13
+ img_array = np.array(cropped_img)
14
+ pixels = img_array.reshape(-1, 3)
15
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42).fit(pixels)
16
+ labels = kmeans.labels_.reshape(img_array.shape[:2])
17
+ return labels
18
+
19
+
20
+ from collections import Counter
21
+ import numpy as np
22
+
23
+ def build_parcel_map(clustered_img, grid_size=20):
24
+ height, width = clustered_img.shape
25
+ n_rows = height // grid_size
26
+ n_cols = width // grid_size
27
+ parcel_map = np.zeros((n_rows, n_cols), dtype=int)
28
+
29
+ for i in range(n_rows):
30
+ for j in range(n_cols):
31
+ patch = clustered_img[i*grid_size:(i+1)*grid_size, j*grid_size:(j+1)*grid_size].flatten()
32
+ dominant = Counter(patch).most_common(1)[0][0]
33
+ parcel_map[i, j] = dominant
34
+
35
+ return parcel_map, n_rows, n_cols
36
+
37
+
38
+ import matplotlib.pyplot as plt
39
+ import matplotlib.patches as mpatches
40
+ from matplotlib.colors import ListedColormap
41
+
42
+ def plot_parcel_map(parcel_map, cluster_labels, land_colors, title="25Γ—25 Land Parcels by Land Type"):
43
+ cmap = ListedColormap(land_colors)
44
+ plt.figure(figsize=(10, 8))
45
+ plt.imshow(parcel_map, cmap=cmap, origin='upper')
46
+ legend_patches = [mpatches.Patch(color=land_colors[i], label=cluster_labels[i]) for i in cluster_labels]
47
+ plt.legend(handles=legend_patches, bbox_to_anchor=(1.05, 1), loc='upper left', title="Land Type")
48
+ plt.title(title)
49
+ plt.axis('off')
50
+ plt.tight_layout()
51
+ plt.show()
52
+
53
+ def plot_parcel_map_to_file(parcel_map, cluster_labels, land_colors, save_path="clustered_map.png", title="25Γ—25 Land Parcels by Land Type"):
54
+ cmap = ListedColormap(land_colors)
55
+ fig, ax = plt.subplots(figsize=(10, 8))
56
+ cax = ax.imshow(parcel_map, cmap=cmap, origin='upper')
57
+ legend_patches = [mpatches.Patch(color=land_colors[i], label=cluster_labels[i]) for i in cluster_labels]
58
+ ax.legend(handles=legend_patches, bbox_to_anchor=(1.05, 1), loc='upper left', title="Land Type")
59
+ ax.set_title(title)
60
+ ax.axis('off')
61
+ plt.tight_layout()
62
+ plt.savefig(save_path)
63
+ plt.close(fig)
64
+
65
+
66
+ def get_cluster_labels():
67
+ return {
68
+ 0: 'Pasture/Desert',
69
+ 1: 'Productive Grass',
70
+ 2: 'Pasture/Desert',
71
+ 3: 'Riparian Sensitive Zone',
72
+ 4: 'Rocky Area',
73
+ 5: 'Water'
74
+ }
75
+
76
+
77
+ def get_land_colors():
78
+ return ['#dfb867', '#a0ca76', '#dfb867', '#5b8558', '#888888', '#3a75a8']
79
+
80
+
81
+
82
+
83
+
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ huggingface_hub==0.25.2
2
+ gradio
3
+ numpy
4
+ pandas
5
+ gradio==4.14.0
6
+ openai>=1.0.0
7
+ scikit-learn
8
+ matplotlib
9
+ numpy
10
+ pillow
11
+ pydantic==2.10.6
12
+ openpyxl
13
+
zone_utils.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cluster_to_class = {
2
+ 0: "desert",
3
+ 1: "pasture",
4
+ 2: "riaprain",
5
+ 3: "sensitive riparian",
6
+ 4: "wetland",
7
+ 5: "water"
8
+ }
9
+
10
+
11
+ import numpy as np
12
+ from collections import deque
13
+
14
+ def identify_zones(parcel_map, connectivity="queen"):
15
+ """
16
+ Identifies contiguous zones in a 2D parcel map using connected component labeling.
17
+
18
+ Parameters:
19
+ parcel_map (np.ndarray): 2D array where each value is a cluster ID (e.g., land type).
20
+ connectivity (str): "rook" (4-way) or "queen" (8-way) connectivity.
21
+
22
+ Returns:
23
+ zone_map (np.ndarray): Same shape as parcel_map, each zone gets a unique integer ID.
24
+ zone_to_cluster (dict): Maps each zone ID to its underlying cluster ID.
25
+ """
26
+ n_rows, n_cols = parcel_map.shape
27
+ zone_map = -1 * np.ones_like(parcel_map, dtype=int)
28
+ visited = np.zeros_like(parcel_map, dtype=bool)
29
+ zone_id = 0
30
+
31
+ if connectivity == "queen":
32
+ directions = [(-1, -1), (-1, 0), (-1, 1),
33
+ (0, -1), (0, 1),
34
+ (1, -1), (1, 0), (1, 1)]
35
+ else: # "rook"
36
+ directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
37
+
38
+ for i in range(n_rows):
39
+ for j in range(n_cols):
40
+ if visited[i, j]:
41
+ continue
42
+ cluster_id = parcel_map[i, j]
43
+ queue = deque([(i, j)])
44
+ while queue:
45
+ x, y = queue.popleft()
46
+ if visited[x, y] or parcel_map[x, y] != cluster_id:
47
+ continue
48
+ visited[x, y] = True
49
+ zone_map[x, y] = zone_id
50
+ for dx, dy in directions:
51
+ nx, ny = x + dx, y + dy
52
+ if 0 <= nx < n_rows and 0 <= ny < n_cols and not visited[nx, ny]:
53
+ if parcel_map[nx, ny] == cluster_id:
54
+ queue.append((nx, ny))
55
+ zone_id += 1
56
+
57
+ # Optional: Map zone_id β†’ cluster_id
58
+ zone_to_cluster = {}
59
+ for zid in range(zone_id):
60
+ indices = np.argwhere(zone_map == zid)
61
+ if len(indices) > 0:
62
+ i, j = indices[0]
63
+ zone_to_cluster[zid] = parcel_map[i, j]
64
+
65
+ return zone_map, zone_to_cluster
66
+
67
+ import matplotlib.pyplot as plt
68
+ import numpy as np
69
+
70
+ def plot_labeled_zones(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
71
+ """
72
+ Plots a zone map with human-readable labels and cluster-based custom colors.
73
+
74
+ Colors (by cluster ID):
75
+ 0: tan
76
+ 1: green
77
+ 2: rose
78
+ 3: red
79
+ 4: purple
80
+ 5: blue
81
+ """
82
+ # Custom color map for cluster IDs (NOT zone IDs)
83
+ cluster_colors = {
84
+ 0: "#D2B48C", # tan
85
+ 1: "#228B22", # green
86
+ 2: "#FF66CC", # rose
87
+ 3: "#FF0000", # red
88
+ 4: "#800080", # purple
89
+ 5: "#1E90FF", # blue
90
+ }
91
+
92
+ n_rows, n_cols = zone_map.shape
93
+ rgb_map = np.zeros((n_rows, n_cols, 3))
94
+
95
+ # Map each parcel to its cluster color
96
+ for i in range(n_rows):
97
+ for j in range(n_cols):
98
+ zone_id = zone_map[i, j]
99
+ cluster_id = zone_to_cluster.get(zone_id, 0)
100
+ hex_color = cluster_colors.get(cluster_id, "#AAAAAA") # fallback = gray
101
+ rgb = tuple(int(hex_color.lstrip('#')[k:k+2], 16)/255 for k in (0, 2, 4))
102
+ rgb_map[i, j] = rgb
103
+
104
+ fig, ax = plt.subplots(figsize=(8, 6))
105
+ ax.imshow(rgb_map)
106
+
107
+ for zone_id, label in zone_labels.items():
108
+ positions = np.argwhere(zone_map == zone_id)
109
+ if len(positions) == 0:
110
+ continue
111
+ center_i, center_j = positions.mean(axis=0)
112
+ cluster = zone_to_cluster.get(zone_id, "?")
113
+ label_text = f"{label} ({cluster})"
114
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
115
+ fontsize=9, fontweight="bold",
116
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
117
+
118
+ ax.set_title("Labeled Grazing & Riparian Zones (Custom Colors)")
119
+ ax.axis('off')
120
+ plt.tight_layout()
121
+ plt.savefig(save_path)
122
+ plt.close(fig)
123
+
124
+
125
+ import matplotlib.pyplot as plt
126
+ import numpy as np
127
+
128
+ def plot_labeled_zonesold3am(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
129
+ """
130
+ Plots a zone map with fixed colors based on cluster class (e.g., pasture = green, desert = tan),
131
+ and appends the cluster ID to each label (e.g., "A (1)").
132
+
133
+ Parameters:
134
+ zone_map (np.ndarray): 2D array of zone IDs (integers).
135
+ zone_labels (dict): Mapping from zone_id to human-readable label (str).
136
+ zone_to_cluster (dict): Mapping from zone_id to cluster/group ID (int).
137
+ save_path (str): File path to save the plotted image.
138
+ """
139
+
140
+ # === Fixed colors by cluster ID ===
141
+ cluster_color_map = {
142
+ 0: "#d2b48c", # Desert – tan
143
+ 1: "#228B22", # Pasture – green
144
+ 2: "#87CEEB", # Water – light blue
145
+ 3: "#FF69B4", # Riparian – pink
146
+ 4: "#8B0000", # Sensitive riparian – dark red
147
+ 5: "#9370DB", # Town – purple
148
+ }
149
+
150
+ # === Build a color image based on cluster color ===
151
+ rows, cols = zone_map.shape
152
+ color_image = np.zeros((rows, cols, 3))
153
+
154
+ for i in range(rows):
155
+ for j in range(cols):
156
+ zone_id = zone_map[i, j]
157
+ cluster_id = zone_to_cluster.get(zone_id, 0)
158
+ hex_color = cluster_color_map.get(cluster_id, "#888888") # default gray
159
+ rgb = tuple(int(hex_color.lstrip("#")[k:k+2], 16)/255.0 for k in (0, 2, 4))
160
+ color_image[i, j] = rgb
161
+
162
+ # === Plot map ===
163
+ fig, ax = plt.subplots(figsize=(8, 6))
164
+ ax.imshow(color_image)
165
+
166
+ for zone_id in sorted(np.unique(zone_map)):
167
+ if zone_id not in zone_labels:
168
+ continue
169
+ label = zone_labels[zone_id]
170
+ group = zone_to_cluster.get(zone_id, "?")
171
+ label_text = f"{label} ({group})"
172
+ positions = np.argwhere(zone_map == zone_id)
173
+ if len(positions) == 0:
174
+ continue
175
+ center_i, center_j = positions.mean(axis=0)
176
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
177
+ fontsize=9, fontweight="bold",
178
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
179
+
180
+ ax.set_title("Labeled Grazing & Riparian Zones")
181
+ ax.axis('off')
182
+ plt.tight_layout()
183
+ plt.savefig(save_path)
184
+ plt.close(fig)
185
+
186
+
187
+
188
+ import matplotlib.pyplot as plt
189
+ import numpy as np
190
+
191
+ def plot_labeled_zonesold(zone_map, zone_labels, zone_to_cluster, save_path="labeled_zones.png"):
192
+ """
193
+ Plots a zone map with human-readable labels (e.g., "A", "B", "Riparian A"),
194
+ and appends the cluster ID to each label (e.g., "A (0)").
195
+
196
+ Parameters:
197
+ zone_map (np.ndarray): 2D array of zone IDs (integers).
198
+ zone_labels (dict): Mapping from zone_id to human-readable label (str).
199
+ zone_to_cluster (dict): Mapping from zone_id to cluster/group ID (int).
200
+ save_path (str): File path to save the plotted image.
201
+ """
202
+ unique_ids = sorted(np.unique(zone_map))
203
+ color_map = plt.get_cmap('tab20', len(unique_ids))
204
+
205
+ fig, ax = plt.subplots(figsize=(8, 6))
206
+ cax = ax.imshow(zone_map, cmap=color_map, vmin=0, vmax=len(unique_ids) - 1)
207
+
208
+ # Add label with group ID
209
+ for zone_id in unique_ids:
210
+ if zone_id not in zone_labels:
211
+ continue
212
+ label = zone_labels[zone_id]
213
+ group = zone_to_cluster.get(zone_id, "?")
214
+ label_text = f"{label} ({group})" # append group ID
215
+
216
+ positions = np.argwhere(zone_map == zone_id)
217
+ if len(positions) == 0:
218
+ continue
219
+ center_i, center_j = positions.mean(axis=0)
220
+ ax.text(center_j, center_i, label_text, color="white", ha="center", va="center",
221
+ fontsize=9, fontweight="bold",
222
+ bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
223
+
224
+ ax.set_title("Labeled Grazing & Riparian Zones")
225
+ ax.axis('off')
226
+ plt.tight_layout()
227
+ plt.savefig(save_path)
228
+ plt.close(fig)
229
+
230
+
231
+ def assign_zone_labels(zone_to_cluster):
232
+ """
233
+ Assigns human-readable labels to each zone based on its cluster type.
234
+ Riparian zones are labeled like 'Riparian A', 'Riparian B', etc.
235
+ Other zones are labeled 'A', 'B', etc. by group type.
236
+
237
+ Returns:
238
+ zone_labels (dict): zone_id β†’ label string
239
+ """
240
+ label_counts = {} # track how many zones per type
241
+ zone_labels = {}
242
+
243
+ for zone_id, cluster_id in zone_to_cluster.items():
244
+ land_class = cluster_to_class.get(cluster_id, f"cluster{cluster_id}")
245
+ count = label_counts.get(land_class, 0)
246
+ suffix = chr(65 + count) # A, B, C...
247
+ if "riparian" in land_class:
248
+ label = f"{land_class.title()} {suffix}"
249
+ else:
250
+ label = f"{suffix}"
251
+ zone_labels[zone_id] = label
252
+ label_counts[land_class] = count + 1
253
+
254
+ return zone_labels
255
+
256
+
257
+ def assign_zone_labels_old(zone_to_cluster, cluster_to_class):
258
+ """
259
+ Creates a dictionary mapping zone_id β†’ human-readable labels (e.g., "A", "Riparian B").
260
+
261
+ Parameters:
262
+ zone_to_cluster (dict): zone_id β†’ cluster ID
263
+ cluster_to_class (dict): cluster ID β†’ class name (e.g., "pasture", "riparian")
264
+
265
+ Returns:
266
+ dict: zone_id β†’ human-friendly label
267
+ """
268
+ label_counts = {} # Track how many zones per class
269
+ zone_labels = {}
270
+
271
+ for zone_id, cluster_id in zone_to_cluster.items():
272
+ zone_class = cluster_to_class.get(cluster_id, "Unknown")
273
+ count = label_counts.get(zone_class, 0)
274
+
275
+ # Generate a label like "Riparian A", "Pasture B", etc.
276
+ letter = chr(ord('A') + count)
277
+ label = f"{zone_class.capitalize()} {letter}" if zone_class != "Unknown" else f"Zone {zone_id}"
278
+
279
+ zone_labels[zone_id] = label
280
+ label_counts[zone_class] = count + 1
281
+
282
+ return zone_labels
283
+
284
+ import numpy as np
285
+ import pandas as pd
286
+
287
+ def save_zone_info_to_excel(parcel_map, zone_map, zone_labels, zone_to_cluster, cluster_to_class, save_path="zone_details.xlsx"):
288
+ """
289
+ Save detailed zone information to an Excel file.
290
+
291
+ Parameters:
292
+ parcel_map (np.ndarray): 2D array with cluster IDs.
293
+ zone_map (np.ndarray): 2D array with zone IDs.
294
+ zone_labels (dict): zone_id β†’ human-friendly label (e.g., "A", "Riparian B")
295
+ zone_to_cluster (dict): zone_id β†’ cluster ID
296
+ cluster_to_class (dict): cluster ID β†’ land class string (e.g., "pasture", "riparian")
297
+ save_path (str): File path to save Excel.
298
+ """
299
+ rows, cols = parcel_map.shape
300
+ data = []
301
+
302
+ for i in range(rows):
303
+ for j in range(cols):
304
+ zone_id = zone_map[i, j]
305
+ cluster_id = parcel_map[i, j]
306
+ label = zone_labels.get(zone_id, "Unknown")
307
+ zone_class = cluster_to_class.get(cluster_id, "Unknown")
308
+ data.append({
309
+ "Row": i,
310
+ "Col": j,
311
+ "Zone ID": zone_id,
312
+ "Zone Label": label,
313
+ "Cluster ID": cluster_id,
314
+ "Zone Class": zone_class
315
+ })
316
+
317
+ df = pd.DataFrame(data)
318
+ df.to_excel(save_path, index=False)
319
+ print(f"βœ… Zone info saved to {save_path}")
320
+
321
+
322
+ def override_zone_id_and_label(zone_map, zone_labels, from_id, from_label, to_id, to_label):
323
+ """
324
+ Reassigns all parcels with a given zone ID and label to a new zone ID and label.
325
+
326
+ Parameters:
327
+ zone_map (np.ndarray): 2D array with zone IDs.
328
+ zone_labels (dict): zone_id β†’ label.
329
+ from_id (int): Zone ID to search for.
330
+ from_label (str): Must match the current label for that zone.
331
+ to_id (int): Zone ID to assign.
332
+ to_label (str): New label for the new zone ID.
333
+
334
+ Returns:
335
+ zone_map (np.ndarray): Updated zone map.
336
+ zone_labels (dict): Updated labels.
337
+ """
338
+ # Step 1: Confirm the label matches
339
+ if zone_labels.get(from_id) != from_label:
340
+ print(f"❌ Mismatch: Zone {from_id} label is '{zone_labels.get(from_id)}', not '{from_label}'")
341
+ return zone_map, zone_labels
342
+
343
+ # Step 2: Loop through all parcels
344
+ for i in range(zone_map.shape[0]):
345
+ for j in range(zone_map.shape[1]):
346
+ if zone_map[i, j] == from_id:
347
+ zone_map[i, j] = to_id # Override ID
348
+
349
+ # Step 3: Update label dictionary
350
+ zone_labels[to_id] = to_label
351
+ if from_id not in zone_map:
352
+ zone_labels.pop(from_id, None)
353
+
354
+ print(f"βœ… Overrode zone {from_id} ('{from_label}') β†’ zone {to_id} ('{to_label}')")
355
+ return zone_map, zone_labels
356
+
357
+
358
+
359
+ def remap_zone_id_and_label(zone_map, zone_labels, old_zone_id, old_label, new_zone_id, new_label):
360
+ """
361
+ For all parcels where zone_id == old_zone_id and label == old_label:
362
+ β†’ Change zone_id to new_zone_id and label to new_label.
363
+
364
+ Args:
365
+ zone_map (np.ndarray): 2D map of zone IDs.
366
+ zone_labels (dict): zone_id β†’ label.
367
+ old_zone_id (int)
368
+ old_label (str)
369
+ new_zone_id (int)
370
+ new_label (str)
371
+
372
+ Returns:
373
+ zone_map, zone_labels
374
+ """
375
+ # Only proceed if the label for old_zone_id matches
376
+ if zone_labels.get(old_zone_id) != old_label:
377
+ print(f"❌ Skipping: Label mismatch for zone {old_zone_id}")
378
+ return zone_map, zone_labels
379
+
380
+ # Go through every (i,j) and remap matching zones
381
+ rows, cols = zone_map.shape
382
+ for i in range(rows):
383
+ for j in range(cols):
384
+ if zone_map[i, j] == old_zone_id:
385
+ zone_map[i, j] = new_zone_id
386
+
387
+ # Update the label dictionary
388
+ zone_labels[new_zone_id] = new_label
389
+ if old_zone_id in zone_labels:
390
+ del zone_labels[old_zone_id]
391
+
392
+ print(f"βœ… Reassigned zone {old_zone_id} ('{old_label}') β†’ {new_zone_id} ('{new_label}')")
393
+ return zone_map, zone_labels
394
+
395
+
396
+ import matplotlib.pyplot as plt
397
+ import numpy as np
398
+
399
+ def plot_labeled_zones_old(zone_map, zone_labels, save_path="labeled_zones.png"):
400
+ """
401
+ Plots a zone map with human-readable labels (e.g., "A", "B", "Riparian A").
402
+
403
+ Parameters:
404
+ zone_map (np.ndarray): 2D array of integers where each unique int is a zone ID.
405
+ zone_labels (dict): Mapping from zone_id (int) to human label (str).
406
+ save_path (str): File path to save the plotted image.
407
+ """
408
+ unique_ids = sorted(np.unique(zone_map))
409
+ color_map = plt.get_cmap('tab20', len(unique_ids))
410
+
411
+ fig, ax = plt.subplots(figsize=(8, 6))
412
+ cax = ax.imshow(zone_map, cmap=color_map, vmin=0, vmax=len(unique_ids) - 1)
413
+
414
+ # Add human-readable labels at zone centers
415
+ for zone_id in unique_ids:
416
+ if zone_id not in zone_labels:
417
+ continue
418
+ label = zone_labels[zone_id]
419
+ positions = np.argwhere(zone_map == zone_id)
420
+ if len(positions) == 0:
421
+ continue
422
+ center_i, center_j = positions.mean(axis=0)
423
+ ax.text(center_j, center_i, label, color="white", ha="center", va="center",
424
+ fontsize=9, fontweight="bold", bbox=dict(facecolor="black", alpha=0.5, boxstyle="round,pad=0.3"))
425
+
426
+ ax.set_title("Labeled Grazing & Riparian Zones")
427
+ ax.axis('off')
428
+ plt.tight_layout()
429
+ plt.savefig(save_path)
430
+ plt.close(fig)
431
+
432
+