facemorph.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. const db = require("../db.js");
  2. const util = require("../util.js");
  3. const Jimp = require("jimp");
  4. const client = require("../client.js");
  5. const cv = require("opencv4nodejs");
  6. const path = require("path");
  7. const request = require("request-promise-native");
  8. const EMOTE_GUILD = "505333548694241281";
  9. const animeCascade = new cv.CascadeClassifier(path.resolve(__dirname, "./animu.xml"));
  10. const faceCascade = new cv.CascadeClassifier(cv.HAAR_FRONTALFACE_ALT2);
  11. function intersects(r1, r2) {
  12. return (
  13. r1.x <= r2.x + r2.width &&
  14. r1.x + r1.width >= r2.x &&
  15. (r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y)
  16. );
  17. }
  18. async function morphFaces(faces, data) {
  19. let jimpImage = await Jimp.read(data);
  20. let emojiKeys = [
  21. ...client.guilds
  22. .get(EMOTE_GUILD)
  23. .emojis.filter(e => !e.animated)
  24. .keys()
  25. ];
  26. for (const rect of faces) {
  27. let dx = rect.x + rect.width / 2;
  28. let dy = rect.y + rect.height / 2;
  29. let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
  30. let emoji = client.emojis.get(emojiKey);
  31. let emojiImage = await Jimp.read(emoji.url);
  32. let ew = emojiImage.getWidth();
  33. let eh = emojiImage.getHeight();
  34. const CONSTANT_SCALE = 1.1;
  35. let scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
  36. ew *= scaleFactor;
  37. eh *= scaleFactor;
  38. emojiImage = emojiImage.scale(scaleFactor);
  39. jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
  40. }
  41. return jimpImage;
  42. }
  43. const CAPTION_OFFSET = 5;
  44. async function captionFace(faces, data) {
  45. let face = faces[Math.floor(Math.random() * faces.length)];
  46. let squaredFace = await face.toSquareAsync();
  47. let targetSize = db.get("faceEditConfig.captionedImageSize").value();
  48. let img = await Jimp.read(data);
  49. let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
  50. tempImg = await tempImg.blit(
  51. img,
  52. 0,
  53. 0,
  54. squaredFace.x,
  55. squaredFace.y,
  56. squaredFace.width,
  57. squaredFace.height
  58. );
  59. tempImg = await tempImg.scale(targetSize / squaredFace.width);
  60. let font = await Jimp.loadFont(Jimp.FONT_SANS_16_BLACK);
  61. let text = `${db.get("faceCaptions.pre").randomElement().value()} ${db.get("faceCaptions.post").randomElement().value()}`;
  62. let h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
  63. let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, 0xffffffff);
  64. finalImage = await finalImage.print(
  65. font,
  66. CAPTION_OFFSET,
  67. CAPTION_OFFSET,
  68. text,
  69. finalImage.getWidth() - CAPTION_OFFSET
  70. );
  71. finalImage = await finalImage.composite(tempImg, 0, CAPTION_OFFSET * 2 + h);
  72. return finalImage;
  73. }
  74. async function processFaceSwap(message, attachmentUrl, processor, failMessage, successMessage) {
  75. let data = await request(attachmentUrl, { encoding: null });
  76. let im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
  77. let gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
  78. let normGray = await gray.equalizeHistAsync();
  79. let animeFaces = await animeCascade.detectMultiScaleAsync(
  80. normGray,
  81. 1.1,
  82. 5,
  83. 0,
  84. new cv.Size(24, 24)
  85. );
  86. let normalFaces = await faceCascade.detectMultiScaleAsync(gray);
  87. if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
  88. if (failMessage) message.channel.send(failMessage);
  89. return;
  90. }
  91. let faces = [...normalFaces.objects, ...animeFaces.objects];
  92. let normalCount = normalFaces.objects.length;
  93. let animeCount = animeFaces.objects.length;
  94. for (let i = 0; i < normalCount; i++) {
  95. const rNormal = faces[i];
  96. if (animeCount == 0) break;
  97. for (let j = normalCount; j < faces.length; j++) {
  98. const rAnime = faces[j];
  99. if (intersects(rAnime, rNormal)) {
  100. let animeA = rAnime.width * rAnime.height;
  101. let faceA = rNormal.width * rNormal.height;
  102. if (animeA > faceA) {
  103. faces.splice(i, 1);
  104. normalCount--;
  105. i--;
  106. break;
  107. } else {
  108. faces.splice(j, 1);
  109. animeCount--;
  110. j--;
  111. }
  112. }
  113. }
  114. }
  115. let jimpImage;
  116. if(processor)
  117. jimpImage = await processor(faces, data);
  118. else {
  119. if(Math.random() <= db.get("faceEditConfig.captionProbability").value())
  120. jimpImage = await captionFace(faces, data);
  121. else
  122. jimpImage = await morphFaces(faces, data);
  123. }
  124. jimpImage.quality(90);
  125. let buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
  126. let messageContents =
  127. successMessage ||
  128. `I noticed a face in the image. I think this looks better ${client.emojis.get("505076258753740810").toString()}`;
  129. message.channel.send(messageContents, {
  130. files: [buffer]
  131. });
  132. global.gc();
  133. }
  134. const onMessage = (msg, contents, actionsDone) => {
  135. if (actionsDone) return false;
  136. if (msg.mentions.users.size > 0 && msg.mentions.users.first().id == client.user.id)
  137. return false;
  138. let imageAttachment = msg.attachments.find(v => util.isValidImage(v.filename));
  139. if (imageAttachment) {
  140. let probValue = db.get("faceEditChannels").get(msg.channel.id);
  141. if (probValue.isUndefined().value() || probValue.isNull().value()) return false;
  142. if (Math.random() > probValue.value()) return false;
  143. processFaceSwap(msg, imageAttachment.url).catch(err =>
  144. console.log(`Failed to run faceapp because ${err}`)
  145. );
  146. return true;
  147. }
  148. return false;
  149. };
  150. const onDirectMention = (msg, content, actionsDone) => {
  151. if (actionsDone) return false;
  152. let image = msg.attachments.find(v => util.isValidImage(v.filename));
  153. if (!image) {
  154. if (msg.attachments.size > 0) {
  155. msg.channel.send(
  156. `${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`
  157. );
  158. return true;
  159. }
  160. return false;
  161. }
  162. let processor;
  163. if(content.startsWith("caption this"))
  164. processor = captionFace;
  165. else
  166. processor = morphFaces;
  167. processFaceSwap(
  168. msg,
  169. image.url,
  170. processor,
  171. `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
  172. `${msg.author.toString()} ${client.emojis.get("505076258753740810").toString()}`
  173. ).catch(err => console.log(`Failed to run faceapp because ${err}`));
  174. return true;
  175. };
  176. module.exports = {
  177. onMessage: onMessage,
  178. onDirectMention: onDirectMention
  179. };