|
@@ -933,7 +933,7 @@ function chrTemplates(){if(CHR_T)return CHR_T;
|
|
|
return CHR_T;}
|
|
return CHR_T;}
|
|
|
/* 读取红框旁的红字柜体名称标注:高分裁剪→红掩码→列投影分字→数字/短横前缀跳过→汉字模板匹配;返回名称串,失败返回 null */
|
|
/* 读取红框旁的红字柜体名称标注:高分裁剪→红掩码→列投影分字→数字/短横前缀跳过→汉字模板匹配;返回名称串,失败返回 null */
|
|
|
function readRedLabel(b0,AW,AH,k){
|
|
function readRedLabel(b0,AW,AH,k){
|
|
|
- const OF=2,OM=6;
|
|
|
|
|
|
|
+ const OF=Math.max(2,Math.min(4,Math.round(1/k))),OM=6; /* 尽量按原图分辨率裁剪,避免降采样糊化笔画 */
|
|
|
const ox0=Math.max(0,b0.x-OM),oy0=Math.max(0,b0.y-OM);
|
|
const ox0=Math.max(0,b0.x-OM),oy0=Math.max(0,b0.y-OM);
|
|
|
const ow=Math.min(AW-ox0,b0.w+2*OM),oh=Math.min(AH-oy0,b0.h+2*OM);
|
|
const ow=Math.min(AW-ox0,b0.w+2*OM),oh=Math.min(AH-oy0,b0.h+2*OM);
|
|
|
const oc=document.createElement("canvas");oc.width=ow*OF;oc.height=oh*OF;
|
|
const oc=document.createElement("canvas");oc.width=ow*OF;oc.height=oh*OF;
|
|
@@ -957,7 +957,7 @@ function readRedLabel(b0,AW,AH,k){
|
|
|
for(const p of parts){
|
|
for(const p of parts){
|
|
|
const bmp={w:p.w,h:p.h,a:new Uint8Array(p.w*p.h)};
|
|
const bmp={w:p.w,h:p.h,a:new Uint8Array(p.w*p.h)};
|
|
|
for(let y=0;y<p.h;y++)for(let x=0;x<p.w;x++)bmp.a[y*p.w+x]=rm2[(p.y+y)*W+(p.x+x)];
|
|
for(let y=0;y<p.h;y++)for(let x=0;x<p.w;x++)bmp.a[y*p.w+x]=rm2[(p.y+y)*W+(p.x+x)];
|
|
|
- if(p.h<=0.45*maxH){dbg.parts.push({w:p.w,h:p.h,cls:"dash"});continue;} /* 短横分隔符(如"2-电视柜"的"-") */
|
|
|
|
|
|
|
+ if(p.w<=0.45*maxH){dbg.parts.push({w:p.w,h:p.h,cls:"narrow"});continue;} /* 窄部件(短横/数字前缀/断裂笔画):柜名汉字均为全宽字,一律跳过 */
|
|
|
const dn=bmNorm(bmp,9,12);let bD=-1;
|
|
const dn=bmNorm(bmp,9,12);let bD=-1;
|
|
|
for(let dg=0;dg<10;dg++){const v=bmIou(dn,DT[dg]);if(v>bD)bD=v;}
|
|
for(let dg=0;dg<10;dg++){const v=bmIou(dn,DT[dg]);if(v>bD)bD=v;}
|
|
|
const cn2=bmNorm(bmp,20,20);let bC=-1,bCh="";
|
|
const cn2=bmNorm(bmp,20,20);let bC=-1,bCh="";
|
|
@@ -1086,9 +1086,9 @@ async function runAI(){
|
|
|
else logAI("· OCR 未能置信读出蓝色标注值 → 请在下方输入框手动填写标注数值(默认按 mm)","e");}
|
|
else logAI("· OCR 未能置信读出蓝色标注值 → 请在下方输入框手动填写标注数值(默认按 mm)","e");}
|
|
|
}
|
|
}
|
|
|
/* 图纸已用红字标注柜体名称(如"2-电视柜")→ 识别之,对应红框柜体直接采用图纸名称,系统不再二次命名 */
|
|
/* 图纸已用红字标注柜体名称(如"2-电视柜")→ 识别之,对应红框柜体直接采用图纸名称,系统不再二次命名 */
|
|
|
- if(S.redbox){const LM=dilateMask(redm,AW,AH,2);
|
|
|
|
|
|
|
+ if(S.redbox){const LM=dilateMask(redm,AW,AH,3);
|
|
|
for(const b0 of cc(LM)){
|
|
for(const b0 of cc(LM)){
|
|
|
- if(b0.w<18||b0.w>0.4*AW||b0.h<7||b0.h>30||b0.area<40)continue;
|
|
|
|
|
|
|
+ if(b0.w<18||b0.w>0.4*AW||b0.h<7||b0.h>40||b0.area<40)continue;
|
|
|
const txt=readRedLabel(b0,AW,AH,k);if(txt)S.redLabels.push({x:b0.x,y:b0.y,w:b0.w,h:b0.h,text:txt});}
|
|
const txt=readRedLabel(b0,AW,AH,k);if(txt)S.redLabels.push({x:b0.x,y:b0.y,w:b0.w,h:b0.h,text:txt});}
|
|
|
if(S.redLabels.length)logAI("· 图纸红字柜体名称识别:"+S.redLabels.length+" 处("+S.redLabels.map(l=>l.text).join("、")+")→ 对应红框柜体直接按图纸标注命名,系统不二次命名","b");}
|
|
if(S.redLabels.length)logAI("· 图纸红字柜体名称识别:"+S.redLabels.length+" 处("+S.redLabels.map(l=>l.text).join("、")+")→ 对应红框柜体直接按图纸标注命名,系统不二次命名","b");}
|
|
|
setProg(0,"done");setProg(1,"running");
|
|
setProg(0,"done");setProg(1,"running");
|
|
@@ -1405,17 +1405,18 @@ async function runAI(){
|
|
|
if(S.scale||S.ft==="cad"){const wM=c.w/k*dimScale(),hM=c.h/k*dimScale();
|
|
if(S.scale||S.ft==="cad"){const wM=c.w/k*dimScale(),hM=c.h/k*dimScale();
|
|
|
logAI("· "+nm+"["+src+"] "+tag+":实测 "+Math.round(c.w/k)+"×"+Math.round(c.h/k)+"px × "+(dimScale()*1000).toFixed(2)+"mm/px → 最长边 "+fmt2(Math.max(wM,hM))+"m × 高 "+fmt2(fh())+"m → 投影面积 = <b>"+fmt2(Math.max(wM,hM)*fh())+"㎡</b>");}
|
|
logAI("· "+nm+"["+src+"] "+tag+":实测 "+Math.round(c.w/k)+"×"+Math.round(c.h/k)+"px × "+(dimScale()*1000).toFixed(2)+"mm/px → 最长边 "+fmt2(Math.max(wM,hM))+"m × 高 "+fmt2(fh())+"m → 投影面积 = <b>"+fmt2(Math.max(wM,hM)*fh())+"㎡</b>");}
|
|
|
else logAI("· "+nm+"["+src+"] "+tag+":"+Math.round(c.w/k)+"px × "+Math.round(c.h/k)+"px(比例未建立,补充已知尺寸后自动换算)","w");};
|
|
else logAI("· "+nm+"["+src+"] "+tag+":"+Math.round(c.w/k)+"px × "+Math.round(c.h/k)+"px(比例未建立,补充已知尺寸后自动换算)","w");};
|
|
|
- const usedLb=new Set();
|
|
|
|
|
- for(const c of S.redRects){if(cn>=8)break;
|
|
|
|
|
|
|
+ /* 红字标签↔红框全局最近配对:bbox 间隙距离 ≤60px,按距离升序贪心,一框一标签 */
|
|
|
|
|
+ const rcName=new Map();
|
|
|
|
|
+ if(S.redLabels&&S.redLabels.length){const pairs=[];
|
|
|
|
|
+ S.redRects.forEach((c,ri)=>{for(let li=0;li<S.redLabels.length;li++){const lb=S.redLabels[li];
|
|
|
|
|
+ const dx=Math.max(c.x-(lb.x+lb.w),lb.x-(c.x+c.w),0),dy=Math.max(c.y-(lb.y+lb.h),lb.y-(c.y+c.h),0);
|
|
|
|
|
+ const dd2=Math.hypot(dx,dy);if(dd2<=60)pairs.push({d:dd2,ri,li});}});
|
|
|
|
|
+ pairs.sort((a,b)=>a.d-b.d);const usedR=new Set(),usedL=new Set();
|
|
|
|
|
+ for(const p of pairs){if(usedR.has(p.ri)||usedL.has(p.li))continue;usedR.add(p.ri);usedL.add(p.li);rcName.set(p.ri,S.redLabels[p.li].text);}}
|
|
|
|
|
+ for(let ri=0;ri<S.redRects.length;ri++){if(cn>=8)break;const c=S.redRects[ri];
|
|
|
if(kitchen){const ov=Math.max(0,Math.min(c.x+c.w,kitchen.x+kitchen.w)-Math.max(c.x,kitchen.x))*Math.max(0,Math.min(c.y+c.h,kitchen.y+kitchen.h)-Math.max(c.y,kitchen.y));
|
|
if(kitchen){const ov=Math.max(0,Math.min(c.x+c.w,kitchen.x+kitchen.w)-Math.max(c.x,kitchen.x))*Math.max(0,Math.min(c.y+c.h,kitchen.y+kitchen.h)-Math.max(c.y,kitchen.y));
|
|
|
if(ov>=0.9*kitchen.w*kitchen.h)continue;} // 红框整包厨房台面带=校准框,非柜体圈注
|
|
if(ov>=0.9*kitchen.w*kitchen.h)continue;} // 红框整包厨房台面带=校准框,非柜体圈注
|
|
|
- /* 红字标签关联:标签与红框的 bbox 间隙距离 ≤60px 且最近者 → 该红框名称直接采用图纸标注 */
|
|
|
|
|
- let nameOv=null,bd2=1e9,nameOvLi=-1;
|
|
|
|
|
- if(S.redLabels)for(let li=0;li<S.redLabels.length;li++){if(usedLb.has(li))continue;const lb=S.redLabels[li];
|
|
|
|
|
- const dx=Math.max(c.x-(lb.x+lb.w),lb.x-(c.x+c.w),0),dy=Math.max(c.y-(lb.y+lb.h),lb.y-(c.y+c.h),0);
|
|
|
|
|
- const dd2=Math.hypot(dx,dy);if(dd2<=60&&dd2<bd2){bd2=dd2;nameOv=lb.text;nameOvLi=li;}}
|
|
|
|
|
- if(nameOv!==null)usedLb.add(nameOvLi);
|
|
|
|
|
- pushCab(c,"红框",nameOv);}
|
|
|
|
|
|
|
+ pushCab(c,"红框",rcName.get(ri)||null);}
|
|
|
if(S.redbox)logAI("· 场景B:柜体边界直接取图纸红框,系统不二次抓取(不合并 AI 特征抓取、不二次红框标注)","w");
|
|
if(S.redbox)logAI("· 场景B:柜体边界直接取图纸红框,系统不二次抓取(不合并 AI 特征抓取、不二次红框标注)","w");
|
|
|
else for(const c of wards){if(cn>=8)break;pushCab(c,"AI");} /* 场景A 无红框,无需红框去重 */
|
|
else for(const c of wards){if(cn>=8)break;pushCab(c,"AI");} /* 场景A 无红框,无需红框去重 */
|
|
|
logAI(ph("· 柜体识别:"+cn+" 个(最长边×层高=投影面积,均为到顶高柜)"),"b");await S2(200);
|
|
logAI(ph("· 柜体识别:"+cn+" 个(最长边×层高=投影面积,均为到顶高柜)"),"b");await S2(200);
|