Project

General

Profile

Files » main_alpha_blender.py

KAUNG Zin Thu, 01/03/2026 11:12 PM

 
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
           
(4-4/10)