Browse Source

Add face captioning

denikson 6 years ago
parent
commit
ea0f2cd236
4 changed files with 193 additions and 67 deletions
  1. 2 1
      .prettierrc
  2. 131 55
      commands/facemorph.js
  3. 4 1
      commands/react.js
  4. 56 10
      db.js

+ 2 - 1
.prettierrc

@@ -1,3 +1,4 @@
 {
-    "tabWidth": 4
+    "tabWidth": 4,
+    "printWidth": 100
 }

+ 131 - 55
commands/facemorph.js

@@ -11,29 +11,110 @@ const EMOTE_GUILD = "505333548694241281";
 const animeCascade = new cv.CascadeClassifier(path.resolve(__dirname, "./animu.xml"));
 const faceCascade = new cv.CascadeClassifier(cv.HAAR_FRONTALFACE_ALT2);
 
-
 function intersects(r1, r2) {
-    return (r1.x <= r2.x + r2.width && r1.x + r1.width >= r2.x)
-        && (r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y);
+    return (
+        r1.x <= r2.x + r2.width &&
+        r1.x + r1.width >= r2.x &&
+        (r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y)
+    );
+}
+
+async function morphFaces(faces, data) {
+    let jimpImage = await Jimp.read(data);
+    let emojiKeys = [
+        ...client.guilds
+            .get(EMOTE_GUILD)
+            .emojis.filter(e => !e.animated)
+            .keys()
+    ];
+
+    for (const rect of faces) {
+        let dx = rect.x + rect.width / 2;
+        let dy = rect.y + rect.height / 2;
+
+        let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
+        let emoji = client.emojis.get(emojiKey);
+
+        let emojiImage = await Jimp.read(emoji.url);
+        let ew = emojiImage.getWidth();
+        let eh = emojiImage.getHeight();
+
+        const CONSTANT_SCALE = 1.1;
+        let scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
+        ew *= scaleFactor;
+        eh *= scaleFactor;
+
+        emojiImage = emojiImage.scale(scaleFactor);
+        jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
+    }
+
+    return jimpImage;
+}
+
+const CAPTION_OFFSET = 5;
+
+async function captionFace(faces, data) {
+    let face = faces[Math.floor(Math.random() * faces.length)];
+    let squaredFace = await face.toSquareAsync();
+
+    let targetSize = db.get("faceEditConfig.captionedImageSize").value();
+
+    let img = await Jimp.read(data);
+
+    let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
+    tempImg = await tempImg.blit(
+        img,
+        0,
+        0,
+        squaredFace.x,
+        squaredFace.y,
+        squaredFace.width,
+        squaredFace.height
+    );
+    tempImg = await tempImg.scale(targetSize / squaredFace.width);
+
+    let font = await Jimp.loadFont(Jimp.FONT_SANS_16_BLACK);
+
+    let text = `${db.get("faceCaptions.pre").randomElement().value()} ${db.get("faceCaptions.post").randomElement().value()}`;
+
+    let h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
+
+    let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, 0xffffffff);
+
+    finalImage = await finalImage.print(
+        font,
+        CAPTION_OFFSET,
+        CAPTION_OFFSET,
+        text,
+        finalImage.getWidth() - CAPTION_OFFSET
+    );
+    finalImage = await finalImage.composite(tempImg, 0, CAPTION_OFFSET * 2 + h);
+
+    return finalImage;
 }
 
-async function processFaceSwap(message, attachmentUrl, failMessage, successMessage) {
-    let data = await request(attachmentUrl, {encoding: null});
+async function processFaceSwap(message, attachmentUrl, processor, failMessage, successMessage) {
+    let data = await request(attachmentUrl, { encoding: null });
 
     let im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
 
     let gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
     let normGray = await gray.equalizeHistAsync();
 
-    let animeFaces = await animeCascade.detectMultiScaleAsync(normGray, 1.1, 5, 0, new cv.Size(24, 24));
+    let animeFaces = await animeCascade.detectMultiScaleAsync(
+        normGray,
+        1.1,
+        5,
+        0,
+        new cv.Size(24, 24)
+    );
     let normalFaces = await faceCascade.detectMultiScaleAsync(gray);
 
-    if(animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
-        if (failMessage)
-            message.channel.send(failMessage);
+    if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
+        if (failMessage) message.channel.send(failMessage);
         return;
     }
-    
+
     let faces = [...normalFaces.objects, ...animeFaces.objects];
 
     let normalCount = normalFaces.objects.length;
@@ -41,18 +122,17 @@ async function processFaceSwap(message, attachmentUrl, failMessage, successMessa
 
     for (let i = 0; i < normalCount; i++) {
         const rNormal = faces[i];
-        
-        if(animeCount == 0)
-            break;
+
+        if (animeCount == 0) break;
 
         for (let j = normalCount; j < faces.length; j++) {
             const rAnime = faces[j];
-            
-            if(intersects(rAnime, rNormal)) {
+
+            if (intersects(rAnime, rNormal)) {
                 let animeA = rAnime.width * rAnime.height;
                 let faceA = rNormal.width * rNormal.height;
 
-                if(animeA > faceA){
+                if (animeA > faceA) {
                     faces.splice(i, 1);
                     normalCount--;
                     i--;
@@ -66,33 +146,22 @@ async function processFaceSwap(message, attachmentUrl, failMessage, successMessa
         }
     }
 
-    let jimpImage = await Jimp.read(data);
-    let emojiKeys = [...client.guilds.get(EMOTE_GUILD).emojis.filter(e => !e.animated).keys()];
-
-    for (const rect of faces) {
-        let dx = rect.x + rect.width / 2;
-        let dy = rect.y + rect.height / 2;
-
-        let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
-        let emoji = client.emojis.get(emojiKey);
-
-        let emojiImage = await Jimp.read(emoji.url);
-        let ew = emojiImage.getWidth();
-        let eh = emojiImage.getHeight();
-
-        const CONSTANT_SCALE = 1.1;
-        let scaleFactor = Math.max(rect.width, rect.height) / Math.min(ew, eh) * CONSTANT_SCALE;
-        ew *= scaleFactor;
-        eh *= scaleFactor;
-
-        emojiImage = emojiImage.scale(scaleFactor);
-        jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
+    let jimpImage;
+    if(processor)
+        jimpImage = await processor(faces, data);
+    else {
+        if(Math.random() <= db.get("faceEditConfig.captionProbability").value())
+            jimpImage = await captionFace(faces, data);
+        else
+            jimpImage = await morphFaces(faces, data);
     }
 
     jimpImage.quality(90);
     let buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
 
-    let messageContents = successMessage || `I noticed a face in the image. I think this looks better ${client.emojis.get("505076258753740810").toString()}`;
+    let messageContents =
+        successMessage ||
+        `I noticed a face in the image. I think this looks better ${client.emojis.get("505076258753740810").toString()}`;
 
     message.channel.send(messageContents, {
         files: [buffer]
@@ -100,23 +169,22 @@ async function processFaceSwap(message, attachmentUrl, failMessage, successMessa
 }
 
 const onMessage = (msg, contents, actionsDone) => {
-    if (actionsDone)
-        return false;
+    if (actionsDone) return false;
 
-    if(msg.mentions.users.size > 0 && msg.mentions.users.first().id == client.user.id)
+    if (msg.mentions.users.size > 0 && msg.mentions.users.first().id == client.user.id)
         return false;
 
     let imageAttachment = msg.attachments.find(v => util.isValidImage(v.filename));
 
     if (imageAttachment) {
         let probValue = db.get("faceEditChannels").get(msg.channel.id);
-        if (probValue.isUndefined().value() || probValue.isNull().value())
-            return false;
+        if (probValue.isUndefined().value() || probValue.isNull().value()) return false;
 
-        if (Math.random() > probValue.value())
-            return false;
+        if (Math.random() > probValue.value()) return false;
 
-        processFaceSwap(msg, imageAttachment.url).catch(err => console.log(`Failed to run faceapp because ${err}`));
+        processFaceSwap(msg, imageAttachment.url).catch(err =>
+            console.log(`Failed to run faceapp because ${err}`)
+        );
         return true;
     }
 
@@ -124,24 +192,32 @@ const onMessage = (msg, contents, actionsDone) => {
 };
 
 const onDirectMention = (msg, content, actionsDone) => {
-    if (actionsDone)
-        return false;
+    if (actionsDone) return false;
 
     let image = msg.attachments.find(v => util.isValidImage(v.filename));
-    if (!image){
-        if(msg.attachments.size > 0) {
-            msg.channel.send(`${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`);
+    if (!image) {
+        if (msg.attachments.size > 0) {
+            msg.channel.send(
+                `${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`
+            );
             return true;
         }
         return false;
     }
 
+    let processor;
+    if(content.startsWith("caption this"))
+        processor = captionFace;
+    else
+        processor = morphFaces;
+
     processFaceSwap(
-        msg, 
+        msg,
         image.url,
+        processor,
         `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
-        `${msg.author.toString()} ${client.emojis.get("505076258753740810").toString()}`)
-        .catch(err => console.log(`Failed to run faceapp because ${err}`));
+        `${msg.author.toString()} ${client.emojis.get("505076258753740810").toString()}`
+    ).catch(err => console.log(`Failed to run faceapp because ${err}`));
 
     return true;
 };
@@ -149,4 +225,4 @@ const onDirectMention = (msg, content, actionsDone) => {
 module.exports = {
     onMessage: onMessage,
     onDirectMention: onDirectMention
-};
+};

+ 4 - 1
commands/react.js

@@ -110,7 +110,10 @@ const onIndirectMention = (msg, actionsDone) => {
         emote = client.emojis.find(e => e.id == id);
     }
 
-    msg.channel.send(client.emojis.find(e => e.id == id).toString());
+    if(!emote)
+        return false;
+
+    msg.channel.send(emote.toString());
     return true;
 };
 

+ 56 - 10
db.js

@@ -37,10 +37,7 @@ db.defaults({
             "489771390279483394",
             "489773282036547599"
         ],
-        ded: [
-            "505677683544293377",
-            "505677683384909824"
-        ]
+        ded: ["505677683544293377", "505677683384909824"]
     },
     specialUsers: [
         "141880968800763905",
@@ -48,7 +45,7 @@ db.defaults({
         "335898304472678402"
     ],
     reactableMentionedUsers: ["307897683849510912"],
-    bigUsers : ["432821242366656512"],
+    bigUsers: ["432821242366656512"],
     dedUsers: ["326520875027267584"],
     guides: [],
     editors: {
@@ -67,17 +64,66 @@ db.defaults({
         }
     ],
     postedNewsGuids: {},
-    feedOutputs: [
-        "422693157692637184"
-    ],
+    feedOutputs: ["422693157692637184"],
     messageReactions: {},
     faceEditChannels: {
-        "459622760839118848" : 1.0,
+        "459622760839118848": 1.0,
         "297109482905796608": 0.25,
         "297117726546198528": 0.1,
         "401837400109875216": 0.1
     },
+    faceEditConfig: {
+        captionProbability: 0.33,
+        captionedImageSize: 300
+    },
+    faceCaptions: {
+        pre: [
+            "Me when",
+            "I think",
+            "That face when",
+            "That look when",
+            "She:",
+            "He:",
+            "KISS when",
+            "Everyone thinks",
+            "Today",
+            "They look like"
+        ],
+        post: [
+            "my kokoro goes fuwafuwa",
+            "Horse Muku is real",
+            "he THICC",
+            "she THICC",
+            "OwO",
+            "fuzzy pickles",
+            "baka",
+            "too many carrots",
+            "Elon Musk-chan builds a mecha",
+            "gushing granny",
+            "too much coke",
+            "not baka",
+            "want the D",
+            "cringe",
+            "Harambe",
+            "fire",
+            "see lewd",
+            "drink coffee",
+            "horse",
+            "run out of RAM",
+            "passworded mods",
+            "I get pinged",
+            "I sleep",
+            "he attacc",
+            "she protecc",
+            "dead inside",
+            "why?",
+            "what?",
+            "how?",
+            "my waifu leaves me",
+            "I nut"
+        ]
+    },
     quotes: []
 }).write();
 
-module.exports = db;
+module.exports = db;