@ftnext (nikkie)

Pillowで画像処理してモザイクアートを作る

In [9]:
color_data = materials_list_from_file('average_color.csv')

icon_im = Image.open('my_icon.png')
display_image(icon_im)
icon_im_width, icon_im_height = icon_im.size

mosaic_icon_im = Image.new('RGBA', (1600, 1600))
for left in range(0, icon_im_width, DOT_AREA_ONE_SIDE):
    for top in range(0, icon_im_height, DOT_AREA_ONE_SIDE):
        average_color = calc.average_color_in_range(icon_im, left, top,
                            left+DOT_AREA_ONE_SIDE, top+DOT_AREA_ONE_SIDE)
        if len(average_color) != 3:
            continue

        filename = similar_color_filename(average_color, color_data)
        # 距離最小のファイルを縮小して1600×1600の画像に貼り付け
        area_im = Image.open('image/euph_part_icon/'+filename)
        area_im.thumbnail((THUMBNAIL_ONE_SIDE, THUMBNAIL_ONE_SIDE))
        mosaic_icon_im.paste(area_im, (left//DOT_AREA_ONE_SIDE * THUMBNAIL_ONE_SIDE,
                                       top//DOT_AREA_ONE_SIDE * THUMBNAIL_ONE_SIDE))

save_path = 'product/my_icon_mosaic_mean.png'
mosaic_icon_im.save(save_path)

# Display Image ##### @@@@@ #####
#画像の読み込み
im = Image.open(save_path)
display_image(im)

#rettypy #7 (2018/02/10) でやったこと

  • モザイクアートの前処理のプログラムをクラスを使って書き直し
  • Slackのアイコンでモザイクアートが作れなかった件の原因調査

モザイクアートの前処理のプログラムをクラスを使って書き直し

before

calculate_average_color.py

import os
import csv

from PIL import Image

from mosaic_art import calc


data_list = []
for image_name in os.listdir('image/euph_part_icon'):
    if not image_name.endswith('.png'):
        continue
    im = Image.open('image/euph_part_icon/'+image_name)
    im_width, im_height = im.size
    red, green, blue = calc.average_color_in_range(im, 0, 0, im_width, im_height)
    data_list.append([image_name, red, green, blue])

with open('average_color.csv', 'w', newline='') as csv_file:
    csv_writer = csv.writer(csv_file)
    csv_writer.writerows(data_list)

calculate_mode_color.py

import os
import csv

from PIL import Image

from mosaic_art import calc


data_list = []
for image_name in os.listdir('image/euph_part_icon'):
    if not image_name.endswith('.png'):
        continue
    im = Image.open('image/euph_part_icon/'+image_name)
    im_width, im_height = im.size
    red, green, blue = calc.mode_color_in_range(im, 0, 0, im_width, im_height)
    data_list.append([image_name, red, green, blue])

with open('mode_color.csv', 'w', newline='') as csv_file:
    csv_writer = csv.writer(csv_file)
    csv_writer.writerows(data_list)

差分は2箇所

$ diff calculate_average_color.py calculate_mode_color.py

15c15

< red, green, blue = calc.average_color_in_range(im, 0, 0, im_width, im_height)

---

> red, green, blue = calc.mode_color_in_range(im, 0, 0, im_width, im_height)

18c18

< with open('average_color.csv', 'w', newline='') as csv_file:

---

> with open('mode_color.csv', 'w', newline='') as csv_file:

  • 15行目: 素材画像を代表する色を計算するメソッドを呼び分けている
  • 18行目: 素材画像を代表する色の計算結果記入ファイルを切り替えている

→クラスのプロパティとして持たせる。

 インスタンス作成時の引数で適切なプロパティを設定する

after

class ColorCalculator:
    def __init__(self, calc_type):
        self.calc_func = ColorCalculator.calculate_function(calc_type)
        self.csv_name = ColorCalculator.color_csv_name(calc_type)

    def calculate(self):
        data_list = []
        for image_name in os.listdir('image/euph_part_icon'):
            if not image_name.endswith('.png'):
                continue
            im = Image.open('image/euph_part_icon/'+image_name)
            im_width, im_height = im.size
            red, green, blue = self.calc_func(im, 0, 0, im_width, im_height)
            data_list.append([image_name, red, green, blue])

        with open(self.csv_name, 'w', newline='') as csv_file:
            csv_writer = csv.writer(csv_file)
            csv_writer.writerows(data_list)
  • shinyorkeさんから: クラスを持ち込む必要はなかったかもしれない(オブジェクト指向は設計をしっかり考えないと後々苦しむことになる)

→[今後] 関数を使って実装して比較してみる

Slackのアイコンでモザイクアートが作れなかった件の原因調査

In [11]:
icon_im = Image.open('slack.png')
icon_im_width, icon_im_height = icon_im.size
In [12]:
icon_im
Out[12]:
In [13]:
# 元の画像の平均色の可視化
dot_icon_im = icon_im.copy()
for left in range(0, icon_im_width, DOT_AREA_ONE_SIDE):
    for top in range(0, icon_im_height, DOT_AREA_ONE_SIDE):
        red, green, blue = calc.average_color_in_range(icon_im, left, top,
                            left+DOT_AREA_ONE_SIDE, top+DOT_AREA_ONE_SIDE)
        average_color_im = Image.new('RGBA',
                                     (DOT_AREA_ONE_SIDE, DOT_AREA_ONE_SIDE),
                                     (red, green, blue, 255)) # a=0だと透明で何も見えない
        dot_icon_im.paste(average_color_im, (left, top))
display_image(dot_icon_im)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-13-a8bc5bee0fa7> in <module>()
      4     for top in range(0, icon_im_height, DOT_AREA_ONE_SIDE):
      5         red, green, blue = calc.average_color_in_range(icon_im, left, top,
----> 6                             left+DOT_AREA_ONE_SIDE, top+DOT_AREA_ONE_SIDE)
      7         average_color_im = Image.new('RGBA',
      8                                      (DOT_AREA_ONE_SIDE, DOT_AREA_ONE_SIDE),

~/study/20171202mosaic-art-python/mosaic_art/calc.py in average_color_in_range(icon_im, left, top, right, bottom)
     28     mean_color = ImageStat.Stat(im_crop).mean
     29     red   = round(mean_color[0])
---> 30     green = round(mean_color[1])
     31     blue  = round(mean_color[2])
     32     return (red, green, blue)

IndexError: list index out of range

(R, G, B, A)のタプルじゃない。。

In [14]:
from PIL import ImageStat

mean_color = ImageStat.Stat(icon_im).mean
mean_color
Out[14]:
[69.50645]
In [15]:
icon_im.getpixel((100,100))
Out[15]:
55

(R, G, B, A)のタプルを想定

In [16]:
normal_im = Image.open('my_icon.png')
display_image(normal_im)
normal_im_width, normal_im_height = normal_im.size
print(normal_im_width, normal_im_height)
400 400
In [17]:
normal_mean_color = ImageStat.Stat(normal_im).mean
normal_mean_color
Out[17]:
[97.509325, 85.81641875, 87.0306, 254.9675]
In [18]:
normal_im.getpixel((100,100))
Out[18]:
(68, 84, 119, 255)

convertしたら解決した

In [19]:
icon_im2 = icon_im.convert('RGBA')
icon_im2
Out[19]:
In [20]:
icon_im2.getpixel((100,100))
Out[20]:
(130, 210, 224, 255)
In [21]:
ImageStat.Stat(icon_im2).mean
Out[21]:
[159.34800625, 145.5453125, 130.48335625, 248.25294375]

原因はImageのmodeらしい

https://pillow.readthedocs.io/en/5.0.0/handbook/concepts.html#concept-modes

P (8-bit pixels, mapped to any other mode using a color palette)

RGBA (4x8-bit pixels, true color with transparency mask)

In [22]:
icon_im.mode
Out[22]:
'P'
In [23]:
normal_im.mode
Out[23]:
'RGBA'
In [24]:
icon_im2.mode
Out[24]:
'RGBA'
In [27]:
color_data = materials_list_from_file('average_color.csv')

# icon_im = Image.open('my_icon.png')
display_image(icon_im2)
icon_im_width, icon_im_height = icon_im2.size

mosaic_icon_im = Image.new('RGBA', (1600, 1600))
for left in range(0, icon_im_width, DOT_AREA_ONE_SIDE):
    for top in range(0, icon_im_height, DOT_AREA_ONE_SIDE):
        average_color = calc.average_color_in_range(icon_im2, left, top,
                            left+DOT_AREA_ONE_SIDE, top+DOT_AREA_ONE_SIDE)
        if len(average_color) != 3:
            continue

        filename = similar_color_filename(average_color, color_data)
        # 距離最小のファイルを縮小して1600×1600の画像に貼り付け
        area_im = Image.open('image/euph_part_icon/'+filename)
        area_im.thumbnail((THUMBNAIL_ONE_SIDE, THUMBNAIL_ONE_SIDE))
        mosaic_icon_im.paste(area_im, (left//DOT_AREA_ONE_SIDE * THUMBNAIL_ONE_SIDE,
                                       top//DOT_AREA_ONE_SIDE * THUMBNAIL_ONE_SIDE))

save_path = 'product/slack_mosaic_mean.png'
mosaic_icon_im.save(save_path)

# Display Image ##### @@@@@ #####
#画像の読み込み
im = Image.open(save_path)
display_image(im)
In [ ]:
[今後] ソースコードにconvertを導入する

以下、モザイクアートのプログラム

In [3]:
import csv

import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

from mosaic_art import calc


#Jupyterでインライン表示するための宣言
%matplotlib inline

定数宣言

In [4]:
DOT_AREA_ONE_SIDE = 10
THUMBNAIL_ONE_SIDE = 40
# 2つの色(R,G,B)の間の最大の距離
MAX_COLOR_DISTANCE = 255**2 * 3
# CSVファイル中のカラムの意味づけ
POS_NAME  = 0
POS_RED   = 1
POS_GREEN = 2
POS_BLUE  = 3

関数定義

In [5]:
def materials_list_from_file(filename):
    """Returns a list which contains material image information.

    Args:
        filename: File name such as "foo.csv"
            The file contains information on average color of image.
            (Average color maens the average of the values of R of all pixels,
             the average of the values of G of all pixels,
             and the average of the values of B of all pixels)
            A row is as follows:
                image_name, R_average, G_average, B_average

    Returns:
        A list of tuples
        Tuple is like (
            image_name   : str (such as "bar.png"),
            red_average  : int,
            green_average: int,
            blue_average : int
            )
    """
    color_data = []
    with open(filename, 'r', newline='') as csvfile:
        reader = csv.reader(csvfile)
        for row in reader:
            image_info = (row[POS_NAME], int(row[POS_RED]),
                          int(row[POS_GREEN]), int(row[POS_BLUE]))
            color_data.append(image_info)
    return color_data
In [6]:
def color_distance(RGB1, RGB2):
    """Returns color distance

    Considering the distance between two points
    (x1, y1, z1) and (x2, y2, z2) in three dimensions

    Args:
        RGB1: A tuple which means (R, G, B)
        RGB2: A tuple which means (R, G, B)

    Returns:
        color distance(:int)

    """
    d2_r = (RGB1[0] - RGB2[0]) ** 2
    d2_g = (RGB1[1] - RGB2[1]) ** 2
    d2_b = (RGB1[2] - RGB2[2]) ** 2
    return d2_r + d2_g + d2_b
In [7]:
def similar_color_filename(average_color, color_data):
    """Returns name of file similar to average color

    Find the image with average color closest to `average_color` from `color_data`

    Args:
        average_color: a tuple which means (R, G, B) of average color of a certain range
        color_data: A list of tuples
                    Tuple is like (image_name, red_average, green_average, blue_average)

    Returns:
        A name of file such as 'foo.png' (NOT path)
    """
    distance = MAX_COLOR_DISTANCE
    filename = ''
    # 色の差が最小になるファイルを決定(距離に見立てている)
    for color in color_data:
        sample_color = (color[POS_RED], color[POS_GREEN], color[POS_BLUE])
        d = color_distance(average_color, sample_color)
        if d < distance:
            distance = d
            filename = color[POS_NAME]
    return filename
In [8]:
def display_image(image):
    #画像をarrayに変換
    im_list = np.asarray(image)
    #貼り付け
    plt.imshow(im_list)
    #表示
    plt.show()