医学影像质控系统技术方案.md 41 KB

医学影像质控系统技术方案

基于 Spring Boot + OpenCV 的实现方案

版本: v1.0 日期: 2026-01-05 作者: 技术团队


目录

  1. 项目背景
  2. 核心问题分析
  3. 系统架构设计
  4. 因子转换机制
  5. 技术实现方案
  6. 数据库设计
  7. 部署方案
  8. 扩展性设计
  9. 附录

1. 项目背景

1.1 业务需求

医学影像质控系统需要对 DR、CT、MRI 等医学影像进行自动化质量检测,确保影像符合诊断标准。质控因子包括:

  • 基础质控: 清晰度、对比度、亮度
  • 整体标准质控: 体位、影像密度、标准完整性、技术规范
  • 检查部位质控: 检查范围、中心线、摄像角度、图像标识、左右标识、图像伪影、图像清晰度
  • 图像质控等级: X线、CT、MRI 等不同设备类型

1.2 关键挑战

核心问题: 如何将业务层的质控因子(如"清晰度"、"伪影")转换为 OpenCV 可执行的图像处理算法?

技术挑战:

  1. 质控因子可以任意组合
  2. 需要支持"否决项"机制
  3. 不同影像类型有不同的质控标准
  4. 需要动态配置和扩展

2. 核心问题分析

2.1 质控因子表分析

根据提供的质控因子表,系统包含以下核心因子:

标准名称 因子分类 因子名称 适用设备 评价词汇 评分权重 状态 否决项
标准技术质控 基础质控 图像有效性 DR, CT, MRI 影像检查图像的检查范围 20 启用 较差、较差
标准技术质控 基础质控 摆实人体位置 DR, CT, MRI 影像检查图片中的体位位置是否标准 30 启用 非人体
标准技术质控 基础质控 医拍完整图像 DR, CT, MRI 影像检查是否存在切角及部分切分类 20 启用 非医拍组图像
... ... ... ... ... ... ... ...

2.2 转换需求

业务因子技术参数OpenCV 算法

示例:

  • "清晰度" → {method: "laplacian", threshold: 100} → Laplacian 方差计算
  • "伪影检测" → {canny_threshold: [50, 150]} → Canny 边缘检测
  • "亮度" → {range: [50, 200]} → 灰度均值计算

3. 系统架构设计

3.1 整体架构

┌─────────────────────────────────────────────────────────────┐
│                      前端展示层 (Optional)                     │
│                    质控报告可视化界面                           │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    Spring Boot 应用层                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  REST API Controller                                 │   │
│  │  - 单张影像质控: POST /api/quality-control/check      │   │
│  │  - 批量质控: POST /api/quality-control/batch-check   │   │
│  │  - 规则管理: GET/POST /api/rules                     │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    质控规则引擎层                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  QualityRuleEngine                                   │   │
│  │  - 规则加载与解析                                      │   │
│  │  - 检测器调度                                          │   │
│  │  - 否决项判定                                          │   │
│  │  - 结果评估与评分                                      │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    OpenCV 检测器层                            │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │ 清晰度检测器  │ │ 对比度检测器  │ │ 亮度检测器    │        │
│  │ Laplacian   │ │ Histogram    │ │ Mean/Median  │        │
│  └──────────────┘ └──────────────┘ └──────────────┘        │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
│  │ 伪影检测器    │ │ 噪声检测器    │ │ 标注检测器    │        │
│  │ Canny+Morph │ │ SNR Calc     │ │ OCR/Template │        │
│  └──────────────┘ └──────────────┘ └──────────────┘        │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    数据持久化层                                │
│  - 规则配置数据库 (MySQL/PostgreSQL)                          │
│  - 质控报告存储                                               │
│  - 影像文件管理                                               │
└─────────────────────────────────────────────────────────────┘

3.2 核心组件

3.2.1 规则引擎 (QualityRuleEngine)

  • 负责规则加载、解析和执行调度
  • 支持规则的动态组合
  • 实现否决项机制

3.2.2 检测器接口 (QualityDetector)

  • 定义统一的检测接口
  • 每个质控因子对应一个检测器实现
  • 支持参数化配置

3.2.3 规则配置服务 (QualityRuleService)

  • 从数据库/配置文件加载规则
  • 支持规则的增删改查
  • 提供规则组合功能

4. 因子转换机制

4.1 转换层次

业务层 (质控因子)
    ↓
转换层 (参数化配置)
    ↓
技术层 (OpenCV 算法)

4.2 转换映射表

质控因子 OpenCV 检测方法 参数配置示例 判定条件
清晰度 Laplacian 方差 {"threshold": 100.0, "operator": ">"} 方差 > 100
对比度 直方图标准差 {"min": 30.0} 标准差 ≥ 30
亮度 灰度均值 {"range": [50, 200]} 均值 ∈ [50, 200]
伪影 Canny 边缘检测 {"canny_threshold": [50, 150], "max_edge_ratio": 0.3} 边缘比例 ≤ 0.3
噪声 信噪比 (SNR) {"snr_min": 20} SNR ≥ 20
标注完整性 OCR 文本识别 {"required_fields": ["姓名", "日期"]} 包含必需字段
位置信息 模板匹配 {"template_path": "/path/to/template", "threshold": 0.8} 匹配度 ≥ 0.8

4.3 转换实现

4.3.1 策略模式设计

// 检测器接口
public interface QualityDetector {
    DetectionResult detect(Mat image, Map<String, Object> parameters);
    QualityFactor getSupportedFactor();
}

// 具体检测器示例
@Component
public class SharpnessDetector implements QualityDetector {
    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        // 1. 提取参数
        double threshold = (double) parameters.getOrDefault("threshold", 100.0);
        String operator = (String) parameters.getOrDefault("operator", ">");

        // 2. 执行 OpenCV 算法
        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        Mat laplacian = new Mat();
        Imgproc.Laplacian(gray, laplacian, CvType.CV_64F);

        MatOfDouble std = new MatOfDouble();
        Core.meanStdDev(laplacian, new MatOfDouble(), std);
        double variance = Math.pow(std.get(0, 0)[0], 2);

        // 3. 判定结果
        boolean passed = evaluateCondition(variance, operator, threshold);

        // 4. 返回结果
        return DetectionResult.builder()
            .factor(QualityFactor.SHARPNESS)
            .passed(passed)
            .measuredValue(variance)
            .score(calculateScore(variance, threshold))
            .message(String.format("清晰度方差: %.2f", variance))
            .build();
    }
}

4.3.2 参数化配置

# application.yml
quality-control:
  rules:
    chest-xray:
      - factor: SHARPNESS
        veto: true
        parameters:
          threshold: 100.0
          operator: ">"

      - factor: CONTRAST
        veto: false
        parameters:
          min: 30.0

      - factor: BRIGHTNESS
        veto: false
        parameters:
          range: [50, 200]

5. 技术实现方案

5.1 技术栈

  • 后端框架: Spring Boot 2.7+ / 3.x
  • 图像处理: OpenCV 4.7.0
  • OCR (可选): Tesseract 5.x
  • 数据库: MySQL 8.0 / PostgreSQL 14+
  • 缓存: Redis (可选)
  • API 文档: Swagger/OpenAPI 3.0

5.2 Maven 依赖

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- OpenCV -->
    <dependency>
        <groupId>org.openpnp</groupId>
        <artifactId>opencv</artifactId>
        <version>4.7.0-0</version>
    </dependency>

    <!-- Tesseract OCR -->
    <dependency>
        <groupId>net.sourceforge.tess4j</groupId>
        <artifactId>tess4j</artifactId>
        <version>5.7.0</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- MySQL Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

5.3 核心代码实现

5.3.1 实体类定义

// QualityFactor.java - 质控因子枚举
public enum QualityFactor {
    SHARPNESS("清晰度", "laplacian_variance"),
    CONTRAST("对比度", "histogram_contrast"),
    BRIGHTNESS("亮度", "mean_brightness"),
    ANNOTATION("标注完整性", "ocr_detection"),
    ARTIFACT("伪影", "artifact_detection"),
    NOISE("噪声", "snr_calculation"),
    POSITION_INFO("位置信息", "text_detection"),
    BODY_POSITION("摆实人体位置", "body_position_detection"),
    IMAGE_COMPLETENESS("图像完整性", "completeness_check");

    private final String displayName;
    private final String detectorType;

    QualityFactor(String displayName, String detectorType) {
        this.displayName = displayName;
        this.detectorType = detectorType;
    }

    // Getters...
}

// QualityRule.java - 质控规则
@Data
@Builder
@Entity
@Table(name = "quality_rules")
public class QualityRule {
    @Id
    private String ruleId;

    private String ruleName;

    @Enumerated(EnumType.STRING)
    private QualityFactor factor;

    private boolean isVeto;  // 是否为否决项

    private Integer weight;  // 评分权重

    @Column(columnDefinition = "json")
    private String parametersJson;  // JSON 格式参数

    @Transient
    private Map<String, Object> parameters;

    private String operator;  // >, <, ==, range, contains

    private String threshold;  // 阈值(可以是数值、范围等)

    private String status;  // 启用/禁用

    @Column(name = "device_type")
    private String deviceType;  // DR, CT, MRI
}

// DetectionResult.java - 检测结果
@Data
@Builder
public class DetectionResult {
    private QualityFactor factor;
    private boolean passed;
    private double score;
    private Object measuredValue;
    private String message;
    private Map<String, Object> details;
}

// QualityReport.java - 质控报告
@Data
@Entity
@Table(name = "quality_reports")
public class QualityReport {
    @Id
    private String reportId;

    private String imagePath;

    private String ruleSetId;

    private LocalDateTime checkTime;

    private boolean passed;

    private String vetoReason;  // 否决原因

    @Column(columnDefinition = "json")
    private String resultsJson;

    @Transient
    private List<DetectionResult> results;

    private Double overallScore;

    public void calculateOverallScore() {
        if (results == null || results.isEmpty()) {
            this.overallScore = 0.0;
            return;
        }

        double totalScore = results.stream()
            .mapToDouble(DetectionResult::getScore)
            .average()
            .orElse(0);

        this.overallScore = totalScore;
    }
}

5.3.2 检测器实现

// QualityDetector.java - 检测器接口
public interface QualityDetector {
    DetectionResult detect(Mat image, Map<String, Object> parameters);
    QualityFactor getSupportedFactor();
}

// SharpnessDetector.java - 清晰度检测器
@Component
@Slf4j
public class SharpnessDetector implements QualityDetector {

    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        log.info("执行清晰度检测,参数: {}", parameters);

        // 转换为灰度图
        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        // 使用 Laplacian 算子计算方差
        Mat laplacian = new Mat();
        Imgproc.Laplacian(gray, laplacian, CvType.CV_64F);

        MatOfDouble mean = new MatOfDouble();
        MatOfDouble std = new MatOfDouble();
        Core.meanStdDev(laplacian, mean, std);

        double variance = Math.pow(std.get(0, 0)[0], 2);

        // 获取参数
        double threshold = getDoubleParam(parameters, "threshold", 100.0);
        String operator = getStringParam(parameters, "operator", ">");

        // 判定
        boolean passed = evaluateCondition(variance, operator, threshold);
        double score = calculateScore(variance, threshold);

        return DetectionResult.builder()
            .factor(QualityFactor.SHARPNESS)
            .passed(passed)
            .measuredValue(variance)
            .score(score)
            .message(String.format("清晰度方差: %.2f (阈值: %.2f)", variance, threshold))
            .build();
    }

    @Override
    public QualityFactor getSupportedFactor() {
        return QualityFactor.SHARPNESS;
    }

    private boolean evaluateCondition(double value, String operator, double threshold) {
        switch (operator) {
            case ">": return value > threshold;
            case "<": return value < threshold;
            case ">=": return value >= threshold;
            case "<=": return value <= threshold;
            case "==": return Math.abs(value - threshold) < 0.01;
            default: return false;
        }
    }

    private double calculateScore(double value, double threshold) {
        return Math.min(100, (value / threshold) * 100);
    }

    private double getDoubleParam(Map<String, Object> params, String key, double defaultValue) {
        Object value = params.get(key);
        if (value instanceof Number) {
            return ((Number) value).doubleValue();
        }
        return defaultValue;
    }

    private String getStringParam(Map<String, Object> params, String key, String defaultValue) {
        Object value = params.get(key);
        return value != null ? value.toString() : defaultValue;
    }
}

// ContrastDetector.java - 对比度检测器
@Component
@Slf4j
public class ContrastDetector implements QualityDetector {

    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        log.info("执行对比度检测");

        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        // 计算标准差作为对比度指标
        MatOfDouble mean = new MatOfDouble();
        MatOfDouble std = new MatOfDouble();
        Core.meanStdDev(gray, mean, std);

        double contrast = std.get(0, 0)[0];
        double minContrast = getDoubleParam(parameters, "min", 30.0);

        boolean passed = contrast >= minContrast;
        double score = (contrast / minContrast) * 100;

        return DetectionResult.builder()
            .factor(QualityFactor.CONTRAST)
            .passed(passed)
            .measuredValue(contrast)
            .score(Math.min(100, score))
            .message(String.format("对比度: %.2f (最小值: %.2f)", contrast, minContrast))
            .build();
    }

    @Override
    public QualityFactor getSupportedFactor() {
        return QualityFactor.CONTRAST;
    }

    private double getDoubleParam(Map<String, Object> params, String key, double defaultValue) {
        Object value = params.get(key);
        if (value instanceof Number) {
            return ((Number) value).doubleValue();
        }
        return defaultValue;
    }
}

// BrightnessDetector.java - 亮度检测器
@Component
@Slf4j
public class BrightnessDetector implements QualityDetector {

    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        log.info("执行亮度检测");

        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        Scalar meanScalar = Core.mean(gray);
        double brightness = meanScalar.val[0];

        // 支持范围判断
        List<Double> range = (List<Double>) parameters.get("range");
        boolean passed = false;
        String message;

        if (range != null && range.size() == 2) {
            passed = brightness >= range.get(0) && brightness <= range.get(1);
            message = String.format("亮度: %.2f (范围: [%.2f, %.2f])",
                brightness, range.get(0), range.get(1));
        } else {
            message = String.format("亮度: %.2f", brightness);
        }

        double score = passed ? 100 : 50;

        return DetectionResult.builder()
            .factor(QualityFactor.BRIGHTNESS)
            .passed(passed)
            .measuredValue(brightness)
            .score(score)
            .message(message)
            .build();
    }

    @Override
    public QualityFactor getSupportedFactor() {
        return QualityFactor.BRIGHTNESS;
    }
}

// ArtifactDetector.java - 伪影检测器
@Component
@Slf4j
public class ArtifactDetector implements QualityDetector {

    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        log.info("执行伪影检测");

        Mat gray = new Mat();
        Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);

        // 使用 Canny 边缘检测
        Mat edges = new Mat();
        List<Integer> cannyThreshold = (List<Integer>) parameters.get("canny_threshold");
        double low = cannyThreshold != null ? cannyThreshold.get(0) : 50;
        double high = cannyThreshold != null ? cannyThreshold.get(1) : 150;

        Imgproc.Canny(gray, edges, low, high);

        // 形态学操作去除噪声
        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
        Imgproc.morphologyEx(edges, edges, Imgproc.MORPH_CLOSE, kernel);

        // 统计边缘像素比例
        int edgePixels = Core.countNonZero(edges);
        double edgeRatio = (double) edgePixels / (edges.rows() * edges.cols());

        double maxRatio = getDoubleParam(parameters, "max_edge_ratio", 0.3);
        boolean passed = edgeRatio <= maxRatio;

        return DetectionResult.builder()
            .factor(QualityFactor.ARTIFACT)
            .passed(passed)
            .measuredValue(edgeRatio)
            .score(passed ? 100 : (1 - edgeRatio / maxRatio) * 100)
            .message(String.format("边缘比例: %.4f (最大值: %.4f)", edgeRatio, maxRatio))
            .build();
    }

    @Override
    public QualityFactor getSupportedFactor() {
        return QualityFactor.ARTIFACT;
    }

    private double getDoubleParam(Map<String, Object> params, String key, double defaultValue) {
        Object value = params.get(key);
        if (value instanceof Number) {
            return ((Number) value).doubleValue();
        }
        return defaultValue;
    }
}

5.3.3 规则引擎

// QualityRuleEngine.java
@Service
@Slf4j
public class QualityRuleEngine {

    private final Map<QualityFactor, QualityDetector> detectorMap;

    @Autowired
    public QualityRuleEngine(List<QualityDetector> detectors) {
        this.detectorMap = detectors.stream()
            .collect(Collectors.toMap(
                QualityDetector::getSupportedFactor,
                detector -> detector
            ));

        log.info("已注册 {} 个检测器: {}", detectorMap.size(), detectorMap.keySet());
    }

    /**
     * 执行质控检测
     */
    public QualityReport executeQualityControl(String imagePath, List<QualityRule> rules) {
        log.info("开始执行质控检测: {}, 规则数: {}", imagePath, rules.size());

        // 加载图像
        Mat image = Imgcodecs.imread(imagePath);
        if (image.empty()) {
            throw new RuntimeException("无法加载图像: " + imagePath);
        }

        QualityReport report = new QualityReport();
        report.setReportId(UUID.randomUUID().toString());
        report.setImagePath(imagePath);
        report.setCheckTime(LocalDateTime.now());

        List<DetectionResult> results = new ArrayList<>();
        boolean overallPassed = true;

        // 按规则顺序执行检测
        for (QualityRule rule : rules) {
            if (!"启用".equals(rule.getStatus())) {
                continue;
            }

            QualityDetector detector = detectorMap.get(rule.getFactor());
            if (detector == null) {
                log.warn("未找到检测器: {}", rule.getFactor());
                continue;
            }

            try {
                DetectionResult result = detector.detect(image, rule.getParameters());
                results.add(result);

                log.info("检测结果: {} - {} ({})",
                    rule.getFactor(),
                    result.isPassed() ? "通过" : "失败",
                    result.getMessage());

                // 否决项判定
                if (rule.isVeto() && !result.isPassed()) {
                    report.setVetoReason(String.format("[否决项] %s 检测失败: %s",
                        rule.getRuleName(),
                        result.getMessage()));
                    overallPassed = false;
                    log.error("遇到否决项: {}", report.getVetoReason());
                    break;  // 遇到否决项直接终止
                }

                if (!result.isPassed()) {
                    overallPassed = false;
                }

            } catch (Exception e) {
                log.error("检测异常: {} - {}", rule.getFactor(), e.getMessage(), e);
            }
        }

        report.setResults(results);
        report.setPassed(overallPassed);
        report.calculateOverallScore();

        log.info("质控检测完成: {} - 总分: {}",
            overallPassed ? "通过" : "失败",
            report.getOverallScore());

        return report;
    }
}

5.3.4 规则服务

// QualityRuleService.java
@Service
@Slf4j
public class QualityRuleService {

    @Autowired
    private QualityRuleRepository ruleRepository;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 加载规则集
     */
    public List<QualityRule> loadRules(String ruleSetId) {
        log.info("加载规则集: {}", ruleSetId);

        List<QualityRule> rules = ruleRepository.findByRuleSetIdAndStatus(ruleSetId, "启用");

        // 将 JSON 参数转换为 Map
        for (QualityRule rule : rules) {
            try {
                if (rule.getParametersJson() != null) {
                    Map<String, Object> params = objectMapper.readValue(
                        rule.getParametersJson(),
                        new TypeReference<Map<String, Object>>() {}
                    );
                    rule.setParameters(params);
                }
            } catch (JsonProcessingException e) {
                log.error("解析规则参数失败: {}", rule.getRuleId(), e);
            }
        }

        return rules;
    }

    /**
     * 创建规则
     */
    public QualityRule createRule(QualityRule rule) {
        try {
            if (rule.getParameters() != null) {
                String json = objectMapper.writeValueAsString(rule.getParameters());
                rule.setParametersJson(json);
            }
            return ruleRepository.save(rule);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("序列化规则参数失败", e);
        }
    }

    /**
     * 获取默认规则集
     */
    public List<QualityRule> getDefaultRules() {
        List<QualityRule> rules = new ArrayList<>();

        // 清晰度(否决项)
        rules.add(QualityRule.builder()
            .ruleId("R001")
            .ruleName("清晰度检查")
            .factor(QualityFactor.SHARPNESS)
            .isVeto(true)
            .weight(20)
            .parameters(Map.of("threshold", 100.0, "operator", ">"))
            .status("启用")
            .build());

        // 对比度
        rules.add(QualityRule.builder()
            .ruleId("R002")
            .ruleName("对比度检查")
            .factor(QualityFactor.CONTRAST)
            .isVeto(false)
            .weight(30)
            .parameters(Map.of("min", 30.0))
            .status("启用")
            .build());

        // 亮度
        rules.add(QualityRule.builder()
            .ruleId("R003")
            .ruleName("亮度检查")
            .factor(QualityFactor.BRIGHTNESS)
            .isVeto(false)
            .weight(20)
            .parameters(Map.of("range", Arrays.asList(50.0, 200.0)))
            .status("启用")
            .build());

        // 伪影(否决项)
        rules.add(QualityRule.builder()
            .ruleId("R004")
            .ruleName("伪影检测")
            .factor(QualityFactor.ARTIFACT)
            .isVeto(true)
            .weight(30)
            .parameters(Map.of(
                "canny_threshold", Arrays.asList(50, 150),
                "max_edge_ratio", 0.3
            ))
            .status("启用")
            .build());

        return rules;
    }
}

5.3.5 REST API 控制器

// QualityControlController.java
@RestController
@RequestMapping("/api/quality-control")
@Slf4j
public class QualityControlController {

    @Autowired
    private QualityRuleEngine ruleEngine;

    @Autowired
    private QualityRuleService ruleService;

    @Autowired
    private QualityReportRepository reportRepository;

    /**
     * 单张影像质控
     */
    @PostMapping("/check")
    public ResponseEntity<QualityReport> checkImage(
            @RequestParam String imagePath,
            @RequestParam(required = false) String ruleSetId) {

        log.info("收到质控请求: imagePath={}, ruleSetId={}", imagePath, ruleSetId);

        try {
            List<QualityRule> rules = ruleSetId != null
                ? ruleService.loadRules(ruleSetId)
                : ruleService.getDefaultRules();

            QualityReport report = ruleEngine.executeQualityControl(imagePath, rules);

            // 保存报告
            reportRepository.save(report);

            return ResponseEntity.ok(report);

        } catch (Exception e) {
            log.error("质控检测失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(null);
        }
    }

    /**
     * 批量质控
     */
    @PostMapping("/batch-check")
    public ResponseEntity<List<QualityReport>> batchCheck(
            @RequestBody List<String> imagePaths,
            @RequestParam(required = false) String ruleSetId) {

        log.info("收到批量质控请求: 数量={}, ruleSetId={}", imagePaths.size(), ruleSetId);

        List<QualityRule> rules = ruleSetId != null
            ? ruleService.loadRules(ruleSetId)
            : ruleService.getDefaultRules();

        List<QualityReport> reports = imagePaths.stream()
            .map(path -> {
                try {
                    QualityReport report = ruleEngine.executeQualityControl(path, rules);
                    reportRepository.save(report);
                    return report;
                } catch (Exception e) {
                    log.error("处理失败: {}", path, e);
                    return null;
                }
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

        return ResponseEntity.ok(reports);
    }

    /**
     * 查询质控报告
     */
    @GetMapping("/reports/{reportId}")
    public ResponseEntity<QualityReport> getReport(@PathVariable String reportId) {
        return reportRepository.findById(reportId)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    /**
     * 查询规则列表
     */
    @GetMapping("/rules")
    public ResponseEntity<List<QualityRule>> getRules(
            @RequestParam(required = false) String ruleSetId) {

        List<QualityRule> rules = ruleSetId != null
            ? ruleService.loadRules(ruleSetId)
            : ruleService.getDefaultRules();

        return ResponseEntity.ok(rules);
    }
}

6. 数据库设计

6.1 表结构

6.1.1 质控规则表 (quality_rules)

CREATE TABLE quality_rules (
    rule_id VARCHAR(50) PRIMARY KEY COMMENT '规则ID',
    rule_name VARCHAR(100) NOT NULL COMMENT '规则名称',
    factor VARCHAR(50) NOT NULL COMMENT '质控因子',
    is_veto BOOLEAN DEFAULT FALSE COMMENT '是否为否决项',
    weight INT DEFAULT 20 COMMENT '评分权重',
    parameters_json JSON COMMENT '检测参数(JSON格式)',
    operator VARCHAR(20) COMMENT '运算符',
    threshold VARCHAR(100) COMMENT '阈值',
    status VARCHAR(20) DEFAULT '启用' COMMENT '状态',
    device_type VARCHAR(50) COMMENT '设备类型(DR/CT/MRI)',
    rule_set_id VARCHAR(50) COMMENT '规则集ID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_rule_set (rule_set_id),
    INDEX idx_factor (factor),
    INDEX idx_status (status)
) COMMENT='质控规则配置表';

6.1.2 规则集表 (rule_sets)

CREATE TABLE rule_sets (
    set_id VARCHAR(50) PRIMARY KEY COMMENT '规则集ID',
    set_name VARCHAR(100) NOT NULL COMMENT '规则集名称',
    description TEXT COMMENT '描述',
    device_type VARCHAR(50) COMMENT '设备类型',
    exam_type VARCHAR(100) COMMENT '检查类型(胸部X线/腹部CT等)',
    status VARCHAR(20) DEFAULT '启用',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='规则集表';

6.1.3 质控报告表 (quality_reports)

CREATE TABLE quality_reports (
    report_id VARCHAR(50) PRIMARY KEY COMMENT '报告ID',
    image_path VARCHAR(500) NOT NULL COMMENT '影像路径',
    rule_set_id VARCHAR(50) COMMENT '使用的规则集ID',
    check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '检测时间',
    passed BOOLEAN NOT NULL COMMENT '是否通过',
    veto_reason TEXT COMMENT '否决原因',
    results_json JSON COMMENT '检测结果详情(JSON)',
    overall_score DECIMAL(5,2) COMMENT '总分',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_image_path (image_path(255)),
    INDEX idx_check_time (check_time),
    INDEX idx_passed (passed)
) COMMENT='质控报告表';

6.2 示例数据

-- 插入规则集
INSERT INTO rule_sets (set_id, set_name, description, device_type, exam_type) VALUES
('chest-xray-rules', '胸部X线质控规则', '胸部X线标准质控规则集', 'DR', '胸部X线'),
('ct-head-rules', '头部CT质控规则', '头部CT标准质控规则集', 'CT', '头部CT');

-- 插入规则
INSERT INTO quality_rules
(rule_id, rule_name, factor, is_veto, weight, parameters_json, status, rule_set_id)
VALUES
('R001', '清晰度检查', 'SHARPNESS', TRUE, 20,
 '{"threshold": 100.0, "operator": ">"}', '启用', 'chest-xray-rules'),

('R002', '对比度检查', 'CONTRAST', FALSE, 30,
 '{"min": 30.0}', '启用', 'chest-xray-rules'),

('R003', '亮度检查', 'BRIGHTNESS', FALSE, 20,
 '{"range": [50, 200]}', '启用', 'chest-xray-rules'),

('R004', '伪影检测', 'ARTIFACT', TRUE, 30,
 '{"canny_threshold": [50, 150], "max_edge_ratio": 0.3}', '启用', 'chest-xray-rules');

7. 部署方案

7.1 环境要求

  • JDK: 11 或 17
  • Maven: 3.6+
  • 数据库: MySQL 8.0 / PostgreSQL 14+
  • 内存: 最低 2GB,推荐 4GB+
  • OpenCV: 需要系统安装 OpenCV 库

7.2 OpenCV 安装

Linux (Ubuntu/Debian)

sudo apt-get update
sudo apt-get install -y libopencv-dev

macOS

brew install opencv

Windows

下载预编译的 OpenCV 库或使用 openpnp 的 Maven 依赖(已包含在项目中)

7.3 配置文件

# application.yml
spring:
  application:
    name: medical-image-qc-system

  datasource:
    url: jdbc:mysql://localhost:3306/medical_qc?useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

# 质控系统配置
quality-control:
  opencv:
    # OpenCV 库路径(可选)
    library-path: /usr/local/lib

  # 影像存储路径
  image:
    base-path: /data/medical-images

  # 默认规则集
  default-rule-set: chest-xray-rules

  # 并发配置
  thread-pool:
    core-size: 4
    max-size: 10

# 日志配置
logging:
  level:
    root: INFO
    com.example.qc: DEBUG
  file:
    name: logs/quality-control.log

7.4 启动步骤

# 1. 克隆项目
git clone <repository-url>
cd medical-image-qc-system

# 2. 创建数据库
mysql -u root -p
CREATE DATABASE medical_qc CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# 3. 编译打包
mvn clean package -DskipTests

# 4. 运行
java -jar target/medical-image-qc-system-1.0.0.jar

# 或使用 Spring Boot Maven 插件
mvn spring-boot:run

7.5 Docker 部署

# Dockerfile
FROM openjdk:17-slim

# 安装 OpenCV
RUN apt-get update && apt-get install -y \
    libopencv-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY target/medical-image-qc-system-1.0.0.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
# docker-compose.yml
version: '3.8'

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: medical_qc
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql

  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mysql
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/medical_qc
      SPRING_DATASOURCE_USERNAME: root
      SPRING_DATASOURCE_PASSWORD: root123
    volumes:
      - /data/medical-images:/data/medical-images

volumes:
  mysql-data:

8. 扩展性设计

8.1 添加新的质控因子

步骤:

  1. QualityFactor 枚举中添加新因子

    public enum QualityFactor {
    // ... 现有因子
    DICOM_TAGS("DICOM标签完整性", "dicom_tags_check");
    }
    
    1. 实现对应的检测器 ```java @Component public class DicomTagsDetector implements QualityDetector { @Override public DetectionResult detect(Mat image, Map parameters) { // 实现 DICOM 标签检测逻辑 }
    2. @Override public QualityFactor getSupportedFactor() {

      return QualityFactor.DICOM_TAGS;
      

      } } ```

  2. 在数据库中配置规则

    INSERT INTO quality_rules VALUES (...);
    

    8.2 支持深度学习模型

    @Component
    public class DeepLearningDetector implements QualityDetector {
    
    @Autowired
    private TensorFlowService tensorFlowService;
    
    @Override
    public DetectionResult detect(Mat image, Map<String, Object> parameters) {
        String modelPath = (String) parameters.get("model_path");
    
        // 调用 TensorFlow/PyTorch 模型
        float[] predictions = tensorFlowService.predict(image, modelPath);
    
        // 处理预测结果
        // ...
    }
    }
    
  3. 8.3 分布式部署

    使用消息队列(RabbitMQ/Kafka)实现异步处理:

    @Service
    public class AsyncQualityControlService {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        public String submitQualityCheck(String imagePath, String ruleSetId) {
            String taskId = UUID.randomUUID().toString();
    
            QualityCheckTask task = new QualityCheckTask(taskId, imagePath, ruleSetId);
            rabbitTemplate.convertAndSend("qc.queue", task);
    
            return taskId;
        }
    }
    
    @Component
    public class QualityCheckConsumer {
    
        @RabbitListener(queues = "qc.queue")
        public void processQualityCheck(QualityCheckTask task) {
            // 执行质控检测
            // ...
        }
    }
    

    9. 附录

    9.1 API 接口文档

    9.1.1 单张影像质控

    请求:

    POST /api/quality-control/check
    Content-Type: application/x-www-form-urlencoded
    
    imagePath=/data/images/chest-001.dcm&ruleSetId=chest-xray-rules
    

    响应:

    {
      "reportId": "abc123",
      "imagePath": "/data/images/chest-001.dcm",
      "ruleSetId": "chest-xray-rules",
      "checkTime": "2026-01-05T10:30:00",
      "passed": false,
      "vetoReason": "[否决项] 清晰度检查 检测失败: 清晰度方差: 85.23 (阈值: 100.00)",
      "overallScore": 75.5,
      "results": [
        {
          "factor": "SHARPNESS",
          "passed": false,
          "score": 85.23,
          "measuredValue": 85.23,
          "message": "清晰度方差: 85.23 (阈值: 100.00)"
        }
      ]
    }
    

    9.1.2 批量质控

    请求:

    POST /api/quality-control/batch-check?ruleSetId=chest-xray-rules
    Content-Type: application/json
    
    [
      "/data/images/chest-001.dcm",
      "/data/images/chest-002.dcm"
    ]
    

    响应:

    [
      {
        "reportId": "abc123",
        "passed": true,
        "overallScore": 95.0
      },
      {
        "reportId": "def456",
        "passed": false,
        "vetoReason": "..."
      }
    ]
    

    9.2 常见问题

    Q1: OpenCV 无法加载图像?

    A: 检查图像路径是否正确,OpenCV 支持的格式包括 JPG、PNG、BMP、TIFF 等。对于 DICOM 格式,需要先转换为普通图像格式。

    Q2: 如何调整检测参数?

    A: 修改数据库中的 parameters_json 字段或通过 API 动态更新规则。

    Q3: 否决项机制如何工作?

    A: 将规则的 is_veto 设置为 true,一旦该规则检测失败,将立即终止后续检测并返回失败报告。

    9.3 性能优化建议

    1. 图像预处理缓存: 对于相同尺寸的图像,缓存灰度转换结果
    2. 批量处理优化: 使用线程池并行处理多张影像
    3. 数据库优化: 为常用查询字段添加索引
    4. 规则缓存: 使用 Redis 缓存热门规则集

    9.4 参考资料


    文档版本: v1.0 最后更新: 2026-01-05 维护人员: 技术团队