opencv人臉檢測,旋轉處理

年會簽到,拍本身的大頭照,有的人可能會拍成橫向的,須要旋轉,用人臉檢測並修正它(圖片)。html

1. 無腦檢測步驟爲:

1. opencv 讀取圖片,灰度轉換
2. 使用CascadeClassifier()經過訓練數據訓練分類器
3. detectMultiScale()檢測人臉

訓練數據集下最基本的人臉haarcascade_frontalface_default.xmlpython

2. 開始檢測

1) 斜臉檢測失敗
用了一張逃避可恥但有用劇照,不知是gakki臉斜仍是不清晰的緣故,face_cascade.detectMultiScale無結果。git

網上找了一張人臉圖發現能夠有結果[[436 142 604 604]]github

2) 旋轉圖片,被裁剪
測試圖片旋轉90°,180°,270°時,發現有個問題。下面爲旋轉180°,90°的效果:90°(以及270°)的圖片大小沒變,致使橫向過長,縱向太窄,圖片被裁減。app


粗暴的嘗試,直接用:函數

if angle in (90,270):
 w,h = h,w

轉換寬高後,輸出90°爲:測試

出現了奇怪的邊框。優化

再來看一個45°的輸出,也一樣被裁剪.net

直到我找到了 Rotate images (correctly) with OpenCV and Python 這篇文章,寫的太好了,用的動圖也恰到好處。debug

rotating oblong pills using the OpenCV’s standard cv2.getRotationMatrix2D and cv2.warpAffine functions caused me some problems that weren’t immediately obvious.

做者檢測原型藥片旋轉不要緊,用橢圓的藥片就有被裁剪的問題,我上面的那些就是被裁剪了。

使用做者給的函數實驗:

def rotate_bound(image, angle):
    # grab the dimensions of the image and then determine the
    # center
    (h, w) = image.shape[:2]
    (cX, cY) = (w // 2, h // 2)
 
    # grab the rotation matrix (applying the negative of the
    # angle to rotate clockwise), then grab the sine and cosine
    # (i.e., the rotation components of the matrix)
    M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
    cos = np.abs(M[0, 0])
    sin = np.abs(M[0, 1])
 
    # compute the new bounding dimensions of the image
    nW = int((h * sin) + (w * cos))
    nH = int((h * cos) + (w * sin))
 
    # adjust the rotation matrix to take into account translation
    M[0, 2] += (nW / 2) - cX
    M[1, 2] += (nH / 2) - cY
 
    # perform the actual rotation and return the image
    return cv2.warpAffine(image, M, (nW, nH))

-45°旋轉

-90°旋轉

完成。

4) 自動旋轉圖片

做者旋轉的很好看,因此也模仿了一把,將本身展現圖片的函數改成:「 獲取圖片寬高,而後動態改變寬高顯示圖片,ms參數爲停留的毫秒數(寬高除以3是由於圖片太大了,展現很差看)」

def show(img, ms=0):
 """ 顯示 """
 cv2.imshow('show', img)
 h, w = img.shape[:2]
 cv2.resizeWindow("show", w//3, h//3)
 cv2.waitKey(ms)

用本身以前不正確的旋轉函數rotate_img_old測試,有裁剪

for angle in range(0,360,10):
 show(rotate_img_bad(img_bgr, angle), 200)

用正確的測試:裴斐科特

for angle in range(0,360,10):
 show(rotate_img(img_bgr, angle), 200)

3) 再試gakki圖

range(0,360,45)以45°爲步長檢測,發現-315°(即45°)有結果:

檢測到兩處人臉:修改配置 每次縮減比例:scaleFactor=1.2,檢測屢次: minNeighbors=10

# 探測圖片中的人臉
faces = face_cascade.detectMultiScale(
 img_gray,
 scaleFactor=1.2,  # 每次縮減比例
 minNeighbors=10,  # 檢測屢次
 flags=cv2.CASCADE_SCALE_IMAGE,
 minSize=(50,50)
)

以後檢測,只有一處。

4. 生成頭像

最後,想截取有效的最大頭像,

90°旋轉裁剪

在此次真實的年會場合實際使用的時候,不能轉45°去裁剪顯示,斜着頭拍照的可不想轉成正經的證件照,

因此只用轉90°而後裁剪其中最大正方形,取高和寬中小的一個。較簡單,藍色爲實際裁剪區域。

斜的圖則不可這樣,會裁剪黑色的區域,(避免臉在圖中太小,取了**檢測到的臉寬*2.5長度**)

代碼段

if angle%90 == 0:
    # 90° 裁剪圖片
    logging.debug("90° 裁剪圖片")
    logging.debug(f"{ix}, {iy}, {x}, {y}, {w}, {h}")

    length2 = min(ix, iy)

    # 若是人臉過小,放大區域但又不超過圖片長度
    length = int(w*2.5)
    length = min(length2, length)
    logging.debug(f"length: {length2} {length}")

    ow = length-w
    ow1 = ow//2
    oh = length-h
    oh1 = oh//2

    y1, y2 = y-oh1, y+h+oh1
    x1, x2 = x-ow1, x+w+ow1

    # 檢測圖片溢出
    logging.debug(f"{y1}, {y2}, {x1}, {x2}")
    if y1 < 0:
    logging.debug('裁剪:1 頂部溢出')
    y1 = 0
    y2 = length
    if y2 > iy:
    logging.debug('裁剪:2 底部溢出')
    y2 = iy
    y1 = iy-length
    if x1 < 0:
    logging.debug('裁剪:3 左側溢出')
    x1 = 0
    x2 = length
    if x2 > ix:
    logging.debug('裁剪:4 右側溢出')
    x2 = ix
    x1 = ix-length

    # 裁剪標記
    cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)

斜矩形裁剪探索

那麼gakki那張斜的如何裁剪呢,斜矩形以外有黑色背景。舉例:應該取到綠色矩形的擴大版,但又沒有取到黑色背景。好比這樣的藍色區域

轉換爲數學方法爲:todo, 畫在本子上的,最後再補

編寫數學函數:

else:
    # 非90°裁剪圖片
    logging.debug(f"{angle}°裁剪圖片")
    # 1. 求A點座標
    origin_h, origin_w = self.img_bgr.shape[:2]

    # 旋轉角度取的ab ad邊不一樣
    if angle%180<90:
        ab = origin_w
        ad = origin_h
    else:
        ab = origin_h
        ad = origin_w
    op = ix
    ap = math.cos(math.radians(angle)) * ab
    oa = op-ap
    A = Point(oa, 0)
    logging.debug(f"ab={ab}, ad={ad}, op={op}, ap={ap}, oa={oa}, {A}")
    # 2. 人臉中心Z座標
    face_rect = Rectangle(Point(x, y), w, h)
    z = face_rect.center_p

    logging.debug(f"{face_rect} center point is {z}")
    # 3. Z到AB、AD距離
    k = math.tan(math.radians(angle)) # tan(α)
    k2 = -1/k # 垂直
    logging.info(f"k1 = {k}, k2 = {k2}")
    z_ab_len = abs(k*z.x-z.y-oa*k)/math.sqrt(k**2+1)
    z_ad_len = abs(k2*z.x-z.y-oa*k2)/math.sqrt(k2**2+1)
    logging.debug(f"z-ab len is {z_ab_len}, z-ad len is {z_ad_len}")
    # 4. 距離四邊最小距離
    h1 = z_ab_len
    h2 = z_ad_len
    h3 = ad-h1
    h4 = ab-h2
    min_len = min(h1, h2, h3, h4)
    logging.debug(f"face around len is {h1} {h2} {h3} {h4}, min:{int(min_len)}")

    # 5. 圓形標註
    #cv2.line(img, z.r_tuple(), (50, 100), (0,255,0))
    for r in (h1, h2, h3, h4):
        r = int(r)
        if int(min_len) == r:
            cv2.circle(img, z.r_tuple(), r, (255, 0, 0), 3)
        else:
            cv2.circle(img, z.r_tuple(), r, (0, 0, 255), 2)

來測試一波,最後的圓形標註只是爲了輔助驗證。

實際上是有問題的,看效果,最小的藍色圓形標註位置不對,超出原圖片了,並且觀察其餘三個圓也不在圖片邊上。

if angle%180<90:
    ab = origin_w
    ad = origin_h
else:
    ab = origin_h
    ad = origin_w

這塊處理不一樣角度下取的矩形ABCD邊不一樣,數學角度看是沒有錯的,可是Opencv座標圓心在左上角,因此得將數學圖形畫爲:todo

上面賦值代碼只需改成

if angle%180<90:
    ab = origin_h
    ad = origin_w
else:
    ab = origin_w
    ad = origin_h

藍色圈已是咱們要求的矩形頭像外接圓了,其餘三個圓輸出也是貼緊各自的邊框,完美。

在藍圈中畫黑色正方形:

是想要的效果,最後裁剪便可。

多角度修復

測試初始角度爲橫向或者側向時,需修復角度:

# 最後取的圖片和角度無關,只取銳角
angle=angle%90

測試三張不一樣角度照片,和一張不需旋轉的圖:

結果爲:

過程:

參考

相關文章
相關標籤/搜索