init
This commit is contained in:
commit
b6ed2f7e4a
12 changed files with 4272 additions and 0 deletions
5
.env.example
Normal file
5
.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
TELEGRAM_BOT_TOKEN =
|
||||
MONGODB_URI =
|
||||
MONGO_DB =
|
||||
MONGODB_CERT_PATH =
|
||||
MONGODB_COLLECTION =
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.env
|
||||
node_modules
|
||||
63
README.md
Normal file
63
README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Face Detection and Overlay Image Project
|
||||
|
||||
This project is meant to explore and understand the functions of Telegram bots. This project uses the `face-api.js` library to detect faces in an image and overlay an image on the detected faces.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Clone the repository.
|
||||
2. Run `npm install` to install the dependencies.
|
||||
3. Create a `.env` file in the root directory and add your Telegram bot token as `TELEGRAM_BOT_TOKEN`.
|
||||
|
||||
### Arm64
|
||||
|
||||
Note: For arm64 architecture, there is an issue with `canvas` library. To fix this issue run `brew install pkg-config cairo pango libpng jpeg giflib librsvg` first, given that you have homebrew installed, and then run `npm install`.
|
||||
|
||||
https://brew.sh/
|
||||
|
||||
https://github.com/Automattic/node-canvas/issues/1662#issuecomment-1465269869
|
||||
|
||||
### Telegram
|
||||
|
||||
To get the telegram bot token, visit [https://t.me/BotFather](https://t.me/BotFather) and create a new bot.
|
||||
|
||||
Telegram documentation: https://core.telegram.org/bots/tutorial
|
||||
|
||||
### MongoDB
|
||||
|
||||
Get your MongoDB cert and the DB's URI from the website. You might have to whitelist connections from your IP or server in your MongoDB's settings
|
||||
|
||||
## Usage
|
||||
|
||||
Run `node index.js` to start the application.
|
||||
|
||||
Run `npm run start` to start the application in the background.
|
||||
|
||||
Open Telegram, go to your bot, send /start to start the bot and send it a photo
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `index.js`: The main application file. It sets up the face detection models, defines the functions for face detection and overlaying images, and handles the Telegram bot interactions.
|
||||
- `assets/`: Directory containing the overlay image.
|
||||
- `models/`: Directory containing the face detection models. The models can be downloaded from the [face-api.js Github repository](https://github.com/justadudewhohacks/face-api.js/tree/master/weights).
|
||||
|
||||
## Functions
|
||||
|
||||
- `detectFaces(imagePath)`: Detects faces in the given image.
|
||||
- `overlayImageOnFaces(imagePath, detections)`: Overlays an image on the detected faces.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN`: Your Telegram bot token.
|
||||
- `MONGODB_URI`: Your MongoDB URI.
|
||||
- `MONGO_DB`: Your MongoDB database name.
|
||||
- `MONGODB_CERT_PATH`: Path to your MongoDB certificate. (If you're using cloud MongoDB)
|
||||
- `MONGODB_COLLECTION`: Your MongoDB collection name.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `node-telegram-bot-api`: For interacting with the Telegram API.
|
||||
- `face-api.js`: For face detection.
|
||||
- `canvas`: For image manipulation.
|
||||
- `dotenv`: For loading environment variables from a `.env` file.
|
||||
- `mongodb`: For connecting to the MongoDB database.
|
||||
- `pm2`: For managing the application in the background.
|
||||
BIN
assets/overlay.png
Normal file
BIN
assets/overlay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
318
index.js
Normal file
318
index.js
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
const TelegramBot = require("node-telegram-bot-api");
|
||||
const fs = require("fs");
|
||||
const faceapi = require("face-api.js");
|
||||
const canvas = require("canvas");
|
||||
require("dotenv").config();
|
||||
const crypto = require("crypto");
|
||||
const { MongoClient, ServerApiVersion } = require('mongodb');
|
||||
|
||||
// MongoDB variables
|
||||
const credentials = process.env.MONGODB_CERT_PATH;
|
||||
const mongoUri = process.env.MONGODB_URI;
|
||||
const dbName = process.env.MONGO_DB;
|
||||
const dbCollection = process.env.MONGODB_COLLECTION;
|
||||
let db;
|
||||
|
||||
//
|
||||
// MongoDB
|
||||
//
|
||||
// Connect to MongoDB
|
||||
async function connectToMongo() {
|
||||
const client = new MongoClient(mongoUri, {
|
||||
tlsCertificateKeyFile: credentials,
|
||||
serverApi: ServerApiVersion.v1
|
||||
});
|
||||
await client.connect();
|
||||
db = client.db(dbName);
|
||||
console.log('Connected to MongoDB');
|
||||
}
|
||||
|
||||
// Function to save user height preference to MongoDB
|
||||
async function saveUserPreference(userId, height) {
|
||||
try {
|
||||
const preferencesCollection = db.collection(dbCollection);
|
||||
await preferencesCollection.updateOne({ userId }, { $set: { height } }, { upsert: true });
|
||||
console.log(`Saved user preference for user ${userId}: ${height}`);
|
||||
} catch (error) {
|
||||
console.error('Error saving user preference:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load user height preference from MongoDB
|
||||
async function loadUserPreference(userId) {
|
||||
try {
|
||||
const preferencesCollection = db.collection(dbCollection);
|
||||
const userPreference = await preferencesCollection.findOne({ userId });
|
||||
return userPreference ? userPreference.height : '50%';
|
||||
} catch (error) {
|
||||
console.error('Error loading user preference:', error);
|
||||
return '50%';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Face-api.js
|
||||
//
|
||||
// Path to the overlay image
|
||||
const overlayImagePath = "./assets/overlay.png";
|
||||
|
||||
// Set up canvas
|
||||
faceapi.env.monkeyPatch({
|
||||
Canvas: canvas.Canvas,
|
||||
Image: canvas.Image,
|
||||
ImageData: canvas.ImageData,
|
||||
});
|
||||
|
||||
// Load face detection models
|
||||
Promise.all([
|
||||
faceapi.nets.tinyFaceDetector.loadFromDisk("./models"),
|
||||
faceapi.nets.faceLandmark68Net.loadFromDisk("./models"),
|
||||
]);
|
||||
|
||||
// Function to detect faces in an image
|
||||
async function detectFaces(imagePath) {
|
||||
try {
|
||||
// Load the image
|
||||
const img = await canvas.loadImage(imagePath);
|
||||
|
||||
// Detect faces in the image
|
||||
const detections = await faceapi
|
||||
.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions())
|
||||
.withFaceLandmarks();
|
||||
|
||||
return detections;
|
||||
} catch (error) {
|
||||
console.error("Error detecting faces:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to overlay an image onto the detected faces
|
||||
async function overlayImageOnFaces(imagePath, detections, userHeight) {
|
||||
try {
|
||||
// Load the base image
|
||||
const img = await canvas.loadImage(imagePath);
|
||||
const ctx = canvas.createCanvas(img.width, img.height).getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Load the overlay image
|
||||
const overlayImg = await canvas.loadImage(overlayImagePath);
|
||||
|
||||
if (detections.length === 0) {
|
||||
const imageWidth = img.width * 0.3; // Adjust image width based on face width
|
||||
const imageHeight = imageWidth * (overlayImg.height / overlayImg.width); // Maintain aspect ratio
|
||||
|
||||
// Calculate the position to place the image in the middle of the canvas
|
||||
const centerX = img.width / 2 - imageWidth / 2;
|
||||
let y;
|
||||
switch (userHeight) {
|
||||
case '100%':
|
||||
y = 0; // Top of the image
|
||||
break;
|
||||
case '75%':
|
||||
y = img.height * 0.25 - imageHeight / 2; // Between top and middle
|
||||
break;
|
||||
case '50%':
|
||||
y = img.height / 2 - imageHeight / 2; // Center
|
||||
break;
|
||||
case '25%':
|
||||
y = img.height * 0.75 - imageHeight / 2; // Between bottom and middle
|
||||
break;
|
||||
default:
|
||||
// Handle invalid preference
|
||||
y = img.height / 2 - imageHeight / 2; // Default to top of the image
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw the image in the middle of the canvas
|
||||
ctx.drawImage(overlayImg, centerX, y, imageWidth, imageHeight);
|
||||
} else {
|
||||
// Loop through each detected face and overlay the image
|
||||
detections.forEach((detection) => {
|
||||
const box = detection.detection.box;
|
||||
|
||||
const landmarks = detection.landmarks;
|
||||
const topCenter = landmarks.getNose()[0]; // Assuming the nose is the top-center of the face
|
||||
|
||||
const imageWidth = box.width * 0.85; // Adjust image width based on face width
|
||||
const imageHeight = imageWidth * (overlayImg.height / overlayImg.width); // Maintain aspect ratio
|
||||
ctx.drawImage(
|
||||
overlayImg,
|
||||
topCenter.x - imageWidth / 2,
|
||||
topCenter.y - imageHeight / 2,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.canvas;
|
||||
} catch (error) {
|
||||
console.error("Error overlaying image on faces:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Misc functions
|
||||
//
|
||||
function randomStartPhrase(){
|
||||
const phrases = [
|
||||
"Send me any picture with a face and I'll make it COOL for you!",
|
||||
"Send me an image to get started.",
|
||||
"Send a picture to begin.",
|
||||
"Share an image so we can start.",
|
||||
"Got a photo? Let's make it COOL!",
|
||||
"Let's start with an image with a face in it.",
|
||||
"Start by sending me an image.",
|
||||
]
|
||||
return phrases[crypto.randomInt(0, phrases.length)];
|
||||
}
|
||||
|
||||
function randomHoldPhrase(){
|
||||
const phrases = [
|
||||
"Please hold...",
|
||||
"Hold please...",
|
||||
"One moment please...",
|
||||
"Hang tight for a moment...",
|
||||
"Just a moment...",
|
||||
"Kindly wait...",
|
||||
"Bear with me for a moment...",
|
||||
"Give me a moment...",
|
||||
"I'll be right with you...",
|
||||
"Just a second...",
|
||||
"Allow me a moment...",
|
||||
"Hold tight...",
|
||||
"Just hang on a moment...",
|
||||
]
|
||||
|
||||
return phrases[crypto.randomInt(0, phrases.length)];
|
||||
}
|
||||
|
||||
// Function to send an inline keyboard for setting image height preferences
|
||||
async function sendOverlayHeightKeyboard(chatId, chatType) {
|
||||
// Check if the chat is a group chat
|
||||
if (chatType === 'group' || chatType === 'supergroup') {
|
||||
bot.sendMessage(chatId, 'Sorry, the /setheight command is disabled in group chats. DM me to set your preferred height');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[{ text: '100% (Top)', callback_data: '100%' }],
|
||||
[{ text: '75% (Middle Top)', callback_data: '75%' }],
|
||||
[{ text: '50% (Middle)', callback_data: '50%' }],
|
||||
[{ text: '25% (Lower)', callback_data: '25%' }]
|
||||
]
|
||||
};
|
||||
|
||||
// Send the inline keyboard to the user
|
||||
await bot.sendMessage(chatId, 'Select your preferred height:', { reply_markup: JSON.stringify(keyboard) });
|
||||
}
|
||||
|
||||
//
|
||||
// Telegram
|
||||
//
|
||||
// Telegram bot token
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
|
||||
// Create a bot instance
|
||||
const bot = new TelegramBot(token, { polling: true });
|
||||
|
||||
// Event listener for incoming commands
|
||||
bot.onText(/^\/start$/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
// Reply with a description of the bot
|
||||
const description = randomStartPhrase();
|
||||
await bot.sendMessage(chatId, description + ` \n\nWorks well if there's a face in the image and the eyes are horizontal.\nUse command /setheight to set your preferred height for images without a face. (Default is 50%)\n`);
|
||||
});
|
||||
|
||||
// Check if the message contains command for help
|
||||
bot.onText(/^\/help$/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
// Reply with helpful information
|
||||
const description =
|
||||
`Send an image to make your photo COOL! Works really well if there's a face in the image and your eyes are horizontal.
|
||||
|
||||
/start to get greeted
|
||||
/setheight to set your preferred height on the image if there is no face detected. (Default is 50%)
|
||||
/help to open this help message`;
|
||||
await bot.sendMessage(chatId, description);
|
||||
})
|
||||
|
||||
|
||||
// Event listener for incoming commands
|
||||
bot.onText(/^\/setheight$/, async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
const chatType = msg.chat.type;
|
||||
|
||||
// Send the inline keyboard for setting hat height preferences
|
||||
await sendOverlayHeightKeyboard(chatId, chatType);
|
||||
});
|
||||
|
||||
// Handle callback queries
|
||||
bot.on('callback_query', async (callbackQuery) => {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const userId = callbackQuery.from.id;
|
||||
const height = callbackQuery.data; // Get the selected height from the callback data
|
||||
const username = callbackQuery.from.username; // Get the username of the user
|
||||
|
||||
// Save user preference to MongoDB
|
||||
await saveUserPreference(userId, height);
|
||||
|
||||
// Send a confirmation message to the user
|
||||
await bot.sendMessage(chatId, `Your preferred height has been set to ${height}.`);
|
||||
});
|
||||
|
||||
// Event listener for incoming messages
|
||||
bot.on("message", async (msg) => {
|
||||
const chatId = msg.chat.id;
|
||||
const userId = msg.from.id;
|
||||
|
||||
// Check if the message contains an image
|
||||
if (msg.photo) {
|
||||
try {
|
||||
// Send a message to the user
|
||||
const holdReply = randomHoldPhrase();
|
||||
await bot.sendMessage(chatId, holdReply);
|
||||
|
||||
// Get the file ID of the largest photo
|
||||
const fileId = msg.photo[msg.photo.length - 1].file_id;
|
||||
|
||||
// Get the file path of the photo
|
||||
const filePath = await bot.getFileLink(fileId);
|
||||
|
||||
// Detect faces in the image
|
||||
const detections = await detectFaces(filePath);
|
||||
|
||||
// Load user's preferred image height from MongoDB
|
||||
const userHeight = await loadUserPreference(userId);
|
||||
|
||||
// Overlay the supplied image onto the detected faces
|
||||
const imageWithOverlay = await overlayImageOnFaces(filePath, detections, userHeight);
|
||||
|
||||
// Save the resulting image
|
||||
const resultPath = Date.now() + "result_image.png";
|
||||
const out = fs.createWriteStream(resultPath);
|
||||
const stream = imageWithOverlay.createPNGStream();
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.pipe(out);
|
||||
out.on("finish", resolve);
|
||||
out.on("error", reject);
|
||||
});
|
||||
|
||||
// Send the resulting image back to the user
|
||||
await bot.sendPhoto(chatId, fs.readFileSync(resultPath));
|
||||
|
||||
// Delete the temporary image file
|
||||
fs.unlinkSync(resultPath);
|
||||
} catch (err) {
|
||||
console.error("Error:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Connect to MongoDB
|
||||
connectToMongo();
|
||||
BIN
models/face_landmark_68_model-shard1
Normal file
BIN
models/face_landmark_68_model-shard1
Normal file
Binary file not shown.
1
models/face_landmark_68_model-weights_manifest.json
Normal file
1
models/face_landmark_68_model-weights_manifest.json
Normal file
File diff suppressed because one or more lines are too long
BIN
models/face_recognition_model-shard1
Normal file
BIN
models/face_recognition_model-shard1
Normal file
Binary file not shown.
BIN
models/tiny_face_detector_model-shard1
Normal file
BIN
models/tiny_face_detector_model-shard1
Normal file
Binary file not shown.
1
models/tiny_face_detector_model-weights_manifest.json
Normal file
1
models/tiny_face_detector_model-weights_manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}]
|
||||
3856
package-lock.json
generated
Normal file
3856
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "overlayShade",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "pm2 start index.js --name overlayShade",
|
||||
"stop": "pm2 stop overlayShade",
|
||||
"restart": "pm2 restart overlayShade",
|
||||
"logs": "pm2 logs overlayShade",
|
||||
"delete": "pm2 delete overlayShade",
|
||||
"list": "pm2 status overlayShade",
|
||||
"monit": "pm2 monit"
|
||||
},
|
||||
"author": "afiqzudinhadi",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"canvas": "^2.11.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"face-api.js": "^0.22.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"mongodb": "^6.5.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"pm2": "^5.3.1"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue