🥧

【Python】 カラーハーフトーンの作り方

2022/07/31に公開


元画像


Cyan + Magenta + Yellow


Cyan + Magenta

この記事では、画像をカラーモデルのCMYKに分離してハーフトーンを生成する、カラーハーフトーンの作り方について解説する。ただし、見よう見まねの我流で作ったものなので、出力のクォリティについては大目に見てほしい。

そもそもハーフトーンとは?

ハーフトーンは画像内の範囲ピクセル、たとえば10x10ピクセルのような範囲の明るさ平均を算出し、その値をもとに特定の形で描画し直す処理である。上記画像では明るさが大きいほど、大きい白円を描く仕組みになっている。

これから作るカラーハーフトーンも基本的には同じ仕組みで、画像をCMYKの4つのチャンネルに分離して、それらから生成したハーフトーン画像を合成したものがカラーハーフトーンの作成結果になる。

アルゴリズム

まず画像をCMYKの各チャンネルに分離する。


https://ja.wikipedia.org/wiki/CMYK より引用

次にチャンネルごとに画像を傾ける。これはもともとモアレ防止の対策らしいが、デザインとして利用ができるので、ここでもそのまま傾ける処理を採用する。

そして上記引用画像のように、傾けた画像から一定範囲おきに明るさを計算して、その明るさに応じた大きさの円を描く。

描かれた4つの画像を合成して一つの画像としてまとめる。ただし、下記の実装例ではCMYKのKを利用していない。これは合成を行うとCMYだけで黒が表現できるのでKが不要になるからである。

制作環境

  • Python3
  • Pillow (PIL)

https://python-pillow.org/

Pythonでの実装例

https://github.com/BaroqueEngine/python_color_halftone/

上記実装例の解説

from PIL import Image, ImageDraw, ImageStat

def norm(v, a, b):
  return (v - a) / (b - a)

def lerp(a, b, t):
  return a + (b - a) * t

def map(v, a, b, c, d):
  return lerp(c, d, norm(v, a, b))

# src: Image.open() で開いた元画像
# size: この範囲おきに明るさを計算して円を描く
# angle: チャンネルをずらす角度
# max_radius: 円の最大半径
# channel_index: CMYに対応するインデックス C = 0, M = 1, Y = 2
def convert(src, size, angle, max_radius, channel_index):
    width, height = src.size
    # 画像をCMYKに分離する
    cmyk = src.convert("CMYK").split()
    channel = cmyk[channel_index]

    # angleの角度でチャンネルを回転。はみ出た部分は拡張する。
    channel = channel.rotate(angle, expand=True)
    channel_width, channel_height = channel.size
    # 円を描くための新しい画像
    img = Image.new("L", (channel_width, channel_height))
    canvas = ImageDraw.Draw(img)
    
    # size x sizeおきに円を描く
    for y in range(0, channel_height, size):
        for x in range(0, channel_width, size):
            # 領域を取り出して、平均色を計算する。
            area = channel.crop((x, y, x + size, y + size))
            stat = ImageStat.Stat(area)
            avg = stat.mean[0]
      
            # 平均色の値を1~max_radiusの範囲にマッピングする。
            r = map(avg, 0, 255, 1, max_radius)
      
            # 半径rの円を描く。
            x0 = x + size / 2 - r
            y0 = y + size / 2 - r
            x1 = x + size / 2 + r
            y1 = y + size / 2 + r
            canvas.ellipse((x0, y0, x1, y1), fill=255)
    
    # チャンネルの回転を元に戻す。
    img = img.rotate(-angle, expand=True)
    
    # 回転のために拡大した画像から、
    # 元画像のサイズ分を中央を軸に切り抜く。
    new_width, new_height = img.size
    px = (new_width - width) // 2
    py = (new_height - height) // 2
    img = img.crop((px, py, px + width, py + height))

    return img

angles = [0, 15, 30] # CMY毎の回転角度
size = 20 # 各領域のサイズ
max_radius = 15 # 円の最大半径
src = Image.open("input.jpg") # 元画像
width, height = src.size

# CMY毎にハーフトーン画像を作る。
c = convert(src, size, angles[0], max_radius, 0)
m = convert(src, size, angles[1], max_radius, 1)
y = convert(src, size, angles[2], max_radius, 2)
# 今回Kは必要ないが、下記のImage.merge()を行うのに画像が必要になるので、
# 黒画像を用意しておく。
k = Image.new("L", (width, height), 0)

# 合成して1つの画像にまとめて出力。
output = Image.merge("CMYK", [c, m, y, k])
output.convert("RGB").save("output.png")

Discussion