CaptureOCR is a Windows desktop application that transforms screenshots into structured data (Markdown tables, CSV) using local OCR. Built with Python, PySide6, and ONNX Runtime, it processes everything on-device with zero cloud dependencies.
Product link: CaptureOCR on Microsoft Store — $2.99 one-time, no subscription.
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ UI Layer (PySide6) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Full-screen │ │ Region │ │ Canvas/Annotation │ │
│ │ Selection │ │ Drawer │ │ Toolbar │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Application Services │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ Capture Manager │ │ OCR Workflow Orchestrator │ │
│ │ - mss grab │ │ - Queue management │ │
│ │ - DPI mapping │ │ - Model tier switching │ │
│ │ - Multi-monitor │ │ - Progress reporting │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Export Service │ │
│ │ - Markdown/CSV │ │
│ │ - Session (.snipx)│ │
│ └─────────────────┘ │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ OCR Backend │ │ Tiling │ │ Scroll Capture │ │
│ │ (RapidOCR + │ │ Engine │ │ (Row signatures) │ │
│ │ ONNX Runtime)│ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Key Design Decisions
1. Single OCR Worker Thread
Decision: All model inference runs on a dedicated background thread.
Rationale:
- ONNX Runtime's
InferenceSessionis not guaranteed thread-safe - Avoids complex locking around model loads/unloads
- For a desktop tool processing ~10 regions, serial throughput is sufficient
- Predictable memory profile (no concurrent model instances)
# Simplified worker pattern
class OcrWorker(QThread):
result_ready = pyqtSignal(OcrResult)
def __init__(self):
super().__init__()
self.queue = Queue()
self.session = None
self.current_model = None
def run(self):
while True:
task = self.queue.get()
if task is None:
break
result = self._process(task)
self.result_ready.emit(result)
2. Two-Tier Model Strategy
| Tier | Model | Size | Behavior |
|---|---|---|---|
| Fast | PP-OCRv6 tiny | 6.6 MB | Bundled in MSIX, preloaded at startup, stays resident |
| Accurate | PP-OCRv6 small | 29.8 MB | Downloaded on demand (SHA-256 verified), unloads after 5s idle |
Implementation:
class ModelManager:
def __init__(self):
self.tiny_session = self._load_tiny() # Always loaded
self.small_session = None
self.last_small_use = 0
def get_session(self, tier: str):
if tier == "fast":
return self.tiny_session
if self.small_session is None:
self.small_session = self._download_and_load_small()
self.last_small_use = time.time()
return self.small_session
def maybe_unload_small(self):
if self.small_session and time.time() - self.last_small_use > 5:
self.small_session = None
gc.collect()
3. DPI-Aware Multi-Monitor Capture
Windows captures in physical pixels; Qt works in logical coordinates. On 125-150% scaling (and mixed-DPI setups), this mismatch causes blurry captures or offset selections.
Solution: Per-monitor ScreenInfo with both logical and physical rects + device pixel ratio.
@dataclass
class ScreenInfo:
index: int
logical_rect: QRect # Qt coordinates
physical_rect: QRect # Screen pixels
dpr: float # Device pixel ratio
def logical_to_physical(screen: ScreenInfo, logical_rect: QRect) -> QRect:
"""Convert Qt logical rect to physical pixels for mss capture."""
x = int((logical_rect.x() - screen.logical_rect.x()) * screen.dpr)
y = int((logical_rect.y() - screen.logical_rect.y()) * screen.dpr)
w = int(logical_rect.width() * screen.dpr)
h = int(logical_rect.height() * screen.dpr)
return QRect(x, y, w, h)
4. Tiled OCR for Long Images
Feeding a 20,000px tall screenshot into a 1600px OCR cap scales text to ~2px — unreadable. Instead, tile vertically:
Tile parameters:
- Target height: 1600px
- Overlap: 64px (≈2 lines at 1080p)
- Core bands: [0, 1536), [1536, 3072), ...
- Each tile extends 32px above/below core for context
- Line assigned to tile where box center Y falls in core band
This ensures every line is processed at native resolution, claimed exactly once.
5. Row-Signature Scroll Stitching
Template matching fails for scrolling (neither frame contains the other). Instead:
- Convert frame to grayscale, ignore right 8% (scrollbars)
- Compress each row to 32 column-bucket means →
(H, 32)signature - For each candidate shift
s, compute MAE betweenprev[s:]andcur[:H-s] - Accept shift only if best score < threshold AND clearly better than median
- Sticky headers: retry skipping 8%/16%/25% of top rows
- Adaptive wheel step: measure first scroll, target ~45% of selection height
Performance Characteristics
| Metric | Tiny (Fast) | Small (Accurate) |
|---|---|---|
| Model size | 6.61 MB | 29.79 MB |
| Load time | ~1.35s | ~1.06s |
| Avg latency (23-img set) | 78.7 ms | 257.5 ms |
| P95 latency | 144 ms | 471 ms |
| Char accuracy (CER) | 99.24% | 99.69% |
Test set: 23 mixed Chinese/English screenshots, 1600px cap, local CPU. Not a public benchmark — treat as order of magnitude.
Distribution
- Format: MSIX (runFullTrust), distributed via Microsoft Store
- Size: ~126 MB (includes tiny model, ONNX Runtime, Python runtime)
- Dependencies: Zero — fully self-contained
- Auto-updates: Handled by Store
Lessons Learned
Detection config matters more than model choice: Switching from
limit_type="min"(upscales short side) tolimit_type="max"(only downscale long side) was a bigger win than model tuning.Disable classification for screenshots:
use_cls=False— the 0°/180° classifier adds latency with no signal for upright captures.DPI bugs are subtle: Logical/physical coordinate mismatch only appears on scaled displays. Test on 125%, 150%, and mixed-DPI multi-monitor.
Serialization format for reproducibility: The
.snipxsession package (ZIP withsession.json+ original PNGs + Markdown/CSV) enables lossless round-tripping and collaboration.
Resources
- GitHub: github.com/lijiannan1241/CaptureOCR — product page & issue tracker
- Microsoft Store: CaptureOCR
- Source availability: Application is closed-source; repo hosts product page and issue tracker only
Disclosure: I'm the developer of CaptureOCR. This is a paid product ($6.99 one-time). The application source code is not public — the repository serves as the product page and issue tracker.
Top comments (0)