aa
好的,如果你已经能从表格识别步骤中获取到 polygons 数据(一个包含多个单元格多边形坐标的列表),我们可以专注于 Java 实现的以下部分:
- 解析
polygons数据:将原始的坐标列表转换为结构化的单元格对象。 - 单元格排序:按照从上到下、从左到右的顺序。
- 分配行列编号:为每个单元格确定其在表格中的逻辑行和列。
- 单元格裁剪:根据每个单元格的边界框从原始图像中裁剪出对应的图像区域。
下面是更新后的 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();
// }
}
}
}
如何使用和集成:
-
获取原始图像 (
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."); // } // } -
获取
polygonsData:- 如果
polygons数据与图像一起上传(例如,作为 JSON 请求体的一部分),你可以直接解析它。 - 如果
polygons数据是由另一个 Java 服务或方法生成的(例如,一个基于 DJL 或 OpenCV 的表格识别步骤),你可以直接传递该列表。
- 如果
-
调用
processCellsFromPolygons: 将BufferedImage和polygonsData传递给这个方法。 -
处理结果:
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模型进行检测。

浙公网安备 33010602011771号