空洞率量测工具
局部背景差分 · Blob 过滤 · 人工修正
量测本地计算 · Agent 按授权连接百炼
未加载图像
等待上传
上传图像后,可框选矩形或绘制不规则区域
'; downloadBlob(new Blob([html],{type:"text/html;charset=utf-8"}),safeBaseName()+"_空洞率分析报告.html"); } async function decodeStandardImage(file){ let bmp; try{ bmp = await createImageBitmap(file); } catch(e){ const url=URL.createObjectURL(file); try{ bmp=await new Promise(function(resolve,reject){const im=new Image();im.onload=function(){resolve(im);};im.onerror=reject;im.src=url;}); } finally{ setTimeout(function(){URL.revokeObjectURL(url);},1000); } } const c=document.createElement("canvas"); c.width=bmp.width||bmp.naturalWidth; c.height=bmp.height||bmp.naturalHeight; const cctx=c.getContext("2d"); cctx.drawImage(bmp,0,0,c.width,c.height); const src=cctx.getImageData(0,0,c.width,c.height).data; const out=new Uint8Array(c.width*c.height); for(let i=0,j=0;i127)n-=256; if(n>=0){const count=n+1;for(let k=0;k=-127){const count=1-n,v=src[si++];for(let k=0;k>>8)&255;}else{strip[a]=(v>>>8)&255;strip[a+1]=v&255;} } } } } for(let ry=0;ry=lowN){lo=i;break;}} acc=0;for(let i=0;i=highN){hi=i;break;}} if(hi<=lo){lo=0;hi=65535;}const out=new Uint8Array(arr.length),range=hi-lo; for(let i=0;i50*1024*1024){setStatus("文件超过 50 MB",false,true);return;} setStatus("正在读取图像…",true,false);$("analyze").disabled=true;resetResults(); try{ const buffer=await file.arrayBuffer(), b=new Uint8Array(buffer); const isTiff=(b[0]===73&&b[1]===73&&b[2]===42&&b[3]===0)||(b[0]===77&&b[1]===77&&b[2]===0&&b[3]===42); let decoded; if(isTiff)decoded=decodeTiff(buffer);else decoded=await decodeStandardImage(file); gray8=percentileNormalize(decoded.data);imageWidth=decoded.width;imageHeight=decoded.height;imageType=decoded.type;fileName=file.name; polygonPoints=[];polygonClosed=false;polygonHover=null;selectionMode="rect"; document.querySelectorAll(".selection-tab").forEach(function(b){b.classList.toggle("active",b.dataset.selection==="rect");}); roiFields.forEach(function(id){$(id).disabled=false;});$("selectionHint").textContent="在图像上拖拽框选矩形 ROI。";$("resetPolygon").style.display="none"; makeBaseImage();$("x").value=0;$("y").value=0;$("w").value=imageWidth;$("h").value=imageHeight; $("empty").hidden=true;$("canvasbox").hidden=false;$("filename").style.display="block";$("filename").textContent=file.name; $("imginfo").textContent=file.name+" · "+imageWidth+"×"+imageHeight;$("analyze").disabled=false;$("clearAll").disabled=false; fitCanvas();setStatus("已加载 · "+imageType,false,false); }catch(e){ gray8=null;$("empty").hidden=false;$("canvasbox").hidden=true;setStatus(e.message||"图像读取失败",false,true); $("note").textContent=(e.message||"图像读取失败")+"。可尝试从设备软件无损导出为 PNG 后再量测。"; } } function boxesForGauss(sigma,n){ const ideal=Math.sqrt(12*sigma*sigma/n+1),wl=Math.max(1,Math.floor(ideal)|1),wu=wl+2; const m=Math.round((12*sigma*sigma-n*wl*wl-4*n*wl-3*n)/(-4*wl-4)); const a=[];for(let i=0;i=0&&ny>=0&&nx=w||ny>=h||!binary[ny*w+nx])perimeter++;} for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++){ if(dx===0&&dy===0)continue;const nx=x+dx,ny=y+dy;if(nx<0||ny<0||nx>=w||ny>=h)continue; const np=ny*w+nx;if(binary[np]&&!seen[np]){seen[np]=1;queue[tail++]=np;} } } const area=tail,circ=perimeter?4*Math.PI*area/(perimeter*perimeter):0; if(area>=minArea&&area<=maxArea&&circ>=minCirc){ count++;total+=area;if(area>largest)largest=area;if(final)for(let i=0;iscanY)||(b.y<=scanY&&a.y>scanY))intersections.push(a.x+(scanY-a.y)*(b.x-a.x)/(b.y-a.y)); } intersections.sort(function(a,b){return a-b;}); for(let k=0;k+1threshold:residual<-threshold)?1:0;} let cleaned=morph(binary,q.w,q.h,2,true);cleaned=morph(cleaned,q.w,q.h,2,false);cleaned=morph(cleaned,q.w,q.h,1,false);cleaned=morph(cleaned,q.w,q.h,1,true); for(let i=0;i1)return; const p=pointerPos(e),point={x:p.x/displayScale,y:p.y/displayScale}; if(polygonPoints.length>=3){const first=polygonPoints[0],dx=point.x-first.x,dy=point.y-first.y;if(Math.sqrt(dx*dx+dy*dy)*displayScale<=12){closePolygon();return;}} if(polygonClosed){polygonPoints=[];polygonClosed=false;}polygonPoints.push(point);polygonHover=point;syncPolygonFields();renderComposite();$("analyze").disabled=true;setStatus("已添加 "+polygonPoints.length+" 个顶点",false,false); }); canvas.addEventListener("dblclick",function(e){if(!analyzed&&selectionMode==="polygon"){e.preventDefault();closePolygon();}}); function endPointer(){if(agentDrawing)finishAgentAnnotation();drawingROI=false;editing=false;}canvas.addEventListener("pointerup",endPointer);canvas.addEventListener("pointercancel",endPointer); window.addEventListener("resize",function(){if(gray8)fitCanvas();});