版本: v1.0 日期: 2026-01-05 作者: 技术团队
医学影像质控系统需要对 DR、CT、MRI 等医学影像进行自动化质量检测,确保影像符合诊断标准。质控因子包括:
核心问题: 如何将业务层的质控因子(如"清晰度"、"伪影")转换为 OpenCV 可执行的图像处理算法?
技术挑战:
根据提供的质控因子表,系统包含以下核心因子:
| 标准名称 | 因子分类 | 因子名称 | 适用设备 | 评价词汇 | 评分权重 | 状态 | 否决项 |
|---|---|---|---|---|---|---|---|
| 标准技术质控 | 基础质控 | 图像有效性 | DR, CT, MRI | 影像检查图像的检查范围 | 20 | 启用 | 较差、较差 |
| 标准技术质控 | 基础质控 | 摆实人体位置 | DR, CT, MRI | 影像检查图片中的体位位置是否标准 | 30 | 启用 | 非人体 |
| 标准技术质控 | 基础质控 | 医拍完整图像 | DR, CT, MRI | 影像检查是否存在切角及部分切分类 | 20 | 启用 | 非医拍组图像 |
| ... | ... | ... | ... | ... | ... | ... | ... |
业务因子 → 技术参数 → OpenCV 算法
示例:
{method: "laplacian", threshold: 100} → Laplacian 方差计算{canny_threshold: [50, 150]} → Canny 边缘检测{range: [50, 200]} → 灰度均值计算┌─────────────────────────────────────────────────────────────┐
│ 前端展示层 (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) │
│ - 质控报告存储 │
│ - 影像文件管理 │
└─────────────────────────────────────────────────────────────┘
业务层 (质控因子)
↓
转换层 (参数化配置)
↓
技术层 (OpenCV 算法)
| 质控因子 | 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 |
// 检测器接口
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();
}
}
# 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]
<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>
// 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;
}
}
// 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;
}
}
// 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;
}
}
// 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;
}
}
// 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);
}
}
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='质控规则配置表';
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='规则集表';
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='质控报告表';
-- 插入规则集
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');
sudo apt-get update
sudo apt-get install -y libopencv-dev
brew install opencv
下载预编译的 OpenCV 库或使用 openpnp 的 Maven 依赖(已包含在项目中)
# 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
# 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
# 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:
步骤:
在 QualityFactor 枚举中添加新因子
public enum QualityFactor {
// ... 现有因子
DICOM_TAGS("DICOM标签完整性", "dicom_tags_check");
}
@Override public QualityFactor getSupportedFactor() {
return QualityFactor.DICOM_TAGS;
} } ```
在数据库中配置规则
INSERT INTO quality_rules VALUES (...);
@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);
// 处理预测结果
// ...
}
}
使用消息队列(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) {
// 执行质控检测
// ...
}
}
请求:
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)"
}
]
}
请求:
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": "..."
}
]
Q1: OpenCV 无法加载图像?
A: 检查图像路径是否正确,OpenCV 支持的格式包括 JPG、PNG、BMP、TIFF 等。对于 DICOM 格式,需要先转换为普通图像格式。
Q2: 如何调整检测参数?
A: 修改数据库中的 parameters_json 字段或通过 API 动态更新规则。
Q3: 否决项机制如何工作?
A: 将规则的 is_veto 设置为 true,一旦该规则检测失败,将立即终止后续检测并返回失败报告。
文档版本: v1.0 最后更新: 2026-01-05 维护人员: 技术团队