diff options
Diffstat (limited to 'js/analysis.js')
| -rw-r--r-- | js/analysis.js | 673 |
1 files changed, 673 insertions, 0 deletions
diff --git a/js/analysis.js b/js/analysis.js new file mode 100644 index 0000000..fd78855 --- /dev/null +++ b/js/analysis.js @@ -0,0 +1,673 @@ +function analyseCriteria(face) { + let points = { + leftIris: face.annotations.rightEyeIris[0], + rightIris: face.annotations.leftEyeIris[0], + leftLateralCanthus: face.annotations.rightEyeLower1[0], + leftMedialCanthus: face.annotations.rightEyeLower1[7], + rightLateralCanthus: face.annotations.leftEyeLower1[0], + rightMedialCanthus: face.annotations.leftEyeLower1[7], + leftEyeUpper: face.annotations.rightEyeUpper0[4], + leftEyeLower: face.annotations.rightEyeLower0[4], + rightEyeUpper: face.annotations.leftEyeUpper0[4], + rightEyeLower: face.annotations.leftEyeLower0[4], + leftEyebrow: face.annotations.rightEyebrowUpper[6], + rightEyebrow: face.annotations.leftEyebrowUpper[6], + leftZygo: face.annotations.silhouette[28], + rightZygo: face.annotations.silhouette[8], + noseBottom: face.annotations.noseBottom[0], + leftNoseCorner: face.annotations.noseRightCorner[0], + rightNoseCorner: face.annotations.noseLeftCorner[0], + leftCupidBow: face.annotations.lipsUpperOuter[4], + lipSeparation: face.annotations.lipsUpperInner[5], + rightCupidBow: face.annotations.lipsUpperOuter[6], + leftLipCorner: face.annotations.lipsUpperOuter[0], + rightLipCorner: face.annotations.lipsUpperOuter[10], + lowerLip: face.annotations.lipsLowerOuter[4], + upperLip: face.annotations.lipsUpperOuter[5], + leftGonial: face.annotations.silhouette[24], + rightGonial: face.annotations.silhouette[12], + chinLeft: face.annotations.silhouette[19], + chinTip: face.annotations.silhouette[18], + chinRight: face.annotations.silhouette[17], + }; + return [points, { + midfaceRatio: new MidfaceRatio(face, points), + facialWidthToHeightRatio: new FacialWidthToHeightRatio(face, points), + chinToPhiltrumRatio: new ChinToPhiltrumRatio(face, points), + canthalTilt: new CanthalTilt(face, points), + mouthToNoseRatio: new MouthToNoseRatio(face, points), + bigonialWidth: new BigonialWidth(face, points), + lipRatio: new LipRatio(face, points), + eyeSeparationRatio: new EyeSeparationRatio(face, points), + eyeToMouthAngle: new EyeToMouthAngle(face, points), + lowerThirdHeight: new LowerThirdHeight(face, points), + palpebralFissureLength: new PalpebralFissureLength(face, points), + eyeColor: new EyeColor(face, points), + }]; +} + +async function setupDatabase() { + return await fetch("database.json") + .then(res => res.text()) + .then(text => { + console.log("✅ database.json loaded"); + return JSON.parse(text); + }) + .catch(err => { + console.error("❌ Failed to load or parse database.json:", err); + return { entries: {} }; + }); + +} + +class Criteria { + constructor(face, points) { + this.face = face; + this.points = points; + + for(let i in points) { + if (points.hasOwnProperty(i)) { + Object.defineProperty(this, i, { get: () => this.points[i] }); + } + } + + return this; + } + + createPoint(name, value) { + this.points[name] = value; + Object.defineProperty(this, name, { get: () => this.points[name] }); + } + + calculate() { + /* abstract */ + } + + render() { + /* abstract */ + } + + ideal() { + /* abstract */ + } + + assess() { + /* abstract */ + } + + draw(ctx) { + /* abstract */ + } + + necessaryPoints() { + /* abstract */ + } +} + +class MidfaceRatio extends Criteria { + constructor(face, points) { + super(face, points); + + let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow); + let leftLine = bottomLine.perpendicular(this.leftIris); + let rightLine = bottomLine.perpendicular(this.rightIris); + this.createPoint("bottomLeftMidface", bottomLine.intersect(leftLine)); + this.createPoint("bottomRightMidface", bottomLine.intersect(rightLine)); + } + + calculate() { + this.ratio = ((distance(this.leftIris, this.rightIris) / distance(this.leftIris, this.bottomLeftMidface)) + + (distance(this.leftIris, this.rightIris) / distance(this.rightIris, this.bottomRightMidface))) / 2; + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.midfaceRatio.idealLower} to ${database.entries.midfaceRatio.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.midfaceRatio; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "red", [this.leftIris, this.rightIris, this.bottomRightMidface, this.bottomLeftMidface]); + } + + necessaryPoints() { + return ["leftIris", "rightIris", "bottomLeftMidface", "bottomRightMidface"]; + } +} + +class FacialWidthToHeightRatio extends Criteria { + constructor(face, points) { + super(face, points); + + let topLine = Fn.fromTwoPoints(this.leftEyeUpper, this.rightEyeUpper); + let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow); + let leftLine = topLine.perpendicular(this.leftZygo); + let rightLine = topLine.perpendicular(this.rightZygo); + this.createPoint("topLeft", leftLine.intersect(topLine)); + this.createPoint("topRight", rightLine.intersect(topLine)); + this.createPoint("bottomLeft", leftLine.intersect(bottomLine)); + this.createPoint("bottomRight", rightLine.intersect(bottomLine)); + } + + calculate() { + this.ratio = (((distance(this.topLeft, this.topRight) / distance(this.topLeft, this.bottomLeft)) + + (distance(this.bottomLeft, this.bottomRight) / distance(this.topRight, this.bottomRight))) / 2); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `more than ${database.entries.facialWidthToHeightRatio.idealLower}`; + } + + assess() { + let { idealLower, deviation, deviatingLow } = database.entries.facialWidthToHeightRatio; + return assess(this.ratio, idealLower, undefined, deviation, deviatingLow, undefined); + } + + draw(ctx) { + draw(ctx, "lightblue", [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft]) + } + + necessaryPoints() { + return ["topLeft", "topRight", "bottomLeft", "bottomRight"]; + } +} + +class ChinToPhiltrumRatio extends Criteria { + calculate() { + this.ratio = distance(this.chinTip, this.lowerLip) / distance(this.upperLip, this.noseBottom); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.chinToPhiltrumRatio.idealLower} to ${database.entries.chinToPhiltrumRatio.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.chinToPhiltrumRatio; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "blue", [this.chinTip, this.lowerLip]); + draw(ctx, "blue", [this.upperLip, this.noseBottom]); + } + + necessaryPoints() { + return ["chinTip", "lowerLip", "upperLip", "noseBottom"]; + } +} + +class CanthalTilt extends Criteria { + calculate() { + let line = [this.rightZygo[0] - this.leftZygo[0], this.rightZygo[1] - this.leftZygo[1]]; + let lineFn = Fn.fromTwoPoints(this.rightZygo, this.leftZygo); + let left = [this.leftLateralCanthus[0] - this.leftMedialCanthus[0], this.leftLateralCanthus[1] - this.leftMedialCanthus[1]]; + let right = [this.rightLateralCanthus[0] - this.rightMedialCanthus[0], this.rightLateralCanthus[1] - this.rightMedialCanthus[1]]; + let pointOnLeftLine = lineFn.getY(this.leftMedialCanthus[0]) + left[1]; + let pointOnRightLine = lineFn.getY(this.rightMedialCanthus[0]) + right[1]; + this.leftCanthalTilt = Math.acos( + Math.abs((-1) * (line[0] * left[0] + line[1] * left[1])) + / + (Math.sqrt(line[0] ** 2 + line[1] ** 2) * Math.sqrt(left[0] ** 2 + left[1] ** 2)) + ) * (180 / Math.PI) * (lineFn.getY(this.leftLateralCanthus[0]) - pointOnLeftLine > 0 ? 1 : -1); + this.rightCanthalTilt = Math.acos( + Math.abs(line[0] * right[0] + line[1] * right[1]) + / + (Math.sqrt(line[0] ** 2 + line[1] ** 2) * Math.sqrt(right[0] ** 2 + right[1] ** 2)) + ) * (180 / Math.PI) * (lineFn.getY(this.rightLateralCanthus[0]) - pointOnRightLine > 0 ? 1 : -1); + } + + render() { + return `left ${round(this.rightCanthalTilt, 0)}°, right ${round(this.leftCanthalTilt, 0)}°`; + } + + ideal() { + return `more than ${database.entries.canthalTilt.idealLower}`; + } + + assess() { + let { idealLower, deviation, deviatingLow } = database.entries.canthalTilt; + return assess((this.leftCanthalTilt + this.rightCanthalTilt) / 2, idealLower, undefined, deviation, deviatingLow, undefined); + } + + draw(ctx) { + draw(ctx, "pink", [this.leftLateralCanthus, this.leftMedialCanthus]); + draw(ctx, "pink", [this.rightLateralCanthus, this.rightMedialCanthus]); + } + + necessaryPoints() { + return ["leftLateralCanthus", "leftMedialCanthus", "rightLateralCanthus", "rightMedialCanthus"]; + } +} + +class MouthToNoseRatio extends Criteria { + calculate() { + this.ratio = distance(this.leftLipCorner, this.rightLipCorner) / distance(this.leftNoseCorner, this.rightNoseCorner); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.mouthToNoseRatio.idealLower} to ${database.entries.mouthToNoseRatio.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.mouthToNoseRatio; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "purple", [this.leftLipCorner, this.rightLipCorner]); + draw(ctx, "purple", [this.leftNoseCorner, this.rightNoseCorner]); + } + + necessaryPoints() { + return ["leftLipCorner", "rightLipCorner", "leftNoseCorner", "rightNoseCorner"]; + } +} + +class BigonialWidth extends Criteria { + calculate() { + this.ratio = distance(this.leftZygo, this.rightZygo) / distance(this.leftGonial, this.rightGonial); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.bigonialWidth.idealLower} to ${database.entries.bigonialWidth.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.bigonialWidth; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "gold", [this.leftGonial, this.rightGonial]); + draw(ctx, "gold", [this.leftZygo, this.rightZygo]); + } + + necessaryPoints() { + return ["leftZygo", "rightZygo", "leftGonial", "rightGonial"]; + } +} + +class LipRatio extends Criteria { + constructor(face, points) { + super(face, points); + + let topLip = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow); + let lowerLip = topLip.parallel(this.lowerLip); + this.createPoint("upperLipEnd", topLip.intersect(topLip.perpendicular(this.lipSeparation))); + this.createPoint("lowerLipEnd", lowerLip.intersect(lowerLip.perpendicular(this.lipSeparation))); + } + + calculate() { + this.ratio = distance(this.lowerLipEnd, this.lipSeparation) / distance(this.upperLipEnd, this.lipSeparation); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.lipRatio.idealLower} to ${database.entries.lipRatio.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.lipRatio; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "lightgreen", [this.upperLipEnd, this.lipSeparation]); + draw(ctx, "lightgreen", [this.lipSeparation, this.lowerLipEnd]); + } + + necessaryPoints() { + return ["upperLipEnd", "lowerLipEnd", "lipSeparation"]; + } +} + +class EyeSeparationRatio extends Criteria { + calculate() { + this.ratio = distance(this.leftIris, this.rightIris) / distance(this.leftZygo, this.rightZygo); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `${database.entries.eyeSeparationRatio.idealLower} to ${database.entries.eyeSeparationRatio.idealUpper}`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.eyeSeparationRatio; + return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "orange", [this.leftIris, this.rightIris]); + draw(ctx, "orange", [this.leftZygo, this.rightZygo]); + } + + necessaryPoints() { + return ["leftIris", "rightIris", "leftZygo", "rightZygo"]; + } +} + +class EyeToMouthAngle extends Criteria { + calculate() { + let a = [this.leftIris[0] - this.lipSeparation[0], this.leftIris[1] - this.lipSeparation[1]]; + let b = [this.rightIris[0] - this.lipSeparation[0], this.rightIris[1] - this.lipSeparation[1]]; + this.angle = Math.acos( + (a[0] * b[0] + a[1] * b[1]) + / + (Math.sqrt(a[0] ** 2 + a[1] ** 2) * Math.sqrt(b[0] ** 2 + b[1] ** 2)) + ) * (180 / Math.PI); + } + + render() { + return `${round(this.angle, 0)}°`; + } + + ideal() { + return `${database.entries.eyeToMouthAngle.idealLower}° to ${database.entries.eyeToMouthAngle.idealUpper}°`; + } + + assess() { + let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.eyeToMouthAngle; + return assess(this.angle, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh); + } + + draw(ctx) { + draw(ctx, "brown", [this.leftIris, this.lipSeparation, this.rightIris, this.lipSeparation, this.leftIris]); + } + + necessaryPoints() { + return ["leftIris", "lipSeparation", "rightIris"]; + } +} + +class LowerThirdHeight extends Criteria { + calculate() { + let middlePoint = [this.leftNoseCorner[0] + (1/2) * (this.rightNoseCorner[0] - this.leftNoseCorner[0]), this.leftNoseCorner[1] + (1/2) * (this.rightNoseCorner[1] - this.leftNoseCorner[1])]; + let middleLine = Fn.fromTwoPoints(this.leftNoseCorner, this.rightNoseCorner).perpendicular(middlePoint); + let topPoint = middleLine.intersect(Fn.fromTwoPoints(this.leftEyebrow, this.rightEyebrow)); + let bottomPoint = middleLine.intersect(Fn.fromTwoPoints(this.chinLeft, this.chinRight)); + this.ratio = distance(bottomPoint, middlePoint) / distance(middlePoint, topPoint); + } + + render() { + return `${round(this.ratio, 2)}`; + } + + ideal() { + return `more than ${database.entries.lowerThirdHeight.idealLower}`; + } + + assess() { + let { idealLower, deviation, deviatingLow } = database.entries.lowerThirdHeight; + return assess(this.ratio, idealLower, undefined, deviation, deviatingLow, undefined); + } + + draw(ctx) { + draw(ctx, "grey", [this.leftEyebrow, this.rightEyebrow, this.rightNoseCorner, this.leftNoseCorner]); + draw(ctx, "grey", [this.leftNoseCorner, this.rightNoseCorner, this.chinRight, this.chinLeft]); + } + + necessaryPoints() { + return ["leftNoseCorner", "rightNoseCorner", "leftEyebrow", "rightEyebrow", "chinLeft", "chinRight"]; + } +} + +class PalpebralFissureLength extends Criteria { + calculate() { + this.leftPFL = distance(this.leftLateralCanthus, this.leftMedialCanthus) / distance(this.leftEyeUpper, this.leftEyeLower); + this.rightPFL = distance(this.rightLateralCanthus, this.rightMedialCanthus) / distance(this.rightEyeUpper, this.rightEyeLower); + } + + render() { + return `left ${round(this.rightPFL, 2)}, right ${round(this.leftPFL, 2)}`; + } + + ideal() { + return `more than ${database.entries.palpebralFissureLength.idealLower}`; + } + + assess() { + let { idealLower, deviation, deviatingLow } = database.entries.palpebralFissureLength; + return assess((this.leftPFL + this.rightPFL) / 2, idealLower, undefined, deviation, deviatingLow, undefined); + } + + draw(ctx) { + draw(ctx, "aquamarine", [this.leftLateralCanthus, this.leftMedialCanthus]); + draw(ctx, "aquamarine", [this.leftEyeUpper, this.leftEyeLower]); + draw(ctx, "aquamarine", [this.rightLateralCanthus, this.rightMedialCanthus]); + draw(ctx, "aquamarine", [this.rightEyeUpper, this.rightEyeLower]); + } + + necessaryPoints() { + return ["leftLateralCanthus", "leftMedialCanthus", "leftEyeUpper", "leftEyeLower", "rightLateralCanthus", "rightMedialCanthus", "rightEyeUpper", "rightEyeLower"]; + } +} + +class EyeColor extends Criteria { + calculate() { + this.leftIrisCoordinates = this.face.annotations.rightEyeIris; + this.leftIrisWidth = this.leftIrisCoordinates[1][0] - this.leftIrisCoordinates[3][0]; + this.leftIrisHeight = this.leftIrisCoordinates[4][1] - this.leftIrisCoordinates[2][1]; + this.rightIrisCoordinates = this.face.annotations.leftEyeIris; + this.rightIrisWidth = this.rightIrisCoordinates[3][0] - this.rightIrisCoordinates[1][0]; + this.rightIrisHeight = this.rightIrisCoordinates[4][1] - this.rightIrisCoordinates[2][1]; + } + + render() { + return `<canvas height="0" width="0"></canvas><canvas height="0" width="0"></canvas>`; + } + + ideal() { + return ""; + } + + assess() { + return ""; + } + + detect(image, [ctx0, ctx1]) { + ctx0.canvas.width = this.leftIrisWidth; + ctx0.canvas.height = this.leftIrisHeight; + ctx0.drawImage(image, this.leftIrisCoordinates[3][0], this.leftIrisCoordinates[2][1], this.leftIrisWidth, this.leftIrisHeight, 0, 0, this.leftIrisWidth, this.leftIrisHeight); + ctx1.canvas.width = this.rightIrisWidth; + ctx1.canvas.height = this.rightIrisHeight; + ctx1.drawImage(image, this.rightIrisCoordinates[1][0], this.rightIrisCoordinates[2][1], this.rightIrisWidth, this.rightIrisHeight, 0, 0, this.rightIrisWidth, this.rightIrisHeight); + } + + necessaryPoints() { + return []; + } +} + +function distance([ax, ay], [bx, by]) { + return Math.sqrt((ax - bx) ** 2 + (ay - by) ** 2); +} + +class Fn { + constructor(a, b) { + // y = ax + b + this.a = a; + this.b = b; + + this.slope = this.a; + this.yintersect = this.b; + } + + static fromTwoPoints([ax, ay], [bx, by]) { + // y = (ay / by) / (ax - bx) * (x - ax) + ay + return Fn.fromOffset((ay - by) / (ax - bx), ax, ay); + } + + static fromOffset(a, b, c) { + // y = a * (x - b) + c + return new Fn(a, c - (a * b)); + } + + getY(x) { + return this.a * x + this.b; + } + + perpendicular([x, y]) { + return Fn.fromOffset((-1) * (1 / this.a), x, y); + } + + parallel([x, y]) { + return Fn.fromOffset(this.a, x, y); + } + + intersect(fn) { + let x = (fn.b - this.b) / (this.a - fn.a); + return [x, this.getY(x)]; + } + + draw(ctx, color) { + let points = []; + for(let i = 0; i < ctx.canvas.width; i += 1) { + points.push([i, this.getY(i)]); + } + draw(ctx, color || "red", points); + } +} + +function round(n, digits) { + digits = 10 ** (isNaN(digits) ? 2 : digits); + return Math.round(n * digits) / digits; +} + +function assess(value, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh) { + function renderMultiplier(multiplier) { + if (multiplier === 0) { + return "slightly too"; + } else if (multiplier === 1) { + return "noticeably"; + } else if (multiplier === 2) { + return "significantly too"; + } else if (multiplier === 3) { + return "horribly"; + } else { + return "extremely"; + } + } + + function calculate(value, idealLower, idealUpper, deviation) { + + if (idealUpper !== undefined && idealLower !== undefined) { + if (idealUpper >= value && idealLower <= value) { + return { + type: "perfect", + }; + } + } else if (((idealUpper && !idealLower) && value <= idealUpper) || ((!idealUpper && idealLower) && value >= idealLower)) { + return { + type: "perfect", + }; + } + + if (value < idealLower) { + let multiplier = 0; + while ((value += deviation) < idealLower) { + multiplier++; + } + return { + type: "low", + multiplier: Math.min(multiplier, 4), + text: renderMultiplier(multiplier), + }; + } + + if (value > idealUpper) { + let multiplier = 0; + while ((value -= deviation) > idealUpper) { + multiplier++; + } + return { + type: "high", + multiplier: Math.min(multiplier, 4), + text: renderMultiplier(multiplier), + }; + } + + } + + let { type, multiplier, text } = calculate(value, idealLower, idealUpper, deviation); + + if (type === "perfect") { + return `<span class="perfect">perfect</span>`; + } else if (type === "low") { + return `<span class="deviation-${multiplier}">${text} ${deviatingLow}</span>`; + } else if (type === "high") { + return `<span class="deviation-${multiplier}">${text} ${deviatingHigh}</span>`; + } +} + +function draw(ctx, color, points) { + ctx.strokeStyle = color; + ctx.fillStyle = color; + + let current = points[0]; + + var fontBase = canvas.width * 0.6; // selected default width for canvas + var fontSize = 20; // default size for font + + + var ratio = fontSize / fontBase; // calc ratio + var size = canvas.width * ratio; // get font size based on current width + if(canvas.width > 600) { + ctx.font= size + 'px sans-serif'; + } else { + ctx.font= '18px sans-serif'; + + } var textWidth=ctx.measureText("text").width; + ctx.globalAlpha=.50; + ctx.fillStyle='white' + var text = "www.incel.solutions (powered by $INCEL COIN)" + cw = canvas.width; + ch = canvas.height; + var textWidth=ctx.measureText(text).width; + ctx.fillStyle='gray' + ctx.fillText(text,cw-textWidth-10,ch-20); + ctx.fillStyle='white' + ctx.fillText(text,cw-textWidth-10+2,ch-20+2); + + for (let i of points.concat([points[0]])) { + let [x, y] = i; + + ctx.beginPath(); + ctx.moveTo(current[0], current[1]); + ctx.lineTo(x, y); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(x, y, ctx.arcRadius, 0, 2 * Math.PI); + ctx.fill(); + + current = i; + } +} |