mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
优化OCR识别内存管理并支持分块识别
This commit is contained in:
@@ -21,6 +21,7 @@ public class OcrRecOptions {
|
||||
private boolean enableLineSplit = true;
|
||||
|
||||
|
||||
|
||||
public OcrRecOptions(boolean enableDirectionCorrect, boolean enableLineSplit) {
|
||||
this.enableDirectionCorrect = enableDirectionCorrect;
|
||||
this.enableLineSplit = enableLineSplit;
|
||||
|
||||
@@ -94,8 +94,10 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(Image image){
|
||||
long start = System.nanoTime();
|
||||
List<Image> imageList = Collections.singletonList(image);
|
||||
List<List<OcrBox>> result = batchDetectDJLImage(imageList);
|
||||
log.debug("文本检测模型单图调用耗时={}ms, 检测框数量={}", elapsedMillis(start), result.isEmpty() ? 0 : result.get(0).size());
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
@@ -192,12 +194,19 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
||||
if(!ImageUtils.isAllImageSizeEqual(imageList)){
|
||||
throw new OcrException("图片尺寸不一致");
|
||||
}
|
||||
long totalStart = System.nanoTime();
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
predictor = detPredictorPool.borrowObject();
|
||||
List<NDList> result = predictor.batchPredict(imageList);
|
||||
result.forEach(ndList -> ndList.attach(manager));
|
||||
return OcrUtils.convertToOcrBox(result);
|
||||
List<List<OcrBox>> boxes = OcrUtils.convertToOcrBox(result);
|
||||
int totalBoxes = 0;
|
||||
for (List<OcrBox> boxList : boxes) {
|
||||
totalBoxes += boxList == null ? 0 : boxList.size();
|
||||
}
|
||||
log.debug("文本检测总耗时={}ms, batchSize={}, totalBoxes={}", elapsedMillis(totalStart), imageList.size(), totalBoxes);
|
||||
return boxes;
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
@@ -257,5 +266,9 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
||||
return fromFactory;
|
||||
}
|
||||
|
||||
private long elapsedMillis(long startNanos) {
|
||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -108,13 +108,13 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
if(Objects.isNull(textDetModel)){
|
||||
throw new OcrException("textDetModel is null");
|
||||
}
|
||||
//检测文本
|
||||
List<OcrBox> boxeList = textDetModel.detect(image);
|
||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Mat srcMat = ImageUtils.toMat(image);
|
||||
return detect(boxeList, srcMat);
|
||||
List<OcrItem> result = detect(boxeList, srcMat);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,7 +274,9 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
List<Image> imageList = new ArrayList<Image>();
|
||||
List<Boolean> isRotatedList = new ArrayList<Boolean>();
|
||||
int index = 0;
|
||||
long totalStart = System.nanoTime();
|
||||
try (NDManager manager = model.getNDManager().newSubManager()){
|
||||
long prepareStart = System.nanoTime();
|
||||
for(int i = 0; i < srcMatList.size(); i++){
|
||||
for (int j = 0; j < boxList.get(i).size(); j++){
|
||||
//透视变换及裁剪
|
||||
@@ -292,8 +294,11 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
log.debug("方向模型裁剪预处理耗时={}ms, batchSize={}, textBlocks={}", elapsedMillis(prepareStart), srcMatList.size(), imageList.size());
|
||||
List<List<OcrItem>> result = new ArrayList<>();
|
||||
long predictStart = System.nanoTime();
|
||||
List<DirectionInfo> directionInfos = batchDetect(imageList);
|
||||
log.debug("方向分类模型调用耗时={}ms, textBlocks={}", elapsedMillis(predictStart), imageList.size());
|
||||
//释放
|
||||
imageList.forEach(image -> ImageUtils.releaseOpenCVMat(image));
|
||||
if(CollectionUtils.isEmpty(directionInfos)){
|
||||
@@ -327,6 +332,7 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
}
|
||||
result.add(ocrItemList);
|
||||
}
|
||||
log.debug("方向模型总耗时={}ms, batchSize={}, textBlocks={}", elapsedMillis(totalStart), srcMatList.size(), index);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -335,7 +341,8 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
Predictor<Image, DirectionInfo> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
return predictor.batchPredict(imageList);
|
||||
List<DirectionInfo> result = predictor.batchPredict(imageList);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
@@ -399,4 +406,8 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
||||
public boolean isFromFactory() {
|
||||
return fromFactory;
|
||||
}
|
||||
|
||||
private long elapsedMillis(long startNanos) {
|
||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,19 @@ public interface OcrCommonRecModel extends AutoCloseable{
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于已有文本检测框执行文本识别。
|
||||
* 适合调用方已经完成文本检测,想避免重复检测的场景。
|
||||
*
|
||||
* @param image 原图
|
||||
* @param boxList 已有文本检测框
|
||||
* @param options 识别选项
|
||||
* @return OCR 结果
|
||||
*/
|
||||
default OcrInfo recognize(Image image, List<OcrBox> boxList, OcrRecOptions options) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
@@ -141,6 +154,18 @@ public interface OcrCommonRecModel extends AutoCloseable{
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于已有文本检测框执行批量文本识别。
|
||||
*
|
||||
* @param imageList 原图列表
|
||||
* @param boxList 每张图对应的文本检测框列表
|
||||
* @param options 识别选项
|
||||
* @return OCR 结果
|
||||
*/
|
||||
default List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, List<List<OcrBox>> boxList, OcrRecOptions options) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
default GenericObjectPool<Predictor<Image, String>> getPool() {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
|
||||
private static final int REC_CHUNK_SIZE = 64;
|
||||
|
||||
private GenericObjectPool<Predictor<Image, String>> recPredictorPool;
|
||||
|
||||
private OcrRecModelConfig config;
|
||||
@@ -111,10 +113,27 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
*/
|
||||
@Override
|
||||
public OcrInfo recognize(Image image, OcrRecOptions options) {
|
||||
long start = System.nanoTime();
|
||||
List<OcrInfo> result = batchRecognizeDJLImage(Collections.singletonList(image), options);
|
||||
if (CollectionUtils.isEmpty(result)) {
|
||||
throw new OcrException("OCR识别结果为空");
|
||||
}
|
||||
log.debug("OCR识别单图总耗时={}ms", elapsedMillis(start));
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OcrInfo recognize(Image image, List<OcrBox> boxList, OcrRecOptions options) {
|
||||
long start = System.nanoTime();
|
||||
List<OcrInfo> result = batchRecognizeDJLImage(
|
||||
Collections.singletonList(image),
|
||||
Collections.singletonList(boxList),
|
||||
options
|
||||
);
|
||||
if (CollectionUtils.isEmpty(result)) {
|
||||
throw new OcrException("OCR识别结果为空");
|
||||
}
|
||||
log.debug("OCR识别单图总耗时={}ms", elapsedMillis(start));
|
||||
return result.get(0);
|
||||
}
|
||||
|
||||
@@ -212,10 +231,8 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
//分行判断
|
||||
for (int i = 1; i < initList.size(); i++) {
|
||||
RotatedBoxCompX tmpBox = new RotatedBoxCompX(initList.get(i).getBox(), initList.get(i).getText());
|
||||
float y1 = firstBox.getBox().toFloatArray()[1];
|
||||
float y2 = tmpBox.getBox().toFloatArray()[1];
|
||||
float dis = Math.abs(y2 - y1);
|
||||
if (dis < 20) { // 认为是同 1 行 - Considered to be in the same line
|
||||
boolean isSameRow = OcrUtils.isSameRow(firstBox.getBox(), tmpBox.getBox());
|
||||
if (isSameRow) {
|
||||
line.add(tmpBox);
|
||||
} else { // 换行 - Line break
|
||||
firstBox = tmpBox;
|
||||
@@ -346,6 +363,11 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
|
||||
@Override
|
||||
public List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, OcrRecOptions options) {
|
||||
return batchRecognizeDJLImage(imageList, null, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, List<List<OcrBox>> boxeList, OcrRecOptions options) {
|
||||
if (Objects.isNull(textDetModel)) {
|
||||
throw new OcrException("textDetModel is null");
|
||||
}
|
||||
@@ -356,64 +378,82 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
if (CollectionUtils.isEmpty(imageList)) {
|
||||
throw new OcrException("imageList is empty");
|
||||
}
|
||||
//检测文本
|
||||
List<List<OcrBox>> boxeList = textDetModel.batchDetectDJLImage(imageList);
|
||||
if (CollectionUtils.isEmpty(boxeList) || boxeList.size() != imageList.size()) {
|
||||
long totalStart = System.nanoTime();
|
||||
List<List<OcrBox>> effectiveBoxList = boxeList;
|
||||
if (CollectionUtils.isEmpty(effectiveBoxList)) {
|
||||
effectiveBoxList = textDetModel.batchDetectDJLImage(imageList);
|
||||
}
|
||||
if (CollectionUtils.isEmpty(effectiveBoxList) || effectiveBoxList.size() != imageList.size()) {
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Predictor<Image, String> predictor = null;
|
||||
List<OcrInfo> ocrInfoList = new ArrayList<OcrInfo>();
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
predictor = recPredictorPool.borrowObject();
|
||||
List<Image> allImageAlignList = new ArrayList<Image>();
|
||||
//检测方向
|
||||
if (ocrRecOptions.isEnableDirectionCorrect()) {
|
||||
if (Objects.isNull(directionModel)) {
|
||||
throw new OcrException("请配置方向模型");
|
||||
}
|
||||
List<Mat> matList = imageList.stream()
|
||||
.map(image -> ImageUtils.toMat(image))
|
||||
.collect(Collectors.toList());
|
||||
List<List<OcrItem>> ocrItemList = directionModel.batchDetect(boxeList, matList);
|
||||
if (CollectionUtils.isEmpty(ocrItemList) || ocrItemList.size() != imageList.size()) {
|
||||
throw new OcrException("方向检测失败");
|
||||
}
|
||||
allImageAlignList = new ArrayList<Image>();
|
||||
for (int i = 0; i < ocrItemList.size(); i++) {
|
||||
Mat srcMat = ImageUtils.toMat(imageList.get(i));
|
||||
List<Image> imageAlignList = batchAlignWithDirection(ocrItemList.get(i), srcMat, manager);
|
||||
// for(int j = 0; j < imageAlignList.size(); j++){
|
||||
// ImageUtils.saveImage(imageAlignList.get(j),"dir-"+i+"-"+j+".png","/Users/xxx/Downloads/testing33");
|
||||
// }
|
||||
allImageAlignList.addAll(imageAlignList);
|
||||
long directionStart = System.nanoTime();
|
||||
List<Mat> matList = new ArrayList<>(imageList.size());
|
||||
try {
|
||||
for (Image image : imageList) {
|
||||
matList.add(ImageUtils.toMat(image));
|
||||
}
|
||||
List<List<OcrItem>> ocrItemList = directionModel.batchDetect(effectiveBoxList, matList);
|
||||
log.debug("OCR流程-文本方向分类耗时={}ms, batchSize={}", elapsedMillis(directionStart), imageList.size());
|
||||
if (CollectionUtils.isEmpty(ocrItemList) || ocrItemList.size() != imageList.size()) {
|
||||
throw new OcrException("方向检测失败");
|
||||
}
|
||||
long alignStart = System.nanoTime();
|
||||
List<String> textList = new ArrayList<>();
|
||||
List<Image> chunkImages = new ArrayList<>(REC_CHUNK_SIZE);
|
||||
for (int i = 0; i < ocrItemList.size(); i++) {
|
||||
Mat srcMat = matList.get(i);
|
||||
List<Image> imageAlignList = batchAlignWithDirection(ocrItemList.get(i), srcMat, manager);
|
||||
for (Image alignImage : imageAlignList) {
|
||||
chunkImages.add(alignImage);
|
||||
if (chunkImages.size() >= REC_CHUNK_SIZE) {
|
||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug("OCR流程-方向矫正裁剪耗时={}ms, textBlocks={}", elapsedMillis(alignStart), textList.size() + chunkImages.size());
|
||||
long recStart = System.nanoTime();
|
||||
if (!chunkImages.isEmpty()) {
|
||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
||||
}
|
||||
log.debug("OCR流程-识别模型调用耗时={}ms, textBlocks={}", elapsedMillis(recStart), textList.size());
|
||||
return buildOcrInfoList(effectiveBoxList, ocrRecOptions, manager, textList, imageList.size(), totalStart);
|
||||
} finally {
|
||||
releaseTemporaryMats(imageList, matList);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < boxeList.size(); i++) {
|
||||
Mat srcMat = ImageUtils.toMat(imageList.get(i));
|
||||
List<Image> imageAlignList = batchAlign(boxeList.get(i), srcMat, manager);
|
||||
// for(int j = 0; j < imageAlignList.size(); j++){
|
||||
// ImageUtils.saveImage(imageAlignList.get(j),i+"-"+j+".png","/Users/wenjie/Downloads/testing33");
|
||||
// }
|
||||
allImageAlignList.addAll(imageAlignList);
|
||||
}
|
||||
}
|
||||
List<String> textList = batchRecognize(allImageAlignList);
|
||||
int textIndex = 0;
|
||||
for (int i = 0; i < boxeList.size(); i++) {
|
||||
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
||||
for (int j = 0; j < boxeList.get(i).size(); j++) {
|
||||
if (textIndex >= textList.size()) {
|
||||
throw new OcrException("识别失败: 第" + i + "张图片, 第" + j + "个文本块,未识别到文本");
|
||||
List<String> textList = new ArrayList<>();
|
||||
List<Image> chunkImages = new ArrayList<>(REC_CHUNK_SIZE);
|
||||
for (int i = 0; i < effectiveBoxList.size(); i++) {
|
||||
Mat srcMat = null;
|
||||
try {
|
||||
srcMat = ImageUtils.toMat(imageList.get(i));
|
||||
List<Image> imageAlignList = batchAlign(effectiveBoxList.get(i), srcMat, manager);
|
||||
for (Image alignImage : imageAlignList) {
|
||||
chunkImages.add(alignImage);
|
||||
if (chunkImages.size() >= REC_CHUNK_SIZE) {
|
||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
releaseTemporaryMat(imageList.get(i), srcMat);
|
||||
}
|
||||
OcrBox box = boxeList.get(i).get(j);
|
||||
NDArray pointsArray = manager.create(box.toFloatArray());
|
||||
rotatedBoxes.add(new RotatedBox(pointsArray, textList.get(textIndex)));
|
||||
textIndex++;
|
||||
}
|
||||
OcrInfo ocrInfo = postProcessOcrResult(rotatedBoxes, ocrRecOptions);
|
||||
ocrInfoList.add(ocrInfo);
|
||||
long recStart = System.nanoTime();
|
||||
if (!chunkImages.isEmpty()) {
|
||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
||||
}
|
||||
log.debug("OCR流程-识别模型调用耗时={}ms, textBlocks={}", elapsedMillis(recStart), textList.size());
|
||||
return buildOcrInfoList(effectiveBoxList, ocrRecOptions, manager, textList, imageList.size(), totalStart);
|
||||
}
|
||||
return ocrInfoList;
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
} finally {
|
||||
@@ -432,28 +472,41 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> batchRecognize(List<Image> imageAlignList) {
|
||||
Predictor<Image, String> predictor = null;
|
||||
private List<OcrInfo> buildOcrInfoList(List<List<OcrBox>> effectiveBoxList,
|
||||
OcrRecOptions ocrRecOptions,
|
||||
NDManager manager,
|
||||
List<String> textList,
|
||||
int batchSize,
|
||||
long totalStart) {
|
||||
int textIndex = 0;
|
||||
List<OcrInfo> ocrInfoList = new ArrayList<>();
|
||||
for (int i = 0; i < effectiveBoxList.size(); i++) {
|
||||
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
||||
for (int j = 0; j < effectiveBoxList.get(i).size(); j++) {
|
||||
if (textIndex >= textList.size()) {
|
||||
throw new OcrException("识别失败: 第" + i + "张图片, 第" + j + "个文本块,未识别到文本");
|
||||
}
|
||||
OcrBox box = effectiveBoxList.get(i).get(j);
|
||||
NDArray pointsArray = manager.create(box.toFloatArray());
|
||||
rotatedBoxes.add(new RotatedBox(pointsArray, textList.get(textIndex)));
|
||||
textIndex++;
|
||||
}
|
||||
OcrInfo ocrInfo = postProcessOcrResult(rotatedBoxes, ocrRecOptions);
|
||||
ocrInfoList.add(ocrInfo);
|
||||
}
|
||||
log.debug("OCR流程总耗时={}ms, batchSize={}", elapsedMillis(totalStart), batchSize);
|
||||
return ocrInfoList;
|
||||
}
|
||||
|
||||
private List<String> batchRecognizeChunk(Predictor<Image, String> predictor, List<Image> imageAlignList) {
|
||||
try {
|
||||
predictor = recPredictorPool.borrowObject();
|
||||
List<String> textList = predictor.batchPredict(imageAlignList);
|
||||
imageAlignList.forEach(subImg -> ImageUtils.releaseOpenCVMat(subImg));
|
||||
return textList;
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
} finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
recPredictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
imageAlignList.forEach(ImageUtils::releaseOpenCVMat);
|
||||
imageAlignList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,4 +578,24 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||
public boolean isFromFactory() {
|
||||
return fromFactory;
|
||||
}
|
||||
|
||||
private long elapsedMillis(long startNanos) {
|
||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
||||
}
|
||||
|
||||
private void releaseTemporaryMats(List<Image> imageList, List<Mat> matList) {
|
||||
int size = Math.min(imageList.size(), matList.size());
|
||||
for (int i = 0; i < size; i++) {
|
||||
releaseTemporaryMat(imageList.get(i), matList.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseTemporaryMat(Image image, Mat mat) {
|
||||
if (mat == null || image == null) {
|
||||
return;
|
||||
}
|
||||
if (!(image.getWrappedImage() instanceof Mat)) {
|
||||
mat.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
278
ocr/src/main/java/cn/smartjavaai/ocr/utils/BoxUtils.java
Normal file
278
ocr/src/main/java/cn/smartjavaai/ocr/utils/BoxUtils.java
Normal file
@@ -0,0 +1,278 @@
|
||||
package cn.smartjavaai.ocr.utils;
|
||||
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2026/1/11
|
||||
*/
|
||||
public class BoxUtils {
|
||||
|
||||
|
||||
public enum Direction {
|
||||
UP, DOWN, LEFT, RIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找指定方向上距离最近的框
|
||||
*
|
||||
* @param anchor 锚点框,作为搜索的起始参考框
|
||||
* @param boxList 候选框列表,在其中搜索目标框
|
||||
* @param direction 搜索方向,可选 UP(上)、DOWN(下)、LEFT(左)、RIGHT(右)
|
||||
* @return 找到的最近的 OcrBox,如果找不到符合条件的框则返回 null
|
||||
*
|
||||
* @apiNote
|
||||
* - 该方法会在指定方向上寻找与锚点框在同一行或同一列的最近邻框
|
||||
* - 水平方向(LEFT/RIGHT):要求候选框与锚点框在 Y 轴上有重叠(同一行)
|
||||
* - 垂直方向(UP/DOWN):要求候选框与锚点框在 X 轴上有重叠(同一列)
|
||||
* - 使用欧几里得距离计算中心点之间的距离
|
||||
*/
|
||||
public static OcrBox findNearestBox(OcrBox anchor, List<OcrBox> boxList, Direction direction) {
|
||||
OcrBox nearest = null;
|
||||
double bestScore = Double.MAX_VALUE;
|
||||
AxisSystem axisSystem = buildAxisSystem(anchor);
|
||||
|
||||
for (OcrBox target : boxList) {
|
||||
if (target == anchor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CandidateMetrics metrics = evaluateCandidate(anchor, target, direction, axisSystem);
|
||||
if (metrics == null) {
|
||||
continue;
|
||||
}
|
||||
if (metrics.score < bestScore) {
|
||||
bestScore = metrics.score;
|
||||
nearest = target;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找指定方向上距离最近的多个框(按距离升序返回)
|
||||
*
|
||||
* @param anchor 锚点框
|
||||
* @param boxList 候选框列表
|
||||
* @param direction 搜索方向
|
||||
* @param limit 返回的最大数量(<=0 时返回空列表)
|
||||
* @return 按距离由近到远排序的 OcrBox 列表
|
||||
*/
|
||||
public static List<OcrBox> findNearestBoxes(OcrBox anchor, List<OcrBox> boxList, Direction direction, int limit) {
|
||||
if (anchor == null || boxList == null || boxList.isEmpty() || limit <= 0) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
AxisSystem axisSystem = buildAxisSystem(anchor);
|
||||
List<Neighbor> candidates = new ArrayList<>();
|
||||
|
||||
for (OcrBox target : boxList) {
|
||||
if (target == anchor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CandidateMetrics metrics = evaluateCandidate(anchor, target, direction, axisSystem);
|
||||
if (metrics == null) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(new Neighbor(target, metrics.score));
|
||||
}
|
||||
|
||||
candidates.sort(Comparator.comparingDouble(n -> n.distance));
|
||||
|
||||
List<OcrBox> result = new ArrayList<>();
|
||||
int size = Math.min(limit, candidates.size());
|
||||
for (int i = 0; i < size; i++) {
|
||||
result.add(candidates.get(i).box);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部使用的邻居结构体,存储框和距离
|
||||
*/
|
||||
private static class Neighbor {
|
||||
private final OcrBox box;
|
||||
private final double distance;
|
||||
|
||||
private Neighbor(OcrBox box, double distance) {
|
||||
this.box = box;
|
||||
this.distance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
private static CandidateMetrics evaluateCandidate(OcrBox anchor, OcrBox target, Direction direction, AxisSystem axisSystem) {
|
||||
double mainAxisX = isHorizontalDirection(direction) ? axisSystem.horizontalAxisX : axisSystem.verticalAxisX;
|
||||
double mainAxisY = isHorizontalDirection(direction) ? axisSystem.horizontalAxisY : axisSystem.verticalAxisY;
|
||||
double crossAxisX = isHorizontalDirection(direction) ? axisSystem.verticalAxisX : axisSystem.horizontalAxisX;
|
||||
double crossAxisY = isHorizontalDirection(direction) ? axisSystem.verticalAxisY : axisSystem.horizontalAxisY;
|
||||
|
||||
Projection anchorMain = projectBox(anchor, mainAxisX, mainAxisY);
|
||||
Projection anchorCross = projectBox(anchor, crossAxisX, crossAxisY);
|
||||
Projection targetMain = projectBox(target, mainAxisX, mainAxisY);
|
||||
Projection targetCross = projectBox(target, crossAxisX, crossAxisY);
|
||||
Point anchorCenter = getCenter(anchor);
|
||||
Point targetCenter = getCenter(target);
|
||||
double centerMainDelta = projectPointDelta(anchorCenter, targetCenter, mainAxisX, mainAxisY);
|
||||
|
||||
double mainGap;
|
||||
switch (direction) {
|
||||
case RIGHT:
|
||||
case DOWN:
|
||||
mainGap = targetMain.min - anchorMain.max;
|
||||
if (centerMainDelta <= 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case LEFT:
|
||||
case UP:
|
||||
mainGap = anchorMain.min - targetMain.max;
|
||||
if (centerMainDelta >= 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
double anchorMainSize = Math.max(1.0, anchorMain.max - anchorMain.min);
|
||||
double targetMainSize = Math.max(1.0, targetMain.max - targetMain.min);
|
||||
double allowedBacktrack = Math.min(anchorMainSize, targetMainSize) * 0.35;
|
||||
if (mainGap < -allowedBacktrack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double overlap = Math.max(0.0, Math.min(anchorCross.max, targetCross.max) - Math.max(anchorCross.min, targetCross.min));
|
||||
double minCrossSize = Math.max(1.0, Math.min(anchorCross.max - anchorCross.min, targetCross.max - targetCross.min));
|
||||
double overlapRatio = overlap / minCrossSize;
|
||||
|
||||
double anchorCrossCenter = (anchorCross.min + anchorCross.max) / 2.0;
|
||||
double targetCrossCenter = (targetCross.min + targetCross.max) / 2.0;
|
||||
double crossCenterDistance = Math.abs(anchorCrossCenter - targetCrossCenter);
|
||||
double crossTolerance = Math.max(anchorCross.max - anchorCross.min, targetCross.max - targetCross.min) * 0.6;
|
||||
|
||||
if (overlapRatio < 0.2 && crossCenterDistance > crossTolerance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double score = Math.max(0.0, mainGap) * 10.0 + crossCenterDistance + Math.abs(centerMainDelta) * 0.01;
|
||||
if (overlapRatio < 0.2) {
|
||||
score += (0.2 - overlapRatio) * 100.0;
|
||||
}
|
||||
return new CandidateMetrics(score);
|
||||
}
|
||||
|
||||
private static boolean isHorizontalDirection(Direction direction) {
|
||||
return direction == Direction.LEFT || direction == Direction.RIGHT;
|
||||
}
|
||||
|
||||
private static Point getCenter(OcrBox box) {
|
||||
float cx = (float) (box.getTopLeft().getX() + box.getTopRight().getX() + box.getBottomRight().getX() + box.getBottomLeft().getX()) / 4;
|
||||
float cy = (float) (box.getTopLeft().getY() + box.getTopRight().getY() + box.getBottomRight().getY() + box.getBottomLeft().getY()) / 4;
|
||||
return new Point(cx, cy);
|
||||
}
|
||||
|
||||
private static AxisSystem buildAxisSystem(OcrBox anchor) {
|
||||
Point topLeft = anchor.getTopLeft();
|
||||
Point topRight = anchor.getTopRight();
|
||||
Point bottomLeft = anchor.getBottomLeft();
|
||||
|
||||
double horizontalX = topRight.getX() - topLeft.getX();
|
||||
double horizontalY = topRight.getY() - topLeft.getY();
|
||||
double verticalX = bottomLeft.getX() - topLeft.getX();
|
||||
double verticalY = bottomLeft.getY() - topLeft.getY();
|
||||
|
||||
double horizontalNorm = Math.hypot(horizontalX, horizontalY);
|
||||
double verticalNorm = Math.hypot(verticalX, verticalY);
|
||||
|
||||
if (horizontalNorm < 1e-6) {
|
||||
horizontalX = 1.0;
|
||||
horizontalY = 0.0;
|
||||
horizontalNorm = 1.0;
|
||||
}
|
||||
if (verticalNorm < 1e-6) {
|
||||
verticalX = 0.0;
|
||||
verticalY = 1.0;
|
||||
verticalNorm = 1.0;
|
||||
}
|
||||
|
||||
return new AxisSystem(
|
||||
horizontalX / horizontalNorm,
|
||||
horizontalY / horizontalNorm,
|
||||
verticalX / verticalNorm,
|
||||
verticalY / verticalNorm
|
||||
);
|
||||
}
|
||||
|
||||
private static Projection projectBox(OcrBox box, double axisX, double axisY) {
|
||||
double[] values = new double[]{
|
||||
dot(box.getTopLeft(), axisX, axisY),
|
||||
dot(box.getTopRight(), axisX, axisY),
|
||||
dot(box.getBottomRight(), axisX, axisY),
|
||||
dot(box.getBottomLeft(), axisX, axisY)
|
||||
};
|
||||
double min = values[0];
|
||||
double max = values[0];
|
||||
for (int i = 1; i < values.length; i++) {
|
||||
min = Math.min(min, values[i]);
|
||||
max = Math.max(max, values[i]);
|
||||
}
|
||||
return new Projection(min, max);
|
||||
}
|
||||
|
||||
private static double dot(Point point, double axisX, double axisY) {
|
||||
return point.getX() * axisX + point.getY() * axisY;
|
||||
}
|
||||
|
||||
private static double projectPointDelta(Point from, Point to, double axisX, double axisY) {
|
||||
return (to.getX() - from.getX()) * axisX + (to.getY() - from.getY()) * axisY;
|
||||
}
|
||||
|
||||
private static double min(double a, double b, double c, double d) {
|
||||
return Math.min(Math.min(a, b), Math.min(c, d));
|
||||
}
|
||||
|
||||
private static double max(double a, double b, double c, double d) {
|
||||
return Math.max(Math.max(a, b), Math.max(c, d));
|
||||
}
|
||||
|
||||
private static class AxisSystem {
|
||||
private final double horizontalAxisX;
|
||||
private final double horizontalAxisY;
|
||||
private final double verticalAxisX;
|
||||
private final double verticalAxisY;
|
||||
|
||||
private AxisSystem(double horizontalAxisX, double horizontalAxisY, double verticalAxisX, double verticalAxisY) {
|
||||
this.horizontalAxisX = horizontalAxisX;
|
||||
this.horizontalAxisY = horizontalAxisY;
|
||||
this.verticalAxisX = verticalAxisX;
|
||||
this.verticalAxisY = verticalAxisY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Projection {
|
||||
private final double min;
|
||||
private final double max;
|
||||
|
||||
private Projection(double min, double max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CandidateMetrics {
|
||||
private final double score;
|
||||
|
||||
private CandidateMetrics(double score) {
|
||||
this.score = score;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ai.djl.modality.cv.util.NDImageUtils;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.ndarray.types.Shape;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
@@ -412,4 +413,42 @@ public class OcrUtils {
|
||||
}
|
||||
|
||||
|
||||
public static boolean isSameRow(NDArray box1, NDArray box2) {
|
||||
// 1. 确保 box 是 (4, 2) 的形状
|
||||
NDArray b1 = formatBox(box1);
|
||||
NDArray b2 = formatBox(box2);
|
||||
|
||||
// 2. 获取 Y 坐标列(索引为 1 的列)
|
||||
NDArray y1 = b1.get(":, 1");
|
||||
NDArray y2 = b2.get(":, 1");
|
||||
|
||||
float yMin1 = y1.min().getFloat();
|
||||
float yMax1 = y1.max().getFloat();
|
||||
float yMin2 = y2.min().getFloat();
|
||||
float yMax2 = y2.max().getFloat();
|
||||
|
||||
// 3. 计算重叠高度
|
||||
float overlapHeight = Math.min(yMax1, yMax2) - Math.max(yMin1, yMin2);
|
||||
|
||||
if (overlapHeight <= 0) return false;
|
||||
|
||||
// 4. 计算各自高度
|
||||
float h1 = yMax1 - yMin1;
|
||||
float h2 = yMax2 - yMin2;
|
||||
|
||||
// 5. 判定标准
|
||||
return overlapHeight > (Math.min(h1, h2) * 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:将 1D 的 8个元素 转换为 2D 的 (4, 2)
|
||||
*/
|
||||
private static NDArray formatBox(NDArray box) {
|
||||
if (box.getShape().dimension() == 1) {
|
||||
// 如果是 [x0, y0, x1, y1...] 这种 8 个元素的 1D 阵,转为 (4, 2)
|
||||
return box.reshape(new Shape(4, 2));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user