1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

[Python] Poisson Blending in Image Stitching Results in Blurred, Ghostly Images

Discussão em 'Python' iniciado por Stack, Setembro 23, 2024.

  1. Stack

    Stack Membro Participativo

    I'm working on an image stitching project using Poisson blending to seamlessly blend overlapping images. While the blending quality is generally good, I'm encountering an issue where the resulting stitched image has blurred layers the same size as the masks, leading to a ghostly, blurry result.

    Problem:

    When stitching multiple images using their corresponding homographies, the final panorama shows ghosting and blurring in the areas where images overlap.

    Linear blending result

    Poisson blending result

    import cv2
    import numpy as np
    import os
    import matplotlib.pyplot as plt
    from skimage import img_as_ubyte

    def process_images_and_masks(images: list[np.ndarray], homographies: list[np.ndarray]) -> tuple[list[np.ndarray], list[np.ndarray]]:
    """
    Process images with their homographies and return the warped images and masks.
    """
    def get_transformed_corners(image, H):
    h, w = image.shape[:2]
    corners = np.float32([[0, 0], [0, h-1], [w-1, h-1], [w-1, 0]]).reshape(-1, 1, 2)
    transformed_corners = cv2.perspectiveTransform(corners, H)
    return transformed_corners

    # Calculate the bounds of the canvas
    all_corners = []
    for image, H in zip(images, homographies):
    transformed_corners = get_transformed_corners(image, H)
    all_corners.append(transformed_corners)

    all_corners = np.concatenate(all_corners, axis=0).reshape(-1, 2)
    min_x, min_y = np.min(all_corners, axis=0).astype(int)
    max_x, max_y = np.max(all_corners, axis=0).astype(int)

    canvas_width = max_x - min_x
    canvas_height = max_y - min_y

    # Adjust the homographies to fit all images within the canvas
    translation = np.array([[1, 0, -min_x], [0, 1, -min_y], [0, 0, 1]])
    adjusted_homographies = [translation @ H for H in homographies]

    new_images = []
    visible_masks = []

    for image, H in zip(images, adjusted_homographies):
    warped_image = cv2.warpPerspective(image, H, (canvas_width, canvas_height))

    # Create a binary mask for the original image
    mask = np.ones(image.shape[:2], dtype=np.uint8) * 255

    # Warp the mask using the same homography
    warped_mask = cv2.warpPerspective(mask, H, (canvas_width, canvas_height))

    # Append the results to the lists
    new_images.append(warped_image)
    visible_masks.append(warped_mask)

    return new_images, visible_masks

    def divAB(a, b, fill=np.nan):
    """ Element-wise division of a by b, handling division by zero. """
    with np.errstate(divide='ignore', invalid='ignore'):
    c = np.true_divide(a, b)
    if np.isscalar(c):
    return c if np.isfinite(c) else fill
    else:
    c[~np.isfinite(c)] = fill
    return c

    def computeGrad(img):
    # Compute gradient of the image.
    out_x = np.zeros(img.shape, np.double)
    out_y = np.zeros(img.shape, np.double)
    out_x[:, 1:-1, :] = 0.5 * (img[:, 2:, :].astype('double') - img[:, :-2, :].astype('double'))
    out_y[1:-1, :, :] = 0.5 * (img[2:, :, :].astype('double') - img[:-2, :, :].astype('double'))
    return out_x, out_y

    def computeGuidanceField(grads, masks):
    # Convert masks to float64 and normalize to [0, 1]
    masks = [mask[:, :, np.newaxis].astype(np.float64) / 255.0 for mask in masks]

    # Stack the gradients and masks
    grad_x_stack = np.stack([grad[0] for grad in grads], axis=0) # Shape: (num_images, H, W, C)
    grad_y_stack = np.stack([grad[1] for grad in grads], axis=0)
    mask_stack = np.stack(masks, axis=0) # Shape: (num_images, H, W, 1)

    # Sum of masks at each pixel
    mask_sum = np.sum(mask_stack, axis=0)

    # Avoid division by zero
    mask_sum[mask_sum == 0] = 1

    # Normalize masks to sum to 1 at each pixel
    normalized_masks = mask_stack / mask_sum

    # Weighted sum of gradients
    guide_x = np.sum(grad_x_stack * normalized_masks, axis=0)
    guide_y = np.sum(grad_y_stack * normalized_masks, axis=0)

    return guide_x, guide_y

    def convertDouble2Uint8(img):
    img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
    return img.astype(np.uint8)

    def mft(U):
    # Define a shortcut for the Fourier transform.
    return np.fft.fftshift(np.fft.fft2(U))

    def imft(U):
    # Define a shortcut for the inverse Fourier transform.
    return np.fft.ifft2(np.fft.ifftshift(U)).real

    # Expand the image to create symmetry to avoid boundary problems
    def expandImage(original_image, d="x"):
    # Get the dimensions of the original image
    rows, cols, ch = np.shape(original_image)

    # Create an empty array to hold the final image
    final_image = np.empty((rows * 2, cols * 2, ch), dtype=original_image.dtype)

    if d == "x":
    # Place the original and mirror images in the final image
    final_image[0:rows, 0:cols, :] = original_image
    final_image[0:rows, cols:cols*2, :] = -np.flip(original_image, axis=1)
    final_image[rows:rows*2, 0:cols, :] = np.flip(original_image, axis=0)
    final_image[rows:rows*2, cols:cols*2, :] = -np.flip(original_image, axis=(0,1))
    elif d == "y":
    # Place the original and mirror images in the final image
    final_image[0:rows, 0:cols, :] = original_image
    final_image[0:rows, cols:cols*2, :] = np.flip(original_image, axis=1)
    final_image[rows:rows*2, 0:cols, :] = -np.flip(original_image, axis=0)
    final_image[rows:rows*2, cols:cols*2, :] = -np.flip(original_image, axis=(0,1))

    return final_image

    def poissonSolver(gx, gy):
    print("poissonSolver")
    # Initialization of the output image
    I = np.zeros(gx.shape)

    # Expand the image to create symmetry to avoid boundary problems
    gx = expandImage(gx, "x")
    gy = expandImage(gy, "y")

    H,W,C = gx.shape

    # Define frequency domain
    wx, wy = np.meshgrid(np.arange(1, W+1, 1), np.arange(1, H+1, 1))

    wx0 = np.floor(W/2)+1
    wy0 = np.floor(H/2)+1 # Zero frequency
    wx = wx - wx0
    wy = wy - wy0

    cx = ((1j*2*np.pi)/W)*wx
    cy = ((1j*2*np.pi)/H)*wy
    d = (cx)**2 + (cy)**2
    print("---", gx.shape)

    print(f"{'-->':>4} Print zeros : {np.argwhere(np.abs(d) == 0)}, Center: ({int(wy0)}, {int(wx0)})")

    del wx, wy

    for c in range(0, C):
    Gx = gx[:,:,c]
    Gy = gy[:,:,c]

    Vx = (cx) * mft(Gx)
    Vy = (cy) * mft(Gy)

    # FT_I = ( Vx + Vy ) / ( d )
    FT_I = np.zeros_like(Vx)
    np.divide(( Vx + Vy ), d, out=FT_I, where=d!=0) # Only divide non-zeros else 1

    FT_I[int(wy0-1), int(wx0-1)] = 10 # Set DC value (undefined in the previous div.)

    Aux = imft(FT_I)
    I[:,:,c] = Aux[0:int(H/2), 0:int(W/2)] # Keep the original portion of the space.

    cv2.normalize(I, I, 0, 1, cv2.NORM_MINMAX)
    return I

    def poisson_blending(images_list, homographies_list) -> np.ndarray:
    new_images, visible_masks = process_images_and_masks(images_list, homographies_list)
    grads = [computeGrad(img) for img in new_images]
    guide_x, guide_y = computeGuidanceField(grads, visible_masks)

    img_out = poissonSolver(guide_x, guide_y)
    img_out = convertDouble2Uint8(img_out)

    return guide_x, guide_y, img_out

    # Directory where images and H matrices are stored
    output_dir = 'samples/output_images'

    # List to store images and homographies
    images = []
    homographies = []

    # Load the images
    image_filenames = sorted([f for f in os.listdir(output_dir) if f.endswith('.png')])
    for image_filename in image_filenames:
    image_path = os.path.join(output_dir, image_filename)
    image = cv2.imread(image_path)
    images.append(image)

    # Load the H matrices (homographies)
    H_matrices_filename = os.path.join(output_dir, 'H_matrices.npy')
    homographies = np.load(H_matrices_filename, allow_pickle=True)

    guide_x, guide_y, img_out = poisson_blending(images, homographies)

    # Display the resulting image
    plt.axis("off") # Hide axis
    plt.imshow(cv2.cvtColor(img_out, cv2.COLOR_BGR2RGB)) # Convert from BGR to RGB for correct display
    plt.show()



    Additional Information: For each image, there is a corresponding homography that positions the image in 2D space. The images and homographies are loaded from the samples/output_images directory. The masks are created as binary masks (all ones) and warped using the homographies.

    Data link: output_images - Google Drive

    How can I adjust the Poisson blending process to eliminate the blurring and achieve a seamless panorama?

    Continue reading...

Compartilhe esta Página