Spaces:
Runtime error
Runtime error
import cv2 | |
import numpy as np | |
import scipy.sparse as sp | |
import scipy.sparse.linalg as splin | |
from numba import jit | |
import gradio as gr | |
def build_poisson_sparse_matrix(ys, xs, im2var, img_s, img_t, mask): | |
nnz = len(ys) | |
img_s_h, img_s_w = img_s.shape | |
A_data = np.zeros(16 * nnz, dtype=np.float64) | |
A_rows = np.zeros(16 * nnz, dtype=np.int32) | |
A_cols = np.zeros(16 * nnz, dtype=np.int32) | |
b = np.zeros(4 * nnz, dtype=np.float64) | |
offsets = np.array([(0, 1), (0, -1), (1, 0), (-1, 0)]) | |
idx = 0 | |
for n in range(nnz): | |
y, x = ys[n], xs[n] | |
for i in range(4): | |
dy, dx = offsets[i] | |
n_y, n_x = y + dy, x + dx | |
e = 4 * n + i | |
if 0 <= n_y < img_s_h and 0 <= n_x < img_s_w: | |
A_data[idx] = 1 | |
A_rows[idx] = e | |
A_cols[idx] = im2var[y, x] | |
idx += 1 | |
b[e] = img_s[y, x] - img_s[n_y, n_x] | |
if im2var[n_y, n_x] != -1: | |
A_data[idx] = -1 | |
A_rows[idx] = e | |
A_cols[idx] = im2var[n_y, n_x] | |
idx += 1 | |
else: | |
b[e] += img_t[n_y, n_x] | |
return A_data[:idx], A_rows[:idx], A_cols[:idx], b | |
def poisson_blend_fast_jit(img_s: np.ndarray, mask: np.ndarray, img_t: np.ndarray) -> np.ndarray: | |
nnz = np.sum(mask > 0) | |
im2var = np.full(mask.shape, -1, dtype=np.int32) | |
im2var[mask > 0] = np.arange(nnz) | |
ys, xs = np.nonzero(mask) | |
A_data, A_rows, A_cols, b = build_poisson_sparse_matrix(ys, xs, im2var, img_s, img_t, mask) | |
A = sp.csr_matrix((A_data, (A_rows, A_cols)), shape=(4*nnz, nnz)) | |
v = splin.lsqr(A, b)[0] | |
img_t_out = img_t.copy() | |
img_t_out[mask > 0] = v[im2var[mask > 0]] | |
return np.clip(img_t_out, 0, 1) | |
def neighbours(i: int, j: int, max_i: int, max_j: int): | |
pairs = [] | |
for n in (-1, 1): | |
if 0 <= i+n <= max_i: | |
pairs.append((i+n, j)) | |
if 0 <= j+n <= max_j: | |
pairs.append((i, j+n)) | |
return pairs | |
def build_mixed_blend_sparse_matrix(ys, xs, im2var, img_s, img_t, mask): | |
nnz = len(ys) | |
img_s_h, img_s_w = img_s.shape | |
A_data = np.zeros(8 * nnz, dtype=np.float64) | |
A_rows = np.zeros(8 * nnz, dtype=np.int32) | |
A_cols = np.zeros(8 * nnz, dtype=np.int32) | |
b = np.zeros(4 * nnz, dtype=np.float64) | |
idx = 0 | |
e = 0 | |
for n in range(nnz): | |
y, x = ys[n], xs[n] | |
for n_y, n_x in neighbours(y, x, img_s_h-1, img_s_w-1): | |
ds = img_s[y, x] - img_s[n_y, n_x] | |
dt = img_t[y, x] - img_t[n_y, n_x] | |
d = ds if abs(ds) > abs(dt) else dt | |
A_data[idx] = 1 | |
A_rows[idx] = e | |
A_cols[idx] = im2var[y, x] | |
idx += 1 | |
b[e] = d | |
if im2var[n_y, n_x] != -1: | |
A_data[idx] = -1 | |
A_rows[idx] = e | |
A_cols[idx] = im2var[n_y, n_x] | |
idx += 1 | |
else: | |
b[e] += img_t[n_y, n_x] | |
e += 1 | |
return A_data[:idx], A_rows[:idx], A_cols[:idx], b[:e] | |
def mixed_blend_fast_jit(img_s: np.ndarray, mask: np.ndarray, img_t: np.ndarray) -> np.ndarray: | |
nnz = np.sum(mask > 0) | |
im2var = np.full(mask.shape, -1, dtype=np.int32) | |
im2var[mask > 0] = np.arange(nnz) | |
ys, xs = np.nonzero(mask) | |
A_data, A_rows, A_cols, b = build_mixed_blend_sparse_matrix(ys, xs, im2var, img_s, img_t, mask) | |
A = sp.csr_matrix((A_data, (A_rows, A_cols)), shape=(len(b), nnz)) | |
v = splin.spsolve(A.T @ A, A.T @ b) | |
img_t_out = img_t.copy() | |
img_t_out[mask > 0] = v[im2var[mask > 0]] | |
return np.clip(img_t_out, 0, 1) | |
def _2d_gaussian(sigma: float) -> np.ndarray: | |
ksize = np.int64(np.ceil(sigma)*6+1) | |
gaussian_1d = cv2.getGaussianKernel(ksize, sigma) | |
return gaussian_1d * np.transpose(gaussian_1d) | |
def _low_pass_filter(img: np.ndarray, sigma: float) -> np.ndarray: | |
return cv2.filter2D(img, -1, _2d_gaussian(sigma)) | |
def _high_pass_filter(img: np.ndarray, sigma: float) -> np.ndarray: | |
return img - _low_pass_filter(img, sigma) | |
def _gaus_pyramid(img: np.ndarray, depth: int, sigma: int): | |
_im = img.copy() | |
pyramid = [] | |
for d in range(depth-1): | |
_im = _low_pass_filter(_im.copy(), sigma) | |
pyramid.append(_im) | |
_im = cv2.pyrDown(_im) | |
return pyramid | |
def _lap_pyramid(img: np.ndarray, depth: int, sigma: int): | |
_im = img.copy() | |
pyramid = [] | |
for d in range(depth-1): | |
lap = _high_pass_filter(_im.copy(), sigma) | |
pyramid.append(lap) | |
_im = cv2.pyrDown(_im) | |
return pyramid | |
def _blend(img1: np.ndarray, img2: np.ndarray, mask: np.ndarray) -> np.ndarray: | |
return img1 * mask + img2 * (1.0 - mask) | |
def laplacian_blend(img1: np.ndarray, img2: np.ndarray, mask: np.ndarray, depth: int, sigma: int) -> np.ndarray: | |
mask_gaus_pyramid = _gaus_pyramid(mask, depth, sigma) | |
img1_lap_pyramid, img2_lap_pyramid = _lap_pyramid(img1, depth, sigma), _lap_pyramid(img2, depth, sigma) | |
blended = [_blend(obj, bg, mask) for obj, bg, mask in zip(img1_lap_pyramid, img2_lap_pyramid, mask_gaus_pyramid)][::-1] | |
h, w = blended[0].shape[:2] | |
img1 = cv2.resize(img1, (w, h)) | |
img2 = cv2.resize(img2, (w, h)) | |
mask = cv2.resize(mask, (w, h)) | |
blanded_img = _blend(img1, img2, mask) | |
blanded_img = cv2.resize(blanded_img, blended[0].shape[:2]) | |
imgs = [] | |
for d in range(0, depth-1): | |
gaussian_img = _low_pass_filter(blanded_img.copy(), sigma) | |
gaussian_img = cv2.resize(gaussian_img, (blended[d].shape[1], blended[d].shape[0])) # Ensure sizes match | |
# Ensure the number of channels match | |
if len(gaussian_img.shape) == 2: | |
gaussian_img = np.stack([gaussian_img] * 3, axis=-1) | |
reconstructed_img = cv2.add(blended[d], gaussian_img) | |
imgs.append(reconstructed_img) | |
blanded_img = cv2.pyrUp(reconstructed_img) | |
return np.clip(imgs[-1], 0, 1) | |
def get_image(img_path: str, mask: bool=False, scale: bool=True) -> np.array: | |
""" | |
Gets image in appropriate format | |
""" | |
if isinstance(img_path, np.ndarray): | |
img = img_path | |
else: | |
img = cv2.imread(img_path) | |
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Convert BGR to RGB for file inputs | |
if mask: | |
if len(img.shape) == 3: | |
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) | |
_, binary_mask = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) | |
return np.where(binary_mask == 255, 1, 0) | |
if scale: | |
return img.astype('double') / 255.0 | |
return img | |
def blend_images(bg_img, obj_img, mask_img, method): | |
bg_img = get_image(bg_img) | |
obj_img = get_image(obj_img) | |
mask_img = get_image(mask_img, mask=True) | |
if method == "Poisson": | |
blend_img = np.zeros_like(bg_img) | |
for b in range(3): | |
blend_img[:,:,b] = poisson_blend_fast_jit(obj_img[:,:,b], mask_img, bg_img[:,:,b].copy()) | |
elif method == "Mixed Gradient": | |
blend_img = np.zeros_like(bg_img) | |
for b in range(3): | |
blend_img[:,:,b] = mixed_blend_fast_jit(obj_img[:,:,b], mask_img, bg_img[:,:,b].copy()) | |
elif method == "Laplacian": | |
mask_stack = np.stack((mask_img.astype(float),) * 3, axis=-1) | |
blend_img = laplacian_blend(obj_img, bg_img, mask_stack, 5, 25.0) | |
return (blend_img * 255).astype(np.uint8) | |
with gr.Blocks(theme='bethecloud/storj_theme') as iface: | |
gr.HTML("<h1>Image Blending with Multiple Methods</h1>") | |
with gr.Row(): | |
with gr.Column(): | |
bg_img = gr.Image(label="Background Image", type="numpy", height=300) | |
with gr.Column(): | |
obj_img = gr.Image(label="Object Image", type="numpy", height=300) | |
with gr.Column(): | |
mask_img = gr.Image(label="Mask Image", type="numpy", height=300) | |
with gr.Row(): | |
with gr.Column(): | |
method = gr.Radio(["Laplacian", "Poisson", "Mixed Gradient"], label="Blending Method", value="Laplacian") | |
with gr.Column(): | |
blend_button = gr.Button("Blend Images") | |
output_image = gr.Image(label="Blended Image") | |
blend_button.click( | |
blend_images, | |
inputs=[bg_img, obj_img, mask_img, method], | |
outputs=output_image | |
) | |
gr.Examples( | |
examples=[ | |
["img1.jpg", "img2.jpg", "mask1.jpg", "Poisson"], | |
["img3.jpg", "img4.jpg", "mask2.jpg", "Mixed Gradient"], | |
["img6.jpg", "img9.jpg", "mask3.jpg", "Laplacian"] | |
], | |
inputs=[bg_img, obj_img, mask_img, method], | |
outputs=output_image, | |
fn=blend_images, | |
cache_examples=True, | |
) | |
iface.launch() |