天天看點

Python 制作馬賽克拼合圖像

Python 制作馬賽克拼合圖像

文章目錄

  • ​​Python 制作馬賽克拼合圖像​​
  • ​​知識點​​
  • ​​效果:​​
  • ​​環境​​
  • ​​原理​​
  • ​​RGB 色彩空間​​
  • ​​HSV 色彩空間​​
  • ​​RGB 與 HSV 色彩空間的轉換​​
  • ​​馬賽克圖檔拼合​​
  • ​​資料準備:​​
  • ​​導入需要的庫​​
  • ​​計算圖像平均 HSV 值​​
  • ​​生成素材資料庫​​
  • ​​生成馬賽克圖檔​​
  • ​​效果展示​​
  • ​​完整代碼:​​

200 行 Python 代碼完成生成馬賽克圖檔的腳本

知識點

  • 什麼是 RGB
  • HSV 色彩空間
  • Python 基礎
  • 包括 pillow 庫的使用
  • 多程序庫 multiprocessing 的使用
  • 制作馬賽克圖像的原理
效果:

可以生成類似下面的圖檔:

Python 制作馬賽克拼合圖像

放大上圖之後,可以看到上圖是由許多小的圖像組成的:

Python 制作馬賽克拼合圖像

環境

  • Python 3.5.2
  • numpy==1.18.1
  • Pillow==8.0.0

我們會使用 Python 中的 pillow(PIL)庫來處理圖像,使用 numpy 來進行一些數值計算。 我們首先使用下面的指令來安裝這兩個庫:

pip install numpy 
pip install pillow      

原理

一張圖像是通過許多的像素組成的。 為了生成馬賽克圖檔,我們的想法是,将原有圖像的每一個小部分,使用顔色與這一小部分相似的圖像進行替換,進而生成馬賽克風格的圖像。

下面是整個結構(圖檔可能會有點小,可以點選檢視大圖),我們會依次進行介紹:

Python 制作馬賽克拼合圖像

在接下來的中,我們會:

  • 首先介紹 RGB 與 HSV 色彩空間,并介紹如何從 RGB 色彩空間轉換到 HSV 色彩空間。
  • 接着我們會介紹馬賽克圖檔拼合的實驗步驟,主要包含對素材圖像的處理,生成圖像資料庫;
  • 最後,我們使用處理之後的素材圖像,生成馬賽克圖像。

RGB 色彩空間

RGB 色彩空間是,由三個通道表示一幅圖像。三個通道分别為紅色®,綠色(G)和藍色(B)。 RGB 色彩空間由紅綠藍三原色的色度定義,借此可以定義出相應的色三角,生成其它顔色。 通過這三種顔色的不同組合,可以形成幾乎所有的其他顔色。 最常見的 RGB 空間是 sRGB。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-WSxWTo1u-1636963261977)(https://doc.shiyanlou.com/courses/3007/246442/43c98ec982b69be0a54c798540467cde-1)]

但是,在自然環境下,圖像容易受自然光照、遮擋等情況的影響。 也就是人眼觀察圖檔會對圖檔的亮度比較敏感。 而 RGB 色彩空間的三個分量都與亮度密切相關,隻要亮度改變,RGB 顔色的三個分量都會改變.

同時,由于人眼對于這 RGB 這三種顔色的敏感程度是不一樣的。 在單色中,人眼對紅色最不敏感,藍色最敏感,是以 RGB 色彩空間是一種均勻性較差的色彩空間。

如果在 RGB 的色彩空間上,我們直接用歐氏距離來度量衡量顔色的相似度,他的結果與人眼視覺會有較大的偏差。

HSV 色彩空間

由于上面講到的 RGB 色彩空間不能友善的比較顔色之間的相似度,于是在處理這一類問題的時候我們更多的是使用 HSV 色彩空間。 HSV 色彩空間也是由三個分量組成的,分别是:

  • Hue(色調)
  • Saturation (飽和度)
  • Value (明度)

我們會常用下圖的圓柱體來表示 HSV 色彩空間,其中:

  • H 用極坐标的極角表示;
  • S 用極坐标的軸的長度表示;
  • V 用圓柱的高度表示;
Python 制作馬賽克拼合圖像

在 RGB 色彩空間中,顔色由三個共同的值決定。 例如黃色對應的 RGB 值為 ​

​(255, 255, 0)​

​​。 但是在 HSV 色彩空間中,黃色隻有 H(Hue)決定,即 ​

​Hue=60​

​ 即可。 下圖為在 HSV 空間中,黃色的表示:

Python 制作馬賽克拼合圖像

在确定了 H(Hue)之後,我們可以更改 Saturation 和 Value。

  • Saturation(飽和度):飽和度表示顔色接近光譜色的程度。飽和度越高,說明顔色越深,越接近光譜色;飽和度越低,說明顔色越淺,越接近白色。
  • Value(明度):明度決定色彩空間中顔色的明暗程度。明度越高,表示顔色越明亮。明度為 0 的時候,為全黑。下圖為明度為 0 的時候,最後顔色是黑色。
Python 制作馬賽克拼合圖像

RGB 與 HSV 色彩空間的轉換

接下來我們介紹一下如何将顔色從 RGB 色彩空間轉換為 HSV 色彩空間。 首先我們定義max = \max(R, G, B)max=max(R,G,B),min = \min(R, G, B)min=min(R,G,B)。 接下來分别計算 H、S、V 的值。 其中 V 的計算式子如下所示:

V = maxV=max

S 的計算式子如下所示:

S = \begin{cases} & \frac{max-min}{max} ,& if \ V \neq 0 \ & 0 ,& if \ V = 0 \end{cases}S={maxma**x−min,0,i**f V\=0i**f V=0

H 的計算式子如下所示:

h = \begin{cases} & 60° \times (0 + \frac{G-B}{max-min}) , & if \ max = R \ & 60° \times (2 + \frac{B-R}{max-min}) , & if \ max = G \ & 60° \times (4 + \frac{R-G}{max-min}) , & if \ max = B \end{cases}h=⎩⎪⎪⎨⎪⎪⎧60°×(0+max−min**G−B),60°×(2+max−min**B−R),60°×(4+max−min**R−G),i**f max=Rif max=Gif max=B

我們不需要自己寫轉換的函數,可以直接使用 ​

​colorsys​

​​ 這個庫。 其中包含兩個函數,分别是 ​

​rgb_to_hsv​

​​ 和 ​

​hsv_to_rgb​

​。 分别是将顔色從 RGB 空間轉換到 HSV 色彩空間,和将顔色從 HSV 色彩空間轉換為 RGB 色彩空間。 我們下面用黃色做一個小的轉換測試。

我們首先将黃色從 RGB 色彩空間轉換為 HSV 色彩空間。 我們需要注意的是,這裡 ​

​rgb_to_hsv​

​ 需要将 RGB 值轉換為 0 到 1 之間,是以我們除以 255。

Python 制作馬賽克拼合圖像

上面結果中的 0.1666 就是\frac{60}{360}36060。 接着我們将其從 HSV 色彩空間轉換為 RGB 色彩空間。

Python 制作馬賽克拼合圖像

馬賽克圖檔拼合

分别是:

  • 生成圖像素材資料庫;
  • 對原始圖像每一小塊進行分析,與圖像資料庫進行比較,找出最接近的圖檔進行替換;

于是我們将上面的功能寫成兩個類,分别是 ​

​mosaic.create_image_db​

​​,用來生成素材資料庫;​

​mosaic.create_mosaic​

​​ 用來生成馬賽克圖像。 這兩個類都繼承自基類,​

​mosaic.mosaic​

​​。這個基類裡有兩個方法,分别是實作調整圖檔的大小(​

​resize_pic​

​​)和計算圖像的評價 HSV 值(​

​get_avg_color​

​)。 下面我們會依次對這三個類進行介紹。

Python 制作馬賽克拼合圖像
資料準備:
https://labfile.oss.aliyuncs.com/courses/3007/mosaic_images.zip      

導入需要的庫

我們首先導入必要的庫。 在檔案中,我們會使用 Python 中的 pillow(PIL)庫來處理圖像,使用 numpy 來進行一些數值計算。 我們在 ​

​mosaic.py​

​ 檔案中先引入需要的庫。

import os
import sys
import time
import math
import numpy as np
from PIL import Image, ImageOps
from multiprocessing import Pool
from colorsys import rgb_to_hsv, hsv_to_rgb
from typing import List, Tuple, Union      

計算圖像平均 HSV 值

上面我們提到,通過 HSV 色彩空間來比較圖檔顔色的相似度是比較好的。 是以這裡我們希望實作這樣的一個功能:輸入一個圖檔,傳回這個圖檔的平均 HSV 值。

我們的想法是周遊這個圖像的每一個像素點,獲得每一個像素點的 RGB 值。 接着通過上面介紹的函數 ​

​rgb_to_hsv​

​ ,将 RGB 值轉換為 HSV 值。 最後分别求 H(Hue)、S(Saturation)和 V(Saturation)的平均值。

因為之後我們需要對素材圖檔和待轉換的圖檔都求平均 HSV 值,是以我們建立一個父類 ​

​mosaic​

​​,裡面包含一個計算圖像平均 HSV 值的方法,之後可以繼承這個類。 同時,因為之後會用到圖像大小的轉換,是以我們也在這個類裡定義一個圖像 ​

​resize​

​ 的方法。

class mosaic(object):
    """定義計算圖檔的平均hsv值
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        self.IN_DIR = IN_DIR  # 原始的圖像素材所在檔案夾
        self.OUT_DIR = OUT_DIR  # 輸出素材的檔案夾, 這些都是計算過hsv和經過resize之後的圖像
        self.SLICE_SIZE = SLICE_SIZE  # 圖像放縮後的大小
        self.REPATE = REPATE  # 同一張圖檔可以重複使用的次數
        self.OUT_SIZE = OUT_SIZE  # 最終圖檔輸出的大小

    def resize_pic(self, in_name: str, size: int) -> Image:
        """轉換圖像大小
        """
        img = Image.open(in_name)
        img = ImageOps.fit(img, (size, size), Image.ANTIALIAS)
        return img

    def get_avg_color(self, img: Image) -> Tuple[float, float, float]:
        """計算圖像的平均hsv
        """
        width, height = img.size
        pixels = img.load()
        if type(pixels) is not int:
            data = []  # 存儲圖像像素的值
            for x in range(width):
                for y in range(height):
                    cpixel = pixels[x, y]  # 獲得每一個像素的值
                    data.append(cpixel)
            h = 0
            s = 0
            v = 0
            count = 0
            for x in range(len(data)):
                r = data[x][0]
                g = data[x][1]
                b = data[x][2]  # 得到一個點的GRB三色
                count += 1
                hsv = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
                h += hsv[0]
                s += hsv[1]
                v += hsv[2]

            hAvg = round(h / count, 3)
            sAvg = round(s / count, 3)
            vAvg = round(v / count, 3)

            if count > 0:  # 像素點的個數大于0
                return (hAvg, sAvg, vAvg)
            else:
                raise IOError("讀取圖檔資料失敗")
        else:
            raise IOError("PIL 讀取圖檔資料失敗")      

生成素材資料庫

之前我們把我們的素材圖檔都下載下傳并解壓到了 ​

​images​

​ 檔案夾内。 但由于我們準備的圖檔可能在大小上不同。 于是,為了友善之後圖檔的生成,我們先對原始素材圖檔進行一次處理,主要包含兩個部分:

  • 将原始素材圖檔轉換為統一的格式,這裡使用在上面類内定義的 ​

    ​resize_pic​

    ​ 方法完成;
  • 計算圖檔的平均 HSV 值,并将其作為新的檔案名進行儲存;

于是,我們周遊整個素材檔案夾,對其中的每一張圖檔進行大小的轉換和計算平均 HSV 值。 并将新的圖檔儲存在檔案夾 ​

​OUT_DIR​

​​ 内。 這裡我們會使用多程序,使用 ​

​multiprocessing​

​ 來完成多程序的使用。 這一部分完整的類如下所示:

class create_image_db(mosaic):
    """建立所需要的資料
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        super(create_image_db, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE,
                                              REPATE, OUT_SIZE)

    def make_dir(self) -> None:
        os.makedirs(os.path.dirname(self.OUT_DIR), exist_ok=True) # 沒有就建立檔案夾

    def get_image_paths(self) -> List[str]:
        """擷取檔案夾内圖像的位址
        """
        paths = []
        suffixs = ['png', 'jpg']
        for file_ in os.listdir(self.IN_DIR):
            suffix = file_.split('.', 1)[1]  # 獲得檔案字尾
            if suffix in suffixs:  # 通過字尾判斷是否是圖檔
                paths.append(self.IN_DIR + file_)  # 添加圖像路徑
            else:
                print("非圖檔:%s" % file_)
        if len(paths) > 0:
            print("一共找到了%s" % len(paths) + "張圖檔")
        else:
            raise IOError("未找到任何圖檔")

        return paths

    def convert_image(self, path):
        """轉換圖像大小, 同時計算一個圖像的平均hsv值.
        """
        img = self.resize_pic(path, self.SLICE_SIZE)
        color = self.get_avg_color(img)
        img.save(str(self.OUT_DIR) + str(color) + ".png")

    def convert_all_images(self) -> None:
        """将所有圖像進行轉換
        """
        self.make_dir()
        paths = self.get_image_paths()
        print("正在生成馬賽克塊...")
        pool = Pool()  # 多程序處理
        pool.map(self.convert_image, paths)  # 對已有的圖像進行處理, 轉換為對應的色塊
        pool.close()
        pool.join()      

之後運作這一部分代碼之後,會在目前檔案夾下生成一個 ​

​outputImages​

​​ 的檔案夾。 這裡是我們經過處理之後的圖像,所有圖像的大小是一樣的,同時圖檔的名稱改為了這個圖檔的平均 HSV 值。 下面是之後運作 ​

​mosaic.py​

​ 會生成的檔案夾,和其中的圖檔。

Python 制作馬賽克拼合圖像

暫時我們在這裡先不運作,先繼續往後面編寫,完成生成馬賽克圖檔的類。 之後會運作 ​

​mosaic.py​

​ 檔案,我們可以再進行檢視。

生成馬賽克圖檔

在有了處理好的素材照片之後,下面我們就可以開始生成馬賽克圖檔了。 這裡的整個流程流程為:

  • 首先周遊我們生成的素材檔案夾,獲得裡面所有圖檔的平均 HSV 值,儲存在一個 list 中;
  • 接着我們将原始圖檔分為一小塊一小塊,每一個小塊會計算他的平均 HSV 值;
  • 接着我們在上面生成素材的平均 HSV 值的 list 中,找到與這個小塊的平均 HSV 值最接近的那張圖檔,并用那張圖檔來替換這個小塊;
  • 依次對整個圖形進行這樣的操作,這樣就可以使用素材圖像生成一個圖像;
  • 最後可以選擇将生成的圖像與原始圖像重疊,使用 ​

    ​Image.blend​

    ​ 完成,這一步是可以選擇的;

我們将上面的步驟寫在一個類裡面,下面是完整的代碼。

class create_mosaic(mosaic):
    """建立馬賽克圖檔
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        super(create_mosaic, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE, REPATE,
                                           OUT_SIZE)

    def read_img_db(self) -> List[List[Union[float, int]]]:
        """讀取所有的圖檔
        """
        img_db = []  # 存儲color_list
        for file_ in os.listdir(self.OUT_DIR):
            if file_ == 'None.png':
                pass
            else:
                file_ = file_.split('.png')[0]  # 獲得檔案名
                file_ = file_[1:-1].split(',')  # 獲得hsv三個值
                file_ = [float(i) for i in file_]
                file_.append(0)  # 最後一位計算圖像使用次數
                img_db.append(file_)
        return img_db

    def find_closiest(self, color: Tuple[float, float, float],
                      list_colors: List[List[Union[float, int]]]) -> str:
        """尋找與像素塊顔色最接近的圖像
        """
        FAR = 10000000
        for cur_color in list_colors:  # list_color是圖像庫中是以圖像的平均hsv顔色
            n_diff = np.sum((color - np.absolute(cur_color[:3]))**2)
            if cur_color[3] <= self.REPATE:  # 同一個圖檔使用次數不能太多
                if n_diff < FAR:  # 修改最接近的顔色
                    FAR = n_diff
                    cur_closer = cur_color
        cur_closer[3] += 1
        return "({}, {}, {})".format(cur_closer[0], cur_closer[1],
                                     cur_closer[2])  # 傳回hsv顔色

    def make_puzzle(self, img: str) -> bool:
        """制作拼圖
        """
        img = self.resize_pic(img, self.OUT_SIZE)  # 讀取圖檔并修改大小
        color_list = self.read_img_db()  # 擷取所有的顔色的list

        width, height = img.size  # 獲得圖檔的寬度和高度
        print("Width = {}, Height = {}".format(width, height))
        background = Image.new('RGB', img.size,
                               (255, 255, 255))  # 建立一個空白的背景, 之後向裡面填充圖檔
        total_images = math.floor(
            (width * height) / (self.SLICE_SIZE * self.SLICE_SIZE))  # 需要多少小圖檔
        now_images = 0  # 用來計算完成度
        for y1 in range(0, height, self.SLICE_SIZE):
            for x1 in range(0, width, self.SLICE_SIZE):
                try:
                    # 計算目前位置
                    y2 = y1 + self.SLICE_SIZE
                    x2 = x1 + self.SLICE_SIZE
                    # 截取圖像的一小塊, 并計算平均hsv
                    new_img = img.crop((x1, y1, x2, y2))
                    color = self.get_avg_color(new_img)
                    # 找到最相似顔色的照片
                    close_img_name = self.find_closiest(color, color_list)
                    close_img_name = self.OUT_DIR + str(
                        close_img_name) + '.png'  # 圖檔的位址
                    paste_img = Image.open(close_img_name)
                    # 計算完成度
                    now_images += 1
                    now_done = math.floor((now_images / total_images) * 100)
                    r = '\r[{}{}]{}%'.format("#" * now_done,
                                             " " * (100 - now_done), now_done)
                    sys.stdout.write(r)
                    sys.stdout.flush()
                    background.paste(paste_img, (x1, y1))
                except IOError:
                    print('建立馬賽克塊失敗')
        # 保持最後的結果
        background.save('out_without_background.jpg')
        img = Image.blend(background, img, 0.5)
        img.save('out_with_background.jpg')
        return True      

這裡的參數 ​

​REPATE​

​​ 表示每一張圖檔可以最多重複的次數。 如果我們圖檔足夠多,可以設定為 ​

​REPATE=1​

​,此時每一張圖檔隻能使用一次。

效果展示

上面我們完成了代碼的主體架構,下面我們運作一下看一下結果。 首先我們下載下傳使用的測試圖檔:

https://labfile.oss.aliyuncs.com/courses/3007/Zelda.jpg      

也是塞爾達中的一張圖檔,如下圖所示:

Python 制作馬賽克拼合圖像

接着我們編寫​

​main​

​函數:

if __name__ == "__main__":
    filePath = os.path.dirname(os.path.abspath(__file__))  # 擷取目前的路徑
    start_time = time.time()  # 程式開始運作時間, 記錄一共運作了多久
    # 建立馬賽克塊, 建立素材庫
    createdb = create_image_db(IN_DIR=os.path.join(filePath, 'images/'),
                               OUT_DIR=os.path.join(filePath, 'outputImages/'),
                               SLICE_SIZE=100,
                               REPATE=20,
                               OUT_SIZE=5000)
    createdb.convert_all_images()
    # 建立拼圖 (這裡使用絕對路徑)
    createM = create_mosaic(IN_DIR=os.path.join(filePath, 'images/'),
                           OUT_DIR=os.path.join(filePath, 'outputImages/'),
                           SLICE_SIZE=100,
                           REPATE=20,
                           OUT_SIZE=5000)
    out = createM.make_puzzle(img=os.path.join(filePath, 'Zelda.jpg'))
    # 列印時間
    print("耗時: %s" % (time.time() - start_time))
    print("已完成")      

接着我們儲存并關閉剛剛書寫的檔案,​

​mosaic.py​

​。 為了更好的觀察圖檔的效果,我們需要安裝一款檢視圖檔的軟體,Eye of GNOME(ego)。

運作mosaic.py檔案

這裡大概會等待 2-3 分鐘左右。 運作過程大緻如下圖所示:

Python 制作馬賽克拼合圖像

運作結束之後,會在我們目前目錄下生成兩張圖檔,一張是沒有和原始圖檔進行融合的,檔案名為​

​'out_without_background.jpg'​

​。 我們使用 eog 來檢視圖像:

沒有與原始圖檔融合,此時效果如下圖所示:

Python 制作馬賽克拼合圖像

同樣我們也可以檢視與原始圖檔融合之後的圖檔的效果。

最終的效果如下圖所示:

Python 制作馬賽克拼合圖像
完整代碼:
import os
import sys
import time
import math
import numpy as np
from PIL import Image, ImageOps
from multiprocessing import Pool
from colorsys import rgb_to_hsv, hsv_to_rgb
from typing import List, Tuple, Union


class mosaic(object):
    """定義計算圖檔的平均hsv值
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        self.IN_DIR = IN_DIR  # 原始的圖像素材所在檔案夾
        self.OUT_DIR = OUT_DIR  # 輸出素材的檔案夾, 這些都是計算過hsv和經過resize之後的圖像
        self.SLICE_SIZE = SLICE_SIZE  # 圖像放縮後的大小
        self.REPATE = REPATE  # 同一張圖檔可以重複使用的次數
        self.OUT_SIZE = OUT_SIZE  # 最終圖檔輸出的大小

    def resize_pic(self, in_name: str, size: int) -> Image:
        """轉換圖像大小
        """
        img = Image.open(in_name)
        img = ImageOps.fit(img, (size, size), Image.ANTIALIAS)
        return img

    def get_avg_color(self, img: Image) -> Tuple[float, float, float]:
        """計算圖像的平均hsv
        """
        width, height = img.size
        pixels = img.load()
        if type(pixels) is not int:
            data = []  # 存儲圖像像素的值
            for x in range(width):
                for y in range(height):
                    cpixel = pixels[x, y]  # 獲得每一個像素的值
                    data.append(cpixel)
            h = 0
            s = 0
            v = 0
            count = 0
            for x in range(len(data)):
                r = data[x][0]
                g = data[x][1]
                b = data[x][2]  # 得到一個點的GRB三色
                count += 1
                hsv = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
                h += hsv[0]
                s += hsv[1]
                v += hsv[2]

            hAvg = round(h / count, 3)
            sAvg = round(s / count, 3)
            vAvg = round(v / count, 3)

            if count > 0:  # 像素點的個數大于0
                return (hAvg, sAvg, vAvg)
            else:
                raise IOError("讀取圖檔資料失敗")
        else:
            raise IOError("PIL 讀取圖檔資料失敗")


class create_image_db(mosaic):
    """建立所需要的資料
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        super(create_image_db, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE,
                                              REPATE, OUT_SIZE)

    def make_dir(self) -> None:
        os.makedirs(os.path.dirname(self.OUT_DIR), exist_ok=True) # 沒有就建立檔案夾

    def get_image_paths(self) -> List[str]:
        """擷取檔案夾内圖像的位址
        """
        paths = []
        suffixs = ['png', 'jpg']
        for file_ in os.listdir(self.IN_DIR):
            suffix = file_.split('.', 1)[1]  # 獲得檔案字尾
            if suffix in suffixs:  # 通過字尾判斷是否是圖檔
                paths.append(self.IN_DIR + file_)  # 添加圖像路徑
            else:
                print("非圖檔:%s" % file_)
        if len(paths) > 0:
            print("一共找到了%s" % len(paths) + "張圖檔")
        else:
            raise IOError("未找到任何圖檔")

        return paths

    def convert_image(self, path):
        """轉換圖像大小, 同時計算一個圖像的平均hsv值.
        """
        img = self.resize_pic(path, self.SLICE_SIZE)
        color = self.get_avg_color(img)
        img.save(str(self.OUT_DIR) + str(color) + ".png")

    def convert_all_images(self) -> None:
        """将所有圖像進行轉換
        """
        self.make_dir()
        paths = self.get_image_paths()
        print("正在生成馬賽克塊...")
        pool = Pool()  # 多程序處理
        pool.map(self.convert_image, paths)  # 對已有的圖像進行處理, 轉換為對應的色塊
        pool.close()
        pool.join()


class create_mosaic(mosaic):
    """建立馬賽克圖檔
    """
    def __init__(self, IN_DIR: str, OUT_DIR: str, SLICE_SIZE: int, REPATE: int,
                 OUT_SIZE: int) -> None:
        super(create_mosaic, self).__init__(IN_DIR, OUT_DIR, SLICE_SIZE, REPATE,
                                           OUT_SIZE)

    def read_img_db(self) -> List[List[Union[float, int]]]:
        """讀取所有的圖檔
        """
        img_db = []  # 存儲color_list
        for file_ in os.listdir(self.OUT_DIR):
            if file_ == 'None.png':
                pass
            else:
                file_ = file_.split('.png')[0]  # 獲得檔案名
                file_ = file_[1:-1].split(',')  # 獲得hsv三個值
                file_ = [float(i) for i in file_]
                file_.append(0)  # 最後一位計算圖像使用次數
                img_db.append(file_)
        return img_db

    def find_closiest(self, color: Tuple[float, float, float],
                      list_colors: List[List[Union[float, int]]]) -> str:
        """尋找與像素塊顔色最接近的圖像
        """
        FAR = 10000000
        for cur_color in list_colors:  # list_color是圖像庫中是以圖像的平均hsv顔色
            n_diff = np.sum((color - np.absolute(cur_color[:3]))**2)
            if cur_color[3] <= self.REPATE:  # 同一個圖檔使用次數不能太多
                if n_diff < FAR:  # 修改最接近的顔色
                    FAR = n_diff
                    cur_closer = cur_color
        cur_closer[3] += 1
        return "({}, {}, {})".format(cur_closer[0], cur_closer[1],
                                     cur_closer[2])  # 傳回hsv顔色

    def make_puzzle(self, img: str) -> bool:
        """制作拼圖
        """
        img = self.resize_pic(img, self.OUT_SIZE)  # 讀取圖檔并修改大小
        color_list = self.read_img_db()  # 擷取所有的顔色的list

        width, height = img.size  # 獲得圖檔的寬度和高度
        print("Width = {}, Height = {}".format(width, height))
        background = Image.new('RGB', img.size,
                               (255, 255, 255))  # 建立一個空白的背景, 之後向裡面填充圖檔
        total_images = math.floor(
            (width * height) / (self.SLICE_SIZE * self.SLICE_SIZE))  # 需要多少小圖檔
        now_images = 0  # 用來計算完成度
        for y1 in range(0, height, self.SLICE_SIZE):
            for x1 in range(0, width, self.SLICE_SIZE):
                try:
                    # 計算目前位置
                    y2 = y1 + self.SLICE_SIZE
                    x2 = x1 + self.SLICE_SIZE
                    # 截取圖像的一小塊, 并計算平均hsv
                    new_img = img.crop((x1, y1, x2, y2))
                    color = self.get_avg_color(new_img)
                    # 找到最相似顔色的照片
                    close_img_name = self.find_closiest(color, color_list)
                    close_img_name = self.OUT_DIR + str(
                        close_img_name) + '.png'  # 圖檔的位址
                    paste_img = Image.open(close_img_name)
                    # 計算完成度
                    now_images += 1
                    now_done = math.floor((now_images / total_images) * 100)
                    r = '\r[{}{}]{}%'.format("#" * now_done,
                                             " " * (100 - now_done), now_done)
                    sys.stdout.write(r)
                    sys.stdout.flush()
                    background.paste(paste_img, (x1, y1))
                except IOError:
                    print('建立馬賽克塊失敗')
        # 保持最後的結果
        background.save('out_without_background.jpg')
        img = Image.blend(background, img, 0.5)
        img.save('out_with_background.jpg')
        return True


if __name__ == "__main__":
    filePath = os.path.dirname(os.path.abspath(__file__))  # 擷取目前的路徑
    start_time = time.time()  # 程式開始運作時間, 記錄一共運作了多久
    # 建立馬賽克塊, 建立素材庫
    createdb = create_image_db(IN_DIR=os.path.join(filePath, 'images/'),
                               OUT_DIR=os.path.join(filePath, 'outputImages/'),
                               SLICE_SIZE=100,
                               REPATE=20,
                               OUT_SIZE=5000)
    createdb.convert_all_images()
    # 建立拼圖 (這裡使用絕對路徑)
    createM = create_mosaic(IN_DIR=os.path.join(filePath, 'images/'),
                           OUT_DIR=os.path.join(filePath, 'outputImages/'),
                           SLICE_SIZE=100,
                           REPATE=20,
                           OUT_SIZE=5000)
    out = createM.make_puzzle(img=os.path.join(filePath, 'Zelda.jpg'))
    # 列印時間
    print("耗時: %s" % (time.time() - start_time))
    print("已完成")