dreamlessx commited on
Commit
392e60f
·
verified ·
1 Parent(s): 2905c51

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +229 -0
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LandmarkDiff Hugging Face Spaces Demo - TPS-only (CPU)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cv2
6
+ import gradio as gr
7
+ import numpy as np
8
+
9
+ from landmarkdiff.landmarks import extract_landmarks, render_landmark_image
10
+ from landmarkdiff.conditioning import render_wireframe
11
+ from landmarkdiff.manipulation import apply_procedure_preset, PROCEDURE_LANDMARKS
12
+ from landmarkdiff.masking import generate_surgical_mask
13
+
14
+
15
+ def warp_image_tps(image, src_pts, dst_pts):
16
+ """Thin-plate spline warp (CPU only)."""
17
+ from landmarkdiff.synthetic.tps_warp import warp_image_tps as _warp
18
+ return _warp(image, src_pts, dst_pts)
19
+
20
+
21
+ def mask_composite(warped, original, mask):
22
+ """Alpha blend warped into original using mask."""
23
+ mask_3 = np.stack([mask] * 3, axis=-1) if mask.ndim == 2 else mask
24
+ return (warped * mask_3 + original * (1.0 - mask_3)).astype(np.uint8)
25
+
26
+
27
+ PROCEDURES = list(PROCEDURE_LANDMARKS.keys())
28
+
29
+
30
+ def process_image(image_rgb, procedure, intensity):
31
+ """Process a single image through the TPS pipeline."""
32
+ if image_rgb is None:
33
+ blank = np.zeros((512, 512, 3), dtype=np.uint8)
34
+ return blank, blank, blank, blank, "Upload a face photo to begin."
35
+
36
+ image_bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
37
+ image_bgr = cv2.resize(image_bgr, (512, 512))
38
+ image_rgb_512 = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
39
+
40
+ face = extract_landmarks(image_bgr)
41
+ if face is None:
42
+ return image_rgb_512, image_rgb_512, image_rgb_512, image_rgb_512, "No face detected. Try a clearer photo with good lighting."
43
+
44
+ # Manipulate landmarks
45
+ manipulated = apply_procedure_preset(face, procedure, float(intensity), image_size=512)
46
+
47
+ # Generate wireframe
48
+ wireframe = render_wireframe(manipulated, (512, 512))
49
+ wireframe_rgb = cv2.cvtColor(wireframe, cv2.COLOR_BGR2RGB) if wireframe.ndim == 3 else wireframe
50
+
51
+ # Generate mask
52
+ mask = generate_surgical_mask(face, procedure, 512, 512)
53
+ mask_vis = (mask * 255).astype(np.uint8)
54
+
55
+ # TPS warp + composite
56
+ warped = warp_image_tps(image_bgr, face.pixel_coords, manipulated.pixel_coords)
57
+ composited = mask_composite(warped, image_bgr, mask)
58
+ composited_rgb = cv2.cvtColor(composited, cv2.COLOR_BGR2RGB)
59
+
60
+ # Side by side
61
+ side_by_side = np.hstack([image_rgb_512, composited_rgb])
62
+
63
+ # Displacement stats
64
+ displacement = np.mean(np.linalg.norm(
65
+ manipulated.pixel_coords - face.pixel_coords, axis=1
66
+ ))
67
+
68
+ info = (
69
+ f"Procedure: {procedure}\n"
70
+ f"Intensity: {intensity:.0f}%\n"
71
+ f"Landmarks: {len(face.landmarks)}\n"
72
+ f"Avg displacement: {displacement:.1f} px\n"
73
+ f"Confidence: {face.confidence:.2f}\n"
74
+ f"Mode: TPS (CPU)"
75
+ )
76
+
77
+ return wireframe_rgb, mask_vis, composited_rgb, side_by_side, info
78
+
79
+
80
+ def compare_procedures(image_rgb, intensity):
81
+ """Compare all procedures at the same intensity."""
82
+ if image_rgb is None:
83
+ blank = np.zeros((512, 512, 3), dtype=np.uint8)
84
+ return [blank] * len(PROCEDURES)
85
+
86
+ image_bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
87
+ image_bgr = cv2.resize(image_bgr, (512, 512))
88
+
89
+ face = extract_landmarks(image_bgr)
90
+ if face is None:
91
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
92
+ return [rgb] * len(PROCEDURES)
93
+
94
+ results = []
95
+ for proc in PROCEDURES:
96
+ manip = apply_procedure_preset(face, proc, float(intensity), image_size=512)
97
+ mask = generate_surgical_mask(face, proc, 512, 512)
98
+ warped = warp_image_tps(image_bgr, face.pixel_coords, manip.pixel_coords)
99
+ comp = mask_composite(warped, image_bgr, mask)
100
+ results.append(cv2.cvtColor(comp, cv2.COLOR_BGR2RGB))
101
+
102
+ return results
103
+
104
+
105
+ def intensity_sweep(image_rgb, procedure):
106
+ """Generate intensity sweep from 0 to 100."""
107
+ if image_rgb is None:
108
+ return []
109
+
110
+ image_bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
111
+ image_bgr = cv2.resize(image_bgr, (512, 512))
112
+
113
+ face = extract_landmarks(image_bgr)
114
+ if face is None:
115
+ return []
116
+
117
+ steps = [0, 20, 40, 60, 80, 100]
118
+ results = []
119
+ for val in steps:
120
+ if val == 0:
121
+ results.append((cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB), "0%"))
122
+ continue
123
+ manip = apply_procedure_preset(face, procedure, float(val), image_size=512)
124
+ mask = generate_surgical_mask(face, procedure, 512, 512)
125
+ warped = warp_image_tps(image_bgr, face.pixel_coords, manip.pixel_coords)
126
+ comp = mask_composite(warped, image_bgr, mask)
127
+ results.append((cv2.cvtColor(comp, cv2.COLOR_BGR2RGB), f"{val}%"))
128
+
129
+ return results
130
+
131
+
132
+ with gr.Blocks(
133
+ title="LandmarkDiff - Surgical Outcome Prediction",
134
+ theme=gr.themes.Soft(),
135
+ ) as demo:
136
+ gr.Markdown(
137
+ "# LandmarkDiff\n"
138
+ "**Anatomically-conditioned facial surgery outcome prediction**\n\n"
139
+ "Upload a face photo, select a procedure, and adjust intensity. "
140
+ "This demo uses TPS warping (CPU) for real-time preview. "
141
+ "GPU-accelerated ControlNet/img2img modes are available in the full package.\n\n"
142
+ "[GitHub](https://github.com/dreamlessx/LandmarkDiff-public) | "
143
+ "[Paper](https://github.com/dreamlessx/LandmarkDiff-public/tree/main/paper)"
144
+ )
145
+
146
+ with gr.Tab("Single Procedure"):
147
+ with gr.Row():
148
+ with gr.Column(scale=1):
149
+ input_image = gr.Image(label="Upload Face Photo", type="numpy", height=350)
150
+ procedure = gr.Radio(
151
+ choices=PROCEDURES,
152
+ value="rhinoplasty",
153
+ label="Surgical Procedure",
154
+ )
155
+ intensity = gr.Slider(
156
+ minimum=0, maximum=100, value=50, step=1,
157
+ label="Intensity (%)",
158
+ info="0 = no change, 100 = maximum effect",
159
+ )
160
+ run_btn = gr.Button("Generate Preview", variant="primary", size="lg")
161
+ info_box = gr.Textbox(label="Info", lines=6, interactive=False)
162
+
163
+ with gr.Column(scale=2):
164
+ with gr.Row():
165
+ out_wireframe = gr.Image(label="Deformed Wireframe", height=256)
166
+ out_mask = gr.Image(label="Surgical Mask", height=256)
167
+ with gr.Row():
168
+ out_result = gr.Image(label="Predicted Result", height=256)
169
+ out_sidebyside = gr.Image(label="Before / After", height=256)
170
+
171
+ run_btn.click(
172
+ fn=process_image,
173
+ inputs=[input_image, procedure, intensity],
174
+ outputs=[out_wireframe, out_mask, out_result, out_sidebyside, info_box],
175
+ )
176
+ for trigger in [procedure, intensity]:
177
+ trigger.change(
178
+ fn=process_image,
179
+ inputs=[input_image, procedure, intensity],
180
+ outputs=[out_wireframe, out_mask, out_result, out_sidebyside, info_box],
181
+ )
182
+
183
+ with gr.Tab("Compare Procedures"):
184
+ gr.Markdown("Compare all procedures side by side at the same intensity.")
185
+ with gr.Row():
186
+ with gr.Column(scale=1):
187
+ cmp_image = gr.Image(label="Upload Face Photo", type="numpy", height=300)
188
+ cmp_intensity = gr.Slider(0, 100, 50, step=1, label="Intensity (%)")
189
+ cmp_btn = gr.Button("Compare All", variant="primary", size="lg")
190
+ with gr.Column(scale=2):
191
+ cmp_outputs = []
192
+ rows_needed = (len(PROCEDURES) + 2) // 3
193
+ for row_idx in range(rows_needed):
194
+ with gr.Row():
195
+ for col_idx in range(3):
196
+ proc_idx = row_idx * 3 + col_idx
197
+ if proc_idx < len(PROCEDURES):
198
+ cmp_outputs.append(
199
+ gr.Image(label=PROCEDURES[proc_idx].replace("_", " ").title(), height=200)
200
+ )
201
+
202
+ cmp_btn.click(
203
+ fn=compare_procedures,
204
+ inputs=[cmp_image, cmp_intensity],
205
+ outputs=cmp_outputs,
206
+ )
207
+
208
+ with gr.Tab("Intensity Sweep"):
209
+ gr.Markdown("See how a procedure looks across intensity levels.")
210
+ with gr.Row():
211
+ with gr.Column(scale=1):
212
+ sweep_image = gr.Image(label="Upload Face Photo", type="numpy", height=300)
213
+ sweep_procedure = gr.Radio(
214
+ choices=PROCEDURES,
215
+ value="rhinoplasty",
216
+ label="Procedure",
217
+ )
218
+ sweep_btn = gr.Button("Generate Sweep", variant="primary", size="lg")
219
+ with gr.Column(scale=2):
220
+ sweep_gallery = gr.Gallery(label="Intensity Sweep (0% - 100%)", columns=3, height=400)
221
+
222
+ sweep_btn.click(
223
+ fn=intensity_sweep,
224
+ inputs=[sweep_image, sweep_procedure],
225
+ outputs=[sweep_gallery],
226
+ )
227
+
228
+ if __name__ == "__main__":
229
+ demo.launch()