aa

好的,如果你已经能从表格识别步骤中获取到 polygons 数据(一个包含多个单元格多边形坐标的列表),我们可以专注于 Java 实现的以下部分:

  1. 解析 polygons 数据:将原始的坐标列表转换为结构化的单元格对象。
  2. 单元格排序:按照从上到下、从左到右的顺序。
  3. 分配行列编号:为每个单元格确定其在表格中的逻辑行和列。
  4. 单元格裁剪:根据每个单元格的边界框从原始图像中裁剪出对应的图像区域。

下面是更新后的 Java 代码,它将实现这些功能。我们将使用之前定义的 DTO(数据传输对象),并对其进行一些调整以处理浮点坐标。

1. 更新/确认 DTOs (数据传输对象)

确保你的 DTOs 如下所示:

  • Point.java (使用 double 类型处理浮点坐标)
package com.example.tablerecognition.dto;

// 使用 double 来精确表示坐标
public record Point(double x, double y) {}
  • CellPolygon.java (表示一个单元格的多边形及其边界框)
    这个类将负责从8个双精度浮点数(代表4个点)的列表中创建多边形,并计算其边界框。
package com.example.tablerecognition.dto;

import java.util.ArrayList;
import java.util.List;

public class CellPolygon {
    private final List<Point> points; // 原始的4个角点
    private final double minX, minY, maxX, maxY; // 单元格的边界框

    // 构造函数,接收一个包含8个double值的列表 (x1,y1, x2,y2, x3,y3, x4,y4)
    public CellPolygon(List<Double> coordinates) {
        if (coordinates == null || coordinates.size() != 8) {
            throw new IllegalArgumentException("Coordinates list must contain 8 values (4 points).");
        }
        this.points = new ArrayList<>();
        for (int i = 0; i < coordinates.size(); i += 2) {
            this.points.add(new Point(coordinates.get(i), coordinates.get(i + 1)));
        }

        double currentMinX = Double.MAX_VALUE;
        double currentMinY = Double.MAX_VALUE;
        double currentMaxX = Double.MIN_VALUE;
        double currentMaxY = Double.MIN_VALUE;

        for (Point p : this.points) {
            if (p.x() < currentMinX) currentMinX = p.x();
            if (p.y() < currentMinY) currentMinY = p.y();
            if (p.x() > currentMaxX) currentMaxX = p.x();
            if (p.y() > currentMaxY) currentMaxY = p.y();
        }
        this.minX = currentMinX;
        this.minY = currentMinY;
        this.maxX = currentMaxX;
        this.maxY = currentMaxY;
    }

    // Getters
    public List<Point> getPoints() { return points; }
    public double getMinX() { return minX; }
    public double getMinY() { return minY; } // 用于排序的 "left_top.y"
    public double getMaxX() { return maxX; }
    public double getMaxY() { return maxY; }

    // 用于排序的 "left_top.x" (使用边界框的左上角X)
    public double getEffectiveLeftTopX() { return minX; }
    // 用于排序的 "left_top.y" (使用边界框的左上角Y)
    public double getEffectiveLeftTopY() { return minY; }

    // 单元格高度,用于行ID分配
    public double getHeight() {
        return maxY - minY;
    }

    @Override
    public String toString() {
        return "CellPolygon{" +
                "points=" + points +
                ", minX=" + minX +
                ", minY=" + minY +
                ", maxX=" + maxX +
                ", maxY=" + maxY +
                '}';
    }
}
  • CellInfo.java (包含单元格多边形、行号和列号)
package com.example.tablerecognition.dto;

public class CellInfo {
    private final CellPolygon polygon; // 单元格的多边形和边界框信息
    private int rowId;
    private int colId;
    private BufferedImage croppedImage; // 存储裁剪后的图像

    public CellInfo(CellPolygon polygon) {
        this.polygon = polygon;
    }

    // Getters and Setters
    public CellPolygon getPolygon() { return polygon; }
    public int getRowId() { return rowId; }
    public void setRowId(int rowId) { this.rowId = rowId; }
    public int getColId() { return colId; }
    public void setColId(int colId) { this.colId = colId; }
    public BufferedImage getCroppedImage() { return croppedImage; }
    public void setCroppedImage(BufferedImage croppedImage) { this.croppedImage = croppedImage; }


    @Override
    public String toString() {
        return "CellInfo{" +
            "polygon=" + polygon +
            ", rowId=" + rowId +
            ", colId=" + colId +
            (croppedImage != null ? ", croppedImage.size=" + croppedImage.getWidth() + "x" + croppedImage.getHeight() : "") +
            '}';
    }
}
  • java.awt.image.BufferedImage 将用于存储裁剪后的图像。

2. ImageProcessorService.java 的核心逻辑

这个服务类将包含转换、排序、编号和裁剪的逻辑。

package com.example.tablerecognition.service;

import com.example.tablerecognition.dto.CellInfo;
import com.example.tablerecognition.dto.CellPolygon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; // 如果你需要处理上传

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class ImageProcessorService {
    private static final Logger logger = LoggerFactory.getLogger(ImageProcessorService.class);

    // 此方法现在主要用于演示单元格处理,而不是完整的YOLO检测流程
    // 它接收原始图像和已提取的多边形数据
    public List<CellInfo> processCellsFromPolygons(BufferedImage originalImage, List<List<Double>> rawPolygonsData) {
        // 1. 转换原始多边形数据为 CellInfo 对象列表
        List<CellInfo> cells = convertRawPolygonsToCellInfoList(rawPolygonsData);
        logger.info("Initial cells count: {}", cells.size());

        // 2. 按位置排序单元格 (从上到下,从左到右)
        sortCells(cells);
        logger.info("Cells sorted.");

        // 3. 分配行列编号
        assignRowColIds(cells);
        logger.info("Row and Column IDs assigned.");

        // 4. 裁剪每个单元格的图像区域
        cropCellImages(originalImage, cells);
        logger.info("Cell images cropped.");

        // 打印一些结果信息
        cells.forEach(cell -> logger.debug("Processed Cell: Row {}, Col {}, MinX: {}, MinY: {}, Width: {}, Height: {}",
                cell.getRowId(), cell.getColId(),
                cell.getPolygon().getMinX(), cell.getPolygon().getMinY(),
                cell.getPolygon().getMaxX() - cell.getPolygon().getMinX(),
                cell.getPolygon().getHeight()));
        
        return cells; // 返回处理过的单元格信息列表,包括裁剪后的图像
    }

    /**
     * 将原始多边形坐标列表转换为 CellInfo 对象列表。
     * 每个内部列表代表一个单元格的8个坐标值 (x1,y1, x2,y2, x3,y3, x4,y4)。
     */
    private List<CellInfo> convertRawPolygonsToCellInfoList(List<List<Double>> rawPolygonsList) {
        if (rawPolygonsList == null) {
            return new ArrayList<>();
        }
        return rawPolygonsList.stream()
                .map(coords -> {
                    try {
                        CellPolygon polygon = new CellPolygon(coords);
                        return new CellInfo(polygon);
                    } catch (IllegalArgumentException e) {
                        logger.warn("Skipping invalid polygon data: {}", coords, e);
                        return null;
                    }
                })
                .filter(java.util.Objects::nonNull)
                .collect(Collectors.toList());
    }

    /**
     * 按A4纸顺序(从上到下,从左到右)排序单元格。
     * 使用单元格边界框的左上角Y坐标,然后是X坐标。
     */
    private void sortCells(List<CellInfo> cells) {
        cells.sort(Comparator
                .comparingDouble((CellInfo cell) -> cell.getPolygon().getEffectiveLeftTopY())
                .thenComparingDouble((CellInfo cell) -> cell.getPolygon().getEffectiveLeftTopX())
        );
    }

    /**
     * 分配逻辑行号和列号。
     */
    private void assignRowColIds(List<CellInfo> cells) {
        if (cells == null || cells.isEmpty()) {
            return;
        }

        double tolerance = 10.0; // 允许的行Y坐标误差 (与Python代码一致)
        cells.get(0).setRowId(1);
        cells.get(0).setColId(1);
        // 使用第一个单元格的高度作为初始的基准行高
        double baseRowHeight = cells.get(0).getPolygon().getHeight();

        for (int i = 1; i < cells.size(); i++) {
            CellInfo prev = cells.get(i - 1);
            CellInfo curr = cells.get(i);
            double currCellHeight = curr.getPolygon().getHeight();

            // 比较当前单元格和前一个单元格的左上角Y坐标
            // Python: abs(curr.value.left_top.y - prev.value.left_top.y) > max(base_row_height/2, tolerance)
            if (Math.abs(curr.getPolygon().getEffectiveLeftTopY() - prev.getPolygon().getEffectiveLeftTopY()) >
                Math.max(baseRowHeight / 2.0, tolerance)) {
                // 新行
                curr.setRowId(prev.getRowId() + 1);
                curr.setColId(1);
                baseRowHeight = currCellHeight > 0 ? currCellHeight : baseRowHeight; // 更新基准行高,防止除以0或无效高度
            } else {
                // 同一行
                curr.setRowId(prev.getRowId());
                curr.setColId(prev.getColId() + 1);
            }
        }
    }

    /**
     * 从原始图像中裁剪每个单元格的图像区域。
     */
    private void cropCellImages(BufferedImage originalImage, List<CellInfo> cells) {
        if (originalImage == null) {
            logger.error("Original image is null, cannot crop cells.");
            return;
        }
        int imgWidth = originalImage.getWidth();
        int imgHeight = originalImage.getHeight();

        for (CellInfo cell : cells) {
            CellPolygon polygon = cell.getPolygon();
            // 确保裁剪区域在图像范围内
            // .getSubimage的x,y是相对于图像左上角的坐标
            int x = (int) Math.round(Math.max(0, polygon.getMinX()));
            int y = (int) Math.round(Math.max(0, polygon.getMinY()));
            // .getSubimage的width,height是裁剪区域的宽高
            int width = (int) Math.round(polygon.getMaxX() - polygon.getMinX());
            int height = (int) Math.round(polygon.getMaxY() - polygon.getMinY());

            // 调整确保裁剪区域不超过图像边界
            if (x + width > imgWidth) {
                width = imgWidth - x;
            }
            if (y + height > imgHeight) {
                height = imgHeight - y;
            }

            if (width > 0 && height > 0) {
                try {
                    BufferedImage cropped = originalImage.getSubimage(x, y, width, height);
                    cell.setCroppedImage(cropped); // 将裁剪的图像存入CellInfo
                } catch (Exception e) {
                     logger.error("Error cropping cell image for cell Row {}, Col {}: x={}, y={}, w={}, h={}. Error: {}",
                            cell.getRowId(), cell.getColId(), x, y, width, height, e.getMessage());
                }
            } else {
                logger.warn("Skipping crop for cell Row {}, Col {} due to invalid dimensions: w={}, h={}",
                           cell.getRowId(), cell.getColId(), width, height);
            }
        }
    }

    // --- 用于测试的辅助方法 ---
    public static void main(String[] args) {
        // 模拟原始图像 (实际中你会从文件加载)
        BufferedImage testImage = new BufferedImage(2000, 1000, BufferedImage.TYPE_INT_RGB); // 示例尺寸

        // 你提供的 polygons 数据
        List<List<Double>> rawPolygonsData = List.of(
                List.of(1097.25537109375, 505.6126708984375, 1096.457763671875, 597.1835327148438, 1322.4451904296875, 596.0982666015625, 1320.86376953125, 505.06756591796875),
                List.of(149.78807067871094, 461.4259948730469, 149.2377471923828, 599.0805053710938, 298.29913330078125, 598.6973266601562, 298.0114440917969, 462.0270080566406),
                List.of(1320.86376953125, 505.06756591796875, 1322.4451904296875, 596.0982666015625, 1570.6605224609375, 595.882568359375, 1570.7291259765625, 504.37725830078125)
        );

        ImageProcessorService processor = new ImageProcessorService();
        List<CellInfo> processedCells = processor.processCellsFromPolygons(testImage, rawPolygonsData);

        System.out.println("\n--- Processed Cells Summary ---");
        for (CellInfo cell : processedCells) {
            System.out.printf("Cell Position: %d-%d, BBox: [minX=%.2f, minY=%.2f, maxX=%.2f, maxY=%.2f], Cropped: %s%n",
                    cell.getRowId(), cell.getColId(),
                    cell.getPolygon().getMinX(), cell.getPolygon().getMinY(),
                    cell.getPolygon().getMaxX(), cell.getPolygon().getMaxY(),
                    cell.getCroppedImage() != null ? "Yes (" + cell.getCroppedImage().getWidth() + "x" + cell.getCroppedImage().getHeight() + ")" : "No"
            );
            // 如果需要,可以在这里保存裁剪的图像到文件
            // try {
            // if (cell.getCroppedImage() != null) {
            // ImageIO.write(cell.getCroppedImage(), "PNG", new File("cropped_cell_" + cell.getRowId() + "_" + cell.getColId() + ".png"));
            // }
            // } catch (IOException e) {
            // e.printStackTrace();
            // }
        }
    }
}

如何使用和集成:

  1. 获取原始图像 (BufferedImage): 你需要先将上传的图像文件或任何图像源加载为 BufferedImage 对象。例如,在 Spring Boot 控制器中:

    // 在你的Controller中
    // @Autowired
    // private ImageProcessorService imageProcessorService;
    
    // @PostMapping("/upload-and-process")
    // public ResponseEntity<?> handleFileUpload(@RequestParam("file") MultipartFile file,
    //                                         @RequestBody List<List<Double>> polygonsData) { // 或者polygonsData来自其他途径
    //     try (InputStream inputStream = file.getInputStream()) {
    //         BufferedImage image = ImageIO.read(inputStream);
    //         if (image == null) {
    //             return ResponseEntity.badRequest().body("Invalid image file.");
    //         }
    //         // 假设 polygonsData 是你已经通过某种方式(例如请求体、另一个服务调用)获得的
    //         List<CellInfo> processedCells = imageProcessorService.processCellsFromPolygons(image, polygonsData);
    //         // ... 返回结果
    //         return ResponseEntity.ok(processedCells); // 你可能想返回一个更结构化的API响应
    //     } catch (IOException e) {
    //         logger.error("Error processing uploaded file", e);
    //         return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error processing image.");
    //     }
    // }
    
  2. 获取 polygonsData:

    • 如果 polygons 数据与图像一起上传(例如,作为 JSON 请求体的一部分),你可以直接解析它。
    • 如果 polygons 数据是由另一个 Java 服务或方法生成的(例如,一个基于 DJL 或 OpenCV 的表格识别步骤),你可以直接传递该列表。
  3. 调用 processCellsFromPolygons: 将 BufferedImagepolygonsData 传递给这个方法。

  4. 处理结果: processCellsFromPolygons 方法会返回一个 List<CellInfo>。每个 CellInfo 对象都将包含其行号、列号以及裁剪后的 BufferedImage。你可以进一步处理这些裁剪后的图像(例如,传递给YOLO模型进行对象检测,或保存到文件)。

关键点和解释:

  • CellPolygon 构造函数: 从8个 double 值创建4个 Point 对象,并计算这些点的最小/最大 X、Y 坐标以确定边界框。
  • convertRawPolygonsToCellInfoList: 将输入的原始 List<List<Double>> 转换为 List<CellInfo>
  • sortCells: 使用 Comparator 实现了与 Python 版本中 lambda 键类似的排序逻辑。首先按单元格边界框的最小 Y 坐标 (getEffectiveLeftTopY),然后按最小 X 坐标 (getEffectiveLeftTopX) 排序。
  • assignRowColIds: 逻辑与 Python 版本非常相似,使用了 tolerance 和动态计算的 baseRowHeight 来判断新行。
  • cropCellImages: 使用 BufferedImage.getSubimage(x, y, width, height) 来裁剪图像。它还包括了边界检查,以确保裁剪参数不会超出原始图像的范围。裁剪后的图像存储在 CellInfo 对象的 croppedImage 字段中。
  • main 方法: 提供了一个简单的本地测试用例,使用你提供的 polygons 示例数据和一个虚拟的 BufferedImage 来演示整个流程。

这个实现现在能够获取你的 polygons 数据,对其进行编号(分配行列ID),并完成每个单元格图像的裁剪。后续你可以将每个 cell.getCroppedImage() 传递给你的YOLO模型进行检测。

posted @ 2025-05-08 14:36  执语  阅读(12)  评论(0)    收藏  举报