ユーニックス総合研究所

  • home
  • archives
  • pillow-nijimi

Pillowで文字ににじみを作る【画像処理, Python】

  • 作成日: 2021-04-13
  • 更新日: 2023-12-24
  • カテゴリ: Pillow

Pillowでにじみを作る

「画像処理」と画像に加工などを加える処理のことです。
Pythonには画像処理ライブラリとして有名なものにPillowがあります。

今回はこのPillowを使って、日本語の文章ににじみを作るという画像処理をやってみたいと思います。
具体的には↓を見ていきます。

  • スクリプトの実行結果
  • 「にじみ」とは?
  • スクリプトのソースコード
  • ソースコードの解説

スクリプトの実行結果

今回作成するスクリプトを実行すると↓のような画像が生成されます。

「こんにちは ご機嫌よう さようなら」という文章ににじみが出来てるのがわかります。
今回作成するスクリプトはこのように文字列ににじみを作ります。

「にじみ」とは?

そもそも「にじみ」とはどういうものでしょうか?
にじみとは、オブジェクトの周辺に出来る近似色のことです。
今回の例で言えば、文字列の黒いドットの周りに出来る灰色のドットがにじみです。
輪郭の周辺にこのような薄い近似色を塗ることで、その輪郭部分がぼやけ、にじんでいるように見えます。

現実世界でも和紙に習字を書き、それを水に浸けると同じようににじみが出来ます。
にじみとは水による作用です。色素に水が触れてその色素が周りににじみだすことでにじみが出来ます。

ソフトウェアでこのにじみを再現するには、まずにじませたいオブジェクトの色を取得しておきます。
今回の例で言えば文字列の色です。
そしてその色に水を垂らしたような色合いの色を新しく作り、それを輪郭の周辺に置きます。
こうすることで輪郭がぼやけ、オブジェクトがにじんでいるように見えます。

今回、作成するにじみはテクスチャの効果を考慮しません。
つまり、紙に書かれたモノであるとか、石に書かれたモノであるかとかの判断はしません。
一律に円形の筆でにじみを作るだけです。

紙のようなテクスチャを考慮したい場合は、紙の表面に描画色がにじんでいく効果を実装する必要があります。
紙の目は無数の繊維がからみあってできており、このすきまに色素が落ちていって色がつきます。
このような効果は、ペイントソフトによっては再現しているものもあります。

スクリプトのソースコード

今回作成したスクリプトのソースコードです。

"""  
テキストに滲みを作るスクリプト  
"""  
from PIL import Image, ImageDraw, ImageFont  
import random  


def draw_ellipse(im, draw, fs, x, y):  
    """  
    drawに円を描画する  
    """  
    doit = random.randint(0, 10)  # 確率で実行する  
    if doit != 5:  
        return  # 実行しない  

    pix = im.getpixel((x, y))  # 座標上のピクセルを取る  
    if pix[0] != 0 or pix[1] != 0 or pix[2] != 0:  
        return  # 黒色じゃなければ何もしない  

    ex = random.randint(x - 5, x + 5)  # 円のX座標  
    ey = random.randint(y - 5, y + 5)  # 円のY座標  
    ew = random.randint(4, 8)  # 円の横幅  
    eh = ew  # 円の高さ  
    r = random.randint(120, 230)  # 色の濃さ  
    g = r  
    b = r  
    draw.ellipse([(ex, ey), (ex + ew, ey + eh)], fill=(r, g, b))  # 円を描く  


def draw_msg(draw, font, fs, msg):  
    """  
    drawにメッセージを描画する  
    """  
    for y, s in enumerate(msg):  # 1行ずつメッセージを描画  
        draw.text((0, y * fs), s, 'black', font=font)  


def main():  
    msg = ['こんにちは', 'ご機嫌よう', 'さようなら']  # メッセージはリストにしておく  
    fs = 32  # フォントサイズ  
    im = Image.new('RGB', (32 * 5, 32 * len(msg)), (255, 255, 255))  # キャンバスを作る  
    draw = ImageDraw.Draw(im)  # 描画コンテキストの取得  
    font = ImageFont.truetype('C:/Windows/Fonts/msgothic.ttc', fs)  # フォントのロード  

    draw_msg(draw, font, fs, msg)  # 最初にメッセージを描画  

    # その上からにじみを描画する  
    for y in range(im.size[1]):  
        for x in range(im.size[0]):  
            draw_ellipse(im, draw, fs, x, y)  

    draw_msg(draw, font, fs, msg)  # 再びメッセージを描画してハッキリさせる  
    im.save('out.png')  # 画像を保存  


main()  

ソースコードの解説

スクリプトはmain()関数から始まります。

必要モジュールのインポート

必要となるモジュールをインポートします。
今回はPillow(PIL)からImage, ImageDraw, ImageFontを使います。
それからにじみをランダムに配置するのでrandomモジュールもインポートしておきます。

from PIL import Image, ImageDraw, ImageFont  
import random  

main()関数

main()関数内ではまずさまざまなオブジェクトを初期化します。
msgにはリストで文字列を入れておきます。これが描画される文字列です。
そしてfsという変数には文字列のフォントサイズ(px)を入れておきます。

Image.new()でキャンバスを作成します。
キャンバスのモードはRGBです。それから横幅はfs * 5で、高さはfs * len(msg)です。
キャンバスの背景色は(255, 255, 255)で真っ白にしておきます。

ImageDraw.Draw(im)drawという描画コンテキストを作っておきます。
これは描画を便利にするためのユーティリティーです。

それから文字列の描画のためにfontにフォントをロードしておきます。
今回はWindows環境のMSゴシックフォントを使います。

最初にdraw_msg()でキャンバスにmsgを描画しておきます。
その後にキャンバスににじみを描画します。

2重のfor文を回してdraw_ellipse()を呼び出し、円形の描画をキャンバス上にランダムに置いていきます。

その後、ふたたびdraw_msg()でキャンバスにmsgを描画します。
これはにじみでぼやけてしまった文字列の輪郭をハッキリさせるためです。

その後はim.save()で生成した画像を保存します。

def main():  
    msg = ['こんにちは', 'ご機嫌よう', 'さようなら']  # メッセージはリストにしておく  
    fs = 32  # フォントサイズ  
    im = Image.new('RGB', (fs * 5, fs * len(msg)), (255, 255, 255))  # キャンバスを作る  
    draw = ImageDraw.Draw(im)  # 描画コンテキストの取得  
    font = ImageFont.truetype('C:/Windows/Fonts/msgothic.ttc', fs)  # フォントのロード  

    draw_msg(draw, font, fs, msg)  # 最初にメッセージを描画  

    # その上からにじみを描画する  
    for y in range(im.size[1]):  
        for x in range(im.size[0]):  
            draw_ellipse(im, draw, fs, x, y)  

    draw_msg(draw, font, fs, msg)  # 再びメッセージを描画してハッキリさせる  
    im.save('out.png')  # 画像を保存  


main()  

draw_msg()でメッセージを描画

draw_msg()は引数drawを使って引数msgを描画します。
内容的にはfor文でmsgを回して、draw.text()で文字列を描画します。
draw.text()の第1引数には描画位置(xy座標)、第2引数には文字列、第3引数には色の指定、そしてfont引数にはフォントを指定します。

def draw_msg(draw, font, fs, msg):  
    """  
    drawにメッセージを描画する  
    """  
    for y, s in enumerate(msg):  # 1行ずつメッセージを描画  
        draw.text((0, y * fs), s, 'black', font=font)  

draw_ellipse()で円を描画

draw_ellipse()は引数drawを使って引数x, yの周辺ににじみを作ります。

まずrandom.randint()で関数を実行するかどうかの確率判定を行います。
random.randint(0, 10)の返り値が5でなければそのままreturnして関数の実行を終了します。
5であれば関数を実行します。
このdraw_ellipse()は画像の画素ごとに呼ばれるので、これぐらいの確率で十分機能します。

im.getpixel()で引数x, yの座標の画素(pix)を取得します。
このpixが黒色でなければなにもせずreturnします。
黒色であれば関数を継続します。

ex, eyにはにじみの座標を保存します。これは引数x, yの周辺の座標で、ランダムに取得します。
ew, ehには円の横幅と高さを保存します。これは4から8の値です。
r, g, bには120から230の値が保存されます。これは描画色です。

パラメーターが揃ったらdraw.ellipse()で円を描画します。
draw.ellipse()の第1引数はリストを渡します。このリストの内訳は最初が円の始点座標で次が円の終了座標です。
それからfill引数には色を指定します。

def draw_ellipse(im, draw, fs, x, y):  
    """  
    drawに円を描画する  
    """  
    doit = random.randint(0, 10)  # 確率で実行する  
    if doit != 5:  
        return  # 実行しない  

    pix = im.getpixel((x, y))  # 座標上のピクセルを取る  
    if pix[0] != 0 or pix[1] != 0 or pix[2] != 0:  
        return  # 黒色じゃなければ何もしない  

    ex = random.randint(x - 5, x + 5)  # 円のX座標  
    ey = random.randint(y - 5, y + 5)  # 円のY座標  
    ew = random.randint(4, 8)  # 円の横幅  
    eh = ew  # 円の高さ  
    r = random.randint(120, 230)  # 色の濃さ  
    g = r  
    b = r  
    draw.ellipse([(ex, ey), (ex + ew, ey + eh)], fill=(r, g, b))  # 円を描く  

おわりに

今回はPillowで文字列ににじみを作ってみました。
今回使ったアルゴリズムは非常にシンプルなもので、生成されるにじみのクオリティもそれほど高くはありません。
もっと凝ったアルゴリズム使ってみたい方はぜひチャレンジしてみてください。

🦝 < 努力がにじみ出る

🐭 < 元気がにじみ出る