Revert "Update stereoimage_generation.py"
This reverts commit 9483638900.
This commit is contained in:
+103
-403
@@ -1,137 +1,38 @@
|
||||
try:
|
||||
from numba import njit, prange
|
||||
except Exception as e:
|
||||
print(f"WARNING! Numba failed to import! Stereoimage generation will be much slower! ({str(e)})")
|
||||
print(f"WARINING! Numba failed to import! Stereoimage generation will be much slower! ({str(e)})")
|
||||
from builtins import range as prange
|
||||
def njit(parallel=False):
|
||||
def Inner(func): return lambda *args, **kwargs: func(*args, **kwargs)
|
||||
return Inner
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import math
|
||||
|
||||
def blur_depth_map(depth, sigma):
|
||||
"""
|
||||
Applies a separable Gaussian blur to a 2D depth map.
|
||||
'sigma' is the standard deviation in pixels.
|
||||
If sigma <= 0, the original depth map is returned.
|
||||
"""
|
||||
if sigma <= 0:
|
||||
return depth
|
||||
radius = int(3 * sigma)
|
||||
x = np.arange(-radius, radius + 1)
|
||||
kernel = np.exp(- (x ** 2) / (2 * sigma * sigma))
|
||||
kernel /= kernel.sum()
|
||||
|
||||
h, w = depth.shape
|
||||
# Horizontal pass
|
||||
blurred = np.empty_like(depth, dtype=np.float64)
|
||||
for i in range(h):
|
||||
row = depth[i, :]
|
||||
padded = np.pad(row, (radius, radius), mode='edge')
|
||||
conv = np.convolve(padded, kernel, mode='valid')
|
||||
blurred[i, :] = conv
|
||||
# Vertical pass
|
||||
final = np.empty_like(blurred, dtype=np.float64)
|
||||
for j in range(w):
|
||||
col = blurred[:, j]
|
||||
padded = np.pad(col, (radius, radius), mode='edge')
|
||||
conv = np.convolve(padded, kernel, mode='valid')
|
||||
final[:, j] = conv
|
||||
return final
|
||||
|
||||
def edge_selective_blur_depth_map(depth, sigma, edge_threshold):
|
||||
"""
|
||||
Applies a global edge-selective blur (ignoring direction) to the depth map.
|
||||
(This is our previous implementation.)
|
||||
"""
|
||||
h, w = depth.shape
|
||||
# Compute gradients using Sobel operators.
|
||||
padded = np.pad(depth, 1, mode='edge')
|
||||
sobel_x = np.array([[-1, 0, 1],
|
||||
[-2, 0, 2],
|
||||
[-1, 0, 1]])
|
||||
sobel_y = np.array([[-1, -2, -1],
|
||||
[ 0, 0, 0],
|
||||
[ 1, 2, 1]])
|
||||
grad_x = np.zeros_like(depth)
|
||||
grad_y = np.zeros_like(depth)
|
||||
for i in range(h):
|
||||
for j in range(w):
|
||||
region = padded[i:i+3, j:j+3]
|
||||
grad_x[i, j] = np.sum(region * sobel_x)
|
||||
grad_y[i, j] = np.sum(region * sobel_y)
|
||||
grad_mag = np.sqrt(grad_x**2 + grad_y**2)
|
||||
# Create weight: values >= edge_threshold get full weight.
|
||||
weight = np.minimum(grad_mag / edge_threshold, 1.0)
|
||||
blurred = blur_depth_map(depth, sigma)
|
||||
output = (1.0 - weight) * depth + weight * blurred
|
||||
return output
|
||||
|
||||
def left_direction_aware_blur_depth_map(depth, sigma, edge_threshold):
|
||||
"""
|
||||
For the left image: only blur pixels where the horizontal gradient is positive
|
||||
(i.e. a dark->light transition). The weight is given by:
|
||||
weight = min( (grad / edge_threshold), 1 ) if grad > 0, else 0.
|
||||
"""
|
||||
h, w = depth.shape
|
||||
grad = np.zeros_like(depth)
|
||||
# Compute a simple horizontal gradient using central differences.
|
||||
for i in range(h):
|
||||
row = depth[i, :]
|
||||
padded = np.pad(row, (1, 1), mode='edge')
|
||||
for j in range(w):
|
||||
grad[i, j] = (padded[j+2] - padded[j]) / 2.0
|
||||
weight = np.where(grad > 0, np.minimum(grad / edge_threshold, 1.0), 0.0)
|
||||
blurred = blur_depth_map(depth, sigma)
|
||||
return (1.0 - weight) * depth + weight * blurred
|
||||
|
||||
def right_direction_aware_blur_depth_map(depth, sigma, edge_threshold):
|
||||
"""
|
||||
For the right image: only blur pixels where the horizontal gradient is negative
|
||||
(i.e. a light->dark transition). The weight is given by:
|
||||
weight = min( (|grad| / edge_threshold), 1 ) if grad < 0, else 0.
|
||||
"""
|
||||
h, w = depth.shape
|
||||
grad = np.zeros_like(depth)
|
||||
for i in range(h):
|
||||
row = depth[i, :]
|
||||
padded = np.pad(row, (1, 1), mode='edge')
|
||||
for j in range(w):
|
||||
grad[i, j] = (padded[j+2] - padded[j]) / 2.0
|
||||
weight = np.where(grad < 0, np.minimum(np.abs(grad) / edge_threshold, 1.0), 0.0)
|
||||
blurred = blur_depth_map(depth, sigma)
|
||||
return (1.0 - weight) * depth + weight * blurred
|
||||
|
||||
def create_stereoimages(original_image, depthmap, divergence, separation=0.0, modes=None,
|
||||
stereo_balance=0.0, stereo_offset_exponent=1.0, fill_technique='polylines_sharp',
|
||||
depth_blur_sigma=0.0, depth_blur_edge_threshold=10.0,
|
||||
direction_aware_depth_blur=True, return_modified_depth=True):
|
||||
"""
|
||||
Creates stereoscopic images.
|
||||
|
||||
Parameters:
|
||||
- original_image: The source image.
|
||||
- depthmap: A depth map (white = near, black = far).
|
||||
- divergence: 3D effect strength in percentages.
|
||||
- separation: Additional horizontal shift percentage.
|
||||
- modes: List of output modes (e.g., 'left-right', 'red-cyan-anaglyph').
|
||||
- stereo_balance: How divergence is split between the two eyes.
|
||||
- stereo_offset_exponent: Exponent controlling the depth-to-offset mapping.
|
||||
- fill_technique: Method used to fill gaps (e.g., 'polylines_sharp', etc.).
|
||||
- depth_blur_sigma: Standard deviation for Gaussian blur.
|
||||
- depth_blur_edge_threshold: Gradient threshold controlling blur strength.
|
||||
- direction_aware_depth_blur: If True, generate two depth maps:
|
||||
one for the left eye (blurring only on positive horizontal gradients)
|
||||
and one for the right eye (blurring only on negative horizontal gradients).
|
||||
- return_modified_depth: If True, return the modified depth maps as well.
|
||||
|
||||
Returns:
|
||||
If return_modified_depth is True and direction_aware_depth_blur is True, a tuple
|
||||
(stereo_images, left_modified_depth, right_modified_depth) is returned. Otherwise,
|
||||
if direction_aware_depth_blur is False and return_modified_depth is True, a tuple
|
||||
(stereo_images, modified_depth) is returned, where modified_depth is produced by the
|
||||
global edge-selective blur. If return_modified_depth is False, only stereo_images is returned.
|
||||
stereo_balance=0.0, stereo_offset_exponent=1.0, fill_technique='polylines_sharp'):
|
||||
"""Creates stereoscopic images.
|
||||
An effort is made to make them look nice, but beware that the resulting image will have some distortion.
|
||||
The correctness was not rigorously tested.
|
||||
|
||||
:param original_image: original image from which the 3D image (stereoimage) will be created
|
||||
:param depthmap: depthmap corresponding to the original image. White = near, black = far.
|
||||
:param float divergence: the measure of 3D effect, in percentages.
|
||||
A good value will likely be somewhere in the [0.05; 10.0) interval.
|
||||
:param float separation: measure by how much to move two halves of the stereoimage apart from each-other.
|
||||
Measured in percentages. Negative values move two parts closer together.
|
||||
Affects which parts of the image will be visible in left and/or right half.
|
||||
:param list modes: how the result will look like. By default only 'left-right' is generated
|
||||
- a picture for the left eye will be on the left and the picture from the right eye - on the right.
|
||||
Some of the supported modes are: 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'red-cyan-anaglyph'.
|
||||
:param float stereo_balance: has to do with how the divergence will be split among the two parts of the image,
|
||||
must be in the [-1.0; 1.0] interval.
|
||||
:param float stereo_offset_exponent: Higher values move objects residing
|
||||
between close and far plane more to the far plane
|
||||
:param str fill_technique: applying divergence inevitably creates some gaps in the image.
|
||||
This parameter specifies the technique that will be used to fill in the blanks in the two resulting images.
|
||||
Must be one of the following: 'none', 'naive', 'naive_interpolating', 'polylines_soft', 'polylines_sharp'.
|
||||
"""
|
||||
if modes is None:
|
||||
modes = ['left-right']
|
||||
@@ -139,314 +40,57 @@ def create_stereoimages(original_image, depthmap, divergence, separation=0.0, mo
|
||||
modes = [modes]
|
||||
if len(modes) == 0:
|
||||
return []
|
||||
|
||||
original_image = np.asarray(original_image)
|
||||
depthmap = np.asarray(depthmap).astype(np.float64)
|
||||
|
||||
if direction_aware_depth_blur:
|
||||
left_depthmap = left_direction_aware_blur_depth_map(depthmap, depth_blur_sigma, depth_blur_edge_threshold)
|
||||
right_depthmap = right_direction_aware_blur_depth_map(depthmap, depth_blur_sigma, depth_blur_edge_threshold)
|
||||
else:
|
||||
# Global edge-selective blur (as before)
|
||||
if depth_blur_sigma > 0:
|
||||
depthmap = edge_selective_blur_depth_map(depthmap, depth_blur_sigma, depth_blur_edge_threshold)
|
||||
left_depthmap = right_depthmap = depthmap
|
||||
|
||||
# Save modified depth maps for output.
|
||||
if direction_aware_depth_blur:
|
||||
mod_left = left_depthmap.copy()
|
||||
mod_right = right_depthmap.copy()
|
||||
else:
|
||||
mod_depth = depthmap.copy()
|
||||
|
||||
original_image = np.asarray(original_image)
|
||||
balance = (stereo_balance + 1) / 2
|
||||
left_eye = original_image if balance < 0.001 else \
|
||||
apply_stereo_divergence(original_image, left_depthmap, +1 * divergence, -1 * separation,
|
||||
apply_stereo_divergence(original_image, depthmap, +1 * divergence * balance, -1 * separation,
|
||||
stereo_offset_exponent, fill_technique)
|
||||
right_eye = original_image if balance > 0.999 else \
|
||||
apply_stereo_divergence(original_image, right_depthmap, -1 * divergence, separation,
|
||||
apply_stereo_divergence(original_image, depthmap, -1 * divergence * (1 - balance), separation,
|
||||
stereo_offset_exponent, fill_technique)
|
||||
|
||||
|
||||
results = []
|
||||
for mode in modes:
|
||||
if mode == 'left-right':
|
||||
if mode == 'left-right': # Most popular format. Common use case: displaying in HMD.
|
||||
results.append(np.hstack([left_eye, right_eye]))
|
||||
elif mode == 'right-left':
|
||||
elif mode == 'right-left': # Cross-viewing
|
||||
results.append(np.hstack([right_eye, left_eye]))
|
||||
elif mode == 'top-bottom':
|
||||
results.append(np.vstack([left_eye, right_eye]))
|
||||
elif mode == 'bottom-top':
|
||||
results.append(np.vstack([right_eye, left_eye]))
|
||||
elif mode == 'red-cyan-anaglyph':
|
||||
elif mode == 'red-cyan-anaglyph': # Anaglyth glasses
|
||||
results.append(overlap_red_cyan(left_eye, right_eye))
|
||||
elif mode == 'left-only':
|
||||
results.append(left_eye)
|
||||
elif mode == 'only-right':
|
||||
results.append(right_eye)
|
||||
elif mode == 'cyan-red-reverseanaglyph':
|
||||
elif mode == 'cyan-red-reverseanaglyph': # Anaglyth glasses worn upside down
|
||||
# Better for people whose main eye is left
|
||||
results.append(overlap_red_cyan(right_eye, left_eye))
|
||||
else:
|
||||
raise Exception('Unknown mode')
|
||||
|
||||
stereo_images = [Image.fromarray(r) for r in results]
|
||||
if return_modified_depth:
|
||||
if direction_aware_depth_blur:
|
||||
left_mod_img = Image.fromarray(np.clip(mod_left, 0, 255).astype(np.uint8))
|
||||
right_mod_img = Image.fromarray(np.clip(mod_right, 0, 255).astype(np.uint8))
|
||||
return stereo_images, left_mod_img, right_mod_img
|
||||
else:
|
||||
mod_img = Image.fromarray(np.clip(mod_depth, 0, 255).astype(np.uint8))
|
||||
return stereo_images, mod_img
|
||||
else:
|
||||
return stereo_images
|
||||
return [Image.fromarray(r) for r in results]
|
||||
|
||||
|
||||
def apply_stereo_divergence(original_image, depth, divergence, separation, stereo_offset_exponent, fill_technique):
|
||||
"""
|
||||
Dispatches to the desired stereo mapping algorithm.
|
||||
"""
|
||||
assert original_image.shape[:2] == depth.shape, 'Depthmap and the image must have the same size'
|
||||
depth_min = depth.min()
|
||||
depth_max = depth.max()
|
||||
normalized_depth = (depth - depth_min) / (depth_max - depth_min)
|
||||
divergence_px = (divergence / 100.0) * original_image.shape[1]
|
||||
separation_px = (separation / 100.0) * original_image.shape[1]
|
||||
|
||||
if fill_technique == 'none_post':
|
||||
return apply_stereo_divergence_naive_post(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
if fill_technique == 'inverse_post':
|
||||
return apply_stereo_divergence_inverse_post(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
if fill_technique == 'hybrid_edge_plus':
|
||||
return apply_stereo_divergence_hybrid_edge_plus(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
if fill_technique == 'hybrid_edge':
|
||||
return apply_stereo_divergence_hybrid_edge(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
|
||||
if fill_technique in ['none', 'naive', 'naive_interpolating']:
|
||||
return apply_stereo_divergence_naive(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent, fill_technique)
|
||||
return apply_stereo_divergence_naive(
|
||||
original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent, fill_technique
|
||||
)
|
||||
if fill_technique in ['polylines_soft', 'polylines_sharp']:
|
||||
return apply_stereo_divergence_polylines(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent, fill_technique)
|
||||
if fill_technique == 'inverse':
|
||||
return apply_stereo_divergence_inverse(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
return apply_stereo_divergence_polylines(
|
||||
original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent, fill_technique
|
||||
)
|
||||
|
||||
return original_image # Fallback
|
||||
|
||||
@njit(parallel=True)
|
||||
def enhanced_inverse_mapping_with_mask(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float):
|
||||
"""
|
||||
Enhanced inverse mapping that distributes each source pixel's color over three adjacent columns
|
||||
using a Gaussian kernel. Returns both the accumulated image and a binary mask.
|
||||
"""
|
||||
h, w, c = original_image.shape
|
||||
accum = np.zeros((h, w, c), dtype=np.float64)
|
||||
weight_sum = np.zeros((h, w), dtype=np.float64)
|
||||
mask = np.zeros((h, w), dtype=np.uint8)
|
||||
sigma = 1.0 # standard deviation for the subpixel kernel
|
||||
for row in prange(h):
|
||||
for x in range(w):
|
||||
offset = (normalized_depth[row, x] ** stereo_offset_exponent) * divergence_px
|
||||
dest_x = x + 0.5 + offset + separation_px
|
||||
j_center = int(math.floor(dest_x))
|
||||
for d in (-1, 0, 1):
|
||||
j = j_center + d
|
||||
if j >= 0 and j < w:
|
||||
diff = dest_x - j
|
||||
wght = math.exp(- (diff * diff) / (2 * sigma * sigma))
|
||||
for ch in range(c):
|
||||
accum[row, j, ch] += original_image[row, x, ch] * wght
|
||||
weight_sum[row, j] += wght
|
||||
mask[row, j] = 1
|
||||
# Normalize the accumulated image.
|
||||
output = np.zeros((h, w, c), dtype=np.uint8)
|
||||
for row in range(h):
|
||||
for j in range(w):
|
||||
if weight_sum[row, j] > 0:
|
||||
for ch in range(c):
|
||||
val = accum[row, j, ch] / weight_sum[row, j]
|
||||
if val < 0:
|
||||
val = 0
|
||||
elif val > 255:
|
||||
val = 255
|
||||
output[row, j, ch] = int(val)
|
||||
return output, mask
|
||||
|
||||
|
||||
@njit(parallel=True)
|
||||
def naive_mapping_with_mask(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float):
|
||||
h, w, c = original_image.shape
|
||||
derived_image = np.zeros_like(original_image)
|
||||
filled = np.zeros(h * w, dtype=np.uint8)
|
||||
for row in prange(h):
|
||||
if divergence_px < 0:
|
||||
rng = range(w)
|
||||
else:
|
||||
rng = range(w - 1, -1, -1)
|
||||
for col in rng:
|
||||
offset = (normalized_depth[row, col] ** stereo_offset_exponent) * divergence_px + separation_px
|
||||
col_d = col + int(offset)
|
||||
if 0 <= col_d < w:
|
||||
derived_image[row, col_d] = original_image[row, col]
|
||||
filled[row * w + col_d] = 1
|
||||
filled_mask = np.empty((h, w), dtype=np.uint8)
|
||||
for i in range(h * w):
|
||||
filled_mask.flat[i] = filled[i]
|
||||
return derived_image, filled_mask
|
||||
|
||||
|
||||
@njit(parallel=True)
|
||||
def inverse_mapping_with_mask(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float):
|
||||
h, w, c = original_image.shape
|
||||
derived_image = np.zeros_like(original_image)
|
||||
mask = np.zeros((h, w), dtype=np.uint8)
|
||||
for row in prange(h):
|
||||
depth_buffer = np.full(w, -1.0, dtype=np.float64)
|
||||
for x in range(w):
|
||||
offset = (normalized_depth[row, x] ** stereo_offset_exponent) * divergence_px
|
||||
dest_x = x + 0.5 + offset + separation_px
|
||||
closeness = normalized_depth[row, x]
|
||||
j = int(np.floor(dest_x))
|
||||
frac = dest_x - j
|
||||
if 0 <= j < w:
|
||||
if closeness > depth_buffer[j]:
|
||||
derived_image[row, j] = original_image[row, x]
|
||||
depth_buffer[j] = closeness
|
||||
mask[row, j] = 1
|
||||
if 0 <= j + 1 < w:
|
||||
if closeness > depth_buffer[j + 1]:
|
||||
derived_image[row, j + 1] = original_image[row, x]
|
||||
depth_buffer[j + 1] = closeness
|
||||
mask[row, j + 1] = 1
|
||||
return derived_image, mask
|
||||
|
||||
@njit(parallel=True)
|
||||
def apply_stereo_divergence_inverse(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float):
|
||||
h, w, c = original_image.shape
|
||||
derived_image = np.zeros_like(original_image)
|
||||
for row in prange(h):
|
||||
depth_buffer = np.full(w, -1.0, dtype=np.float64)
|
||||
for x in range(w):
|
||||
offset = (normalized_depth[row, x] ** stereo_offset_exponent) * divergence_px
|
||||
dest_x = x + 0.5 + offset + separation_px
|
||||
closeness = normalized_depth[row, x]
|
||||
j = int(np.floor(dest_x))
|
||||
frac = dest_x - j
|
||||
if 0 <= j < w:
|
||||
if closeness > depth_buffer[j]:
|
||||
derived_image[row, j] = original_image[row, x]
|
||||
depth_buffer[j] = closeness
|
||||
if 0 <= j + 1 < w:
|
||||
if closeness > depth_buffer[j + 1]:
|
||||
derived_image[row, j + 1] = original_image[row, x]
|
||||
depth_buffer[j + 1] = closeness
|
||||
return derived_image
|
||||
|
||||
|
||||
def rgb2gray(image):
|
||||
"""Convert an RGB image (H x W x 3) to grayscale using standard weights."""
|
||||
return 0.299 * image[:, :, 0] + 0.587 * image[:, :, 1] + 0.114 * image[:, :, 2]
|
||||
|
||||
|
||||
def edge_aware_gap_fill(image, mask, guidance, window_size=3, sigma_s=1.0, sigma_r=10.0):
|
||||
"""
|
||||
For each pixel not filled (mask==0) in 'image', perform 2D interpolation using neighboring
|
||||
pixels that are filled. The weights are computed based on both spatial distance and guidance difference.
|
||||
'guidance' is a single-channel (grayscale) image used to preserve edges.
|
||||
"""
|
||||
h, w, c = image.shape
|
||||
filled = image.astype(np.float64).copy()
|
||||
half_win = window_size // 2
|
||||
for i in range(h):
|
||||
for j in range(w):
|
||||
if mask[i, j] == 0:
|
||||
new_val = np.zeros(c, dtype=np.float64)
|
||||
weight_total = 0.0
|
||||
for di in range(-half_win, half_win+1):
|
||||
for dj in range(-half_win, half_win+1):
|
||||
ni = i + di
|
||||
nj = j + dj
|
||||
if ni >= 0 and ni < h and nj >= 0 and nj < w:
|
||||
if mask[ni, nj] != 0:
|
||||
dsq = di*di + dj*dj
|
||||
w_s = math.exp(- dsq / (2 * sigma_s * sigma_s))
|
||||
diff = guidance[i, j] - guidance[ni, nj]
|
||||
w_r = math.exp(- (diff*diff) / (2 * sigma_r * sigma_r))
|
||||
wght = w_s * w_r
|
||||
new_val += image[ni, nj].astype(np.float64) * wght
|
||||
weight_total += wght
|
||||
if weight_total > 0:
|
||||
filled[i, j] = new_val / weight_total
|
||||
return np.clip(filled, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
|
||||
def apply_stereo_divergence_hybrid_edge_plus(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent):
|
||||
"""
|
||||
Hybrid method that first uses enhanced inverse mapping with 2D edge-aware gap filling
|
||||
(the "hybrid_edge" method) and then, for any pixels that remain unfilled (detected as black),
|
||||
uses a fallback from the polylines_soft method.
|
||||
"""
|
||||
# First, get the initial result using our enhanced method.
|
||||
base_img, mask = enhanced_inverse_mapping_with_mask(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
# Compute a guidance image from the original (grayscale)
|
||||
guidance = rgb2gray(np.asarray(original_image))
|
||||
# Apply 2D edge-aware gap filling:
|
||||
filled_img = edge_aware_gap_fill(base_img, mask, guidance, window_size=3, sigma_s=1.0, sigma_r=10.0)
|
||||
|
||||
# Next, compute an alternative mapping using the polylines_soft method.
|
||||
poly_img = apply_stereo_divergence_polylines(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent, 'polylines_soft')
|
||||
|
||||
# Finally, combine the two: for any pixel where filled_img remains black, use the poly_img pixel.
|
||||
h, w, c = filled_img.shape
|
||||
final_img = filled_img.copy()
|
||||
for i in range(h):
|
||||
for j in range(w):
|
||||
# If a pixel is unfilled, we assume its channels are all zero.
|
||||
if (final_img[i, j, 0] == 0 and final_img[i, j, 1] == 0 and final_img[i, j, 2] == 0):
|
||||
final_img[i, j] = poly_img[i, j]
|
||||
return final_img
|
||||
|
||||
def apply_stereo_divergence_naive_post(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent):
|
||||
base_img, mask = naive_mapping_with_mask(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
h, w, c = base_img.shape
|
||||
output = base_img.astype(np.float64).copy()
|
||||
for row in range(h):
|
||||
x_coords = np.arange(w, dtype=np.float64)
|
||||
valid = np.nonzero(mask[row])[0]
|
||||
if valid.size == 0:
|
||||
continue
|
||||
for ch in range(c):
|
||||
row_data = base_img[row, :, ch].astype(np.float64)
|
||||
interpolated = np.interp(x_coords, valid.astype(np.float64), row_data[valid])
|
||||
output[row, :, ch] = interpolated
|
||||
return output.astype(np.uint8)
|
||||
|
||||
|
||||
def apply_stereo_divergence_inverse_post(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent):
|
||||
base_img, mask = inverse_mapping_with_mask(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
h, w, c = base_img.shape
|
||||
output = base_img.astype(np.float64).copy()
|
||||
for row in range(h):
|
||||
x_coords = np.arange(w, dtype=np.float64)
|
||||
valid = np.nonzero(mask[row])[0]
|
||||
if valid.size == 0:
|
||||
continue
|
||||
for ch in range(c):
|
||||
row_data = base_img[row, :, ch].astype(np.float64)
|
||||
interpolated = np.interp(x_coords, valid.astype(np.float64), row_data[valid])
|
||||
output[row, :, ch] = interpolated
|
||||
return output.astype(np.uint8)
|
||||
|
||||
|
||||
|
||||
def apply_stereo_divergence_hybrid_edge(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent):
|
||||
"""
|
||||
Hybrid method: uses enhanced inverse mapping (with subpixel distribution over 3 columns) to produce
|
||||
an initial stereo image and mask, then applies 2D, edge-aware gap filling.
|
||||
"""
|
||||
# First, get the base image and mask using the enhanced mapping.
|
||||
base_img, mask = enhanced_inverse_mapping_with_mask(original_image, normalized_depth, divergence_px, separation_px, stereo_offset_exponent)
|
||||
# Compute a guidance image (grayscale) from the original image.
|
||||
guidance = rgb2gray(np.asarray(original_image))
|
||||
# Apply 2D edge-aware gap filling over the entire image.
|
||||
filled_img = edge_aware_gap_fill(base_img, mask, guidance, window_size=3, sigma_s=1.0, sigma_r=10.0)
|
||||
return filled_img
|
||||
|
||||
@njit(parallel=False)
|
||||
def apply_stereo_divergence_naive(
|
||||
@@ -470,6 +114,7 @@ def apply_stereo_divergence_naive(
|
||||
if fill_technique == 'naive_interpolating':
|
||||
for row in range(h):
|
||||
for l_pointer in range(w):
|
||||
# This if (and the next if) performs two checks that are almost the same - for performance reasons
|
||||
if sum(derived_image[row][l_pointer]) != 0 or filled[row * w + l_pointer]:
|
||||
continue
|
||||
l_border = derived_image[row][l_pointer - 1] if l_pointer > 0 else np.zeros(3, dtype=np.uint8)
|
||||
@@ -484,6 +129,11 @@ def apply_stereo_divergence_naive(
|
||||
l_border = r_border
|
||||
elif sum(r_border) == 0:
|
||||
r_border = l_border
|
||||
# Example illustrating positions of pointers at this point in code:
|
||||
# is filled? : + - - - - +
|
||||
# pointers : l r
|
||||
# interpolated: 0 1 2 3 4 5
|
||||
# In total: 5 steps between two filled pixels
|
||||
total_steps = 1 + r_pointer - l_pointer
|
||||
step = (r_border.astype(np.float_) - l_border) / total_steps
|
||||
for col in range(l_pointer, r_pointer):
|
||||
@@ -508,13 +158,22 @@ def apply_stereo_divergence_naive(
|
||||
else: # none
|
||||
return derived_image
|
||||
|
||||
@njit(parallel=True)
|
||||
def apply_stereo_divergence_polylines(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float, fill_technique: str):
|
||||
|
||||
@njit(parallel=True) # fastmath=True does not reasonably improve performance
|
||||
def apply_stereo_divergence_polylines(
|
||||
original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float,
|
||||
fill_technique: str):
|
||||
# This code treats rows of the image as polylines
|
||||
# It generates polylines, morphs them (applies divergence) to them, and then rasterizes them
|
||||
EPSILON = 1e-7
|
||||
PIXEL_HALF_WIDTH = 0.45 if fill_technique == 'polylines_sharp' else 0.0
|
||||
# PERF_COUNTERS = [0, 0, 0]
|
||||
|
||||
h, w, c = original_image.shape
|
||||
derived_image = np.zeros_like(original_image)
|
||||
for row in prange(h):
|
||||
# generating the vertices of the morphed polyline
|
||||
# format: new coordinate of the vertex, divergence (closeness), column of pixel that contains the point's color
|
||||
pt = np.zeros((5 + 2 * w, 3), dtype=np.float_)
|
||||
pt_end: int = 0
|
||||
pt[pt_end] = [-1.0 * w, 0.0, 0.0]
|
||||
@@ -531,34 +190,60 @@ def apply_stereo_divergence_polylines(original_image, normalized_depth, divergen
|
||||
pt_end += 2
|
||||
pt[pt_end] = [2.0 * w, 0.0, w - 1]
|
||||
pt_end += 1
|
||||
|
||||
# generating the segments of the morphed polyline
|
||||
# format: coord_x, coord_d, color_i of the first point, then the same for the second point
|
||||
sg_end: int = pt_end - 1
|
||||
sg = np.zeros((sg_end, 6), dtype=np.float_)
|
||||
for i in range(sg_end):
|
||||
sg[i] += np.concatenate((pt[i], pt[i + 1]))
|
||||
# Here is an informal proof that this (morphed) polyline does not self-intersect:
|
||||
# Draw a plot with two axes: coord_x and coord_d. Now draw the original line - it will be positioned at the
|
||||
# bottom of the graph (that is, for every point coord_d == 0). Now draw the morphed line using the vertices of
|
||||
# the original polyline. Observe that for each vertex in the new polyline, its increments
|
||||
# (from the corresponding vertex in the old polyline) over coord_x and coord_d are in direct proportion.
|
||||
# In fact, this proportion is equal for all the vertices and it is equal either -1 or +1,
|
||||
# depending on the sign of divergence_px. Now draw the lines from each old vertex to a corresponding new vertex.
|
||||
# Since the proportions are equal, these lines have the same angle with an axe and are parallel.
|
||||
# So, these lines do not intersect. Now rotate the plot by 45 or -45 degrees and observe that
|
||||
# each dot of the polyline is further right from the last dot,
|
||||
# which makes it impossible for the polyline to self-intersect. QED.
|
||||
|
||||
# sort segments and points using insertion sort
|
||||
# has a very good performance in practice, since these are almost sorted to begin with
|
||||
for i in range(1, sg_end):
|
||||
u = i - 1
|
||||
while pt[u][0] > pt[u + 1][0] and 0 <= u:
|
||||
pt[u], pt[u + 1] = np.copy(pt[u + 1]), np.copy(pt[u])
|
||||
sg[u], sg[u + 1] = np.copy(sg[u + 1]), np.copy(sg[u])
|
||||
u -= 1
|
||||
|
||||
# rasterizing
|
||||
# at each point in time we keep track of segments that are "active" (or "current")
|
||||
csg = np.zeros((5 * int(abs(divergence_px)) + 25, 6), dtype=np.float_)
|
||||
csg_end: int = 0
|
||||
sg_pointer: int = 0
|
||||
# and index of the point that should be processed next
|
||||
pt_i: int = 0
|
||||
for col in range(w):
|
||||
color = np.full(c, 0.5, dtype=np.float_)
|
||||
for col in range(w): # iterate over regions (that will be rasterized into pixels)
|
||||
color = np.full(c, 0.5, dtype=np.float_) # we start with 0.5 because of how floats are converted to ints
|
||||
while pt[pt_i][0] < col:
|
||||
pt_i += 1
|
||||
pt_i -= 1
|
||||
pt_i -= 1 # pt_i now points to the dot before the region start
|
||||
# Finding segment' parts that contribute color to the region
|
||||
while pt[pt_i][0] < col + 1:
|
||||
coord_from = max(col, pt[pt_i][0]) + EPSILON
|
||||
coord_to = min(col + 1, pt[pt_i + 1][0]) - EPSILON
|
||||
significance = coord_to - coord_from
|
||||
# the color at center point is the same as the average of color of segment part
|
||||
coord_center = coord_from + 0.5 * significance
|
||||
|
||||
# adding segments that now may contribute
|
||||
while sg_pointer < sg_end and sg[sg_pointer][0] < coord_center:
|
||||
csg[csg_end] = sg[sg_pointer]
|
||||
sg_pointer += 1
|
||||
csg_end += 1
|
||||
# removing segments that will no longer contribute
|
||||
csg_i = 0
|
||||
while csg_i < csg_end:
|
||||
if csg[csg_i][3] < coord_center:
|
||||
@@ -566,42 +251,57 @@ def apply_stereo_divergence_polylines(original_image, normalized_depth, divergen
|
||||
csg_end -= 1
|
||||
else:
|
||||
csg_i += 1
|
||||
# finding the closest segment (segment with most divergence)
|
||||
# note that this segment will be the closest from coord_from right up to coord_to, since there
|
||||
# no new segments "appearing" inbetween these two and _the polyline does not self-intersect_
|
||||
best_csg_i: int = 0
|
||||
# PERF_COUNTERS[0] += 1
|
||||
if csg_end != 1:
|
||||
# PERF_COUNTERS[1] += 1
|
||||
best_csg_closeness: float = -EPSILON
|
||||
for csg_i in range(csg_end):
|
||||
ip_k = (coord_center - csg[csg_i][0]) / (csg[csg_i][3] - csg[csg_i][0])
|
||||
# assert 0.0 <= ip_k <= 1.0
|
||||
closeness = (1.0 - ip_k) * csg[csg_i][1] + ip_k * csg[csg_i][4]
|
||||
if best_csg_closeness < closeness and 0.0 < ip_k < 1.0:
|
||||
best_csg_closeness = closeness
|
||||
best_csg_i = csg_i
|
||||
# getting the color
|
||||
col_l: int = int(csg[best_csg_i][2] + EPSILON)
|
||||
col_r: int = int(csg[best_csg_i][5] + EPSILON)
|
||||
if col_l == col_r:
|
||||
color += original_image[row][col_l] * significance
|
||||
else:
|
||||
# PERF_COUNTERS[2] += 1
|
||||
ip_k = (coord_center - csg[best_csg_i][0]) / (csg[best_csg_i][3] - csg[best_csg_i][0])
|
||||
color += (original_image[row][col_l] * (1.0 - ip_k) +
|
||||
original_image[row][col_r] * ip_k
|
||||
) * significance
|
||||
pt_i += 1
|
||||
derived_image[row][col] = np.asarray(color, dtype=np.uint8)
|
||||
# print(PERF_COUNTERS)
|
||||
return derived_image
|
||||
|
||||
|
||||
|
||||
@njit(parallel=True)
|
||||
def overlap_red_cyan(im1, im2):
|
||||
width1 = im1.shape[1]
|
||||
height1 = im1.shape[0]
|
||||
width2 = im2.shape[1]
|
||||
height2 = im2.shape[0]
|
||||
|
||||
# final image
|
||||
composite = np.zeros((height2, width2, 3), np.uint8)
|
||||
|
||||
# iterate through "left" image, filling in red values of final image
|
||||
for i in prange(height1):
|
||||
for j in range(width1):
|
||||
composite[i, j, 0] = im1[i, j, 0]
|
||||
|
||||
# iterate through "right" image, filling in blue/green values of final image
|
||||
for i in prange(height2):
|
||||
for j in range(width2):
|
||||
composite[i, j, 1] = im2[i, j, 1]
|
||||
composite[i, j, 2] = im2[i, j, 2]
|
||||
|
||||
return composite
|
||||
|
||||
Reference in New Issue
Block a user