Skip to content

Commit

Permalink
Merge branch 'sql-refactoring' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Chinlinlee committed Dec 22, 2023
2 parents bdc1ebe + f08bf44 commit 89669b0
Show file tree
Hide file tree
Showing 163 changed files with 9,960 additions and 483 deletions.
18 changes: 9 additions & 9 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# MongoDB
MONGODB_NAME="raccoon-dicom"
MONGODB_HOSTS=["mongodb"]
MONGODB_PORTS=[27017]
MONGODB_USER="root"
MONGODB_PASSWORD="root"
MONGODB_AUTH_SOURCE="admin"
MONGODB_OPTIONS=""
MONGODB_IS_SHARDING_MODE=false
# SQL
SQL_HOST="127.0.0.1"
SQL_PORT="5432"
SQL_DB="raccoon"
SQL_TYPE="postgres"
SQL_USERNAME="postgres"
SQL_PASSWORD="postgres"
SQL_LOGGING=false
SQL_FORCE_SYNC=false

# Server
SERVER_PORT=8081
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@
config/ae-prod.properties

# ignore jscpd output report
/report
/report

/jsconfig.json
233 changes: 233 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

83 changes: 63 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
# raccoon-only-dicom
Another raccoon focus on dicom. The original raccoon combine the FHIR and DICOM together in MongoDB, I think it will cause performance and maintenance issues. So, here start a new project only for DICOM.
# raccoon-dicom

<p align="center">
<img src="https://user-images.githubusercontent.com/49154622/236814496-a87eb89f-9cbe-4898-a7bf-aa2b27d97596.svg" alt="logo" width="10%"/>
</p>
<br/>

Another Raccoon focus on DICOM.

[English](README.md) | [繁體中文](README.zh-TW.md)

---

**Raccoon-DICOM** is a noSQL-based medical image archive designed for managing DICOM images, utilizing MongoDB to store and manage the images while providing RESTful APIs that support [DICOMweb](https://www.dicomstandard.org/dicomweb/") protocols for querying, retrieving, and managing DICOM images.

# Installation
- [Installation](https://github.com/Chinlinlee/raccoon-dicom/wiki/Installation)
- Step by Step guide to installing Raccoon-DICOM - Windows (WIP🚧)
- Step by Step guide to installing Raccoon-DICOM - Ubuntu (WIP🚧)

# Troubleshooting on linux
- `Unknown VR: Tag not found in data dictionary` when using `STOW-RS`
- You need set the `DCMDICTPATH` environment variable
- The `dicom.dic` can find in the `/usr/share/libdcmtk{version}` or `./models/DICOM/dcmtk/dicom.dic`
> The {version} corresponds to dcmtk version, e.g. 3.6.5 => libdcmtk15
- Set `DCMDICTPATH` environment variable using command or you can add the command to profile file(`~/.bashrc`,`~/.profile` etc.), example **with dcmtk 3.6.5**:
```sh
export DCMDICTPATH=/usr/share/libdcmtk15/dicom.dic
```
- Check the environment variable
```sh
echo $DCMDICTPATH
```

# Features
The features implemented here:
- [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6)
- [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5)
- Convert DICOM to ImagingStudy, Endpoint, Patient of FHIR resources and sync FHIR resources to FHIR server
- [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1)
- [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1)
- [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2)
- [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3)
- [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1)

# Environment Requirements
- node.js >= 16
- Java JDK >= 11

> **Note**
> - You should copy opencv_java library to JDK's lib directory
> - In windows, copy `opencv_java.dll`
> - In linux, copy `libclib_jiio.so` and `libopencv_java.so`
## [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6)
### Support Format (Media Types)

Format | Support |
---------|----------|
application/dicom+json | ✅ |
multipart/related; type="application/dicom+xml | ❌ |

### Support Query Parameter

Query Parameter | Support |
---------|----------|
fuzzymatching | ❌ |
includefield | ✅ |
limit | ✅ |
offset | ✅ |


## [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5)
- You can set `SYCN_TO_FHIR_SERVER=true` in .env to convert DICOM to ImagingStudy, Endpoint, Patient of FHIR resources and sync FHIR resources to FHIR server
## [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1)
- [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1)
- [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2)
- [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3)
- [Retrieve Transaction Thumbnail Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-4)
- [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1)


# API Documentation
- raccoon-dicom uses swagger ui hosting openapi.json to generate documentation
- [API Documentation](https://chinlinlee.github.io/raccoon-dicom/)


# Wiki
Our [wiki](https://github.com/Chinlinlee/raccoon-dicom/wiki) includes a lot of information about raccoon-dicom, we heavily encourage you to take a look!!

72 changes: 72 additions & 0 deletions README.zh-TW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# raccoon-dicom

<p align="center">
<img src="https://user-images.githubusercontent.com/49154622/236814496-a87eb89f-9cbe-4898-a7bf-aa2b27d97596.svg" alt="logo" width="10%"/>
</p>
<br/>

Another Raccoon focus on DICOM.

[English](README.md) | [繁體中文](README.zh-TW.md)

---

**Raccoon-DICOM** 是使用 no-SQL 資料庫實作的醫學影像儲存系統(DICOMweb PACS),其使用 MongoDB 管理 DICOM 影像並提供 [DICOMweb](https://www.dicomstandard.org/dicomweb/") RESTful API 功能進行儲存、查詢以及調閱


# 安裝
- [安裝手冊](https://github.com/Chinlinlee/raccoon-dicom/wiki/Installation.zh-TW)
- [從 0 開始部屬 Raccoon - Windows](https://github.com/Chinlinlee/raccoon-dicom/wiki/From-zero-to-deploy.zh-TW): 在 Windows 上,一步一步從安裝到部屬
- 從 0 開始部屬 Raccoon - Ubuntu (WIP🚧)

# Troubleshooting on linux
- `Unknown VR: Tag not found in data dictionary` when using `STOW-RS`
- 您必須設定 `DCMDICTPATH` 環境變數
- `dicom.dic` 檔案可以在`/usr/share/libdcmtk{version}``./models/DICOM/dcmtk/dicom.dic`找到
> {version} 對應到dcmtk的版本, e.g. 3.6.5 => libdcmtk15
- 使用指令設定 `DCMDICTPATH` 或者您可以將指令加入到profile檔案中(`~/.bashrc`,`~/.profile` etc.), example **with dcmtk 3.6.5**:
```sh
export DCMDICTPATH=/usr/share/libdcmtk15/dicom.dic
```
- 檢查環境變數
```sh
echo $DCMDICTPATH
```

# 提供之功能
目前以實作的功能如下:
## [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6)
### 支援的格式 (Media Types)

Format | Support |
---------|----------|
application/dicom+json | ✅ |
multipart/related; type="application/dicom+xml | ❌ |

### 支援的查詢參數

Query Parameter | Support |
---------|----------|
fuzzymatching | ❌ |
includefield | ✅ |
limit | ✅ |
offset | ✅ |


## [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5)
- 您可以在 .env 設定 `SYCN_TO_FHIR_SERVER=true` 以將 DICOM 轉換為 FHIR ImagingStudy, Endpoint 以及 Patient,並同步這些 Resources 至 FHIR server
## [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1)
- [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1)
- [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2)
- [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3)
- [Retrieve Transaction Thumbnail Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-4)
- [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1)


# API Documentation
- raccoon-dicom uses swagger ui hosting openapi.json to generate documentation
- [API Documentation](https://chinlinlee.github.io/raccoon-dicom/)

# Wiki
我們的[Wiki](https://github.com/Chinlinlee/raccoon-dicom/wiki)含有更多與 Raccoon-DICOM 更多的資訊,非常建議您閱讀一下
99 changes: 99 additions & 0 deletions api-sql/WADO-URI/service/WADO-URI.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const fs = require("fs");
const _ = require("lodash");
const renderedService = require("../../dicom-web/controller/WADO-RS/service/rendered.service");
const { Dcm2JpgExecutor } = require("@models/DICOM/dcm4che/wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor");
const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("@models/DICOM/dcm4che/wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions");
const sharp = require('sharp');
const Magick = require("@models/magick");
const { NotFoundInstanceError, InvalidFrameNumberError, InstanceGoneError } = require("@error/dicom-instance");
const { WadoUriService } = require("@root/api/WADO-URI/service/WADO-URI.service");
const { InstanceModel } = require("@models/sql/models/instance.model");
const { ApiLogger } = require("@root/utils/logs/api-logger");
class SqlWadoUriService extends WadoUriService{

/**
*
* @param {import("http").IncomingMessage} req
* @param {import("http").ServerResponse} res
*/
constructor(req, res, apiLogger) {
super(req, res);
this.apiLogger = apiLogger;
}

async getDicomInstancePathObj() {
let {
studyUID,
seriesUID,
objectUID: instanceUID
} = this.request.query;

let imagePathObj = await InstanceModel.getPathOfInstance({
studyUID,
seriesUID,
instanceUID
});

if (imagePathObj) {

try {
await fs.promises.access(imagePathObj.instancePath, fs.constants.F_OK);
} catch(e) {
console.error(e);
throw new InstanceGoneError("The image is deleted permanently, but meta data remain");
}

return imagePathObj;
}

throw new NotFoundInstanceError("Not found instance");
}

async handleFrameNumberAndGetImageObj() {
let imagePathObj = await this.getDicomInstancePathObj();
let instanceFramesObj = await renderedService.getInstanceFrameObj(imagePathObj);
let instanceTotalFrameNumber = _.get(instanceFramesObj, "x00280008") ? _.get(instanceFramesObj, "x00280008") : 1;

let windowCenter = _.get(instanceFramesObj, "x00281050.0", "");
let windowWidth = _.get(instanceFramesObj, "x00281051.0", "");

let transferSyntax = _.get(instanceFramesObj, "x00020010");
let frameNumber = parseInt(_.get(this.request.query, "frameNumber", 1));

if (frameNumber > instanceTotalFrameNumber) {
throw new InvalidFrameNumberError(`Invalid Frame Number, total ${instanceTotalFrameNumber}, but requested ${frameNumber}`);
}

/** @type {Dcm2JpgExecutor$Dcm2JpgOptions} */
let options = await Dcm2JpgExecutor$Dcm2JpgOptions.newInstanceAsync();
options.frameNumber = frameNumber;

if (windowCenter && windowWidth) {
options.windowCenter = windowCenter;
options.windowWidth = windowWidth;
}

let dicomFilename = instanceFramesObj.instancePath;
let jpegFile = dicomFilename.replace(/\.dcm\b/gi , `.${frameNumber-1}.jpg`);

let getFrameImageStatus = await Dcm2JpgExecutor.convertDcmToJpgFromFilename(
dicomFilename,
jpegFile,
options
);

if (getFrameImageStatus.status) {

return {
imageSharp: sharp(jpegFile),
magick: new Magick(jpegFile)
};
}

throw new NotFoundInstanceError("Not found DICOM Instance's Jpeg, may convert error");
}

}

module.exports.WadoUriService = SqlWadoUriService;
module.exports.NotFoundInstanceError = NotFoundInstanceError;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const { MwlItemModel } = require("@models/sql/models/mwlitems.model");
const { ChangeFilteredMwlItemStatusService } = require("@root/api/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status");
const { cloneDeep, set } = require("lodash");
const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service");
const { MwlQueryBuilder } = require("./query/mwlQueryBuilder");
const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service");
const { dictionary } = require("@models/DICOM/dicom-tags-dic");

class SqlChangeFilteredMwlItemStatusService extends ChangeFilteredMwlItemStatusService {
constructor(req, res) {
super(req, res);
}

async changeMwlItemsStatus() {
let { status } = this.request.params;
let mwlItems = await this.getMwlItems();

if (mwlItems.length === 0) {
throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "Can not found any MWL item from query", 404);
}

for (let mwlItem of mwlItems) {
mwlItem.status = status;
set(mwlItem.json, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status);
mwlItem.changed("json", true);
await mwlItem.save();
}

return mwlItems.length;
}

async getMwlItems() {
let queryBuilder = new MwlQueryBuilder({
query: this.query
});
let q = queryBuilder.build();
return await MwlItemModel.findAll(q);
}

initQuery_() {
let query = cloneDeep(this.request.query);
let queryKeys = Object.keys(query).sort();
for (let i = 0; i < queryKeys.length; i++) {
let queryKey = queryKeys[i];
if (!query[queryKey]) delete query[queryKey];
}

this.query = convertAllQueryToDicomTag(query, false);
}
}

module.exports.ChangeFilteredMwlItemStatusService = SqlChangeFilteredMwlItemStatusService;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const _ = require("lodash");
const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service");
const { dictionary } = require("@models/DICOM/dicom-tags-dic");
const { ChangeMwlItemStatusService } = require("@root/api/dicom-web/controller/MWL-RS/service/change-mwlItem-status");
const { MwlItemModel } = require("@models/sql/models/mwlitems.model");
const { Op } = require("sequelize");

class SqlChangeMwlItemStatusService extends ChangeMwlItemStatusService {
constructor(req, res) {
super(req, res);
}

async changeMwlItemsStatus() {
let { status } = this.request.params;
let mwlItem = await this.getMwlItemByStudyUIDAndSpsID();
if (!mwlItem) {
throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "No such object instance", 404);
}

mwlItem.sps_status = status;
_.set(mwlItem.json, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status);
mwlItem.changed("json", true);
await mwlItem.save();

return mwlItem.json;
}

async getMwlItemByStudyUIDAndSpsID() {
return await MwlItemModel.findOne({
where: {
[Op.and]: [
{
sps_id: this.request.params.spsID
},
{
study_instance_uid: this.request.params.studyUID
}
]
}
});
}
}

module.exports.ChangeMwlItemStatusService = SqlChangeMwlItemStatusService;
Loading

0 comments on commit 89669b0

Please sign in to comment.