Skip to content

Commit

Permalink
feat: implement convert study to video #14 gitlab
Browse files Browse the repository at this point in the history
  • Loading branch information
Chinlinlee committed Mar 5, 2024
1 parent e41f1fc commit 7b693d5
Show file tree
Hide file tree
Showing 5 changed files with 477 additions and 12 deletions.
6 changes: 5 additions & 1 deletion api/dicom-web/controller/convert-image/convert-image.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
const { Controller } = require("@root/api/controller.class");
const { ConvertImageService } = require("./service/convert-image.service");
const fs = require("fs");

class ConvertImageController extends Controller {
constructor(req, res) {
super(req, res);
}

async mainProcess() {
return this.response.send({message: "hello world"});
let convertImageService = new ConvertImageService(this.request, this.response);
let videoFile = await convertImageService.convert();
return fs.createReadStream(videoFile).pipe(this.response);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
const { StudyVideoFactory } = require("./video-factory");

class ConvertImageService {
constructor(req, res) {
/** @type { import("express").Request } */
this.request = req;
/** @type { import("express").Response } */
this.response = res;
}
}

async convert() {
const videoMimeType = [
"video/mp4",
"video/x-msvideo",
"video/mpeg",
"video/H265"
];

if (videoMimeType.includes(this.request.headers.accept)) {
let videoFactory = new StudyVideoFactory(this.request.headers.accept, this.request.params);
return await videoFactory.convert();
}
}
}

module.exports.ConvertImageService = ConvertImageService;
268 changes: 268 additions & 0 deletions api/dicom-web/controller/convert-image/service/video-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
const { DicomWebServiceError } = require("@error/dicom-web-service");
const { StudyImagePathFactory } = require("../../WADO-RS/service/WADO-RS.service");
const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions");
const { InstanceModel } = require("@models/sql/models/instance.model");
const { Dcm2JpgExecutor } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor");
const path = require("path");
const mkdirp = require("mkdirp");
const fsP = require("fs/promises");
const { v4: uuidV4 } = require("uuid");
const os = require("os");
const ffmpegInstance = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const { EventEmitter } = require("events");
ffmpeg.setFfmpegPath(ffmpegInstance);

class VideoFactory {
constructor(mimeType, uids) {
this.mimeType = mimeType;
this.uids = uids;
this.imagePathFactory = undefined;
this.imageTempFolder = path.join(__dirname, "../../../../../tempUploadFiles", this.uids.studyUID, uuidV4());
}

/**
* @param {string} folder the folder location that contains frames jpeg images
* @param {string[]} jpegs the jpeg images' file location
*/
async convert(folder, jpegs) {
let filename = await this.getFfmpegInputData(jpegs);
if (this.mimeType === "video/mp4") {
let converter = new Mp4Converter();
return await converter.convert(folder, filename);
} else if (this.mimeType === "video/x-msvideo") {
let converter = new AviConverter();
return await converter.convert(folder, filename);
} else if (this.mimeType === "video/mpeg") {
let converter = new MpegConverter();
return await converter.convert(folder, filename);
} else if (this.mimeType === "video/H265") {
let converter = new H265Converter();
return await converter.convert(folder, filename);
}
}

async getFfmpegInputData(images) {
let ffmpegInputData = images.map(jpeg => `file 'file:${jpeg}'`).join("\n");
let filename = path.join(__dirname, "../../../../../tempUploadFiles", `${uuidV4()}.txt`);
await fsP.writeFile(filename, ffmpegInputData);
return filename;
}

async getImagePaths() {
await this.imagePathFactory.getImagePaths();
let checkAllImageExistResult = await this.imagePathFactory.checkAllImageExist();
if (!checkAllImageExistResult.status) {
throw new DicomWebServiceError("404", checkAllImageExistResult.message, 400);
}

return this.imagePathFactory.imagePaths;
}

async getInstancesFrames() {
let imagePaths = await this.getImagePaths();
let instancesFrames = await Promise.all(imagePaths.map(async imagePath => await InstanceModel.getInstanceFrame(imagePath)));
instancesFrames.sort((a, b) => a["x00200013"] - b["x00200013"]);
return instancesFrames;
}

/**
* TODO: 也許需要重構成 wado-rs/uri 通用的產圖 class
* @param {import("@root/utils/typeDef/dicomImage").InstanceFrameObj} frame
* @param {string} folder
* @returns
*/
async generateJpegImage(frame, folder) {
let jsDcm2Jpegs = [];

let imageFrameNumber = frame["00280008"]?.["Value"]?.[0] || 1;
mkdirp.sync(folder, "0755");
imageFrameNumber = parseInt(imageFrameNumber);
let jpegBaseFilename = path.resolve(
path.join(folder, frame["instanceUID"])
);
let jpegs = [];
for (let i = 1; i <= imageFrameNumber; i++) {

let jpegFilename = `${jpegBaseFilename}.${i - 1}.jpg`;
jpegs.push(jpegFilename);

let dcm2jpgOpt = new Dcm2JpgExecutor$Dcm2JpgOptions();
let windowCenter = frame["00281050"]?.["Value"]?.[0];
let windowWidth = frame["00281051"]?.["Value"]?.[0];

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

dcm2jpgOpt.frameNumber = i;

jsDcm2Jpegs.push({
jsDcm2JpegOption: dcm2jpgOpt,
jpegFilename: jpegFilename
});


if (i % os.cpus().length === 0) {
await Promise.allSettled(
jsDcm2Jpegs.map(async (j) =>
Dcm2JpgExecutor.convertDcmToJpgFromFilename(frame["instancePath"], j.jpegFilename, j.jsDcm2JpegOption)
)
);
jsDcm2Jpegs = [];
}
}

await Promise.allSettled(
jsDcm2Jpegs.map(async (j) =>
Dcm2JpgExecutor.convertDcmToJpgFromFilename(frame["instancePath"], j.jpegFilename, j.jsDcm2JpegOption)
)
);

return jpegs;
}
}

class FfmpegVideoConverter {
constructor() {
this.removeTempFolderEvent = new EventEmitter();
this.removeTempFolderEvent.on("start", (folder) => {
fsP.rm(folder, { recursive: true });
});
}

async convert() {
throw new Error("Method not implemented.");
}
}

class Mp4Converter extends FfmpegVideoConverter {
constructor() {
super();
}
async convert(folder, ffmpegInputData) {
return new Promise((resolve, reject) => {
ffmpeg(ffmpegInputData)
.inputOption("-f concat")
.inputOption("-safe 0")
.inputFPS(60)
.videoCodec("libx264")
.outputOption("-crf 25")
.save(`${folder}.mp4`)
.on("error", (err) => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return reject(err.message);
})
.on("end", () => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return resolve(`${folder}.mp4`);
});
});
}
}

class AviConverter extends FfmpegVideoConverter {
constructor() {
super();
}

async convert(folder, ffmpegInputData) {
return new Promise((resolve, reject) => {
ffmpeg(ffmpegInputData)
.inputOption("-f concat")
.inputOption("-safe 0")
.inputFPS(60)
.outputOption("-crf 25")
.save(`${folder}.avi`)
.on("error", (err) => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return reject(err.message);
})
.on("end", () => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return resolve(`${folder}.avi`);
});
});
}
}

class MpegConverter extends FfmpegVideoConverter {
constructor() {
super();
}

async convert(folder, ffmpegInputData) {
return new Promise((resolve, reject) => {
ffmpeg(ffmpegInputData)
.inputOption("-f concat")
.inputOption("-safe 0")
.inputFPS(60)
.outputOption("-crf 25")
.save(`${folder}.mpeg`)
.on("error", (err) => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return reject(err.message);
})
.on("end", () => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return resolve(`${folder}.mpeg`);
});
});
}
}

class H265Converter extends FfmpegVideoConverter {
constructor() {
super();
}

async convert(folder, ffmpegInputData) {
return new Promise((resolve, reject) => {
ffmpeg(ffmpegInputData)
.inputOption("-f concat")
.inputOption("-safe 0")
.inputFPS(60)
.videoCodec("libx265")
.outputOption("-crf 25")
.save(`${folder}.h265`)
.on("error", (err) => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return reject(err.message);
})
.on("end", () => {
fsP.unlink(ffmpegInputData);
this.removeTempFolderEvent.emit("start", folder);
return resolve(`${folder}.h265`);
});
});
}
}

class StudyVideoFactory extends VideoFactory {
constructor(mimeType, uids) {
super(mimeType, uids);
this.imagePathFactory = new StudyImagePathFactory(this.uids);
}

async convert() {
let instancesFrames = await this.getInstancesFrames();
let allJpegs = [];
for (let instanceFramesObj of instancesFrames) {
let jpegs = await this.generateJpegImage(instanceFramesObj, this.imageTempFolder);
allJpegs.push(...jpegs);
}

return await super.convert(this.imageTempFolder, allJpegs);
}

}

module.exports.StudyVideoFactory = StudyVideoFactory;
Loading

0 comments on commit 7b693d5

Please sign in to comment.