Spaces:
Runtime error
Runtime error
try: | |
from numba import njit, prange | |
except Exception as 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 | |
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'): | |
"""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'] | |
if not isinstance(modes, list): | |
modes = [modes] | |
if len(modes) == 0: | |
return [] | |
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, 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, depthmap, -1 * divergence * (1 - balance), separation, | |
stereo_offset_exponent, fill_technique) | |
results = [] | |
for mode in modes: | |
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': # 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': # 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': # 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') | |
return [Image.fromarray(r) for r in results] | |
def apply_stereo_divergence(original_image, depth, divergence, separation, stereo_offset_exponent, fill_technique): | |
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 in ['none', 'naive', 'naive_interpolating']: | |
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 | |
) | |
def apply_stereo_divergence_naive( | |
original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float, | |
fill_technique: str): | |
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): | |
# Swipe order should ensure that pixels that are closer overwrite | |
# (at their destination) pixels that are less close | |
for col in range(w) if divergence_px < 0 else range(w - 1, -1, -1): | |
col_d = col + int((normalized_depth[row][col] ** stereo_offset_exponent) * divergence_px + separation_px) | |
if 0 <= col_d < w: | |
derived_image[row][col_d] = original_image[row][col] | |
filled[row * w + col_d] = 1 | |
# Fill the gaps | |
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) | |
r_border = np.zeros(3, dtype=np.uint8) | |
r_pointer = l_pointer + 1 | |
while r_pointer < w: | |
if sum(derived_image[row][r_pointer]) != 0 and filled[row * w + r_pointer]: | |
r_border = derived_image[row][r_pointer] | |
break | |
r_pointer += 1 | |
if sum(l_border) == 0: | |
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): | |
derived_image[row][col] = l_border + (step * (col - l_pointer + 1)).astype(np.uint8) | |
return derived_image | |
elif fill_technique == 'naive': | |
derived_fix = np.copy(derived_image) | |
for pos in np.where(filled == 0)[0]: | |
row = pos // w | |
col = pos % w | |
row_times_w = row * w | |
for offset in range(1, abs(int(divergence_px)) + 2): | |
r_offset = col + offset | |
l_offset = col - offset | |
if r_offset < w and filled[row_times_w + r_offset]: | |
derived_fix[row][col] = derived_image[row][r_offset] | |
break | |
if 0 <= l_offset and filled[row_times_w + l_offset]: | |
derived_fix[row][col] = derived_image[row][l_offset] | |
break | |
return derived_fix | |
else: # none | |
return derived_image | |
# 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] | |
pt_end += 1 | |
for col in range(0, w): | |
coord_d = (normalized_depth[row][col] ** stereo_offset_exponent) * divergence_px | |
coord_x = col + 0.5 + coord_d + separation_px | |
if PIXEL_HALF_WIDTH < EPSILON: | |
pt[pt_end] = [coord_x, abs(coord_d), col] | |
pt_end += 1 | |
else: | |
pt[pt_end] = [coord_x - PIXEL_HALF_WIDTH, abs(coord_d), col] | |
pt[pt_end + 1] = [coord_x + PIXEL_HALF_WIDTH, abs(coord_d), col] | |
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): # 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 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: | |
csg[csg_i] = csg[csg_end - 1] | |
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 | |
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 | |