Akjava commited on
Commit
9985495
Β·
1 Parent(s): e0dae12
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.task filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ __pycache__
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Face Crop And Replace CPU
3
  emoji: πŸ“ˆ
4
  colorFrom: indigo
5
  colorTo: indigo
 
1
  ---
2
+ title: mediapipe-face-crop-and-replace
3
  emoji: πŸ“ˆ
4
  colorFrom: indigo
5
  colorTo: indigo
app.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import gradio as gr
3
+ import subprocess
4
+ from PIL import Image,ImageEnhance,ImageFilter
5
+ import json
6
+
7
+ import mp_box
8
+ '''
9
+ Face landmark detection based Face Detection.
10
+ https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker
11
+ from model card
12
+ https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf
13
+ Licensed Apache License, Version 2.0
14
+ Train with google's dataset(more detail see model card)
15
+
16
+ Not Face Detector based
17
+ https://ai.google.dev/edge/mediapipe/solutions/vision/face_detector
18
+
19
+ Bacause this is part of getting-landmark program and need control face edge.
20
+ So I don't know which one is better.never compare these.
21
+ '''
22
+
23
+ def select_box(boxes,box_type):
24
+ if box_type == "type-3":
25
+ box = boxes[2]
26
+ elif box_type =="type-2":
27
+ box = boxes[1]
28
+ elif box_type =="type-1":
29
+ box = boxes[0]
30
+ else:#never happen
31
+ box=[0,0,image.size[0],image.size[1]]
32
+ box_width = box[2]
33
+ box_height = box[3]
34
+ box = mp_box.xywh_to_xyxy(box)
35
+ return box,box_width,box_height
36
+
37
+ def process_images(image,replace_image=None,replace_image_need_crop=False,box_type="type-3",fill_color_mode=False,fill_color="black",custom_color="rgba(255,255,255,1)",image_size=1024,filter_image=False,filter_value="Sharpen",progress=gr.Progress(track_tqdm=True)):
38
+ if image == None:
39
+ raise gr.Error("Need Image")
40
+
41
+ # choose box
42
+ boxes,mp_image,face_landmarker_result = mp_box.mediapipe_to_box(image)
43
+ box,box_width,box_height = select_box(boxes,box_type)
44
+
45
+ # replace-mode
46
+ if replace_image!=None:
47
+ print("replace mode")
48
+
49
+ if replace_image_need_crop:
50
+ replace_boxes,mp_image,face_landmarker_result = mp_box.mediapipe_to_box(replace_image)
51
+ replace_box,replace_box_width,replace_box_height = select_box(replace_boxes,box_type)
52
+
53
+ if fill_color_mode:
54
+ if replace_image_need_crop:
55
+ cropped = replace_image.crop(replace_box)
56
+ cropped.resize(box_width,box_height)
57
+ else:
58
+ cropped = replace_image.crop(box)
59
+ image.paste(cropped,[box[0],box[1]])
60
+ return image
61
+ else:#scale mode
62
+ if replace_image_need_crop:
63
+ replace_image = replace_image.crop(replace_box)
64
+ replace_resized = replace_image.resize((box_width,box_height),Image.Resampling.LANCZOS)
65
+ image.paste(replace_resized,[box[0],box[1]])
66
+ return image
67
+
68
+
69
+ # crop-mode
70
+ if fill_color_mode:
71
+ # choose color
72
+ color_map={
73
+ "black":[0,0,0,1],
74
+ "white":[255,255,255,1],
75
+ "red":[255,0,0,1],
76
+ "brown":[92,33,31,1],
77
+ "pink":[255,192,203,1],
78
+ }
79
+ if fill_color == "custom":
80
+ color_value = custom_color.strip("rgba()").split(",")
81
+ color_value[0] = int(float(color_value[0]))
82
+ color_value[1] = int(float(color_value[1]))
83
+ color_value[2] = int(float(color_value[2]))
84
+ else:
85
+ color_value = color_map[fill_color]
86
+
87
+ cropped = image.crop(box)
88
+
89
+ img = Image.new('RGBA', image.size, (color_value[0], color_value[1], color_value[2]))
90
+ img.paste(cropped,[box[0],box[1]])
91
+ return img
92
+ else:
93
+ #scale up mode
94
+ cropped = image.crop(box)
95
+ resized = resize_image_by_max_dimension(cropped,image_size)
96
+ filter_map={
97
+ "Blur":ImageFilter.BLUR,"Smooth More":ImageFilter.SMOOTH_MORE,"Smooth":ImageFilter.SMOOTH,"Sharpen":ImageFilter.SHARPEN,"Edge Enhance":ImageFilter.EDGE_ENHANCE,"Edge Enhance More":ImageFilter.EDGE_ENHANCE_MORE
98
+ }
99
+ if filter_value not in filter_map:
100
+ raise gr.Error(f"filter {filter_value} not found")
101
+ if filter_image:
102
+
103
+ #resized = resized.filter(ImageFilter.SHARPEN)
104
+ #Gimp's weak 0.1-0.2?
105
+ enhancer = ImageEnhance.Sharpness(resized)
106
+ resized = resized.filter(filter_map[filter_value])
107
+ #resized = enhancer.enhance(sharpen_value)
108
+
109
+
110
+ return resized
111
+
112
+
113
+ def resize_image_by_max_dimension(image, max_size, resampling=Image.Resampling.BICUBIC):
114
+ image_width, image_height = image.size
115
+
116
+ max_dimension = max(image_width, image_height)
117
+
118
+ ratio = max_size / max_dimension
119
+
120
+ new_width = int(image_width * ratio)
121
+ new_height = int(image_height * ratio)
122
+
123
+ return image.resize((new_width, new_height), resampling)
124
+
125
+
126
+ def read_file(file_path: str) -> str:
127
+ """read the text of target file
128
+ """
129
+ with open(file_path, 'r', encoding='utf-8') as f:
130
+ content = f.read()
131
+
132
+ return content
133
+
134
+ css="""
135
+ #col-left {
136
+ margin: 0 auto;
137
+ max-width: 640px;
138
+ }
139
+ #col-right {
140
+ margin: 0 auto;
141
+ max-width: 640px;
142
+ }
143
+ .grid-container {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ gap:10px
148
+ }
149
+
150
+ .image {
151
+ width: 128px;
152
+ height: 128px;
153
+ object-fit: cover;
154
+ }
155
+
156
+ .text {
157
+ font-size: 16px;
158
+ }
159
+ """
160
+
161
+ #css=css,
162
+ def update_button_label(image):
163
+ if image == None:
164
+ return gr.Button(visible=bool(0)),gr.Button(visible=bool(1))
165
+ else:
166
+ return gr.Button(visible=bool(1)),gr.Row(visible=bool(0))
167
+
168
+ def update_visible(fill_color_mode):
169
+ if fill_color_mode:
170
+ return gr.Row(visible=bool(0)),gr.Row(visible=bool(1))
171
+ else:
172
+ return gr.Row(visible=bool(1)),gr.Row(visible=bool(0))
173
+ with gr.Blocks(css=css, elem_id="demo-container") as demo:
174
+ with gr.Column():
175
+ gr.HTML(read_file("demo_header.html"))
176
+ gr.HTML(read_file("demo_tools.html"))
177
+ with gr.Row():
178
+ with gr.Column():
179
+ image = gr.Image(sources=['upload','clipboard'],image_mode='RGB',elem_id="image_upload", type="pil", label="Upload")
180
+ box_type = gr.Dropdown(label="box-type",value="type-3",choices=["type-1","type-2","type-3"])
181
+ with gr.Row(elem_id="prompt-container", equal_height=False):
182
+ with gr.Row():
183
+ btn1 = gr.Button("Face Crop", elem_id="run_button",variant="primary")
184
+ btn2 = gr.Button("Face Replace", elem_id="run_button2",variant="primary",visible=False)
185
+
186
+ replace_image = gr.Image(sources=['upload','clipboard'],image_mode='RGB',elem_id="replace_upload", type="pil", label="replace image")
187
+ replace_image_need_crop = gr.Checkbox(label="Replace image need crop",value=False)
188
+ replace_image.change(update_visible,replace_image,[btn1,btn2])
189
+ with gr.Accordion(label="Advanced Settings", open=False):
190
+ fill_color_mode = gr.Checkbox(label="Fill Color Mode",value=False)
191
+ row1 = gr.Row(equal_height=True)
192
+ row2 = gr.Row(equal_height=True,visible=False)
193
+ fill_color_mode.change(update_visible,fill_color_mode,[row1,row2])
194
+
195
+ with row1:
196
+ image_size = gr.Slider(
197
+ label="Image Size",info = "cropped face size",
198
+ minimum=8,
199
+ maximum=2048,
200
+ step=1,
201
+ value=1024,
202
+ interactive=True)
203
+
204
+ filter_image = gr.Checkbox(label="Filter image")
205
+ filter_value = gr.Dropdown(label="Filter",value="Sharpen",choices=["Blur","Smooth More","Smooth","Sharpen","Edge Enhance","Edge Enhance More"])
206
+ with row2:
207
+
208
+ fill_color = gr.Dropdown(label="fill color",choices=["black","white","red","brown","pink","custom"])
209
+ custom_color = gr.ColorPicker(label="custom color",value="rgba(250, 218, 205, 1)")
210
+
211
+
212
+ with gr.Column():
213
+ image_out = gr.Image(label="Output", elem_id="output-img")
214
+
215
+
216
+
217
+
218
+
219
+
220
+ gr.on(
221
+ [btn1.click,btn2.click],
222
+ fn=process_images, inputs=[image,replace_image,replace_image_need_crop,box_type,fill_color_mode,fill_color,custom_color,image_size,filter_image,filter_value], outputs =[image_out], api_name='infer'
223
+ )
224
+ gr.Examples(
225
+ examples =["examples/00004200.jpg"],
226
+ inputs=[image]
227
+ )
228
+ gr.HTML(read_file("demo_footer.html"))
229
+
230
+ if __name__ == "__main__":
231
+ demo.launch()
demo_footer.html ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ <div>
2
+ <P> Images are generated with <a href="https://huggingface.co/black-forest-labs/FLUX.1-schnell">FLUX.1-schnell</a> and licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0">the Apache 2.0 License</a>
3
+ </div>
demo_header.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div style="text-align: center;">
2
+ <h1>
3
+ Mediapipe Face Crop and Replace
4
+ </h1>
5
+ <div class="grid-container">
6
+ <img src="https://akjava.github.io/AIDiagramChatWithVoice-FaceCharacter/webp/128/00191245_09_00002200.webp" alt="Mediapipe Face Detection" class="image">
7
+
8
+ <p class="text">
9
+ This Space use <a href="http://www.apache.org/licenses/LICENSE-2.0">the Apache 2.0</a> Licensed <a href="https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker">Mediapipe FaceLandmarker</a> <br>
10
+
11
+
12
+ </p>
13
+ </div>
14
+
15
+ </div>
demo_tools.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <div style="text-align: center;">
2
+ <p><a href="https://huggingface.co/spaces/Akjava/mediapipe-face-detect">Mediapipe Face detector</a></p>
3
+ <p></p>
4
+ </div>
examples/00004200.jpg ADDED
face_landmarker.task ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff
3
+ size 3758596
face_landmarker.task.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Face landmark detection
2
+ https://ai.google.dev/edge/mediapipe/solutions/vision/face_landmarker
3
+
4
+ model card page is
5
+ https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf
6
+
7
+ license is Apache2.0
8
+ https://www.apache.org/licenses/LICENSE-2.0.html
mp_box.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mediapipe as mp
2
+ from mediapipe.tasks import python
3
+ from mediapipe.tasks.python import vision
4
+ from mediapipe.framework.formats import landmark_pb2
5
+ from mediapipe import solutions
6
+ import numpy as np
7
+
8
+ # for X,Y,W,H to x1,y1,x2,y2(Left-top,right-bottom style)
9
+ def xywh_to_xyxy(box):
10
+ return [box[0],box[1],box[0]+box[2],box[1]+box[3]]
11
+
12
+ def convert_to_box(face_landmarks_list,indices,w=1024,h=1024):
13
+ x1=w
14
+ y1=h
15
+ x2=0
16
+ y2=0
17
+ for index in indices:
18
+ x=min(w,max(0,(face_landmarks_list[0][index].x*w)))
19
+ y=min(h,max(0,(face_landmarks_list[0][index].y*h)))
20
+ if x<x1:
21
+ x1=x
22
+
23
+ if y<y1:
24
+ y1=y
25
+
26
+ if x>x2:
27
+ x2=x
28
+ if y>y2:
29
+ y2=y
30
+
31
+
32
+ return [int(x1),int(y1),int(x2-x1),int(y2-y1)]
33
+
34
+
35
+ def box_to_square(bbox):
36
+ box=list(bbox)
37
+ if box[2]>box[3]:
38
+ diff = box[2]-box[3]
39
+ box[3]+=diff
40
+ box[1]-=diff/2
41
+ elif box[3]>box[2]:
42
+ diff = box[3]-box[2]
43
+ box[2]+=diff
44
+ box[0]-=diff/2
45
+ return box
46
+
47
+
48
+ def face_landmark_result_to_box(face_landmarker_result,width=1024,height=1024):
49
+ face_landmarks_list = face_landmarker_result.face_landmarks
50
+
51
+
52
+ full_indices = list(range(456))
53
+
54
+ MIDDLE_FOREHEAD = 151
55
+ BOTTOM_CHIN_EX = 152
56
+ BOTTOM_CHIN = 175
57
+ CHIN_TO_MIDDLE_FOREHEAD = [200,14,1,6,18,9]
58
+ MOUTH_BOTTOM = [202,200,422]
59
+ EYEBROW_CHEEK_LEFT_RIGHT = [46,226,50,1,280,446,276]
60
+
61
+ LEFT_HEAD_OUTER_EX = 251 #on side face almost same as full
62
+ LEFT_HEAD_OUTER = 301
63
+ LEFT_EYE_OUTER_EX = 356
64
+ LEFT_EYE_OUTER = 264
65
+ LEFT_MOUTH_OUTER_EX = 288
66
+ LEFT_MOUTH_OUTER = 288
67
+ LEFT_CHIN_OUTER = 435
68
+ RIGHT_HEAD_OUTER_EX = 21
69
+ RIGHT_HEAD_OUTER = 71
70
+ RIGHT_EYE_OUTER_EX = 127
71
+ RIGHT_EYE_OUTER = 34
72
+ RIGHT_MOUTH_OUTER_EX = 58
73
+ RIGHT_MOUTH_OUTER = 215
74
+ RIGHT_CHIN_OUTER = 150
75
+
76
+ # TODO naming line
77
+ min_indices=CHIN_TO_MIDDLE_FOREHEAD+EYEBROW_CHEEK_LEFT_RIGHT+MOUTH_BOTTOM
78
+
79
+ chin_to_brow_indices = [LEFT_CHIN_OUTER,LEFT_MOUTH_OUTER,LEFT_EYE_OUTER,LEFT_HEAD_OUTER,MIDDLE_FOREHEAD,RIGHT_HEAD_OUTER,RIGHT_EYE_OUTER,RIGHT_MOUTH_OUTER,RIGHT_CHIN_OUTER,BOTTOM_CHIN]+min_indices
80
+
81
+ box1 = convert_to_box(face_landmarks_list,min_indices,width,height)
82
+ box2 = convert_to_box(face_landmarks_list,chin_to_brow_indices,width,height)
83
+ box3 = convert_to_box(face_landmarks_list,full_indices,width,height)
84
+ #print(box)
85
+
86
+ return [box1,box2,box3,box_to_square(box1),box_to_square(box2),box_to_square(box3)]
87
+
88
+
89
+ def draw_landmarks_on_image(detection_result,rgb_image):
90
+ face_landmarks_list = detection_result.face_landmarks
91
+ annotated_image = np.copy(rgb_image)
92
+
93
+ # Loop through the detected faces to visualize.
94
+ for idx in range(len(face_landmarks_list)):
95
+ face_landmarks = face_landmarks_list[idx]
96
+
97
+ # Draw the face landmarks.
98
+ face_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
99
+ face_landmarks_proto.landmark.extend([
100
+ landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in face_landmarks
101
+ ])
102
+
103
+ solutions.drawing_utils.draw_landmarks(
104
+ image=annotated_image,
105
+ landmark_list=face_landmarks_proto,
106
+ connections=mp.solutions.face_mesh.FACEMESH_TESSELATION,
107
+ landmark_drawing_spec=None,
108
+ connection_drawing_spec=mp.solutions.drawing_styles
109
+ .get_default_face_mesh_tesselation_style())
110
+
111
+ return annotated_image
112
+
113
+ def mediapipe_to_box(image_data,model_path="face_landmarker.task"):
114
+ BaseOptions = mp.tasks.BaseOptions
115
+ FaceLandmarker = mp.tasks.vision.FaceLandmarker
116
+ FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
117
+ VisionRunningMode = mp.tasks.vision.RunningMode
118
+
119
+ options = FaceLandmarkerOptions(
120
+ base_options=BaseOptions(model_asset_path=model_path),
121
+ running_mode=VisionRunningMode.IMAGE
122
+ ,min_face_detection_confidence=0, min_face_presence_confidence=0
123
+ )
124
+
125
+
126
+ with FaceLandmarker.create_from_options(options) as landmarker:
127
+ if isinstance(image_data,str):
128
+ mp_image = mp.Image.create_from_file(image_data)
129
+ else:
130
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=np.asarray(image_data))
131
+ face_landmarker_result = landmarker.detect(mp_image)
132
+ boxes = face_landmark_result_to_box(face_landmarker_result,mp_image.width,mp_image.height)
133
+ return boxes,mp_image,face_landmarker_result
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ numpy
2
+ torch
3
+ spaces
4
+ mediapipe