Search code examples
pythonimageopencvimage-processing

Warping an image with a given set of points


I am trying to perform image warping, but I can't seem to apply the transformation matrix given a set of points. Am trying to accomplish is a face morphing given the feature points which in total is 68 feature points I already calculated the triangulation between the points but I can't seem to warp the image.

Face.py

import numpy as np
import cv2 as cv
import dlib
from warp import triangulate, warp

def crop_faces(image1_path, image2_path):
    cropped_faces_list = []
    for img in [image1_path, image2_path]:
        image = cv.imread(img)

        # convert to grayscale of each frames
        gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

        # read the haarcascade to detect the faces in an image
        face_cascade = cv.CascadeClassifier(cv.data.haarcascades + 'haarcascade_frontalface_default.xml')
        
        # detects faces in the input image
        faces = face_cascade.detectMultiScale(gray, 1.3, 4)
        print('Number of detected faces:', len(faces))

        # Crop and save each detected face
        cropped_faces = []
        if len(faces) > 0:
            for (x, y, w, h) in faces:
                cropped_faces.append(image[y: y + h, x:x + w])

        cropped_faces_list.append(cropped_faces)
    
    return cropped_faces_list

def generate_face_correspondeces(theImage1, theImage2):
    # Detect the points of face.
    detector = dlib.get_frontal_face_detector()
    predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

    imgList = crop_faces(theImage1, theImage2)
    list1 = []
    list2 = []
    feature_points = []
    cropped_images = []
    j = 1

    for m, img_list in enumerate(imgList):
        for img in img_list:

            if (j == 1):
                currList = list1
            else:
                currList = list2

            # Ask the detector to find the bounding boxes of each face. The 1 in the
            # second argument indicates that we should upsample the image 1 time. This
            # will make everything bigger and allow us to detect more faces.

            dets = detector(img, 1)

            try:
                if len(dets) == 0:
                    raise NoFaceFound  # type: ignore
            except NoFaceFound: # type: ignore
                print("Sorry, but I couldn't find a face in the image.")

            j = j + 1

            for k, rect in enumerate(dets):

                # Get landmarks/part for the face in rect
                shape = predictor(img, rect)
                
                for i in range(0, 68):
                    x = shape.part(i).x
                    y = shape.part(i).y
                    currList.append((x, y))
                    cv.circle(img, (x, y), 1, (0, 255, 0), 2)
            
            feature_points.append(currList)
            cropped_images.append(img)

            cv.imwrite(f"test_{m}.png", img)


    return feature_points, cropped_images


img1 = './Images/mulher1.jpg'
img2 = './Images/homem.jpg'

feature_points, cropped_images = generate_face_correspondeces(img1, img2)

for i, image in enumerate(cropped_images):
    triangulate(image, feature_points[i])

warp(img2, feature_points[0], feature_points[1])

Warp.py

import numpy as np
import cv2 as cv

def triangulate(image, points):

    # Create Subdiv2D object
    rect = (0, 0, image.shape[0], image.shape[1])  # Rectangle covering the entire image
    triangulation = cv.Subdiv2D(rect)

      # Insert points into triangulation object
    valid_points = []  # List to store valid points within the image bounds
    for point in points:
        x, y = point
        if 0 <= x < image.shape[1] and 0 <= y < image.shape[0]:  # Check if point is within image bounds
            triangulation.insert((x, y))
            valid_points.append((x, y))
        
    # Get triangles
    triangleList1 = triangulation.getTriangleList()

    # Draw triangles on the image (optional)
    for t in triangleList1:
        pt1 = (int(t[0]), int(t[1]))
        pt2 = (int(t[2]), int(t[3]))
        pt3 = (int(t[4]), int(t[5]))
        cv.line(image, pt1, pt2, (0, 255, 0), 1, cv.LINE_AA)
        cv.line(image, pt2, pt3, (0, 255, 0), 1, cv.LINE_AA)
        cv.line(image, pt3, pt1, (0, 255, 0), 1, cv.LINE_AA)

    # Show or return the triangulated image (optional)
    cv.imshow('Triangulated Image', image)
    cv.waitKey(0)
    cv.destroyAllWindows()

    return valid_points

def warp(image, points1, points2):
    # Compute affine transformation matrix
    M = cv.getAffineTransform(np.float32(points1), np.float32(points2))

    # Warp image1 onto image2
    rows, cols, _ = image.shape
    warped_image = cv.warpAffine(image, M, (cols, rows))

    # Display or save the warped image
    cv.imshow('Warped Image', warped_image)
    cv.waitKey(0)
    cv.destroyAllWindows()

Am getting this error

    M = cv.getAffineTransform(np.float32(points1), np.float32(points2))
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.9.0) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\imgwarp.cpp:3554: error: (-215:Assertion failed) src.checkVector(2, CV_32F) == 3 && dst.checkVector(2, CV_32F) == 3 in function 'cv::getAffineTransform'

Solution

  • In my initial attempt, i used cv.getAffineTransform() to compute the affine transformation matrix (M) based on the feature points (points1 and points2). However, this function expects exactly three points in each list to compute the transformation matrix for an affine transformation.

    The face landmarks likely consist of more than three points (68 points to be exact), which means cv.getAffineTransform() wouldn't work as intended because it expects only three corresponding points for an affine transformation.

    Instead, i used cv.estimateAffine2D(). This function estimates an affine transformation between two sets of points even when there are more than three points. It computes the best-fit affine transformation matrix based on the provided sets of corresponding points (src_pts and dst_pts). This function is more suitable for the case since i have a larger number of feature points (68 points) to determine the transformation.

    img1 = './Images/mulher1.jpg'
    img2 = './Images/homem.jpg'
    
    feature_points, cropped_images = generate_face_correspondeces(img1, img2)
    
    img_shape = []
    
    for i, image in enumerate(cropped_images):
        triangulate(image, feature_points[i])
    
        img_shape.append((image.shape[1], image.shape[0]))
    
    src_pts = np.asarray(feature_points[1], dtype=np.float32)
    dst_pts = np.asarray(feature_points[0], dtype=np.float32)
    
    transformMatrix = cv.estimateAffine2D(src_pts, dst_pts)
    
    warped_img = cv.warpAffine(cropped_images[1], transformMatrix[0], img_shape[0])
    
    cv.imshow("img", warped_img)
    cv.waitKey(0)