要旨
Poooli L3 プリンタで 8 階調 grayscale 印刷する Python プログラムを改良した。RGB 三色に分解しそれぞれでディザリングした後再び合成し、その合成画像を 8 階調白黒画像とする方法をとった。この方法は chatGPT に案を出させてあみ出した。
前置き
Poooli 社の L3 サーマル・プリンタは、グレイスケール印刷が可能になっている。以前、Poooli L3 プリンタの Bluetooth 出力の解析を行い、grayscale 出力の命令を見出した。しかしながら、一般のカラー画像を適切な grayscale 画像に変換することが出来ず、グラデーションが縞模様になるなど課題を残した。
最近 OpenAI 社の chatGPT が流行っているので、これに頼ってカラー画像を grayscale 画像に変換する方法を得ることを試みた。chatGPT はそのまま使える結果は出してくれなかったものの、複数回質問を重ねるうち画像を RRGB 三色に分解した上で画像処理をするというアイデアを出してきたので、そのアイデアを拝借することにして、残りを適宜作った。
8 階調 grayscale への変換法
処理は画像データを RGB 三色へ分解したのち、それぞれを 2 階調画像にディザリングしたうえで、再びカラー画像として合成する。そうしてこの合成したカラー画像を 8 階調の grayscale 画像に変換する。元々のカラー画像を直接 8 階調の grayscale 画像とすると、グラデーションなどが量子化の閾値で縞模様を作ってしまうが、この方法では縞は目立たない。またディザリング特有の点々感も軽減される。
アルゴリズム中核
img_r, img_g, img_b = image.split()
img_r = img_r.convert(mode="1").convert(mode="L")
img_g = img_g.convert(mode="1").convert(mode="L")
img_b = img_b.convert(mode="1").convert(mode="L")
# black&white dithering
image = Image.merge("RGB", (img_r, img_g, img_b))
image = image.convert(mode="L") # 8bit grayscale
depth = 8
image = image.quantize(depth)
なおプリンタの出力としては、6 階調程度の分解能しか持たないが、全体に色が薄いため色が濃くなうように階調を増やしている。
出力例
下の画像との比較から背景の放射状の光のグラデーションが改善されていることが分かる。(写真のピントが合っていないのでこの画像からは判然としないが)上の画像との比較からディザリング特有の点々の模様も改善されていることも分かる。
スクリプトプログラム
chatGPT に講評を求めたところ、bluetooth 接続時の例外処理が無いとか文句を垂れつつ改善例を示してくれたので、一部拝借して改良した。
- 利用法
python -m スクリプトプログラム名 画像ファイル名
なおスクリプトプログラム名の拡張子の .py は付けなくていい。
import sys
import binascii
import socket
import ctypes
from PIL import Image # , ImageOps
Poooli_L3 = ("00:15:82:93:3C:F9", 6) # MAC address, channel
INITIALIZE = b"\x1b\x1cset mm\x05\x08"
PAPER_TYPE = b"\x10\x7e\x68\x79\x7d\x0d"
# width ed09h 1248dots, 9d0eh 912dots, 850fh 648
PAPER_WIDTH = b"\x10\x7e\x68\x79\x7a" + b"\xed\x09"
# thickness 3Ah 55, 46h 75, 52h 95
DENSITY = b"\x10\x7e\x68\x79\x6e" + b"\x52"
args = sys.argv
fn = args[1]
width = 1248 # default 107mm paper
print("Connecting...")
s = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM,
socket.BTPROTO_RFCOMM)
try:
s.connect(Poooli_L3)
except socket.error as e:
print(f"Connection failed: {e}")
sys.exit(1)
if len(args) > 2:
width = int(args[2])
# s.connect((args[3], 1))
# printer initialization
s.send(INITIALIZE)
s.recv(2)
s.send(PAPER_TYPE)
s.recv(2)
s.send(PAPER_WIDTH)
s.recv(2)
# set concentration
s.send(DENSITY)
s.recv(2)
# self test
# s.send(b"\x16\x16\x0D")
# PIL
image = Image.open(fn)
if image.width > image.height:
image = image.transpose(Image.ROTATE_90)
#
# width 1248/912/576 dots
IMAGE_WIDTH_BITS = width # must be multiple of 8
IMAGE_WIDTH_BYTES = IMAGE_WIDTH_BITS // 8
image = image.resize(size=(IMAGE_WIDTH_BITS, int(
image.height * IMAGE_WIDTH_BITS / image.width)))
#
img_r, img_g, img_b = image.split()
img_r = img_r.convert(mode="1").convert(mode="L")
img_g = img_g.convert(mode="1").convert(mode="L")
img_b = img_b.convert(mode="1").convert(mode="L")
# black&white dithering
image = Image.merge("RGB", (img_r, img_g, img_b))
image = image.convert(mode="L") # 8bit grayscale
depth = 8
image = image.quantize(depth)
# ImageOps.posterize(image, 2)
# image.show()
## print ##
lzo = ctypes.cdll.LoadLibrary('./minilzo.so')
wk = ctypes.create_string_buffer(16384 * 8) # 64bit pointer
#
mx = image.width // 8
ny = image.height
print("Data sending...")
for iy in range(ny):
if iy % 100 == 0:
print(100 * iy // ny, "%")
image_bytes = b""
for ik in range(depth - 0):
for ix in range(mx):
byte = 0
for bit in range(8):
indx = ix * 8 + bit, iy
bw = image.getpixel(indx)
if bw > ik:
byte |= 1 << (7 - bit)
image_bytes += byte.to_bytes(1, "little")
# miniLZO compress
buff = ctypes.create_string_buffer(image_bytes)
ni = ctypes.c_int(len(buff) - 1)
no_max = len(buff) + len(buff) // 16 + 64 + 3
out = ctypes.create_string_buffer(no_max)
no = ctypes.c_int(len(out))
iret = lzo.lzo1x_1_compress(ctypes.byref(buff), ni, ctypes.byref(
out), ctypes.byref(no), ctypes.byref(wk))
lzno = no.value
image_lzo = b"\x12\x78\x07" + \
iy.to_bytes(2, byteorder="little") + \
lzno.to_bytes(4, byteorder="little")
image_lzo += out[:lzno]
#
# crc32
icrc32 = binascii.crc32(image_lzo, -1 ^ 489490)
icrc32_xor = icrc32 ^ (((13 * 256 + 13) * 256 + 13) * 256 + 13)
# send to printer; all data must be XORed with 13 (\x0d)
tmp = b"" # compressed data
for ib in range(no.value + 9):
tmp += (image_lzo[ib] ^ 13).to_bytes(1, "little")
s.send(tmp)
s.send(icrc32_xor.to_bytes(4, byteorder="little"))
# end of grayscale data
print("Data sent...")
s.send(b"\x1F\x75\x04")
ny_xor = (ny - 1) ^ (((13 * 256 + 13) * 256 + 13) * 256 + 13)
#
s.send(ny_xor.to_bytes(4, byteorder="little"))
a = s.recv(10)
print(a[:4])
a = s.recv(12)
print(a[5:11])
# wait till print ends
# s.send(b"\x16\x1f\x69") # get info
s.send(b"\x16\x1f\x7e") # get status
a = s.recv(16)
print("Energy ", int(a[3]), "%")
# close bluetooth connection
s.close()
まとめ
Poooli 社 L3 プリンタの grayscale 出力用 python プログラムを chatGPT の助太刀を得て改良した。chatGPT は求める結果を直接には与えてくれなかったものの、試行錯誤を通じて思いがけないアイデアを得ることが出来た。