Compare commits
1 Commits
fb29acc145
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf381b8f1 |
@@ -62,7 +62,7 @@ public class ExamWordsController {
|
|||||||
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
// bug: 获取单词后,单词的id会乱序、 需要重新更新考试记录中的 id
|
||||||
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
examWordsDO.setWordIds(assessmentWords.stream().map(Word::getId).toList());
|
||||||
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
examWordsService.updateExamWordsWordIdsOrder(examWordsDO);
|
||||||
|
log.info("生成试卷成功 {}", examWordsDO);
|
||||||
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
List<StudentDetail> studentDetailList = studentService.getStudentDetailList(Collections.singletonList(studentId));
|
||||||
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
List<Map<String, Object>> maps = studentDetailList.stream().map(studentDetail -> {
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -118,7 +119,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId) + "上");
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_MIDTERM);
|
||||||
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
examWordsDO.setTitle("期中测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
|
|
||||||
@@ -128,13 +129,13 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
StudentDO studentDO = studentDOMapper.selectStudentById(studentId);
|
||||||
Integer gradeId = studentDO.getGradeId();
|
Integer gradeId = studentDO.getGradeId();
|
||||||
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
List<UnitDO> unitDOS = unitDOMapper.selectByUnitName(ExamWordsConstant.getGradeName(gradeId));
|
||||||
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS);
|
ExamWordsDO examWordsDO = getExamWordsDO(studentId, studentDO, gradeId, unitDOS, ExamWordsConstant.EXAM_TYPE_FINAL);
|
||||||
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
examWordsDO.setTitle("期末测试" + studentDO.getName());
|
||||||
return getExamWordsDO(studentId, examWordsDO);
|
return getExamWordsDO(studentId, examWordsDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS) {
|
private ExamWordsDO getExamWordsDO(Integer studentId, StudentDO studentDO, Integer gradeId, List<UnitDO> unitDOS, Integer type) {
|
||||||
if (unitDOS.isEmpty()) {
|
if (unitDOS.isEmpty()) {
|
||||||
throw new RuntimeException("没有找到对应的单元");
|
throw new RuntimeException("没有找到对应的单元");
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
ExamWordsDO examWordsDO = ExamWordsDO.builder()
|
||||||
.gradeId(gradeId)
|
.gradeId(gradeId)
|
||||||
.level(1)
|
.level(1)
|
||||||
.type(ExamWordsConstant.EXAM_TYPE_BASELINE)
|
.type(type)
|
||||||
.title(studentDO.getName())
|
.title(studentDO.getName())
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
.wordIds(vocabularyBankDOS.stream().map(VocabularyBankDO::getId).toList())
|
||||||
@@ -170,30 +171,55 @@ public class ExamWordsServiceImpl implements ExamWordsService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = RuntimeException.class)
|
@Transactional(rollbackFor = RuntimeException.class)
|
||||||
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
public int saveExamWordsPngToDbAndLocal(MultipartFile file) {
|
||||||
|
// 1. 基础校验:判空
|
||||||
File dir = new File(tmpPng);
|
if (file == null || file.isEmpty()) {
|
||||||
if (!dir.exists()) {
|
throw new RuntimeException("上传文件不能为空");
|
||||||
dir.mkdirs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 安全校验:检查后缀名白名单
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String extension = StringUtils.getFilenameExtension(originalFilename); // Spring工具类
|
||||||
|
List<String> allowedExtensions = Arrays.asList("png", "jpg", "jpeg");
|
||||||
|
|
||||||
|
if (extension == null || !allowedExtensions.contains(extension.toLowerCase())) {
|
||||||
|
throw new RuntimeException("不支持的文件格式,仅支持: " + allowedExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 准备目录 (使用 NIO)
|
||||||
|
// 假设 tmpPng 是配置好的基础路径字符串
|
||||||
|
Path directoryPath = Paths.get(tmpPng);
|
||||||
try {
|
try {
|
||||||
String originalFilename = file.getOriginalFilename();
|
if (!Files.exists(directoryPath)) {
|
||||||
String suffix = "";
|
Files.createDirectories(directoryPath);
|
||||||
if (originalFilename != null && originalFilename.contains(".")) {
|
|
||||||
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
||||||
}
|
}
|
||||||
String newFileName = UUID.randomUUID() + suffix;
|
|
||||||
String path = tmpPng + newFileName;
|
|
||||||
|
|
||||||
File dest = new File(path);
|
// 4. 生成文件名 (防止文件名冲突)
|
||||||
file.transferTo(dest);
|
String newFileName = UUID.randomUUID().toString() + "." + extension;
|
||||||
int insert = examWordsJudgeResultDOMapper.insert(path);
|
|
||||||
log.info("上传文件成功");
|
// 5. 组合最终路径 (自动处理分隔符)
|
||||||
|
Path targetPath = directoryPath.resolve(newFileName);
|
||||||
|
|
||||||
|
// 6. 保存文件
|
||||||
|
file.transferTo(targetPath.toAbsolutePath().toFile());
|
||||||
|
String string = targetPath.toAbsolutePath().toFile().toString();
|
||||||
|
log.info("文件上传成功路径为 {}", string);
|
||||||
|
if (!targetPath.toFile().exists()) {
|
||||||
|
log.error("文件上传失败: {}", newFileName);
|
||||||
|
throw new RuntimeException("文件上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 入库
|
||||||
|
// 建议:存相对路径或文件名,不要存 targetPath.toString() 的绝对路径
|
||||||
|
// 这里为了演示,假设 insert 依然接受字符串,建议存 newFileName
|
||||||
|
int insert = examWordsJudgeResultDOMapper.insert(targetPath.toString());
|
||||||
|
|
||||||
|
log.info("上传文件成功: {}", newFileName);
|
||||||
return insert;
|
return insert;
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("上传失败", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("文件上传失败: {}", originalFilename, e);
|
||||||
|
throw new RuntimeException("上传失败,请稍后重试", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
public void judgeExamWords(int count) {
|
public void judgeExamWords(int count) {
|
||||||
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectUnfinishedExamWordsJudgeResultDOList(count);
|
||||||
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
||||||
|
String ansSheetPath = null;
|
||||||
try {
|
try {
|
||||||
String ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
ansSheetPath = examWordsJudgeResultDO.getAnsSheetPath();
|
||||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(ansSheetPath);
|
||||||
|
|
||||||
// 从图片中获取学生 id 和考试 id
|
// 从图片中获取学生 id 和考试 id
|
||||||
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
StudentExamId studentExamId = PngUtil.analyzeExamWordsIdAndStudentId(ansSheetPath, tessdataPath, coordinatesXIES);
|
||||||
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
Integer examWordsJudgeResultDOId = examWordsJudgeResultDO.getId();
|
||||||
@@ -79,7 +81,7 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
ExamWordsDO examWordsDO = examWordsDOMapper.selectById(examWordsId);
|
||||||
if(examWordsDO == null) {
|
if (examWordsDO == null) {
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDOId, "未找到考试");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -146,9 +148,14 @@ public class ExamWordsJudgeServiceImpl implements ExamWordsJudgeService {
|
|||||||
boolean delete = new File(ansSheetPath).delete();
|
boolean delete = new File(ansSheetPath).delete();
|
||||||
if (delete) {
|
if (delete) {
|
||||||
log.info("删除文件成功:{}", ansSheetPath);
|
log.info("删除文件成功:{}", ansSheetPath);
|
||||||
|
} else {
|
||||||
|
log.error("删除文件失败:{}", ansSheetPath);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("识别考试失败 {}", e.getMessage());
|
log.error("识别考试失败 {}", e.getMessage());
|
||||||
|
if (ansSheetPath != null) {
|
||||||
|
new File(ansSheetPath).delete();
|
||||||
|
}
|
||||||
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
examWordsJudgeResultDOMapper.updateMsg(examWordsJudgeResultDO.getId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.opencv.imgproc.Imgproc;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.DataBufferByte;
|
import java.awt.image.DataBufferByte;
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -31,31 +32,13 @@ public class PngUtil {
|
|||||||
// 获取起始坐标
|
// 获取起始坐标
|
||||||
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
public static List<CoordinatesXY> analysisXY(String imagePath) {
|
||||||
|
|
||||||
|
Mat binary = image2BinaryMath(imagePath);
|
||||||
Mat src = Imgcodecs.imread(imagePath);
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
if (src.empty()) {
|
|
||||||
System.out.println("无法读取图片,请检查路径。");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 预处理
|
|
||||||
// 3.1 转换为灰度图
|
|
||||||
Mat gray = new Mat();
|
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
|
|
||||||
// 3.2 二值化处理 (Thresholding)
|
|
||||||
// 使用 THRESH_BINARY_INV (反转二值化),因为我们需要找的是白色背景上的黑色块。
|
|
||||||
// 反转后,黑色块变成白色(255),背景变成黑色(0),方便 findContours 查找。
|
|
||||||
Mat binary = new Mat();
|
|
||||||
// 阈值设为 50 左右即可,因为块是纯黑的
|
|
||||||
Imgproc.threshold(gray, binary, 80, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
|
|
||||||
// 4. 查找轮廓
|
// 4. 查找轮廓
|
||||||
List<MatOfPoint> contours = new ArrayList<>();
|
List<MatOfPoint> contours = new ArrayList<>();
|
||||||
Mat hierarchy = new Mat();
|
Mat hierarchy = new Mat();
|
||||||
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
|
|
||||||
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||||
//Imgcodecs.imwrite("output_red___v1.png", binary);
|
|
||||||
System.out.println("检测到的轮廓总数: " + contours.size());
|
System.out.println("检测到的轮廓总数: " + contours.size());
|
||||||
System.out.println("------------------------------------------------");
|
System.out.println("------------------------------------------------");
|
||||||
|
|
||||||
@@ -91,36 +74,31 @@ public class PngUtil {
|
|||||||
// 可选:在原图上画出框,用于调试验证
|
// 可选:在原图上画出框,用于调试验证
|
||||||
Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
Imgproc.rectangle(src, rect, new Scalar(0, 0, 255), 2); // 红色框
|
||||||
Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
Imgproc.putText(src, "#" + blockCount, new Point(rect.x, rect.y - 5), Imgproc.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 0, 255), 1);
|
||||||
//Imgcodecs.imwrite("output_red.png", src);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Imgcodecs.imwrite("output_red.png", src);
|
||||||
System.out.println("找到 " + blockCount + " 个黑色块。");
|
System.out.println("找到 " + blockCount + " 个黑色块。");
|
||||||
|
|
||||||
|
|
||||||
// 计算起始坐标
|
// 计算起始坐标
|
||||||
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
||||||
|
|
||||||
|
list.forEach(coordinatesXY -> coordinatesXY.setHeight(coordinatesXY.getHeight() / 51));
|
||||||
|
list.forEach(coordinatesXY -> coordinatesXY.setWidth(coordinatesXY.getWidth() / 3));
|
||||||
|
list.forEach(coordinatesXY -> coordinatesXY.setX(coordinatesXY.getX() + coordinatesXY.getWidth() * 2));
|
||||||
|
|
||||||
|
log.info("起始坐标: {}", list);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取(未背熟)单词的 id
|
// 获取(未背熟)单词的 id
|
||||||
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Integer> wordIds, List<CoordinatesXY> coordinatesXYList) {
|
||||||
|
|
||||||
Mat src = Imgcodecs.imread(filePath);
|
|
||||||
if (src.empty()) {
|
|
||||||
log.error("无法读取图片,请检查路径: {}", filePath);
|
|
||||||
throw new RuntimeException("无法读取图片");
|
|
||||||
}
|
|
||||||
|
|
||||||
Mat gray = new Mat();
|
Mat binary = image2BinaryMath(filePath);
|
||||||
Mat binary = new Mat();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
|
||||||
// 建议:如果光照不均匀,考虑使用 THRESH_OTSU 自动阈值,或者自适应阈值
|
|
||||||
Imgproc.threshold(gray, binary, 150, 255, Imgproc.THRESH_BINARY_INV);
|
|
||||||
// 调试时打印
|
|
||||||
//Imgcodecs.imwrite("output_binary.png", binary);
|
|
||||||
List<Integer> answer = new ArrayList<>();
|
List<Integer> answer = new ArrayList<>();
|
||||||
int words_index = 0;
|
int words_index = 0;
|
||||||
|
|
||||||
@@ -150,8 +128,8 @@ public class PngUtil {
|
|||||||
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
||||||
Mat region = binary.submat(rect);
|
Mat region = binary.submat(rect);
|
||||||
int countNonZero = Core.countNonZero(region);
|
int countNonZero = Core.countNonZero(region);
|
||||||
|
log.info("当前位置为 words_index={},坐标为 x={} y={} 当前区域非零像素数: {}", words_index, currentX, currentY, countNonZero);
|
||||||
if (countNonZero > 370) {
|
if (countNonZero > 1000) {
|
||||||
Integer id = wordIds.get(words_index);
|
Integer id = wordIds.get(words_index);
|
||||||
answer.add(id);
|
answer.add(id);
|
||||||
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
log.info("检测到标记(未背熟):ID={}, 当前坐标 x = {} y = {} ", id, currentX + 1, currentY + 1);
|
||||||
@@ -170,8 +148,6 @@ public class PngUtil {
|
|||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
src.release();
|
|
||||||
gray.release();
|
|
||||||
binary.release();
|
binary.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,21 +167,21 @@ public class PngUtil {
|
|||||||
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
Rect roiRect = new Rect(0, 0, left.getX(), left.getY());
|
||||||
Mat roi = new Mat(src, roiRect);
|
Mat roi = new Mat(src, roiRect);
|
||||||
|
|
||||||
// 3. 图像预处理 (提高 OCR 准确率)
|
// // 3. 图像预处理 (提高 OCR 准确率)
|
||||||
// 3.1 转为灰度图
|
// // 3.1 转为灰度图
|
||||||
Mat gray = new Mat();
|
// Mat gray = new Mat();
|
||||||
Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
// Imgproc.cvtColor(roi, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
//
|
||||||
// 3.2 二值化 (Thresholding)
|
// // 3.2 二值化 (Thresholding)
|
||||||
// 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
// // 使用 OTSU 算法自动寻找最佳阈值,或者手动指定阈值
|
||||||
Mat binary = new Mat();
|
// Mat binary = new Mat();
|
||||||
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
// Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
|
||||||
|
|
||||||
// 可选:保存预处理后的图片查看效果
|
// 可选:保存预处理后的图片查看效果
|
||||||
//Imgcodecs.imwrite("debug_roi.jpg", binary);
|
Imgcodecs.imwrite("debug_roi.jpg", src);
|
||||||
|
|
||||||
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
// 4. 将 OpenCV Mat 转换为 BufferedImage (供 Tess4J 使用)
|
||||||
BufferedImage processedImage = matToBufferedImage(binary);
|
BufferedImage processedImage = matToBufferedImage(src);
|
||||||
|
|
||||||
// 5. 使用 Tesseract 进行 OCR 识别
|
// 5. 使用 Tesseract 进行 OCR 识别
|
||||||
ITesseract instance = new Tesseract();
|
ITesseract instance = new Tesseract();
|
||||||
@@ -230,6 +206,50 @@ public class PngUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Mat image2BinaryMath(String imagePath) {
|
||||||
|
|
||||||
|
if (!new File(imagePath).exists()) {
|
||||||
|
log.error("图片不存在,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("图片不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat src = Imgcodecs.imread(imagePath);
|
||||||
|
|
||||||
|
if (src.empty()) {
|
||||||
|
log.info("无法读取图片,请检查路径: {}", imagePath);
|
||||||
|
throw new RuntimeException("无法读取图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
Mat gray = new Mat();
|
||||||
|
//转换为灰度图
|
||||||
|
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
|
||||||
|
Imgproc.GaussianBlur(gray, gray, new Size(5, 5), 0);
|
||||||
|
|
||||||
|
Mat binary = new Mat();
|
||||||
|
Imgproc.adaptiveThreshold(gray, binary, 255,
|
||||||
|
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
Imgproc.THRESH_BINARY_INV, 25, 10);
|
||||||
|
|
||||||
|
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
|
||||||
|
|
||||||
|
//开运算 (Open):先腐蚀后膨胀,用于去除背景中的微小噪点
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_OPEN, kernel);
|
||||||
|
|
||||||
|
// 闭运算 (Close):先膨胀后腐蚀,用于连接断裂的区域并填充块内部的空洞
|
||||||
|
// 如果块比较大且内部反光严重,可以将 Size(3,3) 改为 Size(5,5) 或更大
|
||||||
|
Mat closeKernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(7, 7));
|
||||||
|
Imgproc.morphologyEx(binary, binary, Imgproc.MORPH_CLOSE, closeKernel);
|
||||||
|
|
||||||
|
// 保存二值化过程图用于调试 (生产环境可注释)
|
||||||
|
Imgcodecs.imwrite("debug_binary_natural.png", binary);
|
||||||
|
|
||||||
|
src.release();
|
||||||
|
gray.release();
|
||||||
|
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
private static @NonNull StudentExamId getStudentExamId(Pattern pattern, String result) {
|
||||||
Matcher matcher = pattern.matcher(result);
|
Matcher matcher = pattern.matcher(result);
|
||||||
StudentExamId studentExamId = new StudentExamId(0, 0);
|
StudentExamId studentExamId = new StudentExamId(0, 0);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ templates:
|
|||||||
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
|
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v3.docx
|
||||||
plan_day: 7
|
plan_day: 7
|
||||||
tmp:
|
tmp:
|
||||||
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
|
png:
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ spring:
|
|||||||
|
|
||||||
|
|
||||||
templates:
|
templates:
|
||||||
word: assessment_v7.docx
|
word: assessment_v9.docx
|
||||||
count: 100
|
count: 100
|
||||||
data: eng.traineddata
|
data:
|
||||||
plan:
|
plan:
|
||||||
weekday: tem_study_plan_v6.docx
|
weekday: tem_study_plan_v7.docx
|
||||||
weekend: study_plan_review_v2.docx
|
weekend: study_plan_review_v3.docx
|
||||||
plan_day: 7
|
plan_day: 7
|
||||||
tmp:
|
tmp:
|
||||||
png: tmp\png\
|
png:
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: dev # 默认激活 dev 本地开发环境
|
active: pro # 默认激活 dev 本地开发环境
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 30MB
|
||||||
|
max-request-size: 30MB
|
||||||
mybatis:
|
mybatis:
|
||||||
# MyBatis xml 配置文件路径
|
# MyBatis xml 配置文件路径
|
||||||
mapper-locations: classpath:/mapper/**/*.xml
|
mapper-locations: classpath:/mapper/**/*.xml
|
||||||
|
|||||||
Reference in New Issue
Block a user