Skip to content

Commit

Permalink
Merge branch 'faces'
Browse files Browse the repository at this point in the history
  • Loading branch information
nfaltermeier committed Nov 13, 2022
2 parents e217bf0 + 2dd355d commit 45245c9
Show file tree
Hide file tree
Showing 12 changed files with 52,448 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,5 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/windows,python

!/src/lib/
autogen_buildtime.py
*.png
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Based off of https://github.com/minimaxir/gpt-2-cloud-run/blob/master/Dockerfile
FROM python:3.8-slim-buster
FROM python:3.10-slim-buster

WORKDIR /code

RUN pip3 --no-cache-dir install -U py-cord==2.0.0b5 python-dotenv
# opencv dependencies https://stackoverflow.com/a/63377623
RUN apt-get update && apt-get install -y ffmpeg libsm6 libxext6 && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY .env src/ secret-scholars-bot-config.json ./
RUN pip3 --no-cache-dir install -U py-cord python-dotenv markovchain pillow opencv-python

COPY .env src/ secret-scholars-bot-config.json markov.txt ./

RUN printf "build_time=%s" "'`date -Iseconds`'" > autogen_buildtime.py

Expand Down
8 changes: 5 additions & 3 deletions Dockerfile-arm32v6
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# Based off of https://github.com/minimaxir/gpt-2-cloud-run/blob/master/Dockerfile
FROM arm32v6/python:3.10-alpine

RUN apk update && apk add gcc musl-dev
RUN apk update && apk add gcc musl-dev py3-opencv zlib-dev jpeg-dev

WORKDIR /code

RUN pip3 --no-cache-dir install -U py-cord python-dotenv markovchain
ENV PYTHONPATH=$PYTHONPATH:/usr/lib/python3.10/site-packages

RUN pip3 --no-cache-dir install -U py-cord python-dotenv markovchain pillow

COPY .env src/ secret-scholars-bot-config.json markov.txt ./

RUN printf "build_time=%s" "'`date -Iseconds`'" > autogen_buildtime.py

CMD [ "python", "./bot.py" ]
# CMD [ "python", "./bot.py" ]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ docker run -d secret-scholars-bot
### ARM32v6
Run
```sh
docker run -d secret-scholars-bot secret-scholars-bot:arm32v6
docker run -d secret-scholars-bot:arm32v6
```
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
opencv-python
numpy
pillow
pycord
python-dotenv
discord
2 changes: 2 additions & 0 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timezone
import donut
import soup
import faces
import roll
import config
import asyncio
Expand Down Expand Up @@ -49,6 +50,7 @@ async def on_message(message):
await soup.on_message(message, client, config)
await excuse.on_message(message)
await markov.on_message(message)
await faces.on_message(message, client, config)
if await roll.on_message(message):
return

Expand Down
9 changes: 9 additions & 0 deletions src/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ excuses = [
("grocery shopping", 2)
] # An array of string messages or tuples containing a string message and a weight. Non-tuples have a weight on 1.
# Weights can be a decimal, 0 < w < infinity

# Face configs
face_picture = 'lib/face.png'

face_dims = [
[43, 208, 430, 430],
[245, 125, 82, 82],
[98, 131, 87, 87]
]
31 changes: 31 additions & 0 deletions src/faces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import lib.faces_util as faces_util
import os
import discord

BYTES_IN_MEGABYTE = 1048576

allowable_attachment_types = ['image/png', 'image/jpeg']

async def on_message(message, client, conf):
if message.content.startswith("$face"):
if len(message.attachments) > 0:
for attachment in message.attachments:
# check if attachment is a picture and is smaller than 8mb
if attachment.content_type in allowable_attachment_types and attachment.size < (8 * BYTES_IN_MEGABYTE):
if attachment.proxy_url != '404':
try:
await attachment.save('attachment_picture.png', use_cached=True)
found, path = faces_util.get_face_replace('attachment_picture.png')
if found:
await message.channel.send(file=discord.File(path))
else:
await message.channel.send('No face found :(')
if os.path.exists('face_detected.png'):
os.remove('face_detected.png')
except discord.HTTPException as error:
logging.exception(f'{datetime.now(timezone.utc)} Face on_message')
await message.channel.send('Could not download attachment :(')
except discord.NotFound as error:
logging.exception(f'{datetime.now(timezone.utc)} Face on_message')
await message.channel.send('Attachment not found :(')

130 changes: 130 additions & 0 deletions src/lib/faces_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import cv2
import numpy as np
import math
import aiohttp
import os
import config as conf
from PIL import Image

face_cascade = cv2.CascadeClassifier('lib/opencv/face_detector.xml')
eye_cascade = cv2.CascadeClassifier('lib/opencv/eye_detector.xml')
smile_cascade = cv2.CascadeClassifier('lib/opencv/smile_detector.xml')

def load_image_rgba(path):
og_image = Image.open(path)
rgba_image = og_image.convert(mode='RGBA')
img = np.array(rgba_image)
# B and R channels are swapped and there didn't seem to be a better way to do this
copy = np.array(img)
img[:,:,0] = copy[:,:,2]
img[:,:,2] = copy[:,:,0]
return img

# https://www.geeksforgeeks.org/python-smile-detection-using-opencv/
def box_faces(gray, frame):
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
for (x, y, w, h) in faces:
# print(f'{x}, {y}, {w}, {h}')
cv2.rectangle(frame, (x, y), ((x + w), (y + h)), (255, 0, 0, 255), 2)
roi_gray = gray[y:y + h, x:x + w]
roi_color = frame[y:y + h, x:x + w]
# smiles = smile_cascade.detectMultiScale(roi_gray, 1.8, 20)
eyes = eye_cascade.detectMultiScale(roi_gray)

for (sx, sy, sw, sh) in eyes:
# print(f'{sx}, {sy}, {sw}, {sh}')
cv2.rectangle(roi_color, (sx, sy), ((sx + sw), (sy + sh)), (0, 0, 255, 255), 2)
return frame

# https://stackoverflow.com/a/59211216
def rgba_overlay(background, foreground):
alpha_background = background[:,:,3] / 255.0
alpha_foreground = foreground[:,:,3] / 255.0

# set adjusted colors
for color in range(0, 3):
background[:,:,color] = alpha_foreground * foreground[:,:,color] + \
alpha_background * background[:,:,color] * (1 - alpha_foreground)

# set adjusted alpha and denormalize back to 0-255
background[:,:,3] = (1 - (1 - alpha_foreground) * (1 - alpha_background)) * 255

def replace_faces(gray, frame, replacement, replacement_dims):
faces = face_cascade.detectMultiScale(gray, 1.1, 5)
base_w = frame.shape[1]
base_h = frame.shape[0]
found = False
for (x, y, w, h) in faces:
found = True
overlay = np.zeros(frame.shape)
replacement_scale_x = w / replacement_dims[0][2]
replacement_scale_y = h / replacement_dims[0][3]
scaled_replacement_x = x - math.ceil(replacement_dims[0][0] * replacement_scale_x)
scaled_replacement_y = y - math.ceil(replacement_dims[0][1] * replacement_scale_y)
scaled_replacement_x = scaled_replacement_x if scaled_replacement_x > 0 else 0
scaled_replacement_y = scaled_replacement_y if scaled_replacement_y > 0 else 0
scaled_full_w = math.ceil(replacement.shape[1] * replacement_scale_x)
scaled_full_h = math.ceil(replacement.shape[0] * replacement_scale_y)
resized = cv2.resize(replacement, (scaled_full_w, scaled_full_h), interpolation=cv2.INTER_CUBIC)
up = base_h - (scaled_full_h + scaled_replacement_y)
up = -scaled_replacement_y if scaled_replacement_y + up < 0 else 0
left = base_w - (scaled_full_w + scaled_replacement_x)
left = -scaled_replacement_x if scaled_replacement_x + left < 0 else 0
insert_w = base_w - scaled_replacement_x if scaled_replacement_x + scaled_full_w > base_w else scaled_full_w
insert_h = base_h - scaled_replacement_y - up if scaled_replacement_y + scaled_full_h + up > base_h else scaled_full_h
overlay[scaled_replacement_y + up:scaled_replacement_y + insert_h + up, scaled_replacement_x + left:scaled_replacement_x + insert_w + left] = resized[0:insert_h, 0:insert_w]
rgba_overlay(frame, overlay)

return found, frame

def get_face_replace(replacing_image):
face_replace = load_image_rgba(conf.face_picture)
attachment_pic = load_image_rgba(replacing_image)
gray = cv2.cvtColor(attachment_pic, cv2.COLOR_RGBA2GRAY)
found,img = replace_faces(gray, attachment_pic, face_replace, conf.face_dims)

if found:
cv2.imwrite('face_detected.png', img)
return True, 'face_detected.png'

return False, None

if __name__ == '__main__':
img = load_image_rgba('student_small.png')
# img = load_image_rgba('group.png')
student = np.array(img)

face = load_image_rgba('face.png')
face_dims = [
[43, 208, 430, 430],
[245, 125, 82, 82],
[98, 131, 87, 87]
]
gray = cv2.cvtColor(img, cv2.COLOR_RGBA2GRAY)
found,img = replace_faces(gray, img, face, face_dims)
cv2.imwrite("face_detected.png", img)
print('Successfully saved')

# A video with faces applied for testing purposes
# video_capture = cv2.VideoCapture(0)
# while video_capture.isOpened():
# # Captures video_capture frame by frame
# _, frame = video_capture.read()

# # To capture image in monochrome
# gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

# # calls the box_faces() function
# canvas = box_faces(gray, frame)

# # Displays the result on camera feed
# cv2.imshow('Video', canvas)

# # The control breaks once q key is pressed
# if cv2.waitKey(1) & 0xff == ord('q'):
# break

# # Release the capture once all the processing is done.
# video_capture.release()
# cv2.destroyAllWindows()

Loading

0 comments on commit 45245c9

Please sign in to comment.