Files
hair/static/hairline_reference_box.js
T
xsl 5b2b73f5d5 feat: 新增52号发际线接口并限制GPU worker来源IP
保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。

worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。

验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
2026-11-04 10:20:29 +08:00

63 lines
3.5 KiB
JavaScript

"use strict";
// Reference geometry is in input-image pixels; no screen-vertical assumptions.
window.hairlineReferenceBox = function(points) {
const p = id => {
const value = points[String(id)];
if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y)) throw new Error(`缺少 ${id} 号关键点`);
return [value.x, value.y];
};
const add = (a,b) => [a[0]+b[0],a[1]+b[1]];
const sub = (a,b) => [a[0]-b[0],a[1]-b[1]];
const mul = (a,s) => [a[0]*s,a[1]*s];
const dot = (a,b) => a[0]*b[0]+a[1]*b[1];
const brow=p(9),chin=p(152),left=p(162),right=p(389);
const delta=sub(chin,brow),length=Math.hypot(...delta);
if(length<1)throw new Error("9→152 方向无法确定");
const up=mul(delta,-1/length),horizontal=[-up[1],up[0]],height=.7*length;
const sidePoint=(anchor,direction,level)=>{
const denominator=dot(direction,up);
if(Math.abs(denominator)<1e-6)throw new Error("参考框侧边与底边平行");
return add(anchor,mul(direction,(level-dot(sub(anchor,brow),up))/denominator));
};
const corners=[sidePoint(left,up,0),sidePoint(right,up,0),
sidePoint(right,up,height),sidePoint(left,up,height)];
if(dot(sub(corners[1],corners[0]),horizontal)<=0)throw new Error("参考框左右位置异常");
return {corners,height,faceLength:length,
landmarks:{9:brow,152:chin,162:left,389:right}};
};
window.clipHairlineToReferenceBox = function(image, points) {
const box=window.hairlineReferenceBox(points);
const width=image.naturalWidth||image.width,height=image.naturalHeight||image.height;
if(!width||!height)throw new Error("发际线图片尚未加载");
const canvas=document.createElement("canvas");canvas.width=width;canvas.height=height;
const ctx=canvas.getContext("2d");
ctx.beginPath();box.corners.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.closePath();
ctx.clip();ctx.drawImage(image,0,0,width,height);
return canvas;
};
window.drawHairlineReferenceBox = function(ctx, points, crop, width, height) {
const box=window.hairlineReferenceBox(points);
const [x0,y0,x1,y1]=crop;
const screen=p=>[(p[0]-x0)/(x1-x0)*width,(p[1]-y0)/(y1-y0)*height];
const color="#e33399";
ctx.save();ctx.strokeStyle=color;ctx.lineWidth=Math.max(2.5,width/280);ctx.lineJoin="round";
ctx.beginPath();box.corners.forEach((p,i)=>{const q=screen(p);i?ctx.lineTo(...q):ctx.moveTo(...q);});ctx.closePath();ctx.stroke();
ctx.lineWidth=1.4;ctx.setLineDash([7,6]);ctx.beginPath();ctx.moveTo(...screen(box.landmarks[9]));ctx.lineTo(...screen(box.landmarks[152]));ctx.stroke();ctx.setLineDash([]);
const fontSize=Math.max(13,width/43);ctx.font=`600 ${fontSize}px sans-serif`;
for(const id of [9,152,162,389]){
const [x,y]=screen(box.landmarks[id]);
if(x<0||x>width||y<0||y>height)continue;
ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,Math.max(3,width/160),0,Math.PI*2);ctx.fill();
const label=String(id),tw=ctx.measureText(label).width;
const tx=Math.max(3,Math.min(width-tw-8,x+7)),ty=Math.max(fontSize+4,Math.min(height-5,y-7));
ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(label,tx,ty);
}
const top=screen([(box.corners[2][0]+box.corners[3][0])/2,(box.corners[2][1]+box.corners[3][1])/2]);
const text="70% · 9→152",tw=ctx.measureText(text).width;
const tx=Math.max(4,Math.min(width-tw-6,top[0]-tw/2)),ty=Math.max(fontSize+4,top[1]-8);
ctx.fillStyle="rgba(255,255,255,.9)";ctx.fillRect(tx-3,ty-fontSize,tw+6,fontSize+4);ctx.fillStyle="#a51868";ctx.fillText(text,tx,ty);
ctx.restore();
};