|
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 is for multi-projector edge blending.
|
|
8
|
## It assumes two projected images overlap horizontally and creates a smooth alpha transition
|
|
9
|
## in the overlap area so that the seam becomes visually less noticeable.
|
|
10
|
##
|
|
11
|
## Main idea:
|
|
12
|
## - A linear alpha ramp (fade-in / fade-out) is generated over the overlap region.
|
|
13
|
## - The ramp is then gamma-corrected to match how projectors actually respond to input intensity.
|
|
14
|
## (Projectors are not linear devices; without correction, the blended region can look too dark/light.)
|
|
15
|
##
|
|
16
|
## Output:
|
|
17
|
## - The output image is saved as a PNG with an updated alpha channel.
|
|
18
|
## - Optionally, the RGB channels in the overlap region are also multiplied by the same mask
|
|
19
|
## as a debug visualization (helps confirm the fade direction and mask shape).
|
|
20
|
|
|
21
|
import cv2
|
|
22
|
import numpy as np
|
|
23
|
from config_reader import ConfigReader
|
|
24
|
|
|
25
|
|
|
26
|
class MainAlphaBlender(object):
|
|
27
|
"""
|
|
28
|
@brief Main controller class for executing the gamma-corrected alpha-blending pipeline.
|
|
29
|
@details
|
|
30
|
This class performs an end-to-end pipeline:
|
|
31
|
- Reads configuration values (input image path, blending side, physical setup, gamma).
|
|
32
|
- Loads a PNG image (ensuring BGRA so alpha can be modified safely).
|
|
33
|
- Computes the overlap width in pixels based on the physical projector setup.
|
|
34
|
- Builds an alpha mask curve (linear ramp) for the overlap region.
|
|
35
|
- Applies gamma correction to the mask curve.
|
|
36
|
- Multiplies the image alpha channel by the mask in the overlap region.
|
|
37
|
- Saves a new PNG with adjusted transparency.
|
|
38
|
"""
|
|
39
|
|
|
40
|
def __init__(self):
|
|
41
|
"""
|
|
42
|
@brief Constructor for MainAlphaBlender.
|
|
43
|
@details
|
|
44
|
Initializes a ConfigReader instance to retrieve blending parameters.
|
|
45
|
The config reader is expected to provide:
|
|
46
|
- input image filename
|
|
47
|
- which side of the overlap this projector corresponds to ("left" or "right")
|
|
48
|
- physical projected image width
|
|
49
|
- physical distance between two projectors
|
|
50
|
- gamma value used for correction
|
|
51
|
@see config_reader.py
|
|
52
|
"""
|
|
53
|
self.__configReader = None
|
|
54
|
self.__configReader = ConfigReader()
|
|
55
|
|
|
56
|
def run(self):
|
|
57
|
"""
|
|
58
|
@brief Executes the full alpha blending process (gamma corrected).
|
|
59
|
@details
|
|
60
|
steps:
|
|
61
|
1) Load configuration parameters:
|
|
62
|
- image_name: input image file (ideally PNG with alpha)
|
|
63
|
- side: "left" or "right" indicating which projector image this is
|
|
64
|
- proj_width_phys: physical width of one projected image
|
|
65
|
- dist_phys: physical distance between projector centers (or image origins depending on setup)
|
|
66
|
- gamma: projector gamma value (> 0)
|
|
67
|
|
|
68
|
2) Load image using OpenCV:
|
|
69
|
- cv2.IMREAD_UNCHANGED preserves alpha if present.
|
|
70
|
- If the image is BGR (3 channels), it is converted to BGRA (4 channels)
|
|
71
|
so we can write an alpha mask safely.
|
|
72
|
|
|
73
|
3) Compute overlap ratio and overlap width (pixels):
|
|
74
|
- overlap_ratio = 1 - (dist_phys / proj_width_phys)
|
|
75
|
Example:
|
|
76
|
dist_phys == proj_width_phys -> overlap_ratio = 0 (no overlap)
|
|
77
|
dist_phys < proj_width_phys -> overlap_ratio > 0 (overlap exists)
|
|
78
|
- overlap_pixels = int(width * overlap_ratio)
|
|
79
|
|
|
80
|
4) Generate a base linear ramp across the overlap region:
|
|
81
|
- linear_ramp is always 0 → 1 across overlap_pixels.
|
|
82
|
- For the left image: fade out (1 → 0) in the overlap.
|
|
83
|
- For the right image: fade in (0 → 1) in the overlap.
|
|
84
|
|
|
85
|
5) Gamma correction:
|
|
86
|
- Projector brightness response is non-linear.
|
|
87
|
- We compensate by applying:
|
|
88
|
mask_curve = base_curve^(1/gamma)
|
|
89
|
- This helps produce a visually smoother blend once projected.
|
|
90
|
|
|
91
|
6) Apply the mask to the alpha channel:
|
|
92
|
- Convert alpha to float in [0, 1].
|
|
93
|
- Expand mask_curve vertically to match image height.
|
|
94
|
- Multiply only the overlap region of alpha.
|
|
95
|
|
|
96
|
7) Save output image:
|
|
97
|
- Writes output_left.png or output_right.png depending on side.
|
|
98
|
|
|
99
|
Error / early-exit conditions:
|
|
100
|
- If the input image cannot be loaded, the process stops.
|
|
101
|
- If overlap_ratio <= 0, there is no overlap (or invalid physical config), so it stops.
|
|
102
|
- If side is not "left" or "right", it stops.
|
|
103
|
|
|
104
|
@return This returns None.
|
|
105
|
@see config_reader.py
|
|
106
|
"""
|
|
107
|
|
|
108
|
print("--- Starting Alpha Blend Process (Gamma Corrected) ---")
|
|
109
|
|
|
110
|
# 1. Get Configuration
|
|
111
|
## @details Retrieve all blending parameters from ConfigReader.
|
|
112
|
image_name = self.__configReader.getImageName()
|
|
113
|
side = self.__configReader.getSide()
|
|
114
|
proj_width_phys = self.__configReader.getProjectedImageWidth()
|
|
115
|
dist_phys = self.__configReader.getDistanceBetweenProjectors()
|
|
116
|
gamma = self.__configReader.getGamma()
|
|
117
|
|
|
118
|
# 2. Loading Image using OpenCV
|
|
119
|
## @details Load the image with alpha preserved (if present).
|
|
120
|
img = cv2.imread(image_name, cv2.IMREAD_UNCHANGED)
|
|
121
|
if img is None:
|
|
122
|
print(f"Error: Could not load image '{image_name}'")
|
|
123
|
return
|
|
124
|
|
|
125
|
# Ensure BGRA format for safe alpha manipulation
|
|
126
|
## @details If the image has only 3 channels (BGR), add an alpha channel.
|
|
127
|
if img.shape[2] == 3:
|
|
128
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
|
129
|
|
|
130
|
height, width = img.shape[:2]
|
|
131
|
|
|
132
|
# 3. Calculate Overlap Ratio and Pixels
|
|
133
|
## @details
|
|
134
|
## overlap_ratio expresses the horizontal overlap fraction of the projected image:
|
|
135
|
## overlap_ratio = 1 - (distance_between_projectors / projected_image_width)
|
|
136
|
## If projected_image_width <= 0, overlap_ratio is forced to 0 to avoid division errors.
|
|
137
|
overlap_ratio = 1.0 - (dist_phys / proj_width_phys) if proj_width_phys > 0 else 0.0
|
|
138
|
|
|
139
|
## @details If overlap_ratio <= 0, there is no meaningful overlap to blend.
|
|
140
|
if overlap_ratio <= 0:
|
|
141
|
print("Warning: No overlap detected.")
|
|
142
|
return
|
|
143
|
|
|
144
|
overlap_pixels = int(width * overlap_ratio)
|
|
145
|
print(f"Processing '{side}' | Overlap: {overlap_pixels} pixels | Gamma: {gamma}")
|
|
146
|
|
|
147
|
# 4. Create a linear fade mask for the overlap region
|
|
148
|
## @brief Linear ramp values spanning the overlap region.
|
|
149
|
## @details This always increases from 0 → 1 (direction is handled separately).
|
|
150
|
linear_ramp = np.linspace(0, 1, overlap_pixels)
|
|
151
|
|
|
152
|
# Determine mask direction based on side
|
|
153
|
## @details
|
|
154
|
## - left: needs to fade out in the overlap => 1 → 0
|
|
155
|
## - right: needs to fade in in the overlap => 0 → 1
|
|
156
|
if side == "left":
|
|
157
|
## @brief Left side base curve (fade-out).
|
|
158
|
base_curve = 1.0 - linear_ramp
|
|
159
|
elif side == "right":
|
|
160
|
## @brief Right side base curve (fade-in).
|
|
161
|
base_curve = linear_ramp
|
|
162
|
else:
|
|
163
|
print("Error: Side must be 'left' or 'right'")
|
|
164
|
|