DCMTK&OpenCV-构建DICOM图像查看器

作者:翟天保Steven

版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

前言

‌‌DICOM(Digital Imaging and Communications in Medicine)是医学影像领域的标准格式,广泛应用于X光、CT、MRI等医学图像。本文将介绍如何使用DCMTK库提取 DICOM 图像数据,并结合 OpenCV 实现图像显示与交互功能。

环境准备

Windows下用CMake编译DCMTK及配置测试-CSDN博客

功能说明

本程序基于DCMTK和OpenCV实现以下核心功能:

  1. 加载DICOM文件:提取DICOM文件的路径和序列号,按序列号进行排序。
  2. 解析DICOM图像数据并显示:使用DCMTK读取图像数据,并用OpenCV显示出来。
  3. 图像增强对比度:图像应用对数变换,使图像清晰。
  4. 导航功能:切换DICOM图像。

使用说明

  1. 需安装DCMTK库及OPENCV库。
  2. 将main函数中DicomData替换你的DICOM数据路径。
  3. 使用C++17特性。 如果设置不成功,解决方法参见:解决方案:__cplusplus宏的值始终为199711L(即 C++98)_199711 c++-CSDN博客

完整代码

复制代码
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <filesystem>
#include <cmath>

// DCMTK头文件
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"         //DcmFileFormat
#include "dcmtk/dcmimgle/dcmimage.h"    //DicomImage
#include "dcmtk/dcmjpeg/djdecode.h"     //DJDecoderRegistration

// OpenCV头文件
#include <opencv2/opencv.hpp>

namespace fs = std::filesystem;
using namespace std;

// 存储DICOM文件路径和对应的InstanceNumber
struct DicomFileInfo {
    std::string filePath;
    int instanceNumber;

    DicomFileInfo() : instanceNumber(-1) {}
    DicomFileInfo(const std::string& path, int number) : filePath(path), instanceNumber(number) {}

    bool operator<(const DicomFileInfo& other) const {
        return instanceNumber < other.instanceNumber;
    }
};

// DICOM数据显示
class DicomViewer {
private:
    std::vector<DicomFileInfo> dicomFiles;
    size_t currentIndex;
    cv::Mat currentImage;
    std::string windowName;

public:
    DicomViewer() : currentIndex(0) {}

    // 加载DICOM文件并排序
    bool loadDicomDirectory(const std::string& directory) {
        dicomFiles.clear();

        try {
            std::vector<std::string> filePaths;
            // 提取所有DICOM文件
            for (const auto& entry : fs::directory_iterator(directory)) {
                if (entry.is_regular_file()) {
                    std::string ext = entry.path().extension().string();
                    std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
                    if (ext == ".dcm" || ext == ".dicm" || ext == ".dicom") {
                        filePaths.push_back(entry.path().string());
                    }
                }
            }

            // 提取DICOM文件的序列号
            for (const auto& filePath : filePaths) {
                DcmFileFormat fileFormat;
                OFCondition status = fileFormat.loadFile(filePath.c_str());

                if (status.good()) {
                    DcmDataset* dataset = fileFormat.getDataset();
                    OFString strInstanceNumber;
                    if (dataset->findAndGetOFString(DCM_InstanceNumber, strInstanceNumber).good()) {
                        int instanceNumber = atoi(strInstanceNumber.c_str());
                        dicomFiles.emplace_back(filePath, instanceNumber);
                    }
                    else {
                        dicomFiles.emplace_back(filePath, -1);
                        std::cerr << "警告: 文件 " << filePath << " 缺少InstanceNumber标签" << std::endl;
                    }
                }
                else {
                    std::cerr << "无法加载文件: " << filePath << " - 错误: " << status.text() << std::endl;
                }
            }

            // 排序
            std::sort(dicomFiles.begin(), dicomFiles.end());

            /*std::cout << "\n按InstanceNumber排序后的文件列表 (" << dicomFiles.size() << "个):" << std::endl;
            for (size_t i = 0; i < dicomFiles.size(); ++i) {
                std::cout << "[" << i + 1 << "] InstanceNumber: "
                    << (dicomFiles[i].instanceNumber >= 0 ? std::to_string(dicomFiles[i].instanceNumber) : "缺失")
                    << " - 文件: " << dicomFiles[i].filePath << std::endl;
            }*/

        }
        catch (const fs::filesystem_error& e) {
            std::cerr << "文件系统错误: " << e.what() << std::endl;
            return false;
        }

        if (dicomFiles.empty()) {
            std::cout << "未找到DICOM文件" << std::endl;
            return false;
        }

        std::cout << "\n成功加载并排序 " << dicomFiles.size() << " 个DICOM文件" << std::endl;
        return true;
    }

    // 对图像应用对数变换增强对比度
    cv::Mat applyLogTransform(const cv::Mat& input) {
        if (input.empty()) return cv::Mat();

        cv::Mat floatImage;
        input.convertTo(floatImage, CV_32F, 1.0 / 255.0); // 归一化到[0,1]

        cv::Mat logImage;
        cv::log(floatImage + 1, logImage); // 对数变换: log(1 + image)

        // 归一化到[0,1]
        double minVal, maxVal;
        cv::minMaxLoc(logImage, &minVal, &maxVal);
        cv::Mat result;
        if (maxVal > minVal) {
            logImage = (logImage - minVal) / (maxVal - minVal);
        }

        // 转换回8位无符号整型
        result = logImage * 255;
        result.convertTo(result, CV_8U);

        return result;
    }

    // 从DICOM文件提取图像数据
    bool loadDicomImage(const std::string& filePath) {
        DicomImage* dcmImage = new DicomImage(filePath.c_str());
        std::cout << "加载文件: " << filePath << std::endl;

        if (dcmImage == nullptr || dcmImage->getStatus() != EIS_Normal) {
            std::cerr << "图像加载失败: " << filePath
                << " - 错误: " << DicomImage::getString(dcmImage->getStatus()) << std::endl;
            delete dcmImage;
            return false;
        }

        // 处理图像数据
        unsigned long width = dcmImage->getWidth();
        unsigned long height = dcmImage->getHeight();
        cv::Mat tempImage;

        if (dcmImage->isMonochrome()) {
            if (dcmImage->getOutputData(8)) {
                tempImage = cv::Mat(height, width, CV_8UC1);
                dcmImage->getOutputData(tempImage.data, width * height, 8);
            }
            else if (dcmImage->getOutputData(16)) {
                tempImage = cv::Mat(height, width, CV_16UC1);
                dcmImage->getOutputData(tempImage.data, width * height, 16);
                cv::Mat displayImage;
                cv::normalize(tempImage, displayImage, 0, 255, cv::NORM_MINMAX, CV_8UC1);
                tempImage = displayImage;
            }
            else {
                std::cerr << "不支持的像素格式" << std::endl;
                delete dcmImage;
                return false;
            }
        }
        else {
            tempImage = cv::Mat(height, width, CV_8UC3);
            dcmImage->getOutputData(tempImage.data, width * height * 3, 24);
        }

        delete dcmImage;

        // 应用对数变换增强对比度
        if (tempImage.channels() == 1) {
            currentImage = applyLogTransform(tempImage);
        }
        else {
            // 彩色图像转换为灰度图后应用对数变换
            cv::Mat grayImage;
            cv::cvtColor(tempImage, grayImage, cv::COLOR_BGR2GRAY);
            currentImage = applyLogTransform(grayImage);
        }

        return true;
    }

    // 显示当前图像(单窗口)
    bool displayCurrentImage() {
        if (dicomFiles.empty()) return false;

        if (loadDicomImage(dicomFiles[currentIndex].filePath)) {
            windowName = "DICOM Viewer";
            cv::imshow(windowName, currentImage);
            return true;
        }
        return false;
    }

    // 导航功能
    bool nextImage() {
        if (dicomFiles.empty()) return false;
        currentIndex = (currentIndex + 1) % dicomFiles.size();
        return displayCurrentImage();
    }
    bool prevImage() {
        if (dicomFiles.empty()) return false;
        currentIndex = (currentIndex - 1 + dicomFiles.size()) % dicomFiles.size();
        return displayCurrentImage();
    }

    size_t getCurrentIndex() const { return currentIndex; }
    size_t getTotalFiles() const { return dicomFiles.size(); }
};

int main() {
    std::string dicomDir = "DicomData"; // 替换为实际路径

    DJDecoderRegistration::registerCodecs();

    DicomViewer viewer;

    if (!viewer.loadDicomDirectory(dicomDir)) return 1;

    if (!viewer.displayCurrentImage()) return 1;

    std::cout << "\n操作说明:" << std::endl;
    std::cout << "  n: 下一张  |  p: 上一张  |  q: 退出" << std::endl;

    char key = 0;
    while (key != 'q') {
        key = cv::waitKey(0);
        switch (key) {
        case 'n': viewer.nextImage(); break;
        case 'p': viewer.prevImage(); break;
        case 'q': break;
        default: std::cout << "未知按键,请使用 n/p/q" << std::endl;
        }
    }

    cv::destroyAllWindows();
    DJDecoderRegistration::cleanup();
    return 0;
}

测试效果

​读取DICOM序列

显示图像

如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

相关推荐
Ronin-Lotus2 分钟前
深度学习篇---图像数据采集
人工智能·opencv·计算机视觉
yivifu12 分钟前
使用OpenCV做个图片校正工具
python·opencv·计算机视觉
洛华3636 小时前
初识opencv04——图像预处理3
人工智能·python·opencv·计算机视觉
IMER SIMPLE16 小时前
人工智能-python-OpenCV 图像基础认知与运用-图像的预处理(1)
人工智能·python·opencv
AI technophile1 天前
OpenCV计算机视觉实战(16)——图像分割技术
人工智能·opencv·计算机视觉
洛华3631 天前
初识opencv03——图像预处理2
人工智能·opencv·计算机视觉
程序猿人大林1 天前
Opencv C# 重叠 粘连 Overlap 轮廓分割 (不知道不知道)
人工智能·opencv·计算机视觉·c#
Wendy14411 天前
【形态学变换】——图像预处理(OpenCV)
人工智能·opencv·计算机视觉
趁早折枝1 天前
从零搭建 OpenCV 项目(新手向)-- 第二天 OpenCV图像预处理(一)
人工智能·opencv·计算机视觉
旭日东升的xu.1 天前
OpenCV(03)插值方法,边缘填充,透视变换,水印制作,噪点消除
人工智能·opencv·计算机视觉