painter3000 commited on
Commit
d233634
·
verified ·
1 Parent(s): ac4d3bc

Update app.py

Browse files

- New Version with Up- and Download Fotoset

Files changed (1) hide show
  1. app.py +427 -93
app.py CHANGED
@@ -10,13 +10,25 @@ import uuid
10
  import subprocess
11
  from glob import glob
12
  from huggingface_hub import snapshot_download
 
 
 
 
 
 
 
13
 
14
- # Download models
15
  os.makedirs("ckpts", exist_ok=True)
16
- snapshot_download(repo_id="pengHTYX/PSHuman_Unclip_768_6views", local_dir="./ckpts")
 
 
 
17
 
18
  os.makedirs("smpl_related", exist_ok=True)
19
- snapshot_download(repo_id="fffiloni/PSHuman-SMPL-related", local_dir="./smpl_related")
 
 
 
20
 
21
  examples_folder = "examples"
22
  images_examples = [
@@ -25,39 +37,17 @@ images_examples = [
25
  if os.path.isfile(os.path.join(examples_folder, file))
26
  ]
27
 
28
-
29
- def find_primary_input_image(temp_dir: str) -> str:
30
- candidates = sorted(glob(os.path.join(temp_dir, "output_image_rmbg_*.png")))
31
- if candidates:
32
- return candidates[0]
33
- candidates = sorted(glob(os.path.join(temp_dir, "input_image_*.png")))
34
- if candidates:
35
- return candidates[0]
36
- raise gr.Error(f"Kein Eingabebild im Session-Ordner gefunden: {temp_dir}")
37
-
38
-
39
-
40
- def get_scene_name_from_temp_dir(temp_dir: str) -> str:
41
- image_path = find_primary_input_image(temp_dir)
42
- return os.path.splitext(os.path.basename(image_path))[0]
43
-
44
-
45
-
46
- def get_multiview_gallery_paths(temp_dir: str):
47
- scene = get_scene_name_from_temp_dir(temp_dir)
48
- edit_dir = os.path.join(temp_dir, "multiview", scene, "edit")
49
- raw_dir = os.path.join(temp_dir, "multiview", scene, "raw")
50
- source_dir = edit_dir if os.path.isdir(edit_dir) else raw_dir
51
- if not os.path.isdir(source_dir):
52
- return []
53
- return sorted(glob(os.path.join(source_dir, "*.png")))
54
 
55
 
 
 
 
56
 
57
  def remove_background(input_pil, remove_bg):
58
  temp_dir = tempfile.mkdtemp(prefix="pshuman_session_")
59
  unique_id = str(uuid.uuid4())
60
- image_path = os.path.join(temp_dir, f'input_image_{unique_id}.png')
61
 
62
  try:
63
  if isinstance(input_pil, Image.Image):
@@ -65,14 +55,15 @@ def remove_background(input_pil, remove_bg):
65
  else:
66
  image = Image.open(input_pil)
67
 
 
68
  image = image.transpose(Image.FLIP_LEFT_RIGHT)
69
  image.save(image_path)
70
  except Exception as e:
71
  shutil.rmtree(temp_dir, ignore_errors=True)
72
- raise gr.Error(f"Fehler beim Laden oder Speichern des Bildes: {str(e)}")
73
 
74
- if remove_bg:
75
- removed_bg_path = os.path.join(temp_dir, f'output_image_rmbg_{unique_id}.png')
76
  try:
77
  img = Image.open(image_path)
78
  result = remove(img)
@@ -81,20 +72,260 @@ def remove_background(input_pil, remove_bg):
81
  except Exception as e:
82
  shutil.rmtree(temp_dir, ignore_errors=True)
83
  raise gr.Error(f"Fehler bei der Hintergrundentfernung: {str(e)}")
 
84
  return removed_bg_path, temp_dir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- return image_path, temp_dir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
 
 
88
 
 
 
89
 
90
- def run_pshuman(temp_dir: str, run_mode: str):
 
 
 
 
 
 
 
 
 
 
 
91
  inference_config = "configs/inference-768-6view.yaml"
92
  pretrained_model = "./ckpts"
93
  crop_size = 740
94
  seed = 600
95
  num_views = 7
96
  save_mode = "rgb"
97
- multiview_dir = os.path.join(temp_dir, "multiview")
98
 
99
  subprocess.run(
100
  [
@@ -103,72 +334,142 @@ def run_pshuman(temp_dir: str, run_mode: str):
103
  f"pretrained_model_name_or_path={pretrained_model}",
104
  f"validation_dataset.crop_size={crop_size}",
105
  "with_smpl=false",
106
- f"validation_dataset.root_dir={temp_dir}",
107
  f"seed={seed}",
108
  f"num_views={num_views}",
109
  f"save_mode={save_mode}",
110
- f"run_mode={run_mode}",
111
  f"multiview_tmp_dir={multiview_dir}",
112
  "prefer_edited_views=true",
113
  ],
114
- check=True,
115
  )
116
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- def get_outputs_for_session(temp_dir: str):
120
- scene = get_scene_name_from_temp_dir(temp_dir)
121
- output_video = glob(os.path.join("out", scene, "*.mp4"))
122
- output_objects = glob(os.path.join("out", scene, "*.obj"))
123
- if len(output_video) < 1 or len(output_objects) < 2:
124
- raise gr.Error(f"Ausgabedateien für Szene '{scene}' wurden nicht vollständig gefunden.")
125
- return output_video, output_objects
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  @spaces.GPU(duration=140)
129
- def generate_views(input_pil, remove_bg, progress=gr.Progress(track_tqdm=True)):
130
  torch.cuda.empty_cache()
131
 
132
- removed_bg_path, temp_dir = remove_background(input_pil, remove_bg)
133
 
134
  try:
135
- run_pshuman(temp_dir, run_mode="generate")
136
- gallery_paths = get_multiview_gallery_paths(temp_dir)
137
- if not gallery_paths:
138
- raise gr.Error("Es wurden keine Multiview-Bilder erzeugt.")
139
  status = (
140
- "Stufe 1 abgeschlossen. Die Multiview-Bilder wurden erzeugt und im Session-Ordner gespeichert. "
141
- "Du kannst die Dateien jetzt im Ordner bearbeiten und danach Stufe 2 starten.\n\n"
142
- f"Session-Ordner: {temp_dir}"
 
143
  )
144
- return gallery_paths, temp_dir, status
 
145
  except subprocess.CalledProcessError as e:
146
- shutil.rmtree(temp_dir, ignore_errors=True)
147
  raise gr.Error(f"Fehler während der Multiview-Erzeugung: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
 
150
  @spaces.GPU(duration=140)
151
- def continue_reconstruction(session_dir, keep_session_files=False, progress=gr.Progress(track_tqdm=True)):
152
- torch.cuda.empty_cache()
 
153
 
154
- if not session_dir or not os.path.isdir(session_dir):
155
- raise gr.Error("Es wurde kein gültiger Session-Ordner übergeben.")
156
 
157
  try:
158
- run_pshuman(session_dir, run_mode="reconstruct")
159
- output_video, output_objects = get_outputs_for_session(session_dir)
160
- status = f"Stufe 2 abgeschlossen. Rekonstruktion für Session '{session_dir}' wurde erzeugt."
161
 
162
- if not keep_session_files:
 
 
163
  shutil.rmtree(session_dir, ignore_errors=True)
164
- status += " Session-Ordner wurde danach gelöscht."
165
 
166
- torch.cuda.empty_cache()
167
- return output_video[0], output_objects[0], output_objects[1], status
168
  except subprocess.CalledProcessError as e:
169
  raise gr.Error(f"Fehler während der Rekonstruktion: {str(e)}")
 
 
 
 
 
 
 
 
170
 
171
 
 
 
 
 
172
  css = """
173
  div#col-container{
174
  margin: 0 auto;
@@ -179,15 +480,25 @@ div#video-out-elm{
179
  }
180
  """
181
 
182
-
183
  def gradio_interface():
184
  with gr.Blocks(css=css) as app:
185
  with gr.Column(elem_id="col-container"):
186
- gr.Markdown("# PSHuman – Zweistufige Pipeline mit editierbaren Multiview-Bildern")
187
- gr.Markdown(
188
- "**Stufe 1:** Eingabebild vorbereiten und Multiview-Bilder erzeugen. \n"
189
- "**Stufe 2:** Die gespeicherten Multiview-Bilder aus dem Session-Ordner wieder einlesen und die Rekonstruktion fortsetzen."
190
- )
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  with gr.Group():
193
  with gr.Row():
@@ -196,17 +507,22 @@ def gradio_interface():
196
  label="Image input",
197
  type="pil",
198
  image_mode="RGBA",
199
- height=480,
200
  )
201
- remove_bg = gr.Checkbox(label="Need to remove BG ?", value=False)
202
- generate_button = gr.Button("1) Multiview erzeugen")
203
- continue_button = gr.Button("2) Rekonstruktion fortsetzen")
204
- keep_session_files = gr.Checkbox(label="Session-Ordner nach Stufe 2 behalten", value=False)
205
- session_dir = gr.Textbox(label="Session-Ordner", interactive=True)
206
- status_box = gr.Textbox(label="Status", lines=6, interactive=False)
 
 
 
207
 
208
  with gr.Column(scale=4):
209
- multiview_gallery = gr.Gallery(label="Multiview-Bilder", columns=4, height=420)
 
 
210
  output_video = gr.Video(label="Output Video", elem_id="video-out-elm")
211
  with gr.Row():
212
  output_object_mesh = gr.Model3D(label=".OBJ Mesh", height=240)
@@ -215,20 +531,38 @@ def gradio_interface():
215
  gr.Examples(
216
  examples=examples_folder,
217
  inputs=[input_image],
218
- examples_per_page=11,
219
  )
220
 
221
- generate_button.click(
222
- generate_views,
223
- inputs=[input_image, remove_bg],
224
- outputs=[multiview_gallery, session_dir, status_box],
225
- )
226
 
227
- continue_button.click(
228
- continue_reconstruction,
229
- inputs=[session_dir, keep_session_files],
230
- outputs=[output_video, output_object_mesh, output_object_color, status_box],
231
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  return app
234
 
 
10
  import subprocess
11
  from glob import glob
12
  from huggingface_hub import snapshot_download
13
+ import zipfile
14
+ import json
15
+ from pathlib import Path
16
+
17
+ # ============================================================
18
+ # Model download
19
+ # ============================================================
20
 
 
21
  os.makedirs("ckpts", exist_ok=True)
22
+ snapshot_download(
23
+ repo_id="pengHTYX/PSHuman_Unclip_768_6views",
24
+ local_dir="./ckpts"
25
+ )
26
 
27
  os.makedirs("smpl_related", exist_ok=True)
28
+ snapshot_download(
29
+ repo_id="fffiloni/PSHuman-SMPL-related",
30
+ local_dir="./smpl_related"
31
+ )
32
 
33
  examples_folder = "examples"
34
  images_examples = [
 
37
  if os.path.isfile(os.path.join(examples_folder, file))
38
  ]
39
 
40
+ ALLOWED_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
 
43
+ # ============================================================
44
+ # Helper: session + image prep
45
+ # ============================================================
46
 
47
  def remove_background(input_pil, remove_bg):
48
  temp_dir = tempfile.mkdtemp(prefix="pshuman_session_")
49
  unique_id = str(uuid.uuid4())
50
+ image_path = os.path.join(temp_dir, f"input_image_{unique_id}.png")
51
 
52
  try:
53
  if isinstance(input_pil, Image.Image):
 
55
  else:
56
  image = Image.open(input_pil)
57
 
58
+ # Keep original PSHuman behavior
59
  image = image.transpose(Image.FLIP_LEFT_RIGHT)
60
  image.save(image_path)
61
  except Exception as e:
62
  shutil.rmtree(temp_dir, ignore_errors=True)
63
+ raise gr.Error(f"Fehler beim Laden/Speichern des Bildes: {str(e)}")
64
 
65
+ if remove_bg is True:
66
+ removed_bg_path = os.path.join(temp_dir, f"output_image_rmbg_{unique_id}.png")
67
  try:
68
  img = Image.open(image_path)
69
  result = remove(img)
 
72
  except Exception as e:
73
  shutil.rmtree(temp_dir, ignore_errors=True)
74
  raise gr.Error(f"Fehler bei der Hintergrundentfernung: {str(e)}")
75
+
76
  return removed_bg_path, temp_dir
77
+ else:
78
+ return image_path, temp_dir
79
+
80
+
81
+ # ============================================================
82
+ # Helper: multiview paths
83
+ # ============================================================
84
+
85
+ def get_multiview_root(session_dir: str) -> Path:
86
+ return Path(session_dir) / "multiview"
87
+
88
+
89
+ def find_single_scene_dir(session_dir: str) -> Path:
90
+ mv_root = get_multiview_root(session_dir)
91
+ if not mv_root.exists():
92
+ raise gr.Error(f"Kein multiview-Ordner gefunden: {mv_root}")
93
+
94
+ scene_dirs = [p for p in mv_root.iterdir() if p.is_dir()]
95
+ if not scene_dirs:
96
+ raise gr.Error(f"Keine Szene im multiview-Ordner gefunden: {mv_root}")
97
+ if len(scene_dirs) > 1:
98
+ raise gr.Error("Mehrere Szenen gefunden. Diese App erwartet aktuell genau eine Szene pro Session.")
99
+
100
+ return scene_dirs[0]
101
+
102
+
103
+ def get_edit_dir(session_dir: str) -> Path:
104
+ return find_single_scene_dir(session_dir) / "edit"
105
+
106
+
107
+ def get_raw_dir(session_dir: str) -> Path:
108
+ return find_single_scene_dir(session_dir) / "raw"
109
+
110
+
111
+ def list_gallery_images(session_dir: str):
112
+ if not session_dir or not Path(session_dir).exists():
113
+ return []
114
+
115
+ mv_root = get_multiview_root(session_dir)
116
+ if not mv_root.exists():
117
+ return []
118
+
119
+ try:
120
+ scene_dir = find_single_scene_dir(session_dir)
121
+ edit_dir = scene_dir / "edit"
122
+ if not edit_dir.exists():
123
+ return []
124
+ return sorted(str(p) for p in edit_dir.glob("color_*") if p.is_file())
125
+ except Exception:
126
+ return []
127
+
128
+
129
+ # ============================================================
130
+ # ZIP helpers
131
+ # ============================================================
132
+
133
+ def create_edit_zip(session_dir: str) -> str:
134
+ scene_dir = find_single_scene_dir(session_dir)
135
+ edit_dir = scene_dir / "edit"
136
+ meta_path = scene_dir / "meta.json"
137
+
138
+ if not edit_dir.exists():
139
+ raise gr.Error(f"Kein edit-Ordner gefunden: {edit_dir}")
140
+
141
+ zip_path = Path(session_dir) / f"{scene_dir.name}_multiview_edit.zip"
142
+
143
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
144
+ for file_path in sorted(edit_dir.iterdir()):
145
+ if file_path.is_file():
146
+ zf.write(file_path, arcname=file_path.name)
147
+
148
+ if meta_path.exists():
149
+ zf.write(meta_path, arcname="meta.json")
150
+
151
+ return str(zip_path)
152
+
153
+
154
+ def inspect_edit_set(edit_dir: Path) -> dict:
155
+ existing_files = {p.name for p in edit_dir.iterdir() if p.is_file()}
156
+
157
+ color_files = sorted([
158
+ f for f in existing_files
159
+ if f.startswith("color_") and Path(f).suffix.lower() in ALLOWED_IMAGE_EXTENSIONS
160
+ ])
161
+ normal_files = sorted([
162
+ f for f in existing_files
163
+ if f.startswith("normal_") and Path(f).suffix.lower() in ALLOWED_IMAGE_EXTENSIONS
164
+ ])
165
+
166
+ if color_files:
167
+ expected_indices = sorted([f.split("_")[1].split(".")[0] for f in color_files])
168
+ elif normal_files:
169
+ expected_indices = sorted([f.split("_")[1].split(".")[0] for f in normal_files])
170
+ else:
171
+ expected_indices = []
172
+
173
+ expected_color_names = {f"color_{idx}.png" for idx in expected_indices}
174
+ expected_normal_names = {f"normal_{idx}.png" for idx in expected_indices}
175
+
176
+ found_colors = {f for f in existing_files if f.startswith("color_")}
177
+ found_normals = {f for f in existing_files if f.startswith("normal_")}
178
+
179
+ missing_colors = sorted(expected_color_names - found_colors) if expected_indices else []
180
+ missing_normals = sorted(expected_normal_names - found_normals) if expected_indices else []
181
+
182
+ has_full_colors = bool(expected_indices) and len(missing_colors) == 0
183
+ has_full_normals = bool(expected_indices) and len(missing_normals) == 0
184
+
185
+ if has_full_colors and has_full_normals:
186
+ state = "READY"
187
+ elif has_full_colors and not has_full_normals:
188
+ state = "NEEDS_NORMALS"
189
+ else:
190
+ state = "INVALID"
191
+
192
+ return {
193
+ "state": state,
194
+ "expected_indices": expected_indices,
195
+ "found_colors": sorted(found_colors),
196
+ "found_normals": sorted(found_normals),
197
+ "missing_colors": missing_colors,
198
+ "missing_normals": missing_normals,
199
+ "has_full_colors": has_full_colors,
200
+ "has_full_normals": has_full_normals,
201
+ }
202
+
203
+
204
+ def validate_uploaded_zip_structure(zf: zipfile.ZipFile) -> list[str]:
205
+ file_names = []
206
+
207
+ for member in zf.infolist():
208
+ if member.is_dir():
209
+ continue
210
+
211
+ member_name = member.filename.replace("\\", "/")
212
+
213
+ if "/" in member_name:
214
+ raise gr.Error(f"ZIP darf keine Unterordner enthalten: {member_name}")
215
+
216
+ suffix = Path(member_name).suffix.lower()
217
+ stem = Path(member_name).stem
218
+
219
+ if member_name == "meta.json":
220
+ file_names.append(member_name)
221
+ continue
222
+
223
+ if suffix not in ALLOWED_IMAGE_EXTENSIONS:
224
+ raise gr.Error(f"Nicht erlaubter Dateityp im ZIP: {member_name}")
225
+
226
+ if not (stem.startswith("color_") or stem.startswith("normal_")):
227
+ raise gr.Error(f"Ungültiger Dateiname im ZIP: {member_name}")
228
+
229
+ file_names.append(member_name)
230
+
231
+ return file_names
232
+
233
+
234
+ def overwrite_edit_set_from_zip(zip_file_path: str, session_dir: str) -> tuple[str, list[str]]:
235
+ scene_dir = find_single_scene_dir(session_dir)
236
+ edit_dir = scene_dir / "edit"
237
+ edit_dir.mkdir(parents=True, exist_ok=True)
238
+
239
+ with zipfile.ZipFile(zip_file_path, "r") as zf:
240
+ uploaded_names = validate_uploaded_zip_structure(zf)
241
+
242
+ has_uploaded_colors = any(name.startswith("color_") for name in uploaded_names)
243
+ has_uploaded_normals = any(name.startswith("normal_") for name in uploaded_names)
244
+
245
+ tmp_extract_dir = Path(session_dir) / "_upload_tmp"
246
+ if tmp_extract_dir.exists():
247
+ shutil.rmtree(tmp_extract_dir)
248
+ tmp_extract_dir.mkdir(parents=True, exist_ok=True)
249
+
250
+ try:
251
+ zf.extractall(tmp_extract_dir)
252
+
253
+ # If only new colors are uploaded, invalidate old normals.
254
+ if has_uploaded_colors and not has_uploaded_normals:
255
+ for p in edit_dir.glob("normal_*"):
256
+ if p.is_file():
257
+ p.unlink()
258
+
259
+ for name in uploaded_names:
260
+ if name == "meta.json":
261
+ continue
262
+ src = tmp_extract_dir / name
263
+ dst = edit_dir / name
264
+ shutil.copy2(src, dst)
265
+
266
+ finally:
267
+ if tmp_extract_dir.exists():
268
+ shutil.rmtree(tmp_extract_dir)
269
+
270
+ report = inspect_edit_set(edit_dir)
271
 
272
+ if report["state"] == "READY":
273
+ status = (
274
+ "Upload erfolgreich.\n"
275
+ "Komplettes Fotoset erkannt:\n"
276
+ f"- Colors: {len(report['found_colors'])}\n"
277
+ f"- Normalmaps: {len(report['found_normals'])}\n"
278
+ "Rekonstruktion kann direkt fortgesetzt werden."
279
+ )
280
+ elif report["state"] == "NEEDS_NORMALS":
281
+ status = (
282
+ "Upload erfolgreich.\n"
283
+ "Nur vollständige Color-Ansichten erkannt, aber keine vollständigen Normalmaps.\n"
284
+ "Alte Normalmaps wurden verworfen bzw. als ungültig behandelt.\n"
285
+ "Normalmaps müssen vor der Rekonstruktion neu berechnet werden."
286
+ )
287
+ else:
288
+ status = (
289
+ "Upload unvollständig oder ungültig.\n"
290
+ f"Fehlende Colors: {report['missing_colors']}\n"
291
+ f"Fehlende Normalmaps: {report['missing_normals']}\n"
292
+ "Bitte ein vollständiges Fotoset hochladen."
293
+ )
294
+
295
+ preview_paths = sorted(str(p) for p in edit_dir.glob("color_*"))
296
+ return status, preview_paths
297
+
298
+
299
+ def ensure_ready_for_reconstruction(session_dir: str) -> str:
300
+ scene_dir = find_single_scene_dir(session_dir)
301
+ edit_dir = scene_dir / "edit"
302
+ report = inspect_edit_set(edit_dir)
303
 
304
+ if report["state"] == "READY":
305
+ return "READY"
306
 
307
+ if report["state"] == "NEEDS_NORMALS":
308
+ raise gr.Error("Es sind nur Color-Bilder vorhanden. Bitte zuerst die Normalmaps neu berechnen.")
309
 
310
+ raise gr.Error(
311
+ "Das Fotoset ist unvollständig. "
312
+ f"Fehlende Colors: {report['missing_colors']} | "
313
+ f"Fehlende Normalmaps: {report['missing_normals']}"
314
+ )
315
+
316
+
317
+ # ============================================================
318
+ # Inference stage calls
319
+ # ============================================================
320
+
321
+ def run_generate_multiview(session_dir: str):
322
  inference_config = "configs/inference-768-6view.yaml"
323
  pretrained_model = "./ckpts"
324
  crop_size = 740
325
  seed = 600
326
  num_views = 7
327
  save_mode = "rgb"
328
+ multiview_dir = str(get_multiview_root(session_dir))
329
 
330
  subprocess.run(
331
  [
 
334
  f"pretrained_model_name_or_path={pretrained_model}",
335
  f"validation_dataset.crop_size={crop_size}",
336
  "with_smpl=false",
337
+ f"validation_dataset.root_dir={session_dir}",
338
  f"seed={seed}",
339
  f"num_views={num_views}",
340
  f"save_mode={save_mode}",
341
+ "run_mode=generate",
342
  f"multiview_tmp_dir={multiview_dir}",
343
  "prefer_edited_views=true",
344
  ],
345
+ check=True
346
  )
347
 
348
 
349
+ def run_reconstruct_from_session(session_dir: str):
350
+ inference_config = "configs/inference-768-6view.yaml"
351
+ pretrained_model = "./ckpts"
352
+ crop_size = 740
353
+ seed = 600
354
+ num_views = 7
355
+ save_mode = "rgb"
356
+ multiview_dir = str(get_multiview_root(session_dir))
357
+
358
+ subprocess.run(
359
+ [
360
+ "python", "inference.py",
361
+ "--config", inference_config,
362
+ f"pretrained_model_name_or_path={pretrained_model}",
363
+ f"validation_dataset.crop_size={crop_size}",
364
+ "with_smpl=false",
365
+ f"validation_dataset.root_dir={session_dir}",
366
+ f"seed={seed}",
367
+ f"num_views={num_views}",
368
+ f"save_mode={save_mode}",
369
+ "run_mode=reconstruct",
370
+ f"multiview_tmp_dir={multiview_dir}",
371
+ "prefer_edited_views=true",
372
+ ],
373
+ check=True
374
+ )
375
+
376
 
377
+ def collect_outputs_from_session(session_dir: str):
378
+ scene_dir = find_single_scene_dir(session_dir)
379
+ scene_name = scene_dir.name
 
 
 
 
380
 
381
+ output_video = glob(os.path.join("out", scene_name, "*.mp4"))
382
+ output_objects = glob(os.path.join("out", scene_name, "*.obj"))
383
+
384
+ video = output_video[0] if output_video else None
385
+ mesh = output_objects[0] if len(output_objects) > 0 else None
386
+ mesh_color = output_objects[1] if len(output_objects) > 1 else None
387
+
388
+ return video, mesh, mesh_color
389
+
390
+
391
+ # ============================================================
392
+ # UI callbacks
393
+ # ============================================================
394
 
395
  @spaces.GPU(duration=140)
396
+ def process_generate(input_pil, remove_bg):
397
  torch.cuda.empty_cache()
398
 
399
+ removed_bg_path, session_dir = remove_background(input_pil, remove_bg)
400
 
401
  try:
402
+ run_generate_multiview(session_dir)
403
+ gallery = list_gallery_images(session_dir)
404
+
 
405
  status = (
406
+ "Stufe 1 abgeschlossen.\n"
407
+ "Multiview-Bilder wurden erzeugt und im Session-Ordner gespeichert.\n"
408
+ "Du kannst jetzt das Fotoset herunterladen, extern bearbeiten und wieder hochladen.\n"
409
+ "Session: " + session_dir
410
  )
411
+
412
+ return session_dir, status, gallery
413
  except subprocess.CalledProcessError as e:
414
+ shutil.rmtree(session_dir, ignore_errors=True)
415
  raise gr.Error(f"Fehler während der Multiview-Erzeugung: {str(e)}")
416
+ finally:
417
+ torch.cuda.empty_cache()
418
+
419
+
420
+ def process_download_set(session_dir):
421
+ if not session_dir or not Path(session_dir).exists():
422
+ raise gr.Error("Kein gültiger Session-Ordner vorhanden.")
423
+ zip_path = create_edit_zip(session_dir)
424
+ status = f"Fotoset als ZIP erstellt: {zip_path}"
425
+ return zip_path, status
426
+
427
+
428
+ def process_upload_set(upload_zip, session_dir):
429
+ if upload_zip is None:
430
+ raise gr.Error("Bitte zuerst eine ZIP-Datei auswählen.")
431
+ if not session_dir or not Path(session_dir).exists():
432
+ raise gr.Error("Kein gültiger Session-Ordner vorhanden.")
433
+
434
+ status, gallery = overwrite_edit_set_from_zip(upload_zip.name, session_dir)
435
+ return status, gallery
436
 
437
 
438
  @spaces.GPU(duration=140)
439
+ def process_reconstruct(session_dir, keep_session):
440
+ if not session_dir or not Path(session_dir).exists():
441
+ raise gr.Error("Kein gültiger Session-Ordner vorhanden.")
442
 
443
+ torch.cuda.empty_cache()
 
444
 
445
  try:
446
+ ensure_ready_for_reconstruction(session_dir)
447
+ run_reconstruct_from_session(session_dir)
448
+ video, mesh, mesh_color = collect_outputs_from_session(session_dir)
449
 
450
+ status = "Stufe 2 abgeschlossen. Rekonstruktion erfolgreich."
451
+
452
+ if not keep_session:
453
  shutil.rmtree(session_dir, ignore_errors=True)
454
+ session_dir = ""
455
 
456
+ return status, video, mesh, mesh_color, session_dir
 
457
  except subprocess.CalledProcessError as e:
458
  raise gr.Error(f"Fehler während der Rekonstruktion: {str(e)}")
459
+ finally:
460
+ torch.cuda.empty_cache()
461
+
462
+
463
+ def process_clear_session(session_dir):
464
+ if session_dir and Path(session_dir).exists():
465
+ shutil.rmtree(session_dir, ignore_errors=True)
466
+ return "", "Session gelöscht.", [], None, None, None, None
467
 
468
 
469
+ # ============================================================
470
+ # UI
471
+ # ============================================================
472
+
473
  css = """
474
  div#col-container{
475
  margin: 0 auto;
 
480
  }
481
  """
482
 
 
483
  def gradio_interface():
484
  with gr.Blocks(css=css) as app:
485
  with gr.Column(elem_id="col-container"):
486
+ gr.Markdown("# PSHuman 2.0 Zwei-Stufen-Pipeline mit Multiview-Export/Import")
487
+ gr.HTML("""
488
+ <div style="display:flex;column-gap:4px;flex-wrap:wrap;">
489
+ <a href="https://github.com/pengHTYX/PSHuman">
490
+ <img src='https://img.shields.io/badge/GitHub-Repo-blue'>
491
+ </a>
492
+ <a href="https://penghtyx.github.io/PSHuman/">
493
+ <img src='https://img.shields.io/badge/Project-Page-green'>
494
+ </a>
495
+ <a href="https://arxiv.org/pdf/2409.10141">
496
+ <img src='https://img.shields.io/badge/ArXiv-Paper-red'>
497
+ </a>
498
+ </div>
499
+ """)
500
+
501
+ session_dir_box = gr.Textbox(label="Session-Ordner", interactive=False)
502
 
503
  with gr.Group():
504
  with gr.Row():
 
507
  label="Image input",
508
  type="pil",
509
  image_mode="RGBA",
510
+ height=480
511
  )
512
+ remove_bg = gr.Checkbox(label="Need to remove BG?", value=False)
513
+ keep_session = gr.Checkbox(label="Session nach Stufe 2 behalten", value=True)
514
+
515
+ btn_generate = gr.Button("1) Multiview erzeugen")
516
+ btn_download = gr.Button("2) Fotoset herunterladen")
517
+ upload_zip = gr.File(label="3) Bearbeitetes Fotoset hochladen", file_types=[".zip"])
518
+ btn_upload = gr.Button("4) Upload prüfen und Bilder überschreiben")
519
+ btn_reconstruct = gr.Button("5) Rekonstruktion fortsetzen")
520
+ btn_clear = gr.Button("Session löschen")
521
 
522
  with gr.Column(scale=4):
523
+ status_box = gr.Textbox(label="Status", lines=8)
524
+ multiview_gallery = gr.Gallery(label="Multiview Edit Set", columns=3, rows=2, height=420)
525
+ download_file = gr.File(label="Download ZIP")
526
  output_video = gr.Video(label="Output Video", elem_id="video-out-elm")
527
  with gr.Row():
528
  output_object_mesh = gr.Model3D(label=".OBJ Mesh", height=240)
 
531
  gr.Examples(
532
  examples=examples_folder,
533
  inputs=[input_image],
534
+ examples_per_page=11
535
  )
536
 
537
+ btn_generate.click(
538
+ process_generate,
539
+ inputs=[input_image, remove_bg],
540
+ outputs=[session_dir_box, status_box, multiview_gallery]
541
+ )
542
 
543
+ btn_download.click(
544
+ process_download_set,
545
+ inputs=[session_dir_box],
546
+ outputs=[download_file, status_box]
547
+ )
548
+
549
+ btn_upload.click(
550
+ process_upload_set,
551
+ inputs=[upload_zip, session_dir_box],
552
+ outputs=[status_box, multiview_gallery]
553
+ )
554
+
555
+ btn_reconstruct.click(
556
+ process_reconstruct,
557
+ inputs=[session_dir_box, keep_session],
558
+ outputs=[status_box, output_video, output_object_mesh, output_object_color, session_dir_box]
559
+ )
560
+
561
+ btn_clear.click(
562
+ process_clear_session,
563
+ inputs=[session_dir_box],
564
+ outputs=[session_dir_box, status_box, multiview_gallery, download_file, output_video, output_object_mesh, output_object_color]
565
+ )
566
 
567
  return app
568