[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7C6SC6MSD280NBC08QX239Q":3,"skill-same:01M7C6SC6MSD280NBC08QX239Q":45},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":30,"install":31,"resources":44},"01M7C6SC6MSD280NBC08QX239Q","image-ocr",null,"Extract text content from images using Tesseract OCR via Python","linyouxue\u002Fskill-repair-benchmark","linyouxue","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F216151464?v=4","01M7BJEVCXKD74KH6FG9CBVSQB","林宥学","01M7BJEVE02NP217V93BKZFC0J",0,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fevaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-gate-round27-opus80-20261012\u002Finputs\u002Fround-27\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: image-ocr\ndescription: Extract text content from images using Tesseract OCR via Python\n---\n\n# Image OCR Skill\n\n## Purpose\nThis skill enables accurate text extraction from image files (JPG, PNG, etc.) using Tesseract OCR via the `pytesseract` Python library. It is suitable for scanned documents, screenshots, photos of text, receipts, forms, and other visual content containing text.\n\n## When to Use\n- Extracting text from scanned documents or photos\n- Reading text from screenshots or image captures\n- Processing batch image files that contain textual information\n- Converting visual documents to machine-readable text\n- Extracting structured data from forms, receipts, or tables in images\n\n## Required Libraries\n\nThe following Python libraries are required:\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n```\n\n## Input Requirements\n- **File formats**: JPG, JPEG, PNG, WEBP\n- **Image quality**: Minimum 300 DPI recommended for printed text; clear and legible text\n- **File size**: Under 5MB per image (resize if necessary)\n- **Text language**: Specify if non-English to improve accuracy\n\n## Output Schema\nAll extracted content must be returned as valid JSON conforming to this schema:\n\n```json\n{\n  \"success\": true,\n  \"filename\": \"example.jpg\",\n  \"extracted_text\": \"Full raw text extracted from the image...\",\n  \"confidence\": \"high|medium|low\",\n  \"metadata\": {\n    \"language_detected\": \"en\",\n    \"text_regions\": 3,\n    \"has_tables\": false,\n    \"has_handwriting\": false\n  },\n  \"warnings\": [\n    \"Text partially obscured in bottom-right corner\",\n    \"Low contrast detected in header section\"\n  ]\n}\n```\n\n\n### Field Descriptions\n\n- `success`: Boolean indicating whether text extraction completed\n- `filename`: Original image filename\n- `extracted_text`: Complete text content in reading order (top-to-bottom, left-to-right)\n- `confidence`: Overall OCR confidence level based on image quality and text clarity\n- `metadata.language_detected`: ISO 639-1 language code\n- `metadata.text_regions`: Number of distinct text blocks identified\n- `metadata.has_tables`: Whether tabular data structures were detected\n- `metadata.has_handwriting`: Whether handwritten text was detected\n- `warnings`: Array of quality issues or potential errors\n\n\n## Code Examples\n\n### Basic OCR Extraction\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_text_from_image(image_path):\n    \"\"\"Extract text from a single image using Tesseract OCR.\"\"\"\n    img = Image.open(image_path)\n    text = pytesseract.image_to_string(img)\n    return text.strip()\n```\n\n### OCR with Confidence Data\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_with_confidence(image_path):\n    \"\"\"Extract text with per-word confidence scores.\"\"\"\n    img = Image.open(image_path)\n\n    # Get detailed OCR data including confidence\n    data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n    words = []\n    confidences = []\n\n    for i, word in enumerate(data['text']):\n        if word.strip():  # Skip empty strings\n            words.append(word)\n            confidences.append(data['conf'][i])\n\n    # Calculate average confidence\n    avg_confidence = sum(c for c in confidences if c > 0) \u002F len([c for c in confidences if c > 0]) if confidences else 0\n\n    return {\n        'text': ' '.join(words),\n        'average_confidence': avg_confidence,\n        'word_count': len(words)\n    }\n```\n\n### Full OCR with JSON Output\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n\ndef ocr_to_json(image_path):\n    \"\"\"Perform OCR and return results as JSON.\"\"\"\n    filename = os.path.basename(image_path)\n    warnings = []\n\n    try:\n        img = Image.open(image_path)\n\n        # Get detailed OCR data\n        data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n        # Extract text preserving structure\n        text = pytesseract.image_to_string(img)\n\n        # Calculate confidence\n        confidences = [c for c in data['conf'] if c > 0]\n        avg_conf = sum(confidences) \u002F len(confidences) if confidences else 0\n\n        # Determine confidence level\n        if avg_conf >= 80:\n            confidence = \"high\"\n        elif avg_conf >= 50:\n            confidence = \"medium\"\n        else:\n            confidence = \"low\"\n            warnings.append(f\"Low OCR confidence: {avg_conf:.1f}%\")\n\n        # Count text regions (blocks)\n        block_nums = set(data['block_num'])\n        text_regions = len([b for b in block_nums if b > 0])\n\n        result = {\n            \"success\": True,\n            \"filename\": filename,\n            \"extracted_text\": text.strip(),\n            \"confidence\": confidence,\n            \"metadata\": {\n                \"language_detected\": \"en\",\n                \"text_regions\": text_regions,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": warnings\n        }\n\n    except Exception as e:\n        result = {\n            \"success\": False,\n            \"filename\": filename,\n            \"extracted_text\": \"\",\n            \"confidence\": \"low\",\n            \"metadata\": {\n                \"language_detected\": \"unknown\",\n                \"text_regions\": 0,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": [f\"OCR failed: {str(e)}\"]\n        }\n\n    return result\n\n# Usage\nresult = ocr_to_json(\"document.jpg\")\nprint(json.dumps(result, indent=2))\n```\n\n### Batch Processing Multiple Images\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\nfrom pathlib import Path\n\ndef process_image_directory(directory_path, output_file):\n    \"\"\"Process all images in a directory and save results.\"\"\"\n    image_extensions = {'.jpg', '.jpeg', '.png', '.webp'}\n    results = []\n\n    for file_path in sorted(Path(directory_path).iterdir()):\n        if file_path.suffix.lower() in image_extensions:\n            result = ocr_to_json(str(file_path))\n            results.append(result)\n            print(f\"Processed: {file_path.name}\")\n\n    # Save results\n    with open(output_file, 'w') as f:\n        json.dump(results, f, indent=2)\n\n    return results\n```\n\n\n## Tesseract Configuration Options\n\n### Language Selection\n\n```python\n# Specify language (default is English)\ntext = pytesseract.image_to_string(img, lang='eng')\n\n# Multiple languages\ntext = pytesseract.image_to_string(img, lang='eng+fra+deu')\n```\n\n### Page Segmentation Modes (PSM)\n\nUse `--psm` to control how Tesseract segments the image:\n\n```python\n# PSM 3: Fully automatic page segmentation (default)\ntext = pytesseract.image_to_string(img, config='--psm 3')\n\n# PSM 4: Assume single column of text\ntext = pytesseract.image_to_string(img, config='--psm 4')\n\n# PSM 6: Assume uniform block of text\ntext = pytesseract.image_to_string(img, config='--psm 6')\n\n# PSM 11: Sparse text - find as much text as possible\ntext = pytesseract.image_to_string(img, config='--psm 11')\n```\n\nCommon PSM values:\n- `0`: Orientation and script detection (OSD) only\n- `3`: Fully automatic page segmentation (default)\n- `4`: Single column of text of variable sizes\n- `6`: Uniform block of text\n- `7`: Single text line\n- `11`: Sparse text\n- `13`: Raw line\n\n\n## Image Preprocessing\n\nFor better OCR accuracy, preprocess images:\n\n```python\nfrom PIL import Image, ImageFilter, ImageOps\n\ndef preprocess_image(image_path):\n    \"\"\"Preprocess image for better OCR results.\"\"\"\n    img = Image.open(image_path)\n\n    # Convert to grayscale\n    img = img.convert('L')\n\n    # Increase contrast\n    img = ImageOps.autocontrast(img)\n\n    # Apply slight sharpening\n    img = img.filter(ImageFilter.SHARPEN)\n\n    return img\n\n# Use preprocessed image for OCR\nimg = preprocess_image(\"document.jpg\")\ntext = pytesseract.image_to_string(img)\n```\n\n### Advanced Preprocessing Strategies\n\nFor difficult images (low contrast, faded text, dark backgrounds), try multiple preprocessing approaches:\n\n1. **Grayscale + Autocontrast** - Basic enhancement for most images\n2. **Inverted** - Use `ImageOps.invert()` for dark backgrounds with light text\n3. **Scaling** - Upscale small images (e.g., 2x or 3x) before OCR to improve character recognition. For faint or thin fonts on receipts, 3x with autocontrast frequently recovers decimal points and punctuation that 1x\u002F2x lose.\n4. **Thresholding** - Convert to binary using `img.point(lambda p: 255 if p > threshold else 0)` with different threshold values (e.g., 100, 128)\n5. **Sharpening** - Apply `ImageFilter.SHARPEN` to improve edge clarity\n\n\n## Multi-Pass OCR Strategy\n\nFor challenging images, a single OCR pass may miss text. Use multiple passes with different configurations:\n\n1. **Try multiple PSM modes** - Different page segmentation modes work better for different layouts (e.g., `--psm 6` for blocks, `--psm 4` for columns, `--psm 11` for sparse text)\n\n2. **Try multiple preprocessing variants** - Run OCR on several preprocessed versions of the same image\n\n3. **Combine results** - Aggregate text from all passes to maximize extraction coverage\n\n```python\ndef multi_pass_ocr(image_path):\n    \"\"\"Run OCR with multiple strategies and combine results.\"\"\"\n    img = Image.open(image_path)\n    gray = ImageOps.grayscale(img)\n    w, h = img.size\n\n    # Generate preprocessing variants (include an upscaled variant; 3x often\n    # recovers thin punctuation such as the decimal point in prices)\n    variants = [\n        ImageOps.autocontrast(gray),\n        ImageOps.invert(ImageOps.autocontrast(gray)),\n        gray.filter(ImageFilter.SHARPEN),\n        ImageOps.autocontrast(img.resize((w * 3, h * 3), Image.LANCZOS).convert('L')),\n    ]\n\n    # PSM modes to try\n    psm_modes = ['--psm 6', '--psm 4', '--psm 11']\n\n    all_text = []\n    for variant in variants:\n        for psm in psm_modes:\n            try:\n                text = pytesseract.image_to_string(variant, config=psm)\n                if text.strip():\n                    all_text.append(text)\n            except Exception:\n                pass\n\n    # Combine all extracted text\n    return \"\\n\".join(all_text)\n```\n\nThis approach improves extraction for receipts, faded documents, and images with varying quality.\n\n### Day\u002FMonth `8` Image Review Gate\n\nFor date extraction, apply this gate before the value-level voting below. An\nunconfirmed date requires an actual image view when the day or month in any raw\nor normalized candidate contains `8`. Check the parsed day\u002Fmonth components,\nnot the complete date string: an `8` only in the year, or a standalone `3`, does\nnot automatically trigger viewing. High confidence, unanimous OCR, and a legal\ncalendar date do not clear a triggered gate.\n\n```python\ndef date_requires_image_review(*day_month_candidates):\n    \"\"\"Pass parsed (day, month) pairs, never complete dates or year tokens.\"\"\"\n    return any(\n        \"8\" in str(day) or \"8\" in str(month)\n        for day, month in day_month_candidates\n    )\n```\n\nFor each triggered receipt, actually open its date image or a saved crop that\nretains the label and complete day\u002Fmonth\u002Fyear context, using an image-view tool\nsuch as `file_editor(command=\"view\", path=\"\u003Cdate image path>\")`. Saving a crop,\nrunning OCR again, or viewing a different receipt is not this inspection. Read\nthe printed date and use that visual evidence to select the accepted value;\nOCR majority and variant order cannot override the inspected reading. Keep the\naccepted date, viewed image path, and inspection evidence in a separate working\nrecord. Reuse the inspection for an unchanged confirmed date instead of viewing\nit again solely because it still contains `8`.\n\nConflicting readings, unclear glyphs, or unresolved day\u002Fmonth\u002Fyear order still\nrequire focused review even without a day\u002Fmonth `8`, including ambiguity in the\nyear and other digit confusions. Keep unresolved dates pending rather than\nguessing. This gate changes date acceptance only; the remaining extraction and\namount guidance below is unchanged.\n\n### Value-Level Voting Across Variants\n\nWhen the goal is a structured value (a date, an amount, an ID) rather than raw text,\ndo not simply concatenate variant outputs. Instead:\n\n1. Run each OCR variant and extract candidate values independently.\n2. Normalize each candidate (see \"Numeric Normalization\" below) before comparing.\n3. Group candidates by normalized value and pick the value that appears in the most variants (majority vote). Break ties by variant order or by keyword-tier priority.\n\nThis prevents a single garbled variant from wiping out a value that other variants clearly agree on.\n\n### Numeric Normalization for Receipts and Forms\n\nOCR frequently corrupts decimal separators and digit spacing in monetary amounts.\nBefore rejecting a candidate as unparseable, apply these repairs to any token that\nsits next to a target keyword line (e.g., `GRAND TOTAL`, `TOTAL RM`):\n\n- Replace `:` between digits with `.` (e.g., `92:80` -> `92.80`).\n- Collapse a single space between a 1-4 digit group and a 2-digit group into `.` (e.g., `92 80` -> `92.80`, `1 234 56` -> keep the comma-thousands intact but treat trailing ` 56` as decimals only when no other decimal separator is present).\n- Reinterpret bare digit clusters of length >=3 whose last two digits look like cents as `X.YY` when no decimal separator was recognized (e.g., `9280` -> `92.80`) **only** when the line contains a strong keyword such as `GRAND TOTAL` and no better candidate exists.\n- Strip stray non-digit characters (`—`, `~`, `\"`, extra dots) adjacent to the number before parsing.\n- Accept commas as thousands separators: `1,234.56` -> `1234.56`.\n\nAlways prefer a well-formed `d+\\.dd` match when one exists on the same line; only\nfall back to repaired forms when no strict match is present. When a keyword line is\ndetected but no candidate can be salvaged from that line, look at the immediately\nfollowing non-blank line for the last number token as the hint suggests.\n\n```python\nimport re\n\nMONEY_STRICT = re.compile(r'(?\u003C![\\d.])(\\d{1,3}(?:,\\d{3})+|\\d+)\\.(\\d{2})(?![\\d.])')\n\ndef parse_money(token: str):\n    \"\"\"Best-effort monetary parse with OCR repair.\"\"\"\n    if token is None:\n        return None\n    s = token.strip().replace('\"', '').replace('~', '').replace('—', '').replace('-', '')\n    m = MONEY_STRICT.search(s)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Repair colon-as-decimal, e.g. '92:80'\n    s2 = re.sub(r'(\\d)[:](\\d{2})\\b', r'\\1.\\2', s)\n    m = MONEY_STRICT.search(s2)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Repair space-as-decimal, e.g. '92 80'\n    s3 = re.sub(r'(\\d)\\s+(\\d{2})\\b', r'\\1.\\2', s)\n    m = MONEY_STRICT.search(s3)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Last resort: bare digit cluster -> X.YY (>= 3 digits)\n    m = re.search(r'\\b(\\d{3,})\\b', s)\n    if m and len(m.group(1)) >= 3:\n        n = m.group(1)\n        return float(n[:-2] + '.' + n[-2:])\n    return None\n```\n\n\n## Error Handling\n\n### Common Issues and Solutions\n\n**Issue**: Tesseract not found\n\n```python\n# Verify Tesseract is installed\ntry:\n    pytesseract.get_tesseract_version()\nexcept pytesseract.TesseractNotFoundError:\n    print(\"Tesseract is not installed or not in PATH\")\n```\n\n**Issue**: Poor OCR quality\n\n- Preprocess image (grayscale, contrast, sharpen)\n- Use appropriate PSM mode for the document type\n- Ensure image resolution is sufficient (300+ DPI)\n\n**Issue**: Empty or garbage output\n\n- Check if image contains actual text\n- Try different PSM modes\n- Verify image is not corrupted\n\n**Issue**: Keyword detected but no numeric value parsed\n\n- Do not immediately return null. First:\n  1. Apply the numeric normalization repairs above to the keyword line.\n  2. Look at the following non-blank line and take its last number.\n  3. Cross-check the value against other OCR variants (majority vote).\n- Only return null after all variants and both the keyword line and the next line\n  fail to yield a plausible number.\n\n\n## Quality Self-Check\n\nBefore returning results, verify:\n\n- [ ] Output is valid JSON (use `json.loads()` to validate)\n- [ ] All required fields are present (`success`, `filename`, `extracted_text`, `confidence`, `metadata`)\n- [ ] Text preserves logical reading order\n- [ ] Confidence level reflects actual OCR quality\n- [ ] Warnings array includes all detected issues\n- [ ] Special characters are properly escaped in JSON\n\n### Structured-Value Extraction Self-Check\n\nWhen the caller extracts specific fields (amounts, dates, IDs), also verify:\n\n- [ ] For every image where a target keyword (e.g., `GRAND TOTAL`, `TOTAL RM`) is detected in **any** OCR variant, a candidate value was produced, or the reason for failure is explicitly logged.\n- [ ] Numeric candidates were passed through the normalization repairs (colon->dot, space->dot, bare-cluster->X.YY) before being rejected.\n- [ ] When multiple OCR variants disagree, the value returned is the majority across variants, not just the first pass.\n- [ ] The fallback rule (\"if the keyword line has no number, use the last number on the next non-blank line\") is applied before returning null.\n- [ ] Exclusion patterns (`SUBTOTAL`, `TAX`, `GST`, `SST`, `DISCOUNT`, `CHANGE`, `CASH TENDERED`) do not accidentally drop lines that also contain the primary keyword `GRAND TOTAL`; the primary keyword takes precedence.\n\n\n## Limitations\n\n- Tesseract works best with printed text; handwriting recognition is limited\n- Accuracy decreases with decorative fonts, artistic text, or extreme stylization\n- Mathematical equations and special notation may not extract accurately\n- Redacted or watermarked text cannot be recovered\n- Severe image degradation (blur, noise, low resolution) reduces accuracy\n- Complex multi-column layouts may require custom PSM configuration\n- Thin punctuation (decimal points, colons) is often lost or substituted; always attempt numeric normalization before rejecting a value\n\n\n## Version History\n\n- **1.0.0** (2026-01-13): Initial release with Tesseract\u002Fpytesseract OCR\n- **1.1.0**: Added value-level voting across variants, numeric normalization guidance for OCR-corrupted amounts, and structured-value extraction self-check.\n","# Image OCR Skill\n\n## Purpose\nThis skill enables accurate text extraction from image files (JPG, PNG, etc.) using Tesseract OCR via the `pytesseract` Python library. It is suitable for scanned documents, screenshots, photos of text, receipts, forms, and other visual content containing text.\n\n## When to Use\n- Extracting text from scanned documents or photos\n- Reading text from screenshots or image captures\n- Processing batch image files that contain textual information\n- Converting visual documents to machine-readable text\n- Extracting structured data from forms, receipts, or tables in images\n\n## Required Libraries\n\nThe following Python libraries are required:\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n```\n\n## Input Requirements\n- **File formats**: JPG, JPEG, PNG, WEBP\n- **Image quality**: Minimum 300 DPI recommended for printed text; clear and legible text\n- **File size**: Under 5MB per image (resize if necessary)\n- **Text language**: Specify if non-English to improve accuracy\n\n## Output Schema\nAll extracted content must be returned as valid JSON conforming to this schema:\n\n```json\n{\n  \"success\": true,\n  \"filename\": \"example.jpg\",\n  \"extracted_text\": \"Full raw text extracted from the image...\",\n  \"confidence\": \"high|medium|low\",\n  \"metadata\": {\n    \"language_detected\": \"en\",\n    \"text_regions\": 3,\n    \"has_tables\": false,\n    \"has_handwriting\": false\n  },\n  \"warnings\": [\n    \"Text partially obscured in bottom-right corner\",\n    \"Low contrast detected in header section\"\n  ]\n}\n```\n\n\n### Field Descriptions\n\n- `success`: Boolean indicating whether text extraction completed\n- `filename`: Original image filename\n- `extracted_text`: Complete text content in reading order (top-to-bottom, left-to-right)\n- `confidence`: Overall OCR confidence level based on image quality and text clarity\n- `metadata.language_detected`: ISO 639-1 language code\n- `metadata.text_regions`: Number of distinct text blocks identified\n- `metadata.has_tables`: Whether tabular data structures were detected\n- `metadata.has_handwriting`: Whether handwritten text was detected\n- `warnings`: Array of quality issues or potential errors\n\n\n## Code Examples\n\n### Basic OCR Extraction\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_text_from_image(image_path):\n    \"\"\"Extract text from a single image using Tesseract OCR.\"\"\"\n    img = Image.open(image_path)\n    text = pytesseract.image_to_string(img)\n    return text.strip()\n```\n\n### OCR with Confidence Data\n\n```python\nimport pytesseract\nfrom PIL import Image\n\ndef extract_with_confidence(image_path):\n    \"\"\"Extract text with per-word confidence scores.\"\"\"\n    img = Image.open(image_path)\n\n    # Get detailed OCR data including confidence\n    data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n    words = []\n    confidences = []\n\n    for i, word in enumerate(data['text']):\n        if word.strip():  # Skip empty strings\n            words.append(word)\n            confidences.append(data['conf'][i])\n\n    # Calculate average confidence\n    avg_confidence = sum(c for c in confidences if c > 0) \u002F len([c for c in confidences if c > 0]) if confidences else 0\n\n    return {\n        'text': ' '.join(words),\n        'average_confidence': avg_confidence,\n        'word_count': len(words)\n    }\n```\n\n### Full OCR with JSON Output\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\n\ndef ocr_to_json(image_path):\n    \"\"\"Perform OCR and return results as JSON.\"\"\"\n    filename = os.path.basename(image_path)\n    warnings = []\n\n    try:\n        img = Image.open(image_path)\n\n        # Get detailed OCR data\n        data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)\n\n        # Extract text preserving structure\n        text = pytesseract.image_to_string(img)\n\n        # Calculate confidence\n        confidences = [c for c in data['conf'] if c > 0]\n        avg_conf = sum(confidences) \u002F len(confidences) if confidences else 0\n\n        # Determine confidence level\n        if avg_conf >= 80:\n            confidence = \"high\"\n        elif avg_conf >= 50:\n            confidence = \"medium\"\n        else:\n            confidence = \"low\"\n            warnings.append(f\"Low OCR confidence: {avg_conf:.1f}%\")\n\n        # Count text regions (blocks)\n        block_nums = set(data['block_num'])\n        text_regions = len([b for b in block_nums if b > 0])\n\n        result = {\n            \"success\": True,\n            \"filename\": filename,\n            \"extracted_text\": text.strip(),\n            \"confidence\": confidence,\n            \"metadata\": {\n                \"language_detected\": \"en\",\n                \"text_regions\": text_regions,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": warnings\n        }\n\n    except Exception as e:\n        result = {\n            \"success\": False,\n            \"filename\": filename,\n            \"extracted_text\": \"\",\n            \"confidence\": \"low\",\n            \"metadata\": {\n                \"language_detected\": \"unknown\",\n                \"text_regions\": 0,\n                \"has_tables\": False,\n                \"has_handwriting\": False\n            },\n            \"warnings\": [f\"OCR failed: {str(e)}\"]\n        }\n\n    return result\n\n# Usage\nresult = ocr_to_json(\"document.jpg\")\nprint(json.dumps(result, indent=2))\n```\n\n### Batch Processing Multiple Images\n\n```python\nimport pytesseract\nfrom PIL import Image\nimport json\nimport os\nfrom pathlib import Path\n\ndef process_image_directory(directory_path, output_file):\n    \"\"\"Process all images in a directory and save results.\"\"\"\n    image_extensions = {'.jpg', '.jpeg', '.png', '.webp'}\n    results = []\n\n    for file_path in sorted(Path(directory_path).iterdir()):\n        if file_path.suffix.lower() in image_extensions:\n            result = ocr_to_json(str(file_path))\n            results.append(result)\n            print(f\"Processed: {file_path.name}\")\n\n    # Save results\n    with open(output_file, 'w') as f:\n        json.dump(results, f, indent=2)\n\n    return results\n```\n\n\n## Tesseract Configuration Options\n\n### Language Selection\n\n```python\n# Specify language (default is English)\ntext = pytesseract.image_to_string(img, lang='eng')\n\n# Multiple languages\ntext = pytesseract.image_to_string(img, lang='eng+fra+deu')\n```\n\n### Page Segmentation Modes (PSM)\n\nUse `--psm` to control how Tesseract segments the image:\n\n```python\n# PSM 3: Fully automatic page segmentation (default)\ntext = pytesseract.image_to_string(img, config='--psm 3')\n\n# PSM 4: Assume single column of text\ntext = pytesseract.image_to_string(img, config='--psm 4')\n\n# PSM 6: Assume uniform block of text\ntext = pytesseract.image_to_string(img, config='--psm 6')\n\n# PSM 11: Sparse text - find as much text as possible\ntext = pytesseract.image_to_string(img, config='--psm 11')\n```\n\nCommon PSM values:\n- `0`: Orientation and script detection (OSD) only\n- `3`: Fully automatic page segmentation (default)\n- `4`: Single column of text of variable sizes\n- `6`: Uniform block of text\n- `7`: Single text line\n- `11`: Sparse text\n- `13`: Raw line\n\n\n## Image Preprocessing\n\nFor better OCR accuracy, preprocess images:\n\n```python\nfrom PIL import Image, ImageFilter, ImageOps\n\ndef preprocess_image(image_path):\n    \"\"\"Preprocess image for better OCR results.\"\"\"\n    img = Image.open(image_path)\n\n    # Convert to grayscale\n    img = img.convert('L')\n\n    # Increase contrast\n    img = ImageOps.autocontrast(img)\n\n    # Apply slight sharpening\n    img = img.filter(ImageFilter.SHARPEN)\n\n    return img\n\n# Use preprocessed image for OCR\nimg = preprocess_image(\"document.jpg\")\ntext = pytesseract.image_to_string(img)\n```\n\n### Advanced Preprocessing Strategies\n\nFor difficult images (low contrast, faded text, dark backgrounds), try multiple preprocessing approaches:\n\n1. **Grayscale + Autocontrast** - Basic enhancement for most images\n2. **Inverted** - Use `ImageOps.invert()` for dark backgrounds with light text\n3. **Scaling** - Upscale small images (e.g., 2x or 3x) before OCR to improve character recognition. For faint or thin fonts on receipts, 3x with autocontrast frequently recovers decimal points and punctuation that 1x\u002F2x lose.\n4. **Thresholding** - Convert to binary using `img.point(lambda p: 255 if p > threshold else 0)` with different threshold values (e.g., 100, 128)\n5. **Sharpening** - Apply `ImageFilter.SHARPEN` to improve edge clarity\n\n\n## Multi-Pass OCR Strategy\n\nFor challenging images, a single OCR pass may miss text. Use multiple passes with different configurations:\n\n1. **Try multiple PSM modes** - Different page segmentation modes work better for different layouts (e.g., `--psm 6` for blocks, `--psm 4` for columns, `--psm 11` for sparse text)\n\n2. **Try multiple preprocessing variants** - Run OCR on several preprocessed versions of the same image\n\n3. **Combine results** - Aggregate text from all passes to maximize extraction coverage\n\n```python\ndef multi_pass_ocr(image_path):\n    \"\"\"Run OCR with multiple strategies and combine results.\"\"\"\n    img = Image.open(image_path)\n    gray = ImageOps.grayscale(img)\n    w, h = img.size\n\n    # Generate preprocessing variants (include an upscaled variant; 3x often\n    # recovers thin punctuation such as the decimal point in prices)\n    variants = [\n        ImageOps.autocontrast(gray),\n        ImageOps.invert(ImageOps.autocontrast(gray)),\n        gray.filter(ImageFilter.SHARPEN),\n        ImageOps.autocontrast(img.resize((w * 3, h * 3), Image.LANCZOS).convert('L')),\n    ]\n\n    # PSM modes to try\n    psm_modes = ['--psm 6', '--psm 4', '--psm 11']\n\n    all_text = []\n    for variant in variants:\n        for psm in psm_modes:\n            try:\n                text = pytesseract.image_to_string(variant, config=psm)\n                if text.strip():\n                    all_text.append(text)\n            except Exception:\n                pass\n\n    # Combine all extracted text\n    return \"\\n\".join(all_text)\n```\n\nThis approach improves extraction for receipts, faded documents, and images with varying quality.\n\n### Day\u002FMonth `8` Image Review Gate\n\nFor date extraction, apply this gate before the value-level voting below. An\nunconfirmed date requires an actual image view when the day or month in any raw\nor normalized candidate contains `8`. Check the parsed day\u002Fmonth components,\nnot the complete date string: an `8` only in the year, or a standalone `3`, does\nnot automatically trigger viewing. High confidence, unanimous OCR, and a legal\ncalendar date do not clear a triggered gate.\n\n```python\ndef date_requires_image_review(*day_month_candidates):\n    \"\"\"Pass parsed (day, month) pairs, never complete dates or year tokens.\"\"\"\n    return any(\n        \"8\" in str(day) or \"8\" in str(month)\n        for day, month in day_month_candidates\n    )\n```\n\nFor each triggered receipt, actually open its date image or a saved crop that\nretains the label and complete day\u002Fmonth\u002Fyear context, using an image-view tool\nsuch as `file_editor(command=\"view\", path=\"\u003Cdate image path>\")`. Saving a crop,\nrunning OCR again, or viewing a different receipt is not this inspection. Read\nthe printed date and use that visual evidence to select the accepted value;\nOCR majority and variant order cannot override the inspected reading. Keep the\naccepted date, viewed image path, and inspection evidence in a separate working\nrecord. Reuse the inspection for an unchanged confirmed date instead of viewing\nit again solely because it still contains `8`.\n\nConflicting readings, unclear glyphs, or unresolved day\u002Fmonth\u002Fyear order still\nrequire focused review even without a day\u002Fmonth `8`, including ambiguity in the\nyear and other digit confusions. Keep unresolved dates pending rather than\nguessing. This gate changes date acceptance only; the remaining extraction and\namount guidance below is unchanged.\n\n### Value-Level Voting Across Variants\n\nWhen the goal is a structured value (a date, an amount, an ID) rather than raw text,\ndo not simply concatenate variant outputs. Instead:\n\n1. Run each OCR variant and extract candidate values independently.\n2. Normalize each candidate (see \"Numeric Normalization\" below) before comparing.\n3. Group candidates by normalized value and pick the value that appears in the most variants (majority vote). Break ties by variant order or by keyword-tier priority.\n\nThis prevents a single garbled variant from wiping out a value that other variants clearly agree on.\n\n### Numeric Normalization for Receipts and Forms\n\nOCR frequently corrupts decimal separators and digit spacing in monetary amounts.\nBefore rejecting a candidate as unparseable, apply these repairs to any token that\nsits next to a target keyword line (e.g., `GRAND TOTAL`, `TOTAL RM`):\n\n- Replace `:` between digits with `.` (e.g., `92:80` -> `92.80`).\n- Collapse a single space between a 1-4 digit group and a 2-digit group into `.` (e.g., `92 80` -> `92.80`, `1 234 56` -> keep the comma-thousands intact but treat trailing ` 56` as decimals only when no other decimal separator is present).\n- Reinterpret bare digit clusters of length >=3 whose last two digits look like cents as `X.YY` when no decimal separator was recognized (e.g., `9280` -> `92.80`) **only** when the line contains a strong keyword such as `GRAND TOTAL` and no better candidate exists.\n- Strip stray non-digit characters (`—`, `~`, `\"`, extra dots) adjacent to the number before parsing.\n- Accept commas as thousands separators: `1,234.56` -> `1234.56`.\n\nAlways prefer a well-formed `d+\\.dd` match when one exists on the same line; only\nfall back to repaired forms when no strict match is present. When a keyword line is\ndetected but no candidate can be salvaged from that line, look at the immediately\nfollowing non-blank line for the last number token as the hint suggests.\n\n```python\nimport re\n\nMONEY_STRICT = re.compile(r'(?\u003C![\\d.])(\\d{1,3}(?:,\\d{3})+|\\d+)\\.(\\d{2})(?![\\d.])')\n\ndef parse_money(token: str):\n    \"\"\"Best-effort monetary parse with OCR repair.\"\"\"\n    if token is None:\n        return None\n    s = token.strip().replace('\"', '').replace('~', '').replace('—', '').replace('-', '')\n    m = MONEY_STRICT.search(s)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Repair colon-as-decimal, e.g. '92:80'\n    s2 = re.sub(r'(\\d)[:](\\d{2})\\b', r'\\1.\\2', s)\n    m = MONEY_STRICT.search(s2)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Repair space-as-decimal, e.g. '92 80'\n    s3 = re.sub(r'(\\d)\\s+(\\d{2})\\b', r'\\1.\\2', s)\n    m = MONEY_STRICT.search(s3)\n    if m:\n        return float(m.group(1).replace(',', '') + '.' + m.group(2))\n    # Last resort: bare digit cluster -> X.YY (>= 3 digits)\n    m = re.search(r'\\b(\\d{3,})\\b', s)\n    if m and len(m.group(1)) >= 3:\n        n = m.group(1)\n        return float(n[:-2] + '.' + n[-2:])\n    return None\n```\n\n\n## Error Handling\n\n### Common Issues and Solutions\n\n**Issue**: Tesseract not found\n\n```python\n# Verify Tesseract is installed\ntry:\n    pytesseract.get_tesseract_version()\nexcept pytesseract.TesseractNotFoundError:\n    print(\"Tesseract is not installed or not in PATH\")\n```\n\n**Issue**: Poor OCR quality\n\n- Preprocess image (grayscale, contrast, sharpen)\n- Use appropriate PSM mode for the document type\n- Ensure image resolution is sufficient (300+ DPI)\n\n**Issue**: Empty or garbage output\n\n- Check if image contains actual text\n- Try different PSM modes\n- Verify image is not corrupted\n\n**Issue**: Keyword detected but no numeric value parsed\n\n- Do not immediately return null. First:\n  1. Apply the numeric normalization repairs above to the keyword line.\n  2. Look at the following non-blank line and take its last number.\n  3. Cross-check the value against other OCR variants (majority vote).\n- Only return null after all variants and both the keyword line and the next line\n  fail to yield a plausible number.\n\n\n## Quality Self-Check\n\nBefore returning results, verify:\n\n- [ ] Output is valid JSON (use `json.loads()` to validate)\n- [ ] All required fields are present (`success`, `filename`, `extracted_text`, `confidence`, `metadata`)\n- [ ] Text preserves logical reading order\n- [ ] Confidence level reflects actual OCR quality\n- [ ] Warnings array includes all detected issues\n- [ ] Special characters are properly escaped in JSON\n\n### Structured-Value Extraction Self-Check\n\nWhen the caller extracts specific fields (amounts, dates, IDs), also verify:\n\n- [ ] For every image where a target keyword (e.g., `GRAND TOTAL`, `TOTAL RM`) is detected in **any** OCR variant, a candidate value was produced, or the reason for failure is explicitly logged.\n- [ ] Numeric candidates were passed through the normalization repairs (colon->dot, space->dot, bare-cluster->X.YY) before being rejected.\n- [ ] When multiple OCR variants disagree, the value returned is the majority across variants, not just the first pass.\n- [ ] The fallback rule (\"if the keyword line has no number, use the last number on the next non-blank line\") is applied before returning null.\n- [ ] Exclusion patterns (`SUBTOTAL`, `TAX`, `GST`, `SST`, `DISCOUNT`, `CHANGE`, `CASH TENDERED`) do not accidentally drop lines that also contain the primary keyword `GRAND TOTAL`; the primary keyword takes precedence.\n\n\n## Limitations\n\n- Tesseract works best with printed text; handwriting recognition is limited\n- Accuracy decreases with decorative fonts, artistic text, or extreme stylization\n- Mathematical equations and special notation may not extract accurately\n- Redacted or watermarked text cannot be recovered\n- Severe image degradation (blur, noise, low resolution) reduces accuracy\n- Complex multi-column layouts may require custom PSM configuration\n- Thin punctuation (decimal points, colons) is often lost or substituted; always attempt numeric normalization before rejecting a value\n\n\n## Version History\n\n- **1.0.0** (2026-01-13): Initial release with Tesseract\u002Fpytesseract OCR\n- **1.1.0**: Added value-level voting across variants, numeric normalization guidance for OCR-corrupted amounts, and structured-value extraction self-check.\n",{"name":5,"description":7},"main","cdcb6d230f3b15c3c33f8a2bdd16439f43602cbe","evaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-gate-round27-opus80-20261012\u002Finputs\u002Fround-27\u002Fskills\u002Fimage-ocr",1,"https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark","https:\u002F\u002Fgithub.com\u002Flinyouxue","2026-11-14 20:28:35",{"source":8,"skillName":5,"commands":32,"prompt":42,"zipAvailable":43},[33,36,39],{"tool":34,"command":35},"npx","npx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr",{"tool":37,"command":38},"bunx","bunx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr",{"tool":40,"command":41},"pnpm","pnpm dlx skills add linyouxue\u002Fskill-repair-benchmark --skill image-ocr","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fevaluation\u002Fresults\u002F对于jpg-ocr-stat的尝试\u002Fskills\u002Fbundles\u002Ftest-skillaxe-date-gate-round27-opus80-20261012\u002Finputs\u002Fround-27\u002Fskills\u002Fimage-ocr\u002FSKILL.md",true,[],[46,53,60,66,71,76],{"skillNo":47,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":50,"categories":51,"keywords":52,"publishedAt":19},"01M7E1HGVRY73NBJ022CJG13X6","video-frame-extraction","Extract frames from video files and save them as images using OpenCV","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fvideo-frame-extraction\u002FSKILL.md",[],[],{"skillNo":54,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":57,"categories":58,"keywords":59,"publishedAt":19},"01M7E1HAG5R7CJ9C8NYJS771MB","openai-vision","Analyze images and multi-frame sequences using OpenAI GPT vision models","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fopenai-vision\u002FSKILL.md",[],[],{"skillNo":61,"name":5,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":65,"publishedAt":19},"01M7E1H7DADKB6QBWKC2KQ63SZ","Extract text content from images using Tesseract OCR via Python, including structured field extraction from receipts and forms","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_03\u002Finputs\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[],{"skillNo":67,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":19},"01M7E1H0WBNT43TENGQY7AVZHD","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fvideo-frame-extraction\u002FSKILL.md",[],[],{"skillNo":72,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M7E1GTC7D4N4Q2EF03875J9E","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fopenai-vision\u002FSKILL.md",[],[],{"skillNo":77,"name":5,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M7E1GQ92CWFMGH53KSSE4WER","https:\u002F\u002Fgithub.com\u002Flinyouxue\u002Fskill-repair-benchmark\u002Fblob\u002Fmain\u002Fjpg-ocr-stat\u002Fresult\u002Fskilltta-claude4.7\u002Fgroup_03\u002Frun_02\u002Finputs\u002Fskills\u002Fimage-ocr\u002FSKILL.md",[],[]]