vgvm commited on
Commit
38c3b1f
1 Parent(s): b9ca9c7

ui cleanup

Browse files
Files changed (1) hide show
  1. app.py +108 -102
app.py CHANGED
@@ -22,40 +22,15 @@ class face_image_to_face_mesh:
22
  def __init__(self):
23
  self.zoe_me = True
24
  self.uvwrap = not True
25
- self.css = ("""
26
- #mesh-display-output {
27
- max-height: 44vh;
28
- max-width: 44vh;
29
- width:auto;
30
- height:auto
31
- }
32
- #img-display-output {
33
- max-height: 28vh;
34
- max-width: 28vh;
35
- width:auto;
36
- height:auto
37
- }
38
- """)
39
 
40
  def demo(self):
41
  if self.zoe_me:
42
  DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
43
  self.zoe = torch.hub.load('isl-org/ZoeDepth', "ZoeD_N", pretrained=True).to(DEVICE).eval()
44
 
45
- demo = gr.Blocks(css=self.css, cache_examples=True)
46
  with demo:
47
- gr.Markdown("""
48
- # Face Image to Face Quad Mesh
49
-
50
- Uses MediaPipe to detect a face in an image and convert it to a quad mesh.
51
- Saves to OBJ since gltf does not support quad faces. The 3d viewer has Y pointing the opposite direction from Blender, so ya hafta spin it.
52
-
53
- The face depth with Zoe can be a bit much and without it is a bit generic. In blender you can fix this just by snapping to the high poly model.
54
-
55
- Highly recommend running it locally. The 3D model has uv values in the faces, but you will have to make the mlt file manually at this point."
56
-
57
- Quick import result in examples/converted/movie-gallery.mp4 under files
58
- """)
59
 
60
  with gr.Row():
61
  with gr.Column():
@@ -73,70 +48,14 @@ class face_image_to_face_mesh:
73
  upload_image_btn = gr.Button(value="Detect faces")
74
  if self.zoe_me:
75
  with gr.Group():
76
- use_zoe = gr.Checkbox(label="Use ZoeDepth for Z", value=True)
77
- gr.Textbox(show_label=False, value="Override the MediaPipe depth with ZoeDepth.")
78
- zoe_scale = gr.Slider(label="Zoe Scale", value=1.44, minimum=0.0, maximum=3.3, step=0.1)
79
- gr.Textbox(show_label=False, value="How much to scale the ZoeDepth. 2x is pretty dramatic...")
80
  else:
81
  use_zoe = False
82
  zoe_scale = 0
83
  with gr.Group():
84
- min_detection_confidence = gr.Slider(label="Min detection confidence", value=0.5, minimum=0.0, maximum=1.0, step=0.01)
85
- gr.Textbox(show_label=False, value="Minimum confidence value ([0.0, 1.0]) from the face detection model for the detection to be considered successful.")
86
- with gr.Group():
87
- gr.Markdown(
88
- """
89
- # Using the Textured Mesh in Blender
90
-
91
- There a couple of annoying steps atm after you download the obj from the 3d viewer.
92
-
93
- You can use the script meshin-around.sh in the files section to do the conversion or manually:
94
-
95
- 1. edit the file and change the mtllib line to use fun.mtl
96
- 2. replace / delete all lines that start with 'f', eg :%s,^f.*,,
97
- 3. uncomment all the lines that start with '#f', eg: :%s,^#f,f,
98
- 4. save and exit
99
- 5. create fun.mtl to point to the texture like:
100
-
101
- ```
102
- newmtl MyMaterial
103
- map_Kd fun.png
104
- ```
105
-
106
- Make sure the obj, mtl and png are all in the same directory
107
-
108
- Now the import will have the texture data: File -> Import -> Wavefront (obj) -> fun.obj
109
-
110
- This is all a work around for a weird hf+gradios+babylonjs bug which seems to be related to the version
111
- of babylonjs being used... It works fine in a local babylonjs sandbox...
112
-
113
- # Suggested Workflows
114
-
115
- Here are some workflow ideas.
116
-
117
- ## retopologize high poly face mesh
118
-
119
- 1. sculpt high poly mesh in blender
120
- 2. snapshot the face
121
- 3. generate the mesh using the mediapipe stuff
122
- 4. import the low poly mediapipe face
123
- 5. snap the mesh to the high poly model
124
- 6. model the rest of the low poly model
125
- 7. bake the normal / etc maps to the low poly face model
126
- 8. it's just that easy 😛
127
-
128
- Ideally it would be a plugin...
129
-
130
- ## stable diffusion integration
131
-
132
- 1. generate a face in sd
133
- 2. generate the mesh
134
- 3. repose it and use it for further generation
135
-
136
- May need to expanded the generated mesh to cover more, maybe with
137
- <a href="https://github.com/shunsukesaito/PIFu" target="_blank">PIFu model</a>.
138
-
139
- """)
140
 
141
  with gr.Column():
142
  with gr.Group():
@@ -147,13 +66,13 @@ class face_image_to_face_mesh:
147
 
148
  upload_image_btn.click(
149
  fn=self.detect,
150
- inputs=[upload_image, min_detection_confidence,use_zoe,zoe_scale],
151
  outputs=[output_mesh, output_image, depth_image, num_faces_detected]
152
  )
153
  demo.launch()
154
 
155
 
156
- def detect(self, image, min_detection_confidence, use_zoe,zoe_scale):
157
  width = image.shape[1]
158
  height = image.shape[0]
159
  ratio = width / height
@@ -164,7 +83,7 @@ class face_image_to_face_mesh:
164
 
165
  mesh = "examples/converted/in-granny.obj"
166
 
167
- if self.zoe_me and use_zoe:
168
  depth = self.zoe.infer_pil(image)
169
  idepth = colorize(depth, cmap='gray_r')
170
  else:
@@ -182,7 +101,7 @@ class face_image_to_face_mesh:
182
 
183
  annotated_image = image.copy()
184
  for face_landmarks in results.multi_face_landmarks:
185
- (mesh,mtl,png) = self.toObj(image=image, width=width, height=height, ratio=ratio, landmark_list=face_landmarks, depth=depth, zoe_scale=zoe_scale)
186
 
187
  mp_drawing.draw_landmarks(
188
  image=annotated_image,
@@ -201,7 +120,7 @@ class face_image_to_face_mesh:
201
 
202
  return mesh, annotated_image, idepth, 1
203
 
204
- def toObj( self, image: np.ndarray, width:int, height:int, ratio: float, landmark_list: landmark_pb2.NormalizedLandmarkList, depth: np.ndarray, zoe_scale: float):
205
  print( f'you have such pretty hair', self.temp_dir )
206
 
207
  hf_hack = True
@@ -215,7 +134,7 @@ class face_image_to_face_mesh:
215
  png_file = tempfile.NamedTemporaryFile(suffix='.png', dir=self.temp_dir, delete=False)
216
 
217
  ############################################
218
- (points,coordinates,colors) = self.landmarksToPoints( image, width, height, ratio, landmark_list, depth, zoe_scale )
219
  ############################################
220
 
221
  lines = []
@@ -281,7 +200,7 @@ class face_image_to_face_mesh:
281
  print( f'I know it is special to you so I saved it to {obj_file.name} since we are friends' )
282
  return (obj_file.name,mtl_file.name,png_file.name)
283
 
284
- def landmarksToPoints( self, image:np.ndarray, width: int, height: int, ratio: float, landmark_list: landmark_pb2.NormalizedLandmarkList, depth: np.ndarray, zoe_scale: float ):
285
  points = [] # 3d vertices
286
  coordinates = [] # 2d texture coordinates
287
  colors = [] # 3d rgb info
@@ -289,21 +208,20 @@ class face_image_to_face_mesh:
289
  mins = [+np.inf] * 3
290
  maxs = [-np.inf] * 3
291
 
292
- for idx, landmark in enumerate(landmark_list.landmark):
293
- if ((landmark.HasField('visibility') and
294
- landmark.visibility < _VISIBILITY_THRESHOLD) or
295
- (landmark.HasField('presence') and
296
- landmark.presence < _PRESENCE_THRESHOLD)):
297
- idk_what_to_do_for_this = True
298
 
 
299
  x, y = _normalized_to_pixel_coordinates(landmark.x,landmark.y,width,height)
300
  color = image[y,x]
301
  colors.append( [value / 255 for value in color ] )
302
  coordinates.append( [x/width,1-y/height] )
303
 
304
  if depth is not None:
305
- landmark.z = depth[y, x] * zoe_scale
306
- #point = [landmark.x * ratio, -landmark.y, -landmark.z];
 
 
307
  point = [landmark.x * ratio, landmark.y, landmark.z];
308
  for pidx,value in enumerate( point ):
309
  mins[pidx] = min(mins[pidx],value)
@@ -319,6 +237,7 @@ class face_image_to_face_mesh:
319
  print( f'maxs: {maxs}' )
320
  return (points,coordinates,colors)
321
 
 
322
  def totallyNormal(self, p0, p1, p2):
323
  v1 = np.array(p1) - np.array(p0)
324
  v2 = np.array(p2) - np.array(p0)
@@ -327,6 +246,93 @@ class face_image_to_face_mesh:
327
  return normal.tolist()
328
 
329
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  face_image_to_face_mesh().demo()
331
 
332
  # EOF
 
22
  def __init__(self):
23
  self.zoe_me = True
24
  self.uvwrap = not True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  def demo(self):
27
  if self.zoe_me:
28
  DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
29
  self.zoe = torch.hub.load('isl-org/ZoeDepth', "ZoeD_N", pretrained=True).to(DEVICE).eval()
30
 
31
+ demo = gr.Blocks(css=self.css(), cache_examples=True)
32
  with demo:
33
+ gr.Markdown(self.header())
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  with gr.Row():
36
  with gr.Column():
 
48
  upload_image_btn = gr.Button(value="Detect faces")
49
  if self.zoe_me:
50
  with gr.Group():
51
+ zoe_scale = gr.Slider(label="Mix the ZoeDepth with the MediaPipe Depth", value=1, minimum=0, maximum=1, step=.01)
52
+ flat_scale = gr.Slider(label="Depth scale, smaller is flatter and possibly more flattering", value=1, minimum=0, maximum=1, step=.01)
53
+ min_detection_confidence = gr.Slider(label="Mininum face detection confidence", value=.5, minimum=0, maximum=1.0, step=0.01)
 
54
  else:
55
  use_zoe = False
56
  zoe_scale = 0
57
  with gr.Group():
58
+ gr.Markdown(self.footer())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  with gr.Column():
61
  with gr.Group():
 
66
 
67
  upload_image_btn.click(
68
  fn=self.detect,
69
+ inputs=[upload_image, min_detection_confidence,zoe_scale,flat_scale],
70
  outputs=[output_mesh, output_image, depth_image, num_faces_detected]
71
  )
72
  demo.launch()
73
 
74
 
75
+ def detect(self, image, min_detection_confidence, zoe_scale, flat_scale):
76
  width = image.shape[1]
77
  height = image.shape[0]
78
  ratio = width / height
 
83
 
84
  mesh = "examples/converted/in-granny.obj"
85
 
86
+ if self.zoe_me and 0 < zoe_scale:
87
  depth = self.zoe.infer_pil(image)
88
  idepth = colorize(depth, cmap='gray_r')
89
  else:
 
101
 
102
  annotated_image = image.copy()
103
  for face_landmarks in results.multi_face_landmarks:
104
+ (mesh,mtl,png) = self.toObj(image=image, width=width, height=height, ratio=ratio, landmark_list=face_landmarks, depth=depth, zoe_scale=zoe_scale, flat_scale=flat_scale)
105
 
106
  mp_drawing.draw_landmarks(
107
  image=annotated_image,
 
120
 
121
  return mesh, annotated_image, idepth, 1
122
 
123
+ def toObj( self, image: np.ndarray, width:int, height:int, ratio: float, landmark_list: landmark_pb2.NormalizedLandmarkList, depth: np.ndarray, zoe_scale: float, flat_scale: float):
124
  print( f'you have such pretty hair', self.temp_dir )
125
 
126
  hf_hack = True
 
134
  png_file = tempfile.NamedTemporaryFile(suffix='.png', dir=self.temp_dir, delete=False)
135
 
136
  ############################################
137
+ (points,coordinates,colors) = self.landmarksToPoints( image, width, height, ratio, landmark_list, depth, zoe_scale, flat_scale )
138
  ############################################
139
 
140
  lines = []
 
200
  print( f'I know it is special to you so I saved it to {obj_file.name} since we are friends' )
201
  return (obj_file.name,mtl_file.name,png_file.name)
202
 
203
+ def landmarksToPoints( self, image:np.ndarray, width: int, height: int, ratio: float, landmark_list: landmark_pb2.NormalizedLandmarkList, depth: np.ndarray, zoe_scale: float, flat_scale: float ):
204
  points = [] # 3d vertices
205
  coordinates = [] # 2d texture coordinates
206
  colors = [] # 3d rgb info
 
208
  mins = [+np.inf] * 3
209
  maxs = [-np.inf] * 3
210
 
211
+ mp_scale = 1 - zoe_scale
212
+ print( f'zoe_scale:{zoe_scale}, mp_scale:{mp_scale}' )
 
 
 
 
213
 
214
+ for idx, landmark in enumerate(landmark_list.landmark):
215
  x, y = _normalized_to_pixel_coordinates(landmark.x,landmark.y,width,height)
216
  color = image[y,x]
217
  colors.append( [value / 255 for value in color ] )
218
  coordinates.append( [x/width,1-y/height] )
219
 
220
  if depth is not None:
221
+ landmark.z = depth[y, x] * zoe_scale + mp_scale * landmark.z
222
+
223
+ landmark.z = landmark.z * flat_scale
224
+
225
  point = [landmark.x * ratio, landmark.y, landmark.z];
226
  for pidx,value in enumerate( point ):
227
  mins[pidx] = min(mins[pidx],value)
 
237
  print( f'maxs: {maxs}' )
238
  return (points,coordinates,colors)
239
 
240
+
241
  def totallyNormal(self, p0, p1, p2):
242
  v1 = np.array(p1) - np.array(p0)
243
  v2 = np.array(p2) - np.array(p0)
 
246
  return normal.tolist()
247
 
248
 
249
+ def header(self):
250
+ return ("""
251
+ # Image to Quad Mesh
252
+
253
+ Uses MediaPipe to detect a face in an image and convert it to a quad mesh.
254
+ Saves to OBJ since gltf does not support quad faces. The 3d viewer has Y pointing the opposite direction from Blender, so ya hafta spin it.
255
+
256
+ The face depth with Zoe can be a bit much and without it is a bit generic. In blender you can fix this just by snapping to the high poly model. For photos turning it down to .4 helps, but may still need cleanup...
257
+
258
+ Highly recommend running it locally. The 3D model has uv values in the faces, but you will have to either use the script or do some manually tomfoolery.
259
+
260
+ Quick import result in examples/converted/movie-gallery.mp4 under files
261
+ """)
262
+
263
+
264
+ def footer(self):
265
+ return ( """
266
+ # Using the Textured Mesh in Blender
267
+
268
+ There a couple of annoying steps atm after you download the obj from the 3d viewer.
269
+
270
+ You can use the script meshin-around.sh in the files section to do the conversion or manually:
271
+
272
+ 1. edit the file and change the mtllib line to use fun.mtl
273
+ 2. replace / delete all lines that start with 'f', eg :%s,^f.*,,
274
+ 3. uncomment all the lines that start with '#f', eg: :%s,^#f,f,
275
+ 4. save and exit
276
+ 5. create fun.mtl to point to the texture like:
277
+
278
+ ```
279
+ newmtl MyMaterial
280
+ map_Kd fun.png
281
+ ```
282
+
283
+ Make sure the obj, mtl and png are all in the same directory
284
+
285
+ Now the import will have the texture data: File -> Import -> Wavefront (obj) -> fun.obj
286
+
287
+ This is all a work around for a weird hf+gradios+babylonjs bug which seems to be related to the version
288
+ of babylonjs being used... It works fine in a local babylonjs sandbox...
289
+
290
+ # Suggested Workflows
291
+
292
+ Here are some workflow ideas.
293
+
294
+ ## retopologize high poly face mesh
295
+
296
+ 1. sculpt high poly mesh in blender
297
+ 2. snapshot the face
298
+ 3. generate the mesh using the mediapipe stuff
299
+ 4. import the low poly mediapipe face
300
+ 5. snap the mesh to the high poly model
301
+ 6. model the rest of the low poly model
302
+ 7. bake the normal / etc maps to the low poly face model
303
+ 8. it's just that easy 😛
304
+
305
+ Ideally it would be a plugin...
306
+
307
+ ## stable diffusion integration
308
+
309
+ 1. generate a face in sd
310
+ 2. generate the mesh
311
+ 3. repose it and use it for further generation
312
+
313
+ May need to expanded the generated mesh to cover more, maybe with
314
+ <a href="https://github.com/shunsukesaito/PIFu" target="_blank">PIFu model</a>.
315
+
316
+ """)
317
+
318
+
319
+ def css(self):
320
+ return ("""
321
+ #mesh-display-output {
322
+ max-height: 44vh;
323
+ max-width: 44vh;
324
+ width:auto;
325
+ height:auto
326
+ }
327
+ #img-display-output {
328
+ max-height: 28vh;
329
+ max-width: 28vh;
330
+ width:auto;
331
+ height:auto
332
+ }
333
+ """)
334
+
335
+
336
  face_image_to_face_mesh().demo()
337
 
338
  # EOF