feat(assessment): 添加图片分析及生成摸底测试文档功能
- 新增AssessmentConstant常量接口,定义文件暂存目录及列数常量 - AssessmentController新增上传图片分析接口,支持将上传的PNG文件暂存并解析坐标 - 新增CoordinatesXY数据模型,封装坐标及宽高信息 - 引入OpenCV依赖,新增PngUtil工具类,实现黑色块检测并计算坐标列表 - PngUtil实现对未背熟单词的图片标记分析方法 - 优化AssessmentController使用新版Word模板文件assessment_v3.docx - 删除冗余旧的StudentServiceImpl代码,整合至student包内实现 - 迁移和完善StudentServiceImpl,实现学生分页查询及总数统计接口
This commit is contained in:
@@ -95,6 +95,11 @@
|
||||
<artifactId>poi-tl</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.openpnp</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.yinlihupo.enlish.service.constant;
|
||||
|
||||
public interface AssessmentConstant {
|
||||
|
||||
int PGN_COL = 53;
|
||||
// 文件暂存目录 linux
|
||||
String ASSESSMENT_FELT = "enlish/assessment/";
|
||||
}
|
||||
@@ -4,18 +4,20 @@ package com.yinlihupo.enlish.service.controller;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
import com.yinlihupo.enlish.service.constant.AssessmentConstant;
|
||||
import com.yinlihupo.enlish.service.enums.AssessmentsType;
|
||||
import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
|
||||
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||
import com.yinlihupo.enlish.service.model.vo.assessment.AssessmentStudentReqVO;
|
||||
import com.yinlihupo.enlish.service.service.AssessmentService;
|
||||
import com.yinlihupo.enlish.service.utils.PngUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -57,7 +59,7 @@ public class AssessmentController {
|
||||
data.put("answer", assessmentWords);
|
||||
|
||||
// 4. 渲染并输出
|
||||
try (XWPFTemplate template = XWPFTemplate.compile("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\assessment.docx", config)) {
|
||||
try (XWPFTemplate template = XWPFTemplate.compile("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\templates\\assessment_v3.docx", config)) {
|
||||
template.render(data);
|
||||
String fileName = URLEncoder.encode( assessmentDocxId + "摸底测试.docx", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
@@ -72,6 +74,18 @@ public class AssessmentController {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("analyze")
|
||||
public void generateAssessmentDocxAnalyze(@RequestParam("file") MultipartFile file, @RequestParam("id") Integer assessmentId) {
|
||||
// 把文件暂存在本地
|
||||
String filePath = AssessmentConstant.ASSESSMENT_FELT + assessmentId + ".png";
|
||||
try {
|
||||
file.transferTo(new File(filePath));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
List<CoordinatesXY> coordinatesXIES = PngUtil.analysisXY(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yinlihupo.enlish.service.model.bo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class CoordinatesXY {
|
||||
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
// x 从左往右, y 从上往下
|
||||
// (x, y) ←─── 左上角起点
|
||||
// ↓
|
||||
// +--------------------------------------+
|
||||
// | |
|
||||
// | 宽 (Width) | 从左到右
|
||||
// | |
|
||||
// +--------------------------------------+
|
||||
//
|
||||
// ↕
|
||||
// 高 (Height)
|
||||
// 从上到下
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.yinlihupo.enlish.service.service.assessment;
|
||||
|
||||
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
|
||||
import com.yinlihupo.enlish.service.domain.mapper.StudentDOMapper;
|
||||
import com.yinlihupo.enlish.service.service.StudentService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class StudentServiceImpl implements StudentService {
|
||||
|
||||
@Resource
|
||||
private StudentDOMapper studentDOMapper;
|
||||
|
||||
@Override
|
||||
public List<StudentDO> getStudentsByClassIdAndGradeId(Integer classId, Integer gradeId, Integer pageNo, Integer pageSize) {
|
||||
|
||||
return studentDOMapper.selectStudentDOListByClassIdAndGradeId(classId, gradeId, pageSize, (pageNo - 1) * pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAllStudents() {
|
||||
return studentDOMapper.selectStudentCount();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,28 @@
|
||||
package com.yinlihupo.enlish.service.service.student;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
|
||||
import com.yinlihupo.enlish.service.domain.mapper.StudentDOMapper;
|
||||
import com.yinlihupo.enlish.service.service.StudentService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class StudentServiceImpl {
|
||||
public class StudentServiceImpl implements StudentService {
|
||||
|
||||
@Resource
|
||||
private StudentDOMapper studentDOMapper;
|
||||
|
||||
@Override
|
||||
public List<StudentDO> getStudentsByClassIdAndGradeId(Integer classId, Integer gradeId, Integer pageNo, Integer pageSize) {
|
||||
|
||||
return studentDOMapper.selectStudentDOListByClassIdAndGradeId(classId, gradeId, pageSize, (pageNo - 1) * pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAllStudents() {
|
||||
return studentDOMapper.selectStudentCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.yinlihupo.enlish.service.utils;
|
||||
|
||||
import com.yinlihupo.enlish.service.constant.AssessmentConstant;
|
||||
import com.yinlihupo.enlish.service.model.bo.CoordinatesXY;
|
||||
import com.yinlihupo.enlish.service.model.bo.Word;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nu.pattern.OpenCV;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.opencv.core.*;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class PngUtil {
|
||||
|
||||
static {
|
||||
OpenCV.loadLocally();
|
||||
}
|
||||
|
||||
// 获取起始坐标
|
||||
public static List<CoordinatesXY> analysisXY(String 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, 50, 255, Imgproc.THRESH_BINARY_INV);
|
||||
|
||||
// 4. 查找轮廓
|
||||
List<MatOfPoint> contours = new ArrayList<>();
|
||||
Mat hierarchy = new Mat();
|
||||
// RETR_EXTERNAL 只检测最外层轮廓,忽略块内部可能存在的噪点
|
||||
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
System.out.println("检测到的轮廓总数: " + contours.size());
|
||||
System.out.println("------------------------------------------------");
|
||||
|
||||
// 5. 遍历并筛选轮廓
|
||||
int blockCount = 0;
|
||||
|
||||
// 设定最小面积阈值,过滤掉文字、表格细线等噪点
|
||||
// 根据图片分辨率,这两个块看起来比较大,我们可以设一个较大的值,例如 500 或 1000 像素
|
||||
double minArea = 1000.0;
|
||||
|
||||
List<CoordinatesXY> list = new ArrayList<>();
|
||||
for (int i = 0; i < contours.size(); i++) {
|
||||
MatOfPoint contour = contours.get(i);
|
||||
|
||||
// 计算轮廓面积
|
||||
double area = Imgproc.contourArea(contour);
|
||||
|
||||
// 获取边界矩形
|
||||
Rect rect = Imgproc.boundingRect(contour);
|
||||
// 筛选条件:
|
||||
// 1. 面积必须足够大 (排除文字)
|
||||
// 2. 宽高比不能太极端 (排除长长的表格横线或竖线)
|
||||
// 3. 宽度和高度必须有一定的尺寸
|
||||
if (area > minArea && rect.width > 20 && rect.height > 20) {
|
||||
|
||||
blockCount++;
|
||||
System.out.println("找到黑色块 #" + blockCount);
|
||||
System.out.println("坐标 (X, Y): (" + rect.x + ", " + rect.y + ")");
|
||||
System.out.println("尺寸 (宽 x 高): " + rect.width + " x " + rect.height);
|
||||
System.out.println("中心点: (" + (rect.x + rect.width / 2) + ", " + (rect.y + rect.height / 2) + ")");
|
||||
System.out.println("------------------------------------------------");
|
||||
list.add(CoordinatesXY.builder().x(rect.x).y(rect.y).width(rect.width).height(rect.height).build());
|
||||
// 可选:在原图上画出框,用于调试验证
|
||||
// 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);
|
||||
// Imgcodecs.imwrite("output_red.png", src);
|
||||
}
|
||||
}
|
||||
System.out.println("找到 " + blockCount + " 个黑色块。");
|
||||
|
||||
// 获取每一列的宽度
|
||||
list.sort(Comparator.comparingInt(CoordinatesXY::getHeight));
|
||||
int height = list.get(list.size() - 1).getHeight() / AssessmentConstant.PGN_COL;
|
||||
|
||||
// 删除两列答题卡区块
|
||||
list.sort(Comparator.comparingInt(CoordinatesXY::getWidth));
|
||||
list.remove(list.size() - 1);
|
||||
list.remove(list.size() - 1);
|
||||
list.sort(Comparator.comparingInt(CoordinatesXY::getX));
|
||||
|
||||
// 计算起始坐标
|
||||
List<CoordinatesXY> ans = getCoordinatesXIES(list, height);
|
||||
|
||||
src.release();
|
||||
binary.release();
|
||||
hierarchy.release();
|
||||
binary.release();
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
// 获取(未背熟)单词的 id
|
||||
public static List<Integer> analyzePngForUnmemorizedWordIds(String filePath, List<Word> words, List<CoordinatesXY> coordinatesXYList) {
|
||||
|
||||
Mat src = Imgcodecs.imread(filePath);
|
||||
if (src.empty()) {
|
||||
log.error("无法读取图片,请检查路径: {}", filePath);
|
||||
throw new RuntimeException("无法读取图片");
|
||||
}
|
||||
|
||||
Mat gray = new Mat();
|
||||
Mat binary = new Mat();
|
||||
|
||||
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<>();
|
||||
int words_index = 0;
|
||||
|
||||
for (int i = 0; i < coordinatesXYList.size(); i++) {
|
||||
CoordinatesXY coordinatesXY = coordinatesXYList.get(i);
|
||||
|
||||
int width = coordinatesXY.getWidth();
|
||||
int height = coordinatesXY.getHeight();
|
||||
int currentX = coordinatesXY.getX();
|
||||
int currentY = coordinatesXY.getY();
|
||||
|
||||
int count = i == 0 ? AssessmentConstant.PGN_COL - 1 : AssessmentConstant.PGN_COL;
|
||||
|
||||
// 内层循环:遍历这一列的每一行
|
||||
for (int j = 0; j < count; j++) {
|
||||
// 安全检查:防止单词列表比格子少导致越界
|
||||
if (words_index >= words.size()) {
|
||||
log.warn("单词列表耗尽,停止检测。格子数多于单词数。");
|
||||
break;
|
||||
}
|
||||
log.info("当前坐标 x = {} y = {}", currentX, currentY);
|
||||
// 安全检查:防止 Rect 超出图片边界
|
||||
if (currentX < 0 || currentY < 0 ||
|
||||
currentX + width > binary.cols() || currentY + height > binary.rows()) {
|
||||
log.warn("检测区域越界,跳过: x={}, y={}", currentX, currentY);
|
||||
words_index++;
|
||||
currentY += height; // 依然要移动坐标
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect rect = new Rect(currentX + 1, currentY + 1, width - 2, height - 2);
|
||||
Mat region = binary.submat(rect);
|
||||
int countNonZero = Core.countNonZero(region);
|
||||
|
||||
if (countNonZero > 800) {
|
||||
Word currentWord = words.get(words_index);
|
||||
answer.add(currentWord.getId());
|
||||
log.info("检测到标记(未背熟):ID={}, 词={}", currentWord.getId(), currentWord.getTitle());
|
||||
}
|
||||
|
||||
region.release();
|
||||
words_index++;
|
||||
currentY += height;
|
||||
}
|
||||
}
|
||||
|
||||
return answer;
|
||||
|
||||
} finally {
|
||||
|
||||
src.release();
|
||||
gray.release();
|
||||
binary.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<CoordinatesXY> getCoordinatesXIES(List<CoordinatesXY> list, int height) {
|
||||
List<CoordinatesXY> ans = new ArrayList<>();
|
||||
CoordinatesXY left = new CoordinatesXY();
|
||||
left.setX(list.get(1).getX());
|
||||
left.setWidth(list.get(1).getWidth());
|
||||
left.setHeight(height);
|
||||
left.setY(list.get(0).getY() + left.getHeight());
|
||||
ans.add(left);
|
||||
|
||||
CoordinatesXY right = new CoordinatesXY();
|
||||
right.setX(list.get(2).getX());
|
||||
right.setY(list.get(0).getY());
|
||||
right.setWidth(list.get(1).getWidth());
|
||||
right.setHeight(height);
|
||||
ans.add(right);
|
||||
return ans;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/assessment_v2.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v2.docx
Normal file
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/assessment_v3.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v3.docx
Normal file
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/assessment_v4.docx
Normal file
BIN
enlish-service/src/main/resources/templates/assessment_v4.docx
Normal file
Binary file not shown.
BIN
enlish-service/src/main/resources/templates/p3.png
Normal file
BIN
enlish-service/src/main/resources/templates/p3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
Reference in New Issue
Block a user