PSD-TOOLS get warp data and apply it with PIL

I wanted to ask wether it was possible to retrieve the properties (such as deformation, opacity, warp) of a smart object in a psd file using psd-tools, to then apply these information to a given image using either PIL or OpenCV or else?

I manage to retireve and apply most of them, however it won’t apply the warp (or rather when applying it it will make the overlayed image invisible). Either I’m not taking the warp data correctly or it isn’t applying corerclty. Can someone help me out?

Here’s the code:

from psd_tools import PSDImage
import os
import cv2
import numpy as np

def apply_warp(image, warp):
    height, width = image.shape[:2]
    # Extract warp parameters
    warp_style = warp[b'warpStyle'].enum
    warp_value = warp[b'warpValue']
    warp_perspective = warp[b'warpPerspective']
    bounds = warp[b'bounds']
    # Create meshgrid
    x, y = np.meshgrid(np.arange(width), np.arange(height))
    # Normalize coordinates
    x_norm = x / width
    y_norm = y / height
    # Apply simple warp (you may need to implement more complex warping based on warp_style)
    if warp_style == 'warpNone':
        return image  # No warping
        # Apply a simple warp as an example
        x_new = x + warp_value * np.sin(np.pi * y_norm)
        y_new = y + warp_perspective * (x_norm - 0.5)
    # Remap the image
    return cv2.remap(image, x_new.astype(np.float32), y_new.astype(np.float32), cv2.INTER_LINEAR)

# Paths
psd_path = 
image_A_path = 
image_B_path = 

# Find and open PSD file
psd_file = next((f for f in os.listdir(psd_path) if f.lower().endswith('.psd')), None)
if not psd_file:
    raise FileNotFoundError("No PSD file found in the specified directory.")

psd =, psd_file))

# Find smart object layer
smart_object_layer = next((layer for layer in psd.descendants() if layer.kind == 'smartobject'), None)
if not smart_object_layer:
    raise ValueError("No smart object layer found in the PSD file.")

# Extract properties
smart_object = smart_object_layer.smart_object
warp = smart_object.warp
transform_box = smart_object.transform_box
position = (transform_box[0], transform_box[1])  # Top-left corner
opacity = smart_object_layer.opacity / 255.0  # Normalize to 0-1 range

# Print extracted information
print("Smart Object Properties:")
print(f"Warp: {warp}")
print(f"Position: {position}")
print(f"Opacity: {opacity}")
print("Transform Box:")
for i in range(0, len(transform_box), 2):
    print(f"  Point {i//2 + 1}: ({transform_box[i]}, {transform_box[i+1]})")

# Load images
image_A = cv2.imread(image_A_path, cv2.IMREAD_UNCHANGED)
image_B = cv2.imread(image_B_path)

# Ensure image_A has an alpha channel
if image_A.shape[2] == 3:
    image_A = cv2.cvtColor(image_A, cv2.COLOR_BGR2BGRA)

# Apply warp transformation
warped_A = apply_warp(image_A, warp)

# Apply perspective transform
src_pts = np.array([(0, 0), (warped_A.shape[1], 0), (warped_A.shape[1], warped_A.shape[0]), (0, warped_A.shape[0])], dtype=np.float32)
dst_pts = np.array([(transform_box[i], transform_box[i+1]) for i in range(0, len(transform_box), 2)], dtype=np.float32)
matrix = cv2.getPerspectiveTransform(src_pts, dst_pts)
transformed_A = cv2.warpPerspective(warped_A, matrix, (image_B.shape[1], image_B.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_TRANSPARENT)

# Apply opacity
transformed_A[:, :, 3] = transformed_A[:, :, 3] * opacity

# Blend images
alpha = transformed_A[:, :, 3] / 255.0
for c in range(3):
    image_B[:, :, c] = image_B[:, :, c] * (1 - alpha) + transformed_A[:, :, c] * alpha

# Save result
cv2.imwrite(, image_B)

print("Transformation applied (including warp) and result saved.")