Configure EuroSAT Field Scout submission

#1
by yava-code - opened
.gitattributes CHANGED
@@ -33,3 +33,8 @@ 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
+ weights/simple_net_v1.part00 filter=lfs diff=lfs merge=lfs -text
37
+ weights/simple_net_v1.part01 filter=lfs diff=lfs merge=lfs -text
38
+ weights/simple_net_v1.part02 filter=lfs diff=lfs merge=lfs -text
39
+ weights/simple_net_v1.part03 filter=lfs diff=lfs merge=lfs -text
40
+ weights/simple_net_v1.part04 filter=lfs diff=lfs merge=lfs -text
.hfignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git/
2
+ .cache/
3
+ .hf-inspect/
4
+ output/
5
+ __pycache__/
6
+ *.ipynb
7
+ full_model.pth
8
+ simple_net_v1.pth
9
+ simple_net_v1.weights
10
+ demo.mp4
11
+ 2026-06-13 19-57-34.mp4
FIELD_NOTES.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EuroSAT Field Scout - Field Notes
2
+
3
+ ## What I Built
4
+
5
+ EuroSAT Field Scout is a small Gradio app that classifies Sentinel-style land
6
+ tiles into 10 EuroSAT scene classes. The goal is not to replace a GIS workflow.
7
+ It is a quick first-pass helper for students, mapping volunteers, or anyone who
8
+ wants to sort a handful of tiles before deeper review.
9
+
10
+ ## Why It Fits Build Small
11
+
12
+ The deployed model is a custom PyTorch CNN with 2,492,170 parameters. It is far
13
+ below the 32B parameter cap and runs on CPU inside the Space. There are no cloud
14
+ inference calls and no hidden model API. The repo contains the weights as small
15
+ `weights/simple_net_v1.part*` chunks, and `app.py` reconstructs them at startup.
16
+
17
+ ## Implementation Notes
18
+
19
+ - `model.py` defines `SimpleNet`, a four-block convolutional classifier.
20
+ - `app.py` handles image upload, preprocessing, softmax inference, and Gradio UI.
21
+ - `examples/` contains small RGB EuroSAT samples from `torchgeo/eurosat`.
22
+ - The LinkedIn social post contains the short demo video for the hackathon submission.
23
+
24
+ One deployment issue was the original checkpoint format. The model was first
25
+ stored as a full pickled Python object, which is fragile across PyTorch versions.
26
+ For the Space, I converted it to a plain `state_dict`. To avoid Git LFS upload
27
+ issues in the hackathon org, the deployed file stores floating tensors in
28
+ float16 and splits the bytes into small `weights/simple_net_v1.part*` chunks.
29
+ The app joins the chunks in memory and casts tensors back to float32 before
30
+ loading. The app now instantiates the architecture explicitly and loads only
31
+ tensor weights.
32
+
33
+ ## Runtime Path
34
+
35
+ 1. User uploads an image or clicks a sample.
36
+ 2. The image is converted to RGB and resized to 64 x 64.
37
+ 3. The tensor is normalized with ImageNet-style mean and std values.
38
+ 4. `SimpleNet` runs on CPU.
39
+ 5. The app returns the top EuroSAT class probabilities.
40
+
41
+ ## Limits
42
+
43
+ The model was built for EuroSAT-style RGB tiles. It is useful for a quick demo
44
+ or educational triage, but it should not be used for production remote-sensing
45
+ decisions without calibration, validation on the target geography, and a clearer
46
+ uncertainty policy.
README.md CHANGED
@@ -1,13 +1,120 @@
1
  ---
2
- title: EuroSATFieldScout
3
- emoji: 🌖
4
- colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: EuroSAT Field Scout
3
+ emoji: 🛰️
4
+ colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.34.0
8
+ python_version: 3.11
9
  app_file: app.py
10
  pinned: false
11
+ license: mit
12
+ short_description: Local EuroSAT land-use classifier for map triage.
13
+ tags:
14
+ - gradio
15
+ - build-small-hackathon
16
+ - backyard-ai
17
+ - track:backyard
18
+ - small-models
19
+ - tiny-model
20
+ - computer-vision
21
+ - satellite-imagery
22
+ - pytorch
23
+ - eurosat
24
+ - local-first
25
+ - achievement:offgrid
26
+ - achievement:sharing
27
+ - achievement:fieldnotes
28
+ datasets:
29
+ - torchgeo/eurosat
30
  ---
31
 
32
+ <div align="center">
33
+
34
+ # EuroSAT Field Scout
35
+
36
+ Small local-first land-use triage for satellite tiles.
37
+
38
+ **Try it:** [Live Space](https://huggingface.co/spaces/build-small-hackathon/EuroSATFieldScout) · [Demo video + social post](https://www.linkedin.com/posts/varfolomiy-yasenoviy-a0209a2a3_gradio-huggingface-hackaton-share-7471607916270080000-W9RZ/) · [Field notes](FIELD_NOTES.md)
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## Judges Quick Read
45
+
46
+ - **Track:** Backyard AI
47
+ - **Model cap:** 2.49M-parameter PyTorch CNN, far below the 32B hackathon cap
48
+ - **Runtime:** Gradio Space on CPU
49
+ - **Off the Grid:** no cloud inference API; the app reconstructs local weights from `weights/simple_net_v1.part*`
50
+ - **Use case:** quick first-pass sorting of Sentinel-style land tiles for students, mapping volunteers, and geo demos
51
+
52
+ Upload a satellite or aerial land image and the app predicts the closest EuroSAT
53
+ land-use class:
54
+
55
+ - AnnualCrop
56
+ - Forest
57
+ - HerbaceousVegetation
58
+ - Highway
59
+ - Industrial
60
+ - Pasture
61
+ - PermanentCrop
62
+ - Residential
63
+ - River
64
+ - SeaLake
65
+
66
+ ## Demo
67
+
68
+ The short demo video is attached to the LinkedIn social post:
69
+
70
+ https://www.linkedin.com/posts/varfolomiy-yasenoviy-a0209a2a3_gradio-huggingface-hackaton-share-7471607916270080000-W9RZ/
71
+
72
+ ## Model
73
+
74
+ `SimpleNet` is a lightweight convolutional neural network trained on EuroSAT.
75
+ The Space reconstructs the local weights from `weights/simple_net_v1.part*` and
76
+ runs inference on CPU.
77
+
78
+ | Component | Details |
79
+ | --- | --- |
80
+ | Architecture | Four Conv-BN-ReLU-Pool blocks plus a dense classifier |
81
+ | Parameters | 2,492,170 |
82
+ | Input | RGB image resized to 64 x 64 |
83
+ | Output | 10 EuroSAT land-use classes |
84
+ | Inference | Local PyTorch CPU inference inside the Space |
85
+
86
+ ## Build Notes
87
+
88
+ The checkpoint originally existed as a pickled full model object. For a safer Space
89
+ deploy, it was converted to a plain PyTorch `state_dict` in `simple_net_v1.pth`.
90
+ That avoids PyTorch pickle compatibility issues and makes the app startup path
91
+ simple: instantiate `SimpleNet`, load weights, run inference.
92
+
93
+ For the hackathon Space upload, the state dict is stored as float16 tensors and
94
+ split into small `weights/simple_net_v1.part*` chunks so the submission can be
95
+ reviewed without Git LFS write permissions. The app rebuilds the bytes in memory
96
+ and casts floating tensors back to float32 before loading them into the model.
97
+
98
+ ## Badges
99
+
100
+ | Badge | Status | Why it fits |
101
+ | --- | --- | --- |
102
+ | Backyard AI | Submitted | Helps with a practical local mapping workflow |
103
+ | Off the Grid | Submitted | No hosted LLM or remote inference service |
104
+ | Sharing is Caring | Submitted | LinkedIn post and reusable Space source are public |
105
+ | Field Notes | Submitted | `FIELD_NOTES.md` documents the build and deployment choices |
106
+
107
+ ## Running Locally
108
+
109
+ ```bash
110
+ pip install -r requirements.txt
111
+ python app.py
112
+ ```
113
+
114
+ ## Submission Links
115
+
116
+ | Item | Link |
117
+ | --- | --- |
118
+ | Live Space | https://huggingface.co/spaces/build-small-hackathon/EuroSATFieldScout |
119
+ | Demo video + social post | https://www.linkedin.com/posts/varfolomiy-yasenoviy-a0209a2a3_gradio-huggingface-hackaton-share-7471607916270080000-W9RZ/ |
120
+ | Field notes | [`FIELD_NOTES.md`](FIELD_NOTES.md) |
app.py CHANGED
@@ -1,7 +1,83 @@
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
1
+ from io import BytesIO
2
+ from pathlib import Path
3
+
4
+ import torch
5
  import gradio as gr
6
+ from torchvision import transforms
7
+ from PIL import Image
8
+
9
+ from model import SimpleNet, CLASS_NAMES
10
+
11
+
12
+ WEIGHTS_DIR = Path("weights")
13
+ EXAMPLES = [
14
+ ["examples/annualcrop_sample.jpg"],
15
+ ["examples/forest_sample.jpg"],
16
+ ["examples/highway_sample.jpg"],
17
+ ["examples/industrial_sample.jpg"],
18
+ ["examples/residential_sample.jpg"],
19
+ ["examples/sealake_sample.jpg"],
20
+ ]
21
+
22
+
23
+ def load_model():
24
+ model = SimpleNet(num_classes=10)
25
+ weight_bytes = b"".join(
26
+ path.read_bytes() for path in sorted(WEIGHTS_DIR.glob("simple_net_v1.part*"))
27
+ )
28
+ state_dict = torch.load(BytesIO(weight_bytes), map_location="cpu")
29
+ state_dict = {
30
+ name: tensor.float() if torch.is_floating_point(tensor) else tensor
31
+ for name, tensor in state_dict.items()
32
+ }
33
+ model.load_state_dict(state_dict)
34
+ model.eval()
35
+ return model
36
+
37
+
38
+ model = load_model()
39
+
40
+ preprocess = transforms.Compose([
41
+ transforms.Resize((64, 64)),
42
+ transforms.ToTensor(),
43
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
44
+ std=[0.229, 0.224, 0.225]),
45
+ ])
46
+
47
+
48
+ def predict(image: Image.Image) -> dict[str, float]:
49
+ if image is None:
50
+ return {}
51
+
52
+ image = image.convert("RGB")
53
+ tensor = preprocess(image).unsqueeze(0) # [1, 3, 64, 64]
54
+
55
+ with torch.no_grad():
56
+ logits = model(tensor)
57
+ probs = torch.nn.functional.softmax(logits, dim=1)[0]
58
+
59
+ return {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))}
60
+
61
 
62
+ demo = gr.Interface(
63
+ fn=predict,
64
+ inputs=gr.Image(type="pil", label="Upload a Sentinel-style land image"),
65
+ outputs=gr.Label(num_top_classes=5, label="Top land-use guesses"),
66
+ title="EuroSAT Field Scout",
67
+ description=(
68
+ "A small local-first Gradio classifier for quick land-use triage. "
69
+ "It runs a custom PyTorch CNN trained on EuroSAT and returns the closest scene class."
70
+ ),
71
+ article=(
72
+ "Built for the Build Small Hackathon Backyard AI track. "
73
+ "No cloud inference API, no giant model: the Space loads local weights "
74
+ "and runs CPU inference inside the app."
75
+ ),
76
+ examples=EXAMPLES,
77
+ cache_examples=False,
78
+ allow_flagging="never",
79
+ theme=gr.themes.Soft(),
80
+ )
81
 
82
+ if __name__ == "__main__":
83
+ demo.queue(default_concurrency_limit=2).launch()
examples/annualcrop_sample.jpg ADDED
examples/forest_sample.jpg ADDED
examples/highway_sample.jpg ADDED
examples/industrial_sample.jpg ADDED
examples/residential_sample.jpg ADDED
examples/sealake_sample.jpg ADDED
model.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SimpleNet — lightweight CNN for EuroSAT satellite image classification.
3
+ 4 convolutional blocks (double-and-halve pattern) + FC classifier.
4
+ Input: 3×64×64 RGB | Output: 10 land-use classes | ~2.49M parameters
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+
11
+ CLASS_NAMES = [
12
+ "AnnualCrop", "Forest", "HerbaceousVegetation", "Highway",
13
+ "Industrial", "Pasture", "PermanentCrop", "Residential",
14
+ "River", "SeaLake"
15
+ ]
16
+
17
+
18
+ class SimpleNet(nn.Module):
19
+ def __init__(self, num_classes: int = 10):
20
+ super().__init__()
21
+
22
+ # 64×64 → 32×32
23
+ self.block1 = nn.Sequential(
24
+ nn.Conv2d(3, 32, kernel_size=3, padding=1),
25
+ nn.BatchNorm2d(32),
26
+ nn.ReLU(),
27
+ nn.MaxPool2d(2),
28
+ )
29
+ # 32×32 → 16×16
30
+ self.block2 = nn.Sequential(
31
+ nn.Conv2d(32, 64, kernel_size=3, padding=1),
32
+ nn.BatchNorm2d(64),
33
+ nn.ReLU(),
34
+ nn.MaxPool2d(2),
35
+ )
36
+ # 16×16 → 8×8
37
+ self.block3 = nn.Sequential(
38
+ nn.Conv2d(64, 128, kernel_size=3, padding=1),
39
+ nn.BatchNorm2d(128),
40
+ nn.ReLU(),
41
+ nn.MaxPool2d(2),
42
+ )
43
+ # 8×8 → 4×4
44
+ self.block4 = nn.Sequential(
45
+ nn.Conv2d(128, 256, kernel_size=3, padding=1),
46
+ nn.BatchNorm2d(256),
47
+ nn.ReLU(),
48
+ nn.MaxPool2d(2),
49
+ )
50
+
51
+ self.classifier = nn.Sequential(
52
+ nn.Flatten(),
53
+ nn.Linear(256 * 4 * 4, 512),
54
+ nn.ReLU(),
55
+ nn.Dropout(0.3),
56
+ nn.Linear(512, num_classes),
57
+ )
58
+
59
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
60
+ x = self.block1(x)
61
+ x = self.block2(x)
62
+ x = self.block3(x)
63
+ x = self.block4(x)
64
+ return self.classifier(x)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.2,<3
2
+ torchvision>=0.17,<1
3
+ gradio>=5.34,<6
4
+ huggingface_hub>=0.25,<1
5
+ Pillow>=10
weights/simple_net_v1.part00 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8681a2e1cb7d4188af217f57fdda104d07664455a779cdc241dd39008c102304
3
+ size 1000000
weights/simple_net_v1.part01 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d139af73bfe778f069f0fe9c373a81b401a93f60103b90228bef271345077b46
3
+ size 1000000
weights/simple_net_v1.part02 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8a8e3bd5e1863a6e2976bacfd504ff972d2640f198c49fade23e78232ea464af
3
+ size 1000000
weights/simple_net_v1.part03 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:280199fe980e2ee5845ce5cbe9e7ee56dd5493384b69c970cbf1d9367d2f5d63
3
+ size 1000000
weights/simple_net_v1.part04 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c08cfae02a27254d8aac9d9b5ff7e861010ee8de995aac3471c89205ed576f3
3
+ size 996327