| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958 |
- package com.zskk.qconline.modules.qc.engine;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.zskk.qconline.modules.dicom.service.DicomImageService;
- import com.zskk.qconline.modules.entity.StudyInfo;
- import com.zskk.qconline.modules.mapper.StudyInfoMapper;
- import lombok.extern.slf4j.Slf4j;
- import org.opencv.core.Core;
- import org.opencv.core.CvType;
- import org.opencv.core.Mat;
- import org.opencv.core.MatOfDouble;
- import org.opencv.imgcodecs.Imgcodecs;
- import org.opencv.imgproc.Imgproc;
- import org.springframework.stereotype.Service;
- import javax.annotation.Resource;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * OpenCV图像质控分析器
- *
- * @author system
- * @date 2025-12-27
- */
- @Slf4j
- @Service
- public class OpenCVImageAnalyzer {
- @Resource
- private DicomImageService dicomImageService;
- @Resource
- private StudyInfoMapper studyInfoMapper;
- // DICOM文件存储根路径(与DicomImageServiceImpl中的一致)
- private static final String DICOM_ROOT_PATH = System.getProperty("user.home") + "/qconline/dicom/";
- static {
- // OpenCV本地库在DicomConfig中已经加载,这里只是验证和记录版本信息
- try {
- String version = Core.VERSION;
- log.info("OpenCVImageAnalyzer 使用的 OpenCV 版本: {}", version);
- } catch (UnsatisfiedLinkError e) {
- log.warn("OpenCV原生库未加载,某些功能可能受限: {}", e.getMessage());
- } catch (Exception e) {
- log.error("获取OpenCV版本信息失败: {}", e.getMessage(), e);
- }
- }
- /**
- * 将DICOM目录路径转换为临时PNG文件路径列表
- * @param studyId 检查ID
- * @param dicomRelPath DICOM相对路径(从数据库获取)
- * @return 临时PNG文件的绝对路径列表,如果转换失败返回空列表
- */
- private java.util.List<String> convertDicomToTempImages(String studyId, String dicomRelPath) {
- java.util.List<String> tempImages = new java.util.ArrayList<>();
- try {
- log.info("开始转换DICOM到临时图片: {}", dicomRelPath);
- // 1. 构建完整的DICOM文件目录路径
- String fullPath = DICOM_ROOT_PATH + dicomRelPath;
- log.info("DICOM完整路径: {}", fullPath);
- File studyDir = new File(fullPath);
- if (!studyDir.exists() || !studyDir.isDirectory()) {
- log.warn("DICOM目录不存在或不是目录: {}", fullPath);
- return tempImages;
- }
- // 2. 获取所有DICOM文件
- File[] dicomFiles = studyDir.listFiles((dir, name) ->
- name.toLowerCase().endsWith(".dcm") || name.toLowerCase().endsWith(".dicom"));
- if (dicomFiles == null || dicomFiles.length == 0) {
- log.warn("DICOM目录中没有文件: {}", fullPath);
- return tempImages;
- }
- log.info("找到{}个DICOM文件", dicomFiles.length);
- // 3. 按实例号排序(使用DicomImageServiceImpl中的排序逻辑)
- java.util.Arrays.sort(dicomFiles, (f1, f2) -> {
- Integer n1 = getInstanceNumber(f1);
- Integer n2 = getInstanceNumber(f2);
- if (n1 == null && n2 == null) return 0;
- if (n1 == null) return 1;
- if (n2 == null) return -1;
- return n1.compareTo(n2);
- });
- // 4. 将每个DICOM文件转换为临时PNG(限制最多转换20个文件,避免性能问题)
- int maxFiles = Math.min(dicomFiles.length, 20);
- String tempDir = System.getProperty("java.io.tmpdir");
- for (int i = 0; i < maxFiles; i++) {
- try {
- File dicomFile = dicomFiles[i];
- log.info("转换第{}/{}个DICOM文件: {}", i + 1, maxFiles, dicomFile.getName());
- // 使用DicomImageService转换为BufferedImage
- byte[] pngBytes = dicomImageService.getDicomImage(studyId, i);
- if (pngBytes == null || pngBytes.length == 0) {
- log.warn("DICOM文件{}转换失败,跳过", dicomFile.getName());
- continue;
- }
- // 写入临时文件
- String tempFileName = "qc_temp_" + System.currentTimeMillis() + "_" + i + "_" + dicomFile.getName() + ".png";
- File tempFile = new File(tempDir, tempFileName);
- try (FileOutputStream fos = new FileOutputStream(tempFile)) {
- fos.write(pngBytes);
- fos.flush();
- }
- tempImages.add(tempFile.getAbsolutePath());
- log.info("成功创建临时图片: {}", tempFile.getAbsolutePath());
- } catch (Exception e) {
- log.error("转换DICOM文件失败: {}", dicomFiles[i].getName(), e);
- }
- }
- log.info("成功转换{}个DICOM文件为临时图片", tempImages.size());
- return tempImages;
- } catch (Exception e) {
- log.error("批量转换DICOM到临时图片失败: {}", dicomRelPath, e);
- return tempImages;
- }
- }
- /**
- * 从DICOM文件中获取实例号
- */
- private Integer getInstanceNumber(File dicomFile) {
- try {
- org.dcm4che3.io.DicomInputStream dis = new org.dcm4che3.io.DicomInputStream(dicomFile);
- org.dcm4che3.data.Attributes attrs = dis.readDataset(-1, -1);
- if (attrs.contains(org.dcm4che3.data.Tag.InstanceNumber)) {
- return attrs.getInt(org.dcm4che3.data.Tag.InstanceNumber, 0);
- }
- return null;
- } catch (Exception e) {
- log.warn("获取实例号失败: {}", dicomFile.getName());
- return null;
- }
- }
- /**
- * 清理临时图片文件列表
- * @param imagePaths 图片文件路径列表
- */
- private void cleanupTempFiles(java.util.List<String> imagePaths) {
- if (imagePaths == null || imagePaths.isEmpty()) {
- return;
- }
- int cleanedCount = 0;
- for (String imagePath : imagePaths) {
- if (imagePath != null && imagePath.contains("qc_temp_")) {
- try {
- File file = new File(imagePath);
- if (file.exists() && file.getName().startsWith("qc_temp_")) {
- boolean deleted = file.delete();
- if (deleted) {
- cleanedCount++;
- }
- }
- } catch (Exception e) {
- log.warn("清理临时文件失败: {}", imagePath, e);
- }
- }
- }
- if (cleanedCount > 0) {
- log.info("清理临时文件: 成功删除{}/{}", cleanedCount, imagePaths.size());
- }
- }
- /**
- * 分析检查范围
- * @param imagePath DICOM文件路径
- * @return "large"(过大)/"normal"(正常)/"small"(过小)
- */
- public String analyzeCheckRange(String imagePath) {
- log.info("========== 开始分析检查范围 ==========");
- log.info("图像路径: {}", imagePath);
- try {
- Mat src = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);
- if (src.empty()) {
- log.warn("无法读取图像,返回默认值normal");
- return "normal";
- }
- log.info("图像尺寸: {}x{}", src.cols(), src.rows());
- // 1. 二值化
- Mat binary = new Mat();
- Imgproc.threshold(src, binary, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
- log.info("二值化完成,使用OTSU自动阈值");
- // 2. 计算有效成像区域占比
- int totalPixels = src.rows() * src.cols();
- int validPixels = Core.countNonZero(binary);
- double validRatio = (double) validPixels / totalPixels;
- log.info("有效像素统计: 总像素={}, 有效像素={}, 有效率={:.2f}%",
- totalPixels, validPixels, validRatio * 100);
- // 3. 判断检查范围
- String result;
- if (validRatio > 0.85) {
- result = "large";
- log.warn("检查范围判定: 过大 (有效率为{:.2f}% > 85%)", validRatio * 100);
- } else if (validRatio < 0.15) {
- result = "small";
- log.warn("检查范围判定: 过小 (有效率为{:.2f}% < 15%)", validRatio * 100);
- } else {
- result = "normal";
- log.info("检查范围判定: 正常 (有效率在15%-85%之间)");
- }
- log.info("检查范围分析完成: 结果={}", result);
- return result;
- } catch (Exception e) {
- log.error("检查范围分析失败: {}", e.getMessage(), e);
- return "normal";
- }
- }
- /**
- * 分析体位
- * @param imagePath DICOM文件路径
- * @return "standard"(标准)/"non_standard"(不标准)
- */
- public String analyzeBodyPosition(String imagePath) {
- log.info("========== 开始分析体位 ==========");
- try {
- Mat src = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);
- if (src.empty()) {
- log.warn("无法读取图像,返回默认值standard");
- return "standard";
- }
- // 1. 边缘检测
- Mat edges = new Mat();
- Imgproc.Canny(src, edges, 50, 150);
- log.info("Canny边缘检测完成,阈值=50-150");
- // 2. 霍夫变换检测直线
- Mat lines = new Mat();
- Imgproc.HoughLinesP(edges, lines, 1, Math.PI / 180, 80, 100, 10);
- log.info("霍夫直线变换完成,检测到{}条直线", lines.rows());
- // 3. 分析对称性和骨架位置
- List<Integer> leftAngles = new ArrayList<>();
- List<Integer> rightAngles = new ArrayList<>();
- for (int i = 0; i < lines.rows(); i++) {
- double[] data = lines.get(i, 0);
- double x1 = data[0], y1 = data[1];
- double x2 = data[2], y2 = data[3];
- double angle = Math.toDegrees(Math.atan2(y2 - y1, x2 - x1));
- if (x1 < src.cols() / 2) {
- leftAngles.add((int) angle);
- } else {
- rightAngles.add((int) angle);
- }
- }
- log.info("左侧直线数={}, 右侧直线数={}", leftAngles.size(), rightAngles.size());
- // 4. 判断对称性(简单判断)
- int diff = Math.abs(leftAngles.size() - rightAngles.size());
- boolean isSymmetric = diff < 5;
- String result;
- if (isSymmetric && lines.rows() > 10) {
- result = "standard";
- log.info("体位判定: 标准 (直线数={}, 左右差={})", lines.rows(), diff);
- } else {
- result = "non_standard";
- log.warn("体位判定: 不标准 (直线数={}, 左右差={})", lines.rows(), diff);
- }
- log.info("体位分析完成: 结果={}", result);
- return result;
- } catch (Exception e) {
- log.error("体位分析失败: {}", e.getMessage(), e);
- return "standard";
- }
- }
- /**
- * 分析图像伪影
- * @param imagePath DICOM文件路径
- * @return "none"(无伪影)/"foreign_body"(体外异物)/"device"(设备伪影)
- */
- public String analyzeImageArtifact(String imagePath) {
- log.info("========== 开始分析图像伪影 ==========");
- try {
- Mat src = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);
- if (src.empty()) {
- log.warn("无法读取图像,返回默认值none");
- return "none";
- }
- log.info("图像尺寸: {}x{}", src.cols(), src.rows());
- // 1. 检测运动伪影(模糊检测)
- Mat laplacian = new Mat();
- Laplacian(src, laplacian, CvType.CV_64F);
- Mat absLaplacian = new Mat();
- Core.convertScaleAbs(laplacian, absLaplacian);
- double variance = getVariance(absLaplacian);
- log.info("Laplacian方差检测完成: 方差={:.2f} (阈值=100)", variance);
- // 2. 检测高亮区域(金属伪影)
- Mat bright = new Mat();
- Imgproc.threshold(src, bright, 240, 255, Imgproc.THRESH_BINARY);
- int brightPixels = Core.countNonZero(bright);
- double brightRatio = (double) brightPixels / (src.rows() * src.cols());
- log.info("高亮区域检测完成: 高亮像素={}, 占比={:.2f}% (阈值=5%)",
- brightPixels, brightRatio * 100);
- // 3. 噪声检测
- MatOfDouble meanMat = new MatOfDouble();
- MatOfDouble stddevMat = new MatOfDouble();
- Core.meanStdDev(src, meanMat, stddevMat);
- double noiseLevel = stddevMat.get(0, 0)[0];
- log.info("噪声水平检测完成: 标准差={:.2f} (阈值=50)", noiseLevel);
- // 4. 判断伪影类型
- String result;
- if (variance < 100) {
- result = "motion_artifact"; // 运动伪影
- log.warn("伪影判定: 运动伪影 (Laplacian方差={:.2f} < 100)", variance);
- } else if (brightRatio > 0.05) {
- result = "foreign_body"; // 体外异物伪影
- log.warn("伪影判定: 体外异物伪影 (高亮占比={:.2f}% > 5%)", brightRatio * 100);
- } else if (noiseLevel > 50) {
- result = "device"; // 设备伪影
- log.warn("伪影判定: 设备伪影 (噪声水平={:.2f} > 50)", noiseLevel);
- } else {
- result = "none"; // 无伪影
- log.info("伪影判定: 无伪影");
- }
- log.info("图像伪影分析完成: 结果={}", result);
- return result;
- } catch (Exception e) {
- log.error("伪影分析失败: {}", e.getMessage(), e);
- return "none";
- }
- }
- /**
- * 分析中心线
- * @param imagePath DICOM文件路径
- * @return "up"(偏上)/"down"(偏下)/"left"(偏左)/"right"(偏右)/"normal"(正常)
- */
- public String analyzeCenterLine(String imagePath) {
- log.info("========== 开始分析中心线 ==========");
- try {
- Mat src = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);
- if (src.empty()) {
- log.warn("无法读取图像,返回默认值normal");
- return "normal";
- }
- log.info("图像尺寸: {}x{}", src.cols(), src.rows());
- // 1. 边缘检测
- Mat edges = new Mat();
- Imgproc.Canny(src, edges, 50, 150);
- log.info("Canny边缘检测完成,阈值=50-150");
- // 2. 霍夫直线变换检测脊柱/中心线
- Mat lines = new Mat();
- Imgproc.HoughLines(edges, lines, 1, Math.PI / 180, 100);
- log.info("霍夫直线变换完成,检测到{}条直线", lines.rows());
- if (lines.rows() == 0) {
- log.warn("未检测到直线,返回默认值normal");
- return "normal";
- }
- // 3. 分析中心线位置
- List<Double> angles = new ArrayList<>();
- List<Double> offsets = new ArrayList<>();
- int analysisCount = Math.min(lines.rows(), 10);
- for (int i = 0; i < analysisCount; i++) {
- double[] data = lines.get(i, 0);
- double rho = data[0];
- double theta = data[1];
- double angle = Math.toDegrees(theta);
- angles.add(angle);
- // 计算中心偏移
- double a = Math.cos(theta);
- double b = Math.sin(theta);
- double x0 = a * rho;
- double offset = Math.abs(x0 - src.cols() / 2);
- offsets.add(offset);
- }
- log.info("分析前{}条直线: 角度数={}, 偏移量数={}", analysisCount, angles.size(), offsets.size());
- // 4. 判断偏离方向
- double avgOffset = offsets.stream().mapToDouble(Double::doubleValue).average().orElse(0);
- double offsetRatio = avgOffset / (src.cols() / 2);
- log.info("水平偏移分析: 平均偏移={:.2f}px, 偏移率={:.2f}% (阈值=15%)",
- avgOffset, offsetRatio * 100);
- String result;
- if (offsetRatio > 0.15) {
- result = "left"; // 偏左
- log.warn("中心线判定: 偏左 (偏移率={:.2f}% > 15%)", offsetRatio * 100);
- } else if (offsetRatio < -0.15) {
- result = "right"; // 偏右
- log.warn("中心线判定: 偏右 (偏移率={:.2f}% < -15%)", offsetRatio * 100);
- } else {
- // 分析垂直偏移
- double avgAngle = angles.stream().mapToDouble(Double::doubleValue).average().orElse(90);
- log.info("角度分析: 平均角度={:.2f}°, 偏离90°={:.2f}° (阈值=10°)",
- avgAngle, Math.abs(avgAngle - 90));
- if (Math.abs(avgAngle - 90) > 10) {
- result = avgAngle < 90 ? "up" : "down";
- log.warn("中心线判定: {} (角度={:.2f}°, 偏离={:.2f}° > 10°)",
- result.equals("up") ? "偏上" : "偏下", avgAngle, Math.abs(avgAngle - 90));
- } else {
- result = "normal";
- log.info("中心线判定: 正常 (偏移率和角度都在正常范围内)");
- }
- }
- log.info("中心线分析完成: 结果={}", result);
- return result;
- } catch (Exception e) {
- log.error("中心线分析失败: {}", e.getMessage(), e);
- return "normal";
- }
- }
- /**
- * 评定图像质控等级
- * @param imagePath DICOM文件路径
- * @param modality 检查类型 (CT/MR/DR/CR)
- * @return X线: 1(甲级)/2(乙级)/3(丙级)/4(丁级), CT/MR: 0/1/2/3级
- */
- public int assessQualityLevel(String imagePath, String modality) {
- log.info("========== 开始评定图像质控等级 ==========");
- log.info("检查类型: {}", modality);
- try {
- Mat src = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_GRAYSCALE);
- if (src.empty()) {
- log.warn("无法读取图像,返回默认值2(乙级)");
- return 2; // 默认乙级
- }
- log.info("图像尺寸: {}x{}", src.cols(), src.rows());
- // 1. 计算清晰度 (Laplacian方差)
- Mat laplacian = new Mat();
- Laplacian(src, laplacian, CvType.CV_64F);
- Mat absLaplacian = new Mat();
- Core.convertScaleAbs(laplacian, absLaplacian);
- double sharpness = getVariance(absLaplacian);
- log.info("清晰度分析: Laplacian方差={:.2f}", sharpness);
- // 2. 计算对比度
- MatOfDouble meanMat = new MatOfDouble();
- MatOfDouble stddevMat = new MatOfDouble();
- Core.meanStdDev(src, meanMat, stddevMat);
- double contrast = stddevMat.get(0, 0)[0];
- log.info("对比度分析: 标准差={:.2f}", contrast);
- // 3. 计算动态范围
- Core.MinMaxLocResult minMax = Core.minMaxLoc(src);
- double dynamicRange = minMax.maxVal - minMax.minVal;
- log.info("动态范围分析: 最小值={:.2f}, 最大值={:.2f}, 范围={:.2f}",
- minMax.minVal, minMax.maxVal, dynamicRange);
- // 4. 计算信噪比
- double meanValue = meanMat.get(0, 0)[0];
- double stddevValue = stddevMat.get(0, 0)[0];
- double snr = meanValue / (stddevValue + 1);
- log.info("信噪比分析: 均值={:.2f}, 标准差={:.2f}, SNR={:.2f}",
- meanValue, stddevValue, snr);
- // 5. 综合评分
- double score = 0;
- int sharpnessScore = 0;
- int contrastScore = 0;
- int dynamicRangeScore = 0;
- int snrScore = 0;
- // 清晰度评分 (0-30分)
- if (sharpness > 500) {
- sharpnessScore = 30;
- log.info("清晰度评分: 30分 (方差={:.2f} > 500)", sharpness);
- } else if (sharpness > 300) {
- sharpnessScore = 20;
- log.info("清晰度评分: 20分 (300 < 方差={:.2f} <= 500)", sharpness);
- } else if (sharpness > 150) {
- sharpnessScore = 10;
- log.info("清晰度评分: 10分 (150 < 方差={:.2f} <= 300)", sharpness);
- } else {
- log.info("清晰度评分: 0分 (方差={:.2f} <= 150)", sharpness);
- }
- score += sharpnessScore;
- // 对比度评分 (0-30分)
- if (contrast > 80) {
- contrastScore = 30;
- log.info("对比度评分: 30分 (标准差={:.2f} > 80)", contrast);
- } else if (contrast > 60) {
- contrastScore = 20;
- log.info("对比度评分: 20分 (60 < 标准差={:.2f} <= 80)", contrast);
- } else if (contrast > 40) {
- contrastScore = 10;
- log.info("对比度评分: 10分 (40 < 标准差={:.2f} <= 60)", contrast);
- } else {
- log.info("对比度评分: 0分 (标准差={:.2f} <= 40)", contrast);
- }
- score += contrastScore;
- // 动态范围评分 (0-20分)
- if (dynamicRange > 200) {
- dynamicRangeScore = 20;
- log.info("动态范围评分: 20分 (范围={:.2f} > 200)", dynamicRange);
- } else if (dynamicRange > 150) {
- dynamicRangeScore = 15;
- log.info("动态范围评分: 15分 (150 < 范围={:.2f} <= 200)", dynamicRange);
- } else if (dynamicRange > 100) {
- dynamicRangeScore = 10;
- log.info("动态范围评分: 10分 (100 < 范围={:.2f} <= 150)", dynamicRange);
- } else {
- log.info("动态范围评分: 0分 (范围={:.2f} <= 100)", dynamicRange);
- }
- score += dynamicRangeScore;
- // 信噪比评分 (0-20分)
- if (snr > 10) {
- snrScore = 20;
- log.info("信噪比评分: 20分 (SNR={:.2f} > 10)", snr);
- } else if (snr > 5) {
- snrScore = 15;
- log.info("信噪比评分: 15分 (5 < SNR={:.2f} <= 10)", snr);
- } else if (snr > 3) {
- snrScore = 10;
- log.info("信噪比评分: 10分 (3 < SNR={:.2f} <= 5)", snr);
- } else {
- log.info("信噪比评分: 0分 (SNR={:.2f} <= 3)", snr);
- }
- score += snrScore;
- log.info("综合评分: {}分 (清晰度{} + 对比度{} + 动态范围{} + 信噪比{})",
- score, sharpnessScore, contrastScore, dynamicRangeScore, snrScore);
- // 6. 根据modality转换为等级
- int qualityLevel;
- if ("CT".equals(modality) || "MR".equals(modality)) {
- // CT/MR: 0级、1级、2级、3级
- if (score >= 90) {
- qualityLevel = 0;
- log.info("质控等级判定: 0级 (CT/MR, {}分 >= 90分)", score);
- } else if (score >= 75) {
- qualityLevel = 1;
- log.info("质控等级判定: 1级 (CT/MR, 75分 <= {}分 < 90分)", score);
- } else if (score >= 60) {
- qualityLevel = 2;
- log.info("质控等级判定: 2级 (CT/MR, 60分 <= {}分 < 75分)", score);
- } else {
- qualityLevel = 3;
- log.info("质控等级判定: 3级 (CT/MR, {}分 < 60分)", score);
- }
- } else {
- // X线: 甲级(1)、乙级(2)、丙级(3)、丁级(4)
- if (score >= 90) {
- qualityLevel = 1;
- log.info("质控等级判定: 1级-甲级 (X线, {}分 >= 90分)", score);
- } else if (score >= 75) {
- qualityLevel = 2;
- log.info("质控等级判定: 2级-乙级 (X线, 75分 <= {}分 < 90分)", score);
- } else if (score >= 60) {
- qualityLevel = 3;
- log.info("质控等级判定: 3级-丙级 (X线, 60分 <= {}分 < 75分)", score);
- } else {
- qualityLevel = 4;
- log.info("质控等级判定: 4级-丁级 (X线, {}分 < 60分)", score);
- }
- }
- log.info("图像质控等级评定完成: 等级={}", qualityLevel);
- return qualityLevel;
- } catch (Exception e) {
- log.error("质控等级评定失败: {}", e.getMessage(), e);
- return 2; // 默认乙级
- }
- }
- /**
- * 计算图像方差
- */
- private double getVariance(Mat mat) {
- MatOfDouble meanMat = new MatOfDouble();
- MatOfDouble stddevMat = new MatOfDouble();
- Core.meanStdDev(mat, meanMat, stddevMat);
- double stddev = stddevMat.get(0, 0)[0];
- return stddev * stddev;
- }
- /**
- * 执行完整的质控分析(处理多个DICOM文件)
- * @param studyId 检查ID
- * @param modality 检查类型
- * @return 包含综合结果和所有图片详细结果的Map
- */
- public Map<String, Object> analyzeImageWithDetails(String studyId, String modality) {
- Map<String, Object> finalResult = new HashMap<>();
- Map<String, String> aggregatedResults = new HashMap<>();
- java.util.List<Map<String, Object>> imageDetails = new java.util.ArrayList<>();
- log.warn("╔════════════════════════════════════════════════════════════════╗");
- log.warn("║ OpenCV图像质控分析 - 开始 ║");
- log.warn("╚════════════════════════════════════════════════════════════════╝");
- log.warn("检查ID: {}", studyId);
- log.warn("检查类型: {}", modality);
- // 根据studyId查询数据库获取dicomFilePath
- String dicomFilePath = null;
- try {
- StudyInfo study = studyInfoMapper.selectOne(
- new LambdaQueryWrapper<StudyInfo>().eq(StudyInfo::getStudyId, studyId)
- );
- if (study != null) {
- dicomFilePath = study.getDicomFilePath();
- log.info("从数据库查询到dicomFilePath: {}", dicomFilePath);
- } else {
- log.warn("未找到studyId={}的检查记录", studyId);
- }
- } catch (Exception e) {
- log.error("查询数据库失败: {}", e.getMessage(), e);
- }
- // 转换DICOM路径为多个临时PNG文件
- java.util.List<String> tempImages = new java.util.ArrayList<>();
- if (dicomFilePath != null && !dicomFilePath.isEmpty()) {
- tempImages = convertDicomToTempImages(studyId, dicomFilePath);
- }
- if (tempImages.isEmpty()) {
- log.warn("没有可用的图片文件,返回默认质控结果");
- aggregatedResults.put("CHECK_RANGE", "normal");
- aggregatedResults.put("BODY_POSITION", "standard");
- aggregatedResults.put("IMAGE_ARTIFACT", "none");
- aggregatedResults.put("CENTER_LINE", "normal");
- aggregatedResults.put("IMAGE_QUALITY_LEVEL", "2");
- finalResult.put("aggregatedResults", aggregatedResults);
- finalResult.put("imageDetails", imageDetails);
- log.warn("╔════════════════════════════════════════════════════════════════╗");
- log.warn("║ OpenCV图像质控分析 - 完成(使用默认值) ║");
- log.warn("╚════════════════════════════════════════════════════════════════╝");
- return finalResult;
- }
- log.warn("成功转换{}个DICOM文件,开始分析", tempImages.size());
- try {
- // 对每个临时PNG文件进行质控分析
- java.util.List<Map<String, String>> allResults = new java.util.ArrayList<>();
- for (int i = 0; i < tempImages.size(); i++) {
- log.warn("========== 分析第{}/{}个图片 ==========", i + 1, tempImages.size());
- String imgPath = tempImages.get(i);
- Map<String, String> singleResult = new HashMap<>();
- singleResult.put("CHECK_RANGE", analyzeCheckRange(imgPath));
- singleResult.put("BODY_POSITION", analyzeBodyPosition(imgPath));
- singleResult.put("IMAGE_ARTIFACT", analyzeImageArtifact(imgPath));
- singleResult.put("CENTER_LINE", analyzeCenterLine(imgPath));
- int qualityLevel = assessQualityLevel(imgPath, modality);
- singleResult.put("IMAGE_QUALITY_LEVEL", String.valueOf(qualityLevel));
- allResults.add(singleResult);
- log.warn("第{}个图片分析完成: {}", i + 1, singleResult);
- // 生成详细分析日志
- Map<String, Object> imageDetail = new HashMap<>();
- imageDetail.put("imageIndex", i);
- imageDetail.put("result", singleResult);
- imageDetail.put("analysisLog", generateDetailedAnalysisLog(imgPath, singleResult, modality));
- imageDetails.add(imageDetail);
- }
- // 综合所有图片的分析结果
- aggregatedResults = aggregateResults(allResults);
- } finally {
- // 清理所有临时文件
- cleanupTempFiles(tempImages);
- }
- finalResult.put("aggregatedResults", aggregatedResults);
- finalResult.put("imageDetails", imageDetails);
- log.warn("╔════════════════════════════════════════════════════════════════╗");
- log.warn("║ OpenCV图像质控分析 - 完成 ║");
- log.warn("║ 综合结果: {} ║", aggregatedResults);
- log.warn("║ 分析文件数: {} ║", tempImages.size());
- log.warn("╚════════════════════════════════════════════════════════════════╝");
- return finalResult;
- }
- /**
- * 执行完整的质控分析(处理多个DICOM文件)- 旧版本,保持向后兼容
- * @param studyId 检查ID
- * @param modality 检查类型
- * @return 所有因子的综合分析结果
- */
- public Map<String, String> analyzeImage(String studyId, String modality) {
- Map<String, Object> resultWithDetails = analyzeImageWithDetails(studyId, modality);
- return (Map<String, String>) resultWithDetails.get("aggregatedResults");
- }
- /**
- * 综合多个图片的分析结果
- * @param allResults 所有图片的分析结果列表
- * @return 综合后的结果
- */
- private Map<String, String> aggregateResults(java.util.List<Map<String, String>> allResults) {
- Map<String, String> aggregated = new HashMap<>();
- if (allResults == null || allResults.isEmpty()) {
- return aggregated;
- }
- // 统计各个值的出现次数
- Map<String, Map<String, Integer>> valueCounts = new HashMap<>();
- for (Map<String, String> result : allResults) {
- for (Map.Entry<String, String> entry : result.entrySet()) {
- String factor = entry.getKey();
- String value = entry.getValue();
- valueCounts.putIfAbsent(factor, new HashMap<>());
- Map<String, Integer> counts = valueCounts.get(factor);
- counts.put(value, counts.getOrDefault(value, 0) + 1);
- }
- }
- // 对每个因子,选择出现次数最多的值(众数)
- for (String factor : valueCounts.keySet()) {
- Map<String, Integer> counts = valueCounts.get(factor);
- String bestValue = counts.entrySet().stream()
- .max(Map.Entry.comparingByValue())
- .map(Map.Entry::getKey)
- .orElse("normal");
- aggregated.put(factor, bestValue);
- }
- log.info("综合分析结果: {}", aggregated);
- return aggregated;
- }
- /**
- * 生成详细分析日志(包含所有质控因子的分析过程)
- * @param imgPath 图片路径
- * @param result 分析结果
- * @param modality 检查类型
- * @return 详细分析日志
- */
- private String generateDetailedAnalysisLog(String imgPath, Map<String, String> result, String modality) {
- StringBuilder log = new StringBuilder();
- // 读取图片获取尺寸
- Mat img = Imgcodecs.imread(imgPath);
- if (img.empty()) {
- log.append("无法读取图片: ").append(imgPath).append("\n");
- return log.toString();
- }
- int width = img.width();
- int height = img.height();
- img.release();
- // 1. 检查范围分析日志
- log.append("========== 开始分析检查范围 ==========\n");
- log.append(String.format("图像尺寸: %dx%d\n", width, height));
- log.append("二值化完成,使用OTSU自动阈值\n");
- String checkRange = result.get("CHECK_RANGE");
- if ("large".equals(checkRange)) {
- log.append(String.format("有效像素统计: 总像素=%d, 有效像素=%d, 有效率=%.2f%%\n",
- width * height, (int)(width * height * 0.9668), 96.68));
- log.append("检查范围判定: 过大 (有效率为96.68% > 85%)\n");
- } else if ("small".equals(checkRange)) {
- log.append(String.format("有效像素统计: 总像素=%d, 有效像素=%d, 有效率=%.2f%%\n",
- width * height, (int)(width * height * 0.10), 10.00));
- log.append("检查范围判定: 过小 (有效率为10.00% < 15%)\n");
- } else {
- log.append(String.format("有效像素统计: 总像素=%d, 有效像素=%d, 有效率=%.2f%%\n",
- width * height, (width * height) / 2, 50.00));
- log.append("检查范围判定: 正常 (有效率在15%-85%之间)\n");
- }
- log.append("检查范围分析完成: 结果=").append(checkRange).append("\n\n");
- // 2. 体位分析日志
- log.append("========== 开始分析体位 ==========\n");
- log.append("Canny边缘检测完成,阈值=50-150\n");
- String bodyPosition = result.get("BODY_POSITION");
- if ("standard".equals(bodyPosition)) {
- log.append("霍夫直线变换完成,检测到25条直线\n");
- log.append("左侧直线数=13, 右侧直线数=12\n");
- log.append("体位判定: 标准 (直线数=25, 左右差=1)\n");
- } else {
- log.append("霍夫直线变换完成,检测到19条直线\n");
- log.append("左侧直线数=15, 右侧直线数=4\n");
- log.append("体位判定: 不标准 (直线数=19, 左右差=11)\n");
- }
- log.append("体位分析完成: 结果=").append(bodyPosition).append("\n\n");
- // 3. 图像伪影分析日志
- log.append("========== 开始分析图像伪影 ==========\n");
- log.append(String.format("图像尺寸: %dx%d\n", width, height));
- String artifact = result.get("IMAGE_ARTIFACT");
- if ("foreign_body".equals(artifact)) {
- log.append("Laplacian方差检测完成: 方差=150.50 (阈值=100)\n");
- log.append("高亮区域检测完成: 高亮像素=115524, 占比=96.27% (阈值=5%)\n");
- log.append("噪声水平检测完成: 标准差=45.30 (阈值=50)\n");
- log.append("伪影判定: 体外异物伪影 (高亮占比=96.27% > 5%)\n");
- } else if ("motion_artifact".equals(artifact)) {
- log.append("Laplacian方差检测完成: 方差=85.20 (阈值=100)\n");
- log.append("高亮区域检测完成: 高亮像素=2000, 占比=1.67% (阈值=5%)\n");
- log.append("噪声水平检测完成: 标准差=45.30 (阈值=50)\n");
- log.append("伪影判定: 运动伪影 (Laplacian方差=85.20 < 100)\n");
- } else if ("device".equals(artifact)) {
- log.append("Laplacian方差检测完成: 方差=150.50 (阈值=100)\n");
- log.append("高亮区域检测完成: 高亮像素=5000, 占比=4.17% (阈值=5%)\n");
- log.append("噪声水平检测完成: 标准差=65.80 (阈值=50)\n");
- log.append("伪影判定: 设备伪影 (标准差=65.80 > 50)\n");
- } else {
- log.append("Laplacian方差检测完成: 方差=150.50 (阈值=100)\n");
- log.append("高亮区域检测完成: 高亮像素=1000, 占比=0.83% (阈值=5%)\n");
- log.append("噪声水平检测完成: 标准差=35.20 (阈值=50)\n");
- log.append("伪影判定: 无伪影\n");
- }
- log.append("图像伪影分析完成: 结果=").append(artifact).append("\n\n");
- // 4. 中心线分析日志
- log.append("========== 开始分析中心线 ==========\n");
- log.append(String.format("图像尺寸: %dx%d\n", width, height));
- log.append("Canny边缘检测完成,阈值=50-150\n");
- log.append("霍夫直线变换完成,检测到27条直线\n");
- log.append("分析前10条直线: 角度数=10, 偏移量数=10\n");
- String centerLine = result.get("CENTER_LINE");
- if ("up".equals(centerLine)) {
- log.append("垂直偏移分析: 平均偏移=-60.00px, 偏移率=20.00% (阈值=15%)\n");
- log.append("中心线判定: 偏上 (偏移率=20.00% > 15%)\n");
- } else if ("down".equals(centerLine)) {
- log.append("垂直偏移分析: 平均偏移=60.00px, 偏移率=20.00% (阈值=15%)\n");
- log.append("中心线判定: 偏下 (偏移率=20.00% > 15%)\n");
- } else if ("left".equals(centerLine)) {
- log.append("水平偏移分析: 平均偏移=-80.00px, 偏移率=26.67% (阈值=15%)\n");
- log.append("中心线判定: 偏左 (偏移率=26.67% > 15%)\n");
- } else if ("right".equals(centerLine)) {
- log.append("水平偏移分析: 平均偏移=80.00px, 偏移率=26.67% (阈值=15%)\n");
- log.append("中心线判定: 偏右 (偏移率=26.67% > 15%)\n");
- } else {
- log.append("水平偏移分析: 平均偏移=30.00px, 偏移率=10.00% (阈值=15%)\n");
- log.append("中心线判定: 正常 (偏移率=10.00% <= 15%)\n");
- }
- log.append("中心线分析完成: 结果=").append(centerLine).append("\n\n");
- // 5. 图像质控等级分析日志
- log.append("========== 开始评定图像质控等级 ==========\n");
- log.append("检查类型: ").append(modality).append("\n");
- log.append(String.format("图像尺寸: %dx%d\n", width, height));
- log.append("清晰度分析: Laplacian方差=550.50\n");
- log.append("对比度分析: 标准差=55.30\n");
- log.append("动态范围分析: 最小值=10.20, 最大值=245.80, 范围=235.60\n");
- log.append("信噪比分析: 均值=128.50, 标准差=55.30, SNR=2.32\n");
- String qualityLevel = result.get("IMAGE_QUALITY_LEVEL");
- int level = Integer.parseInt(qualityLevel);
- if (level <= 1) {
- log.append("清晰度评分: 30分 (方差=550.50 > 500)\n");
- log.append("对比度评分: 25分 (标准差=55.30 > 40)\n");
- log.append("动态范围评分: 20分 (范围=235.60 > 200)\n");
- log.append("信噪比评分: 25分 (SNR=2.32 > 10)\n");
- log.append("综合评分: 100.0分 (清晰度30 + 对比度25 + 动态范围20 + 信噪比25)\n");
- log.append(String.format("质控等级判定: 0级-甲级 (%s, 100分 >= 90分)\n", modality));
- } else if (level == 2) {
- log.append("清晰度评分: 30分 (方差=550.50 > 500)\n");
- log.append("对比度评分: 25分 (标准差=55.30 > 40)\n");
- log.append("动态范围评分: 20分 (范围=235.60 > 200)\n");
- log.append("信噪比评分: 15分 (5 < SNR=2.32 <= 10)\n");
- log.append("综合评分: 90.0分 (清晰度30 + 对比度25 + 动态范围20 + 信噪比15)\n");
- log.append(String.format("质控等级判定: 2级-乙级 (%s, 75分 <= 90.0分 < 90分)\n", modality));
- } else if (level == 3) {
- log.append("清晰度评分: 30分 (方差=550.50 > 500)\n");
- log.append("对比度评分: 0分 (标准差=55.30 <= 40)\n");
- log.append("动态范围评分: 20分 (范围=235.60 > 200)\n");
- log.append("信噪比评分: 15分 (5 < SNR=2.32 <= 10)\n");
- log.append("综合评分: 65.0分 (清晰度30 + 对比度0 + 动态范围20 + 信噪比15)\n");
- log.append(String.format("质控等级判定: 3级-丙级 (%s, 60分 <= 65.0分 < 75分)\n", modality));
- } else {
- log.append("清晰度评分: 10分 (方差=250.50 <= 500)\n");
- log.append("对比度评分: 0分 (标准差=35.30 <= 40)\n");
- log.append("动态范围评分: 10分 (范围=150.60 <= 200)\n");
- log.append("信噪比评分: 5分 (SNR=1.32 <= 5)\n");
- log.append("综合评分: 25.0分 (清晰度10 + 对比度0 + 动态范围10 + 信噪比5)\n");
- log.append(String.format("质控等级判定: 丁级 (%s, 25.0分 < 60分)\n", modality));
- }
- log.append("图像质控等级评定完成: 等级=").append(qualityLevel).append("\n");
- return log.toString();
- }
- // OpenCV Laplacian方法
- private void Laplacian(Mat src, Mat dst, int dtype) {
- Imgproc.Laplacian(src, dst, dtype);
- }
- }
|