Revert "Update stereoimage_generation.py"

This reverts commit 9483638900.
This commit is contained in:
Dobidop
2025-02-20 10:07:08 +01:00
parent dccbb40276
commit c88b19cbea
+103 -403
View File
@@ -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