File size: 6,875 Bytes
3881be4
 
 
 
2fb55f2
3881be4
 
 
36ab9d4
3881be4
 
36ab9d4
 
3881be4
 
 
 
2fb55f2
 
 
 
36ab9d4
2fb55f2
 
 
36ab9d4
 
2fb55f2
 
 
 
 
 
 
 
36ab9d4
 
 
 
 
 
 
 
 
2fb55f2
36ab9d4
2fb55f2
 
36ab9d4
 
 
 
 
 
 
 
2fb55f2
 
 
 
 
 
 
 
 
 
 
3881be4
 
 
 
 
 
36ab9d4
3881be4
 
 
2fb55f2
3881be4
 
 
36ab9d4
 
 
 
3881be4
 
 
 
2fb55f2
36ab9d4
3881be4
36ab9d4
2fb55f2
36ab9d4
 
 
 
 
 
2fb55f2
36ab9d4
 
 
 
 
 
 
 
 
 
 
 
40709e4
3881be4
 
 
 
 
36ab9d4
3881be4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36ab9d4
3881be4
 
 
 
36ab9d4
 
3881be4
 
 
 
 
 
 
 
36ab9d4
 
3881be4
 
 
36ab9d4
3881be4
 
 
36ab9d4
3881be4
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import cv2
import insightface
from insightface.app import FaceAnalysis
import os
import numpy as np

class FaceSwapper:
    def __init__(self):
        # Initialize FaceAnalysis with detection and landmark models
        self.app = FaceAnalysis(name='buffalo_l')
        self.app.prepare(ctx_id=0, det_size=(640, 640))
        
        # Initialize the swapper model
        self.swapper = insightface.model_zoo.get_model(
            'inswapper_128.onnx', download=True, download_zip=True
        )

    def transplant_hair(self, src_img, dst_img, src_face, dst_face):
        """
        Warps the source hair onto the destination face using Affine Transformation.
        """
        # 1. Get Landmarks (keypoints)
        src_lm = src_face.kps
        dst_lm = dst_face.kps

        # 2. Calculate Affine Transform Matrix to align Source face to Target face
        # We use the eyes (points 0, 1) and nose (point 2) for alignment
        src_pts = src_lm[:3]
        dst_pts = dst_lm[:3]
        M = cv2.getAffineTransform(src_pts.astype(np.float32), dst_pts.astype(np.float32))

        # 3. Warp the entire Source Image to match Target Geometry
        h, w = dst_img.shape[:2]
        warped_src = cv2.warpAffine(src_img, M, (w, h), borderMode=cv2.BORDER_REFLECT)

        # 4. Create a Mask for the Hair (Estimation based on Landmarks)
        # We assume hair is generally above the eyebrows .
        # Eyebrow points are indices 17-26 in 68-point models, but insightface buffalo_l uses 5 points usually.
        # If 5 points: 0,1=eyes, 2=nose, 3,4=mouth corners.
        # We estimate the forehead/hairline is above the eyes.
        
        eye_y = int((dst_lm[0][1] + dst_lm[1][1]) / 2) # Average eye height
        nose_y = int(dst_lm[2][1])
        face_height = nose_y - eye_y
        
        # Define the hair region (Everything significantly above the eyes)
        hair_mask = np.zeros((h, w, 3), dtype=np.float32)
        
        # Start the mask slightly above the eyes
        forehead_line = int(eye_y - (face_height * 0.8))
        
        # Create a soft gradient mask from the forehead up
        if forehead_line > 0:
            cv2.rectangle(hair_mask, (0, 0), (w, forehead_line), (1, 1, 1), -1)
            # Blur the mask heavily to blend the hairline
            hair_mask = cv2.GaussianBlur(hair_mask, (51, 51), 0)
        
        # 5. Blend: (WarpedSource * Mask) + (Target * (1-Mask))
        dst_float = dst_img.astype(np.float32) / 255.0
        src_float = warped_src.astype(np.float32) / 255.0
        
        final = (src_float * hair_mask) + (dst_float * (1.0 - hair_mask))
        final = np.clip(final * 255.0, 0, 255).astype(np.uint8)
        
        return final

    def swap_faces(self, source_path, source_face_idx, target_path, target_face_idx, swap_hair=False):
        source_img = cv2.imread(source_path)
        target_img = cv2.imread(target_path)

        if source_img is None or target_img is None:
            raise ValueError("Could not read one or both images")

        # Detect faces
        source_faces = self.app.get(source_img)
        target_faces = self.app.get(target_img)

        # Sort faces from left to right
        source_faces = sorted(source_faces, key=lambda x: x.bbox[0])
        target_faces = sorted(target_faces, key=lambda x: x.bbox[0])

        if len(source_faces) < source_face_idx or source_face_idx < 1:
            raise ValueError(f"Source image contains {len(source_faces)} faces, but requested face {source_face_idx}")
        if len(target_faces) < target_face_idx or target_face_idx < 1:
            raise ValueError(f"Target image contains {len(target_faces)} faces, but requested face {target_face_idx}")

        source_face = source_faces[source_face_idx - 1]
        target_face = target_faces[target_face_idx - 1]

        # Step 1: Standard Face Swap (Inswapper)
        result = self.swapper.get(target_img, target_face, source_face, paste_back=True)

        # Step 2: Optional Hair Transplant (The new logic)
        if swap_hair:
            try:
                result = self.transplant_hair(source_img, result, source_face, target_face)
            except Exception as e:
                print(f"Hair swap failed (fallback to standard swap): {e}")
                # If hair swap fails, we just return the face swap result
                pass

        return result

    def count_faces(self, img_path):
        """
        Counts the number of faces in the given image file.
        """
        img = cv2.imread(img_path)
        # Use your face detector here. For example, with OpenCV's Haar cascade:
        face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(gray, 1.1, 4)
        return len(faces)

def main():
    # Paths relative to root
    source_path = os.path.join("SinglePhoto", "data_src.jpg")
    target_path = os.path.join("SinglePhoto", "data_dst.jpg")
    output_dir = os.path.join("SinglePhoto", "output")

    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    swapper = FaceSwapper()

    try:
        # Ask user for target_face_idx, default to 1 if no input or invalid input
        try:
            user_input = input("Enter the target face index (starting from 1, default is 1): ")
            target_face_idx = int(user_input) if user_input.strip() else 1
            if target_face_idx < 1:
                print("Invalid index. Using default value 1.")
                target_face_idx = 1
        except ValueError:
            print("Invalid input. Using default value 1.")
            target_face_idx = 1

        try:
            # Default swap_hair to False in CLI mode, or True if you want to test it
            result = swapper.swap_faces(
                source_path=source_path,
                source_face_idx=1,
                target_path=target_path,
                target_face_idx=target_face_idx,
                swap_hair=True # Enabled for testing
            )
        except ValueError as ve:
            if "Target image contains" in str(ve):
                print(f"Target face idx {target_face_idx} not found, trying with idx 1.")
                result = swapper.swap_faces(
                    source_path=source_path,
                    source_face_idx=1,
                    target_path=target_path,
                    target_face_idx=1,
                    swap_hair=True
                )
            else:
                raise ve

        output_path = os.path.join(output_dir, "swapped_face.jpg")
        cv2.imwrite(output_path, result)
        print(f"Face swap completed successfully. Result saved to: {output_path}")

    except Exception as e:
        print(f"Error occurred: {str(e)}")

if __name__ == "__main__":
    main()