File size: 11,563 Bytes
e4bf926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7ebf4b
 
e4bf926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7ebf4b
e4bf926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
from enum import Enum
import numpy as np
import gradio as gr
import torch
from PIL import Image
from transformers import DPTImageProcessor, DPTForDepthEstimation
from typing import List, Tuple
import random
from PIL import ImageDraw, ImageFont
from gradio.components import Image as grImage
import mediapipe as mp


processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large")

detector = mp.solutions.face_detection.FaceDetection(model_selection=1, min_detection_confidence=0.5)

class Placement(Enum):
    CENTER = 0
    TOP = 1

class FaceKeypointsLabel(Enum):
    OTHER = 0
    NOSE = 1

class Keypoints:
    def __init__(self, x: float, y: float, label: FaceKeypointsLabel):
        """
        :param x: x coordinate of the keypoint, normalized between 0 and 1
        :param y: y coordinate of the keypoint, normalized between 0 and 1
        """
        self.x = x
        self.y = y
        self.label = label

class BoundingBox:
    def __init__(self, x_min: int, y_min: int, width: int, height: int):
        self.x_min = x_min
        self.y_min = y_min
        self.width = width
        self.height = height

class FaceDetectionResult:
    """
    A class to represent the result of a face detection
    """
    def __init__(self, bounding_box : BoundingBox, keypoints: List[Keypoints]):
        self.bounding_box  = bounding_box
        self.keypoints = keypoints


def detect_face(image: Image) -> List[any]:
    """
    Use mediapipe to detect faces in an image
    """
    result = detector.process(np.array(image))
    if result.detections is None:
        return []
    return result.detections


def predict_depth(image: Image) -> np.ndarray:
    """
    Predict depth for an image
    """
    inputs = processor(images=image, return_tensors="pt")

    with torch.no_grad():
        outputs = model(**inputs)
        predicted_depth = outputs.predicted_depth

    # Interpolate to original size
    prediction = torch.nn.functional.interpolate(
        predicted_depth.unsqueeze(1),
        size=image.size[::-1],
        mode="bicubic",
        align_corners=False,
    )

    output = prediction.squeeze().cpu().numpy()
    return (output * 255 / np.max(output)).astype("uint8")

def estimate_depth_at_points(depth_map: np.ndarray, coordinates: List[Tuple[int, int]]) -> List[float]:
    """
    Get the depth at a given coordinates
    """
    depth_estimates = []

    # Iterate through the given coordinates and estimate depth at each point
    for x, y in coordinates:
        depth_estimate = depth_map[y, x]  # Access depth at the given point
        depth_estimates.append(depth_estimate)

    return depth_estimates


class Person:
    """
    A class to represent a person in an image
    """

    def __init__(self, nose_x: int, nose_y: int, head_width: int, head_height: int, middle_top_head_x: int, middle_top_head_y: int):            
        self.nose_x = nose_x
        self.nose_y = nose_y
        self.head_width = head_width
        self.head_height = head_height
        self.middle_top_head_x = middle_top_head_x
        self.middle_top_head_y = middle_top_head_y
        self.nose_width = int(head_width / 5)
        self.nose_height = int(head_height / 3)

def extract_persons(face_detection_results: List[FaceDetectionResult], image: Image) -> List[Person]:
    """
    Extract a list of people from a face detection result
    """
    persons = []

    for face_result in face_detection_results:
        bbox = face_result.bounding_box
        keypoints = face_result.keypoints

        # Assuming the nose is the first keypoint in the list.
        # You might need to adjust this based on how keypoints are ordered.
        for keypoint in keypoints:
            if keypoint.label == FaceKeypointsLabel.NOSE:
                nose_keypoint = keypoint
                break

        nose_x = int(nose_keypoint.x * image.width)
        nose_y = int(nose_keypoint.y * image.height)

        # Bounding box details
        middle_top_head_x = int(bbox.x_min + bbox.width // 2)
        middle_top_head_y = bbox.y_min
        head_width = bbox.width
        head_height = bbox.height

        # Create and add Person object
        person = Person(nose_x, nose_y, head_width, head_height, middle_top_head_x, middle_top_head_y)
        persons.append(person)

    return persons

def add_mask(image: Image, mask: Image, coordinate: Tuple[int, int], size: Tuple[int, int], placement: Placement) -> Image:
    """
    Add a mask (a static image) to an image
    """

    # maintain aspect ratio
    if len(size) == 1:
        height = mask.height
        width = mask.width
        ratio = height / width
        size = (size[0], int(size[0] * ratio))

    if placement == Placement.CENTER:
        coordinate = (coordinate[0] - size[0] // 2, coordinate[1] - size[1] // 2)
    elif placement == Placement.TOP:
        coordinate = (coordinate[0] - size[0] // 2, coordinate[1] - size[1])

    mask = mask.resize(size)
    image.paste(mask, coordinate, mask)
    return image

def draw_attributes(image: Image, persons: List[Person]) -> Image:
    """
    Debug function to the face recognition attributes on an image
    """
    draw = ImageDraw.Draw(image)
    font = ImageFont.load_default()

    for person in persons:
        # Draw a circle at the nose position
        draw.ellipse([(person.nose_x - 5, person.nose_y - 5), (person.nose_x + 5, person.nose_y + 5)], fill=(0, 255, 0))

        # Draw the head rectangle
        draw.rectangle([(person.middle_top_head_x - person.head_width // 2, person.middle_top_head_y),
                        (person.middle_top_head_x + person.head_width // 2, person.middle_top_head_y + person.head_height)],
                       outline=(0, 255, 0))

        # Put text for dimensions
        draw.text((person.middle_top_head_x, person.middle_top_head_y - 20), f"Width: {person.head_width}, Height: {person.head_height}", fill=(255, 255, 255), font=font)
        # put location of nose
        draw.text((person.nose_x, person.nose_y + 10), f"({person.nose_x}, {person.nose_y})", fill=(255, 255, 255), font=font)

        # draw dot at middle top head
        draw.ellipse([(person.middle_top_head_x - 5, person.middle_top_head_y - 5), (person.middle_top_head_x + 5, person.middle_top_head_y + 5)], fill=(255, 0, 0))

    return image

def apply_reindeer_mask(image: Image, person: Person) -> Image:
    """
    Apply a reindeer mask to a person in an image
    """
    reindeer_nose = Image.open("mask/reindeer_nose.png")
    reindeer_antlers = Image.open("mask/reindeer_antlers.png")

    reindeer_nose_coordinate = (person.nose_x, person.nose_y)

    reindeer_nose_size = (person.nose_height, person.nose_height)
    image = add_mask(image, reindeer_nose, reindeer_nose_coordinate, reindeer_nose_size, Placement.CENTER)

    reindeer_antlers_size = (person.head_width, ) 
    reindeer_antlers_coordinate = (person.middle_top_head_x, person.middle_top_head_y)
    image = add_mask(image, reindeer_antlers, reindeer_antlers_coordinate, reindeer_antlers_size, Placement.TOP)
    return image

def apply_santa_hat_mask(image: Image, person: Person) -> Image:
    """
    Apply a santa hat mask to a person in an image
    """
    santa_hat = Image.open("mask/santa_hat.png")
    santa_hat_size = (person.head_width, ) 
    santa_hat_coordinate = (person.middle_top_head_x, person.middle_top_head_y)
    image = add_mask(image, santa_hat, santa_hat_coordinate, santa_hat_size, Placement.TOP)
    return image

def add_text(image: Image, text: str, font_size: int = 30) -> Image:
    """
    Add text to an image
    """
    draw = ImageDraw.Draw(image)

    # Calculate text width and height for centering
    text_width, text_height = draw.textsize(text)
    text_x = (image.width - text_width) // 2
    text_y = (image.height - text_height) // 2

    draw.text((text_x, text_y), text, fill=(255, 0, 0))
    return image

def apply_random_mask(image: Image, person: Person) -> Image:
    """
    Apply a random mask to a person in an image
    """
    mask = random.choice([apply_santa_hat_mask, apply_reindeer_mask])
    image = mask(image, person)
    return image


def process_image(image : Image):
    """
    The full pipeline that take an image and returns an image with more christmas spirit :)
    """

    # Potential improvement this could be done in parallel
    depth_result = predict_depth(image)
    detections = detect_face(image)

    face_detection_results = parse_detection_result(detections, image)
    persons = extract_persons(face_detection_results, image)

    if len(persons) == 0:
        return add_text(image, "No faces detected in the image")
    if len(persons) == 1:
        image = apply_random_mask(image,persons[0])
    elif len(persons) > 1:
        # Apply the rules of the assignment, closest person gets santa hat, furthest person gets reindeer mask
        # All other people get a random mask (either santa hat or reindeer mask) (as this was not specified in the assignment)

        depth_estimates = estimate_depth_at_points(depth_result, [(person.nose_x, person.nose_y) for person in persons])
        closest_camera_index = np.argmin(depth_estimates)
        furthest_camera_index = np.argmax(depth_estimates)
        santa_person = persons[closest_camera_index]
        reindeer_person = persons[furthest_camera_index]

        image = apply_reindeer_mask(image, reindeer_person)
        image = apply_santa_hat_mask(image, santa_person)

        for i, person in enumerate(persons):
            if i != closest_camera_index and i != furthest_camera_index:
                image = apply_random_mask(image, person)

    return image

def parse_detection_to_face_detection_result(detection, image_width: int, image_height: int) -> FaceDetectionResult:
    """
    Parse a mediapipe detection to a FaceDetectionResult
    """
    
    # Extract bounding box
    bbox = detection.location_data.relative_bounding_box
    x_min = int(bbox.xmin * image_width)
    y_min = int(bbox.ymin * image_height)
    width = int(bbox.width * image_width)
    height = int(bbox.height * image_height)
    bounding_box = BoundingBox(x_min, y_min, width, height)

    # Extract keypoints
    keypoints = []
    for i, keypoint in enumerate(detection.location_data.relative_keypoints):
        x = keypoint.x
        y = keypoint.y
        face_type = FaceKeypointsLabel.OTHER
        if i == 2:
            face_type = FaceKeypointsLabel.NOSE
        keypoints.append(Keypoints(x, y, face_type))

    return FaceDetectionResult(bounding_box, keypoints)


def parse_detection_result(detection_result, image: Image) -> List[FaceDetectionResult]:
    """
    Parse a mediapipe detection result to a list of FaceDetectionResult
    """
    face_detection_results = []


    for detection in detection_result:
        face_detection_result = parse_detection_to_face_detection_result(detection, image.width, image.height)
        face_detection_results.append(face_detection_result)

    return face_detection_results

def main():

    # Remarks: the code is in one file for simplicity, but it would be better to split it up in multiple files

    # Create a gradio interface
    iface = gr.Interface(
    fn=process_image, 
    inputs=grImage(type="pil"), 
    outputs=grImage(type="pil"),
    title="Image Processor",
    description="Upload an image to detect faces and apply transformations."
    )

    # Launch the interface
    iface.launch()


if __name__ == "__main__":
    main()