ppu-paddle-ocr
Version:
Blazing-fast and lightweight PaddleOCR library for Node.js and Bun. Perform accurate text detection, recognition, and image deskew with a simple, modern, and type-safe API. Ideal for document processing, data extraction, and computer vision tasks.
1 lines • 6.15 kB
JavaScript
export class DeskewService{options;debugging;constructor(options={},debugging={}){this.options={...DEFAULT_DETECTION_OPTIONS,...options};this.debugging={...DEFAULT_DEBUGGING_OPTIONS,...debugging}}log(message){if(this.debugging.verbose){console.log(`[DeskewService] ${message}`)}}async calculateSkewAngle(probabilityMapCanvas){let processor=new ImageProcessor(probabilityMapCanvas);let mat=processor.grayscale().threshold({lower:0,upper:255,type:cv.THRESH_BINARY+cv.THRESH_OTSU}).toMat();let contours=new Contours(mat,{mode:cv.RETR_LIST,method:cv.CHAIN_APPROX_SIMPLE});processor.destroy();let minAngle=-20;let maxAngle=20;let minArea=this.options.minimumAreaThreshold||20;let textRegions=[];contours.iterate((contour)=>{let rect=contours.getRect(contour);let area=rect.width*rect.height;if(area<minArea)return;let aspectRatio=rect.width/rect.height;if(aspectRatio>0.2&&aspectRatio<10){textRegions.push({rect,contour,area,aspectRatio})}});if(textRegions.length===0){this.log("No valid text regions found for skew calculation.");contours.destroy();return 0}let averageHeight=textRegions.reduce((sum,region)=>sum+region.rect.height,0)/textRegions.length;let filteredRegions=textRegions.filter((region)=>{return region.rect.height<=averageHeight*1.5});this.log(`Found ${filteredRegions.length} text regions for skew analysis.`);let minRectAngles=this.calculateMinRectAngles(filteredRegions,contours);let baselineAngles=this.calculateBaselineAngles(filteredRegions);let houghAngles=this.calculateHoughAngles(mat,minAngle,maxAngle);contours.destroy();let allAngles=[...minRectAngles.map((a)=>({...a,method:"minRect"})),...baselineAngles.map((a)=>({...a,method:"baseline"})),...houghAngles.map((a)=>({...a,method:"hough"}))];if(allAngles.length===0){this.log("No angles detected from any method.");return 0}let consensusAngle=this.calculateConsensusAngle(allAngles,minAngle,maxAngle);this.log(`Calculated skew angle: ${consensusAngle.toFixed(3)}° (from ${allAngles.length} measurements)`);return consensusAngle}calculateMinRectAngles(textRegions,contours){let angles=[];for(let region of textRegions){try{let minRect=cv.minAreaRect(region.contour);if(!minRect)continue;let angle=minRect.angle;if(angle>45){angle-=90}else if(angle<-45){angle+=90}let areaWeight=Math.log(region.area+1);let aspectWeight=Math.min(region.aspectRatio,1/region.aspectRatio)*2;let weight=areaWeight*aspectWeight;angles.push({angle,weight})}catch(error){continue}}return angles}calculateBaselineAngles(textRegions){let angles=[];for(let region of textRegions){try{let points=region.contour.data32S;if(!points||points.length<8)continue;let bottomPoints=[];for(let i=0;i<points.length;i+=2){let x=points[i];let y=points[i+1];if(x!==undefined&&y!==undefined){bottomPoints.push({x,y})}}if(bottomPoints.length<3)continue;bottomPoints.sort((a,b)=>a.x-b.x);let segments=3;let segmentSize=Math.floor(bottomPoints.length/segments);let baselinePoints=[];for(let seg=0;seg<segments;seg++){let start=seg*segmentSize;let end=seg===segments-1?bottomPoints.length:(seg+1)*segmentSize;let segmentPoints=bottomPoints.slice(start,end);if(segmentPoints.length>0){let maxYPoint=segmentPoints.reduce((max,point)=>point.y>max.y?point:max);baselinePoints.push(maxYPoint)}}if(baselinePoints.length>=2){let angle=this.calculateLineAngle(baselinePoints);let weight=region.area*Math.min(region.aspectRatio,1/region.aspectRatio);angles.push({angle,weight})}}catch(error){continue}}return angles}calculateHoughAngles(mat,minAngle,maxAngle){let angles=[];try{let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(3,1));let morphed=new cv.Mat;cv.morphologyEx(mat,morphed,cv.MORPH_CLOSE,kernel);let lines=new cv.Mat;cv.HoughLinesP(morphed,lines,1,Math.PI/180,30,50,10);for(let i=0;i<lines.rows;i++){let line=lines.data32S.subarray(i*4,(i+1)*4);const[x1,y1,x2,y2]=line;if(x1!==undefined&&y1!==undefined&&x2!==undefined&&y2!==undefined){let dx=x2-x1;let dy=y2-y1;if(Math.abs(dx)>1){let angle=Math.atan2(dy,dx)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;if(angle>=minAngle&&angle<=maxAngle){let lineLength=Math.sqrt(dx*dx+dy*dy);angles.push({angle,weight:lineLength})}}}}morphed.delete();lines.delete();kernel.delete()}catch(error){this.log("Hough transform failed, skipping this method.")}return angles}calculateLineAngle(points){if(points.length<2)return 0;let n=points.length;let sumX=points.reduce((sum,p)=>sum+p.x,0);let sumY=points.reduce((sum,p)=>sum+p.y,0);let sumXY=points.reduce((sum,p)=>sum+p.x*p.y,0);let sumXX=points.reduce((sum,p)=>sum+p.x*p.x,0);let denominator=n*sumXX-sumX*sumX;if(Math.abs(denominator)<0.0000000001)return 0;let slope=(n*sumXY-sumX*sumY)/denominator;let angle=Math.atan(slope)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;return angle}calculateConsensusAngle(angles,minAngle,maxAngle){if(angles.length===0)return 0;let sortedAngles=[...angles].sort((a,b)=>a.angle-b.angle);let q1Index=Math.floor(sortedAngles.length*0.25);let q3Index=Math.floor(sortedAngles.length*0.75);let q1=sortedAngles[q1Index]?.angle||0;let q3=sortedAngles[q3Index]?.angle||0;let iqr=q3-q1;let lowerBound=q1-1.5*iqr;let upperBound=q3+1.5*iqr;let filteredAngles=angles.filter((a)=>a.angle>=lowerBound&&a.angle<=upperBound&&a.angle>=minAngle&&a.angle<=maxAngle);if(filteredAngles.length===0){this.log("All angles filtered out as outliers, using median of original set.");let medianIndex=Math.floor(sortedAngles.length/2);return sortedAngles[medianIndex]?.angle||0}let totalWeight=filteredAngles.reduce((sum,a)=>sum+a.weight,0);if(totalWeight===0){let average=filteredAngles.reduce((sum,a)=>sum+a.angle,0)/filteredAngles.length;return average}let weightedSum=filteredAngles.reduce((sum,a)=>sum+a.angle*a.weight,0);let weightedAverage=weightedSum/totalWeight;let methodCounts=filteredAngles.reduce((counts,a)=>{counts[a.method]=(counts[a.method]||0)+1;return counts},{});this.log(`Angle methods used: ${Object.entries(methodCounts).map(([method,count])=>`${method}:${count}`).join(", ")}`);return Math.max(minAngle,Math.min(maxAngle,weightedAverage))}}import{Contours,ImageProcessor,cv}from"ppu-ocv";import{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_DETECTION_OPTIONS}from"../constants";