Voice changerを作りたくて(0): 何から始めたらええねん
出田 守です。 このシリーズは、ほとんど何もわからない状況からVoice changerを作る過程の記録です。
環境(私も含めて)
[私について]
算数・数学: 苦手で避けてきた(後悔中)。おそらく中学2年生で止まっている。
プログラミング: 努力中
[開発環境 - PC0]
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.39.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell
voice changerを作りたくなりました
以前ある外国のhackerがある企業の社長になりすまして、情報を盗んだという事件の記事をどこかで見ました。その手口がvoice changerを使って社長の声になりすましたそうです。(どこにその記事があるのか分からなくなりました)
この時、voice changerを作ってみたくなったのです。もちろん犯罪目的ではなく、あくまでも純粋に面白そうだからです。人を笑わせたり、ちょっとしたイタズラに使えそうです(笑)
voice changerってなんや
私はvoice changerの専門家ではないので、まずはvoice changerについて調べます。
wikipediaでは以下のように説明されています。
ボイスチェンジャー (Voice changer) とは、入力された音声を加工して違う音声に聞こえるようにする機械[1]、またはその機能のついた電話機である。変声機や音声変換機とも呼ばれる。機材の他にもパソコンで使えるソフトウェア[2]のボイスチェンジャーも存在する。
入力された音声信号を何かしら加工して出力する機械ですね。
どのように作っていくか
voice changerの実現方法はソフトウェアとハードウェアの2種類あると思っています。
まずは、コストがかからないソフトウェアで作って、雰囲気をつかんでからハードウェアに挑戦します。
ゼロからソフトウェアで作っていくうえで、主な流れを以下に示します。
いっちょまえにフーリエ変換とか書いていますが、何にも分かっていません。環境の章でも書いた通り数学が苦手です。ですので、フーリエ変換を学ぶために -> 三角関数を学ぶために -> ...という再帰ループが待っていそうです。
まあ、とりあえずこのような感じでやっていきます。
レインボーテーブル入門
出田 守です。 情報セキュリティに興味を持ち、独学で勉強中です。 主にPythonを使用します。時々勉強のためにCを使用することもあります。
ゴール
レインボーテーブルについて理解したことを簡単に説明して、実際に実装してみます。
レインボーテーブルとは
あるハッシュ値から平文を得るために使われる技術です。
最も単純な方法
あるハッシュ値から平文を得るための最も単純な方法は、ハッシュ値と平文のペアから成るテーブルを作って、あるハッシュ値で検索をすれば実現できます。例えば、以下のテーブルから「password」のmd5ハッシュ値「5f4dcc3b5aa765d61d8327deb882cf99」を検索することで元の平文「password」が得られます。
ハッシュ値 | 平文 |
---|---|
e10adc3949ba59abbe56e057f20f883e | 123456 |
5f4dcc3b5aa765d61d8327deb882cf99 | password |
25f9e794323b453885f5181f1b624d0b | 123456789 |
... | ... |
ただ、このテーブルではかなり大きなサイズになってしまいます。
そこで考えられたのがレインボーテーブルです。
レインボーテーブル作成
まず、レインボーテーブルを作成するにあたり、二つの関数を用意します。
- ハッシュ関数: 平文からハッシュ値を得るための関数です。
- 還元関数: ハッシュ値から無造作に平文を選択する関数です。ただし、同じハッシュ値なら常に同じ平文を選択する必要があります。この条件を満たしていればどのような関数でも良いようです。
次に、上記二つの関数を使って、チェインを作ります。具体的には以下のように作ります。
- 平文からハッシュ関数を使って、ハッシュ値を得ます。
- 得られたハッシュ値を還元関数を使って、別の平文を得ます。
- さらにその別の平文をハッシュ関数を使って、ハッシュ値を得ます。
- 1.~3.を決められたチェインの長さまで繰り返します。ただし、還元関数は列毎に別の平文を得られるようにします。
- 最後に、先頭と末尾の平文のみを残したものが一つのチェインとなります。
このチェインを決められた数まで作成したものがレインボーテーブルとなります。
ここまでを一度実装してみます。
還元関数のアイデアは以下のページを参考にしました。
d.hatena.ne.jp
また、レインボーテーブル作成時にtqdmライブラリを使って、プログレスバーを表示していますので、tqdmをインストールする必要があります。
import hashlib import itertools import random from tqdm import tqdm class RainbowTable(object): NUM = "0123456789" LOWER = "abcdefghijklmnopqrstuvwxyz" UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" def __init__(self, func_hash, char_type, char_length, chain_length, n_chains): self.func_hash = self.md5 if func_hash=="md5" else self.sha1 self.char_type = char_type self.chars = {i:c for i, c in enumerate(char_type)} self.char_length = char_length self.chain_length = chain_length self.n_chains = n_chains self.rainbowtable = [] if self.char_length>16: print("Please char_length must be 16 or less.") exit(1) def md5(self, p): return hashlib.md5(p.encode()).hexdigest() def sha1(self, p): return hashlib.sha1(p.encode()).hexdigest() def reduction(self, h, i): p = "" i = int(h, 16)+i while i: p += self.chars[i%len(self.char_type)] i //= len(self.char_type) return p[:self.char_length] def chain(self, p): ch = [] for i in range(self.chain_length): ch.extend([p, self.func_hash(p)]) # plain + hash p = rt.reduction(ch[-1], i) # reduction plain ch.append(p) return ch def rainbow_table(self, path): fp = open(path, "w") p = itertools.product(self.char_type, repeat=self.char_length) # product all patern in char_type for i, s in enumerate(tqdm(p), start=1): s = "".join(s) chain = self.chain(s) # self.rainbowtable.append([chain[0], chain[-1]]) # append only chain head and chain tail fp.writelines(" ".join([chain[0], chain[-1]])) fp.write("\n") # write format: "head_chain tail_chain\n" if self.n_chains and i>=self.n_chains: # continue if n_chains==0 or i<n_chains, n_chains==0 is all patern break fp.close() def read_table(self, path): fp = open(path, "r") self.rainbowtable = [s.strip().split(" ") for s in fp.readlines()] # read format: "head_chain tail_chain\n" fp.close() if __name__ == "__main__": p = "00000000" rt = RainbowTable(func_hash="md5", char_type=RainbowTable.NUM+RainbowTable.LOWER, char_length=8, chain_length=1000, n_chains=1000) rt.rainbow_table("sample.rt") rt.read_table("sample.rt")
解読
あるハッシュ値からレインボーテーブルを使って平文を得る方法が以下になります。
- あるハッシュ値から末尾で使用した還元関数を使って、平文を得ます。
- 得られた平文とレインボーテーブルの末尾の平文が一致するか確かめます。
ここまでを実装してみます。
... class RainbowTable(object): ... def decode(self, t): if not self.rainbowtable: return False for i in range(self.chain_length-1, -1, -1): # column p = self.reduction(t, i) # to plain for j in range((self.chain_length-1)-i): # loop diff from tail h = self.func_hash(p) p = self.reduction(h, i+j+1) matched_chain = self.match_tail(p) if matched_chain: break if not matched_chain: return False matched_chain = self.chain(matched_chain[0]) return matched_chain[matched_chain.index(t)-1] ... if __name__ == "__main__": ... print("decoded =", rt.decode(rt.md5(p)))
githubに公開
Steganography(ステガノグラフィ)入門
出田 守です。 情報セキュリティに興味を持ち、独学で勉強中です。 主にPythonを使用します。時々勉強のためにCを使用することもあります。
目的
CTFの問題を解いていると、Steganographyに出会いました。名前は聞いたことあるけど、詳しくは知らなかったので調べました。調べた内容の説明後、実装までが今回の目的です。
Steganographyとは
Steganographyとはあるデータを他のデータに埋め込み秘匿してしまう技術の一種です。埋め込まれるデータをvesselやdummyと呼ばれるそうです。データとしては画像の他、音声データ、動画データなども対象になるようです。 従来のSteganography技術には以下があるようです。
ただし、これらの技術のデータ埋め込み容量は小さいそうです。一方BPCS-Steganographyではデータ埋め込み容量は約50%にもなるようです。 今回は特に画像を対象にBPCS-Steganographyについて学んでみます。
BPCS-Steganography
BPCS-SteganographyはSteganography技術の一種です。ビットプレーン分解と人間の視覚情報認識能力を利用した手法のようです。
ビットプレーン分解
1情報のうちに複数のビット情報を持つものを1ビット毎に分解すること。例えば24色画像の場合1ピクセルあたり224色あります。224色の内訳はRED、GREEN、BLUEそれぞれ28色で構成されています。つまり282828色ということです。ビットプレーンは抽出した特定のビットのことを言います。ここで例えばREDの28の最上位ビットをMSBといい、最下位ビットをLSBといいます。
ここでビットプレーン分解を行うプログラムを実装してみます。
import numpy as np from PIL import Image def read_image_as_numpy(path): img = Image.open(path) # read img arr = np.array(img) # convert to numpy array return arr def to_binary(arr): if len(arr.shape)==3: # 3 channels (R, G, B) bits = np.unpackbits(arr, axis=2) # [[[128,128,128]...]...] => [[[100000001000000010000000]...]...] bits = bits.reshape(bits.shape[:2]+(arr.shape[2], 8,)) # divide each color (h, w, c*8) => (h, w, c, 8) return bits elif len(arr.shape)==2: # 2 channels gray-scale bits = np.unpackbits(arr, axis=1) bits = bits.reshape(bits.shape[:1]+(arr.shape[1], 8,)) # (h, w*8) => (h, w, 8) return bits elif len(arr.shape)==1: org = arr.shape # original shape arr = np.unpackbits(arr) arr = arr.reshape(org+(8,)) # (original shape*bits) => (original shape, 8) return arr else: print("Unsupported shape of image") exit(1) return None def to_image(arr): if len(arr.shape)==4: # for 3 channels (R, G, B) arr = arr.reshape(arr.shape[:2]+(arr.shape[2]*arr.shape[3],)) # (h, w, c, 8) => (h, w, c*8) arr = np.packbits(arr, axis=2) return arr elif len(arr.shape)==3: # for 2 channels gray-scale arr = arr.reshape(arr.shape[:1]+(arr.shape[1]*arr.shape[2],)) # (h, w, 8) => (h, w*8) arr = np.packbits(arr, axis=1) return arr else: print("Unsupported shape of image") exit(1) return None def extract_bitplane(arr, color, bit): if len(arr.shape)==4: # RGB bitplane = arr[:,:,color,bit] # bitplane shape is (h, w) 2d array elif len(arr.shape)==3: # gray-scale bitplane = arr[:,:,bit] # bitplane shape is (h, w) 2d array else: print("Unsupported shape of image") exit(1) return bitplane
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<4: print("USAGE: {0} <PATH> <COLOR> <BIT>".format(sys.argv[0])) print(" PATH: image path") print(" COLOR: GRAY=-1, RED=0, GREEN=1, BLUE=2") print(" BIT : 0~7 (0:MSB, 7:LSB)") exit(1) PATH = sys.argv[1] COLOR = int(sys.argv[2]) BIT = int(sys.argv[3]) def merge_bitplane_to_image(bitplane, arr, color): arr = bp.to_image(arr) img = np.zeros(arr.shape) img[:,:,color] = bitplane return img arr = bp.read_image_as_numpy(PATH) if len(arr.shape)<2 or len(arr.shape)>3: print("Unsupported shape of image") exit(1) arr = bp.to_binary(arr) # arr.shape = (h, w, 3(color), 8(byte)) or (h, w, 8(byte)) # arr = bp.to_image(arr) # arr.shape = (h, w, 3) or (h, w) bitplane = bp.extract_bitplane(arr, COLOR, BIT) bitplane[bitplane>0] = 255 if COLOR!=-1 and len(arr.shape)==4: arr = merge_bitplane_to_image(bitplane, arr, COLOR) else: arr = bitplane Image.fromarray(np.uint8(arr)).show() # show image # Image.fromarray(np.uint8(arr)).save("test.png") # save image
$ python show_specific_bits.py sample.jpg 0 7
第一引数に画像パス、第二引数に抽出カラー、第三引数に抽出ビットです。
結果が以下になりました。
下位ビットにいくにつれて元画像が分からなくなっていますね。
視覚情報認識能力
上記のビットプレーン分解を見ると、LSBよりMSBのほうが、より元画像に近く見えます。このことは人間にとって重要な情報はMSBに近いビットさえあれば元画像として認識可能ということです。
PBCとCGC
画像にデータを埋め込む際、PBC(Pure Binary Code)よりもCGC(Canonical Gray Code)の方が埋め込み後の画像の変化が少ないそうです。
PBCからCGCへの変換は以下の参照ページを参照してください。
では、PBCからCGCとCGCからPBCの変換を行うプログラムを実装してみます。
... def pbc_to_cgc(arr): """ ref:http://datahide.org/BPCSj/pbc-vs-cgc-j.html """ if len(arr.shape)==3 and arr.shape[2]==3: print("Please first convert to binary. i.e. arr = to_binary(arr)") exit(1) cgc = arr.copy() if len(cgc.shape)==4: # RGB for i in range(1, 8): cgc[:,:,:,i] = np.logical_xor(arr[:,:,:,i-1], arr[:,:,:,i]) # gi = bi-1^bi elif len(cgc.shape)==3: # gray-scale for i in range(1, 8): cgc[:,:,i] = np.logical_xor(arr[:,:,i-1], arr[:,:,i]) # gi = bi-1^bi else: print("Unsupported shape of image") exit(1) return cgc def cgc_to_pbc(arr): """ ref:http://datahide.org/BPCSj/pbc-vs-cgc-j.html """ if len(arr.shape)==3 and arr.shape[2]==3: print("Please first convert to binary. i.e. arr = to_binary(arr)") exit(1) pbc = arr.copy() if len(pbc.shape)==4: # RGB for i in range(1, 8): pbc[:,:,:,i] = np.logical_xor(arr[:,:,:,i], pbc[:,:,:,i-1]) # bi = gi^bi-1 elif len(pbc.shape)==3: # gray-scale for i in range(1, 8): pbc[:,:,i] = np.logical_xor(arr[:,:,i], pbc[:,:,i-1]) # bi = gi^bi-1 else: print("Unsupported shape of image") exit(1) return pbc
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<2: print("USAGE: {0} <PATH>".format(sys.argv[0])) print(" PATH: image path") exit(1) PATH = sys.argv[1] arr = bp.read_image_as_numpy(PATH) if len(arr.shape)<2 or len(arr.shape)>3: print("Unsupported shape of image") exit(1) arr = bp.to_binary(arr) arr = bp.pbc_to_cgc(arr) # arr = bp.cgc_to_pbc(arr) arr = bp.to_image(arr) Image.fromarray(np.uint8(arr)).show() # show image # Image.fromarray(np.uint8(arr)).save("test.png") # save image
試した結果が以下です。味のある感じになりましたね笑 ちなみに元画像と同じですがcgcからpbcにした結果も載せておきます。
参照: Two Binary Number Coding Systems
複雑さ
埋め込まれる画像が埋め込まれる対象として適しているかを示す尺度のようです。下記参照ページでは白黒の境界の合計値にその画像での最大境界数を割った割合を複雑さとしています。この複雑さは画像全体としても局所的にも適用できます。まずは、画像全体のそれぞれのbitplaneでの複雑さを出力してみます。
... def get_block_as_iter(bitplane, blocksize): n_rows, n_cols = bitplane.shape for y in range(0, n_rows, blocksize[0]): for x in range(0, n_cols, blocksize[1]): block = bitplane[y:y+blocksize[0], x:x+blocksize[1]] if block.shape==blocksize: yield x, y, block return None def complexity(bitplane): n_rows, n_cols = bitplane.shape # bitplane shape is (h, w) 2d array max_cpx = ((n_rows-1)*n_cols)+((n_cols-1)*n_rows) # max complexity k = 0 xor = np.logical_xor(bitplane[1:], bitplane[:-1]) # row complexity k += len(xor[xor==True]) xor = np.logical_xor(bitplane[:,1:], bitplane[:,:-1]) # col complexity k += len(xor[xor==True]) return k/max_cpx*1.0
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<4: print("USAGE: {0} <PATH> <COLOR> <BIT>".format(sys.argv[0])) print(" PATH: image path") print(" COLOR: GRAY=-1, RED=0, GREEN=1, BLUE=2") print(" BIT : 0~7 (0:MSB, 7:LSB)") exit(1) PATH = sys.argv[1] COLOR = int(sys.argv[2]) BIT = int(sys.argv[3]) arr = bp.read_image_as_numpy(PATH) if len(arr.shape)<2 or len(arr.shape)>3: print("Unsupported shape of image") exit(1) # arr = bp.pbc_to_cgc(arr) arr = bp.to_binary(arr) # image complexity bitplane = bp.extract_bitplane(arr, COLOR, BIT) # extract bitplane as binary, shape is (h, w) cpx = bp.complexity(bitplane) print("image complexity =", cpx) # block complexity for i, (x, y, block) in enumerate(bp.get_block_as_iter(bitplane, (8,8))): cpx = bp.complexity(block) print("block[{0}] complexity =".format(i), cpx)
MSBに近いと複雑さはないです。LSBは0.5付近の複雑さを多く持っています。
埋込みデータ
秘密データは決められたサイズに小ブロック化します。もしそのブロックの複雑さが閾値以下なら、白黒データ(1から始まる縞々データ)とXORさせて複雑にします。秘密データ抽出のために、そのブロックをXORしたかどうかをコンジュゲーションマップとしてどこかに記録しておく必要があります。ということは埋め込む際に何かしらフォーマットを決めておくと良さそうです。
ではまず、秘密データを小ブロック化して、白黒データをXORするプログラムを実装してみます。なお、今回は秘密データとしてASCII文字列メッセージを対象とします。
I am hiding. Please do not search.\n
... def padding(arr, blocksize): block_row,block_col = blocksize n_row = len(arr) lack_row = block_row-n_row%block_row if lack_row>0: wb = create_wb((lack_row, 8)) # ascii only,so 8bit. wb is [[1,0,1,0,1,0,1,0]*lack_row] arr = np.concatenate((arr, np.packbits(wb))) # concatenate arr and packed wb return arr def read_message_as_numpy(path, blocksize): fp = open(path, "r") msg = fp.read() msg = np.array([ord(c) for c in msg], dtype=np.uint8) msg = np.concatenate((msg, SHINOBI)) msg = padding(msg, blocksize) fp.close() return msg def create_wb(size): if size[1]==1: return np.array([[1]*size[0]]) wb = np.tile([1, 0], (size[0],size[1]))[:size[0], :size[1]] if len(wb[1::2])>0: wb[1::2] = 1-wb[1::2] # invert only odd rows return wb def complication(block): wb = create_wb(block.shape) return np.uint8(np.logical_xor(block, wb))
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<2: print("USAGE: {0} <PATH>".format(sys.argv[0])) print(" PATH: secret file path") exit(1) PATH = sys.argv[1] blocksize = (8,8) ath = 0.45 # complexity threshold arr = bp.read_message_as_numpy(PATH, blocksize) arr = bp.to_binary(arr) for x, y, block in bp.get_block_as_iter(arr, blocksize): cpx = bp.complexity(block) print(block) print(block.shape) print("original complexity =", cpx) if cpx<ath: block = bp.complication(block) print("converted complexity =", bp.complexity(block)) print("="*30)
秘密データの終わりに特定の文字を仕込みました。
各ブロックの複雑さを見てみると、0.45以下のものを白黒データでXORしてあげると、複雑さが増しているのが分かります。
データ埋込み
いよいよデータを埋め込む準備ができました。
埋め込みはREDのLSBのビットプレーンから順番に各ブロックを走査し、複雑さが閾値以上のブロックと秘密データのブロックを入れ替えます。
なお、埋め込み後の画像出力はPNGです。JPGだと埋め込んだデータが壊れていためです。おそらく原因は非可逆圧縮だからでしょうか。
コンジュゲーションマップは秘密データの各ブロックがXORしたかを0または1で表すことにしました。これの埋め込みは秘密データの各ブロックの埋め込み前に埋め込まれるデータブロックの複雑さに関係なく埋め込まれます。注意として、コンジュゲーションマップは複雑さが閾値以下でも複雑化していません。
def secret_blocks(arr, blocksize, ath): # create secret blocks secret_blocks = [] conj_map = np.array([0]*int(arr.shape[0]/arr.shape[1]), dtype=np.uint8) for i, (x, y, block) in enumerate(get_block_as_iter(arr, blocksize)): cpx = complexity(block) if cpx<ath: block = complication(block) conj_map[i] = 1 secret_blocks.append(block) # create conjugation map (NOTE: not complication) conj_map = conjugation_map(conj_map, blocksize) return secret_blocks, conj_map def encode(arr, secret_blocks, conj_map, blocksize, ath): COLOR = 1 if len(arr.shape)==4: # RGB COLOR = 3 elif len(arr.shape)==3: # gray-scale arr = arr.reshape(arr.shape[:2]+(1, arr.shape[2],)) COLOR = 1 else: print("Unsupported shape of image") exit(1) secret_blocks = secret_blocks[:] conj_map = conj_map[:] for color in range(COLOR): for bit in (7, -1, -1): bitplane = extract_bitplane(arr, color, bit) for i, (x, y, block) in enumerate(get_block_as_iter(bitplane, blocksize)): if conj_map: bitplane[y:y+blocksize[0], x:x+blocksize[1]] = conj_map.pop(0) elif complexity(block)>ath: bitplane[y:y+blocksize[0], x:x+blocksize[1]] = secret_blocks.pop(0) if not secret_blocks: break arr[:,:,color,bit] = bitplane if not secret_blocks: if arr.shape[2]==1: # gray-scale arr = arr.reshape(arr.shape[:2]+(arr.shape[3],)) return arr return None
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<3: print("USAGE: {0} <IPATH> <SPATH>".format(sys.argv[0])) print(" IPATH: image path") print(" SPATH: secret file path") exit(1) IPATH = sys.argv[1] SPATH = sys.argv[2] blocksize = (8,8) ath = 0.45 # complexity threshold # prepare secret blocks arr = bp.read_message_as_numpy(SPATH, blocksize) arr = bp.to_binary(arr) secret_blocks, conj_map = bp.secret_blocks(arr, blocksize, ath) # encode arr = bp.read_image_as_numpy(IPATH) arr = bp.to_binary(arr) arr = bp.encode(arr, secret_blocks, conj_map, blocksize, ath) arr = bp.to_image(arr) Image.fromarray(np.uint8(arr)).show() Image.fromarray(np.uint8(arr)).save("images/encoded.png")
元画像が以下です。
そして、埋込み後の結果は以下です。
全く違いが分かりませんね!
ついでに、MSBのビットプレーンにも埋め込んでみました。
帽子付近にちょこちょこっとノイズが入っていますね。
データ抽出
一応埋め込めましたが、実際に取り出せないとただの落書きになってしまうので、埋め込んだデータを抽出してみます。
... def decode_block(block): decoded = np.packbits(block).tolist() decoded = "".join([chr(c) for c in decoded]) return decoded def decode(arr, blocksize, ath): COLOR = 1 if len(arr.shape)==4: # RGB COLOR = 3 elif len(arr.shape)==3: # gray-scale arr = arr.reshape(arr.shape[:2]+(1, arr.shape[2],)) COLOR = 1 else: print("Unsupported shape of image") exit(1) conj = True conj_map = [] decoded = "" shinobi = "".join([chr(c) for c in SHINOBI]) tail = "".join([chr(c) for c in TAIL]) for color in range(COLOR): for bit in (7, -1, -1): bitplane = extract_bitplane(arr, color, bit) for x, y, block in get_block_as_iter(bitplane, blocksize): if conj: decoded += decode_block(block) if tail in decoded: conj_map = [ord(c) for c in decoded[:decoded.index(tail)]] conj = False decoded = "" elif complexity(block)>ath: if conj_map and conj_map.pop(0): decoded += decode_block(complication(block)) else: decoded += decode_block(block) if shinobi in decoded: return decoded[:decoded.index(shinobi)] return None
import sys import numpy as np import bpcs as bp from PIL import Image if len(sys.argv)<2: print("USAGE: {0} <PATH>".format(sys.argv[0])) print(" PATH: encoded image path") exit(1) PATH = sys.argv[1] blocksize = (8,8) ath = 0.45 # complexity threshold arr = bp.read_image_as_numpy(PATH) arr = bp.to_binary(arr) decoded = bp.decode(arr, blocksize, ath) print("[decoded code]") print(decoded)
githubに公開
完全版を公開しました。