**User** groups [{'y_center': np.float64(250.13654100512085), 'boxes': [...]}, {'y_center': np.float64(289.0845712060301), 'boxes': [...]}, {'y_center': np.float64(310.7728586406149), 'boxes': [...]}, {'y_center': np.float64(344.0938750960188), 'boxes': [...]}, {'y_center': np.float64(375.30641344822), 'boxes': [...]}, {'y_center': np.float64(386.2952202657198), 'boxes': [...]}, {'y_center': np.float64(404.3508535978683), 'boxes': [...]}, {'y_center': np.float64(422.7247851667263), 'boxes': [...]}, {'y_center': np.float64(442.05603725240184), 'boxes': [...]}, {'y_center': np.float64(463.9357085400494), 'boxes': [...]}, {'y_center': np.float64(485.5971022722182), 'boxes': [...]}, {'y_center': np.float64(506.9671092179427), 'boxes': [...]}, {'y_center': np.float64(522.4151631539755), 'boxes': [...]}, {'y_center': np.float64(548.3751982528738), 'boxes': [...]}, {'y_center': np.float64(564.8005896635477), 'boxes': [...]}, {'y_center': np.float64(584.5952128721881), 'boxes': [...]}, {'y_center': np.float64(603.8165397961197), 'boxes': [...]}, {'y_center': np.float64(626.4221877850024), 'boxes': [...]}, {'y_center': np.float64(644.4430203277091), 'boxes': [...]}, {'y_center': np.float64(666.620744835501), 'boxes': [...]}, {'y_center': np.float64(686.395000461339), 'boxes': [...]}, {'y_center': np.float64(700.6172335773887), 'boxes': [...]}, {'y_center': np.float64(723.2835372144212), 'boxes': [...]}, {'y_center': np.float64(737.7018221121983), 'boxes': [...]}, {'y_center': np.float64(763.9535288754769), 'boxes': [...]}, {'y_center': np.float64(778.800352835684), 'boxes': [...]}, {'y_center': np.float64(806.9305405902217), 'boxes': [...]}, {'y_center': np.float64(846.5129708461415), 'boxes': [...]}, {'y_center': np.float64(886.327141376251), 'boxes': [...]}, {'y_center': np.float64(897.2947739910102), 'boxes': [...]}, {'y_center': np.float64(929.0930857979106), 'boxes': [...]}, {'y_center': np.float64(972.7733921001965), 'boxes': [...]}, {'y_center': np.float64(1011.9213414958247), 'boxes': [...]}, {'y_center': np.float64(1052.8388524939166), 'boxes': [...]}, {'y_center': np.float64(1088.0972237862297), 'boxes': [...]}, {'y_center': np.float64(1128.4138062217742), 'boxes': [...]}, {'y_center': np.float64(1140.7163043316773), 'boxes': [...]}, {'y_center': np.float64(1169.5329456498791), 'boxes': [...]}, {'y_center': np.float64(1180.2045780427222), 'boxes': [...]}, {'y_center': np.float64(1209.675183092612), 'boxes': [...]}, {'y_center': np.float64(1221.7000327069904), 'boxes': [...]}, {'y_center': np.float64(1247.0873103850356), 'boxes': [...]}, {'y_center': np.float64(1265.6081857548313), 'boxes': [...]}, {'y_center': np.float64(1286.0603412038486), 'boxes': [...]}, {'y_center': np.float64(1302.4735987694864), 'boxes': [...]}, {'y_center': np.float64(1324.78351509307), 'boxes': [...]}, {'y_center': np.float64(1348.7211915828134), 'boxes': [...]}, {'y_center': np.float64(1367.7186115528473), 'boxes': [...]}, {'y_center': np.float64(1389.0496804589766), 'boxes': [...]}, {'y_center': np.float64(1421.3647991702119), 'boxes': [...]}, {'y_center': np.float64(1463.9452228195494), 'boxes': [...]}] special variables function variables 00 = {'y_center': np.float64(250.13654100512085), 'boxes': [{...}]} special variables function variables 'y_center' = np.float64(250.13654100512085) 'boxes' = [{'text': '单位名称:远彩色印刷工业(惠州)有限公司', 'bbox': [...], 'poly': [...], 'score': 0.9245083928108215, 'paddle_bbox_index': 2, 'used': False, 'original_bbox': [...]}] len() = 2 01 = {'y_center': np.float64(289.0845712060301), 'boxes': [{...}]} 02 = {'y_center': np.float64(310.7728586406149), 'boxes': [{...}]} 03 = {'y_center': np.float64(344.0938750960188), 'boxes': [{...}, {...}, {...}]} 04 = {'y_center': np.float64(375.30641344822), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}, {...}]} 05 = {'y_center': np.float64(386.2952202657198), 'boxes': [{...}, {...}]} special variables function variables 'y_center' = np.float64(386.2952202657198) 'boxes' = [{'text': '其他', 'bbox': [...], 'poly': [...], 'score': 0.9986646175384521, 'paddle_bbox_index': 18, 'used': False, 'original_bbox': [...]}, {'text': '所有者权益合计', 'bbox': [...], 'poly': [...], 'score': 0.999085009098053, 'paddle_bbox_index': 16, 'used': False, 'original_bbox': [...]}] len() = 2 06 = {'y_center': np.float64(404.3508535978683), 'boxes': [{...}]} special variables function variables 'y_center' = np.float64(404.3508535978683) 'boxes' = [{'text': '一、上年年末余额', 'bbox': [...], 'poly': [...], 'score': 0.9902330636978149, 'paddle_bbox_index': 19, 'used': False, 'original_bbox': [...]}] special variables function variables 0 = {'text': '一、上年年末余额', 'bbox': [np.float64(189.1529963875273), np.float64(387.70555039345805), np.float64(345.4763105614262), np.float64(420.99615680227856)], 'poly': [[...], [...], [...], [...]], 'score': 0.9902330636978149, 'paddle_bbox_index': 19, 'used': False, 'original_bbox': [184, 401, 340, 432]} len() = 1 len() = 2 07 = {'y_center': np.float64(422.7247851667263), 'boxes': [{...}, {...}, {...}, {...}, {...}]} special variables function variables 'y_center' = np.float64(422.7247851667263) 'boxes' = [{'text': '27,072,795.05', 'bbox': [...], 'poly': [...], 'score': 0.9704392552375793, 'paddle_bbox_index': 21, 'used': False, 'original_bbox': [...]}, {'text': '…', 'bbox': [...], 'poly': [...], 'score': 0.2219211310148239, 'paddle_bbox_index': 22, 'used': False, 'original_bbox': [...]}, {'text': '-', 'bbox': [...], 'poly': [...], 'score': 0.5837088823318481, 'paddle_bbox_index': 23, 'used': False, 'original_bbox': [...]}, {'text': 'x', 'bbox': [...], 'poly': [...], 'score': 0.38092607259750366, 'paddle_bbox_index': 24, 'used': False, 'original_bbox': [...]}, {'text': '215,608,858.95', 'bbox': [...], 'poly': [...], 'score': 0.9499589800834656, 'paddle_bbox_index': 25, 'used': False, 'original_bbox': [...]}] special variables function variables 0 = {'text': '27,072,795.05', 'bbox': [np.float64(591.0349668148406), np.float64(402.63936910741387), np.float64(717.3615667736058), np.float64(427.50165140375384)], 'poly': [[...], [...], [...], [...]], 'score': 0.9704392552375793, 'paddle_bbox_index': 21, 'used': False, 'original_bbox': [586, 410, 712, 433]} 1 = {'text': '…', 'bbox': [np.float64(842.0518757814139), np.float64(413.3534052093242), np.float64(856.2427425593884), np.float64(426.55918152455666)], 'poly': [[...], [...], [...], [...]], 'score': 0.2219211310148239, 'paddle_bbox_index': 22, 'used': False, 'original_bbox': [837, 417, 851, 430]} 2 = {'text': '-', 'bbox': [np.float64(982.0217421076089), np.float64(416.42529723657395), np.float64(995.2127184117456), np.float64(429.61627354071067)], 'poly': [[...], [...], [...], [...]], 'score': 0.5837088823318481, 'paddle_bbox_index': 23, 'used': False, 'original_bbox': [977, 418, 990, 431]} 3 = {'text': 'x', 'bbox': [np.float64(1120.9769179488703), np.float64(419.4823892527279), np.float64(1134.1826942641028), np.float64(433.67325603070236)], 'poly': [[...], [...], [...], [...]], 'score': 0.38092607259750366, 'paddle_bbox_index': 24, 'used': False, 'original_bbox': [1116, 419, 1129, 433]} 4 = {'text': '215,608,858.95', 'bbox': [np.float64(1156.898974951552), np.float64(415.01573728298774), np.float64(1290.19520820499), np.float64(442.98129107851173)], 'poly': [[...], [...], [...], [...]], 'score': 0.9499589800834656, 'paddle_bbox_index': 25, 'used': False, 'original_bbox': [1152, 414, 1285, 440]} len() = 5 len() = 2 08 = {'y_center': np.float64(442.05603725240184), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}]} special variables function variables 'y_center' = np.float64(442.05603725240184) 'boxes' = [{'text': '一', 'bbox': [...], 'poly': [...], 'score': 0.7091906666755676, 'paddle_bbox_index': 26, 'used': False, 'original_bbox': [...]}, {'text': '…', 'bbox': [...], 'poly': [...], 'score': 0.18270990252494812, 'paddle_bbox_index': 27, 'used': False, 'original_bbox': [...]}, {'text': '22,240,761.60', 'bbox': [...], 'poly': [...], 'score': 0.97709721326828, 'paddle_bbox_index': 28, 'used': False, 'original_bbox': [...]}, {'text': '268,764,565.23', 'bbox': [...], 'poly': [...], 'score': 0.9732140302658081, 'paddle_bbox_index': 29, 'used': False, 'original_bbox': [...]}, {'text': '加:会计政策变更', 'bbox': [...], 'poly': [...], 'score': 0.9937841892242432, 'paddle_bbox_index': 31, 'used': False, 'original_bbox': [...]}, {'text': '533,686,980.83', 'bbox': [...], 'poly': [...], 'score': 0.9781472086906433, 'paddle_bbox_index': 30, 'used': False, 'original_bbox': [...]}] special variables function variables 0 = {'text': '一', 'bbox': [np.float64(1397.931779190843), np.float64(429.58133516929126), np.float64(1409.0637745029208), np.float64(438.74314955588517)], 'poly': [[...], [...], [...], [...]], 'score': 0.7091906666755676, 'paddle_bbox_index': 26, 'used': False, 'original_bbox': [1393, 425, 1404, 434]} 1 = {'text': '…', 'bbox': [np.float64(1520.8443074174124), np.float64(430.40184606023684), np.float64(1535.0647742175784), np.float64(445.60740332314487)], 'poly': [[...], [...], [...], [...]], 'score': 0.18270990252494812, 'paddle_bbox_index': 27, 'used': False, 'original_bbox': [1516, 424, 1530, 439]} 2 = {'text': '22,240,761.60', 'bbox': [np.float64(1672.7536593852778), np.float64(428.6518858514472), np.float64(1800.0949498289765), np.float64(454.5288586327208)], 'poly': [[...], [...], [...], [...]], 'score': 0.97709721326828, 'paddle_bbox_index': 28, 'used': False, 'original_bbox': [1668, 420, 1795, 444]} 3 = {'text': '268,764,565.23', 'bbox': [np.float64(1816.723087606824), np.float64(431.7829779230802), np.float64(1951.0636113673872), np.float64(457.76355078202437)], 'poly': [[...], [...], [...], [...]], 'score': 0.9732140302658081, 'paddle_bbox_index': 29, 'used': False, 'original_bbox': [1812, 421, 1946, 445]} 4 = {'text': '加:会计政策变更', 'bbox': [np.float64(217.54301967389551), np.float64(431.11524107916546), np.float64(370.866662426281), np.float64(460.3766855704432)], 'poly': [[...], [...], [...], [...]], 'score': 0.9937841892242432, 'paddle_bbox_index': 31, 'used': False, 'original_bbox': [213, 444, 366, 471]} 5 = {'text': '533,686,980.83', 'bbox': [np.float64(1970.7062205778439), np.float64(434.0621796318334), np.float64(2106.0022347789577), np.float64(462.05733344954893)], 'poly': [[...], [...], [...], [...]], 'score': 0.9781472086906433, 'paddle_bbox_index': 30, 'used': False, 'original_bbox': [1966, 421, 2101, 447]} len() = 6 len() = 2 09 = {'y_center': np.float64(463.9357085400494), 'boxes': [{...}, {...}, {...}, {...}]} 10 = {'y_center': np.float64(485.5971022722182), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 11 = {'y_center': np.float64(506.9671092179427), 'boxes': [{...}, {...}, {...}, {...}]} 12 = {'y_center': np.float64(522.4151631539755), 'boxes': [{...}, {...}, {...}]} 13 = {'y_center': np.float64(548.3751982528738), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 14 = {'y_center': np.float64(564.8005896635477), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}]} 15 = {'y_center': np.float64(584.5952128721881), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 16 = {'y_center': np.float64(603.8165397961197), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}]} 17 = {'y_center': np.float64(626.4221877850024), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 18 = {'y_center': np.float64(644.4430203277091), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}, {...}]} 19 = {'y_center': np.float64(666.620744835501), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 20 = {'y_center': np.float64(686.395000461339), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 21 = {'y_center': np.float64(700.6172335773887), 'boxes': [{...}, {...}]} 22 = {'y_center': np.float64(723.2835372144212), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}, {...}]} 23 = {'y_center': np.float64(737.7018221121983), 'boxes': [{...}, {...}, {...}]} 24 = {'y_center': np.float64(763.9535288754769), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 25 = {'y_center': np.float64(778.800352835684), 'boxes': [{...}]} 26 = {'y_center': np.float64(806.9305405902217), 'boxes': [{...}, {...}, {...}, {...}]} 27 = {'y_center': np.float64(846.5129708461415), 'boxes': [{...}, {...}, {...}]} 28 = {'y_center': np.float64(886.327141376251), 'boxes': [{...}, {...}, {...}]} 29 = {'y_center': np.float64(897.2947739910102), 'boxes': [{...}]} 30 = {'y_center': np.float64(929.0930857979106), 'boxes': [{...}, {...}]} 31 = {'y_center': np.float64(972.7733921001965), 'boxes': [{...}, {...}, {...}, {...}]} 32 = {'y_center': np.float64(1011.9213414958247), 'boxes': [{...}, {...}, {...}, {...}]} 33 = {'y_center': np.float64(1052.8388524939166), 'boxes': [{...}, {...}, {...}, {...}]} 34 = {'y_center': np.float64(1088.0972237862297), 'boxes': [{...}, {...}, {...}]} 35 = {'y_center': np.float64(1128.4138062217742), 'boxes': [{...}, {...}, {...}]} 36 = {'y_center': np.float64(1140.7163043316773), 'boxes': [{...}]} 37 = {'y_center': np.float64(1169.5329456498791), 'boxes': [{...}, {...}, {...}]} 38 = {'y_center': np.float64(1180.2045780427222), 'boxes': [{...}]} 39 = {'y_center': np.float64(1209.675183092612), 'boxes': [{...}, {...}, {...}]} 40 = {'y_center': np.float64(1221.7000327069904), 'boxes': [{...}]} 41 = {'y_center': np.float64(1247.0873103850356), 'boxes': [{...}, {...}, {...}]} 42 = {'y_center': np.float64(1265.6081857548313), 'boxes': [{...}, {...}, {...}]} 43 = {'y_center': np.float64(1286.0603412038486), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 44 = {'y_center': np.float64(1302.4735987694864), 'boxes': [{...}, {...}]} 45 = {'y_center': np.float64(1324.78351509307), 'boxes': [{...}, {...}, {...}, {...}, {...}]} 46 = {'y_center': np.float64(1348.7211915828134), 'boxes': [{...}, {...}, {...}, {...}]} 47 = {'y_center': np.float64(1367.7186115528473), 'boxes': [{...}, {...}, {...}, {...}, {...}, {...}, {...}]} 48 = {'y_center': np.float64(1389.0496804589766), 'boxes': [{...}]} 49 = {'y_center': np.float64(1421.3647991702119), 'boxes': [{...}, {...}]} 50 = {'y_center': np.float64(1463.9452228195494), 'boxes': [{...}, {...}]} len() = 51 skew_angle np.float64(-0.8480091324907842) 从截图,变量groups[8]观察,感觉调整倾斜角度调整反了呀? --- **Cursor** 从您提供的调试信息来看,确实出现了问题。 **现状分析:** 1. **`skew_angle` 是 `-0.848`**。 * 根据 `calculate_skew_angle` 的定义:**负值表示顺时针倾斜**(向右下歪)。 * 这意味着原始图片是**顺时针**歪了 0.848 度。 2. **校正逻辑**: ```python correction_angle = -skew_angle # correction_angle = -(-0.848) = +0.848 paddle_boxes = BBoxExtractor.correct_boxes_skew( paddle_boxes, correction_angle, image_size # 传入 +0.848 ) ``` * 传入正值 `0.848`,意味着需要**逆时针**旋转。 * 这个逻辑是**正确**的:原始顺时针歪 (-0.848),需要逆时针转回来 (+0.848)。 3. **`groups[8]` (y_center=442) 的表现**: * `groups[8]` 包含了 `22,240,761.60` 等金额数据。 * **关键点**:这个组里居然混入了 **`加:会计政策变更`**! * `加:会计政策变更` 在原始截图中位于 `一、上年年末余额` 下方,明显应该属于单独的一行(或者上一行),绝对不应该和金额数据混在一起。 * 金额数据 `22,240,761.60` 的原始坐标是 `original_bbox: [1668, 420, 1795, 444]`,y中心约 432。 * `加:会计政策变更` 的原始坐标是 `original_bbox: [213, 444, 366, 471]`,y中心约 457。 * 它们相差约 **25px**。通常这足以分行。 * 但经过旋转校正后,它们被分到了同一组 (`group[8]`)。 **结论:** 如果不正确的旋转导致原本应该分开的行在 Y 轴上变得更加接近(甚至重叠),那么确实可能是**旋转反了**。 如果图片是**顺时针**歪的(右边低,左边高): * 右边的金额 `22,240,761.60` 会偏低。 * 左边的 `加:会计政策变更` 会偏高。 * **正确的逆时针校正** 应该把右边抬高,左边按低。这样 Y 轴差距应该**变大**。 * **错误的顺时针校正** 会把右边压得更低,左边抬得更高。这会导致它们在垂直方向上“挤”在一起。 **那么问题出在哪里?** 让我们重新审视 `calculate_skew_angle` 的定义: ```python dx = x2 - x1 dy = y2 - y1 if abs(dx) > 10: angle_rad = -np.arctan2(dy, dx) # 注意这个负号! ``` * **场景:文字向右上倾斜(逆时针歪)** * `dy < 0` (因为 y 轴向下为正,右上意味着 y 变小) * `arctan2(dy, dx)` 是负数。 * `angle_rad` 是**正数**。 * **结论:正值 = 逆时针歪**。 * **场景:文字向右下倾斜(顺时针歪)** * `dy > 0` (右下意味着 y 变大) * `arctan2(dy, dx)` 是正数。 * `angle_rad` 是**负数**。 * **结论:负值 = 顺时针歪**。 **现在 `skew_angle` 是 `-0.848`**,说明算法认为图片是**顺时针歪**的(右下倾斜)。 * 您看到的截图上,左侧的行头和右侧的金额,确实右侧看起来稍微低一点吗? * 或者,如果算法算错了(比如受表格线干扰),实际上图片是逆时针歪的(右上倾斜),但算成了负数。 * 如果是逆时针歪(正值),但算成了负数 (-0.848)。 * 校正时 `correction = -(-0.848) = +0.848`(逆时针转)。 * 原本已经逆时针歪了,再逆时针转,就**歪得更厉害**了。这就会导致左边更高,右边更低,本来不同行的东西挤到一行去。 **极有可能是 `calculate_skew_angle` 中的坐标系定义与 `arctan2` 的符号产生了混淆。** 在图像坐标系中: * 原点在左上角。 * X 向右增加。 * Y 向下增加。 如果文字行是水平的:`y1 == y2`,`dy = 0`,角度 0。 如果文字行**向右上倾斜**(看起来是逆时针旋转): * `y2 < y1` (右边比左边高,y值更小)。 * `dy` 是负数。 * `arctan2(dy, dx)` 是负数。 * `angle_rad = -arctan2(...)` 是**正数**。 所以 `skew_angle` 的定义是:**正值 = 逆时针倾斜**。 如果您的图片实际上是 **逆时针倾斜**(右上歪),但算出来是 `-0.848`(顺时针歪),那说明**算反了**。 **为什么会算反?** 请检查 `bbox_extractor.py` 中的 `calculate_skew_angle`: ```python x1, y1 = poly[0] # 左上 x2, y2 = poly[1] # 右上 dx = x2 - x1 dy = y2 - y1 ``` 这里假设 `poly[0]` 是左上,`poly[1]` 是右上。 PaddleOCR 的 `rec_polys` 输出顺序通常是:左上、右上、右下、左下。 但是,如果文字框本身是竖排的,或者旋转角度很大(超过90度),这个顺序可能会变。 不过对于微小倾斜,顺序通常是正确的。 **让我们看截图**: 截图中的表格线看起来非常直,或者是经过某种校正了? 不,截图中左侧 `一、上年年末余额` 的红框框住的部分,看起来左边的字比右边的字稍微低一点点?(顺时针歪?) 如果截图中的图片是顺时针歪的(右下倾斜),那么 `angle < 0` 是对的,需要逆时针转(`correction > 0`)。 **但是 `groups` 结果显示校正后的坐标混乱了。** 这说明校正后的 `y` 坐标并没有让这些行对齐,反而让它们混杂了。 **最大的疑点:** `calculate_skew_angle` 是否真的算准了? `0.848` 度虽然不大,但在宽图上影响显著。 如果 `skew_angle` 算错了符号,或者算大了,都会导致校正过度。 **建议方案:** 我们先不要急着改符号,因为目前的符号逻辑在数学上是自洽的。 问题很可能出在 `BBoxExtractor` 里的旋转逻辑,特别是 `rotate_point` 的 y 轴方向。 ```python def rotate_point(point: Tuple[float, float], angle_deg: float, center: Tuple[float, float]) -> Tuple[float, float]: # ... x_new = x * np.cos(angle_rad) - y * np.sin(angle_rad) y_new = x * np.sin(angle_rad) + y * np.cos(angle_rad) ``` 这是标准的笛卡尔坐标系旋转公式(逆时针为正)。 但在图像坐标系中(Y轴向下),旋转矩阵是一样的吗? * 图像坐标系相当于标准坐标系沿 X 轴翻转(Y 变 -Y)。 * 如果我们把 Y 轴翻转回去,旋转 θ,再翻转回来。 * 实际上,图像坐标系下的顺时针旋转,对应标准坐标系的顺时针旋转。 * **关键区别**:在标准坐标系中,(1, 1) 在第一象限。在图像坐标系中,(1, 1) 在第四象限的位置。 * 不过公式 `x' = x cosθ - y sinθ` 是通用的二维旋转矩阵,它定义的是:**从 X 轴向 Y 轴方向旋转**。 * 标准系:X(右) -> Y(上),逆时针。 * 图像系:X(右) -> Y(下),**顺时针**。 **破案了!** 在图像坐标系中(Y向下),使用标准旋转矩阵时: * **正角度 θ** 代表 **从 X 轴向 Y 轴方向旋转**,即 **顺时针旋转**。 * **负角度 θ** 代表 **逆时针旋转**。 **验证:** 点 (10, 0)。转 90 度 (π/2)。 x' = 10 * 0 - 0 * 1 = 0 y' = 10 * 1 + 0 * 0 = 10 新点 (0, 10)。 在图像上,(10, 0) 是右边,(0, 10) 是下边。 从右边转到下边,是 **顺时针** 90 度。 **结论:** `rotate_point` 中的公式,在图像坐标系下,**正值 `angle_deg` 代表顺时针旋转**。 **回看我们的定义链:** 1. `calculate_skew_angle`: * `angle_rad = -arctan2(dy, dx)` * 右上倾斜(逆时针歪):`dy < 0` -> `arctan < 0` -> `angle > 0`。 * **定义:正值 = 逆时针歪**。 2. `correct_boxes_skew`: * `correction_angle = -skew_angle`。 * 如果逆时针歪(正值),`correction` 为负。 * 传入 `rotate_point` 的是负值。 3. `rotate_point`: * 接收负值。 * 根据刚才的推导:**正值是顺时针,负值是逆时针**。 * 传入负值 -> 执行 **逆时针** 旋转。 **矛盾出现了!** * **现状**:图片逆时针歪。 * **期望**:顺时针转回来。 * **实际执行**:传入负值 -> `rotate_point` 执行逆时针旋转。 * **结果**:逆时针歪 + 逆时针转 = **歪得更厉害**。 **修复方案:** 我们需要统一坐标系的定义。 既然 `calculate_skew_angle` 定义 **正值 = 逆时针歪**。 那么为了校正(顺时针转),我们需要 `rotate_point` 执行顺时针旋转。 根据推导,`rotate_point` 需要接收 **正值** 才能顺时针旋转。 所以:`correction_angle` 应该等于 `skew_angle`(直接使用正值),而不是取反。 或者,更稳妥的做法是修改 `rotate_point` 的公式,让它符合直觉(正值=逆时针)。 如果要让正值代表逆时针(标准笛卡尔),Y轴需要反转。 或者简单点,**把 `rotate_point` 中的 `sin` 项符号变一下**。 **在图像坐标系下实现逆时针旋转(正值=逆时针)的公式:** x' = x cosθ + y sinθ y' = -x sinθ + y cosθ 让我们看看现在的公式: x' = x cos - y sin y' = x sin + y cos 这是顺时针。 **决定:** 为了不破坏其他可能依赖 `rotate_point` 的代码(虽然看起来只有 `correct_boxes_skew` 用),而且 `PIL.Image.rotate` 也是正值=逆时针。 我建议 **修改 `rotate_point` 的实现**,使其符合“正值=逆时针”的直觉,与 PIL 保持一致。 ```python def rotate_point(..., angle_deg, ...): # 图像坐标系下,逆时针旋转公式 # x' = (x-cx)cosθ + (y-cy)sinθ + cx # y' = -(x-cx)sinθ + (y-cy)cosθ + cy angle_rad = np.radians(angle_deg) x -= cx y -= cy x_new = x * np.cos(angle_rad) + y * np.sin(angle_rad) y_new = -x * np.sin(angle_rad) + y * np.cos(angle_rad) # ... ``` **或者**,如果我们不想改数学公式,就在 `correct_boxes_skew` 里,**不要取反**,也不要改符号。 * 歪了 `0.48` (逆时针)。 * 想顺时针转 `0.48`。 * 现在的 `rotate_point`:传入正值就是顺时针。 * 所以直接传 `0.48` 进去就行了! **回顾之前的修改:** 之前代码是: `correction_angle = -skew_angle` 传入 `-0.48`。 `rotate_point` 接收 `-0.48` -> 逆时针旋转 -> **错误**。 **如果改成:** `correction_angle = skew_angle` (或者去掉那个负号) 传入 `0.48`。 `rotate_point` 接收 `0.48` -> 顺时针旋转 -> **正确!** **所以,解决方案是:** 在 `table_cell_matcher.py` 和 `table_line_generator.py` 中,计算 `correction_angle` 时,**不要取反**。或者在 `bbox_extractor.py` 的 `rotate_point` 里改公式。 考虑到 `PIL.rotate` 的行为是 **正值=逆时针**。 为了让 `correction_angle` 的含义在 PIL 和 bbox 上统一(都表示“逆时针旋转多少度”): 如果我要顺时针修 `0.48` 度,我的 `correction_angle` 应该是 `-0.48`。 * PIL: `rotate(-0.48)` -> 顺时针。OK。 * BBox: `rotate_point(..., -0.48)` -> 此时希望它是顺时针。 * 但现在的 `rotate_point(-0.48)` 是逆时针! **最终结论:** 必须修改 `rotate_point` 的公式,让它 **正值=逆时针**。这样才能和 PIL 统一,也能符合直觉。 **修改计划:** 修改 `ocr_verify/merger/bbox_extractor.py` 中的 `rotate_point` 方法,使其在图像坐标系下遵循 **正值=逆时针** 的规则。 ```python @staticmethod def rotate_point(point: Tuple[float, float], angle_deg: float, center: Tuple[float, float] = (0, 0)) -> Tuple[float, float]: """ 旋转点坐标 (图像坐标系:Y轴向下) Args: angle_deg: 旋转角度(度数)。正值 = 逆时针旋转。 """ # ... # 图像坐标系逆时针旋转公式: # x' = x cosθ + y sinθ # y' = -x sinθ + y cosθ x_new = x * np.cos(angle_rad) + y * np.sin(angle_rad) y_new = -x * np.sin(angle_rad) + y * np.cos(angle_rad) # ... ``` 这样改完后: 1. 图片逆时针歪 `0.48`。 2. `skew_angle = 0.48`。 3. `correction_angle = -0.48` (表示需要顺时针转)。 4. PIL: `rotate(-0.48)` -> 顺时针。**对**。 5. BBox: `rotate_point(-0.48)` -> 公式修正后 -> 顺时针。**对**。 我这就去修改 `rotate_point`。 ```python correction_angle = -skew_angle # correction_angle = -(-0.848) = +0.848 paddle_boxes = BBoxExtractor.correct_boxes_skew( paddle_boxes, correction_angle, image_size # 传入 +0.848 ) ``` ```python dx = x2 - x1 dy = y2 - y1 if abs(dx) > 10: angle_rad = -np.arctan2(dy, dx) # 注意这个负号! ``` ```python x1, y1 = poly[0] # 左上 x2, y2 = poly[1] # 右上 dx = x2 - x1 dy = y2 - y1 ``` ```python def rotate_point(point: Tuple[float, float], angle_deg: float, center: Tuple[float, float]) -> Tuple[float, float]: # ... x_new = x * np.cos(angle_rad) - y * np.sin(angle_rad) y_new = x * np.sin(angle_rad) + y * np.cos(angle_rad) ``` ```python def rotate_point(..., angle_deg, ...): # 图像坐标系下,逆时针旋转公式 # x' = (x-cx)cosθ + (y-cy)sinθ + cx # y' = -(x-cx)sinθ + (y-cy)cosθ + cy angle_rad = np.radians(angle_deg) x -= cx y -= cy x_new = x * np.cos(angle_rad) + y * np.sin(angle_rad) y_new = -x * np.sin(angle_rad) + y * np.cos(angle_rad) # ... ``` ```python @staticmethod def rotate_point(point: Tuple[float, float], angle_deg: float, center: Tuple[float, float] = (0, 0)) -> Tuple[float, float]: """ 旋转点坐标 (图像坐标系:Y轴向下) Args: angle_deg: 旋转角度(度数)。正值 = 逆时针旋转。 """ # ... # 图像坐标系逆时针旋转公式: # x' = x cosθ + y sinθ # y' = -x sinθ + y cosθ x_new = x * np.cos(angle_rad) + y * np.sin(angle_rad) y_new = -x * np.sin(angle_rad) + y * np.cos(angle_rad) # ... ``` ```python """ bbox 提取模块 负责从 PaddleOCR 结果中提取文字框信息 """ from typing import List, Dict, Tuple import numpy as np from pathlib import Path class BBoxExtractor: """bbox 提取器""" @staticmethod def extract_paddle_text_boxes(paddle_data: Dict) -> Tuple[List[Dict], float, Tuple[int, int]]: """ 提取 PaddleOCR 的文字框信息 Args: paddle_data: PaddleOCR 输出的数据 Returns: 文字框列表(保持旋转后的angle角度)和旋转角度 """ text_boxes = [] rotation_angle = 0.0 orig_image_size = (0,0) if 'overall_ocr_res' not in paddle_data: return text_boxes, rotation_angle, orig_image_size ocr_res = paddle_data['overall_ocr_res'] rec_texts = ocr_res.get('rec_texts', []) rec_polys = ocr_res.get('rec_polys', []) rec_scores = ocr_res.get('rec_scores', []) # 🎯 获取旋转角度 rotation_angle = BBoxExtractor._get_rotation_angle(paddle_data) if rotation_angle != 0: orig_image_size = BBoxExtractor._get_original_image_size(paddle_data) print(f"🔄 检测到旋转角度: {rotation_angle}°") print(f"📐 原始图像尺寸: {orig_image_size[0]} x {orig_image_size[1]}") for i, (text, poly, score) in enumerate(zip(rec_texts, rec_polys, rec_scores)): if text and text.strip(): # 计算 bbox (x_min, y_min, x_max, y_max) bbox = BBoxExtractor._poly_to_bbox(poly) text_boxes.append({ 'text': text, 'bbox': bbox, 'poly': poly, 'score': score, 'paddle_bbox_index': i, 'used': False }) return text_boxes, rotation_angle, orig_image_size @staticmethod def extract_paddle_text_boxes_inverse_rotate(paddle_data: Dict) -> Tuple[List[Dict], float, Tuple[int, int]]: """ 提取 PaddleOCR 的文字框信息 Args: paddle_data: PaddleOCR 输出的数据 Returns: 文字框列表(坐标已转换为 angle=0 时的坐标) """ text_boxes = [] rotation_angle = 0.0 orig_image_size = (0,0) if 'overall_ocr_res' not in paddle_data: return text_boxes, rotation_angle, orig_image_size ocr_res = paddle_data['overall_ocr_res'] rec_texts = ocr_res.get('rec_texts', []) rec_polys = ocr_res.get('rec_polys', []) rec_scores = ocr_res.get('rec_scores', []) # 🎯 获取旋转角度 rotation_angle = BBoxExtractor._get_rotation_angle(paddle_data) if rotation_angle != 0: orig_image_size = BBoxExtractor._get_original_image_size(paddle_data) print(f"🔄 检测到旋转角度: {rotation_angle}°") print(f"📐 原始图像尺寸: {orig_image_size[0]} x {orig_image_size[1]}") for i, (text, poly, score) in enumerate(zip(rec_texts, rec_polys, rec_scores)): if text and text.strip(): # 🎯 如果有旋转角度,转换坐标 if rotation_angle != 0 and orig_image_size: poly = BBoxExtractor._inverse_rotate_coordinates( poly, rotation_angle, orig_image_size ) # 计算 bbox (x_min, y_min, x_max, y_max) bbox = BBoxExtractor._poly_to_bbox(poly) text_boxes.append({ 'text': text, 'bbox': bbox, 'poly': poly, 'score': score, 'paddle_bbox_index': i, 'used': False }) return text_boxes, rotation_angle, orig_image_size @staticmethod def _get_rotation_angle(paddle_data: Dict) -> float: """获取旋转角度""" if 'doc_preprocessor_res' not in paddle_data: return 0.0 doc_res = paddle_data['doc_preprocessor_res'] if isinstance(doc_res, dict) and 'angle' in doc_res: return float(doc_res['angle']) return 0.0 @staticmethod def _get_original_image_size(paddle_data: Dict) -> tuple: """ 获取原始图像尺寸(从图片文件读取) Args: paddle_data: PaddleOCR 数据 Returns: (width, height) 元组 """ from PIL import Image # 🎯 从 input_path 读取图像 input_path = paddle_data.get('input_path') if input_path and Path(input_path).exists(): try: with Image.open(input_path) as img: # 返回原始图像尺寸 return img.size # (width, height) except Exception as e: print(f"⚠️ 无法读取图像文件 {input_path}: {e}") # 🎯 降级方案:从 layout_det_res 推断 if 'layout_det_res' in paddle_data: layout_res = paddle_data['layout_det_res'] if 'boxes' in layout_res and layout_res['boxes']: max_x = 0 max_y = 0 for box in layout_res['boxes']: coord = box.get('coordinate', []) if len(coord) >= 4: max_x = max(max_x, coord[2]) max_y = max(max_y, coord[3]) if max_x > 0 and max_y > 0: return (int(max_x) + 50, int(max_y) + 50) # 🎯 最后降级:从 overall_ocr_res 推断 if 'overall_ocr_res' in paddle_data: ocr_res = paddle_data['overall_ocr_res'] rec_polys = ocr_res.get('rec_polys', []) if rec_polys: max_x = 0 max_y = 0 for poly in rec_polys: for point in poly: max_x = max(max_x, point[0]) max_y = max(max_y, point[1]) if max_x > 0 and max_y > 0: return (int(max_x) + 50, int(max_y) + 50) # 🎯 默认 A4 尺寸 print("⚠️ 无法确定原始图像尺寸,使用默认值") return (2480, 3508) @staticmethod def rotate_box_coordinates(bbox: List[float], angle: float, orig_image_size: tuple) -> List[float]: """ 旋转 bbox 坐标(与图像旋转保持一致) 参考 ocr_validator_utils.rotate_image_and_coordinates 的操作 旋转逻辑: - 0°: 不旋转 - 90°: 逆时针旋转 90° - 180°: 旋转 180° - 270°: 顺时针旋转 90°(或逆时针 270°) Args: bbox: 原图像上的边界框 [x_min, y_min, x_max, y_max] angle: 旋转角度(0, 90, 180, 270) orig_image_size: 原始图像尺寸 (width, height) """ poly = BBoxExtractor._bbox_to_poly(bbox) rotated_poly = BBoxExtractor._rotate_coordinates(poly, angle, orig_image_size) rotated_bbox = BBoxExtractor._poly_to_bbox(rotated_poly) return rotated_bbox @staticmethod def inverse_rotate_box_coordinates(bbox: List[float], angle: float, orig_image_size: tuple) -> List[float]: """ 反向旋转 bbox 坐标 参考 ocr_validator_utils.rotate_image_and_coordinates 的逆操作 PaddleOCR 在旋转后的图像上识别,坐标是旋转后的 我们需要将坐标转换回原始图像(未旋转) Args: bbox: 旋转后图像上的边界框 [x_min, y_min, x_max, y_max] angle: 旋转角度(度数,PaddleX 使用的角度) orig_image_size: 原始图像尺寸 (width, height) """ poly = BBoxExtractor._bbox_to_poly(bbox) inverse_poly = BBoxExtractor._inverse_rotate_coordinates(poly, angle, orig_image_size) inverse_bbox = BBoxExtractor._poly_to_bbox(inverse_poly) return inverse_bbox @staticmethod def _inverse_rotate_coordinates(poly: List[List[float]], angle: float, orig_image_size: tuple) -> List[List[float]]: """ 反向旋转坐标 参考 ocr_validator_utils.rotate_image_and_coordinates 的逆操作 PaddleOCR 在旋转后的图像上识别,坐标是旋转后的 我们需要将坐标转换回原始图像(未旋转) Args: poly: 旋转后图像上的多边形坐标 [[x',y'], ...] angle: 旋转角度(度数,PaddleX 使用的角度) orig_image_size: 原始图像尺寸 (width, height) Returns: 原始图像上的多边形坐标 [[x,y], ...] """ orig_width, orig_height = orig_image_size # 🎯 根据旋转角度计算旋转后的图像尺寸 if angle == 90: rotated_width, rotated_height = orig_height, orig_width elif angle == 270: rotated_width, rotated_height = orig_height, orig_width else: rotated_width, rotated_height = orig_width, orig_height inverse_poly = [] for point in poly: x_rot, y_rot = point[0], point[1] # 旋转后的坐标 # 🎯 反向旋转(参考 rotate_image_and_coordinates 的逆操作) if angle == 90: # 正向: rotated = image.rotate(90, expand=True) # x_rot = y_orig # y_rot = rotated_width - x_orig = orig_height - x_orig # 反向: x_orig = rotated_width - y_rot = orig_height - y_rot # y_orig = x_rot x_orig = rotated_width - y_rot y_orig = x_rot elif angle == 270: # 正向: rotated = image.rotate(-90, expand=True) # x_rot = rotated_width - y_orig = orig_height - y_orig # y_rot = x_orig # 反向: y_orig = rotated_width - x_rot = orig_height - x_rot # x_orig = y_rot x_orig = y_rot y_orig = rotated_width - x_rot elif angle == 180: # 正向: rotated = image.rotate(180) # x_rot = orig_width - x_orig # y_rot = orig_height - y_orig # 反向: x_orig = orig_width - x_rot # y_orig = orig_height - y_rot x_orig = orig_width - x_rot y_orig = orig_height - y_rot else: # 其他角度或0度,不转换 x_orig = x_rot y_orig = y_rot inverse_poly.append([x_orig, y_orig]) return inverse_poly @staticmethod def _rotate_coordinates(poly: List[List[float]], angle: float, orig_image_size: tuple) -> List[List[float]]: """ 旋转多边形坐标(与图像旋转保持一致) 参考 ocr_validator_utils.rotate_image_and_coordinates 的操作 旋转逻辑: - 0°: 不旋转 - 90°: 逆时针旋转 90° - 180°: 旋转 180° - 270°: 顺时针旋转 90°(或逆时针 270°) Args: poly: 原图像上的多边形坐标 [[x', y'], ...] angle: 旋转角度(0, 90, 180, 270) orig_image_size: 原始图像尺寸 (width, height) Returns: 旋转后的多边形坐标 [[x, y], ...] Example: >>> poly = [[100, 200], [150, 200], [150, 250], [100, 250]] >>> rotated = rotate_coordinates(poly, 90, (1000, 800)) >>> print(rotated) [[200, 900], [200, 850], [250, 850], [250, 900]] """ if not poly or angle == 0: return poly orig_width, orig_height = orig_image_size rotated_poly = [] for point in poly: x, y = point[0], point[1] if angle == 90: # 逆时针旋转 90° # 新坐标系: 宽度=原高度, 高度=原宽度 # x_new = y_old # y_new = 原宽度 - x_old new_x = y new_y = orig_width - x elif angle == 180: # 旋转 180° # 新坐标系: 宽度=原宽度, 高度=原高度 # x_new = 原宽度 - x_old # y_new = 原高度 - y_old new_x = orig_width - x new_y = orig_height - y elif angle == 270: # 顺时针旋转 90°(或逆时针 270°) # 新坐标系: 宽度=原高度, 高度=原宽度 # x_new = 原高度 - y_old # y_new = x_old new_x = orig_height - y new_y = x else: # 不支持的角度,保持原坐标 new_x, new_y = x, y rotated_poly.append([new_x, new_y]) return rotated_poly @staticmethod def _bbox_to_poly(bbox: List[float]) -> List[List[float]]: """ 将 bbox 转换为多边形(4个角点,逆时针顺序) Args: bbox: 边界框 [x_min, y_min, x_max, y_max] Returns: 多边形坐标 [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] 顺序:左上 -> 右上 -> 右下 -> 左下(逆时针) Example: >>> bbox = [100, 200, 150, 250] >>> poly = BBoxExtractor._bbox_to_poly(bbox) >>> print(poly) [[100, 200], [150, 200], [150, 250], [100, 250]] """ if not bbox or len(bbox) < 4: return [] x_min, y_min, x_max, y_max = bbox[:4] # 🎯 4个角点(逆时针顺序) poly = [ [x_min, y_min], # 左上角 [x_max, y_min], # 右上角 [x_max, y_max], # 右下角 [x_min, y_max] # 左下角 ] return poly @staticmethod def _poly_to_bbox(poly: List[List[float]]) -> List[float]: """将多边形转换为 bbox [x_min, y_min, x_max, y_max]""" xs = [p[0] for p in poly] ys = [p[1] for p in poly] return [min(xs), min(ys), max(xs), max(ys)] @staticmethod def extract_table_cells_with_bbox(merged_data: List[Dict]) -> List[Dict]: """ 提取所有表格单元格及其 bbox 信息 Args: merged_data: 合并后的数据 Returns: 单元格列表 """ import json from bs4 import BeautifulSoup cells = [] for item in merged_data: if item['type'] != 'table': continue html = item.get('table_body_with_bbox', item.get('table_body', '')) soup = BeautifulSoup(html, 'html.parser') for row_idx, row in enumerate(soup.find_all('tr')): for col_idx, cell in enumerate(row.find_all(['td', 'th'])): cell_text = cell.get_text(strip=True) bbox_str = cell.get('data-bbox', '') if bbox_str: try: bbox = json.loads(bbox_str) cells.append({ 'text': cell_text, 'bbox': bbox, 'row': row_idx, 'col': col_idx, 'score': float(cell.get('data-score', 0)), 'paddle_index': int(cell.get('data-paddle-index', -1)) }) except (json.JSONDecodeError, ValueError): pass return cells @staticmethod def calculate_skew_angle(paddle_boxes: List[Dict], sample_ratio: float = 0.5, outlier_threshold: float = 0.3) -> float: """ 计算文档倾斜角度(基于文本行分析) Args: paddle_boxes: Paddle OCR 结果(包含 poly) sample_ratio: 采样比例(使用中间区域) outlier_threshold: 异常值阈值(弧度) Returns: 倾斜角度(度数,正值=逆时针,负值=顺时针) """ if not paddle_boxes: return 0.0 # 收集文本行的倾斜角度 line_angles = [] for box in paddle_boxes: poly = box.get('poly', []) if len(poly) < 4: continue x1, y1 = poly[0] x2, y2 = poly[1] width = abs(x2 - x1) height = abs(poly[2][1] - y1) # 过滤条件 if width < 50 or width < height * 0.5: continue dx = x2 - x1 dy = y2 - y1 if abs(dx) > 10: angle_rad = -np.arctan2(dy, dx) if abs(angle_rad) < np.radians(15): line_angles.append({ 'angle': angle_rad, 'weight': width, 'y_center': (y1 + poly[2][1]) / 2 }) if len(line_angles) < 5: return 0.0 # 中间区域采样 line_angles.sort(key=lambda x: x['y_center']) start_idx = int(len(line_angles) * (1 - sample_ratio) / 2) end_idx = int(len(line_angles) * (1 + sample_ratio) / 2) sampled_angles = line_angles[start_idx:end_idx] # 计算中位数 raw_angles = [item['angle'] for item in sampled_angles] median_angle = np.median(raw_angles) # 过滤异常值 filtered_angles = [ item for item in sampled_angles if abs(item['angle'] - median_angle) < outlier_threshold ] if len(filtered_angles) < 3: return np.degrees(median_angle) # 加权平均 total_weight = sum(item['weight'] for item in filtered_angles) weighted_angle = sum( item['angle'] * item['weight'] for item in filtered_angles ) / total_weight return np.degrees(weighted_angle) @staticmethod def rotate_point(point: Tuple[float, float], angle_deg: float, center: Tuple[float, float] = (0, 0)) -> Tuple[float, float]: """ 旋转点坐标 (图像坐标系:Y轴向下) Args: point: 原始点 (x, y) angle_deg: 旋转角度(度数,正值=逆时针) center: 旋转中心 Returns: 旋转后的点 (x', y') """ x, y = point cx, cy = center angle_rad = np.radians(angle_deg) x -= cx y -= cy # 图像坐标系(Y轴向下)下的逆时针旋转公式 # x' = x cosθ + y sinθ # y' = -x sinθ + y cosθ x_new = x * np.cos(angle_rad) + y * np.sin(angle_rad) y_new = -x * np.sin(angle_rad) + y * np.cos(angle_rad) x_new += cx y_new += cy return (x_new, y_new) @staticmethod def correct_boxes_skew(paddle_boxes: List[Dict], correction_angle: float, image_size: Tuple[int, int]) -> List[Dict]: """ 校正文本框的倾斜 Args: paddle_boxes: Paddle OCR 结果 correction_angle: 校正旋转角度(度数,正值=逆时针,负值=顺时针) 注意:这里直接传入需要旋转的角度,不再自动取反 image_size: 图像尺寸 (width, height) Returns: 校正后的文本框列表 """ if abs(correction_angle) < 0.01: return paddle_boxes width, height = image_size center = (width / 2, height / 2) corrected_boxes = [] for box in paddle_boxes: poly = box.get('poly', []) # 🆕 修复:如果没有 poly,尝试从 bbox 生成 # 这是为了兼容 MinerU 或其他没有 poly 的数据源 if not poly or len(poly) < 4: if 'bbox' in box and len(box['bbox']) == 4: poly = BBoxExtractor._bbox_to_poly(box['bbox']) else: corrected_boxes.append(box) continue # 旋转多边形 rotated_poly = [] for point in poly: # 确保点是 tuple 或 list,并只有 2 个坐标 p = (point[0], point[1]) if isinstance(point, (list, tuple)) and len(point) >= 2 else (0.0, 0.0) # 直接使用 correction_angle 进行旋转 rotated_point = BBoxExtractor.rotate_point(p, correction_angle, center) rotated_poly.append([rotated_point[0], rotated_point[1]]) # 转换回 list 以匹配 _poly_to_bbox 类型 # 重新计算 bbox corrected_bbox = BBoxExtractor._poly_to_bbox(rotated_poly) corrected_box = box.copy() corrected_box['bbox'] = corrected_bbox corrected_box['poly'] = rotated_poly corrected_box['original_bbox'] = box['bbox'] corrected_boxes.append(corrected_box) return corrected_boxes ```