深入學習OpenCV檢測及分割影象的目標區域

戰爭熱誠發表於2019-07-27

準備1:OpenCV常用圖片轉換技巧

  在進行計算機視覺模型訓練前,我們經常會用到影象增強的技巧來獲取更多的樣本,但是有些深度學習框架中的方法對影象的變換方式可能並不滿足我們的需求,所以掌握OpenCV中一些常用的影象處理技巧對我們還是有很多幫助的。

影象通道分離

  我們知道每個影象是由RGB三個顏色通道構成,所以我們可以使用split函式對原影象的三個通道進行分離:

B, G, R = cv2.split(img)

  進行通道分離的後,我們就可以在每個通道上獨立的進行數值變換,變換完成後再來組合來生成新的影象,比如提升影象的亮度:

B,G,R = cv2.split(img)
for i in (B,G,R):
    randint = random.randint(50,100)
    limit = 255-randint
    i[i>limit]=255
    i[i<=limit]=randint+i[i<=limit]
img_merge = cv2.merge((B,G,R))
cv2.imshow("img_merge",img_merge)
key = cv2.waitKey()
if key==27:
    cv2.destroyAllWindows()

  

影象旋轉

   還可以使用 warpAffine 函式根據我們的設定的角度完成影象的旋轉:

M = cv2.getRotationMatrix2D((img.shape[1] / 2, img.shape[0] / 2), 30, 1)
img_rotate = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))

cv2.imshow('img_rotate', img_rotate)
key = cv2.waitKey(0)
if key == 27:
    cv2.destroyAllWindows()

  這裡我們不對影象進行縮放,旋轉角度為30度。

仿射變換

  仿射變換允許影象傾斜並且可以在任意兩個方向上發生伸縮。程式碼如下:

def random_warp(img, row, col):
    height, width, channels = img.shape

    random_margin = 100
    x1 = random.randint(-random_margin, random_margin)
    y1 = random.randint(-random_margin, random_margin)
    x2 = random.randint(width - random_margin - 1, width - 1)
    y2 = random.randint(-random_margin, random_margin)
    x3 = random.randint(width - random_margin - 1, width - 1)
    y3 = random.randint(height - random_margin - 1, height - 1)
    x4 = random.randint(-random_margin, random_margin)
    y4 = random.randint(height - random_margin - 1, height - 1)

    dx1 = random.randint(-random_margin, random_margin)
    dy1 = random.randint(-random_margin, random_margin)
    dx2 = random.randint(width - random_margin - 1, width - 1)
    dy2 = random.randint(-random_margin, random_margin)
    dx3 = random.randint(width - random_margin - 1, width - 1)
    dy3 = random.randint(height - random_margin - 1, height - 1)
    dx4 = random.randint(-random_margin, random_margin)
    dy4 = random.randint(height - random_margin - 1, height - 1)

    pts1 = np.float32([[x1, y1], [x2, y2], [x3, y3], [x4, y4]])
    pts2 = np.float32([[dx1, dy1], [dx2, dy2], [dx3, dy3], [dx4, dy4]])
    
    
    M_warp = cv2.getPerspectiveTransform(pts1, pts2)
    img_warp = cv2.warpPerspective(img, M_warp, (width, height))
    return img_warp

img_warp = random_warp(img, img.shape[0], img.shape[1])

cv2.imshow('img_warp', img_warp)
key = cv2.waitKey(0)
if key == 27:
    cv2.destroyAllWindows()

  

伽馬修正

  伽馬修正提升影象的對比度,讓影象看起來更加的“明亮”。程式碼如下:

def adjust_gamma(image, gamma=1.0):
    invGamma = 1.0/gamma
    table = []
    for i in range(256):
        table.append(((i / 255.0) ** invGamma) * 255)
    table = np.array(table).astype("uint8")
    return cv2.LUT(image, table)

img_gamma = adjust_gamma(img, 2)
cv2.imshow("img",img)
cv2.imshow("img_gamma",img_gamma)

key = cv2.waitKey()
if key == 27:
    cv2.destroyAllWindows()

  

準備2:下載並安裝cv2

  下載輪子如下:

  然後直接使用pip 命令即可:

pip  install  opencv_python-3.4.3-cp37-cp37m-win_amd64.whl

  注意:現在OpenCV for Python 就是通過Numpy 進行繫結的。所以在使用時必須掌握一些Numpy的相關知識!影象就是一個矩陣,在OpenCV for Python 中,影象就是Numpy中的陣列!

1,影象的載入,顯示和儲存

  如果讀取影象,只需要imread即可。

import cv2

# 獲取圖片
img_path = r'1.jpg'
img = cv2.imread(img_path)

  OpenCV目前支援讀取bmp,jpg,png,tiff等常用格式。

  我們還可以檢視影象的一些基本屬性:

print(img)
print(img.dtype)
print(img.shape)

  

  接著建立一個視窗

cv2.namedWindow("Image")

  然後在視窗中顯示影象

cv2.imshow('Image', img)

  最後還要新增一句:

cv2.waitKey(0)

  如果不新增最後一句,在IDLE中執行視窗直接無響應。在命令列中執行的話,則是一閃而過。

  儲存影象很簡單,直接cv.imwrite即可。

cv2.imwrite(save_path, crop_img)

  第一個引數是儲存的路徑及檔名,第二個是影象矩陣。其中,imwrite()有個可選的第三個引數,如下:

cv2.imwrite("cat.jpg", img,[int(cv2.IMWRITE_JPEG_QUALITY), 5])

  第三個引數針對特定的格式: 對於JPEG,其表示的是影象的質量,用0-100的整數表示,預設為95。 注意,cv2.IMWRITE_JPEG_QUALITY型別為Long,必須轉換成int。下面是以不同質量儲存的兩幅圖:

  對於PNG,第三個參數列示的是壓縮級別。cv2.IMWRITE_PNG_COMPRESSION,從0到9,壓縮級別越高,影象尺寸越小。預設級別為3:

cv2.imwrite("./cat.png", img, [int(cv2.IMWRITE_PNG_COMPRESSION), 0])   

cv2.imwrite("./cat2.png", img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])  

  儲存的影象尺寸如下:

  還有一種支援的影象,一般不常用。

  完整的程式為:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2

# 獲取圖片
img_path = r'1.jpg'
img = cv2.imread(img_path)
cv2.namedWindow("Image")
cv2.imshow('Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

  最後釋放視窗是個好習慣!,檢視圖片效果如下:

 

 2,轉換灰度並去噪聲

  我們可以得到兩張圖片,第一張是灰度圖,第二章是去噪之後的。去噪有很多種方法,均值濾波法,高斯濾波法,中值濾波器,雙邊濾波器等。這裡展示灰度化,高斯去噪的程式碼:

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

blurred = cv2.GaussianBlur(gray, (9, 9),0)

  下圖展示效果,(這裡取高斯是因為高斯去噪效果是最好的)

 

3,提取影象的梯度

  用Sobel運算元計算x,y方向上的梯度,之後在x方向上 minus y方向上的梯度,通過這個操作,會留下具有高水平梯度和低垂直梯度的影象區域。

  程式碼入下:

# 提取影象的梯度
gradX = cv2.Sobel(gray, ddepth=cv2.CV_32F, dx=1, dy=0)
gradY = cv2.Sobel(gray, ddepth=cv2.CV_32F, dx=0, dy=1)

gradient = cv2.subtract(gradX, gradY)
gradient = cv2.convertScaleAbs(gradient)

  此時我們會得到如下的影象:

 

4,繼續去噪聲

  考慮到影象的孔隙,首先使用低通濾波器平滑影象,這將有助於平滑影象中的高頻噪聲。低通濾波器的目的是降低影象的變化率。

  如果將每個畫素替換為該影象周圍畫素的均值,這樣就可以平滑並替代那些強度變化明顯的區域。

  對模糊影象二值化,顧名思義就是把影象數值以某一邊界分成兩種數值:

blurred = cv2.GaussianBlur(gradient, (9, 9), 0)

(_, thresh) = cv2.threshold(blurred, 90, 255, cv2.THRESH_BINARY)

  此時效果如下:

  其實就算手動分割,我們也是需要找到一個邊界,可以看出輪廓出來了,但是我們最終要的是整個輪廓,所以內部小區域就不要了。

5,影象形態學

   在這裡我們選取ELLIPSE核,採用CLOSE操作。

# 影象形態學
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25))

closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)

  此時,效果如下:

6,細節刻畫

   從上圖我們可以發現和原圖對比,發現有細節丟失,這會干擾之後的昆蟲輪廓的檢測,要把它們擴充,分別執行4次形態學腐蝕與膨脹,程式碼如下:

# 細節刻畫,分別執行四次形態學腐蝕與膨脹
closed = cv2.erode(closed, None, iterations=4)

closed = cv2.dilate(closed, None, iterations=4)

  效果如下:

 7,找出昆蟲區域的輪廓,並畫出

  此時用 cv2.findContours() 函式如下:

(_, cnts, _) = cv2.findContours(
   引數一: 二值化影象
   closed.copy(),
   引數二:輪廓型別
   #表示只檢測外輪廓
   # cv2.RETR_EXTERNAL,    
   #建立兩個等級的輪廓,上一層是邊界        
   # cv2.RETR_CCOMP,    
   #檢測的輪廓不建立等級關係            
   # cv2.RETR_LIST,          
   #建立一個等級樹結構的輪廓      
   # cv2.RETR_TREE,    
   #儲存所有的輪廓點,相鄰的兩個點的畫素位置差不超過1             
   # cv2.CHAIN_APPROX_NONE,         
   引數三:處理近似方法
   #例如一個矩形輪廓只需4個點來儲存輪廓資訊
   # cv2.CHAIN_APPROX_SIMPLE,         
   # cv2.CHAIN_APPROX_TC89_L1,
   # cv2.CHAIN_APPROX_TC89_KCOS
   )

  第一個引數是要檢索的圖片,必須是二值圖,即黑白的(不是灰度圖)。

# 這裡opencv3返回的是三個引數
(_, cnts, _) = cv2.findContours(
    # 引數一:二值化影象
    closed.copy(),
    # 引數二:輪廓型別
    cv2.RETR_LIST,
    cv2.CHAIN_APPROX_SIMPLE
)
c = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
rect = cv2.minAreaRect(c)
box = np.int0(cv2.boxPoints(rect))

draw_img = cv2.drawContours(img.copy(), [box], -1, (0, 0, 255), 3)
cv2.imshow("draw_img", draw_img)

  此時,會得到:

 

8,裁剪

  影象的裁剪最簡單的方式就是獲取影象陣列的切片,如下:

img_crop = img[100:300,100:300]
cv2.imshow("img_crop", img_crop)
key = cv2.waitKey()
if key == 27:
    cv2.destroyAllWindows()

  當然,這裡我們直接找到四個點,切出來就OK.

  其實,box裡儲存的是綠色矩陣區域四個頂點的座標。我們按照下圖所示裁剪昆蟲影象。

  方法是找到四個頂點的x,y座標的最大最小值。新影象的高等於 max(Y) - min(Y),寬等於 max(X) - min(X)。

Xs = [i[0] for i in box]
Ys = [i[1] for i in box]
x1 = min(Xs)
x2 = max(Xs)
y1 = min(Ys)
y2 = max(Ys)
hight = y2 - y1
width = x2 - x1
crop_img= img[y1:y1+hight, x1:x1+width]
cv2.imshow('crop_img', crop_img)

  

9,完整程式碼

#-*- coding: UTF-8 -*- 
import cv2
import numpy as np


def get_image(path):
   #獲取圖片
   img=cv2.imread(path)
   gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
   
   return img, gray
   
def Gaussian_Blur(gray):
   # 高斯去噪
   blurred = cv2.GaussianBlur(gray, (9, 9),0)
   
   return blurred
   
def Sobel_gradient(blurred):
   # 索比爾運算元來計算x、y方向梯度
   gradX = cv2.Sobel(blurred, ddepth=cv2.CV_32F, dx=1, dy=0)
   gradY = cv2.Sobel(blurred, ddepth=cv2.CV_32F, dx=0, dy=1)
   
   gradient = cv2.subtract(gradX, gradY)
   gradient = cv2.convertScaleAbs(gradient)
   
   return gradX, gradY, gradient

def Thresh_and_blur(gradient):
   
   blurred = cv2.GaussianBlur(gradient, (9, 9),0)
   (_, thresh) = cv2.threshold(blurred, 90, 255, cv2.THRESH_BINARY)
   
   return thresh
   
def image_morphology(thresh):
   # 建立一個橢圓核函式
   kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25))
   # 執行影象形態學, 細節直接查文件,很簡單
   closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
   closed = cv2.erode(closed, None, iterations=4)
   closed = cv2.dilate(closed, None, iterations=4)
   
   return closed
   
def findcnts_and_box_point(closed):
   # 這裡opencv3返回的是三個引數
   (_, cnts, _) = cv2.findContours(closed.copy(), 
       cv2.RETR_LIST, 
       cv2.CHAIN_APPROX_SIMPLE)
   c = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
   # compute the rotated bounding box of the largest contour
   rect = cv2.minAreaRect(c)
   box = np.int0(cv2.boxPoints(rect))
   
   return box

def drawcnts_and_cut(original_img, box):
   # 因為這個函式有極強的破壞性,所有需要在img.copy()上畫
   # draw a bounding box arounded the detected barcode and display the image
   draw_img = cv2.drawContours(original_img.copy(), [box], -1, (0, 0, 255), 3)
   
   Xs = [i[0] for i in box]
   Ys = [i[1] for i in box]
   x1 = min(Xs)
   x2 = max(Xs)
   y1 = min(Ys)
   y2 = max(Ys)
   hight = y2 - y1
   width = x2 - x1
   crop_img = original_img[y1:y1+hight, x1:x1+width]
   
   return draw_img, crop_img
   
def walk():
   
   img_path = r'C:\Users\aixin\Desktop\chongzi.png'
   save_path = r'C:\Users\aixin\Desktop\chongzi_save.png'
   original_img, gray = get_image(img_path)
   blurred = Gaussian_Blur(gray)
   gradX, gradY, gradient = Sobel_gradient(blurred)
   thresh = Thresh_and_blur(gradient)
   closed = image_morphology(thresh)
   box = findcnts_and_box_point(closed)
   draw_img, crop_img = drawcnts_and_cut(original_img,box)
   
   # 暴力一點,把它們都顯示出來看看
   
   cv2.imshow('original_img', original_img)
   cv2.imshow('blurred', blurred)
   cv2.imshow('gradX', gradX)
   cv2.imshow('gradY', gradY)
   cv2.imshow('final', gradient)
   cv2.imshow('thresh', thresh)
   cv2.imshow('closed', closed)
   cv2.imshow('draw_img', draw_img)
   cv2.imshow('crop_img', crop_img)
   cv2.waitKey(20171219)
   cv2.imwrite(save_path, crop_img)

walk()

  

附錄程式碼:

# 用來轉化影象格式的
img = cv2.cvtColor(src, 
   COLOR_BGR2HSV # BGR---->HSV
   COLOR_HSV2BGR # HSV---->BGR
   ...)
# For HSV, Hue range is [0,179], Saturation range is [0,255] and Value range is [0,255]


# 返回一個閾值,和二值化影象,第一個閾值是用來otsu方法時候用的
# 不過現在不用了,因為可以通過mahotas直接實現
T = ret = mahotas.threshold(blurred)
ret, thresh_img = cv2.threshold(src, # 一般是灰度影象
   num1, # 影象閾值
   num2, # 如果大於或者num1, 畫素值將會變成 num2
# 最後一個二值化引數
   cv2.THRESH_BINARY      # 將大於閾值的灰度值設為最大灰度值,小於閾值的值設為0
   cv2.THRESH_BINARY_INV  # 將大於閾值的灰度值設為0,大於閾值的值設為最大灰度值
   cv2.THRESH_TRUNC       # 將大於閾值的灰度值設為閾值,小於閾值的值保持不變
   cv2.THRESH_TOZERO      # 將小於閾值的灰度值設為0,大於閾值的值保持不變
   cv2.THRESH_TOZERO_INV  # 將大於閾值的灰度值設為0,小於閾值的值保持不變
)
thresh = cv2.AdaptiveThreshold(src, 
   dst, 
   maxValue, 
   # adaptive_method 
   ADAPTIVE_THRESH_MEAN_C,      
   ADAPTIVE_THRESH_GAUSSIAN_C,      
   # thresholdType
   THRESH_BINARY, 
   THRESH_BINARY_INV, 
   blockSize=3,
   param1=5
)


# 一般是在黑色背景中找白色物體,所以原始影象背景最好是黑色
# 在執行找邊緣的時候,一般是threshold 或者是canny 邊緣檢測後進行的。
# warning:此函式會修改原始影象、
# 返回:座標位置(x,y), 
(_, cnts, _) = cv2.findContours(mask.copy(), 
   # cv2.RETR_EXTERNAL,             #表示只檢測外輪廓
   # cv2.RETR_CCOMP,                #建立兩個等級的輪廓,上一層是邊界
   cv2.RETR_LIST,                 #檢測的輪廓不建立等級關係
   # cv2.RETR_TREE,                   #建立一個等級樹結構的輪廓
   # cv2.CHAIN_APPROX_NONE,           #儲存所有的輪廓點,相鄰的兩個點的畫素位置差不超過1
   cv2.CHAIN_APPROX_SIMPLE,       #例如一個矩形輪廓只需4個點來儲存輪廓資訊
   # cv2.CHAIN_APPROX_TC89_L1,
   # cv2.CHAIN_APPROX_TC89_KCOS
  )
img = cv2.drawContours(src, cnts, whichToDraw(-1), color, line)


img = cv2.imwrite(filename, dst,  # 檔案路徑,和目標影象檔案矩陣
   
   # 對於JPEG,其表示的是影象的質量,用0-100的整數表示,預設為95
   # 注意,cv2.IMWRITE_JPEG_QUALITY型別為Long,必須轉換成int
   [int(cv2.IMWRITE_JPEG_QUALITY), 5] 
   [int(cv2.IMWRITE_JPEG_QUALITY), 95]
   # 從0到9,壓縮級別越高,影象尺寸越小。預設級別為3
   [int(cv2.IMWRITE_PNG_COMPRESSION), 5])
   [int(cv2.IMWRITE_PNG_COMPRESSION), 9])

# 如果你不知道用哪個flags,畢竟太多了哪能全記住,直接找找。
尋找某個函式或者變數
events = [i for i in dir(cv2) if 'PNG' in i]
print( events )

尋找某個變數開頭的flags
flags = [i for i in dir(cv2) if i.startswith('COLOR_')]
print flags

批量讀取檔名字
import os
filename_rgb = r'C:\Users\aixin\Desktop\all_my_learning\colony\20170629'
for filename in os.listdir(filename_rgb):              #listdir的引數是資料夾的路徑
   print (filename)

  

 參考文獻:

https://mp.weixin.qq.com/s?__biz=MzUyMjE2MTE0Mw==&mid=2247485167&idx=1&sn=dd124331da063dc5adf5a9d21c97cd5f&chksm=f9d15877cea6d161c55d7cd2969a0ed8f9275e2391ad36dfda0abc77752593afedbe9351c998&scene=21#wechat_redirect

https://blog.csdn.net/sunny2038/article/category/904451

https://www.cnblogs.com/zangyu/p/5802142.html

相關文章