File size: 30,050 Bytes
b3e6fcf
 
f1554a2
 
b3e6fcf
 
f1554a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1554a2
 
 
 
 
 
 
 
 
b3e6fcf
f1554a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3e6fcf
 
f1554a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1554a2
b3e6fcf
 
f1554a2
 
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1554a2
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1554a2
 
 
 
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c42b13
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c42b13
b3e6fcf
 
 
 
 
1c42b13
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c42b13
b3e6fcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
import time

import numpy as np
import cv2
import pandas as pd
from PIL import Image, ImageDraw, ImageFont


class Element:
    def __init__(self, id, corner, category, text_content=None):
        self.id = id
        self.category = category
        self.col_min, self.row_min, self.col_max, self.row_max = corner
        self.width = self.col_max - self.col_min
        self.height = self.row_max - self.row_min
        self.area = self.width * self.height

        self.text_content = text_content
        self.parent_id = None
        self.children = []  # list of elements
        self.label = None

    def __str__(self):
        return (f"Element(id={self.id}, category={self.category}, corner=({self.col_min}, {self.row_min}, "
                f"{self.col_max}, {self.row_max}), width={self.width}, height={self.height}, area={self.area}, "
                f"text_content={self.text_content}, label={self.label}, parent_id={self.parent_id}, "
                f"children_count={len(self.children)})")

    def __eq__(self, other):
        if not isinstance(other, Element):
            return False
        return (self.id == other.id and
                self.category == other.category and
                self.col_min == other.col_min and
                self.row_min == other.row_min and
                self.col_max == other.col_max and
                self.row_max == other.row_max and
                self.text_content == other.text_content and
                self.label == other.label)

    def center(self):
        # 计算中心点
        center_x = (self.col_min + self.col_max) / 2
        center_y = (self.row_min + self.row_max) / 2
        return (center_x, center_y)

    def init_bound(self):
        self.width = self.col_max - self.col_min
        self.height = self.row_max - self.row_min
        self.area = self.width * self.height

    def put_bbox(self):
        return self.col_min, self.row_min, self.col_max, self.row_max

    def wrap_info(self):
        info = {'id': self.id, 'class': self.category, 'height': self.height, 'width': self.width,
                'position': {'column_min': self.col_min, 'row_min': self.row_min, 'column_max': self.col_max,
                             'row_max': self.row_max}, 'label': self.label}
        if self.text_content is not None:
            info['text_content'] = self.text_content
        if len(self.children) > 0:
            info['children'] = []
            for child in self.children:
                info['children'].append(child.id)
        if self.parent_id is not None:
            info['parent'] = self.parent_id
        return info

    def resize(self, resize_ratio):
        self.col_min = int(self.col_min * resize_ratio)
        self.row_min = int(self.row_min * resize_ratio)
        self.col_max = int(self.col_max * resize_ratio)
        self.row_max = int(self.row_max * resize_ratio)
        self.init_bound()

    def element_merge(self, element_b, new_element=False, new_category=None, new_id=None):
        col_min_a, row_min_a, col_max_a, row_max_a = self.put_bbox()
        col_min_b, row_min_b, col_max_b, row_max_b = element_b.put_bbox()
        new_corner = (
            min(col_min_a, col_min_b), min(row_min_a, row_min_b), max(col_max_a, col_max_b), max(row_max_a, row_max_b))
        if element_b.text_content is not None:
            self.text_content = element_b.text_content if self.text_content is None else self.text_content + '\n' + element_b.text_content
        if new_element:
            return Element(new_id, new_corner, new_category)
        else:
            self.col_min, self.row_min, self.col_max, self.row_max = new_corner
            self.init_bound()

    def calc_intersection_area(self, element_b, bias=(0, 0)):
        a = self.put_bbox()
        b = element_b.put_bbox()
        col_min_s = max(a[0], b[0]) - bias[0]
        row_min_s = max(a[1], b[1]) - bias[1]
        col_max_s = min(a[2], b[2])
        row_max_s = min(a[3], b[3])
        w = np.maximum(0, col_max_s - col_min_s)
        h = np.maximum(0, row_max_s - row_min_s)
        inter = w * h

        iou = inter / (self.area + element_b.area - inter)
        ioa = inter / self.area
        iob = inter / element_b.area

        return inter, iou, ioa, iob

    def element_relation(self, element_b, bias=(0, 0)):
        """
        @bias: (horizontal bias, vertical bias)
        :return: -1 : a in b
                 0  : a, b are not intersected
                 1  : b in a
                 2  : a, b are identical or intersected
        """
        inter, iou, ioa, iob = self.calc_intersection_area(element_b, bias)

        # area of intersection is 0
        if ioa == 0:
            return 0
        # a in b
        if ioa >= 1:
            return -1
        # b in a
        if iob >= 1:
            return 1
        return 2

    # def visualize_element(self, img, color=(0, 255, 0), line=3, show=False, ratio=1, expand_size=5, corner_radius=20):
    #     loc = self.put_bbox()
    #
    #     if ratio != 1:
    #         loc = [int(x * ratio) for x in loc]
    #
    #     # 调整线条粗细,根据缩放比例
    #     adjusted_thickness = int(line * ratio)
    #     if adjusted_thickness < 1:
    #         adjusted_thickness = 1  # 确保最小粗细为1
    #
    #     # cv2.rectangle(img, loc[:2], loc[2:], color, line)
    #     # cv2.rectangle(img, (loc[0], loc[1]), (loc[2], loc[3]), color, line)
    #     # 标号
    #     # cv2.putText(img, str(int(self.id) + 1), (int(ratio*(self.col_min - 10)), int(ratio*(self.row_max + 10))),
    #     # cv2.FONT_HERSHEY_SIMPLEX, 1, color, line)
    #
    #     # 扩展边框,在原始坐标的基础上增加 expand_size
    #     loc[0] -= expand_size  # 左
    #     loc[1] -= expand_size + 2  # 上
    #     loc[2] += expand_size + 10  # 右
    #     loc[3] += expand_size + 2  # 下
    #
    #     # 确保圆角半径不会超过矩形的宽或高的一半
    #     corner_radius = min(corner_radius, (loc[2] - loc[0]) // 2, (loc[3] - loc[1]) // 2)
    #
    #     # 绘制圆角矩形
    #     def draw_rounded_rectangle(image, top_left, bottom_right, color, thickness, radius):
    #         x1, y1 = top_left
    #         x2, y2 = bottom_right
    #
    #         # 计算各条边线的起点和终点
    #         points = [
    #             ((x1 + radius, y1), (x2 - radius, y1)),  # 上边
    #             ((x1 + radius, y2), (x2 - radius, y2)),  # 下边
    #             ((x1, y1 + radius), (x1, y2 - radius)),  # 左边
    #             ((x2, y1 + radius), (x2, y2 - radius))  # 右边
    #         ]
    #
    #         # 画四分之一圆角
    #         cv2.ellipse(image, (x1 + radius, y1 + radius), (radius, radius), 180, 0, 90, color, thickness)  # 左上角
    #         cv2.ellipse(image, (x2 - radius, y1 + radius), (radius, radius), 270, 0, 90, color, thickness)  # 右上角
    #         cv2.ellipse(image, (x1 + radius, y2 - radius), (radius, radius), 90, 0, 90, color, thickness)  # 左下角
    #         cv2.ellipse(image, (x2 - radius, y2 - radius), (radius, radius), 0, 0, 90, color, thickness)  # 右下角
    #
    #         # 画四条直线连接四分之一圆角
    #         for point_start, point_end in points:
    #             cv2.line(image, point_start, point_end, color, thickness)
    #
    #     # 绘制圆角矩形
    #     draw_rounded_rectangle(img, (loc[0], loc[1]), (loc[2], loc[3]), color, adjusted_thickness, corner_radius)
    #
    #     # for child in self.children:
    #     #     child.visualize_element(img, color=(255, 0, 255), line=line)
    #     if show:
    #         cv2.imshow('element', img)
    #         cv2.waitKey(0)
    #         cv2.destroyWindow('element')

    def adjust_loc(self, ratio=1, expand_size=5):
        loc = list(self.put_bbox())
        if ratio != 1:
            loc = [int(x * ratio) for x in loc]
            # 扩展框的边界
        loc[0] -= expand_size  # 左扩展
        loc[1] -= expand_size + 2  # 上扩展
        loc[2] += expand_size + 10  # 右扩展
        loc[3] += expand_size + 2  # 下扩展
        return loc

    def draw_and_expand_rounded_rectangle(self, img, loc, color=(0, 255, 0), thickness=-1, expand_size=5,
                                          corner_radius=20, ):
        """
        通用的绘制带扩展边框的圆角矩形的方法。

        参数:
        - img: 输入图像
        - loc: 矩形的左上角和右下角坐标 (x1, y1, x2, y2)
        - color: 矩形颜色
        - line: 矩形边框线条的粗细
        - expand_size: 扩展的边框大小
        - corner_radius: 圆角的半径
        - thickness: 线条粗细(-1 为填充)
        """

        # 确保圆角半径不会超过矩形的宽或高的一半
        corner_radius = min(corner_radius, (loc[2] - loc[0]) // 2, (loc[3] - loc[1]) // 2)

        # 绘制带扩展的圆角矩形
        x1, y1 = loc[0], loc[1]
        x2, y2 = loc[2], loc[3]

        x1 = int(x1)
        y1 = int(y1)
        x2 = int(x2)
        y2 = int(y2)

        # Step 1: 处理填充
        if thickness == -1:
            # 填充模式 - 先绘制没有圆角的矩形部分
            cv2.rectangle(img, (x1 + corner_radius, y1), (x2 - corner_radius, y2), color, cv2.FILLED)  # 上下边部分
            cv2.rectangle(img, (x1, y1 + corner_radius), (x2, y2 - corner_radius), color, cv2.FILLED)  # 左右边部分

        # Step 2: 处理圆角
        # 画四分之一圆角
        cv2.ellipse(img, (x1 + corner_radius, y1 + corner_radius), (corner_radius, corner_radius), 180, 0, 90, color,
                    thickness)  # 左上角
        cv2.ellipse(img, (x2 - corner_radius, y1 + corner_radius), (corner_radius, corner_radius), 270, 0, 90, color,
                    thickness)  # 右上角
        cv2.ellipse(img, (x1 + corner_radius, y2 - corner_radius), (corner_radius, corner_radius), 90, 0, 90, color,
                    thickness)  # 左下角
        cv2.ellipse(img, (x2 - corner_radius, y2 - corner_radius), (corner_radius, corner_radius), 0, 0, 90, color,
                    thickness)  # 右下角

        # Step 3: 处理边框
        if thickness > 0:
            # 绘制矩形四条边框,连接四个圆角
            cv2.line(img, (x1 + corner_radius, y1), (x2 - corner_radius, y1), color, thickness)  # 上边
            cv2.line(img, (x1 + corner_radius, y2), (x2 - corner_radius, y2), color, thickness)  # 下边
            cv2.line(img, (x1, y1 + corner_radius), (x1, y2 - corner_radius), color, thickness)  # 左边
            cv2.line(img, (x2, y1 + corner_radius), (x2, y2 - corner_radius), color, thickness)  # 右边

    def visualize_element(self, img, color=(0, 255, 0), line=3, show=False, ratio=1, expand_size=5, corner_radius=20,
                          loc=None):
        # 调整线条粗细,根据缩放比例
        loc = self.adjust_loc(ratio=ratio, expand_size=expand_size)
        if loc is None:
            loc = [0, 0, 0, 0]
        adjusted_thickness = int(line * ratio)
        if adjusted_thickness < 1:
            adjusted_thickness = 1  # 确保最小粗细为1

        # 调用通用的绘制带扩展的圆角矩形方法
        self.draw_and_expand_rounded_rectangle(img, loc, color, adjusted_thickness, expand_size, corner_radius)

        # 显示图像(可选)
        if show:
            cv2.imshow('element', img)
            cv2.waitKey(0)
            cv2.destroyWindow('element')

    def calculate_text_height(self, font_title, font_text, title, text, bubble_width, inner_padding):
        """
        计算文本内容的高度,包括标题和正文
        """
        # 创建临时 PIL 图像用于计算文本高度
        temp_img = Image.new("RGB", (bubble_width, 500), (255, 255, 255))
        draw = ImageDraw.Draw(temp_img)

        # 计算标题的高度
        title_height = draw.textbbox((0, 0), title, font=font_title)[3] - \
                       draw.textbbox((0, 0), title, font=font_title)[1]

        # 处理正文的自动换行
        words = text.split(' ')
        current_line = ""
        lines = []
        max_text_width = bubble_width - 2 * inner_padding

        for word in words:
            test_line = current_line + word + " "
            text_bbox = draw.textbbox((0, 0), test_line, font=font_text)
            text_width = text_bbox[2] - text_bbox[0]

            if text_width < max_text_width:
                current_line = test_line
            else:
                lines.append(current_line)
                current_line = word + " "

        # 添加最后一行
        lines.append(current_line)

        # 计算正文高度
        text_height = len(lines) * (draw.textbbox((0, 0), lines[0], font=font_text)[3] -
                                    draw.textbbox((0, 0), lines[0], font=font_text)[1]) + len(lines) * 5

        # 计算总高度(标题 + 正文 + 内边距)
        total_height = title_height + text_height + 2 * inner_padding + 50

        return total_height

    def write_text(self, img, bubble_top_left, font_title, font_text, title, text, bubble_width, inner_padding):
        """
        在对话框内绘制标题和正文内容
        """
        # 确保文本和对话框边缘有内边距
        text_area_top_left = (bubble_top_left[0] + inner_padding, bubble_top_left[1] + inner_padding - 15)

        # 将 OpenCV 图像转换为 PIL 图像
        img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

        # 创建 ImageDraw 对象用于绘制文字
        draw = ImageDraw.Draw(img_pil)

        # 绘制标题
        draw.text((text_area_top_left[0], text_area_top_left[1]), title, font=font_title, fill=(255, 255, 255))

        # 处理正文自动换行
        text_start_y = text_area_top_left[1] + font_title.size + 50  # 标题下面再留些空间给正文
        max_text_width = bubble_width - 2 * inner_padding
        lines = []
        words = text.split(' ')
        current_line = ""

        for word in words:
            # 检查当前行的宽度
            test_line = current_line + word + " "
            text_bbox = draw.textbbox((0, 0), test_line, font=font_text)
            text_width = text_bbox[2] - text_bbox[0]

            if text_width < max_text_width:
                current_line = test_line
            else:
                # 当前行内容超出对话框宽度,换行
                lines.append(current_line)
                current_line = word + " "

        # 添加最后一行
        lines.append(current_line)

        # 绘制每一行正文
        for line in lines:
            draw.text((text_area_top_left[0], text_start_y), line, font=font_text, fill=(255, 255, 255))
            text_start_y += font_text.size + 5

        # 将 PIL 图像转换回 OpenCV 图像(直接修改 img)
        img[:] = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

    def process_img(self, img, elements, texts, color=(0, 255, 0), line=3, alpha=0.7, expand_size=5,
                    corner_radius=20, show=False, ratio=1, padding=50, triangle_height=0, inner_padding=100,
                    font_path_title="CDM/detect_merge/title.ttf", font_path_text="CDM/detect_merge/text.ttf",
                    font_size_title=70, font_size_text=50):

        # --------------画所有边框+黑色半透明处理剩余部分----------------------

        black_block_start = time.time()

        # 创建一个全黑遮罩(用于透明处理)
        mask = np.zeros_like(img, dtype=np.uint8)

        # 遍历每个元素并绘制圆角矩形,排除这些区域
        for element in elements:
            loc = element.adjust_loc(ratio=ratio, expand_size=expand_size)
            # 调用通用的绘制带扩展的圆角矩形方法,在遮罩上扣除这些区域
            self.draw_and_expand_rounded_rectangle(mask, loc, (255, 255, 255), -1, expand_size, corner_radius)

        # 创建一个全黑的图像,用于制作黑色遮罩
        overlay = np.zeros_like(img, dtype=np.uint8)
        overlay[:] = (0, 0, 0)  # 这是一个全黑的遮罩

        # 将 overlay 和 img 混合,但根据 mask 的黑色区域应用透明效果,白色区域保持原样
        img_with_overlay = cv2.addWeighted(img, 1 - alpha, overlay, alpha, 0)  # 将黑色透明度应用到整个图像

        # 使用 mask 保留元素区域的原始图像
        result = cv2.bitwise_and(img, mask)  # 仅保留元素区域(白色部分)
        result += cv2.bitwise_and(img_with_overlay, cv2.bitwise_not(mask))  # 将黑色透明遮罩应用到非元素区域(黑色部分)

        # 将结果应用到 img 上
        img[:, :, :] = result

        # 为每个元素再绘制圆角矩形边框
        for element in elements:
            loc = element.adjust_loc(ratio=ratio, expand_size=expand_size)
            element.visualize_element(img, color=color, line=line, show=show, ratio=ratio, loc=loc)

        black_block_cost = time.time() - black_block_start

        # print("绘制带黑色遮罩的框选component的圆角block花费:%2.2f s" % black_block_cost)

        # -----------------------绘制信息部分----------------------------------

        write_text_in_block = time.time()

        # 获取图像的宽度和高度
        img_height, img_width = img.shape[:2]

        # 获取标题和正文的显示信息
        title = "· " + elements[0].label
        df = pd.DataFrame(texts)
        df_unique = df.drop_duplicates(subset='label', keep='first')
        text = df_unique[df_unique['label'] == elements[0].label]['segment'].values[0]
        text = text[0].upper() + text[1:]

        # 加载字体
        font_title = ImageFont.truetype(font_path_title, font_size_title)
        font_text = ImageFont.truetype(font_path_text, font_size_text)

        # 计算所有元素的调整后位置
        locs = [e.adjust_loc(ratio=ratio, expand_size=expand_size) for e in elements]

        # 获取所有元素的上边界(y1)和下边界(y2)
        # 并添加图片的上边界(0)和下边界(img_height)作为特殊的"元素"
        y_positions = []

        for loc in locs:
            y_positions.append((loc[1], 'element_top'))  # 元素的上边界
            y_positions.append((loc[3], 'element_bottom'))  # 元素的下边界

        # 添加图片的上边界和下边界
        y_positions.append((0, 'image_top'))
        y_positions.append((img_height, 'image_bottom'))

        # 按 Y 坐标排序
        y_positions.sort(key=lambda x: x[0])

        # 找到所有的垂直空白区域
        gaps = []

        for i in range(len(y_positions) - 1):
            y1, label1 = y_positions[i]
            y2, label2 = y_positions[i + 1]

            # 如果两个位置之间有空隙
            if y2 > y1:
                gap_height = y2 - y1
                gap = {
                    'start': y1,
                    'end': y2,
                    'height': gap_height,
                    'above': label1,
                    'below': label2
                }
                gaps.append(gap)

        # 找到高度最大的空白区域
        largest_gap = max(gaps, key=lambda x: x['height'])

        # 判断最大的空白区域是否在元素和图片边界之间,还是在元素之间
        is_between_element_and_border = False
        is_between_elements = False

        if largest_gap['above'] == 'image_top' or largest_gap['below'] == 'image_bottom':
            is_between_element_and_border = True
        elif largest_gap['above'] == 'element_bottom' and largest_gap['below'] == 'element_top':
            is_between_elements = True

        # 计算文本框的宽度
        bubble_width = img_width - 2 * padding

        # 计算文本框的高度
        bubble_height = self.calculate_text_height(font_title, font_text, title, text, bubble_width, inner_padding)

        # 确保文本框的高度不超过最大空白区域的高度减去两倍的内边距
        max_bubble_height = largest_gap['height'] - 2 * padding
        if bubble_height > max_bubble_height:
            bubble_height = max_bubble_height

        # 根据判断结果确定文本框的位置
        if is_between_element_and_border:
            # 文本框靠近对应的元素

            if largest_gap['above'] == 'image_top':
                # 空白区域在顶部,文本框在第一个元素的上方
                ref_element = min(elements, key=lambda e: e.adjust_loc(ratio=ratio, expand_size=expand_size)[1])
                loc = ref_element.adjust_loc(ratio=ratio, expand_size=expand_size)
                y1 = loc[1]

                bubble_top = y1 - triangle_height - padding - bubble_height
                bubble_left = padding
            elif largest_gap['below'] == 'image_bottom':
                # 空白区域在底部,文本框在最后一个元素的下方
                ref_element = max(elements, key=lambda e: e.adjust_loc(ratio=ratio, expand_size=expand_size)[3])
                loc = ref_element.adjust_loc(ratio=ratio, expand_size=expand_size)
                y2 = loc[3]

                bubble_top = y2 + triangle_height + padding
                bubble_left = padding

            bubble_top_left = (bubble_left, bubble_top)
            bubble_bottom_right = (bubble_left + bubble_width, bubble_top + bubble_height)

        elif is_between_elements:
            # 文本框在元素之间,垂直居中放置在最大空白区域内
            gap_start = largest_gap['start']
            gap_end = largest_gap['end']

            bubble_top = gap_start + (gap_end - gap_start - bubble_height) / 2
            bubble_left = padding

            bubble_top_left = (bubble_left, bubble_top)
            bubble_bottom_right = (bubble_left + bubble_width, bubble_top + bubble_height)

        else:
            # 如果没有符合条件的空白区域,默认将文本框放在图片的顶部
            bubble_top_left = (padding, padding)
            bubble_bottom_right = (bubble_top_left[0] + bubble_width, bubble_top_left[1] + bubble_height)

        # 确保文本框不会超出图片的顶部或底部边界
        if bubble_top_left[1] < 0:
            bubble_top_left = (bubble_top_left[0], padding)
            bubble_bottom_right = (bubble_bottom_right[0], bubble_top_left[1] + bubble_height)
        elif bubble_bottom_right[1] > img_height:
            bubble_bottom_right = (bubble_bottom_right[0], img_height - padding)
            bubble_top_left = (bubble_top_left[0], bubble_bottom_right[1] - bubble_height)

        # 绘制圆角矩形作为对话框
        self.draw_and_expand_rounded_rectangle(
            img,
            [bubble_top_left[0], bubble_top_left[1], bubble_bottom_right[0], bubble_bottom_right[1]],
            color,
            corner_radius=50
        )

        # 对话框内写入文本
        self.write_text(img, bubble_top_left, font_title, font_text, title, text, bubble_width, inner_padding)

        #     # 计算所有元素的调整后位置
        #     locs = [e.adjust_loc(ratio=ratio, expand_size=expand_size) for e in elements]
        #
        #     # 计算元素组的包围框
        #     min_y1 = min(loc[1] for loc in locs)
        #     max_y2 = max(loc[3] for loc in locs)
        #
        #     # 计算元素组到图片上下边界的距离
        #     top_distance_group = min_y1  # 元素组上边到图片上边界的距离
        #     bottom_distance_group = img_height - max_y2  # 元素组下边到图片下边界的距离
        #
        #     if top_distance_group > bottom_distance_group:
        #         # 如果元素组上方空间更大,选择最靠近上边界的元素
        #         ref_element = min(elements, key=lambda e: e.adjust_loc(ratio=ratio, expand_size=expand_size)[1])
        #     else:
        #         # 否则,选择最靠近下边界的元素
        #         ref_element = max(elements, key=lambda e: e.adjust_loc(ratio=ratio, expand_size=expand_size)[3])
        #
        # # 使用参考元素进行后续操作
        # loc = ref_element.adjust_loc(ratio=ratio, expand_size=expand_size)
        # # 计算每个扩展后边界到图片边界的距离
        # left_distance = loc[0]  # 左边到图像左边界的距离
        # right_distance = img_width - loc[2]  # 右边到图像右边界的距离
        # top_distance = loc[1]  # 上边到图像上边界的距离
        # bottom_distance = img_height - loc[3]  # 下边到图像下边界的距离
        #
        # # 存储每个边界的距离
        # distances = {
        #     'left': left_distance,
        #     'right': right_distance,
        #     'top': top_distance,
        #     'bottom': bottom_distance
        # }
        #
        # # 计算对话框宽度(接近 img 的宽度,但稍微小于 img 宽度)
        # bubble_width = img_width - 2 * padding
        # x1, y1, x2, y2 = loc
        #
        # # 计算文本框的高度
        # bubble_height = self.calculate_text_height(font_title, font_text, title, text, bubble_width, inner_padding)
        #
        # # 根据元素位置绘制对话框
        # if distances['top'] > distances['bottom']:
        #     # 对话框的位置:在元素上方
        #     bubble_top_left = (padding, y1 - triangle_height - padding - bubble_height)
        #     bubble_bottom_right = (bubble_width + padding, y1 - triangle_height - padding)
        # else:
        #     # 对话框的位置:在元素下方
        #     bubble_top_left = (padding, y2 + triangle_height + padding)
        #     bubble_bottom_right = (bubble_width + padding, y2 + triangle_height + padding + bubble_height)
        #
        # # 绘制圆角矩形作为对话框
        # self.draw_and_expand_rounded_rectangle(
        #     img,
        #     [bubble_top_left[0], bubble_top_left[1], bubble_bottom_right[0], bubble_bottom_right[1]],
        #     color,
        #     corner_radius=50
        # )
        #
        # # 对话框内写入文本
        # self.write_text(img, bubble_top_left, font_title, font_text, title, text, bubble_width, inner_padding)

        write_in_block_cost = time.time() - write_text_in_block

        # print("绘制文本展示block+写入文字花费时间:%2.2f s" % write_in_block_cost)





        # =====================================================





        # # 如果这张图只有一个element
        # if len(elements) == 1:
        #     loc = elements[0].adjust_loc(ratio=ratio, expand_size=expand_size)
        #     # 计算每个扩展后边界到图片边界的距离
        #     left_distance = loc[0]  # 左边到图像左边界的距离
        #     right_distance = img_width - loc[2]  # 右边到图像右边界的距离
        #     top_distance = loc[1]  # 上边到图像上边界的距离
        #     bottom_distance = img_height - loc[3]  # 下边到图像下边界的距离
        #
        #     # 存储每个边界的距离
        #     distances = {
        #         'left': left_distance,
        #         'right': right_distance,
        #         'top': top_distance,
        #         'bottom': bottom_distance
        #     }
        #
        #     # 计算对话框宽度(接近 img 的宽度,但稍微小于 img 宽度)
        #     bubble_width = img_width - 2 * padding
        #     x1, y1, x2, y2 = loc
        #
        #     # 计算文本框的高度
        #     bubble_height = self.calculate_text_height(font_title, font_text, title, text, bubble_width, inner_padding)
        #
        #     # 根据元素位置绘制对话框
        #     if distances['top'] > distances['bottom']:
        #         # 对话框的位置:在元素上方
        #         bubble_top_left = (padding, y1 - triangle_height - padding - bubble_height)
        #         bubble_bottom_right = (bubble_width + padding, y1 - triangle_height - padding)
        #         # # 三角形的底边中心位置 (等边三角形)
        #         # triangle_base_y = bubble_bottom_right[1]
        #         # triangle_center_x = (x1 + x2) // 2
        #         # # 三角形顶点朝下
        #         # triangle_tip_y = triangle_base_y + (triangle_height - 10)
        #     else:
        #         # 对话框的位置:在元素下方
        #         bubble_top_left = (padding, y2 + triangle_height + padding)
        #         bubble_bottom_right = (bubble_width + padding, y2 + triangle_height + padding + bubble_height)
        #         # # 三角形的底边中心位置 (等边三角形)
        #         # triangle_base_y = bubble_top_left[1]
        #         # triangle_center_x = (x1 + x2) // 2
        #         # # 三角形顶点朝上
        #         # triangle_tip_y = triangle_base_y - (triangle_height - 10)
        #
        #     # # 三角形底边长度等于高度,所以底边是 [triangle_height] 的两倍
        #     # half_base = triangle_height // 2 + 5
        #     #
        #     # # 绘制等边三角形
        #     # triangle_points = np.array([
        #     #     [triangle_center_x - half_base, triangle_base_y],  # 三角形左边点
        #     #     [triangle_center_x + half_base, triangle_base_y],  # 三角形右边点
        #     #     [triangle_center_x, triangle_tip_y]  # 三角形顶点
        #     # ])
        #     # cv2.fillPoly(img, [triangle_points], color)
        #
        #     # 绘制圆角矩形作为对话框
        #     self.draw_and_expand_rounded_rectangle(img,
        #                                            [bubble_top_left[0], bubble_top_left[1], bubble_bottom_right[0],
        #                                             bubble_bottom_right[1]], color, corner_radius=50)
        #
        #     # 对话框内写入文本
        #     self.write_text(img, bubble_top_left, font_title, font_text, title, text, bubble_width, inner_padding)
        #
        # # else: