JingdongZhang commited on
Commit
18aa143
·
1 Parent(s): be5140b

Update app with gallery layout and add example images

Browse files
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ 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
31
+ -----END CERTIFICATE-----
app.py CHANGED
@@ -7,19 +7,21 @@ from datetime import datetime
7
 
8
  # ================= 配置区域 (Configuration) =================
9
 
10
- # 图片所在的文件夹
11
  IMAGE_DIR = "./user_study_pairs"
12
-
13
- # 结果保存文件
14
  RESULT_FILE = "user_study_results.csv"
15
 
16
- # 每个任务抽取的样本数量 (修改为 5)
 
 
 
 
17
  SAMPLES_PER_TASK = 5
18
 
19
- # 任务列表 (文件名必须以这些开头)
20
  TASKS = ['haze', 'shadow', 'reflections', 'lens_flares']
21
 
22
- # 任务名称映射 (Task Names)
23
  TASK_NAMES = {
24
  'haze': {'cn': '雾霾', 'en': 'Fog/Haze'},
25
  'shadow': {'cn': '阴影', 'en': 'Shadows'},
@@ -30,10 +32,7 @@ TASK_NAMES = {
30
  # ================= 核心逻辑 (Core Logic) =================
31
 
32
  def get_random_batch():
33
- """
34
- 从文件夹中按任务分层随机抽取样本。
35
- Sample images stratified by task.
36
- """
37
  if not os.path.exists(IMAGE_DIR):
38
  return []
39
 
@@ -41,30 +40,21 @@ def get_random_batch():
41
  selected_batch = []
42
 
43
  for task in TASKS:
44
- # 筛选出属于当前 task 的图片
45
  task_files = [f for f in all_files if f.startswith(task)]
46
-
47
- # 随机抽取 N 张
48
  if len(task_files) >= SAMPLES_PER_TASK:
49
  chosen = random.sample(task_files, SAMPLES_PER_TASK)
50
  else:
51
  chosen = task_files
52
-
53
  for f in chosen:
54
- full_path = os.path.join(IMAGE_DIR, f)
55
- selected_batch.append((full_path, f, task))
56
 
57
  random.shuffle(selected_batch)
58
  return selected_batch
59
 
60
  def save_result(user_id, results):
61
- """
62
- 保存结果到 CSV
63
- Save results to CSV.
64
- """
65
  data = []
66
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
67
-
68
  for img_name, task, is_success in results:
69
  data.append({
70
  "User_ID": user_id,
@@ -73,46 +63,35 @@ def save_result(user_id, results):
73
  "Task": task,
74
  "Is_Success": is_success
75
  })
76
-
77
  df = pd.DataFrame(data)
78
-
79
  if not os.path.exists(RESULT_FILE):
80
  df.to_csv(RESULT_FILE, index=False, encoding='utf_8_sig')
81
  else:
82
  df.to_csv(RESULT_FILE, mode='a', header=False, index=False, encoding='utf_8_sig')
83
 
84
  def generate_label_html(task_key):
85
- """
86
- 根据任务生成图片上方的标签(两栏或三栏)
87
- Generate the label above the image based on the task (2-column or 3-column).
88
- """
89
  if task_key == 'shadow':
90
- # Shadow 任务使用三栏布局 (3-column layout for Shadow)
91
  return """
92
  <div class='big-label-container'>
93
- <div style='flex:1;'>Input (输入原图)</div>
94
- <div style='flex:1; border-left: 1px solid #ccc; border-right: 1px solid #ccc;'>Masked Shadow (阴影蒙版)</div>
95
- <div style='flex:1;'>Output (修复结果)</div>
96
  </div>
97
  """
98
  else:
99
- # 其他任务使用两栏布局 (2-column layout for others)
100
  return """
101
  <div class='big-label-container'>
102
- <div style='flex:1;'>Input (输入原图)</div>
103
- <div style='flex:1; border-left: 1px solid #ccc;'>Output (修复结果)</div>
104
  </div>
105
  """
106
 
107
  def generate_question_text(task_key):
108
- """
109
- 生成严格中英双语的问题描述
110
- Generate strictly bilingual question descriptions.
111
- """
112
  name_cn = TASK_NAMES.get(task_key, {}).get('cn', '干扰效应')
113
  name_en = TASK_NAMES.get(task_key, {}).get('en', 'Artifacts')
114
 
115
- # 针对 Shadow 任务的专用 Prompt
116
  if task_key == 'shadow':
117
  q1_cn = "1. <b>蒙版红色区域 (Masked Red Region)</b> 内的阴影是否去除干净?"
118
  q1_en = "1. Is the shadow within the <b>Masked Red Region</b> removed cleanly?"
@@ -120,118 +99,190 @@ def generate_question_text(task_key):
120
  q1_cn = f"1. 画面中的 <b>“{name_cn}”</b> 是否去除干净?"
121
  q1_en = f"1. Is the <b>“{name_en}”</b> removed cleanly?"
122
 
123
- text = f"""
124
  <div class='question-box'>
125
  <h3>请仔细评估 / Please Evaluate:</h3>
126
- <p>{q1_cn}<br>
127
- <span class='en-text'>{q1_en}</span></p>
128
- <hr style='margin: 10px 0; opacity: 0.3;'>
129
  <p>2. 原本的背景内容是否 <b>保持完整</b> 且未被篡改?<br>
130
  <span class='en-text'>2. Is the original background <b>preserved</b> without distortion?</span></p>
131
  </div>
132
  """
133
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  # ================= 界面构建 (UI Layout) =================
136
 
137
  with gr.Blocks(title="Visual Perception Study") as demo:
138
-
139
- # CSS 样式:使用 Flexbox 确保标签与图片列对齐
140
- gr.HTML("""
141
- <style>
142
- footer {visibility: hidden}
143
- .big-label-container {
144
- display: flex;
145
- text-align: center;
146
- font-weight: bold;
147
- font-size: 1.1em;
148
- color: #2c3e50;
149
- background-color: #e8f4f8;
150
- padding: 8px 0;
151
- border-radius: 8px;
152
- margin-bottom: 5px;
153
- width: 100%;
154
- }
155
- .question-box {
156
- background-color: #f9f9f9;
157
- padding: 15px;
158
- border-radius: 10px;
159
- border: 1px solid #ddd;
160
- margin-top: 10px;
161
- }
162
- .en-text {
163
- color: #666;
164
- font-style: italic;
165
- font-size: 0.95em;
166
- }
167
- </style>
168
- """)
169
-
170
  # --- 状态变量 ---
171
  state_batch = gr.State([])
172
  state_index = gr.State(0)
173
  state_results = gr.State([])
174
  state_user_id = gr.State("")
175
 
176
- # --- 页面 1: 欢迎 ---
177
  with gr.Group(visible=True) as welcome_page:
178
  gr.Markdown("# 📋 图像修复主观质量评估 / Perceptual Image Restoration Study")
179
 
180
  with gr.Row():
181
- with gr.Column():
182
  gr.Markdown("""
183
  ### 👋 欢迎 / Welcome
184
  本研究旨在评估图像修复算法在真实场景下的表现。
185
- This study evaluates the performance of image restoration algorithms in real-world scenarios.
186
 
187
  ### 🎯 评估标准 / Criteria for Success
188
  请判断修复结果是否 **同时满足** 以下两个条件:
189
  Please judge whether the restoration meets **BOTH** criteria:
190
 
191
- * **1. 去除彻底 (Removal Effectiveness)**
192
- 中文: 干扰视觉的效应(如雾反光)已基本消失
193
- English: The visual artifacts (haze, shadow, reflections) are effectively removed.
194
-
195
- * **2. 保真度 (Content Fidelity)**
196
- 中文: 原图的背景物体、结构、颜色未被扭曲或篡改。
197
- English: The original background structures, colors, and details are preserved without distortion.
198
  """)
199
 
 
 
200
  gr.Markdown(f"""
201
- ---
202
  * **题目数量 / Questions**: {len(TASKS) * SAMPLES_PER_TASK}
203
- * **匿名性 / Anonymity**: 结果仅用于学术统计 (Results are for academic use only).
204
  """)
205
 
206
- start_btn = gr.Button("开始测试 / Start Survey", variant="primary", size="lg")
207
-
208
- # --- 页面 2: 测试页 ---
209
- with gr.Group(visible=False) as quiz_page:
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- # 顶部进度
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  progress_md = gr.Markdown("### ⏳ Progress: 1 / 20")
213
 
214
- # 图片显示区域
215
  with gr.Column():
216
- # 动态标签占位符 (Dynamic Label Placeholder)
217
  label_html = gr.HTML(value="")
218
 
219
- image_display = gr.Image(
 
220
  label="",
221
- interactive=False,
222
  show_label=False,
223
- container=True
 
 
 
 
224
  )
225
 
226
- # 问题区域
227
- question_html = gr.HTML(value="Loading...")
228
 
229
- # 按钮区域
230
  with gr.Row():
231
- btn_fail = gr.Button("❌ 失败 / Failure\n(没去干净 或 改了背景 / Incomplete or Distorted)", size="lg", variant="stop")
232
- btn_pass = gr.Button("✅ 成功 / Success\n(去除干净 且 背景一致 / Clean & Preserved)", size="lg", variant="primary")
233
 
234
- # --- 页面 3: 结束页 ---
 
 
 
 
 
 
 
 
 
235
  with gr.Group(visible=False) as end_page:
236
  gr.Markdown("""
237
  # 🎉 感谢您的参与! / Thank You!
@@ -240,136 +291,147 @@ with gr.Blocks(title="Visual Perception Study") as demo:
240
  Your evaluation data has been submitted. You may close this page now.
241
  """)
242
 
243
- # ================= 新增:管理员下载区域 =================
244
  gr.Markdown("---")
245
- # 默认折叠,不占地方,也不引人注目
246
  with gr.Accordion("🔧 管理员��板 / Admin Panel", open=False):
247
  with gr.Row():
248
- # 密码输入框 (type='password' 会把输入显示为圆点)
249
- admin_pass = gr.Textbox(label="管理员密码 / Password", placeholder="Enter password to download", type="password")
250
- # 验证按钮
251
- check_btn = gr.Button("验证并获取数据 / Verify & Fetch Data")
252
-
253
- # 下载组件 (初始状态设为不可见 visible=False,输对密码才显示)
254
- file_download = gr.File(label="下载结果 / Download Results", interactive=False, visible=False)
255
-
256
- # 状态提示
257
  msg_box = gr.Markdown("")
258
 
259
  def verify_and_download(password):
260
- # 🔔 请在这里修改你的密码,比如把 "123456" 改成别的
261
  MY_SECRET_PASSWORD = "010929"
262
-
263
  if password == MY_SECRET_PASSWORD:
264
  if os.path.exists(RESULT_FILE):
265
- # 密码正确且文件存在:显示文件下载组件,并给出成功提示
266
- return {
267
- file_download: gr.update(value=RESULT_FILE, visible=True),
268
- msg_box: "✅ 验证成功!请下载文件。/ Success!"
269
- }
270
- else:
271
- return {
272
- file_download: gr.update(visible=False),
273
- msg_box: "⚠️ 暂无数据文件 (还没有人提交过结果)。"
274
- }
275
- else:
276
- # 密码错误:隐藏文件,给出警告
277
- return {
278
- file_download: gr.update(visible=False),
279
- msg_box: "❌ 密码错误 / Wrong Password"
280
- }
281
-
282
- # 绑定点击事件
283
- check_btn.click(
284
- verify_and_download,
285
- inputs=[admin_pass],
286
- outputs=[file_download, msg_box]
287
- )
288
- # =======================================================
289
-
290
- # ================= 交互逻辑 =================
291
 
292
  def start_survey():
293
  batch = get_random_batch()
294
  if not batch:
295
  return {
296
  welcome_page: gr.update(visible=True),
297
- gr.State(""): "Error: No images found"
298
  }
299
-
300
- u_id = str(int(time.time()))
301
 
 
302
  first_path, _, first_task = batch[0]
303
- q_text = generate_question_text(first_task)
304
- l_html = generate_label_html(first_task)
305
 
306
  return {
307
  welcome_page: gr.update(visible=False),
308
- quiz_page: gr.update(visible=True),
 
309
  end_page: gr.update(visible=False),
310
  state_batch: batch,
311
  state_index: 0,
312
  state_results: [],
313
  state_user_id: u_id,
314
- image_display: first_path,
315
  progress_md: f"### ⏳ Progress: 1 / {len(batch)}",
316
- question_html: q_text,
317
- label_html: l_html
 
 
 
 
 
 
 
 
 
318
  }
319
 
320
- def process_answer(is_success, index, batch, results, u_id):
321
- # 记录答案
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  _, current_name, current_task = batch[index]
323
  results.append((current_name, current_task, is_success))
324
 
325
  next_index = index + 1
326
 
327
  if next_index >= len(batch):
328
- # 完成
329
  save_result(u_id, results)
330
  return {
331
- quiz_page: gr.update(visible=False),
332
- end_page: gr.update(visible=True)
 
 
 
 
 
 
 
333
  }
334
  else:
335
- # 下一题
336
  next_path, _, next_task = batch[next_index]
337
- q_text = generate_question_text(next_task)
338
- l_html = generate_label_html(next_task)
339
 
340
  return {
 
 
 
341
  state_index: next_index,
342
  state_results: results,
343
- image_display: next_path,
344
  progress_md: f"### ⏳ Progress: {next_index + 1} / {len(batch)}",
345
- question_html: q_text,
346
- label_html: l_html
347
  }
348
 
349
- # 绑定事件
 
350
  start_btn.click(
351
  start_survey,
352
  inputs=[],
353
- outputs=[welcome_page, quiz_page, end_page, state_batch, state_index, state_results, state_user_id, image_display, progress_md, question_html, label_html]
354
  )
355
 
356
- # 2. 点击成功 (Pass)
357
- # trigger_mode="once": 关键参数!
358
- # 含义:如果当前正在处理上一次点击,则忽略所有新的点击。
359
- # 效果:防止用户手抖双击、防止快速连“盲测”。
360
  btn_pass.click(
361
- process_answer,
362
- inputs=[gr.Number(1, visible=False), state_index, state_batch, state_results, state_user_id],
363
- outputs=[quiz_page, end_page, state_index, state_results, image_display, progress_md, question_html, label_html],
364
- trigger_mode="once" # <--- 在这里加上这一句
 
 
 
 
365
  )
366
 
367
- # 3. 点击失败 (Fail)
368
  btn_fail.click(
369
- process_answer,
370
- inputs=[gr.Number(0, visible=False), state_index, state_batch, state_results, state_user_id],
371
- outputs=[quiz_page, end_page, state_index, state_results, image_display, progress_md, question_html, label_html],
372
- trigger_mode="once" # <--- 在这里加上这一句
 
 
 
 
373
  )
374
 
375
  if __name__ == "__main__":
 
7
 
8
  # ================= 配置区域 (Configuration) =================
9
 
10
+ # 路径配置
11
  IMAGE_DIR = "./user_study_pairs"
 
 
12
  RESULT_FILE = "user_study_results.csv"
13
 
14
+ # 示例路径
15
+ EXAMPLE_POS_DIR = "./example_pos"
16
+ EXAMPLE_NEG_DIR = "./example_neg"
17
+
18
+ # 每个任务抽取的样本数量
19
  SAMPLES_PER_TASK = 5
20
 
21
+ # 任务列表
22
  TASKS = ['haze', 'shadow', 'reflections', 'lens_flares']
23
 
24
+ # 任务名称映射
25
  TASK_NAMES = {
26
  'haze': {'cn': '雾霾', 'en': 'Fog/Haze'},
27
  'shadow': {'cn': '阴影', 'en': 'Shadows'},
 
32
  # ================= 核心逻辑 (Core Logic) =================
33
 
34
  def get_random_batch():
35
+ """从文件夹中按任务分层随机抽取样本"""
 
 
 
36
  if not os.path.exists(IMAGE_DIR):
37
  return []
38
 
 
40
  selected_batch = []
41
 
42
  for task in TASKS:
 
43
  task_files = [f for f in all_files if f.startswith(task)]
 
 
44
  if len(task_files) >= SAMPLES_PER_TASK:
45
  chosen = random.sample(task_files, SAMPLES_PER_TASK)
46
  else:
47
  chosen = task_files
 
48
  for f in chosen:
49
+ selected_batch.append((os.path.join(IMAGE_DIR, f), f, task))
 
50
 
51
  random.shuffle(selected_batch)
52
  return selected_batch
53
 
54
  def save_result(user_id, results):
55
+ """保存结果到 CSV"""
 
 
 
56
  data = []
57
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
58
  for img_name, task, is_success in results:
59
  data.append({
60
  "User_ID": user_id,
 
63
  "Task": task,
64
  "Is_Success": is_success
65
  })
 
66
  df = pd.DataFrame(data)
 
67
  if not os.path.exists(RESULT_FILE):
68
  df.to_csv(RESULT_FILE, index=False, encoding='utf_8_sig')
69
  else:
70
  df.to_csv(RESULT_FILE, mode='a', header=False, index=False, encoding='utf_8_sig')
71
 
72
  def generate_label_html(task_key):
73
+ """生成图片标签 (Dark Mode 兼容)"""
 
 
 
74
  if task_key == 'shadow':
 
75
  return """
76
  <div class='big-label-container'>
77
+ <div style='flex:1;'>Input<br><span class='sub-label'>(输入原图)</span></div>
78
+ <div style='flex:1; border-left: 1px solid #ccc; border-right: 1px solid #ccc;'>Masked Shadow<br><span class='sub-label'>(阴影蒙版)</span></div>
79
+ <div style='flex:1;'>Output<br><span class='sub-label'>(修复结果)</span></div>
80
  </div>
81
  """
82
  else:
 
83
  return """
84
  <div class='big-label-container'>
85
+ <div style='flex:1;'>Input<br><span class='sub-label'>(输入原图)</span></div>
86
+ <div style='flex:1; border-left: 1px solid #ccc;'>Output<br><span class='sub-label'>(修复结果)</span></div>
87
  </div>
88
  """
89
 
90
  def generate_question_text(task_key):
91
+ """生成问题描述"""
 
 
 
92
  name_cn = TASK_NAMES.get(task_key, {}).get('cn', '干扰效应')
93
  name_en = TASK_NAMES.get(task_key, {}).get('en', 'Artifacts')
94
 
 
95
  if task_key == 'shadow':
96
  q1_cn = "1. <b>蒙版红色区域 (Masked Red Region)</b> 内的阴影是否去除干净?"
97
  q1_en = "1. Is the shadow within the <b>Masked Red Region</b> removed cleanly?"
 
99
  q1_cn = f"1. 画面中的 <b>“{name_cn}”</b> 是否去除干净?"
100
  q1_en = f"1. Is the <b>“{name_en}”</b> removed cleanly?"
101
 
102
+ return f"""
103
  <div class='question-box'>
104
  <h3>请仔细评估 / Please Evaluate:</h3>
105
+ <p>{q1_cn}<br><span class='en-text'>{q1_en}</span></p>
106
+ <hr style='margin: 10px 0; opacity: 0.3; border-top: 1px solid #ccc;'>
 
107
  <p>2. 原本的背景内容是否 <b>保持完整</b> 且未被篡改?<br>
108
  <span class='en-text'>2. Is the original background <b>preserved</b> without distortion?</span></p>
109
  </div>
110
  """
111
+
112
+ # ================= 样式定义 (CSS) =================
113
+ # 将 CSS 独立出来,使用 gr.HTML 注入,避免版本兼容性警告
114
+ CUSTOM_CSS = """
115
+ <style>
116
+ footer {visibility: hidden}
117
+
118
+ /* 强制在 Light/Dark 模式下,这些背景框里的文字都是深色 */
119
+ .big-label-container {
120
+ display: flex;
121
+ text-align: center;
122
+ font-weight: bold;
123
+ font-size: 1.1em;
124
+ color: #333 !important;
125
+ background-color: #e8f4f8;
126
+ padding: 8px 0;
127
+ border-radius: 8px;
128
+ margin-bottom: 5px;
129
+ width: 100%;
130
+ align-items: center;
131
+ }
132
+ .question-box {
133
+ background-color: #f9f9f9;
134
+ padding: 15px;
135
+ border-radius: 10px;
136
+ border: 1px solid #ddd;
137
+ margin-top: 10px;
138
+ color: #333 !important;
139
+ }
140
+ .question-box h3 {
141
+ color: #2c3e50 !important;
142
+ margin-top: 0;
143
+ }
144
+ .en-text {
145
+ color: #666 !important;
146
+ font-style: italic;
147
+ font-size: 0.95em;
148
+ }
149
+ .sub-label {
150
+ font-size: 0.8em;
151
+ font-weight: normal;
152
+ color: #555 !important;
153
+ }
154
+ /* Loading 样式 */
155
+ .loading-container {
156
+ display: flex;
157
+ justify-content: center;
158
+ align-items: center;
159
+ height: 300px;
160
+ font-size: 1.5em;
161
+ color: #666;
162
+ flex-direction: column;
163
+ }
164
+ </style>
165
+ """
166
 
167
  # ================= 界面构建 (UI Layout) =================
168
 
169
  with gr.Blocks(title="Visual Perception Study") as demo:
170
+ # 注入样式
171
+ gr.HTML(CUSTOM_CSS)
172
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  # --- 状态变量 ---
174
  state_batch = gr.State([])
175
  state_index = gr.State(0)
176
  state_results = gr.State([])
177
  state_user_id = gr.State("")
178
 
179
+ # ================= 页面 1: 欢迎与示例 =================
180
  with gr.Group(visible=True) as welcome_page:
181
  gr.Markdown("# 📋 图像修复主观质量评估 / Perceptual Image Restoration Study")
182
 
183
  with gr.Row():
184
+ with gr.Column(scale=2):
185
  gr.Markdown("""
186
  ### 👋 欢迎 / Welcome
187
  本研究旨在评估图像修复算法在真实场景下的表现。
188
+ This study evaluates image restoration algorithms in real-world scenarios.
189
 
190
  ### 🎯 评估标准 / Criteria for Success
191
  请判断修复结果是否 **同时满足** 以下两个条件:
192
  Please judge whether the restoration meets **BOTH** criteria:
193
 
194
+ 1. **去除彻底 (Clean Removal)**: 干扰(雾霾/烟雾/阴影/反光/镜头炫光)已消失。
195
+ 2. **保真度 (Content Fidelity)**: 背景物体结构颜色未被篡改
 
 
 
 
 
196
  """)
197
 
198
+ start_btn = gr.Button("🚀 开始测试 / Start Survey", variant="primary", size="lg")
199
+
200
  gr.Markdown(f"""
 
201
  * **题目数量 / Questions**: {len(TASKS) * SAMPLES_PER_TASK}
202
+ * **匿名性 / Anonymity**: 结果仅用于学术统计
203
  """)
204
 
205
+ gr.Markdown("---")
206
+ gr.Markdown("### 📚 示例参考 / Examples")
207
+
208
+ # --- Positive Examples ---
209
+ gr.Markdown("#### ✅ 成功示例 / Success Cases (Clean & Preserved)")
210
+
211
+ # 预先构建数据列表 [(路径, 标题), ...]
212
+ pos_gallery_data = []
213
+ pos_files = ["pos_haze.png", "pos_shadow.png", "pos_ref.png", "pos_flare.png"]
214
+ pos_captions = ["Haze Removal", "Shadow Removal", "Reflection Removal", "Flare Removal"]
215
+
216
+ for p_file, p_cap in zip(pos_files, pos_captions):
217
+ p_path = os.path.join(EXAMPLE_POS_DIR, p_file)
218
+ if os.path.exists(p_path):
219
+ pos_gallery_data.append((p_path, p_cap))
220
 
221
+ # 使用 Gallery 组件:columns=4 强制一行四列对齐
222
+ if pos_gallery_data:
223
+ gr.Gallery(
224
+ value=pos_gallery_data,
225
+ show_label=False,
226
+ columns=4, # 强制 4 列对齐
227
+ rows=1,
228
+ height="auto",
229
+ object_fit="contain"
230
+ )
231
+
232
+ # --- Negative Examples ---
233
+ gr.Markdown("#### ❌ 失败示例 / Failure Cases")
234
+ with gr.Row():
235
+ # Neg 1: Identity Changed
236
+ neg_path_1 = os.path.join(EXAMPLE_NEG_DIR, "neg_identity_changed.png")
237
+ if os.path.exists(neg_path_1):
238
+ with gr.Column():
239
+ gr.Image(neg_path_1, interactive=False, show_label=False)
240
+ gr.Markdown("**❌ 失败原因:背景结构改变**<br>*(Failure: Structure/Identity Changed)*", elem_classes="question-box")
241
+
242
+ # Neg 2: Not Removed
243
+ neg_path_2 = os.path.join(EXAMPLE_NEG_DIR, "neg_not_fully_removed.png")
244
+ if os.path.exists(neg_path_2):
245
+ with gr.Column():
246
+ gr.Image(neg_path_2, interactive=False, show_label=False)
247
+ gr.Markdown("**❌ 失败原因:干扰未去干净**<br>*(Failure: Artifacts Not Fully Removed)*", elem_classes="question-box")
248
+
249
+ # ================= 页面 2: 测试页 (包含 Loading) =================
250
+
251
+ # 2.1 真实的答题区域 (Group)
252
+ with gr.Group(visible=False) as quiz_main_ui:
253
  progress_md = gr.Markdown("### ⏳ Progress: 1 / 20")
254
 
 
255
  with gr.Column():
256
+ # 动态标签
257
  label_html = gr.HTML(value="")
258
 
259
+ # 图片组件:保留 show_fullscreen_button 以支持放大
260
+ image_display = gr.Gallery(
261
  label="",
 
262
  show_label=False,
263
+ columns=1,
264
+ rows=1,
265
+ height="auto",
266
+ object_fit="contain",
267
+ elem_id="main_task_gallery" # 方便 CSS 控制
268
  )
269
 
270
+ question_html = gr.HTML(value="")
 
271
 
 
272
  with gr.Row():
273
+ btn_fail = gr.Button("❌ 失败 / Failure\n(Incomplete / Distorted)", size="lg", variant="stop")
274
+ btn_pass = gr.Button("✅ 成功 / Success\n(Clean & Preserved)", size="lg", variant="primary")
275
 
276
+ # 2.2 Loading 等待区域 (Group)
277
+ with gr.Group(visible=False) as loading_ui:
278
+ gr.HTML("""
279
+ <div class='loading-container'>
280
+ <p>🔄 Loading next image...</p>
281
+ <p>正在加载下一张...</p>
282
+ </div>
283
+ """)
284
+
285
+ # ================= 页面 3: 结束页 =================
286
  with gr.Group(visible=False) as end_page:
287
  gr.Markdown("""
288
  # 🎉 感谢您的参与! / Thank You!
 
291
  Your evaluation data has been submitted. You may close this page now.
292
  """)
293
 
294
+ # 管理员面板
295
  gr.Markdown("---")
 
296
  with gr.Accordion("🔧 管理员��板 / Admin Panel", open=False):
297
  with gr.Row():
298
+ admin_pass = gr.Textbox(label="Password", type="password")
299
+ check_btn = gr.Button("Download Data")
300
+ file_download = gr.File(label="Result CSV", interactive=False, visible=False)
 
 
 
 
 
 
301
  msg_box = gr.Markdown("")
302
 
303
  def verify_and_download(password):
 
304
  MY_SECRET_PASSWORD = "010929"
 
305
  if password == MY_SECRET_PASSWORD:
306
  if os.path.exists(RESULT_FILE):
307
+ return {file_download: gr.update(value=RESULT_FILE, visible=True), msg_box: "✅ Success!"}
308
+ return {file_download: gr.update(visible=False), msg_box: "⚠️ No data yet."}
309
+ return {file_download: gr.update(visible=False), msg_box: "❌ Wrong Password"}
310
+
311
+ check_btn.click(verify_and_download, inputs=[admin_pass], outputs=[file_download, msg_box])
312
+
313
+ # ================= 交互逻辑 (Event Handlers) =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
  def start_survey():
316
  batch = get_random_batch()
317
  if not batch:
318
  return {
319
  welcome_page: gr.update(visible=True),
320
+ gr.State(""): "Error"
321
  }
 
 
322
 
323
+ u_id = str(int(time.time()))
324
  first_path, _, first_task = batch[0]
 
 
325
 
326
  return {
327
  welcome_page: gr.update(visible=False),
328
+ quiz_main_ui: gr.update(visible=True),
329
+ loading_ui: gr.update(visible=False),
330
  end_page: gr.update(visible=False),
331
  state_batch: batch,
332
  state_index: 0,
333
  state_results: [],
334
  state_user_id: u_id,
335
+ image_display: [first_path], # <--- 注意这里加了中括号!
336
  progress_md: f"### ⏳ Progress: 1 / {len(batch)}",
337
+ question_html: generate_question_text(first_task),
338
+ label_html: generate_label_html(first_task)
339
+ }
340
+
341
+ # --- 关键逻辑:分两步走以解决卡顿 ---
342
+
343
+ # 步骤 1: 立即切换到 Loading 界面 (前端快速响应)
344
+ def switch_to_loading():
345
+ return {
346
+ quiz_main_ui: gr.update(visible=False), # 立即隐藏按钮,防止连点
347
+ loading_ui: gr.update(visible=True) # 显示 Loading
348
  }
349
 
350
+ # 步骤 2: 后台处理数据,准备好后切换回答题界面
351
+ def process_and_next(is_success, index, batch, results, u_id):
352
+ # 安全检查
353
+ if not batch or index >= len(batch):
354
+ return {
355
+ quiz_main_ui: gr.update(visible=False),
356
+ loading_ui: gr.update(visible=False),
357
+ end_page: gr.update(visible=True),
358
+ state_index: index,
359
+ state_results: results,
360
+ image_display: gr.update(),
361
+ progress_md: gr.update(),
362
+ question_html: gr.update(),
363
+ label_html: gr.update()
364
+ }
365
+
366
+ # 记录数据
367
  _, current_name, current_task = batch[index]
368
  results.append((current_name, current_task, is_success))
369
 
370
  next_index = index + 1
371
 
372
  if next_index >= len(batch):
373
+ # 完成所有题目
374
  save_result(u_id, results)
375
  return {
376
+ quiz_main_ui: gr.update(visible=False),
377
+ loading_ui: gr.update(visible=False),
378
+ end_page: gr.update(visible=True),
379
+ state_index: next_index,
380
+ state_results: results,
381
+ image_display: gr.update(),
382
+ progress_md: gr.update(),
383
+ question_html: gr.update(),
384
+ label_html: gr.update()
385
  }
386
  else:
387
+ # 加载下一题
388
  next_path, _, next_task = batch[next_index]
 
 
389
 
390
  return {
391
+ quiz_main_ui: gr.update(visible=True),
392
+ loading_ui: gr.update(visible=False),
393
+ end_page: gr.update(visible=False),
394
  state_index: next_index,
395
  state_results: results,
396
+ image_display: [next_path], # <--- [修改处 4] 注意这里加了中括号!
397
  progress_md: f"### ⏳ Progress: {next_index + 1} / {len(batch)}",
398
+ question_html: generate_question_text(next_task),
399
+ label_html: generate_label_html(next_task)
400
  }
401
 
402
+ # --- 绑定事件 ---
403
+
404
  start_btn.click(
405
  start_survey,
406
  inputs=[],
407
+ outputs=[welcome_page, quiz_main_ui, loading_ui, end_page, state_batch, state_index, state_results, state_user_id, image_display, progress_md, question_html, label_html]
408
  )
409
 
410
+ # 链式调用:先变Loading,再算结果
411
+ # queue=False 保证第一步立即执行,不排队
412
+
413
+ # 点击 Pass
414
  btn_pass.click(
415
+ switch_to_loading,
416
+ inputs=[],
417
+ outputs=[quiz_main_ui, loading_ui],
418
+ queue=False
419
+ ).then(
420
+ process_and_next,
421
+ inputs=[gr.Number(1, visible=False), state_index, state_batch, state_results, state_user_id],
422
+ outputs=[quiz_main_ui, loading_ui, end_page, state_index, state_results, image_display, progress_md, question_html, label_html]
423
  )
424
 
425
+ # 点击 Fail
426
  btn_fail.click(
427
+ switch_to_loading,
428
+ inputs=[],
429
+ outputs=[quiz_main_ui, loading_ui],
430
+ queue=False
431
+ ).then(
432
+ process_and_next,
433
+ inputs=[gr.Number(0, visible=False), state_index, state_batch, state_results, state_user_id],
434
+ outputs=[quiz_main_ui, loading_ui, end_page, state_index, state_results, image_display, progress_md, question_html, label_html]
435
  )
436
 
437
  if __name__ == "__main__":
example_neg/neg_identity_changed.png ADDED

Git LFS Details

  • SHA256: deed92dd33c75157c8e24bfa3c36584b8fd86830d6b0fa2adaec59b5ae031547
  • Pointer size: 132 Bytes
  • Size of remote file: 8.19 MB
example_neg/neg_not_fully_removed.png ADDED

Git LFS Details

  • SHA256: a834b7b00abdc47a292678991d094af4988532953215152b46f03ebeeec638ae
  • Pointer size: 133 Bytes
  • Size of remote file: 13.6 MB
example_pos/pos_flare.png ADDED

Git LFS Details

  • SHA256: dad3966a5f8b476b94488f01f0e8b9b62b71e3438bbebafdbb78fabb5eea15e8
  • Pointer size: 132 Bytes
  • Size of remote file: 5.19 MB
example_pos/pos_haze.png ADDED

Git LFS Details

  • SHA256: 16e0fdd103f23f25e8496800ebccdb5e1937c6b6430517a63a38eff3c7d49b76
  • Pointer size: 133 Bytes
  • Size of remote file: 10.1 MB
example_pos/pos_ref.png ADDED

Git LFS Details

  • SHA256: 0ce672577637744a76477ec2d1af1111d69663bbd9abbc37d8306733c6036c44
  • Pointer size: 132 Bytes
  • Size of remote file: 7.96 MB
example_pos/pos_shadow.png ADDED

Git LFS Details

  • SHA256: 9036428c27886ab924741d9569cf6d7cf9351a9278ede682f5c8ef1bb00b5755
  • Pointer size: 132 Bytes
  • Size of remote file: 9.07 MB