From 4ecc0cb7e8bcfd242b91c01b0a27a42e9193cec3 Mon Sep 17 00:00:00 2001 From: Rasmus Thomsen <oss@cogitri.dev> Date: Sat, 15 May 2021 12:01:15 +0200 Subject: [PATCH] backend: add API endpoint for comparing images --- backend/Procfile | 2 +- backend/src/image.ts | 55 ++++++++++++++++++++++++++++++++++++++++ backend/src/index.ts | 10 +++++++- backend/src/ribbon.ts | 59 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 backend/src/image.ts diff --git a/backend/Procfile b/backend/Procfile index 225d088..f64d669 100644 --- a/backend/Procfile +++ b/backend/Procfile @@ -1 +1 @@ -web: deno run --allow-net src/index.ts --port=${PORT} +web: deno run --allow-write --allow-read --allow-net src/index.ts --port=${PORT} diff --git a/backend/src/image.ts b/backend/src/image.ts new file mode 100644 index 0000000..04f36fd --- /dev/null +++ b/backend/src/image.ts @@ -0,0 +1,55 @@ +import { + readAll, + readerFromStreamReader, +} from "https://deno.land/std@0.95.0/io/mod.ts"; +import { decode, Image } from "https://deno.land/x/jpegts@1.1/mod.ts"; +import { resize } from "https://deno.land/x/deno_image@v0.0.3/mod.ts"; + +export const compareMunicipalRibbon = async ( + ribbonUrl: string, + imageToCompareTo: Image, +): Promise<number> => { + const ribbon = (await fetch(ribbonUrl))?.body?.getReader(); + if (ribbon) { + const reader = readerFromStreamReader(ribbon); + const rawImage = await readAll(reader); + const picture = decode( + await resize(rawImage, { + height: imageToCompareTo.height, + width: imageToCompareTo.width, + aspectRatio: false, + }), + ); + return compareImages(picture, imageToCompareTo); + } + + throw new Error("Couldn't fetch remote Image!"); +}; + +const compareImages = ( + firstImage: Image, + secondImage: Image, +): number => { + if ( + firstImage.width != secondImage.width || + firstImage.height != secondImage.height + ) { + throw new Error( + "Can't compare images of different sizes. " + firstImage.width + "x" + + firstImage.height + " vs " + secondImage.width + "x" + + secondImage.height, + ); + } + + let diff = 0; + for (let i = 0; i < firstImage.data.length / 5; i++) { + diff += Math.abs(firstImage.data[4 * i + 0] - secondImage.data[4 * i + 0]) / + 255; + diff += Math.abs(firstImage.data[4 * i + 1] - secondImage.data[4 * i + 1]) / + 255; + diff += Math.abs(firstImage.data[4 * i + 2] - secondImage.data[4 * i + 2]) / + 255; + } + + return 100 - 100 * diff / (firstImage.width * firstImage.height * 3); +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index cf0c115..4d73fde 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,7 +1,8 @@ import { Application, Router } from "https://deno.land/x/oak@v7.3.0/mod.ts"; import { parse } from "https://deno.land/std@0.95.0/flags/mod.ts"; +import logger from "https://deno.land/x/oak_logger@1.0.0/mod.ts"; -import { getAllRibbons, getRibbon, Ribbon } from "./ribbon.ts"; +import { compareRibbon, getAllRibbons, getRibbon, Ribbon } from "./ribbon.ts"; const { args } = Deno; const DEFAULT_PORT = 8000; @@ -14,10 +15,17 @@ const state: Ribbon[] = await (await fetch( const app = new Application({ state }); +app.use(logger.logger); +app.use(logger.responseTime); + const router = new Router(); router.get("/ribbons", getAllRibbons); router.get("/ribbons/:municipality", getRibbon); +router.post( + "/ribbons/compare/:municipality", + compareRibbon, +); app.use(router.routes()); diff --git a/backend/src/ribbon.ts b/backend/src/ribbon.ts index 33f2996..c0e175c 100644 --- a/backend/src/ribbon.ts +++ b/backend/src/ribbon.ts @@ -1,4 +1,6 @@ import { helpers, RouterContext } from "https://deno.land/x/oak@v7.3.0/mod.ts"; +import { compareMunicipalRibbon } from "./image.ts"; +import { decode, Image } from "https://deno.land/x/jpegts@1.1/mod.ts"; export interface Ribbon { acceptance: string; @@ -42,6 +44,7 @@ export const getAllRibbons = (ctx: RouterContext) => { export const getRibbon = ({ params, response, state }: RouterContext) => { if (params?.municipality === undefined) { response.status = 400; + return; } const municipality = params?.municipality; const ribbon = state.find( @@ -56,3 +59,59 @@ export const getRibbon = ({ params, response, state }: RouterContext) => { response.status = 200; response.body = ribbon; }; + +export const compareRibbon = async ( + { params, request, response, state }: RouterContext, +) => { + if (params?.municipality === undefined) { + response.status = 400; + return; + } + const municipality = params?.municipality; + const ribbon: Ribbon = state.find( + (ribbon: Ribbon) => ribbon.municipality.endsWith(`/${municipality}`), + ); + + if (ribbon === undefined) { + response.status = 404; + response.body = "Couldn't find municipality " + municipality; + return; + } + + const { value } = request.body({ type: "form-data" }); + const { files } = await value.read(); + if (!files) { + response.status = 400; + return; + } + const filename = files[0].filename; + if (!filename) { + response.status = 400; + return; + } + + let img: Image | undefined = undefined; + try { + img = decode( + await Deno.readFile(filename), + ); + } catch (e) { + response.status = 500; + response.body = "" + e; + return; + } + + let matchPercentage = 0; + try { + matchPercentage = await compareMunicipalRibbon( + ribbon.img, + img, + ); + } catch (e) { + response.status = 500; + response.body = "" + e; + return; + } + + response.body = { matchPercentage: matchPercentage }; +}; -- GitLab