MichaelDeutges commited on
Commit
907c840
·
verified ·
1 Parent(s): 6e41dcd

Upload 12 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,11 @@ 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
+ images/BAS_0001.tiff filter=lfs diff=lfs merge=lfs -text
37
+ images/KSC_0001.tiff filter=lfs diff=lfs merge=lfs -text
38
+ images/LYT_0005.tiff filter=lfs diff=lfs merge=lfs -text
39
+ images/MOB_0002.tiff filter=lfs diff=lfs merge=lfs -text
40
+ images/MON_0002.tiff filter=lfs diff=lfs merge=lfs -text
41
+ images/MYB_0003.tiff filter=lfs diff=lfs merge=lfs -text
42
+ images/NGS_0002.tiff filter=lfs diff=lfs merge=lfs -text
43
+ images/PMO_0002.tiff filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: LabelingTest
3
- emoji: 🐠
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Two-Button Image Labeler (HuggingFace Spaces)
3
+
4
+ A zero-install app for collaborators to label images with **two buttons** (left/right).
5
+ Perfect for quick yes/no or binary classification tasks with doctors or non-technical users.
6
+
7
+ ## Features
8
+ - Shows one image at a time.
9
+ - Two big buttons (configurable labels).
10
+ - Keyboard shortcuts: **A** for left, **L** for right.
11
+ - "Skip" button.
12
+ - Captures annotator's name.
13
+ - Saves to `labels.csv` and (optionally) **auto-uploads** it back to your Space repository after every label.
14
+ - Hides already-labeled images by default (configurable).
15
+ - Works with multiple annotators at once (file lock ensures safe writes).
16
+
17
+ ---
18
+
19
+ ## Quick Start (HuggingFace Spaces)
20
+
21
+ 1. Create a new **Space** → **Gradio**.
22
+ 2. Upload the files from this repo (`app.py`, `requirements.txt`). You can use the zip attached or drag & drop.
23
+ 3. Create a folder `images/` in the Space and upload your images there (PNG/JPG/etc.).
24
+ 4. (Optional but recommended) Set secrets in the Space **Settings → Variables and secrets**:
25
+ - `HF_TOKEN` — a write token from your Hugging Face account.
26
+ - `HF_SPACE_REPO` — your Space repo id, e.g. `yourname/your-space`.
27
+ - With these set, the app will push updates to `labels.csv` back into the repo automatically so you always have the latest labels.
28
+ 5. Share the Space link with your doctors. They:
29
+ - Enter their name
30
+ - Click **Start**
31
+ - Press **← Class A** or **Class B →** (or **A/L** keys)
32
+
33
+ You can download the latest labels at any time with the **Download CSV** button (Admin section).
34
+
35
+ ---
36
+
37
+ ## Configuration (Environment Variables)
38
+
39
+ | Variable | Default | Description |
40
+ |---|---|---|
41
+ | `IMAGE_DIR` | `images` | Folder containing images (can include subfolders). |
42
+ | `LABELS_CSV` | `labels.csv` | Path to the CSV file saved by the app. |
43
+ | `LABEL_A` | `Class A` | Text on the left button. |
44
+ | `LABEL_B` | `Class B` | Text on the right button. |
45
+ | `APP_TITLE` | `Two-Button Image Labeler` | Title at the top of the app. |
46
+ | `APP_DESCRIPTION` | *(see app.py)* | Instructions shown under the title. |
47
+ | `SHOW_ALREADY_LABELED` | `0` | If `1`, includes images that already have a label. |
48
+ | `SHUFFLE_IMAGES` | `1` | If `1`, randomize image order per session. |
49
+ | `HF_TOKEN` | *(none)* | Your HF write token. If set with `HF_SPACE_REPO`, labels are auto-pushed to the repo. |
50
+ | `HF_SPACE_REPO` | *(none)* | Your Space repo id, e.g. `yourname/your-space`. |
51
+ | `UPLOAD_TO_REPO` | `1` | If `1`, attempt to push labels to the repo on every write. |
52
+ | `AUTH_USER`, `AUTH_PASS` | *(none)* | If set, password-protect the app (basic auth). Useful if your Space is public. |
53
+
54
+ **Note:** If you keep `SHOW_ALREADY_LABELED=0`, the app hides any image that appears at least once in `labels.csv` (regardless of annotator). Set to `1` if you want multiple independent labels per image.
55
+
56
  ---
57
+
58
+ ## CSV Format
59
+
60
+ The app appends one row per click with these columns:
61
+ - `image` — relative path to the image from `IMAGE_DIR`
62
+ - `label` — the chosen label (`LABEL_A` or `LABEL_B`)
63
+ - `annotator` — whatever the user entered under "Your name"
64
+ - `timestamp` — ISO 8601 UTC time
65
+
66
  ---
67
 
68
+ ## Deploy Locally (Optional)
69
+
70
+ ```bash
71
+ pip install -r requirements.txt
72
+ python app.py
73
+ ```
74
+ Then open http://localhost:7860 and start labeling.
75
+ To add a password:
76
+ ```bash
77
+ export AUTH_USER=myuser
78
+ export AUTH_PASS=mypassword
79
+ python app.py
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Tips for Medical Data
85
+
86
+ - If images are sensitive, keep the Space **private** (available on paid HF plans) or enable basic auth via `AUTH_USER`/`AUTH_PASS`.
87
+ - You can also host the same app on your institution's intranet server; it’s just a Gradio Python script.
88
+
89
+ ---
90
+
91
+ ## Troubleshooting
92
+
93
+ - **No images found**: Make sure your Space repo has an `/images` folder with JPG/PNG files.
94
+ - **CSV not updating in Git**: Set both `HF_TOKEN` and `HF_SPACE_REPO` secrets. The app uploads `labels.csv` using `huggingface_hub` on each write.
95
+ - **Multiple annotators**: Supported. A file lock prevents collisions when writing `labels.csv`.
app.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import glob
4
+ import random
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ import gradio as gr
9
+ import pandas as pd
10
+ from filelock import FileLock
11
+ from huggingface_hub import HfApi
12
+
13
+ # ---------------------- Configuration via ENV ----------------------
14
+ IMAGE_DIR = os.getenv("IMAGE_DIR", "images") # folder inside repo
15
+ LABELS_CSV = os.getenv("LABELS_CSV", "labels.csv")
16
+ LABEL_A = os.getenv("LABEL_A", "Class A")
17
+ LABEL_B = os.getenv("LABEL_B", "Class B")
18
+ APP_TITLE = os.getenv("APP_TITLE", "Two-Button Image Labeler")
19
+ APP_DESCRIPTION = os.getenv(
20
+ "APP_DESCRIPTION",
21
+ "Press the left or right button (or use keyboard shortcuts) to label each image. "
22
+ "Enter your name first so we can attribute labels."
23
+ )
24
+ SHOW_ALREADY_LABELED = os.getenv("SHOW_ALREADY_LABELED", "0") == "1" # if 0, hide images that already have a label
25
+ SHUFFLE_IMAGES = os.getenv("SHUFFLE_IMAGES", "1") == "1"
26
+
27
+ # Hugging Face repo auto-sync (optional)
28
+ HF_TOKEN = os.getenv("HF_TOKEN") # add as a secret in Spaces
29
+ HF_SPACE_REPO = os.getenv("HF_SPACE_REPO") # e.g. "username/space-name"
30
+ UPLOAD_TO_REPO = os.getenv("UPLOAD_TO_REPO", "1") == "1"
31
+
32
+ # Optional basic auth
33
+ AUTH_USER = os.getenv("AUTH_USER")
34
+ AUTH_PASS = os.getenv("AUTH_PASS")
35
+
36
+ SUPPORTED_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tif", ".tiff", ".webp")
37
+
38
+
39
+ def list_images(image_dir: str):
40
+ paths = []
41
+ for p in glob.glob(os.path.join(image_dir, "**", "*"), recursive=True):
42
+ if p.lower().endswith(SUPPORTED_EXTS):
43
+ paths.append(p)
44
+ return sorted(paths)
45
+
46
+
47
+ def read_labels():
48
+ if os.path.exists(LABELS_CSV):
49
+ try:
50
+ return pd.read_csv(LABELS_CSV)
51
+ except Exception:
52
+ # if corrupted for any reason, don't crash
53
+ return pd.DataFrame(columns=["image", "label", "annotator", "timestamp"])
54
+ return pd.DataFrame(columns=["image", "label", "annotator", "timestamp"])
55
+
56
+
57
+ def rel_to_image_dir(p: str):
58
+ # Make relative paths for portability
59
+ try:
60
+ return str(Path(p).resolve().relative_to(Path(IMAGE_DIR).resolve()))
61
+ except Exception:
62
+ return p
63
+
64
+
65
+ def write_label(record: dict):
66
+ # Append a single record to LABELS_CSV safely
67
+ os.makedirs(os.path.dirname(LABELS_CSV) or ".", exist_ok=True)
68
+ with FileLock(LABELS_CSV + ".lock", timeout=10):
69
+ exists = os.path.exists(LABELS_CSV)
70
+ df = pd.DataFrame([record])
71
+ df.to_csv(LABELS_CSV, mode="a", header=not exists, index=False)
72
+
73
+ # Optional: upload the CSV to the Space repo so you automatically receive results
74
+ if HF_TOKEN and HF_SPACE_REPO and UPLOAD_TO_REPO:
75
+ try:
76
+ api = HfApi()
77
+ api.upload_file(
78
+ path_or_fileobj=LABELS_CSV,
79
+ path_in_repo=LABELS_CSV,
80
+ repo_id=HF_SPACE_REPO,
81
+ repo_type="space",
82
+ token=HF_TOKEN,
83
+ commit_message=f"Update labels: {datetime.now(timezone.utc).isoformat()}",
84
+ )
85
+ except Exception as e:
86
+ print("Warning: upload to repo failed:", e)
87
+
88
+
89
+ def start_session(annotator):
90
+ annotator = (annotator or "").strip()
91
+ if not annotator:
92
+ raise gr.Error("Please enter your name before starting.")
93
+
94
+ imgs = list_images(IMAGE_DIR)
95
+ if not imgs:
96
+ raise gr.Error("No images found. Upload images to the 'images' folder.")
97
+
98
+ labels_df = read_labels()
99
+ if not SHOW_ALREADY_LABELED and len(labels_df) > 0:
100
+ labeled = set(labels_df["image"].astype(str).tolist())
101
+ # Images in CSV are stored as relative paths (if possible). Normalize current list to relative too.
102
+ rel_imgs = [rel_to_image_dir(p) for p in imgs]
103
+ imgs = [p for p, r in zip(imgs, rel_imgs) if r not in labeled]
104
+
105
+ if not imgs:
106
+ raise gr.Error("All images appear to be labeled already.")
107
+
108
+ if SHUFFLE_IMAGES:
109
+ random.shuffle(imgs)
110
+
111
+ state = {
112
+ "annotator": annotator,
113
+ "order": imgs,
114
+ "idx": 0,
115
+ "total": len(imgs),
116
+ }
117
+ progress = f"0 / {state['total']}"
118
+ # Show the first image
119
+ return state, imgs[0], progress, gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
120
+
121
+
122
+ def next_image(state):
123
+ if state["idx"] >= state["total"]:
124
+ return None # triggers end
125
+ return state["order"][state["idx"]]
126
+
127
+
128
+ def render_progress(state):
129
+ return f"{min(state['idx'], state['total'])} / {state['total']}"
130
+
131
+
132
+ def label_and_next(state, choice):
133
+ if state is None:
134
+ raise gr.Error("Click 'Start' first.")
135
+
136
+ if state["idx"] >= state["total"]:
137
+ return state, None, "Done", gr.update(visible=False)
138
+
139
+ current_image = state["order"][state["idx"]]
140
+ record = {
141
+ "image": rel_to_image_dir(current_image),
142
+ "label": choice,
143
+ "annotator": state["annotator"],
144
+ "timestamp": datetime.now(timezone.utc).isoformat(),
145
+ }
146
+ write_label(record)
147
+ state["idx"] += 1
148
+
149
+ nxt = next_image(state)
150
+ if nxt is None:
151
+ # Finished
152
+ return state, None, "All done 🎉 Thank you!", gr.update(visible=False)
153
+ else:
154
+ return state, nxt, render_progress(state), gr.update(visible=True)
155
+
156
+
157
+ def skip_and_next(state):
158
+ if state is None:
159
+ raise gr.Error("Click 'Start' first.")
160
+ state["idx"] += 1
161
+ nxt = next_image(state)
162
+ if nxt is None:
163
+ return state, None, "All done 🎉 Thank you!", gr.update(visible=False)
164
+ else:
165
+ return state, nxt, render_progress(state), gr.update(visible=True)
166
+
167
+
168
+ def download_labels():
169
+ if not os.path.exists(LABELS_CSV):
170
+ raise gr.Error("No labels yet.")
171
+ return LABELS_CSV
172
+
173
+
174
+ with gr.Blocks(title=APP_TITLE, css=".gr-button {height: 64px; font-size: 18px;} .progress{font-weight:600}") as demo:
175
+ gr.Markdown(f"## {APP_TITLE}\n{APP_DESCRIPTION}")
176
+
177
+ with gr.Group():
178
+ with gr.Row():
179
+ annotator = gr.Textbox(label="Your name", placeholder="Type your name", scale=3)
180
+ start_btn = gr.Button("Start", variant="primary", scale=1)
181
+ with gr.Row():
182
+ info = gr.Markdown(
183
+ "1) Enter your name → 2) Click **Start** → 3) Use **Left / Right** buttons (shortcuts: **A** and **L**)."
184
+ )
185
+
186
+ image = gr.Image(label="Image", interactive=False)
187
+ progress = gr.Markdown("")
188
+ with gr.Row(visible=False) as buttons_row:
189
+ left = gr.Button(f"← {LABEL_A}", variant="secondary")
190
+ skip = gr.Button("Skip")
191
+ right = gr.Button(f"{LABEL_B} →", variant="secondary")
192
+
193
+ with gr.Accordion("Admin", open=False, visible=True) as admin:
194
+ with gr.Row():
195
+ download = gr.Button("Download CSV")
196
+ csv_file = gr.File(label="labels.csv", visible=False)
197
+
198
+ gr.Markdown(
199
+ "Tips: set `HF_TOKEN` and `HF_SPACE_REPO` secrets to automatically push `labels.csv` to the Space repository."
200
+ )
201
+
202
+ state = gr.State(None)
203
+
204
+ # Start flow
205
+ start_btn.click(
206
+ start_session,
207
+ inputs=[annotator],
208
+ outputs=[state, image, progress, buttons_row, admin, start_btn],
209
+ )
210
+
211
+ # Label buttons with shortcuts
212
+ left.click(
213
+ lambda s: label_and_next(s, LABEL_A),
214
+ inputs=[state],
215
+ outputs=[state, image, progress, buttons_row],
216
+ api_name="left_label",
217
+ )
218
+
219
+ right.click(
220
+ lambda s: label_and_next(s, LABEL_B),
221
+ inputs=[state],
222
+ outputs=[state, image, progress, buttons_row],
223
+ api_name="right_label",
224
+ )
225
+
226
+ skip.click(
227
+ skip_and_next,
228
+ inputs=[state],
229
+ outputs=[state, image, progress, buttons_row],
230
+ )
231
+
232
+ download.click(download_labels, outputs=[csv_file])
233
+
234
+ demo = demo.queue(max_size=64)
235
+
236
+ if __name__ == "__main__":
237
+ # Optional basic auth for local runs; on Spaces this will also work
238
+ if AUTH_USER and AUTH_PASS:
239
+ demo.launch(server_name="0.0.0.0", server_port=7860, auth=(AUTH_USER, AUTH_PASS))
240
+ else:
241
+ demo.launch(server_name="0.0.0.0", server_port=7860)
images/.gitkeep ADDED
File without changes
images/BAS_0001.tiff ADDED

Git LFS Details

  • SHA256: 5d6dab498963e663a92cea551ec5f4650dbee2df4b835657b2bb65e5dd2df307
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/KSC_0001.tiff ADDED

Git LFS Details

  • SHA256: 1c9b785d48af9da7b3b34a6ac249cc216544a5f4513fd456655b75eb67ffe879
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/LYT_0005.tiff ADDED

Git LFS Details

  • SHA256: f960227d1b60188bd60ba18b75eee77df6ef42c989996c02b41f5d16bbc4f4f1
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/MOB_0002.tiff ADDED

Git LFS Details

  • SHA256: 4b4ec6e0f969a5b3999ad9d9148d5e436d0a7d74af236b878c499dd3b112d167
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/MON_0002.tiff ADDED

Git LFS Details

  • SHA256: 546ba06fd8d2f8d2ba92490fb7be5a21ce267ea50f3a05618e7049e9eb3b0bb8
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/MYB_0003.tiff ADDED

Git LFS Details

  • SHA256: e8c83f074eee179ccd61e235c3c2d3b82700625dd28a60247d3caeb325c4963f
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/NGS_0002.tiff ADDED

Git LFS Details

  • SHA256: 3163eb8ad4d7370d30fcf939b8b4ef21071c77b8e9ee606b7c30bbda105c1a10
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
images/PMO_0002.tiff ADDED

Git LFS Details

  • SHA256: 75d8ac158b2e1a89d63174eaf4482abcb6f83bebe853729c13a3de3155a5defb
  • Pointer size: 131 Bytes
  • Size of remote file: 640 kB
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ gradio>=4.26.0
3
+ pandas>=2.1.0
4
+ huggingface_hub>=0.24.0
5
+ filelock>=3.13.0
6
+ Pillow>=10.0.0