File size: 11,847 Bytes
2cf467c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import numpy as np
from typing import Tuple
from .mask_utils import calculate_mask, expand_mask
import os
from PIL import Image

def find_best_size_and_position(main_mask: np.ndarray, image_content: str, padding: int, mode: str = "side", chart_bbox: dict = None, avoid_mask: np.ndarray = None) -> Tuple[int, int, int]:
    """
    通过降采样加速查找最佳图片尺寸和位置
    
    Args:
        main_mask: 主要内容的mask
        image_content: base64图片内容
        padding: 边界padding
        mode: 放置模式,可选"side"、"background"或"overlay"
        chart_bbox: 图表边界框,格式为{"x": x, "y": y, "width": width, "height": height}
        avoid_mask: 需要避免重叠的区域mask
    
    Returns:
        Tuple[int, int, int]: (image_size, best_x, best_y)
    """
    # Save the main_mask to PNG for debugging
    os.makedirs('tmp', exist_ok=True)
    mask_image = Image.fromarray((main_mask * 255).astype(np.uint8))
    mask_image.save('tmp/main_mask.png')
    
    grid_size = 5
    
    # 将main_mask降采样到1/grid_size大小
    h, w = main_mask.shape
    downsampled_h = h // grid_size
    downsampled_w = w // grid_size
    downsampled_main = np.zeros((downsampled_h, downsampled_w), dtype=np.uint8)
        
    # 对每个grid进行降采样,只要原grid中有内容(1)就标记为1
    for i in range(downsampled_h):
        for j in range(downsampled_w):
            y_start = max(0, (i - 1) * (grid_size))
            x_start = max(0, (j - 1) * (grid_size))
            y_end = min((i + 2) * (grid_size), h)
            x_end = min((j + 2) * (grid_size), w)
            grid = main_mask[y_start:y_end, x_start:x_end]
            downsampled_main[i, j] = 1 if np.any(grid == 1) else 0
    
    # 如果有avoid_mask,也进行降采样
    downsampled_avoid = None
    if avoid_mask is not None:
        downsampled_avoid = np.zeros((downsampled_h, downsampled_w), dtype=np.uint8)
        for i in range(downsampled_h):
            for j in range(downsampled_w):
                y_start = max(0, (i - 1) * (grid_size))
                x_start = max(0, (j - 1) * (grid_size))
                y_end = min((i + 2) * (grid_size), h)
                x_end = min((j + 2) * (grid_size), w)
                grid = avoid_mask[y_start:y_end, x_start:x_end]
                downsampled_avoid[i, j] = 1 if np.any(grid == 1) else 0
    
    # 调整padding到降采样尺度
    downsampled_padding = max(1, padding // grid_size)
    
    # 二分查找最佳尺寸
    min_size = max(1, 64 // grid_size)  # 最小尺寸也要降采样
    max_size = int(min(downsampled_main.shape) * 1)
    best_size = min_size
    best_x = downsampled_padding
    best_y = downsampled_padding
    
    if mode == "side":
        best_overlap_ratio = float('inf')
    elif mode == "background":
        best_overlap_ratio = float('inf')
    else:
        best_overlap_ratio = 0
        
    overlap_threshold = 0.01
    if mode == "side":
        overlap_threshold = 0.01
    elif mode == "background":
        overlap_threshold = 0.05
    elif mode == "overlay":
        overlap_threshold = 0.97
    
    while max_size - min_size >= 2:  # 由于降采样,可以用更小的阈值
        mid_size = (min_size + max_size) // 2
        
        # 生成当前尺寸的图片mask并降采样
        original_size = mid_size * grid_size
        temp_svg = f"""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{original_size}" height="{original_size}">
            <image width="{original_size}" height="{original_size}" href="{image_content}"/>
        </svg>"""
        image_mask = calculate_mask(temp_svg, original_size, original_size, 0, grid_size=grid_size, bg_threshold=240)
        if mode == "background":
            image_mask = expand_mask(image_mask, 10)
        # Save the original image mask to PNG for debugging
        os.makedirs('tmp', exist_ok=True)
        mask_image = Image.fromarray((image_mask * 255).astype(np.uint8))
        mask_image.save('tmp/image_mask.png')
        # 将image_mask降采样
        downsampled_image = np.zeros((mid_size, mid_size), dtype=np.uint8)
        for i in range(mid_size):
            for j in range(mid_size):
                y_start = max(0, (i - 1) * (grid_size))
                x_start = max(0, (j - 1) * (grid_size))
                y_end = min((i + 2) * (grid_size), original_size)
                x_end = min((j + 2) * (grid_size), original_size)
                grid = image_mask[y_start:y_end, x_start:x_end]
                downsampled_image[i, j] = 1 if np.any(grid == 1) else 0
        
        # 计算有效的搜索范围
        if mode == "background" and chart_bbox is not None:
            # 将chart_bbox转换到降采样尺度
            chart_x = max(0, chart_bbox["x"] // grid_size)
            chart_y = max(0, chart_bbox["y"] // grid_size)
            chart_width = min(chart_bbox["width"] // grid_size, downsampled_w - chart_x)
            chart_height = min(chart_bbox["height"] // grid_size, downsampled_h - chart_y)
            
            # 确保搜索范围在chart_bbox内
            y_range = chart_height - mid_size - downsampled_padding * 2
            x_range = chart_width - mid_size - downsampled_padding * 2
            
            if y_range <= 0 or x_range <= 0:
                max_size = mid_size - 1
                continue
        else:
            y_range = downsampled_h - mid_size - downsampled_padding * 2
            x_range = downsampled_w - mid_size - downsampled_padding * 2
            
            if y_range <= 0 or x_range <= 0:
                max_size = mid_size - 1
                continue
        
        # 在降采样空间中寻找最佳位置
        min_overlap = float('inf')
        if mode == "side" or mode == "background":
            min_overlap = float('inf')
        elif mode == "overlay":
            min_overlap = 0
        current_x = downsampled_padding
        current_y = downsampled_padding
        min_distance = float('inf')
        mask_center_x = np.mean(np.where(downsampled_main == 1)[1]) if np.any(downsampled_main == 1) else downsampled_w // 2
        mask_center_y = np.mean(np.where(downsampled_main == 1)[0]) if np.any(downsampled_main == 1) else downsampled_h // 2

        if mode == "background" and chart_bbox is not None:
            y_start = chart_y + downsampled_padding
            y_end = chart_y + chart_height - mid_size - downsampled_padding + 1
            x_start = chart_x + downsampled_padding
            x_end = chart_x + chart_width - mid_size - downsampled_padding + 1
        else:
            y_start = downsampled_padding
            y_end = downsampled_h - mid_size - downsampled_padding + 1
            x_start = downsampled_padding
            x_end = downsampled_w - mid_size - downsampled_padding + 1
        
        for y in range(y_start, y_end):
            for x in range(x_start, x_end):
                region = downsampled_main[y:y + mid_size, x:x + mid_size]
                
                overlap = np.sum((region == 1) & (downsampled_image == 1))
                total = np.sum(downsampled_image == 1)
                overlap_ratio = overlap / total if total > 0 else 1.0
                
                # 检查与avoid_mask的重叠
                avoid_overlap = 0
                if downsampled_avoid is not None:
                    avoid_region = downsampled_avoid[y:y + mid_size, x:x + mid_size]
                    avoid_overlap = np.sum((avoid_region == 1) & (downsampled_image == 1))
                
                if mode == "side" or mode == "background":
                    if mode == "background" and chart_bbox is not None:
                        distance_to_left = x - (chart_x + downsampled_padding)
                        distance_to_right = (chart_x + chart_width - mid_size - downsampled_padding) - x
                        distance_to_top = y - (chart_y + downsampled_padding)
                        distance_to_bottom = (chart_y + chart_height - mid_size - downsampled_padding) - y
                    else:
                        distance_to_left = x - downsampled_padding
                        distance_to_right = downsampled_w - mid_size - downsampled_padding - x
                        distance_to_top = y - downsampled_padding
                        distance_to_bottom = downsampled_h - mid_size - downsampled_padding - y

                    distance_to_border = min(distance_to_left, distance_to_right, distance_to_top, distance_to_bottom)
                    if overlap_ratio < min_overlap or (overlap_ratio < overlap_threshold and distance_to_border < min_distance):
                        min_overlap = overlap_ratio
                        current_x = x
                        current_y = y
                        min_distance = distance_to_border
                elif mode == "overlay":
                    # 对于overlay模式,需要同时满足与main_mask的重叠足够大,且与avoid_mask没有重叠
                    if avoid_overlap > 0:
                        continue  # 跳过与avoid_mask有重叠的位置
                    distance_to_center = np.sqrt(((x + mid_size / 2 - mask_center_x) ** 2 + (y + mid_size / 2 - mask_center_y) ** 2))
                    if overlap_ratio > min_overlap or (overlap_ratio > overlap_threshold and distance_to_center < min_distance):
                        min_overlap = overlap_ratio
                        current_x = x
                        current_y = y
                        min_distance = distance_to_center
        
        # print(f"Trying size {mid_size * grid_size}x{mid_size * grid_size}, minimum overlap ratio: {min_overlap:.3f}")
        
        if mode == "side" or mode == "background":
            if min_overlap < overlap_threshold:
                best_size = mid_size
                best_overlap_ratio = min_overlap
                best_x = current_x
                best_y = current_y
                min_size = mid_size + 1
            else:
                max_size = mid_size - 1
        elif mode == "overlay":
            if min_overlap > overlap_threshold:
                best_size = mid_size
                best_overlap_ratio = min_overlap
                best_x = current_x
                best_y = current_y
                min_size = mid_size + 1
            else:
                max_size = mid_size - 1

    if best_overlap_ratio > overlap_threshold and (mode == "side" or mode == "background"):
        return 0, 0, 0
    if best_overlap_ratio < overlap_threshold and mode == "overlay":
        return 0, 0, 0

    final_size = best_size * grid_size
    final_x = best_x * grid_size
    final_y = best_y * grid_size
    
    '''
    # 生成最终尺寸的图片mask
    temp_svg = f"""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{final_size}" height="{final_size}">
        <image width="{final_size}" height="{final_size}" href="{image_content}"/>
    </svg>"""
    final_image_mask = calculate_mask(temp_svg, final_size, final_size, 0)
    
    # 创建合并的mask,将image_mask放在正确的位置
    combined_mask = np.zeros_like(main_mask)
    combined_mask[main_mask == 1] = 1
    # 将image_mask放在正确的位置
    combined_mask[final_y:final_y + final_size, final_x:final_x + final_size] = np.where(final_image_mask == 1, 2, combined_mask[final_y:final_y + final_size, final_x:final_x + final_size])
    
    # 保存合并的mask
    combined_image = Image.fromarray((combined_mask * 127).astype(np.uint8))
    combined_image.save('tmp/all_mask.png')
    
    print(f"Final result: size={final_size}x{final_size}, position=({final_x}, {final_y}), overlap ratio={best_overlap_ratio:.3f}")
    '''
    return final_size, final_x, final_y