#!/usr/bin/env python3 """Debug script: analyze text line slopes on deskewed image to determine true residual shear.""" import sys, math, asyncio sys.path.insert(0, "/app/backend") import cv2 import numpy as np import pytesseract from ocr_pipeline_session_store import get_session_db SESSION_ID = "3dcb1897-09a6-4b80-91b5-7e4207980bf3" async def main(): s = await get_session_db(SESSION_ID) if not s: print("Session not found") return deskewed_png = s.get("deskewed_png") if not deskewed_png: print("No deskewed_png stored") return arr = np.frombuffer(deskewed_png, dtype=np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_COLOR) h, w = img.shape[:2] print(f"Deskewed image: {w}x{h}") # Detect text line slopes using Tesseract word positions gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) data = pytesseract.image_to_data(gray, output_type=pytesseract.Output.DICT, config="--psm 6") lines = {} for i in range(len(data["text"])): txt = (data["text"][i] or "").strip() if len(txt) < 2 or data["conf"][i] < 30: continue key = (data["block_num"][i], data["par_num"][i], data["line_num"][i]) cx = data["left"][i] + data["width"][i] / 2 cy = data["top"][i] + data["height"][i] / 2 if key not in lines: lines[key] = [] lines[key].append((cx, cy)) slopes = [] for key, pts in lines.items(): if len(pts) < 3: continue pts.sort(key=lambda p: p[0]) xs = np.array([p[0] for p in pts]) ys = np.array([p[1] for p in pts]) if xs[-1] - xs[0] < w * 0.2: continue A = np.vstack([xs, np.ones(len(xs))]).T result = np.linalg.lstsq(A, ys, rcond=None) slope = result[0][0] angle_deg = math.degrees(math.atan(slope)) slopes.append(angle_deg) if not slopes: print("No text lines detected") return median_slope = sorted(slopes)[len(slopes) // 2] mean_slope = sum(slopes) / len(slopes) print(f"Text lines found: {len(slopes)}") print(f"Median slope: {median_slope:.4f} deg") print(f"Mean slope: {mean_slope:.4f} deg") print(f"Range: [{min(slopes):.4f}, {max(slopes):.4f}]") print() print("Individual line slopes:") for s in sorted(slopes): print(f" {s:+.4f}") # Also test the 4 dewarp methods directly print("\n--- Dewarp method results on deskewed image ---") from cv_vocab_pipeline import ( _detect_shear_angle, _detect_shear_by_projection, _detect_shear_by_hough, _detect_shear_by_text_lines, ) for name, fn in [ ("vertical_edge", _detect_shear_angle), ("projection", _detect_shear_by_projection), ("hough_lines", _detect_shear_by_hough), ("text_lines", _detect_shear_by_text_lines), ]: r = fn(img) print(f" {name}: shear={r['shear_degrees']:.4f} conf={r['confidence']:.3f}") # The user says "right side needs to come down 3mm" # For a ~85mm wide image (1002px at ~300DPI), 3mm ~ 35px # shear angle = atan(35 / 1556) ~ 1.29 degrees # Let's check: what does the image look like if we apply 0.5, 1.0, 1.5 deg shear? print("\n--- Pixel shift at right edge for various shear angles ---") for deg in [0.5, 0.8, 1.0, 1.3, 1.5, 2.0]: shift_px = h * math.tan(math.radians(deg)) shift_mm = shift_px / (w / 85.0) # approximate mm print(f" {deg:.1f} deg -> {shift_px:.0f}px shift -> ~{shift_mm:.1f}mm") asyncio.run(main())