|
1
|
#!/usr/bin/env python
|
|
2
|
# -*- coding: utf-8 -*-
|
|
3
|
|
|
4
|
## @file main_alpha_blender.py
|
|
5
|
## @brief This program applies gamma-corrected alpha blending to overlapping projector images.
|
|
6
|
## @details
|
|
7
|
## This program finds the overlap between two projected images and computes
|
|
8
|
## a spatially smooth alpha mask. The mask is gamma-corrected to compensate for
|
|
9
|
## projector luminance behavior and to avoid visible seams in the blended region.
|
|
10
|
|
|
11
|
import cv2
|
|
12
|
import numpy as np
|
|
13
|
from config_reader import ConfigReader
|
|
14
|
|
|
15
|
|
|
16
|
class MainAlphaBlender(object):
|
|
17
|
"""
|
|
18
|
@brief This is the main controller class for executing the alpha-blending pipeline.
|
|
19
|
@details
|
|
20
|
This class loads configuration parameters imported from config_reader, generates a gamma-corrected
|
|
21
|
alpha mask, applies it to the selected side of the image, and outputs
|
|
22
|
a seamless PNG with adjusted transparency.
|
|
23
|
"""
|
|
24
|
|
|
25
|
def __init__(self):
|
|
26
|
"""
|
|
27
|
@brief This is the constructor for MainAlphaBlender.
|
|
28
|
@details This initializes and loads configuration parameters through ConfigReader.
|
|
29
|
@see config_reader.py
|
|
30
|
"""
|
|
31
|
self.__configReader = None
|
|
32
|
self.__configReader = ConfigReader()
|
|
33
|
|
|
34
|
def run(self):
|
|
35
|
"""
|
|
36
|
@brief This executes the entire alpha blending process.
|
|
37
|
@details
|
|
38
|
This function performs the following steps:
|
|
39
|
1. Load parameters from configuration.
|
|
40
|
2. Load the input image (must be PNG/BGRA).
|
|
41
|
3. Estimate the overlap region in pixels.
|
|
42
|
4. Generate a linear alpha curve depending on 'left' or 'right' side.
|
|
43
|
5. Apply gamma correction (crucial for projector luminance).
|
|
44
|
6. Write the alpha-corrected output image.
|
|
45
|
@return this will return None.
|
|
46
|
@see config_reader.py
|
|
47
|
"""
|
|
48
|
|
|
49
|
print("--- Starting Alpha Blend Process (Gamma Corrected) ---")
|
|
50
|
|
|
51
|
# 1. Get Configuration
|
|
52
|
image_name = self.__configReader.getImageName()
|
|
53
|
side = self.__configReader.getSide()
|
|
54
|
proj_width_phys = self.__configReader.getProjectedImageWidth()
|
|
55
|
dist_phys = self.__configReader.getDistanceBetweenProjectors()
|
|
56
|
gamma = self.__configReader.getGamma()
|
|
57
|
|
|
58
|
# 2. Loading Image using OpenCV
|
|
59
|
img = cv2.imread(image_name, cv2.IMREAD_UNCHANGED)
|
|
60
|
if img is None:
|
|
61
|
print(f"Error: Could not load image '{image_name}'")
|
|
62
|
return
|
|
63
|
|
|
64
|
# Ensure BGRA format for safe alpha manipulation
|
|
65
|
if img.shape[2] == 3:
|
|
66
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
|
67
|
|
|
68
|
height, width = img.shape[:2]
|
|
69
|
|
|
70
|
# 3. Calculate Overlap Ratio and Pixels
|
|
71
|
overlap_ratio = 1.0 - (dist_phys / proj_width_phys) if proj_width_phys > 0 else 0.0
|
|
72
|
|
|
73
|
if overlap_ratio <= 0:
|
|
74
|
print("Warning: No overlap detected.")
|
|
75
|
return
|
|
76
|
|
|
77
|
overlap_pixels = int(width * overlap_ratio)
|
|
78
|
print(f"Processing '{side}' | Overlap: {overlap_pixels} pixels | Gamma: {gamma}")
|
|
79
|
|
|
80
|
# 4. Create a linear fade mask for the overlap region
|
|
81
|
## @brief Linear ramp values (0 → 1)
|
|
82
|
linear_ramp = np.linspace(0, 1, overlap_pixels)
|
|
83
|
|
|
84
|
# Determine mask direction based on side
|
|
85
|
if side == "left":
|
|
86
|
## @brief Left side fades out (1 → 0)
|
|
87
|
base_curve = 1.0 - linear_ramp
|
|
88
|
elif side == "right":
|
|
89
|
## @brief Right side fades in (0 → 1)
|
|
90
|
base_curve = linear_ramp
|
|
91
|
else:
|
|
92
|
print("Error: Side must be 'left' or 'right'")
|
|
93
|
return
|
|
94
|
|
|
95
|
# 5. Apply Gamma Correction
|
|
96
|
## @details Raises the curve to power (1/gamma) to compensate projector response.
|
|
97
|
mask_curve = np.power(base_curve, 1.0 / gamma)
|
|
98
|
|
|
99
|
# 6. Apply Alpha Mask to Image
|
|
100
|
alpha_channel = img[:, :, 3].astype(float) / 255.0
|
|
101
|
mask_block = np.tile(mask_curve, (height, 1))
|
|
102
|
|
|
103
|
if side == "left":
|
|
104
|
alpha_channel[:, width - overlap_pixels:] *= mask_block
|
|
105
|
else:
|
|
106
|
alpha_channel[:, :overlap_pixels] *= mask_block
|
|
107
|
|
|
108
|
img[:, :, 3] = (alpha_channel * 255).astype(np.uint8)
|
|
109
|
|
|
110
|
# Optional RGB burn-in (debug visualization)
|
|
111
|
for c in range(3):
|
|
112
|
if side == "left":
|
|
113
|
roi = img[:, :, c][:, width - overlap_pixels:]
|
|
114
|
img[:, :, c][:, width - overlap_pixels:] = (roi * mask_block).astype(np.uint8)
|
|
115
|
else:
|
|
116
|
roi = img[:, :, c][:, :overlap_pixels]
|
|
117
|
img[:, :, c][:, :overlap_pixels] = (roi * mask_block).astype(np.uint8)
|
|
118
|
|
|
119
|
# 7. Save Output
|
|
120
|
output_name = f"output_{side}.png"
|
|
121
|
cv2.imwrite(output_name, img)
|
|
122
|
print(f"Saved: {output_name}")
|
|
123
|
|
|
124
|
|
|
125
|
if __name__ == "__main__":
|
|
126
|
"""
|
|
127
|
@brief Program entry point.
|
|
128
|
@details Instantiates MainAlphaBlender and runs the pipeline.
|
|
129
|
"""
|
|
130
|
blender = MainAlphaBlender()
|
|
131
|
blender.run()
|