1. 程式人生 > >使用python和opencv將圖片轉化為素描圖-python程式碼解析

使用python和opencv將圖片轉化為素描圖-python程式碼解析

實際上為了有效地創造黑白素描圖,你真正需要的是一些模糊和兩張圖片的混合技術,叫做dodging and burning.

用OpenCV、Python一張RGB顏色的影象經過下面四個步驟就能夠生成出一張素描圖:

  1. 將RGB圖轉化為灰度圖。
  2. 灰度圖進行反色操作。
  3. 將步驟1中的灰度影象和步驟三中的模糊反色影象混合,這裡就用到亮化(Dodging)和暗化(burning)的技術。

前三步使用Opencv都是直接可以做到的,我也看到其他的有些部落格在嘗試解決第四步的問題,因為Opencv內部不提供亮化和暗化的技術。但是,我們採取一些技巧的話,我們將會實現這個功能,最終看起來也會非常地簡單。

Step 1: 將影象轉化為灰度圖

This should be really easy to do even for an OpenCV novice. Images can be opened with cv2.imread and can be converted between color spaces with cv2.cvtColor. Alternatively, you can pass an additional argument to cv2.imread that specifies the color mode in which to open the image.

在Opencv中的話,這個功能將會非常簡單,影象的開啟可以通過cv2.imread

程式碼開啟,cv2.cvtColor可以將圖片轉化為灰度圖。你也可以在讀取圖片的時候增加一個額外的引數使得影象直接轉化為灰度圖:

import cv2
img_rgb = cv2.imread('example.jpg')
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
cv2.imshow('original', img_gray)
cv2.waitKey(0)

上面的程式碼是讀取圖片後,再通過呼叫cv2.cvtColor函式將圖片轉換成灰度圖,實際上我們可以直接在讀取圖片時候就直接轉換圖片,即:

img_gray = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

這裡呼叫cv2.imread函式時,設定了cv2.IMREAD_GRAYSCALE的標誌,表示載入灰度圖。在imread函式中是設定了三種標誌,分別是

  • cv2.IMREAD_COLOR : 預設使用該種標識。載入一張彩色圖片,忽視它的透明度。

  • cv2.IMREAD_GRAYSCALE : 載入一張灰度圖。

  • cv2.IMREAD_UNCHANGED : 載入影象,包括它的Alpha 通道(Alpha 表示圖片的透明度)。

另外,如果覺得以上標誌太長,可以簡單使用 1,0,-1 代替,效果是相同的。

Step 2: 灰度反色操作

灰度圖反色影象可以通過將灰度圖每個畫素點取反得到,由於灰度圖的畫素點的在0-255之間,將其取反的話就是255-當前畫素點。

img_gray_inv = 255 - img_gray

其實就是暗的地方變亮了,亮的地方變暗了。

Step 3: 高斯模糊

Gaussian blur能夠很有效地減少影象中的噪聲,能夠將影象變得更加平滑一點,在數學上等價於用高斯核來對影象進行卷積操作。我們可以通過cv2.GaussianBlur來實現高斯模糊操作,引數ksize表示高斯核的大小。sigmaX和sigmaY分別表示高斯核在 X 和 Y 方向上的標準差。

Step 4: 灰度圖與高斯模糊底片的融合

這一步驟自然就是需要得到最終的素描圖結果了。在傳統照相技術中,當需要對圖片某個區域變得更亮或者變暗,可以通過控制它的曝光時間,這裡就用到亮化(Dodging)和暗化(burning)的技術。

在現代影象編輯工具,比如 PS 可以實現上述說的兩種技術。比如對於顏色亮化技術,給定一張圖片 A 和 蒙版 B,那麼實現做法如下所示:

(B[idx] == 255)?B[idx]:min(255, ((A[idx] << 8) / (255-B[idx])))

This is essentially dividing the grayscale (or channel) value of an image pixel A[idx] by the inverse of the mask pixel value B[idx], while making sure that the resulting pixel value will be in the range [0,255] and that we do not divide by zero. We could translate this into a naïve Python function that accepts two OpenCV matrices (an image and a mask) and returns the blended mage:

import cv2
import numpy as np

def dodgeNaive(image, mask):
    # determine the shape of the input image
    width, height = image.shape[:2]

    # prepare output argument with same size as image
    blend = np.zeros((width, height), np.uint8)

    for col in range(width):
        for row in range(height):
            # do for every pixel
            if mask[col, row] == 255:
                # avoid division by zero
                blend[col, row] = 255
            else:
                # shift image pixel value by 8 bits
                # divide by the inverse of the mask
                tmp = (image[col, row] << 8) / (255 - mask)

                # make sure resulting value stays within bounds
                if tmp > 255:
                    tmp = 255
                    blend[col, row] = tmp

    return blend

上述程式碼雖然實現了這個功能,但是很明顯會非常耗時(我拿自己破電腦試了一下確實蠻久的,不建議跑),中間採用了一個兩層迴圈,計算複雜度是 O(w*h) ,也就是如果圖片的寬和高的乘積越大,耗時就越長,所以就有了升級版的程式碼版本:

def dodgeV2(image, mask):
    return cv2.divide(image, 255 - mask, scale=256)

另一種技術---暗化操作的程式碼如下所示:

def burnV2(image, mask):
    return 255 - cv2.divide(255 - image, 255 - mask, scale=256)

完整版程式碼如下所示:

import cv2
import numpy as np


def dodgeNaive(image, mask):
    # determine the shape of the input image
    width, height = image.shape[:2]

    # prepare output argument with same size as image
    blend = np.zeros((width, height), np.uint8)

    for col in range(width):
        for row in range(height):
            # do for every pixel
            if mask[col, row] == 255:
                # avoid division by zero
                blend[col, row] = 255
            else:
                # shift image pixel value by 8 bits
                # divide by the inverse of the mask
                tmp = (image[col, row] << 8) / (255 - mask)
                # print('tmp={}'.format(tmp.shape))
                # make sure resulting value stays within bounds
                if tmp.any() > 255:
                    tmp = 255
                    blend[col, row] = tmp

    return blend


def dodgeV2(image, mask):
    return cv2.divide(image, 255 - mask, scale=256)


def burnV2(image, mask):
    return 255 - cv2.divide(255 - image, 255 - mask, scale=256)


def rgb_to_sketch(src_image_name, dst_image_name):
    img_rgb = cv2.imread(src_image_name)
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
    # 讀取圖片時直接轉換操作
    # img_gray = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

    img_gray_inv = 255 - img_gray
    img_blur = cv2.GaussianBlur(img_gray_inv, ksize=(21, 21),
                                sigmaX=0, sigmaY=0)
    img_blend = dodgeV2(img_gray, img_blur)

    cv2.imshow('original', img_rgb)
    cv2.imshow('gray', img_gray)
    cv2.imshow('gray_inv', img_gray_inv)
    cv2.imshow('gray_blur', img_blur)
    cv2.imshow("pencil sketch", img_blend)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    cv2.imwrite(dst_image_name, img_blend)


if __name__ == '__main__':
    src_image_name = 'example.jpg'
    dst_image_name = 'sketch_example.jpg'
    rgb_to_sketch(src_image_name, dst_image_name)

上面是我盜的圖,嘻嘻,那個企鵝轉化出來的效果實在太差了。