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