facemorph.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import { isValidImage } from "../util";
  2. import Jimp from "jimp";
  3. import { client } from "../client";
  4. import * as cv from "opencv4nodejs";
  5. import * as path from "path";
  6. import request from "request-promise-native";
  7. import { Message } from "discord.js";
  8. import { getRepository } from "typeorm";
  9. import { FaceCaptionMessage, FaceCaptionType } from "@shared/db/entity/FaceCaptionMessage";
  10. import { KnownChannel } from "@shared/db/entity/KnownChannel";
  11. import { CommandSet, Action, ActionType, Command } from "src/model/command";
  12. const EMOTE_GUILD = "505333548694241281";
  13. const animeCascade = new cv.CascadeClassifier(path.resolve(process.cwd(), "animu.xml"));
  14. const faceCascade = new cv.CascadeClassifier(cv.HAAR_FRONTALFACE_ALT2);
  15. const CAPTION_IMG_SIZE = 300;
  16. const CAPTION_PROBABILITY = 0.33;
  17. type ImageProcessor = (faces: cv.Rect[], data: Buffer) => Promise<Jimp>;
  18. const CAPTION_OFFSET = 5;
  19. @CommandSet
  20. export class Facemorph {
  21. intersects(r1: cv.Rect, r2: cv.Rect) {
  22. return (
  23. r1.x <= r2.x + r2.width &&
  24. r1.x + r1.width >= r2.x &&
  25. (r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y)
  26. );
  27. }
  28. morphFaces = async (faces: cv.Rect[], data: Buffer) => {
  29. let padoru = Math.random() <= this.getPadoruChance();
  30. let jimpImage = await Jimp.read(data);
  31. let emojiKeys = process.env.FOOLS != "TRUE" ? [
  32. ...client.guilds
  33. .resolve(EMOTE_GUILD)
  34. .emojis.cache.filter(e => !e.animated && e.name.startsWith("PADORU") == padoru)
  35. .keys()
  36. ]:
  37. [
  38. "505335829565276160",
  39. "430434087157760003",
  40. "456472341874999297",
  41. "649677767348060170",
  42. "589706788782342183",
  43. "665272109227835422"
  44. ];
  45. for (const rect of faces) {
  46. let dx = rect.x + rect.width / 2;
  47. let dy = rect.y + rect.height / 2;
  48. let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
  49. let emoji = client.emojis.resolve(emojiKey);
  50. let emojiImage = await Jimp.read(emoji.url);
  51. let ew = emojiImage.getWidth();
  52. let eh = emojiImage.getHeight();
  53. const CONSTANT_SCALE = 1.1;
  54. let scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
  55. ew *= scaleFactor;
  56. eh *= scaleFactor;
  57. emojiImage = emojiImage.scale(scaleFactor);
  58. jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
  59. }
  60. return jimpImage;
  61. }
  62. async getRandomCaption(type: FaceCaptionType) {
  63. let repo = getRepository(FaceCaptionMessage);
  64. let caption = await repo.query(`select message
  65. from face_caption_message
  66. where type = $1
  67. order by random()
  68. limit 1`, [type]) as FaceCaptionMessage[];
  69. if (caption.length == 0)
  70. return null;
  71. return caption[0];
  72. }
  73. captionFace = async (faces: cv.Rect[], data: Buffer) => {
  74. let padoru = Math.random() <= this.getPadoruChance();
  75. let face = faces[Math.floor(Math.random() * faces.length)];
  76. let squaredFace = await face.toSquareAsync();
  77. let targetSize = CAPTION_IMG_SIZE;
  78. let img = await Jimp.read(data);
  79. let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
  80. tempImg = await tempImg.blit(
  81. img,
  82. 0,
  83. 0,
  84. squaredFace.x,
  85. squaredFace.y,
  86. squaredFace.width,
  87. squaredFace.height
  88. );
  89. tempImg = await tempImg.scale(targetSize / squaredFace.width);
  90. let font = await Jimp.loadFont(padoru ? Jimp.FONT_SANS_16_WHITE : Jimp.FONT_SANS_16_BLACK);
  91. let text = "";
  92. if(padoru)
  93. text = "PADORU PADORU";
  94. else if(process.env.FOOLS == "TRUE") {
  95. const titles = ["They are horse", "Neigh!", "Insert carrots into them!", "They will become horse!", "They will serve Geoffrey!", "tfw no carrots"];
  96. text = titles[Math.floor(Math.random() * titles.length)];
  97. }
  98. else
  99. text = `${(await this.getRandomCaption(FaceCaptionType.PREFIX)).message} ${(await this.getRandomCaption(FaceCaptionType.POSTFIX)).message}`;
  100. let h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
  101. let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, padoru ? "#FD2027" : "#FFFFFF");
  102. finalImage = await finalImage.print(
  103. font,
  104. CAPTION_OFFSET,
  105. CAPTION_OFFSET,
  106. text,
  107. finalImage.getWidth() - CAPTION_OFFSET
  108. );
  109. finalImage = await finalImage.composite(tempImg, 0, CAPTION_OFFSET * 2 + h);
  110. return finalImage;
  111. }
  112. /**
  113. * PADORU PADORU
  114. */
  115. getPadoruChance() {
  116. let now = new Date();
  117. if (now.getUTCMonth() != 11 || now.getUTCDate() > 25)
  118. return 0;
  119. return 1 / (27.0 - now.getUTCDate());
  120. }
  121. async processFaceSwap(message: Message, attachmentUrl: string, processor?: ImageProcessor, failMessage?: string, successMessage?: string) {
  122. let data = await request(attachmentUrl, { encoding: null }) as Buffer;
  123. let im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
  124. let gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
  125. let normGray = await gray.equalizeHistAsync();
  126. let animeFaces = await animeCascade.detectMultiScaleAsync(
  127. normGray,
  128. 1.1,
  129. 5,
  130. 0,
  131. new cv.Size(24, 24)
  132. );
  133. let normalFaces = await faceCascade.detectMultiScaleAsync(gray);
  134. if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
  135. if (failMessage) message.channel.send(failMessage);
  136. return;
  137. }
  138. let faces = [...normalFaces.objects, ...animeFaces.objects];
  139. let normalCount = normalFaces.objects.length;
  140. let animeCount = animeFaces.objects.length;
  141. for (let i = 0; i < normalCount; i++) {
  142. const rNormal = faces[i];
  143. if (animeCount == 0) break;
  144. for (let j = normalCount; j < faces.length; j++) {
  145. const rAnime = faces[j];
  146. if (this.intersects(rAnime, rNormal)) {
  147. let animeA = rAnime.width * rAnime.height;
  148. let faceA = rNormal.width * rNormal.height;
  149. if (animeA > faceA) {
  150. faces.splice(i, 1);
  151. normalCount--;
  152. i--;
  153. break;
  154. } else {
  155. faces.splice(j, 1);
  156. animeCount--;
  157. j--;
  158. }
  159. }
  160. }
  161. }
  162. let jimpImage;
  163. if (processor)
  164. jimpImage = await processor(faces, data);
  165. else {
  166. if (Math.random() <= CAPTION_PROBABILITY)
  167. jimpImage = await this.captionFace(faces, data);
  168. else
  169. jimpImage = await this.morphFaces(faces, data);
  170. }
  171. jimpImage.quality(90);
  172. let buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
  173. let messageContents =
  174. successMessage ||
  175. `I noticed a face in the image. I think this looks better ${client.emojis.resolve("505076258753740810").toString()}`;
  176. message.channel.send(messageContents, {
  177. files: [buffer]
  178. });
  179. }
  180. processLastImage(msg: Message, processor: ImageProcessor) {
  181. let lastImagedMessage = msg.channel.messages.cache.filter(m => !m.author.bot && m.attachments.find(v => isValidImage(v.name) !== undefined) != undefined).last();
  182. if (!lastImagedMessage) {
  183. msg.channel.send(`${msg.author.toString()} Sorry, I couldn't find any recent messages with images.`);
  184. return;
  185. }
  186. let image = lastImagedMessage.attachments.find(v => isValidImage(v.name));
  187. let replyEmoji = client.emojis.resolve("505076258753740810");
  188. let emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
  189. this.processFaceSwap(
  190. msg,
  191. image.url,
  192. processor,
  193. `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
  194. `${msg.author.toString()} ${emojiText}`
  195. ).catch(err => console.log(`Failed to run faceapp because ${err}`));
  196. }
  197. @Action(ActionType.MESSAGE)
  198. async morphRandomImage(actionsDone: boolean, msg: Message, contests: string) {
  199. if (actionsDone) return false;
  200. if (msg.mentions.users.size > 0 && msg.mentions.users.first().id == client.user.id)
  201. return false;
  202. let imageAttachment = msg.attachments.find(v => isValidImage(v.name));
  203. if (imageAttachment) {
  204. let repo = getRepository(KnownChannel);
  205. let knownChannel = await repo.findOne({
  206. where: { channelId: msg.channel.id },
  207. select: ["faceMorphProbability"]
  208. });
  209. if (!knownChannel || Math.random() > knownChannel.faceMorphProbability)
  210. return false;
  211. this.processFaceSwap(msg, imageAttachment.url).catch(err =>
  212. console.log(`Failed to run faceapp because ${err}`)
  213. );
  214. return true;
  215. }
  216. return false;
  217. }
  218. @Action(ActionType.DIRECT_MENTION)
  219. async morphProvidedImage(actionsDone: boolean, msg: Message, content: string) {
  220. if (actionsDone) return false;
  221. let image = msg.attachments.find(v => isValidImage(v.name));
  222. if (!image) {
  223. if (msg.attachments.size > 0) {
  224. msg.channel.send(
  225. `${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`
  226. );
  227. return true;
  228. }
  229. return false;
  230. }
  231. let processor;
  232. if (content.startsWith("caption this"))
  233. processor = this.captionFace;
  234. else
  235. processor = this.morphFaces;
  236. let replyEmoji = client.emojis.resolve("505076258753740810");
  237. let emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
  238. this.processFaceSwap(
  239. msg,
  240. image.url,
  241. processor,
  242. `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
  243. `${msg.author.toString()} ${emojiText}`
  244. ).catch(err => console.log(`Failed to run faceapp because ${err}`));
  245. return true;
  246. }
  247. @Command({
  248. pattern: "caption last image"
  249. })
  250. captionLastImage(msg: Message) {
  251. this.processLastImage(msg, this.captionFace);
  252. }
  253. @Command({
  254. pattern: "look at last image"
  255. })
  256. lookLastImage(msg: Message) {
  257. this.processLastImage(msg, this.morphFaces)
  258. }
  259. };