facemorph.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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 padoru = Math.random() <= getPadoruChance();
  20. let jimpImage = await Jimp.read(data);
  21. let emojiKeys = [
  22. ...client.guilds
  23. .get(EMOTE_GUILD)
  24. .emojis.filter(e => !e.animated && e.name.startsWith("PADORU") == padoru)
  25. .keys()
  26. ];
  27. for (const rect of faces) {
  28. let dx = rect.x + rect.width / 2;
  29. let dy = rect.y + rect.height / 2;
  30. let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
  31. let emoji = client.emojis.get(emojiKey);
  32. let emojiImage = await Jimp.read(emoji.url);
  33. let ew = emojiImage.getWidth();
  34. let eh = emojiImage.getHeight();
  35. const CONSTANT_SCALE = 1.1;
  36. let scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
  37. ew *= scaleFactor;
  38. eh *= scaleFactor;
  39. emojiImage = emojiImage.scale(scaleFactor);
  40. jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
  41. }
  42. return jimpImage;
  43. }
  44. const CAPTION_OFFSET = 5;
  45. async function captionFace(faces, data) {
  46. let padoru = Math.random() <= getPadoruChance();
  47. let face = faces[Math.floor(Math.random() * faces.length)];
  48. let squaredFace = await face.toSquareAsync();
  49. let targetSize = db.get("faceEditConfig.captionedImageSize").value();
  50. let img = await Jimp.read(data);
  51. let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
  52. tempImg = await tempImg.blit(
  53. img,
  54. 0,
  55. 0,
  56. squaredFace.x,
  57. squaredFace.y,
  58. squaredFace.width,
  59. squaredFace.height
  60. );
  61. tempImg = await tempImg.scale(targetSize / squaredFace.width);
  62. let font = await Jimp.loadFont(padoru ? Jimp.FONT_SANS_16_WHITE : Jimp.FONT_SANS_16_BLACK);
  63. let text = padoru ? "PADORU PADORU" : `${db.get("faceCaptions.pre").randomElement().value()} ${db.get("faceCaptions.post").randomElement().value()}`;
  64. let h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
  65. let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, padoru ? "#FD2027" : "#FFFFFF");
  66. finalImage = await finalImage.print(
  67. font,
  68. CAPTION_OFFSET,
  69. CAPTION_OFFSET,
  70. text,
  71. finalImage.getWidth() - CAPTION_OFFSET
  72. );
  73. finalImage = await finalImage.composite(tempImg, 0, CAPTION_OFFSET * 2 + h);
  74. return finalImage;
  75. }
  76. /**
  77. * PADORU PADORU
  78. */
  79. function getPadoruChance() {
  80. let now = new Date();
  81. if(now.getUTCMonth() != 11 || now.getUTCDate() > 25)
  82. return 0;
  83. return 1 / (27.0 - now.getUTCDate());
  84. }
  85. async function processFaceSwap(message, attachmentUrl, processor, failMessage, successMessage) {
  86. let data = await request(attachmentUrl, { encoding: null });
  87. let im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
  88. let gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
  89. let normGray = await gray.equalizeHistAsync();
  90. let animeFaces = await animeCascade.detectMultiScaleAsync(
  91. normGray,
  92. 1.1,
  93. 5,
  94. 0,
  95. new cv.Size(24, 24)
  96. );
  97. let normalFaces = await faceCascade.detectMultiScaleAsync(gray);
  98. if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
  99. if (failMessage) message.channel.send(failMessage);
  100. return;
  101. }
  102. let faces = [...normalFaces.objects, ...animeFaces.objects];
  103. let normalCount = normalFaces.objects.length;
  104. let animeCount = animeFaces.objects.length;
  105. for (let i = 0; i < normalCount; i++) {
  106. const rNormal = faces[i];
  107. if (animeCount == 0) break;
  108. for (let j = normalCount; j < faces.length; j++) {
  109. const rAnime = faces[j];
  110. if (intersects(rAnime, rNormal)) {
  111. let animeA = rAnime.width * rAnime.height;
  112. let faceA = rNormal.width * rNormal.height;
  113. if (animeA > faceA) {
  114. faces.splice(i, 1);
  115. normalCount--;
  116. i--;
  117. break;
  118. } else {
  119. faces.splice(j, 1);
  120. animeCount--;
  121. j--;
  122. }
  123. }
  124. }
  125. }
  126. let jimpImage;
  127. if(processor)
  128. jimpImage = await processor(faces, data);
  129. else {
  130. if(Math.random() <= db.get("faceEditConfig.captionProbability").value())
  131. jimpImage = await captionFace(faces, data);
  132. else
  133. jimpImage = await morphFaces(faces, data);
  134. }
  135. jimpImage.quality(90);
  136. let buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
  137. let messageContents =
  138. successMessage ||
  139. `I noticed a face in the image. I think this looks better ${client.emojis.get("505076258753740810").toString()}`;
  140. message.channel.send(messageContents, {
  141. files: [buffer]
  142. });
  143. }
  144. const onMessage = (msg, contents, actionsDone) => {
  145. if (actionsDone) return false;
  146. if (msg.mentions.users.size > 0 && msg.mentions.users.first().id == client.user.id)
  147. return false;
  148. let imageAttachment = msg.attachments.find(v => util.isValidImage(v.filename));
  149. if (imageAttachment) {
  150. let probValue = db.get("faceEditChannels").get(msg.channel.id);
  151. if (probValue.isUndefined().value() || probValue.isNull().value()) return false;
  152. if (Math.random() > probValue.value()) return false;
  153. processFaceSwap(msg, imageAttachment.url).catch(err =>
  154. console.log(`Failed to run faceapp because ${err}`)
  155. );
  156. return true;
  157. }
  158. return false;
  159. };
  160. const onDirectMention = (msg, content, actionsDone) => {
  161. if (actionsDone) return false;
  162. let image = msg.attachments.find(v => util.isValidImage(v.filename));
  163. if (!image) {
  164. if (msg.attachments.size > 0) {
  165. msg.channel.send(
  166. `${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`
  167. );
  168. return true;
  169. }
  170. return false;
  171. }
  172. let processor;
  173. if(content.startsWith("caption this"))
  174. processor = captionFace;
  175. else
  176. processor = morphFaces;
  177. processFaceSwap(
  178. msg,
  179. image.url,
  180. processor,
  181. `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
  182. `${msg.author.toString()} ${client.emojis.get("505076258753740810").toString()}`
  183. ).catch(err => console.log(`Failed to run faceapp because ${err}`));
  184. return true;
  185. };
  186. function processLastImage(msg, processor) {
  187. let lastImagedMessage = msg.channel.messages.filter(m => !m.author.bot && m.attachments.find(v => util.isValidImage(v.filename) !== undefined)).last();
  188. if(!lastImagedMessage) {
  189. msg.channel.send(`${msg.author.toString()} Sorry, I couldn't find any recent messages with images.`);
  190. return;
  191. }
  192. let image = lastImagedMessage.attachments.find(v => util.isValidImage(v.filename));
  193. processFaceSwap(
  194. msg,
  195. image.url,
  196. processor,
  197. `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
  198. `${msg.author.toString()} ${client.emojis.get("505076258753740810").toString()}`
  199. ).catch(err => console.log(`Failed to run faceapp because ${err}`));
  200. }
  201. const commands = [
  202. {
  203. pattern: "caption last image",
  204. action: msg => processLastImage(msg, captionFace)
  205. },
  206. {
  207. pattern: "look at last image",
  208. action: msg => processLastImage(msg, morphFaces)
  209. }];
  210. module.exports = {
  211. onMessage: onMessage,
  212. onDirectMention: onDirectMention,
  213. commands: commands
  214. };