diff --git a/enlish-service/pom.xml b/enlish-service/pom.xml
index 92963e1..58e9094 100644
--- a/enlish-service/pom.xml
+++ b/enlish-service/pom.xml
@@ -95,6 +95,11 @@
poi-tl
+
+ org.openpnp
+ opencv
+
+
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/AssessmentConstant.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/AssessmentConstant.java
new file mode 100644
index 0000000..a0ef877
--- /dev/null
+++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/constant/AssessmentConstant.java
@@ -0,0 +1,8 @@
+package com.yinlihupo.enlish.service.constant;
+
+public interface AssessmentConstant {
+
+ int PGN_COL = 53;
+ // 文件暂存目录 linux
+ String ASSESSMENT_FELT = "enlish/assessment/";
+}
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/AssessmentController.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/AssessmentController.java
index 5248e61..e769b4a 100644
--- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/AssessmentController.java
+++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/controller/AssessmentController.java
@@ -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 coordinatesXIES = PngUtil.analysisXY(filePath);
}
}
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/CoordinatesXY.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/CoordinatesXY.java
new file mode 100644
index 0000000..5573571
--- /dev/null
+++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/model/bo/CoordinatesXY.java
@@ -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)
+// 从上到下
+}
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/assessment/StudentServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/assessment/StudentServiceImpl.java
deleted file mode 100644
index f57460d..0000000
--- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/assessment/StudentServiceImpl.java
+++ /dev/null
@@ -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 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();
- }
-}
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java
index b05318e..6ec2b64 100644
--- a/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java
+++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/service/student/StudentServiceImpl.java
@@ -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 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();
+ }
}
diff --git a/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/PngUtil.java b/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/PngUtil.java
new file mode 100644
index 0000000..56d844d
--- /dev/null
+++ b/enlish-service/src/main/java/com/yinlihupo/enlish/service/utils/PngUtil.java
@@ -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 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 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 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 ans = getCoordinatesXIES(list, height);
+
+ src.release();
+ binary.release();
+ hierarchy.release();
+ binary.release();
+
+ return ans;
+ }
+
+ // 获取(未背熟)单词的 id
+ public static List analyzePngForUnmemorizedWordIds(String filePath, List words, List 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 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 getCoordinatesXIES(List list, int height) {
+ List 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;
+ }
+}
diff --git a/enlish-service/src/main/resources/templates/assessment.docx b/enlish-service/src/main/resources/templates/assessment.docx
index 0458696..7fcc9ec 100644
Binary files a/enlish-service/src/main/resources/templates/assessment.docx and b/enlish-service/src/main/resources/templates/assessment.docx differ
diff --git a/enlish-service/src/main/resources/templates/assessment_v2.docx b/enlish-service/src/main/resources/templates/assessment_v2.docx
new file mode 100644
index 0000000..39b418e
Binary files /dev/null and b/enlish-service/src/main/resources/templates/assessment_v2.docx differ
diff --git a/enlish-service/src/main/resources/templates/assessment_v3.docx b/enlish-service/src/main/resources/templates/assessment_v3.docx
new file mode 100644
index 0000000..08d5864
Binary files /dev/null and b/enlish-service/src/main/resources/templates/assessment_v3.docx differ
diff --git a/enlish-service/src/main/resources/templates/assessment_v4.docx b/enlish-service/src/main/resources/templates/assessment_v4.docx
new file mode 100644
index 0000000..c965bac
Binary files /dev/null and b/enlish-service/src/main/resources/templates/assessment_v4.docx differ
diff --git a/enlish-service/src/main/resources/templates/p3.png b/enlish-service/src/main/resources/templates/p3.png
new file mode 100644
index 0000000..a30248d
Binary files /dev/null and b/enlish-service/src/main/resources/templates/p3.png differ
diff --git a/pom.xml b/pom.xml
index c768f7e..0318a8f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,6 +109,14 @@
1.12.1
+
+
+ org.openpnp
+ opencv
+ 4.7.0-0
+
+
+
org.projectlombok