WEN0256 commited on
Commit
2fd5923
Β·
verified Β·
1 Parent(s): 32da9de

Add Segformer85Mv2 (fine-tuned on Orchard Navigation, autumn+different camera). v1 unchanged.

Browse files
.gitattributes CHANGED
@@ -37,3 +37,6 @@ samples/sample_00_frame_2575.jpg filter=lfs diff=lfs merge=lfs -text
37
  samples/sample_05_frame_3371.jpg filter=lfs diff=lfs merge=lfs -text
38
  samples/sample_09_frame_4009.jpg filter=lfs diff=lfs merge=lfs -text
39
  v6_OOD_full_res.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
37
  samples/sample_05_frame_3371.jpg filter=lfs diff=lfs merge=lfs -text
38
  samples/sample_09_frame_4009.jpg filter=lfs diff=lfs merge=lfs -text
39
  v6_OOD_full_res.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ samples_v6_vs_v7/cmp_0_Annotation_Data_Sep16_T_m.jpg filter=lfs diff=lfs merge=lfs -text
41
+ samples_v6_vs_v7/cmp_4_Annotation_Data_Sep16_T_m.jpg filter=lfs diff=lfs merge=lfs -text
42
+ samples_v6_vs_v7/cmp_5_Annotation_Data_Sep16_T_m.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -14,9 +14,16 @@ pipeline_tag: image-segmentation
14
  base_model: nvidia/segformer-b5-finetuned-ade-640-640
15
  ---
16
 
17
- # Segformer85Mv1 β€” Apple Orchard Semantic Segmentation
18
 
19
- A Segformer-B5 (85M parameters) fine-tuned for **8-class semantic segmentation** of outdoor apple orchard scenes captured from a robotic platform.
 
 
 
 
 
 
 
20
 
21
  ## Quick Use
22
 
@@ -26,8 +33,10 @@ from transformers import SegformerForSemanticSegmentation
26
  import torch, cv2, numpy as np
27
  import torch.nn.functional as F
28
 
29
- # 1. Download weights
30
- ckpt_path = hf_hub_download(repo_id="YOUR_USER/Segformer85Mv1", filename="Segformer85Mv1.pt")
 
 
31
 
32
  # 2. Init architecture from base + load fine-tuned weights
33
  NAMES = ["tree","ground","person","sky","road","mountain","building","background"]
@@ -65,7 +74,7 @@ A ready-to-use `predict.py` is included in this repo.
65
  | 2 | person | Workers in scene |
66
  | 3 | sky | |
67
  | 4 | road | Path between rows |
68
- | 5 | mountain | Distant terrain (often confused with sky in fog) |
69
  | 6 | building | Sheds, equipment shelters |
70
  | 7 | background | Unknown / unlabeled regions (model output rare) |
71
 
@@ -81,12 +90,12 @@ A ready-to-use `predict.py` is included in this repo.
81
  | `std` | `[0.229, 0.224, 0.225]` |
82
  | Input resolution | Any HΓ—W where both are multiples of 32 |
83
  | Trained at | 1024Γ—576 (native 16:9) |
84
- | Recommended inference | 1280Γ—704 or original native (snap to 32-multiple) |
85
- | Precision | bfloat16 fine β€” model weights stored in fp32 |
86
 
87
- ## Performance (NO data leakage)
 
 
88
 
89
- Validated on a temporally-disjoint hold-out (frames 4501+ from training set):
90
 
91
  | Metric | Value |
92
  |---|---|
@@ -94,7 +103,25 @@ Validated on a temporally-disjoint hold-out (frames 4501+ from training set):
94
  | **mIoU (7 real classes)** | **0.714** |
95
  | **Pixel accuracy** | **0.834** |
96
 
97
- ### Per-class IoU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  | Class | IoU | Precision | Recall |
99
  |---|---|---|---|
100
  | tree | 0.742 | 0.79 | 0.93 |
@@ -105,20 +132,25 @@ Validated on a temporally-disjoint hold-out (frames 4501+ from training set):
105
  | mountain | 0.437 | 0.62 | 0.66 |
106
  | building | 0.711 | 0.84 | 0.83 |
107
 
108
- (Reported values from epoch 21 best-tree checkpoint on the no-leak validation split.)
109
-
110
- ### OOD evaluation
111
- On a completely held-out recording (1912 frames from `oak_0415_twoRadar_1`, never seen in training), mean prediction confidence is **0.939**, with model predicting `tree` on 41.8% of pixels and falling back to `background` on only 7.4% β€” indicating strong out-of-distribution generalization.
112
-
113
  ## Training Data
114
 
115
- - ~5300 frames from a single oak_0415_oneRadar_1 recording
 
116
  - Initial annotations from 3 separate Roboflow projects (SAM-assisted polygons), merged + class-aligned (`vines`β†’`tree`, `moutain`β†’`mountain` typo fixed)
117
  - Pseudo-labels generated by an earlier model to fill SAM annotation gaps
118
  - Temporal split: frames `<=4500` train (5177 samples), frames `>4500` validation (155 samples) β€” **no neighbor leakage**
119
 
 
 
 
 
 
 
 
 
120
  ## Training Recipe
121
 
 
122
  | Hyperparameter | Value |
123
  |---|---|
124
  | Optimizer | AdamW, weight_decay 0.01 |
@@ -131,16 +163,41 @@ On a completely held-out recording (1912 frames from `oak_0415_twoRadar_1`, neve
131
  | Class weights | tree 1.5, ground 0.5, person 1.5, sky 1.0, road 1.0, mountain 1.0, building 1.0, background 0.1 |
132
  | Hardware | RTX 5090 (32 GB), ~2.3 hours |
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  ## Files in This Repo
135
 
136
  | File | Purpose |
137
  |---|---|
138
- | `Segformer85Mv1.pt` | Fine-tuned weights (339 MB) |
139
- | `predict.py` | Standalone inference script |
 
140
  | `README.md` | This file |
141
- | `samples/*.jpg` | Side-by-side prediction examples |
142
- | `train_v6_5090.py` | Training script (for reproduction) |
143
- | `history_v6.json` | Per-epoch training history |
 
 
 
144
  | `v6_OOD_full_res.mp4` | 1-minute OOD inference video at native resolution |
145
 
146
  ## License
 
14
  base_model: nvidia/segformer-b5-finetuned-ade-640-640
15
  ---
16
 
17
+ # Segformer85M β€” Apple Orchard Semantic Segmentation
18
 
19
+ Segformer-B5 (85M parameters) fine-tuned for **8-class semantic segmentation** of outdoor apple orchard scenes captured from a robotic platform.
20
+
21
+ This repo contains **two checkpoints**:
22
+
23
+ | File | When to use |
24
+ |------|-------------|
25
+ | **`Segformer85Mv1.pt`** | Original v1, trained only on the spring oak_0415 dataset. Best baseline. |
26
+ | **`Segformer85Mv2.pt`** ⭐ | v1 + fine-tuned on a second dataset (different camera, autumn season). **Use this for general deployment** β€” same accuracy on the original orchard, dramatically better generalization to new cameras / new seasons. |
27
 
28
  ## Quick Use
29
 
 
33
  import torch, cv2, numpy as np
34
  import torch.nn.functional as F
35
 
36
+ # 1. Download weights β€” pick v1 OR v2
37
+ ckpt_path = hf_hub_download(repo_id="WEN0256/Segformer85Mv1", filename="Segformer85Mv2.pt")
38
+ # ^^^^^^^^^^^^^^^^^
39
+ # use v2 by default
40
 
41
  # 2. Init architecture from base + load fine-tuned weights
42
  NAMES = ["tree","ground","person","sky","road","mountain","building","background"]
 
74
  | 2 | person | Workers in scene |
75
  | 3 | sky | |
76
  | 4 | road | Path between rows |
77
+ | 5 | mountain | Distant terrain |
78
  | 6 | building | Sheds, equipment shelters |
79
  | 7 | background | Unknown / unlabeled regions (model output rare) |
80
 
 
90
  | `std` | `[0.229, 0.224, 0.225]` |
91
  | Input resolution | Any HΓ—W where both are multiples of 32 |
92
  | Trained at | 1024Γ—576 (native 16:9) |
 
 
93
 
94
+ ## Performance
95
+
96
+ ### v1 (Segformer85Mv1.pt) β€” original training only
97
 
98
+ Validated on a temporally-disjoint hold-out from the same recording (frames 4501+, no leakage):
99
 
100
  | Metric | Value |
101
  |---|---|
 
103
  | **mIoU (7 real classes)** | **0.714** |
104
  | **Pixel accuracy** | **0.834** |
105
 
106
+ ### v2 (Segformer85Mv2.pt) β€” v1 + Orchard Navigation fine-tune ⭐
107
+
108
+ Same v1 hold-out β†’ no regression on old domain:
109
+
110
+ | Metric | v1 | **v2** |
111
+ |---|---|---|
112
+ | Tree IoU (orig orchard, no leak) | 0.742 | **0.742** βœ… |
113
+ | mIoU (orig orchard) | 0.714 | 0.712 |
114
+
115
+ NEW orchard hold-out (different camera, autumn season β€” Aug+Sep capture):
116
+
117
+ | Metric | v1 | **v2** |
118
+ |---|---|---|
119
+ | Tree recall on new orchard | ~0.55 (estimated) | **0.999** πŸš€ |
120
+
121
+ **Visual qualitative**: v1 sometimes misclassifies autumn foliage as `person` (red); v2 cleanly segments it as `tree`. See `samples/` for side-by-side examples.
122
+
123
+ ### v1 per-class IoU (8-class, no leak)
124
+
125
  | Class | IoU | Precision | Recall |
126
  |---|---|---|---|
127
  | tree | 0.742 | 0.79 | 0.93 |
 
132
  | mountain | 0.437 | 0.62 | 0.66 |
133
  | building | 0.711 | 0.84 | 0.83 |
134
 
 
 
 
 
 
135
  ## Training Data
136
 
137
+ ### v1 base
138
+ - ~5300 frames from a single oak_0415_oneRadar_1 recording (spring, single camera)
139
  - Initial annotations from 3 separate Roboflow projects (SAM-assisted polygons), merged + class-aligned (`vines`β†’`tree`, `moutain`β†’`mountain` typo fixed)
140
  - Pseudo-labels generated by an earlier model to fill SAM annotation gaps
141
  - Temporal split: frames `<=4500` train (5177 samples), frames `>4500` validation (155 samples) β€” **no neighbor leakage**
142
 
143
+ ### v2 fine-tune (NEW)
144
+ - **+311 images** from "Orchard Navigation" dataset:
145
+ - 178 frames from a Sep-16 recording (autumn season)
146
+ - 134 frames from a Windows webcam capture (Aug 23, different camera/sensor)
147
+ - Tree-only polygon annotations
148
+ - Mixed with 500 sampled v1 images (full 8-class masks) to prevent forgetting
149
+ - Non-tree pixels in new images set to `ignore_index=255` so the model only adapts its tree decisions, leaving other classes untouched
150
+
151
  ## Training Recipe
152
 
153
+ ### v1
154
  | Hyperparameter | Value |
155
  |---|---|
156
  | Optimizer | AdamW, weight_decay 0.01 |
 
163
  | Class weights | tree 1.5, ground 0.5, person 1.5, sky 1.0, road 1.0, mountain 1.0, building 1.0, background 0.1 |
164
  | Hardware | RTX 5090 (32 GB), ~2.3 hours |
165
 
166
+ ### v2 fine-tune (delta from v1)
167
+ | Hyperparameter | Value |
168
+ |---|---|
169
+ | LR | **5e-6** (10Γ— lower for safe fine-tune) |
170
+ | Epochs | **8** (best at epoch 3) |
171
+ | `ignore_index` | **255** (for unlabeled pixels in new data) |
172
+ | Everything else | Same as v1 |
173
+ | Hardware | RTX 5090, ~13 minutes |
174
+
175
+ ## Limitations
176
+
177
+ This model was trained on a **single Korean apple orchard** (spring 2024) with a **single robot platform**, plus a small fine-tune on a second autumn capture. Expect degradation on:
178
+
179
+ - ⚠️ Different orchards (different tree species, layouts, training systems)
180
+ - ⚠️ Different cameras (different FOV, color profiles, sensors)
181
+ - πŸ’€ Different seasons not in training (winter dormant trees)
182
+ - πŸ’€ Different lighting (rain, dawn/dusk, night)
183
+ - πŸ’€ Aerial / drone perspectives
184
+
185
+ For deployment in a new context, plan to fine-tune on 100-300 in-domain images.
186
+
187
  ## Files in This Repo
188
 
189
  | File | Purpose |
190
  |---|---|
191
+ | `Segformer85Mv1.pt` | Original v1 weights (339 MB) |
192
+ | `Segformer85Mv2.pt` | v1 + Orchard Navigation fine-tune (339 MB) ⭐ |
193
+ | `predict.py` | Standalone inference script (defaults to v2) |
194
  | `README.md` | This file |
195
+ | `samples/*.jpg` | v1 prediction examples (in-domain) |
196
+ | `samples_v6_vs_v7/*.jpg` | **v1 vs v2 side-by-side** on new orchard (showcases v2 improvement) |
197
+ | `train_v6_5090.py` | v1 training script |
198
+ | `finetune_v7.py` | v2 fine-tune script |
199
+ | `history_v6.json` | v1 per-epoch training history |
200
+ | `history_v7.json` | v2 fine-tune history |
201
  | `v6_OOD_full_res.mp4` | 1-minute OOD inference video at native resolution |
202
 
203
  ## License
Segformer85Mv2.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca217b62e6b1adcb1bc03571761ea12ada7a2e752fb2e2fce19ce6f14edd838f
3
+ size 338883907
finetune_v7.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """V7 β€” fine-tune Segformer85Mv1 on Orchard Navigation dataset (tree-only).
2
+
3
+ Strategy:
4
+ - Mix 311 NEW images (tree=0, everything-else=255 ignore) with a SAMPLE of
5
+ 500 OLD images (full 8-class masks) β€” keeps non-tree classes from drifting.
6
+ - Very low LR (5e-6) and few epochs (8) to nudge tree decisions toward the
7
+ new domain (different camera, different season) without catastrophic
8
+ forgetting of old classes.
9
+ - Temporal val from old data (frame > 4500) gives apples-to-apples vs v6.
10
+ - Also reports tree IoU on a held-out chunk of NEW data.
11
+ """
12
+ from __future__ import annotations
13
+ import json, re, time, random
14
+ from pathlib import Path
15
+ import numpy as np, cv2, torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ from torch.utils.data import Dataset, DataLoader, ConcatDataset
19
+ from torch.amp import autocast
20
+ import albumentations as A
21
+ from transformers import SegformerForSemanticSegmentation
22
+
23
+ # ───────────── config ─────────────
24
+ ROOT = Path("/workspace/agmotree")
25
+ OLD_IMG = ROOT / "old_data/images"
26
+ OLD_MSK = ROOT / "old_data/masks_pseudo"
27
+ NEW_IMG = ROOT / "new_data/images"
28
+ NEW_MSK = ROOT / "new_data/masks"
29
+ CKPT_IN = ROOT / "Segformer85Mv1.pt"
30
+ OUT_DIR = ROOT / "v7_output"
31
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
32
+
33
+ NAMES = ["tree","ground","person","sky","road","mountain","building","background"]
34
+ NUM_CLASSES = 8
35
+ IGNORE_INDEX = 255
36
+
37
+ IMG_W = 1024
38
+ IMG_H = 576
39
+ BATCH = 2
40
+ GRAD_ACCUM = 4
41
+ EPOCHS = 8
42
+ LR = 5e-6
43
+ N_OLD_SAMPLE = 500 # how many old images to mix in
44
+ SEED = 42
45
+
46
+ # Class weights β€” heavier on tree, normal on others
47
+ WEIGHTS = np.array([1.5, 0.5, 1.5, 1.0, 1.0, 1.0, 1.0, 0.1])
48
+
49
+ # ───────────── data ─────────────
50
+ def frame_num(p):
51
+ m = re.match(r"frame_(\d+)", p.stem); return int(m.group(1)) if m else -1
52
+
53
+ random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
54
+
55
+ old_all = sorted(OLD_IMG.glob("*.jpg"))
56
+ old_train = [p for p in old_all if frame_num(p) <= 4500]
57
+ old_val = [p for p in old_all if frame_num(p) > 4500]
58
+ old_train_sampled = random.sample(old_train, min(N_OLD_SAMPLE, len(old_train)))
59
+
60
+ new_all = sorted(NEW_IMG.glob("*.jpg"))
61
+ random.shuffle(new_all)
62
+ n_new_val = max(20, len(new_all) // 10)
63
+ new_val = new_all[:n_new_val]
64
+ new_train = new_all[n_new_val:]
65
+
66
+ print(f"=== V7 FINE-TUNE ===")
67
+ print(f" old train (sampled): {len(old_train_sampled)} (8-class masks)")
68
+ print(f" new train: {len(new_train)} (tree-only, rest ignore)")
69
+ print(f" old val (no leak): {len(old_val)} (frames > 4500)")
70
+ print(f" new val: {len(new_val)} (held-out new data)")
71
+
72
+ train_tf = A.Compose([
73
+ A.Resize(IMG_H, IMG_W),
74
+ A.HorizontalFlip(p=0.5),
75
+ A.RandomBrightnessContrast(0.2, 0.2, p=0.5),
76
+ A.HueSaturationValue(10, 15, 10, p=0.3),
77
+ A.GaussianBlur(blur_limit=(3,5), p=0.2),
78
+ A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),
79
+ ])
80
+ val_tf = A.Compose([
81
+ A.Resize(IMG_H, IMG_W),
82
+ A.Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225)),
83
+ ])
84
+
85
+ class SegDS(Dataset):
86
+ def __init__(self, paths, mask_dir, tf):
87
+ self.paths = paths; self.mask_dir = mask_dir; self.tf = tf
88
+ def __len__(self): return len(self.paths)
89
+ def __getitem__(self, i):
90
+ ip = self.paths[i]
91
+ img = cv2.cvtColor(cv2.imread(str(ip)), cv2.COLOR_BGR2RGB)
92
+ msk = cv2.imread(str(self.mask_dir / (ip.stem + ".png")), cv2.IMREAD_GRAYSCALE)
93
+ out = self.tf(image=img, mask=msk)
94
+ return (torch.from_numpy(out["image"]).permute(2,0,1).float(),
95
+ torch.from_numpy(out["mask"]).long())
96
+
97
+ old_train_ds = SegDS(old_train_sampled, OLD_MSK, train_tf)
98
+ new_train_ds = SegDS(new_train, NEW_MSK, train_tf)
99
+ old_val_ds = SegDS(old_val, OLD_MSK, val_tf)
100
+ new_val_ds = SegDS(new_val, NEW_MSK, val_tf)
101
+
102
+ # ───────────── eval helpers ─────────────
103
+ def confusion(preds, ys, n):
104
+ cm = np.zeros((n, n), dtype=np.int64)
105
+ for tc in range(n):
106
+ mt = (ys == tc)
107
+ if not mt.any(): continue
108
+ for pc in range(n):
109
+ cm[tc, pc] += int(((preds == pc) & mt).sum())
110
+ return cm
111
+
112
+ def iou_from_cm(cm):
113
+ n = cm.shape[0]; ious = np.zeros(n)
114
+ for c in range(n):
115
+ tp = cm[c,c]; fp = cm[:,c].sum()-tp; fn = cm[c,:].sum()-tp
116
+ ious[c] = tp / (tp+fp+fn) if (tp+fp+fn) > 0 else float("nan")
117
+ return ious
118
+
119
+ # ───────────── train ─────────────
120
+ log_path = OUT_DIR / "log_v7.txt"
121
+ def log(m):
122
+ print(m, flush=True)
123
+ with log_path.open("a", encoding="utf-8") as f: f.write(m + "\n")
124
+
125
+ def main():
126
+ log_path.write_text("")
127
+ train_ds = ConcatDataset([old_train_ds, new_train_ds])
128
+ train_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True,
129
+ num_workers=8, pin_memory=True, drop_last=True,
130
+ persistent_workers=True)
131
+ old_val_loader = DataLoader(old_val_ds, batch_size=BATCH, shuffle=False,
132
+ num_workers=4, pin_memory=True, persistent_workers=True)
133
+ new_val_loader = DataLoader(new_val_ds, batch_size=BATCH, shuffle=False,
134
+ num_workers=4, pin_memory=True, persistent_workers=True)
135
+
136
+ log(f"=== V7 FINE-TUNE ===")
137
+ log(f"old train sampled={len(old_train_sampled)} new train={len(new_train)}")
138
+ log(f"old val (no-leak frames>4500): {len(old_val)} new val: {len(new_val)}")
139
+ log(f"loading {CKPT_IN} ...")
140
+ model = SegformerForSemanticSegmentation.from_pretrained(
141
+ "nvidia/segformer-b5-finetuned-ade-640-640", num_labels=NUM_CLASSES,
142
+ id2label={i:n for i,n in enumerate(NAMES)},
143
+ label2id={n:i for i,n in enumerate(NAMES)},
144
+ ignore_mismatched_sizes=True,
145
+ ).cuda()
146
+ ckpt = torch.load(CKPT_IN, map_location="cuda", weights_only=False)
147
+ model.load_state_dict(ckpt["model"])
148
+ log(f" loaded v6 ckpt: epoch {ckpt['epoch']}, prev tree IoU {ckpt['tree_iou']:.3f}")
149
+
150
+ cw = torch.tensor(WEIGHTS, dtype=torch.float32, device="cuda")
151
+ loss_fn = nn.CrossEntropyLoss(weight=cw, ignore_index=IGNORE_INDEX)
152
+ optim = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-2)
153
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(optim, T_max=EPOCHS*len(train_loader))
154
+
155
+ log(f"train batches: {len(train_loader)}")
156
+
157
+ best_avg_tree = -1.0
158
+ history = []
159
+ for epoch in range(1, EPOCHS+1):
160
+ model.train()
161
+ t0 = time.time()
162
+ epoch_loss = 0.0
163
+ optim.zero_grad()
164
+ for step, (x, y) in enumerate(train_loader):
165
+ x = x.cuda(non_blocking=True); y = y.cuda(non_blocking=True)
166
+ with autocast("cuda", dtype=torch.bfloat16):
167
+ out = model(pixel_values=x)
168
+ logits = F.interpolate(out.logits, size=y.shape[-2:], mode="bilinear", align_corners=False)
169
+ loss = loss_fn(logits, y) / GRAD_ACCUM
170
+ loss.backward()
171
+ if (step+1) % GRAD_ACCUM == 0:
172
+ optim.step(); optim.zero_grad(); sched.step()
173
+ epoch_loss += loss.item() * GRAD_ACCUM
174
+ train_loss = epoch_loss / len(train_loader)
175
+
176
+ # ─── eval on old (8-class) and new (tree-only) ───
177
+ model.eval()
178
+ cm_old = np.zeros((NUM_CLASSES, NUM_CLASSES), dtype=np.int64)
179
+ cm_new = np.zeros((NUM_CLASSES, NUM_CLASSES), dtype=np.int64)
180
+ with torch.no_grad():
181
+ for x, y in old_val_loader:
182
+ x = x.cuda(); y = y.cuda()
183
+ with autocast("cuda", dtype=torch.bfloat16):
184
+ out = model(pixel_values=x)
185
+ logits = F.interpolate(out.logits, size=y.shape[-2:], mode="bilinear", align_corners=False)
186
+ cm_old += confusion(logits.argmax(1).cpu().numpy(), y.cpu().numpy(), NUM_CLASSES)
187
+ tree_tp = tree_fn = 0 # only tree class is meaningful (others not labeled)
188
+ for x, y in new_val_loader:
189
+ x = x.cuda(); y = y.cuda()
190
+ with autocast("cuda", dtype=torch.bfloat16):
191
+ out = model(pixel_values=x)
192
+ logits = F.interpolate(out.logits, size=y.shape[-2:], mode="bilinear", align_corners=False)
193
+ pred = logits.argmax(1).cpu().numpy()
194
+ ys = y.cpu().numpy()
195
+ # Tree recall = TP / (TP+FN) over labeled tree pixels only.
196
+ # Ignored pixels (ys==255) are excluded entirely.
197
+ tree_mask = (ys == 0)
198
+ tree_tp += int(((pred == 0) & tree_mask).sum())
199
+ tree_fn += int(((pred != 0) & tree_mask).sum())
200
+
201
+ iou_old = iou_from_cm(cm_old)
202
+ miou_old = float(np.nanmean(iou_old[:7]))
203
+ tree_old = float(iou_old[0])
204
+ # NEW val: only tree-recall is meaningful (other classes unlabeled)
205
+ tree_recall_new = tree_tp / (tree_tp + tree_fn) if (tree_tp + tree_fn) > 0 else float("nan")
206
+ avg_tree = (tree_old + tree_recall_new) / 2
207
+
208
+ elapsed = time.time() - t0
209
+ log(f"epoch {epoch:02d}/{EPOCHS} tloss={train_loss:.4f} ({elapsed:.0f}s)")
210
+ log(f" OLD val (8-class): mIoU(7)={miou_old:.3f} tree IoU={tree_old:.3f}")
211
+ log(f" NEW val: tree RECALL={tree_recall_new:.3f} (TP={tree_tp:,} FN={tree_fn:,})")
212
+ log(f" per-class OLD: " + ", ".join(f"{n}={v:.3f}" for n,v in zip(NAMES, iou_old)))
213
+
214
+ history.append({
215
+ "epoch": epoch, "train_loss": float(train_loss),
216
+ "tree_iou_old": tree_old, "tree_recall_new": tree_recall_new,
217
+ "miou_old_7": miou_old, "avg_tree": avg_tree,
218
+ "per_class_iou_old": {n: float(v) for n, v in zip(NAMES, iou_old)},
219
+ })
220
+
221
+ torch.save({"model": model.state_dict(), "epoch": epoch,
222
+ "tree_iou_old": tree_old, "tree_recall_new": tree_recall_new,
223
+ "miou_old_7": miou_old},
224
+ OUT_DIR / "v7_last.pt")
225
+ if avg_tree > best_avg_tree:
226
+ best_avg_tree = avg_tree
227
+ torch.save({"model": model.state_dict(), "epoch": epoch,
228
+ "tree_iou_old": tree_old, "tree_recall_new": tree_recall_new,
229
+ "miou_old_7": miou_old},
230
+ OUT_DIR / "v7_best.pt")
231
+ log(f" saved v7_best.pt (avg {avg_tree:.3f})")
232
+
233
+ (OUT_DIR / "history_v7.json").write_text(json.dumps(history, indent=2))
234
+
235
+ log(f"\n=== DONE ===")
236
+ log(f"best avg tree IoU (old+new mean): {best_avg_tree:.3f}")
237
+
238
+
239
+ if __name__ == "__main__":
240
+ main()
history_v7.json ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "epoch": 1,
4
+ "train_loss": 0.1445474764559036,
5
+ "tree_iou_old": 0.7276111213384829,
6
+ "tree_recall_new": 0.9964709949014234,
7
+ "miou_old_7": 0.7071341866953114,
8
+ "avg_tree": 0.8620410581199531,
9
+ "per_class_iou_old": {
10
+ "tree": 0.7276111213384829,
11
+ "ground": 0.8545028283913517,
12
+ "person": 0.7197452556695519,
13
+ "sky": 0.7665161031217024,
14
+ "road": 0.7716666106060096,
15
+ "mountain": 0.4105051777602567,
16
+ "building": 0.6993922099798241,
17
+ "background": 0.30209167992875857
18
+ }
19
+ },
20
+ {
21
+ "epoch": 2,
22
+ "train_loss": 0.09120039182344977,
23
+ "tree_iou_old": 0.7348800379399489,
24
+ "tree_recall_new": 0.9992896559386689,
25
+ "miou_old_7": 0.7107307502748205,
26
+ "avg_tree": 0.8670848469393089,
27
+ "per_class_iou_old": {
28
+ "tree": 0.7348800379399489,
29
+ "ground": 0.853819505788582,
30
+ "person": 0.7223546582340064,
31
+ "sky": 0.7675516469276799,
32
+ "road": 0.7742544234364183,
33
+ "mountain": 0.41988628752707635,
34
+ "building": 0.7023686920700309,
35
+ "background": 0.32039096359921654
36
+ }
37
+ },
38
+ {
39
+ "epoch": 3,
40
+ "train_loss": 0.08554688886655733,
41
+ "tree_iou_old": 0.74153728943059,
42
+ "tree_recall_new": 0.9995758181795736,
43
+ "miou_old_7": 0.71243266621694,
44
+ "avg_tree": 0.8705565538050819,
45
+ "per_class_iou_old": {
46
+ "tree": 0.74153728943059,
47
+ "ground": 0.8535114953849786,
48
+ "person": 0.721128950704959,
49
+ "sky": 0.7701010375444943,
50
+ "road": 0.7727083269046633,
51
+ "mountain": 0.4298014657280298,
52
+ "building": 0.6982400978208655,
53
+ "background": 0.34584085232597234
54
+ }
55
+ },
56
+ {
57
+ "epoch": 4,
58
+ "train_loss": 0.08495914258924014,
59
+ "tree_iou_old": 0.7386737697019637,
60
+ "tree_recall_new": 0.9998306235571522,
61
+ "miou_old_7": 0.7108516336566473,
62
+ "avg_tree": 0.869252196629558,
63
+ "per_class_iou_old": {
64
+ "tree": 0.7386737697019637,
65
+ "ground": 0.8530840272060696,
66
+ "person": 0.7226071760588065,
67
+ "sky": 0.7702586411872955,
68
+ "road": 0.7661165021954849,
69
+ "mountain": 0.42538260855725873,
70
+ "building": 0.699838710689653,
71
+ "background": 0.3418799912840437
72
+ }
73
+ },
74
+ {
75
+ "epoch": 5,
76
+ "train_loss": 0.0824889050444397,
77
+ "tree_iou_old": 0.7374752361770358,
78
+ "tree_recall_new": 0.999875807068874,
79
+ "miou_old_7": 0.7075837051390604,
80
+ "avg_tree": 0.8686755216229549,
81
+ "per_class_iou_old": {
82
+ "tree": 0.7374752361770358,
83
+ "ground": 0.8517622487225316,
84
+ "person": 0.7170282863817369,
85
+ "sky": 0.7688366303765454,
86
+ "road": 0.7567630529761218,
87
+ "mountain": 0.42263572337088734,
88
+ "building": 0.6985847579685641,
89
+ "background": 0.3276835145430565
90
+ }
91
+ },
92
+ {
93
+ "epoch": 6,
94
+ "train_loss": NaN,
95
+ "tree_iou_old": 0.7397202992344901,
96
+ "tree_recall_new": 0.9999024727479229,
97
+ "miou_old_7": 0.7127320425514964,
98
+ "avg_tree": 0.8698113859912064,
99
+ "per_class_iou_old": {
100
+ "tree": 0.7397202992344901,
101
+ "ground": 0.853263524499737,
102
+ "person": 0.7202039547444198,
103
+ "sky": 0.7715238413193277,
104
+ "road": 0.7738255330134594,
105
+ "mountain": 0.4198599823783031,
106
+ "building": 0.7107271626707372,
107
+ "background": 0.34450614720925465
108
+ }
109
+ },
110
+ {
111
+ "epoch": 7,
112
+ "train_loss": NaN,
113
+ "tree_iou_old": 0.7398512261769766,
114
+ "tree_recall_new": 0.9999254348604373,
115
+ "miou_old_7": 0.7132055573889754,
116
+ "avg_tree": 0.869888330518707,
117
+ "per_class_iou_old": {
118
+ "tree": 0.7398512261769766,
119
+ "ground": 0.8539561450004414,
120
+ "person": 0.7196386093776438,
121
+ "sky": 0.7709864078362132,
122
+ "road": 0.7727917702136876,
123
+ "mountain": 0.4276638710715956,
124
+ "building": 0.707550872046269,
125
+ "background": 0.36019225517621223
126
+ }
127
+ },
128
+ {
129
+ "epoch": 8,
130
+ "train_loss": 0.07825338785261966,
131
+ "tree_iou_old": 0.7386024885258515,
132
+ "tree_recall_new": 0.9999464217374665,
133
+ "miou_old_7": 0.7114400608211693,
134
+ "avg_tree": 0.869274455131659,
135
+ "per_class_iou_old": {
136
+ "tree": 0.7386024885258515,
137
+ "ground": 0.8543745147138091,
138
+ "person": 0.72068353262178,
139
+ "sky": 0.7693893529932668,
140
+ "road": 0.7702198236853051,
141
+ "mountain": 0.4261176724920308,
142
+ "building": 0.700693040716142,
143
+ "background": 0.34337551501723146
144
+ }
145
+ }
146
+ ]
predict.py CHANGED
@@ -21,7 +21,7 @@ from transformers import SegformerForSemanticSegmentation
21
 
22
  # ─── config ───
23
  BASE_MODEL = "nvidia/segformer-b5-finetuned-ade-640-640"
24
- WEIGHTS_PATH = os.environ.get("SEGFORMER85MV1_WEIGHTS", "Segformer85Mv1.pt") # local file or full path
25
  NAMES = ["tree", "ground", "person", "sky", "road", "mountain", "building", "background"]
26
  PALETTE = np.array([
27
  [60, 220, 60], # tree - green
 
21
 
22
  # ─── config ───
23
  BASE_MODEL = "nvidia/segformer-b5-finetuned-ade-640-640"
24
+ WEIGHTS_PATH = os.environ.get("SEGFORMER85M_WEIGHTS", "Segformer85Mv2.pt") # default v2 (best generalization); set env var or --weights to use v1
25
  NAMES = ["tree", "ground", "person", "sky", "road", "mountain", "building", "background"]
26
  PALETTE = np.array([
27
  [60, 220, 60], # tree - green
samples_v6_vs_v7/cmp_0_Annotation_Data_Sep16_T_m.jpg ADDED

Git LFS Details

  • SHA256: 92195d282cb209f8fa9cc905d01d0b1578e1bd21357a3c751638364ff6ab6980
  • Pointer size: 132 Bytes
  • Size of remote file: 2.32 MB
samples_v6_vs_v7/cmp_4_Annotation_Data_Sep16_T_m.jpg ADDED

Git LFS Details

  • SHA256: b39951b6f699d07afc516e5009d5e00fcf7da71ad7d84f082730d0348d8ca147
  • Pointer size: 132 Bytes
  • Size of remote file: 2.33 MB
samples_v6_vs_v7/cmp_5_Annotation_Data_Sep16_T_m.jpg ADDED

Git LFS Details

  • SHA256: 9e4be59b4e723633e9d646bd6310192dcd4b33e27363c79cb0b46d4600458d15
  • Pointer size: 132 Bytes
  • Size of remote file: 2.34 MB