山形の日本酒「ソースコード」を読み解いてみた


エンジニアなら気になるお酒の中に「ソースコード」というのがあります。

https://kurand.jp/products/sourcecode

ストーリーは以下のとおりです。

> 日々奮闘するエンジニアたちへ
こだわりの詰まった純米吟醸酒を
プログラミング言語を操りながら、多くのプログラムを作り上げていくシステムエンジニア。プログラムを表現する「ソースコード」には、エンジニアたちの個性や魂が詰め込まれています。日々闘い続けるエンジニアたちに捧げるのは、酒蔵の個性が詰まった純米吟醸酒。ラベル内に書かれたソースコードには、ちょっとした仕掛けを施しました。自分へのご褒美や、システムエンジニアとして日々奮闘する方へのプレゼントにもおすすめです。

> 可能性は無限大
探究心をくすぐる日本酒
ある時は、キレ良くすっきりとした味わいを生かして、さまざまな食事と合わせて。ある時は、冷酒から常温、ぬる燗まで幅広い温度帯で楽しめるため、それぞれを飲み比べて。1本で無数の楽しみ方ができる、この日本酒の可能性は無限大。「自分にとっての最適解」を見つけたいと、飲む人の探究心をくすぐります。「難解なロジックを実装したい」。そんな欲望をもつエンジニアたちを刺激する1本です。

> 山形の酒蔵が地元の素材を使用
こだわりと個性を詰め込んで
山形県鶴岡市で約400年以上続く老舗酒蔵「渡會（わたらい）本店」。地元の月山・朝日山系の山々から流れる清浄水と、山形県のオリジナル酒米「出羽の里」で丁寧に仕込みました。溢れ出すお米の旨みと雑味の少ない上品な味わい。そしてお酒を仕込む際、自然にゆだねて酵母が糖を分解しつくすことで発酵を止める「完全発酵」による、すっきりとしたキレの良さ。無駄なく、完成された純米吟醸酒に仕上がりました。


そんな「ソースコード」ですが、ラベルになにか書いてあります。

https://kurand.jp/cdn/shop/files/soursecode.jpg

ん？sour$\huge{s}$ecode？まあいいや

## ラベルに書いてある文字を起こす
ラベルにどうやらソースコードが書いてあるのですが、このように印刷されたままだと実行することはできません。どうにかして文字に起こす必要があります。
買ったラベルを読み込むということもできますが、ラベルはボトルにぐるりと円柱状に貼り付けられていて読み込むのは難しいです。
ECサイトの画像をそのまま使うほうがまだ読み取りやすいです。

しかしOCRで行こうとしましたが失敗しましたので、
$\huge{結局手書きになりました}$


### 失敗例

愚直にOCRする

1.まずはpngに変換

```bash
convert soursecode.jpg image.png
```

2.ボトルが邪魔なのでyoloで消す。

```python
from ultralytics import YOLO
import cv2
import numpy as np

image_path = "./image.png"

original_image = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB)

model = YOLO("yolo11n-seg.pt")
results = model(original_image)

for result in results:
    masks = result.masks.data.cpu().numpy()
    for idx, mask in enumerate(masks):

        # マスクを元の画像サイズにリサイズ
        resized_mask = cv2.resize(mask, (original_image.shape[1], original_image.shape[0]))

        # マスク部分だけを抽出
        mask_binary = (resized_mask > 0.5).astype(np.uint8)  # バイナリマスク (0 または 1)

        # マスクされた部分だけ黒に
        mask_overlay = np.zeros_like(original_image)
        mask_overlay[mask_binary == 1] = [35, 23, 23]

        # 元の画像にマスク部分を反映
        colored_image = original_image.copy()
        colored_image[mask_binary == 1] = mask_overlay[mask_binary == 1]

```

3.OCRしやすいように加工

```
        # 白い文字を緑に
        threshold = 180
        mask_white = np.all(colored_image >= threshold, axis=-1)  # 白い部分のマスク
        colored_image[mask_white] = [60, 160, 60]  # 白い部分を黒に置き換え


        # 黒の部分を白色に
        lower_black = np.array([0, 0, 0])
        upper_black = np.array([50, 50, 50])
        black_mask = cv2.inRange(colored_image, lower_black, upper_black)
        colored_image[black_mask > 0] = [255, 255, 255]  


        # 緑色の文字を黒に
        hsv_image = cv2.cvtColor(colored_image, cv2.COLOR_RGB2HSV)
        lower_green = np.array([40, 50, 50])
        upper_green = np.array([80, 255, 255])
        green_mask = cv2.inRange(hsv_image, lower_green, upper_green)
        colored_image[green_mask > 0] = [0, 0, 0]

        # おまじない
        gray_image = cv2.cvtColor(colored_image, cv2.COLOR_BGR2GRAY)
        _, binary_image = cv2.threshold(gray_image, 128, 255, cv2.THRESH_BINARY)
        enhanced_image = cv2.equalizeHist(binary_image)
        denoised_image = cv2.medianBlur(enhanced_image, 3)

        cv2.imwrite(f'code.png', denoised_image)
```


ここまでやるとこうなります。

![code.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230043/2e23f4fa-bb3e-53ec-d9b0-5c5e6056fce2.png)

そしてこの状態でOCRすると

```bash
tesseract code.png out -c load_system_dawg=F -c load_freq_dawg=F -c tessedit_char_whitelist='[]()!+,'
```

LSTMが悪さしているのか全然期待通り動いてくれない…

```
console.log(String.fromCodePoint((!(0+8Q+iQ d+ O+uOugip Hide + 0, Ljoin("")), St:
O.+20.+!0.).join("")),String.fromCodePoint((+!!J,+),~~f,] join("")), String.fromCodePoint([
("")), String.fromCodePoint(([+0,+!0, {0+ O+0+(],].join("")), String. fromCodePoint([!!}+!]+!!
OFS) join("")), String. fromCodePoint(+!0,+! 0. O+!O+! Oe tot Ot+ip,].join(")),
(AND. FU AFF t ene g+ig)join("")), String.fromCodePoint([+![],~~![]
(7")), String .fromCodePoimt((UiJ+i(J+u(j+tueQyele(Je h(t eee gegeii+!
("")), String.fromCodePoint((+!![), +10, WO NO+ Qe NO+o+0..join("")),String.fromCodePoint(!
),).join("")), String. fromCodePoint([+!![],+!![], +!![],]. join("")),String.fromCodePoi
("")), String.fromCodePoint (+0, +O, O+t oti sot, |.join("")),String.fromCodePoint([!
("")) Jconsole.log(String.fromCodePoint((HU(J+NJ+H tie +O UO+u +e
(*")), String.fromCodePoint((+![J,+![J,+!![J,].join("")), String.fromCodePoi:
(°")), String.fromCodePoint((+"9,~"0, 0+" 0+, ].join("")), String.fromCodePoint((+!’
(°"")), String.fromCodePoint((UQ +N +N 0+ +l +u0+ i DHT So uaa a i 0), Lioin(””
O48 80+ 8 e+ o+i +0, ).join("")), String.fromCodePoint(([+!(],+!0.+!!0+

("")), String.fromCodePoint((+!!Q,--"O.MO+lQ+ug+i0,].join("")), String. fromCodePoint(
CPHL eet et il). join(’*)), String. fromCodePoint((+i[J +i]!

("")), String. fromCodePoint([+!![J,-~(), ++ O+io+![),].join("")), String.fromce

("")), String. fromCodePoint((+!![],+!0,~~(],].join("")), String. fromCodePoint([+!

(°")), String.fromCodePoint({G+!0+!,.0+"0+"0,).join("")),)console.log(String.fron

OP OF O+0+ 0+ O+0+7,].join("")), String fromCodePoint([+9,+0,+!0,)].join("")),St
0,).join("")),String.fromCodePoint((+!9,~~J.U0+0+H0,.)]Jjoin(*")),String.fromCodeP

("")), String. fromCodePoint((H(J+U [+i +lOti tue eur +g gee!

(°")), String. fromCodePoint((+!!0,+!/ 0, 00+ (HU YH UOtG, Ljoin("")), String.fromCod:
OFN0+!0,)join("")),String.fromCodePoint(+!9,-~! 0M 0+ 0+! 0410, ].join("")),String.!

OF OtOt Oe OOF Ott + ++i. ).join("")),String.fromCodePoint((+!!

(°°). String.fromCodePoint([+!!(J,-~(),Y0+ MUU O+to+g].join("")),String.from:

("")), String.fromCodePoint((+!!"],+"0,~~[],].join("")), String. fromCodePoint([+!

("")), String.fromCodePoint( 0+! "0."0+00+i0,].join("")),)

SOURCE CODE

odePoint((+!!
+10+N0,).join
NO+ DH g
smCodePoint
1N+NQ],).join
}+![],). join
AOA DHA
we[],].join
‘IO+![], join
+1[J,J.join
~[].].join
+H], J.join
Point([+!!
‘Q),].join
ule!
J.join

].join

‘join

Mo+!!

'D.~~

join

join

+t!

tge!!

join

join

join

```

この場合は手書きとはいえ難解言語のOCRなんてやってる人いない…

https://x.com/awellbottom/status/1873054868728062017

## というわけで手書きで読み取る


よく見てみると規則性があります。

* String.FromCodePointしか使われていない
* !![]と~~[]と+と,の組み合わせでできている
* 例えば最初の`String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]].join(""))`は「C」を出力する。このような感じで続いている。
* これらのことからボトルで失われた情報は復元できる

＝＞　実は一文字一文字起こしていく必要はない！

後はコードポイントを数えていくだけ！

## 文字に起こした結果

というわけで、手で文字起こしした結果です。
これを実行すると…？

```
console.log(String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![],!![]+!![]+!![],].join("")),)console.log(String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![],!![]+!![]+!![],].join("")),)console.log(String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![],!![]+!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],~~[],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([+!![],+!![],+!![],].join("")),String.fromCodePoint([+!![],+!![],~~[],].join("")),String.fromCodePoint([+!![],+!![],!![]+!![]+!![]+!![]+!![],].join("")),String.fromCodePoint([!![]+!![]+!![],!![]+!![]+!![],].join("")),)
```

## 感想

typo多くね？