wkdhkr/dedupper

View on GitHub
src/services/deepLearning/FaceSpinnerService.js

Summary

Maintainability
F
3 days
Test Coverage
// @flow
// import cv from "opencv4nodejs-prebuilt";
import pLimit from "p-limit";
import FormData from "form-data";
import axios from "axios";
import concat from "concat-stream";
import fs from "fs-extra";
import typeof { Logger } from "log4js";
import followRedirects from "follow-redirects";

import OpenCVHelper from "../../helpers/OpenCVHelper";
import FileNameMarkHelper from "../../helpers/FileNameMarkHelper";
import MathHelper from "../../helpers/MathHelper";
import { MARK_ERASE } from "../../types/FileNameMarks";
import type { Config } from "../../types";

const cv = OpenCVHelper.loadOpenCv();
followRedirects.maxBodyLength = 1024 * 1024 * 1000;

type FaceSpinnerResponse = {
  x: number,
  y: number,
  w: number,
  h: number,
  angle: number,
  scale: number,
  conf: number,
  corners: [number, number][]
};

type FaceSpinnerExtendedResponse = FaceSpinnerResponse & {
  buttomExpandedCorners: [number, number][],
  expandedCorners: [number, number][]
};

const apiPoolOffsetLookup = {
  faceSpinnerApi: -1
};

export default class FaceSpinnerService {
  log: Logger;

  config: Config;

  limitDetect: any => Promise<any>;

  constructor(config: Config) {
    this.log = config.getLogger(this);
    this.limitDetect = pLimit(
      config.deepLearningConfig.faceDetectWithGenderApi.length * 2
    );
    this.config = config;
  }

  detectApiUrl: (kind?: string) => string = (
    kind: string = "faceSpinnerApi"
  ): string => {
    apiPoolOffsetLookup[kind] += 1;
    const currentApi = this.config.deepLearningConfig[kind][
      apiPoolOffsetLookup[kind]
    ];
    if (currentApi) {
      return currentApi;
    }
    apiPoolOffsetLookup[kind] = -1;
    return this.detectApiUrl(kind);
  };

  expandFaceBound: (
    corners: Array<[number, number]>,
    expandRateLookup: {
      buttom: number,
      left: number,
      right: number,
      top: number,
      ...
    }
  ) => Array<[number, number]> = (
    corners: [number, number][],
    expandRateLookup: {
      top: number,
      right: number,
      left: number,
      buttom: number
    }
  ): [number, number][] => {
    const resultCorners = [...corners];
    const templateLookup = {
      top: 0,
      right: 0,
      left: 0,
      buttom: 0
    };
    [
      {
        ...templateLookup,
        top: expandRateLookup.top
      },
      {
        ...templateLookup,
        right: expandRateLookup.right
      },
      {
        ...templateLookup,
        left: expandRateLookup.left
      },
      {
        ...templateLookup,
        buttom: expandRateLookup.buttom
      }
    ].forEach(lookup => {
      if (lookup.top !== 0) {
        resultCorners[0] = MathHelper.getExtendedPoint(
          corners[1][0],
          corners[1][1],
          corners[0][0],
          corners[0][1],
          lookup.top
        );
        resultCorners[3] = MathHelper.getExtendedPoint(
          corners[2][0],
          corners[2][1],
          corners[3][0],
          corners[3][1],
          lookup.top
        );
      }
      if (lookup.left !== 0) {
        resultCorners[0] = MathHelper.getExtendedPoint(
          corners[3][0],
          corners[3][1],
          corners[0][0],
          corners[0][1],
          lookup.left
        );
        resultCorners[1] = MathHelper.getExtendedPoint(
          corners[2][0],
          corners[2][1],
          corners[1][0],
          corners[1][1],
          lookup.left
        );
      }
      if (lookup.buttom !== 0) {
        resultCorners[1] = MathHelper.getExtendedPoint(
          corners[0][0],
          corners[0][1],
          corners[1][0],
          corners[1][1],
          lookup.buttom
        );
        resultCorners[2] = MathHelper.getExtendedPoint(
          corners[3][0],
          corners[3][1],
          corners[2][0],
          corners[2][1],
          lookup.buttom
        );
      }
      if (lookup.right !== 0) {
        resultCorners[3] = MathHelper.getExtendedPoint(
          corners[0][0],
          corners[0][1],
          corners[3][0],
          corners[3][1],
          lookup.right
        );
        resultCorners[2] = MathHelper.getExtendedPoint(
          corners[1][0],
          corners[1][1],
          corners[2][0],
          corners[2][1],
          lookup.right
        );
      }
    });

    return (resultCorners: any);
  };

  boundFaces: (mat: any, faces: Array<FaceSpinnerExtendedResponse>) => any = (
    mat: any,
    faces: FaceSpinnerExtendedResponse[]
  ) => {
    const red = new cv.Vec(0, 0, 255);
    const green = new cv.Vec(0, 255, 0);
    faces.forEach(face => {
      const corners = face.expandedCorners;
      mat.drawLine(
        new cv.Point(corners[0][0], corners[0][1]),
        new cv.Point(corners[1][0], corners[1][1]),
        {
          color: green,
          thickness: 2
        }
      );
      mat.drawLine(
        new cv.Point(corners[1][0], corners[1][1]),
        new cv.Point(corners[2][0], corners[2][1]),
        {
          color: green,
          thickness: 2
        }
      );
      mat.drawLine(
        new cv.Point(corners[2][0], corners[2][1]),
        new cv.Point(corners[3][0], corners[3][1]),
        {
          color: green,
          thickness: 2
        }
      );
      mat.drawLine(
        new cv.Point(corners[3][0], corners[3][1]),
        new cv.Point(corners[0][0], corners[0][1]),
        {
          color: red,
          thickness: 2
        }
      );
    });
    return mat;
  };

  demo: (
    targetPath: string
  ) => Promise<Array<FaceSpinnerExtendedResponse>> = async (
    targetPath: string
  ): Promise<FaceSpinnerExtendedResponse[]> => {
    const results = await this.query(targetPath);

    const mat = await cv.imreadAsync(targetPath);
    const destPath = FileNameMarkHelper.mark(targetPath, new Set([MARK_ERASE]));
    await cv.imwriteAsync(destPath, this.boundFaces(mat, results));

    return results;
  };

  query: (
    targetPath: string
  ) => Promise<Array<FaceSpinnerExtendedResponse>> = async (
    targetPath: string
  ): Promise<FaceSpinnerExtendedResponse[]> => {
    const s = await fs.createReadStream(targetPath);
    return new Promise((resolve, reject) => {
      const form = new FormData();
      form.append("image", s);
      form.pipe(
        concat({ encoding: "buffer" }, async data => {
          const res = await this.limitDetect(() =>
            axios
              .post(this.detectApiUrl(), data, {
                headers: form.getHeaders()
              })
              .catch(reject)
          );
          if (res && res.data) {
            const faces: FaceSpinnerExtendedResponse[] = (res.data: FaceSpinnerResponse[]).map(
              face => ({
                ...face,
                // contain whole head
                // corners: this.expandFaceBound(
                //   this.expandFaceBound(face.corners, {
                //     top: 0.55,
                //     right: 0,
                //     left: 0,
                //     buttom: 0.2
                //   }),
                //   {
                //     top: 0,
                //     right: 0.1,
                //     left: 0.1,
                //     buttom: 0
                //   }
                // )
                buttomExpandedCorners: this.expandFaceBound(face.corners, {
                  top: 0.0,
                  right: 0,
                  left: 0,
                  buttom: 0.2
                }),
                expandedCorners: this.expandFaceBound(face.corners, {
                  top: 0.55,
                  right: 0.1,
                  left: 0.1,
                  buttom: 0.2
                })
              })
            );
            resolve(faces);
          } else {
            reject(new Error("no data"));
          }
        })
      );
    });
  };
}