SnapSticker / app.py
RedBottle13's picture
Update app.py
ab3cfe3 verified
import gradio as gr
import cv2
import numpy as np
from PIL import Image
import dlib
import os
import math
from constants import *
MAX_EXPECTED_FACES=7
# get a list of faces in the image
def face_detecting(image):
detector = dlib.get_frontal_face_detector()
faces = detector(image, 1)
return faces
# show all the faces in rectangles in the image
def face_showing(image, faces):
for face in faces:
cv2.rectangle(image, (face.left(), face.top()), (face.right(), face.bottom()), (255, 255, 255), 2)
return image
# highlight the selected face in the image, using index to select the face
def face_selecting(image, faces, index):
face = faces[index]
cv2.rectangle(image, (face.left(), face.top()), (face.right(), face.bottom()), (255, 255, 255), 2)
return image
# get the landmarks of the face
def face_landmarking(image, face):
predictor = dlib.shape_predictor('shape_predictor_81_face_landmarks.dat')
landmarks = predictor(image, face)
return landmarks
# Function to overlay a transparent image onto another image
def overlay_transparent(background, overlay, x, y):
bg_height, bg_width = background.shape[:2]
if x >= bg_width or y >= bg_height:
return background
h, w = overlay.shape[:2]
if x + w > bg_width:
w = bg_width - x
overlay = overlay[:, :w]
if y + h > bg_height:
h = bg_height - y
overlay = overlay[:h]
if overlay.shape[2] < 4:
overlay = np.concatenate([overlay, np.ones((overlay.shape[0], overlay.shape[1], 1), dtype=overlay.dtype) * 255], axis=2)
overlay_img = overlay[..., :3]
mask = overlay[..., 3:] / 255.0
background[y:y+h, x:x+w] = (1.0 - mask) * background[y:y+h, x:x+w] + mask * overlay_img
return background
def calculate_eye_angle(landmarks, left_eye_indices, right_eye_indices):
# Calculate the center point of the left eye
left_eye_center = (
sum([landmarks.part(i).x for i in left_eye_indices]) // len(left_eye_indices),
sum([landmarks.part(i).y for i in left_eye_indices]) // len(left_eye_indices)
)
# Calculate the center point of the right eye
right_eye_center = (
sum([landmarks.part(i).x for i in right_eye_indices]) // len(right_eye_indices),
sum([landmarks.part(i).y for i in right_eye_indices]) // len(right_eye_indices)
)
# Calculate the differences in the x and y coordinates between the centers of the eyes
dx = right_eye_center[0] - left_eye_center[0]
dy = right_eye_center[1] - left_eye_center[1]
# Calculate the angle using the arctangent of the differences
angle = math.degrees(math.atan2(dy, dx))
return angle
# Function to add ear stickers
def add_ears_sticker(img_bgr, sticker_path, faces):
ears_pil = Image.open(sticker_path)
# Check the color mode and convert to RGBA
ears_rgba = ears_pil.convert('RGBA')
# Convert the ears_rgba to BGRA
r, g, b, a = ears_rgba.split()
ears_bgra = Image.merge("RGBA", (b, g, r, a))
# A copy of the original image
img_with_stickers = img_bgr.copy()
for face in faces:
landmarks = face_landmarking(img_bgr, face)
# the landmarks 68 to 80 are for the forehead
forehead = [landmarks.part(i) for i in range(68, 81)]
# The landmarks 36 to 41 are for the left eye, and 42 to 47 are for the right eye
left_eye = [landmarks.part(i) for i in range(36, 42)]
right_eye = [landmarks.part(i) for i in range(42, 48)]
# Calculate the center point between the eyes
left_eye_center = ((left_eye[0].x + left_eye[3].x) // 2, (left_eye[0].y + left_eye[3].y) // 2)
right_eye_center = ((right_eye[0].x + right_eye[3].x) // 2, (right_eye[0].y + right_eye[3].y) // 2)
# Calculate the angle of tilt
dx = right_eye_center[0] - left_eye_center[0]
dy = right_eye_center[1] - left_eye_center[1]
angle = math.degrees(math.atan2(dy, dx))
# Calculate the bounding box for the ears based on the eye landmarks
ears_width = int(abs(forehead[0].x - forehead[-1].x) * 2.1)
ears_height = int(ears_width * ears_bgra.height / ears_bgra.width)
# Resize the ears image
resized_ears_pil = ears_bgra.resize((ears_width, ears_height))
rotated_ears = resized_ears_pil.rotate(-angle, expand=True, resample=Image.BICUBIC)
# Calculate the position for the ears
y1 = min([point.y for point in forehead]) - int(0.7 * ears_height)
x1 = forehead[0].x - int(0.2 * ears_width)
# Convert PIL image to NumPy array
# ears_np = np.array(resized_ears_pil)
ears_np = np.array(rotated_ears)
# Overlay the ears on the image
img_with_stickers = overlay_transparent(img_with_stickers, ears_np, x1, y1)
return img_with_stickers
# Function to add hats stickers
def add_hats_sticker(img_bgr, sticker_path, faces):
hat_pil = Image.open(sticker_path)
# Check the color mode and convert to RGBA
hat_rgba = hat_pil.convert('RGBA')
# Convert the hat_rgba to BGRA
r, g, b, a = hat_rgba.split()
hat_bgra = Image.merge("RGBA", (b, g, r, a))
# A copy of the original image
img_with_stickers = img_bgr.copy()
for face in faces:
landmarks = face_landmarking(img_bgr, face)
# The landmarks 36 to 41 are for the left eye, and 42 to 47 are for the right eye
left_eye = [landmarks.part(i) for i in range(36, 42)]
right_eye = [landmarks.part(i) for i in range(42, 48)]
forehead = [landmarks.part(i) for i in range(68, 81)]
# Calculate the center point between the eyes
left_eye_center = ((left_eye[0].x + left_eye[3].x) // 2, (left_eye[0].y + left_eye[3].y) // 2)
right_eye_center = ((right_eye[0].x + right_eye[3].x) // 2, (right_eye[0].y + right_eye[3].y) // 2)
eye_center_x = (left_eye_center[0] + right_eye_center[0]) // 2
eye_center_y = (left_eye_center[1] + right_eye_center[1]) // 2
# Calculate the angle of tilt
dx = right_eye_center[0] - left_eye_center[0]
dy = right_eye_center[1] - left_eye_center[1]
angle = math.degrees(math.atan2(dy, dx))
# Calculate the size of the hat based on the width between the eyes
hat_width = int(abs(left_eye[0].x - right_eye[3].x) * 1.75)
hat_height = int(hat_width * hat_bgra.height / hat_bgra.width)
# Resize and rotate the hat image
resized_hat = hat_bgra.resize((hat_width, hat_height))
rotated_hat = resized_hat.rotate(-0.8*angle, expand=True, resample=Image.BICUBIC)
# Calculate the position for the hat
y1 = eye_center_y - hat_height - int(0.3 * hat_height)
# x1 = eye_center_x - (hat_width // 2) # Centering the hat on the midpoint between the eyes
# x1 = eye_center_x - (hat_width // 2) - int(0.03 * hat_width) # Moving the hat a bit further to the left
x1 = forehead[0].x - int(0.2 * hat_width)
# Convert PIL image to NumPy array
hat_np = np.array(rotated_hat)
# Overlay the hat on the image
img_with_stickers = overlay_transparent(img_with_stickers, hat_np, x1, y1)
return img_with_stickers
# Function to add glasses stickers
def add_glasses_sticker(img_bgr, sticker_path, faces):
glasses_pil = Image.open(sticker_path)
# Check the color mode and convert to RGBA
glasses_rgba = glasses_pil.convert('RGBA')
# Convert the glasses_rgba to BGRA
r, g, b, a = glasses_rgba.split()
glasses_bgra = Image.merge("RGBA", (b, g, r, a))
# A copy of the original image
img_with_stickers = img_bgr.copy()
for face in faces:
landmarks = face_landmarking(img_bgr, face)
# the landmarks 36 to 41 are for the left eye, and 42 to 47 are for the right eye
left_eye = [landmarks.part(i) for i in range(36, 42)]
right_eye = [landmarks.part(i) for i in range(42, 48)]
# Calculate the center points of the eyes
left_eye_center = (sum([p.x for p in left_eye]) // len(left_eye), sum([p.y for p in left_eye]) // len(left_eye))
right_eye_center = (sum([p.x for p in right_eye]) // len(right_eye), sum([p.y for p in right_eye]) // len(right_eye))
# Calculate the angle of tilt
dx = right_eye_center[0] - left_eye_center[0]
dy = right_eye_center[1] - left_eye_center[1]
angle = math.degrees(math.atan2(dy, dx)) # Angle in degrees
# Calculate the bounding box for the glasses based on the eye landmarks
glasses_width = int(abs(left_eye_center[0] - right_eye_center[0]) * 2)
glasses_height = int(glasses_width * glasses_bgra.height / glasses_bgra.width)
# Resize and rotate the glasses image
resized_glasses = glasses_bgra.resize((glasses_width, glasses_height))
rotated_glasses = resized_glasses.rotate(-0.8*angle, expand=True, resample=Image.BICUBIC) # Negative angle to correct orientation
# Calculate the position for the glasses, adjusting for the rotation
x1 = left_eye_center[0] - int(0.25 * glasses_width)
y1 = min(left_eye_center[1], right_eye_center[1]) - int(0.45 * glasses_height)
# Convert PIL image to NumPy array
glasses_np = np.array(rotated_glasses)
# Overlay the glasses on the image
img_with_stickers = overlay_transparent(img_with_stickers, glasses_np, x1, y1)
return img_with_stickers
def add_noses_sticker(img_bgr, sticker_path, faces):
nose_pil = Image.open(sticker_path)
# Check the color mode and convert to RGBA
nose_rgba = nose_pil.convert('RGBA')
# Convert the nose_rgba to BGRA
r, g, b, a = nose_rgba.split()
nose_bgra = Image.merge("RGBA", (b, g, r, a))
# A copy of the original image
img_with_stickers = img_bgr.copy()
for face in faces:
landmarks = face_landmarking(img_bgr, face)
# Assuming that the landmarks 27 to 35 are for the nose area
nose_area = [landmarks.part(i) for i in range(27, 36)]
# Calculate the bounding box for the nose based on the nose landmarks
nose_width = int(abs(nose_area[0].x - nose_area[-1].x) * 2.1)
nose_height = int(nose_width * nose_bgra.height / nose_bgra.width)
# the landmarks 31 and 35 are the leftmost and rightmost points of the nose area
nose_left = landmarks.part(31)
nose_right = landmarks.part(35)
# Calculate the center point of the nose
nose_center_x = (nose_left.x + nose_right.x) // 2
nose_top = landmarks.part(27) # Use 28 if it's more accurate
nose_bottom = landmarks.part(33)
# Calculate the midpoint of the vertical length of the nose
nose_center_y = (nose_top.y + nose_bottom.y) // 2
# Calculate the angle of tilt using the eyes as reference
left_eye_indices = range(36, 42)
right_eye_indices = range(42, 48)
angle = calculate_eye_angle(landmarks, left_eye_indices, right_eye_indices)
# Resize the nose image
resized_nose_pil = nose_bgra.resize((nose_width, nose_height))
rotated_nose = resized_nose_pil.rotate(-angle, expand=True, resample=Image.BICUBIC)
# the position for the nose
x1 = nose_center_x - (nose_width // 2)
y1 = nose_center_y - (nose_height // 2)+ int(0.1 * nose_height) # Adding a slight downward offset
# Convert PIL image to NumPy array
nose_np = np.array(rotated_nose)
# Overlay the nose on the image
img_with_stickers = overlay_transparent(img_with_stickers, nose_np, x1, y1)
return img_with_stickers
def add_animal_faces_sticker(img_bgr, sticker_path, faces):
animal_face_pil = Image.open(sticker_path)
# Check the color mode and convert to RGBA
animal_face_rgba = animal_face_pil.convert('RGBA')
# Convert the animal_face_rgba to BGRA
r, g, b, a = animal_face_rgba.split()
animal_face_bgra = Image.merge("RGBA", (b, g, r, a))
# A copy of the original image
img_with_stickers = img_bgr.copy()
for face in faces:
landmarks = face_landmarking(img_bgr, face)
# Find the top of the forehead using landmarks above the eyes
# Assuming landmarks 19 to 24 represent the eyebrows
forehead_top = min(landmarks.part(i).y for i in range(68, 81))
# Calculate the center point between the eyes as an anchor
left_eye = [landmarks.part(i) for i in range(36, 42)]
right_eye = [landmarks.part(i) for i in range(42, 48)]
eye_center_x = (left_eye[0].x + right_eye[3].x) // 2
eye_center_y = (left_eye[3].y + right_eye[0].y) // 2
# Calculate the size of the animal face sticker based on the width between the temples
head_width = int(abs(landmarks.part(0).x - landmarks.part(16).x)*1.4)
head_height = int(head_width * animal_face_bgra.height *1.2 / animal_face_bgra.width)
# Calculate the angle of tilt using the eyes as reference
left_eye_indices = range(36, 42)
right_eye_indices = range(42, 48)
angle = calculate_eye_angle(landmarks, left_eye_indices, right_eye_indices)
# Resize the animal face sticker
resized_animal_face = animal_face_bgra.resize((head_width, head_height))
rotated_animal_face = resized_animal_face.rotate(-angle, expand=True, resample=Image.BICUBIC)
# Calculate the position for the animal face sticker
x1 = eye_center_x - (head_width // 2)
y1 = forehead_top - int(0.18 * head_height)
# Convert PIL image to NumPy array
animal_face_np = np.array(rotated_animal_face)
# Overlay the animal face on the image
img_with_stickers = overlay_transparent(img_with_stickers, animal_face_np, x1, y1)
return img_with_stickers
# This dictionary will hold the user's sticker selections
# sticker_selections = {}
# Function to update sticker selections
def update_selections(category, selection):
sticker_selections[category] = None if selection == "None" else selection
return ""
# Function to load an example image
def load_example_image(image_path):
return gr.Image.from_file(image_path)
def resize_image(image, target_width, target_height):
# Maintain aspect ratio
original_width, original_height = image.size
ratio = min(target_width/original_width, target_height/original_height)
new_width = int(original_width * ratio)
new_height = int(original_height * ratio)
# Use Image.LANCZOS for high-quality downsampling
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
return resized_image
def get_face_crops(image_bgr, faces, target_width=500, target_height=130):
face_crops = []
for face in faces:
x, y, w, h = face.left(), face.top(), face.width(), face.height()
face_crop = image_bgr[y:y+h, x:x+w]
face_pil = Image.fromarray(cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB))
# Resize image to fit the display while maintaining aspect ratio
resized_face = resize_image(face_pil, target_width, target_height)
face_crops.append(resized_face)
return face_crops
# Function to process uploaded images and display face crops
def process_and_show_faces(image_input):
# Convert PIL image to OpenCV format BGR
image_bgr = cv2.cvtColor(np.array(image_input), cv2.COLOR_RGB2BGR)
# Detect faces
faces = face_detecting(image_bgr)
# Get individual face crops
face_crops = get_face_crops(image_bgr, faces)
# Return face crops to display them in the interface
return face_crops
face_outputs = []
for i in range(MAX_EXPECTED_FACES):
face_output = gr.Image(label=f"Face {i+1}")
face_outputs.append(face_output)
# This list will hold the Checkbox components for each face
checkboxes = []
def process_selected_faces(image_input, selected_face_indices):
# Convert PIL image to OpenCV format BGR
image_bgr = cv2.cvtColor(np.array(image_input), cv2.COLOR_RGB2BGR)
# Detect all faces
all_faces = face_detecting(image_bgr)
# Filter faces to get only those selected
faces = [all_faces[i] for i in selected_face_indices]
img_with_stickers = image_bgr.copy()
for category, sticker_name in sticker_selections.items():
if sticker_name: # Check if a sticker was selected in this category
# the sticker file path
if sticker_name != 'None':
sticker_path = os.path.join('stickers', category, sticker_name + '.png')
# Apply the selected sticker based on its category
if category == 'ears':
img_with_stickers = add_ears_sticker(img_with_stickers, sticker_path, faces)
elif category == 'glasses':
img_with_stickers = add_glasses_sticker(img_with_stickers, sticker_path, faces)
elif category == 'noses':
img_with_stickers = add_noses_sticker(img_with_stickers, sticker_path, faces)
elif category == 'headbands':
img_with_stickers = add_hats_sticker(img_with_stickers, sticker_path, faces)
elif category == 'hats':
img_with_stickers = add_hats_sticker(img_with_stickers, sticker_path, faces)
elif category == 'animal face':
img_with_stickers = add_animal_faces_sticker(img_with_stickers, sticker_path, faces)
else:
img_with_stickers = img_with_stickers
# Convert back to PIL image
img_with_stickers_pil = Image.fromarray(cv2.cvtColor(img_with_stickers, cv2.COLOR_BGR2RGB))
print("Selected stickers:")
for category, selection in sticker_selections.items():
print(f"{category}: {selection}")
return img_with_stickers_pil
def handle_face_selection(image_input, *checkbox_states):
selected_face_indices = [i for i, checked in enumerate(checkbox_states) if checked]
print("selected_face_indices:",selected_face_indices)
return process_selected_faces(image_input, selected_face_indices)
def update_interface_with_faces(image_input):
image_bgr = cv2.cvtColor(np.array(image_input), cv2.COLOR_RGB2BGR)
faces = face_detecting(image_bgr)
face_crops = get_face_crops(image_bgr, faces)
return [(face, f"Face {i+1}") for i, face in enumerate(face_crops)]
def detect_and_display_faces(image_input):
image_bgr = cv2.cvtColor(np.array(image_input), cv2.COLOR_RGB2BGR)
faces = face_detecting(image_bgr)
face_crops = get_face_crops(image_bgr, faces)
if not face_crops:
# Return empty images and unchecked boxes if no faces are detected
return [None] * MAX_EXPECTED_FACES + [False] * MAX_EXPECTED_FACES
# Return face crops and True for each checkbox to indicate they should be checked
# Pad the list with None and False if fewer faces than MAX_EXPECTED_FACES are detected
output = face_crops + [None] * (MAX_EXPECTED_FACES - len(face_crops))
output += [True] * len(face_crops) + [False] * (MAX_EXPECTED_FACES - len(face_crops))
return output
css = """
#category {
padding-left: 100px;
font-size: 20px;
font-weight: bold;
margin-top: 20px;
}
#sticker {
height: 130px;
width: 30px;
padding: 10px;
}
.radio {
display: flex;
justify-content: space-around;
}
"""
def handle_image_upload(image):
global sticker_selections
sticker_selections = {category: "None" for category in STICKER_PATHS.keys()}
print("reset sticker_selections called") # Reset selections when a new image is loaded
# Print out the sticker selections state for each category
for category, selection in sticker_selections.items():
print(f"{category}: {selection}")
return image
# Initialize the sticker selections dictionary
def initialize_sticker_selections():
return {
'hats': None,
'animal face': None,
'ears': None,
'glasses': None,
'noses': None,
'headbands': None
}
sticker_selections = initialize_sticker_selections()
radio_components = {}
# Create the Gradio interface
with gr.Blocks(css=css) as demo:
with gr.Row():
with gr.Column():
image_input = gr.Image(type="pil", label="Original Image")
image_input.change(
handle_image_upload,
inputs=[image_input],
outputs=[image_input]
)
with gr.Column():
output_image = gr.Image(label="Image with Stickers")
# Prepare the checkboxes and image placeholders
detect_faces_btn = gr.Button("Detect Faces")
with gr.Row():
face_checkboxes = [gr.Checkbox(label=f"Face {i+1}") for i in range(7)]
with gr.Row():
face_images = [gr.Image(height=150, width=100, min_width=30, interactive=False, show_download_button=False) for i in range(7)]
detect_faces_btn.click(
detect_and_display_faces,
inputs=[image_input],
outputs=face_images + face_checkboxes
)
process_button = gr.Button("Apply Stickers To Selected Faces")
process_button.click(
handle_face_selection,
# inputs=[image_input, face_checkboxes, sticker_selections],
inputs=[image_input] + face_checkboxes,
outputs=output_image
)
# Iterate over each category to create a row for the category
for category, stickers in STICKER_PATHS.items():
with gr.Row():
with gr.Column(scale=1, elem_id="category_row"):
gr.Markdown(f"## {category}", elem_id="category")
with gr.Column(scale=10):
# Iterate over stickers in sets of 10
for i in range(0, len(stickers), 10):
with gr.Row():
for sticker_path in stickers[i:i+10]:
gr.Image(value=sticker_path, min_width=50, interactive=False, show_download_button=False, container=False, elem_id="sticker")
with gr.Row():
# radio = gr.Radio(label=' ', choices=[stickers[i].split('/')[-1].replace('.png', '') for i in range(len(stickers))], container=False, min_width=50)
choices = [sticker.split('/')[-1].replace('.png', '') for sticker in stickers]
radio = gr.Radio(label='', choices=choices, value="None", container=False, min_width=50, elem_classes="radio")
radio.change(lambda selection, cat=category: update_selections(cat, selection), inputs=[radio], outputs=[])
radio_components[category] = radio # Store the radio component
demo.launch(share=True)