|
|
@@ -947,6 +947,7 @@ function ocrNumber(mask,W,ar,cc,F){F=F||1;
|
|
|
/* 剔除极细长线(尺寸界线/引出线,非数字笔画);粘连数字按列投影谷点拆分(如"17"连成方块) */
|
|
|
const comps=[];
|
|
|
for(const c of comps0){
|
|
|
+ if(c.area<8*F*F)continue; /* 孤立微小碎块(手写断笔/字脚)不作数字,避免 1700 被插读成 15700 */
|
|
|
if((c.w<=2*F&&c.h>5*c.w)||(c.h<=2*F&&c.w>5*c.h))continue;
|
|
|
if(c.w>0.85*c.h&&c.w>=8*F){
|
|
|
const colS=[];
|
|
|
@@ -1142,9 +1143,18 @@ async function runAI(){
|
|
|
const ink=new Uint8Array(AW*AH),redm=new Uint8Array(AW*AH);
|
|
|
let rx0=1e9,ry0=1e9,rx1=-1,ry1=-1,rc=0,col=0,fp=0;
|
|
|
for(let i=0;i<AW*AH;i++){const r=d[i*4],g=d[i*4+1],b=d[i*4+2];fp=(fp+r*3+g*5+b*7)%1000000;
|
|
|
- if(r>140&&r-g>50&&r-b>50){rc++;redm[i]=1;if(i%AW<rx0)rx0=i%AW;if(i%AW>rx1)rx1=i%AW;const y=(i/AW)|0;if(y<ry0)ry0=y;if(y>ry1)ry1=y;}
|
|
|
- else if(.299*r+.587*g+.114*b<140)ink[i]=1;
|
|
|
+ if(.299*r+.587*g+.114*b<140)ink[i]=1;
|
|
|
else if(Math.max(r,g,b)-Math.min(r,g,b)>15)col++;}
|
|
|
+ /* 红判定改在原生分辨率做:高分辨率源图直接降采样会把细红线稀释成粉红而漏检(箭头断裂/红框缺失/OCR 误读);
|
|
|
+ 原生分辨率下红芯不稀释。笔画红有两类:纯红(237,28,36)与橙红(237,70,20),G 均<130;
|
|
|
+ 而橙/黄/粉色块 G≥150 → 用 g<130 排除色块、保留两类红笔画;再按格 any-pool 投到 AW 栅格,细线保持连续 */
|
|
|
+ const NW=img.naturalWidth,NH=img.naturalHeight;
|
|
|
+ const nc=document.createElement("canvas");nc.width=NW;nc.height=NH;const ng=nc.getContext("2d");
|
|
|
+ ng.drawImage(img,0,0);const nd=ng.getImageData(0,0,NW,NH).data;
|
|
|
+ for(let y=0;y<NH;y++){const cy=Math.min(AH-1,(y*k)|0);
|
|
|
+ for(let x=0;x<NW;x++){const j=y*NW+x;const r=nd[j*4],g=nd[j*4+1],b=nd[j*4+2];
|
|
|
+ if(r>140&&r-g>50&&r-b>50&&g<130){const cx=Math.min(AW-1,(x*k)|0);const c2=cy*AW+cx;
|
|
|
+ if(!redm[c2]){redm[c2]=1;rc++;if(cx<rx0)rx0=cx;if(cx>rx1)rx1=cx;if(cy<ry0)ry0=cy;if(cy>ry1)ry1=cy;}}}}
|
|
|
logAI(ph("· 结构扫描:"+AW+"×"+AH+" 栅格化("+(AW*AH)+"像素),墨线像素 "+ink.reduce((a,v)=>a+v,0)+",图像指纹 #"+fp));await S2(200);
|
|
|
/* 判定① 图纸类型:平面户型图(彩色填充) vs CAD图(黑白线图) */
|
|
|
const colR=col/(AW*AH);
|
|
|
@@ -1195,11 +1205,17 @@ async function runAI(){
|
|
|
S.redRects.push({x:Math.max(0,h.x-2),y:Math.max(0,h.y-2),w:Math.min(AW-h.x-2,h.w+4),h:Math.min(AH-h.y-2,h.h+4)});}
|
|
|
S.redRects.sort((a,b)=>b.w*b.h-a.w*a.h);
|
|
|
logAI(ph("· 红框矩形提取:"+S.redRects.length+" 个柜体边界框(直接取图纸红框为柜体边界,不二次收紧;已过滤虚线箭头/标注文字)"),"w");await S2(200);
|
|
|
+ if(!S.redRects.length){S.redbox=false; /* 有红候选像素但无闭合红框矩形(图纸自带零散红线/色块)→ 降级场景A 全自动,避免"场景B 却 0 柜体" */
|
|
|
+ logAI("· 判定② 修正:检测到红色像素但无闭合红框矩形 → 降级为场景A(无红框,AI全自动 ±5-8%)","w");
|
|
|
+ $("dc-red").textContent="无红框 · 场景A";$("dc-rule").textContent="无红框规则 ±5-8%";$("q-ai-scene-chip").textContent="户型图 · 场景A 无红框";}
|
|
|
+ if(S.redbox){
|
|
|
/* 红色尺寸标注检测:先剥离红框笔画(避免贴着红框的尺寸箭头因连通域合并被过滤),共线实线段合并为双头箭头,单长条亦视为箭头 */
|
|
|
S.redArrow=null;
|
|
|
+ /* 剥红框:红框笔画整体位于其 bbox 内 → 按 bbox±1 整带清零即可除净框笔画/框内杂线;
|
|
|
+ 尺寸箭头在框 bbox 外(常距框2-3px),±1 不外扩伤箭(旧 ±3 外扩会误删贴框箭头) */
|
|
|
const redm2=redm.slice();
|
|
|
- for(const rc of S.redRects)for(let yy=Math.max(0,rc.y-3);yy<=Math.min(AH-1,rc.y+rc.h+3);yy++)for(let xx=Math.max(0,rc.x-3);xx<=Math.min(AW-1,rc.x+rc.w+3);xx++)redm2[yy*AW+xx]=0;
|
|
|
- const arrows=cc(runMask(redm2)).filter(c=>Math.max(c.w,c.h)>=18&&Math.min(c.w,c.h)<=6);
|
|
|
+ for(const rc of S.redRects)for(let yy=Math.max(0,rc.y-1);yy<=Math.min(AH-1,rc.y+rc.h+1);yy++)for(let xx=Math.max(0,rc.x-1);xx<=Math.min(AW-1,rc.x+rc.w+1);xx++)redm2[yy*AW+xx]=0;
|
|
|
+ const arrows=cc(runMask(redm2)).filter(c=>Math.max(c.w,c.h)>=16&&c.area/Math.max(c.w,c.h)<=6); /* 厚度判据(面积/最长边≤6):兼容高分辨率宽箭头头及箭头与框边残迹并联,仍拒绝窄条红框等实心块 */
|
|
|
const abb=(a1,a2)=>({x:Math.min(a1.x,a2.x),y:Math.min(a1.y,a2.y),w:Math.max(a1.x+a1.w,a2.x+a2.w)-Math.min(a1.x,a2.x),h:Math.max(a1.y+a1.h,a2.y+a2.h)-Math.min(a1.y,a2.y)});
|
|
|
/* 候选箭头列表:单长条 + 共线合并双头箭头;按长降序逐个 OCR,首个读出纯整数者胜出(橙色小数标注含 . / m 无法读成纯整数 → 自然排除) */
|
|
|
const candArrows=[];
|
|
|
@@ -1213,22 +1229,22 @@ async function runAI(){
|
|
|
S.redArrow=candArrows[0]||null;
|
|
|
if(candArrows.length){
|
|
|
/* OCR 高分裁剪:降采样后数字过小易误读 → 从原图按 2 倍 AW 比例裁出箭头邻域,用红色掩码识别数字;逐候选尝试 */
|
|
|
- const OF=2,OM=60;let ocrHit=null;
|
|
|
+ const OM=60,CS=Math.max(2,Math.min(6,1/k));let ocrHit=null; /* CS:裁剪倍率取 1/k → 裁剪≈原生分辨率,数字不稀释;F=CS 使 ocrNumber 容差与 AW 坐标成比例 */
|
|
|
for(const ar of candArrows){
|
|
|
const ox0=Math.max(0,ar.x-OM),oy0=Math.max(0,ar.y-OM);
|
|
|
const ow=Math.min(AW-ox0,ar.w+2*OM),oh=Math.min(AH-oy0,ar.h+2*OM);
|
|
|
- const oc2=document.createElement("canvas");oc2.width=ow*OF;oc2.height=oh*OF;
|
|
|
+ const oc2=document.createElement("canvas");oc2.width=Math.round(ow*CS);oc2.height=Math.round(oh*CS);
|
|
|
const og2=oc2.getContext("2d");og2.imageSmoothingEnabled=true;
|
|
|
- og2.drawImage(img,ox0/k,oy0/k,ow/k,oh/k,0,0,ow*OF,oh*OF);
|
|
|
- const d2=og2.getImageData(0,0,ow*OF,oh*OF).data;
|
|
|
- const rm2=new Uint8Array(ow*OF*oh*OF);
|
|
|
- for(let i2=0;i2<rm2.length;i2++){const r2=d2[i2*4],g2=d2[i2*4+1],b3=d2[i2*4+2];if(r2>140&&r2-g2>50&&r2-b3>50)rm2[i2]=1;}
|
|
|
- const cands=ocrNumber(rm2,ow*OF,{x:(ar.x-ox0)*OF,y:(ar.y-oy0)*OF,w:ar.w*OF,h:ar.h*OF},mkCC(ow*OF),OF);
|
|
|
+ og2.drawImage(img,ox0/k,oy0/k,ow/k,oh/k,0,0,oc2.width,oc2.height);
|
|
|
+ const d2=og2.getImageData(0,0,oc2.width,oc2.height).data;
|
|
|
+ const rm2=new Uint8Array(oc2.width*oc2.height);
|
|
|
+ for(let i2=0;i2<rm2.length;i2++){const r2=d2[i2*4],g2=d2[i2*4+1],b3=d2[i2*4+2];if(r2>140&&r2-g2>50&&r2-b3>50&&g2<130)rm2[i2]=1;}
|
|
|
+ const cands=ocrNumber(rm2,oc2.width,{x:(ar.x-ox0)*CS,y:(ar.y-oy0)*CS,w:ar.w*CS,h:ar.h*CS},mkCC(oc2.width),CS);
|
|
|
if(cands.length){ocrHit={ar,cands};break;}}
|
|
|
if(ocrHit){S.redArrow=ocrHit.ar;$("q-red-anno").value=ocrHit.cands[0];S.redAnnoCands=ocrHit.cands;
|
|
|
logAI("· 红色尺寸标注检测:箭头长 "+Math.round(ocrHit.ar.len)+"px → OCR 读出:<b>"+ocrHit.cands[0]+"</b>"+(ocrHit.cands.length>1?"(存在含糊候选 "+ocrHit.cands.join("/")+",将以 600mm 锚点交叉复核)":"")+"(已自动填入,如有出入可在输入框修改)→ 据此建立全图比例","b");}
|
|
|
else{$("q-red-anno").value="";S.redAnnoCands=[]; /* 清空默认值:OCR 失败时不拿假设值校准,回退 600mm 锚点比例 */
|
|
|
- logAI("· 红色尺寸标注检测:箭头长 "+Math.round(S.redArrow.len)+"px,OCR 未能置信读出标注值 → 暂用 600mm 锚点比例;如知标注数值请在输入框手动填入以校准","e");}}
|
|
|
+ logAI("· 红色尺寸标注检测:箭头长 "+Math.round(S.redArrow.len)+"px,OCR 未能置信读出标注值 → 暂用 600mm 锚点比例;如知标注数值请在输入框手动填入以校准","e");}}}
|
|
|
}
|
|
|
/* 图纸已用红字标注柜体名称(如"2-电视柜")→ 识别之,对应红框柜体直接采用图纸名称,系统不再二次命名 */
|
|
|
if(S.redbox){const LM=dilateMask(redm,AW,AH,3);
|