goktug14 commited on
Commit
ac3810c
·
1 Parent(s): 16a6d0d

statistics

Browse files
Files changed (1) hide show
  1. app.py +285 -119
app.py CHANGED
@@ -2,55 +2,83 @@ import gradio as gr
2
  import pandas as pd
3
  from PIL import Image
4
  import os
5
- from collections import Counter
6
 
7
- # --- App Configuration ---
8
-
9
- # Define your sections and labels
10
  SECTION_LABELS = {
11
  "Oil Pore Related Issues": [
12
- "Very Large Pores (Not Red)", "Whiteheads (Clogged Pores)", "Blackheads (Clogged Pores)",
13
- "Shinny Skin", "Sebaceous Filaments (Sebum)"
 
 
 
14
  ],
15
  "Acne and Blemishes": [
16
- "Pustules", "Papules", "Nodules", "Cysts", "Acne", "Rosacea",
17
- "Telangiectasia", "Milia", "Scars", "Ice Berg Scars",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ],
19
- "Redness and Irritation": ["Redness", "Irritation"],
20
- "Dryness and Texture Issues": ["Dryness", "Fine Lines / Wrinkles", "Skin Flakes"],
21
- "Aging and Elasticity Issues": ["Loose Skin", "Deep Wrinkles"],
22
- "Pigmentation Issues": ["Dark Spots", "Melasma", "Freckles"],
23
  "Rosacea": [
24
- "Erythematotelangiectatic Rosacea", "Papulopustular Rosacea",
25
- "Phymatous Rosacea", "Ocular Rosacea"
 
 
26
  ],
27
- "Eczema": ["Deborrheic Dermatitis"]
 
 
28
  }
29
 
30
- # Define the sections for each column to control UI layout
31
  column1_sections = ["Oil Pore Related Issues", "Dryness and Texture Issues"]
32
  column2_sections = ["Acne and Blemishes"]
33
  column3_sections = ["Redness and Irritation", "Pigmentation Issues", "Aging and Elasticity Issues"]
34
  column4_sections = ["Rosacea", "Eczema"]
35
-
36
- # Combine all section lists to define the exact UI order
37
  UI_ORDERED_SECTIONS = column1_sections + column2_sections + column3_sections + column4_sections
38
 
39
- # Flattened labels list, created in the SAME order as the UI checkboxes will be.
40
  ALL_LABELS = [
41
  label
42
  for section_name in UI_ORDERED_SECTIONS
43
  for label in SECTION_LABELS.get(section_name, [])
44
  ]
45
 
46
- # --- Global State ---
47
- images = []
48
- current_index = 0
49
- results = []
50
- annotations = {}
51
-
52
- # --- Core Annotation Functions ---
 
53
 
 
 
 
54
  def display_image(idx):
55
  """Displays the image at the given index and its saved annotations."""
56
  if images:
@@ -75,171 +103,292 @@ def submit(*selections):
75
  """Saves the current annotations to the state and writes to a CSV file."""
76
  if not images:
77
  return "No image to label", None
 
 
78
  annotations[current_index] = list(selections)
79
  fname = os.path.basename(images[current_index])
80
  chosen_labels = [lbl for lbl, sel in zip(ALL_LABELS, selections) if sel]
81
 
82
  global results
 
83
  results = [r for r in results if r['image'] != fname]
84
  results.append({'image': fname, 'labels': ', '.join(chosen_labels)})
 
 
85
  df = pd.DataFrame(results)
86
  df.to_csv('image_labels.csv', index=False)
 
87
  return "Labels saved!", 'image_labels.csv'
88
 
89
  def upload_images(files):
90
- """Handles image uploads, resetting the annotation state."""
91
  global images, current_index, results, annotations
 
 
 
 
 
 
 
 
92
  images = [f.name for f in files]
93
  current_index = 0
94
  results = []
95
  annotations = {}
96
  outputs = display_image(0)
97
- # Hide the uploader component after a successful upload
98
- return outputs + [gr.update(visible=False), gr.update(visible=True)]
 
 
 
 
 
99
 
100
  def load_annotations(csv_file):
101
- """Loads annotations from an uploaded CSV file."""
102
  global annotations, results
103
  if csv_file is None or not images:
 
104
  return display_image(current_index)
 
105
  try:
106
  df = pd.read_csv(csv_file.name)
 
107
  image_map = {os.path.basename(name): i for i, name in enumerate(images)}
 
 
108
  annotations = {}
109
  results = df.to_dict('records')
 
110
  for _, row in df.iterrows():
111
- fname = row['image']
112
  if fname in image_map:
113
  img_idx = image_map[fname]
114
- saved_labels = set(l.strip() for l in row['labels'].split(',')) if pd.notna(row['labels']) else set()
 
 
 
 
 
115
  states = [label in saved_labels for label in ALL_LABELS]
116
  annotations[img_idx] = states
117
  except Exception as e:
118
  print(f"Error loading annotations: {e}")
 
 
 
 
119
  return display_image(current_index)
120
 
121
- # --- Statistics and Mode-Switching Functions ---
 
 
 
 
 
122
 
123
- def calculate_statistics(files):
124
- """Reads multiple CSVs and calculates the frequency of each label."""
125
  if not files:
126
- return [gr.update() for _ in ALL_LABELS] # No change
127
 
128
- label_counts = Counter()
129
- for file_obj in files:
130
  try:
131
- df = pd.read_csv(file_obj.name)
132
- if 'labels' in df.columns:
133
- df.dropna(subset=['labels'], inplace=True)
134
- for label_str in df['labels']:
135
- labels = [l.strip() for l in label_str.split(',')]
136
- label_counts.update(labels)
137
  except Exception as e:
138
- print(f"Could not process file {file_obj.name}: {e}")
139
  continue
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- updated_checkboxes = [gr.update(label=f"{label} (Count: {label_counts.get(label, 0)})") for label in ALL_LABELS]
142
- return updated_checkboxes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  def toggle_mode(current_mode):
145
- """Switches the UI between Annotation and Statistics modes."""
146
- if current_mode == "Annotation":
147
- new_mode = "Statistics"
148
- btn_text = "Switch to Annotation Mode"
149
- anno_upload_visible = False
150
- stats_upload_visible = True
151
- viewer_visible = False
152
- else: # Current mode is "Statistics"
153
- new_mode = "Annotation"
154
- btn_text = "Switch to Statistics Mode"
155
- anno_upload_visible = True
156
- stats_upload_visible = False
157
- viewer_visible = True
158
-
159
- # Reset labels back to their original names
160
- label_updates = [gr.update(label=lbl) for lbl in ALL_LABELS]
161
-
162
- return [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  new_mode,
164
- gr.update(value=btn_text),
165
- gr.update(visible=anno_upload_visible),
166
- gr.update(visible=stats_upload_visible),
167
- gr.update(visible=viewer_visible)
168
- ] + label_updates
 
 
 
 
 
 
 
169
 
170
- # --- Gradio UI Definition ---
 
 
171
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
172
- gr.Markdown("# Dermatology Annotation & Statistics Tool")
173
 
174
- app_mode = gr.State("Annotation")
175
-
176
- # --- TOP ROW CONTROLS (Uploaders and Mode Switch) ---
177
  with gr.Row():
178
- mode_toggle_btn = gr.Button("Switch to Statistics Mode")
179
- # Annotation Uploaders (Visible by default)
180
- with gr.Row(visible=True) as annotation_upload_row:
181
- image_upload = gr.File(label="1. Upload Images", file_count="multiple", file_types=["image"])
182
- csv_upload = gr.File(label="2. (Optional) Upload Annotations CSV", file_types=[".csv"], visible=False)
183
- # Statistics Uploader (Hidden by default)
184
- with gr.Row(visible=False) as statistics_upload_row:
185
- stats_csv_upload = gr.File(label="Upload one or more annotation CSV files", file_count="multiple", file_types=[".csv"])
186
-
187
- gr.Markdown("---")
188
-
189
- # --- MAIN UI ROW (Checkboxes on Left, Image Viewer on Right) ---
190
  checkbox_components = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  with gr.Row():
192
- # --- LEFT SIDE: CHECKBOXES ---
193
- with gr.Column(scale=2):
194
- with gr.Row():
195
- for col_sections in [column1_sections, column2_sections, column3_sections, column4_sections]:
196
- with gr.Column(scale=1, min_width=220):
197
- for section_name in col_sections:
198
- if section_name in SECTION_LABELS:
199
- with gr.Group():
200
- gr.Markdown(f"### {section_name}")
201
- for lbl in SECTION_LABELS[section_name]:
202
- cb = gr.Checkbox(label=lbl)
203
- checkbox_components.append(cb)
204
-
205
- # --- RIGHT SIDE: IMAGE VIEWER AND CONTROLS (for Annotation Mode) ---
206
- with gr.Column(scale=1, visible=True) as image_viewer_col:
207
  img = gr.Image(label="Image")
208
  caption = gr.Label(value="No images uploaded")
209
  with gr.Row():
210
  prev_btn = gr.Button("⬅️ Previous")
211
  next_btn = gr.Button("Next ➡️")
 
 
 
 
 
212
  submit_btn = gr.Button("Submit Labels")
213
  status = gr.Label()
214
  csv_downloader = gr.File(label="Download labels CSV")
215
 
216
  # --- Event Handling ---
217
 
218
- # Mode switching
219
- mode_toggle_btn.click(
220
- fn=toggle_mode,
221
- inputs=app_mode,
222
- outputs=[app_mode, mode_toggle_btn, annotation_upload_row, statistics_upload_row, image_viewer_col] + checkbox_components
223
- )
224
-
225
- # Statistics calculation
226
- stats_csv_upload.upload(
227
- fn=calculate_statistics,
228
- inputs=stats_csv_upload,
229
- outputs=checkbox_components
230
- )
231
-
232
- # Annotation functionality
233
  image_upload.upload(
234
  fn=upload_images,
235
  inputs=image_upload,
236
- outputs=[img, caption] + checkbox_components + [image_upload, csv_upload]
237
  )
 
 
238
  csv_upload.upload(
239
  fn=load_annotations,
240
  inputs=csv_upload,
241
  outputs=[img, caption] + checkbox_components
242
  )
 
 
 
 
 
 
 
 
 
 
243
  prev_btn.click(
244
  fn=lambda: navigate(-1),
245
  outputs=[img, caption] + checkbox_components
@@ -248,11 +397,28 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
248
  fn=lambda: navigate(1),
249
  outputs=[img, caption] + checkbox_components
250
  )
 
 
251
  submit_btn.click(
252
  fn=submit,
253
  inputs=checkbox_components,
254
  outputs=[status, csv_downloader]
255
  )
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  if __name__ == "__main__":
258
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
2
  import pandas as pd
3
  from PIL import Image
4
  import os
 
5
 
6
+ # ---------------------------
7
+ # Label Definitions & Layout
8
+ # ---------------------------
9
  SECTION_LABELS = {
10
  "Oil Pore Related Issues": [
11
+ "Very Large Pores (Not Red)",
12
+ "Whiteheads (Clogged Pores)",
13
+ "Blackheads (Clogged Pores)",
14
+ "Shinny Skin",
15
+ "Sebaceous Filaments (Sebum)"
16
  ],
17
  "Acne and Blemishes": [
18
+ "Pustules",
19
+ "Papules",
20
+ "Nodules",
21
+ "Cysts",
22
+ "Acne",
23
+ "Rosacea",
24
+ "Telangiectasia",
25
+ "Milia",
26
+ "Scars",
27
+ "Ice Berg Scars",
28
+ ],
29
+ "Redness and Irritation": [
30
+ "Redness",
31
+ "Irritation",
32
+ ],
33
+ "Dryness and Texture Issues": [
34
+ "Dryness",
35
+ "Fine Lines / Wrinkles",
36
+ "Skin Flakes"
37
+ ],
38
+ "Aging and Elasticity Issues": [
39
+ "Loose Skin",
40
+ "Deep Wrinkles"
41
+ ],
42
+ "Pigmentation Issues": [
43
+ "Dark Spots",
44
+ "Melasma",
45
+ "Freckles"
46
  ],
 
 
 
 
47
  "Rosacea": [
48
+ "Erythematotelangiectatic Rosacea",
49
+ "Papulopustular Rosacea",
50
+ "Phymatous Rosacea",
51
+ "Ocular Rosacea"
52
  ],
53
+ "Eczema": [
54
+ "Deborrheic Dermatitis"
55
+ ]
56
  }
57
 
 
58
  column1_sections = ["Oil Pore Related Issues", "Dryness and Texture Issues"]
59
  column2_sections = ["Acne and Blemishes"]
60
  column3_sections = ["Redness and Irritation", "Pigmentation Issues", "Aging and Elasticity Issues"]
61
  column4_sections = ["Rosacea", "Eczema"]
 
 
62
  UI_ORDERED_SECTIONS = column1_sections + column2_sections + column3_sections + column4_sections
63
 
 
64
  ALL_LABELS = [
65
  label
66
  for section_name in UI_ORDERED_SECTIONS
67
  for label in SECTION_LABELS.get(section_name, [])
68
  ]
69
 
70
+ # ---------------------------
71
+ # Global State
72
+ # ---------------------------
73
+ images = [] # list of image paths
74
+ current_index = 0 # index of current image
75
+ results = [] # list of {'image': fname, 'labels': '...'}
76
+ annotations = {} # {img_idx: [bool, ...]} aligned to ALL_LABELS
77
+ label_counts = {lbl: 0 for lbl in ALL_LABELS} # used in statistics mode
78
 
79
+ # ---------------------------
80
+ # Core Functions
81
+ # ---------------------------
82
  def display_image(idx):
83
  """Displays the image at the given index and its saved annotations."""
84
  if images:
 
103
  """Saves the current annotations to the state and writes to a CSV file."""
104
  if not images:
105
  return "No image to label", None
106
+
107
+ # Save selections to our annotations dictionary
108
  annotations[current_index] = list(selections)
109
  fname = os.path.basename(images[current_index])
110
  chosen_labels = [lbl for lbl, sel in zip(ALL_LABELS, selections) if sel]
111
 
112
  global results
113
+ # Remove any previous entry for this image to avoid duplicates
114
  results = [r for r in results if r['image'] != fname]
115
  results.append({'image': fname, 'labels': ', '.join(chosen_labels)})
116
+
117
+ # Write the updated results to a CSV file
118
  df = pd.DataFrame(results)
119
  df.to_csv('image_labels.csv', index=False)
120
+
121
  return "Labels saved!", 'image_labels.csv'
122
 
123
  def upload_images(files):
124
+ """Handles image uploads, resetting the application state."""
125
  global images, current_index, results, annotations
126
+ if not files:
127
+ return [None, "No images uploaded"] + [False] * len(ALL_LABELS) + [
128
+ gr.update(visible=True), # image_upload stays visible if nothing came
129
+ gr.update(visible=False), # csv_upload hidden
130
+ gr.update(visible=False) # stats_csv_upload hidden
131
+ ]
132
+
133
+ # Gradio File returns temp files with .name path
134
  images = [f.name for f in files]
135
  current_index = 0
136
  results = []
137
  annotations = {}
138
  outputs = display_image(0)
139
+
140
+ # Hide the image uploader after a successful upload; show per-image CSV upload
141
+ return outputs + [
142
+ gr.update(visible=False), # image_upload
143
+ gr.update(visible=True), # csv_upload
144
+ gr.update(visible=False) # stats_csv_upload
145
+ ]
146
 
147
  def load_annotations(csv_file):
148
+ """Loads annotations from an uploaded CSV file (annotation mode)."""
149
  global annotations, results
150
  if csv_file is None or not images:
151
+ # If no CSV is uploaded or no images are loaded, do nothing.
152
  return display_image(current_index)
153
+
154
  try:
155
  df = pd.read_csv(csv_file.name)
156
+ # Create a quick lookup map from filename to its index in the `images` list
157
  image_map = {os.path.basename(name): i for i, name in enumerate(images)}
158
+
159
+ # Reset existing annotations and results
160
  annotations = {}
161
  results = df.to_dict('records')
162
+
163
  for _, row in df.iterrows():
164
+ fname = row.get('image', '')
165
  if fname in image_map:
166
  img_idx = image_map[fname]
167
+ # Handle cases where labels might be empty (NaN)
168
+ if pd.notna(row.get('labels', None)):
169
+ saved_labels = set(l.strip() for l in str(row['labels']).split(',') if l.strip())
170
+ else:
171
+ saved_labels = set()
172
+ # Create the boolean state list for the checkboxes
173
  states = [label in saved_labels for label in ALL_LABELS]
174
  annotations[img_idx] = states
175
  except Exception as e:
176
  print(f"Error loading annotations: {e}")
177
+ # In case of error, just refresh the current view without changes
178
+ return display_image(current_index)
179
+
180
+ # After loading, refresh the view to show the annotations for the current image
181
  return display_image(current_index)
182
 
183
+ # ---------- Statistics Mode Helpers ----------
184
+ def aggregate_label_counts(files):
185
+ """Read multiple CSVs of annotations and aggregate per-label counts."""
186
+ counts = {lbl: 0 for lbl in ALL_LABELS}
187
+ file_ct = 0
188
+ rows_ct = 0
189
 
 
 
190
  if not files:
191
+ return counts, file_ct, rows_ct
192
 
193
+ for f in files:
 
194
  try:
195
+ df = pd.read_csv(f.name)
 
 
 
 
 
196
  except Exception as e:
197
+ print(f"Failed reading {getattr(f, 'name', 'file')}: {e}")
198
  continue
199
+ file_ct += 1
200
+
201
+ if 'labels' not in df.columns:
202
+ continue
203
+
204
+ for raw in df['labels'].dropna().astype(str):
205
+ rows_ct += 1
206
+ items = [s.strip() for s in raw.split(',') if s.strip()]
207
+ for item in items:
208
+ if item in counts:
209
+ counts[item] += 1
210
+ # silently ignore labels not in ALL_LABELS
211
 
212
+ return counts, file_ct, rows_ct
213
+
214
+ def upload_stats_csvs(files):
215
+ """Handles CSV upload in statistics mode; updates checkbox labels to show counts."""
216
+ global label_counts
217
+ label_counts, file_ct, rows_ct = aggregate_label_counts(files)
218
+
219
+ # Create updates for every checkbox: label "(count)", disabled & unchecked
220
+ checkbox_updates = [
221
+ gr.update(label=f"{lbl} ({label_counts.get(lbl, 0)})", value=False, interactive=False)
222
+ for lbl in ALL_LABELS
223
+ ]
224
+ note = f"Statistics mode: loaded {file_ct} file(s), counted {rows_ct} annotation row(s)."
225
+ return checkbox_updates + [note]
226
+
227
+ def make_checkbox_updates_for_mode(is_stats):
228
+ """Return a list of gr.update for all checkboxes depending on mode."""
229
+ if is_stats:
230
+ # Show counts & disable
231
+ return [
232
+ gr.update(label=f"{lbl} ({label_counts.get(lbl, 0)})", value=False, interactive=False)
233
+ for lbl in ALL_LABELS
234
+ ]
235
+ else:
236
+ # Restore original labels & interactivity; set values to current image's saved state
237
+ states = annotations.get(current_index, [False] * len(ALL_LABELS))
238
+ return [
239
+ gr.update(label=lbl, value=val, interactive=True)
240
+ for lbl, val in zip(ALL_LABELS, states)
241
+ ]
242
 
243
  def toggle_mode(current_mode):
244
+ """
245
+ Toggle between 'annotate' and 'stats' modes.
246
+ Returns updates for:
247
+ - mode_state
248
+ - image_upload (visible)
249
+ - csv_upload (visible)
250
+ - stats_csv_upload (visible)
251
+ - img (visible)
252
+ - caption (visible)
253
+ - prev_btn (visible)
254
+ - next_btn (visible)
255
+ - submit_btn (interactive)
256
+ - csv_downloader (visible)
257
+ - status (value)
258
+ - all checkboxes (labels/value/interactive)
259
+ """
260
+ new_mode = 'stats' if current_mode == 'annotate' else 'annotate'
261
+ is_stats = (new_mode == 'stats')
262
+
263
+ # Visibility & interactivity updates
264
+ img_vis = not is_stats
265
+ nav_vis = not is_stats
266
+ submit_interactive = not is_stats
267
+ downloader_vis = not is_stats
268
+
269
+ # Uploaders
270
+ image_upload_vis = not is_stats
271
+ csv_upload_vis = not is_stats # allow annotation CSV in annotate mode
272
+ stats_csv_upload_vis = is_stats
273
+
274
+ status_text = (
275
+ "Statistics mode: upload CSV files to compute per-label counts."
276
+ if is_stats
277
+ else "Annotation mode: select labels and submit. (Optional: load a CSV for existing annotations.)"
278
+ )
279
+
280
+ checkbox_updates = make_checkbox_updates_for_mode(is_stats)
281
+
282
+ return (
283
  new_mode,
284
+ gr.update(visible=image_upload_vis), # image_upload
285
+ gr.update(visible=csv_upload_vis), # csv_upload
286
+ gr.update(visible=stats_csv_upload_vis),# stats_csv_upload
287
+ gr.update(visible=img_vis), # img
288
+ gr.update(visible=img_vis), # caption
289
+ gr.update(visible=nav_vis), # prev_btn
290
+ gr.update(visible=nav_vis), # next_btn
291
+ gr.update(interactive=submit_interactive), # submit_btn
292
+ gr.update(visible=downloader_vis), # csv_downloader
293
+ status_text, # status (value)
294
+ *checkbox_updates
295
+ )
296
 
297
+ # ---------------------------
298
+ # Gradio UI
299
+ # ---------------------------
300
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
301
+ gr.Markdown("## Dermatology Annotation Tool")
302
 
 
 
 
303
  with gr.Row():
304
+ image_upload = gr.File(label="1. Upload Images", file_count="multiple", file_types=["image"])
305
+ # Annotation CSV (per-image) hidden until an image set is uploaded
306
+ csv_upload = gr.File(label="2. (Optional) Upload Annotations CSV", file_types=[".csv"], visible=False)
307
+ # Statistics CSV uploader (multi) hidden by default; shown in stats mode
308
+ stats_csv_upload = gr.File(label="Upload Annotation CSVs (Statistics Mode)", file_types=[".csv"], file_count="multiple", visible=False)
309
+
 
 
 
 
 
 
310
  checkbox_components = []
311
+
312
+ with gr.Row(): # Main row for the four columns of labels
313
+ # Column 1
314
+ with gr.Column(scale=1, min_width=0):
315
+ for section_name in column1_sections:
316
+ if section_name in SECTION_LABELS:
317
+ with gr.Group():
318
+ gr.Markdown(f"### {section_name}")
319
+ for lbl in SECTION_LABELS[section_name]:
320
+ cb = gr.Checkbox(label=lbl)
321
+ checkbox_components.append(cb)
322
+ # Column 2
323
+ with gr.Column(scale=1, min_width=0):
324
+ for section_name in column2_sections:
325
+ if section_name in SECTION_LABELS:
326
+ with gr.Group():
327
+ gr.Markdown(f"### {section_name}")
328
+ for lbl in SECTION_LABELS[section_name]:
329
+ cb = gr.Checkbox(label=lbl)
330
+ checkbox_components.append(cb)
331
+ # Column 3
332
+ with gr.Column(scale=1, min_width=0):
333
+ for section_name in column3_sections:
334
+ if section_name in SECTION_LABELS:
335
+ with gr.Group():
336
+ gr.Markdown(f"### {section_name}")
337
+ for lbl in SECTION_LABELS[section_name]:
338
+ cb = gr.Checkbox(label=lbl)
339
+ checkbox_components.append(cb)
340
+ # Column 4
341
+ with gr.Column(scale=1, min_width=0):
342
+ for section_name in column4_sections:
343
+ if section_name in SECTION_LABELS:
344
+ with gr.Group():
345
+ gr.Markdown(f"### {section_name}")
346
+ for lbl in SECTION_LABELS[section_name]:
347
+ cb = gr.Checkbox(label=lbl)
348
+ checkbox_components.append(cb)
349
+
350
+ # Image display and controls
351
  with gr.Row():
352
+ with gr.Column(scale=2): # Image display column
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  img = gr.Image(label="Image")
354
  caption = gr.Label(value="No images uploaded")
355
  with gr.Row():
356
  prev_btn = gr.Button("⬅️ Previous")
357
  next_btn = gr.Button("Next ➡️")
358
+
359
+ with gr.Column(scale=1): # Controls and download column
360
+ # --- TOGGLE BUTTON placed just above Submit Labels ---
361
+ mode_state = gr.State("annotate") # 'annotate' or 'stats'
362
+ toggle_btn = gr.Button("🔀 Switch to Statistics Mode", variant="secondary") # sits above Submit
363
  submit_btn = gr.Button("Submit Labels")
364
  status = gr.Label()
365
  csv_downloader = gr.File(label="Download labels CSV")
366
 
367
  # --- Event Handling ---
368
 
369
+ # When images are uploaded (annotation mode)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  image_upload.upload(
371
  fn=upload_images,
372
  inputs=image_upload,
373
+ outputs=[img, caption] + checkbox_components + [image_upload, csv_upload, stats_csv_upload]
374
  )
375
+
376
+ # Load per-image annotations CSV (annotation mode)
377
  csv_upload.upload(
378
  fn=load_annotations,
379
  inputs=csv_upload,
380
  outputs=[img, caption] + checkbox_components
381
  )
382
+
383
+ # Statistics CSVs upload (statistics mode)
384
+ # Updates all checkbox labels with counts & disables them, also sets status text
385
+ stats_csv_upload.upload(
386
+ fn=upload_stats_csvs,
387
+ inputs=stats_csv_upload,
388
+ outputs=checkbox_components + [status]
389
+ )
390
+
391
+ # Navigation
392
  prev_btn.click(
393
  fn=lambda: navigate(-1),
394
  outputs=[img, caption] + checkbox_components
 
397
  fn=lambda: navigate(1),
398
  outputs=[img, caption] + checkbox_components
399
  )
400
+
401
+ # Submit labels (annotation mode)
402
  submit_btn.click(
403
  fn=submit,
404
  inputs=checkbox_components,
405
  outputs=[status, csv_downloader]
406
  )
407
 
408
+ # Toggle mode (annotation <-> statistics)
409
+ toggle_btn.click(
410
+ fn=toggle_mode,
411
+ inputs=[mode_state],
412
+ outputs=[ # keep order in sync with toggle_mode return
413
+ mode_state,
414
+ image_upload, csv_upload, stats_csv_upload,
415
+ img, caption,
416
+ prev_btn, next_btn,
417
+ submit_btn, csv_downloader,
418
+ status,
419
+ *checkbox_components
420
+ ]
421
+ )
422
+
423
  if __name__ == "__main__":
424
+ demo.launch(server_name="0.0.0.0", server_port=7860)