0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

平仮名が得意なOCRと漢字が得意なOCRの結果をマージする

0
Last updated at Posted at 2026-06-24

環境は横着して llama.cpp の llama-cli をPythonから直接叩く形にしています。ちゃんと書く場合は llama-server か llama-cpp-python か PyTorch を使うと良いでしょう。

今回使うモデルはsurya-2(平仮名)とHunyuanOCR(漢字その他)です。
https://huggingface.co/datalab-to/surya-ocr-2-gguf/tree/main
https://huggingface.co/ggml-org/HunyuanOCR-GGUF/tree/main

もっと日本語に特化したOCRモデルもあります(NDLOCR や Sarashina2.2-OCR など)が、導入が簡単では無いため今回は llama.cpp で動くものを選びました。

スクリプトは認識力を上げるため ImageMagick (2ページを1ページに)と deskew_HT(NDLOCR用資料画像の傾き補正モジュール)にも依存してます。
https://github.com/ndl-lab/deskew_HT

my_ocr.py
import unicodedata
from difflib import SequenceMatcher

import sys
if len(sys.argv) < 2:
    print("python my_ocr.py <img path>")
    exit()

img_path = sys.argv[1]
llama_cli = "/home/xxx/data/llama.cpp/build/bin/llama-cli"

# https://github.com/datalab-to/surya/blob/11d18847e8b8c17fd55e254e3e5c44b3fed5c87e/surya/inference/prompts.py#L82
prompt1 = """OCR this image to HTML. Each block is a div with data-label and data-bbox (x0 y0 x1 y1, normalized 0-1000)."""
#prompt2 = "Extract all information from the main body of the document image and represent it in HTML format."
prompt2 = "Extract all."

model1_gguf = "/media/xxx/data/ocr/surya-2.gguf"
mmproj1_gguf = "/media/xxx/data/ocr/surya-2-mmproj.gguf"
model2_gguf = "/media/xxx/data/ocr/HunyuanOCR-bf16.gguf"
mmproj2_gguf = "/media/xxx/data/ocr/mmproj-HunyuanOCR-bf16.gguf"
import subprocess

# ImageMagickで左右にぶった切る
subprocess.run(["convert", img_path, "-crop","2x1@", "+repage", img_path + "_%d.png"])

for n in range(2):
    img_path_n = img_path + f"_{n}.png"
    subprocess.run(["python3", "../deskew_HT/run_deskew.py", img_path_n,"-o", img_path_n])
#    subprocess.run(["python3", "../deskew_HT/run_deskew.py", img_path_n,"-o", img_path_n]) # second deskew

    result1 = subprocess.run([llama_cli, "-p", prompt1, "--image", img_path_n, "-m", model1_gguf,
                                                    "--mmproj", mmproj1_gguf, "--temp", "0", "--single-turn", "--log-disable", "--no-display-prompt",
                                                    "--simple-io", "-c", "8192"],
                                                    capture_output=True, text=True)

    t = result1.stdout.find("Loaded media")
    t = result1.stdout.find(">", t)
    t = result1.stdout.find("\n", t)
    r1 = result1.stdout[t:]
    print(r1)
    result2 = subprocess.run([llama_cli, "-p", prompt2, "--image", img_path_n, "-m", model2_gguf,
                                                    "--mmproj", mmproj2_gguf, "--temp", "0", "--single-turn", "--log-disable", "--no-display-prompt",
                                                    "--simple-io", "-c", "8192"],
                                                    capture_output=True, text=True)

    t = result2.stdout.find("Loaded media")
    t = result2.stdout.find(">", t)
    t = result2.stdout.find("\n", t)
    r2 = result2.stdout[t:]
    r2 = r2.replace("\n", "")
    print(r2)

    def is_kana(c):
        name = unicodedata.name(c, "")
        return "HIRAGANA" in name or "KATAKANA" in name

    def is_kanji(c):
        name = unicodedata.name(c, "")
        return "CJK UNIFIED IDEOGRAPH" in name

    def merge_ocr(ocr_kanji, ocr_kana):
        sm = SequenceMatcher(None, ocr_kanji.replace("", ""), ocr_kana, autojunk=False)

        result = []

        for tag, i1, i2, j1, j2 in sm.get_opcodes():

            if tag == "equal":
                result.extend(ocr_kanji[i1:i2]) # 「ぶ」優先
                continue

            if tag == "replace":

                a = ocr_kanji[i1:i2]
                b = ocr_kana[j1:j2]

                # かなブロック単位で扱う
                # まず「かな側を優先的に採用」
                if any(is_kana(c) for c in b):
                    result.extend(b)
                else:
                    result.extend(a)

                continue

            if tag == "delete":
                result.extend(ocr_kanji[i1:i2])
                continue

            if tag == "insert":
                result.extend(ocr_kana[j1:j2])
                continue

        return "".join(result)

    print(merge_ocr(r2, r1))

テスト

テストに使った画像は国立国会図書館デジタルコレクションにある『新編御伽草子』(1901年)収録の「福富草子」の高解像度JPGです。
https://dl.ndl.go.jp/pid/992342/1/10

OCR結果のHTMLタグと改行は除去してあります。括弧内は掛けてる文字です。「、」の不足は多すぎるため一旦無視しています。

モデル 注釈のOCR結果 誤ブロック数
正解 冒頭の一句は、一篇の綱領なり、 いまだ其藝の何事たるを説破せず、聞の字、笑の字を下す、頗妙なり、 五のたなつものは五穀をいふ、たなつものは種つ物の義なり、日本紀には、水田の種子をしかよめり 一段、福富が福分富有を寫して、後段の地をなす、 築地は、土を高く築きたる垣墻をいふ、練塀の類なり、 0
surya-2 冒頭の一句は、一篇の綱領なり、いまだ其藝の何事たるを説破せず、(聞の字、笑の字を)下す、頗妙なり、五のたなつものは五穀いふ、たなつものは種つ物の義なり、日本紀には、水田の種子をしかよめり一段、福富が福分(富有を寫して、後)段の地をなす、築地は、土を高く築きたる垣墻をいふ、練塀の類なり、 5
HunyuanOCR 冒頭の一句一篇の綱領なりいまだ其藝の何事たる説破せず聞の字笑の字下す頗妙なり五のたなつものは五穀いふたなつもの種つ物のなり日本紀には水田の種于たよめり一段福富が福分富有を寫して後段の地(を)なす築地は土を高く築きたる垣徳たいふ練塀の類なり 9
マージ 冒頭の一句は、一篇の綱領なり、いまだ其藝の何事たるを説破せず聞の字笑の字下す、頗妙なり、五のたなつものは五穀いふ、たなつものは種つ物のなり、日本紀には、水田の種子をしかよめり一段、福富が福分富有を寫して後段の地をなす、築地は、土を高く築きたる垣墻をいふ、練塀の類なり、 4

下記の平仮名の誤字数は約物の誤字(〱とくの混同)を除きます。

モデル 本文のOCR結果 平仮名の誤字数 漢字の誤字数
正解 人は身に應せぬ果報をうらやむまじきことになん侍る、むかし福富の織部とて、長者一人侍りけり、いかなる過去の宿緣にや、身に生れつきたる藝ひとつさふらひけるが、ならはざるに奇特をあらはし、はからざるに名を發して、世の人、神の如くにぞ思ひける、其藝あさましくいぶせければ、上中下の人までも、よく聞き知りて笑をもよほすことなりければ、おのづからおほやけ方にもきこしめし、もて興じおはしましけることなゝめならず、されば富めるが上に富み樂しきが上にたのしみて、棟に棟をあらそひ、藏に藏をたてゝ、五のたなつもの、耕さずして、庭にみち〱たり、それか隣にほくせうの藤太とて、いとまづしきもの侍り、こは織部にひきかへて朝夕の烟も竈に絶え、とつのみち草しげりつゝ、築地にあらぬ柴垣や、慢幕ならぬ薦たれに夜寒の床をあかしかねつゝ、軒もかきをも、このためにこぼちとりて、あまり寒さの風を入れける、夏はあさましき麻の衣ふるびて、やぶれ團扇にて 0 0
surya-2 人は身に應せぬ果報をうらやむまじきことになん侍る、むかし福富の織部とて、長者一人侍りけり、いかなる過去の宿にや、身に生れつきたる藝ひとつさふらひけるが、ならはざるに奇特をあらはし、はからざるに名を發して、世の人、神の如くにぞ思ひける、其藝あさましくいせければ、上中下の人までも、よく聞き知りて笑をもよほすことなりければ、おのづからおほやけ方にもきこしめし、もて興じおはしましけることなめならず、されば富めるが上に富み、樂しきが上にたのしみて、棟に棟をあらそひ、藏に藏をたて(ゝ)、五のたなつもの、耕さずして、庭にみちたり、それか隣にほくせうの藤太とて、いとまづしきもの侍り、こは織部にひきかへて朝夕の烟も竈に絶え、とつのみち草しげりつ(ゝ)、築地にあらぬ柴垣や、慢ならぬたれに夜寒の床をあかしかねつ(ゝ)、軒もかきをも、このためにこぼちとりて、あまり寒さの風を入れける、夏はあさましき麻の衣ふるびて、やれ團扇にて 2 3
HunyuanOCR 人は身に應せ果報をうらやむまじきこになん侍るむかし福富の織部て長者一人侍りけりいかなる過去の宿緣にや身に生れつきたる藝ひどつさふらひけるがならはざるに奇特をあらはしはからざるに名を發して世の人神の如くにぞ思ひける其藝あさましくいぶせければ上中下の人までもよく聞き知りて笑をもよほすこなりければおのづからおほやけ方にもきこしめしもて興じおはしましけるこ(ゝ)めならずされば富めるが上に富みしきが上にたのしみて棟に棟をあらそひ藏に藏をたて(ゝ)五のたなつもの耕さして庭にみち(〱)たりそれか隣にほくせうの藤太どていどまづしきもの侍りこは織部にひきかへて朝夕の烟もに絶えつのみち草しげりつ(ゝ)築地にあらぬ柴垣や慢幕ならぬ薦たれに夜寒の床をあかしかねつ(ゝ)軒もかきをもこのためにこぼちりてあまり寒さの風を入れける夏はあさましき麻の衣ふるびてやぶれ團扇にて 7 2
マージ 人は身に應せぬ果報をうらやむまじきことになん侍る、むかし福富の織部とて、長者一人侍りけり、いかなる過去の宿緣にや、身に生れつきたる藝ひとつさふらひけるが、ならはざるに奇特をあらはし、はからざるに名を發して、世の人、神の如くにぞ思ひける、其藝あさましくいぶせければ、上中下の人までも、よく聞き知りて笑をもよほすことなりければ、おのづからおほやけ方にもきこしめし、もて興じおはしましけることなめならず、されば富めるが上に富みしきが上にたのしみて、棟に棟をあらそひ、藏に藏をたて(ゝ)、五のたなつもの、耕さずして、庭にみちたり、それか隣にほくせうの藤太とて、いとまづしきもの侍り、こは織部にひきかへて朝夕の烟もに絶え、とつのみち草しげりつ(ゝ)、築地にあらぬ柴垣や、慢幕ならぬ薦たれに夜寒の床をあかしかねつ(ゝ)、軒もかきをも、このためにこぼちとりて、あまり寒さの風を入れける、夏はあさましき麻の衣ふるびて、やぶれ團扇にて 0 2

だいぶ良くなりましたが、まだちょっと微妙ですね…。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?