Mercurial > hg > orthanc-stone
changeset 1703:76c590a62755
start work on series with multiple multiframe instances
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 27 Nov 2020 16:36:43 +0100 |
parents | bc40b6450261 |
children | 902d13889ae4 |
files | Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Sources/Loaders/SeriesThumbnailsLoader.h |
diffstat | 4 files changed, 251 insertions(+), 68 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/app.js Fri Nov 27 13:57:28 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/app.js Fri Nov 27 16:36:43 2020 +0100 @@ -224,10 +224,7 @@ }); }, methods: { - SeriesDragAccept: function(event) { - event.preventDefault(); - }, - SeriesDragDrop: function(event) { + DragDrop: function(event) { event.preventDefault(); // The "parseInt()" is because of Microsoft Edge Legacy (*) @@ -368,7 +365,8 @@ selectedStudies: [], series: [], studies: [], - seriesIndex: {} // Maps "SeriesInstanceUID" to "index in this.series" + seriesIndex: {}, // Maps "SeriesInstanceUID" to "index in this.series" + multiframeInstanceThumbnails: {} } }, computed: { @@ -502,7 +500,8 @@ 'complete' : false, 'type' : stone.ThumbnailType.LOADING, 'color': study.color, - 'tags': sourceSeries[i] + 'tags': sourceSeries[i], + 'multiframeInstances': null }); } } @@ -665,6 +664,10 @@ stone.FetchPdf(studyInstanceUid, seriesInstanceUid); delete pendingSeriesPdf_[seriesInstanceUid]; } + + if (stone.LoadMultiframeInstancesFromSeries(seriesInstanceUid)) { + series.multiframeInstances = JSON.parse(stone.GetStringBuffer()); + } } // https://fr.vuejs.org/2016/02/06/common-gotchas/#Why-isn%E2%80%99t-the-DOM-updating @@ -828,6 +831,24 @@ } this.modalNotDiagnostic = this.settingNotDiagnostic; + + var that = this; + + window.addEventListener('MultiframeInstanceThumbnailLoaded', function(args) { + that.$set(that.multiframeInstanceThumbnails, args.detail.sopInstanceUid, args.detail.thumbnail); + }); + + window.addEventListener('ThumbnailLoaded', function(args) { + //var studyInstanceUid = args.detail.studyInstanceUid; + var seriesInstanceUid = args.detail.seriesInstanceUid; + that.UpdateSeriesThumbnail(seriesInstanceUid); + }); + + window.addEventListener('MetadataLoaded', function(args) { + var studyInstanceUid = args.detail.studyInstanceUid; + var seriesInstanceUid = args.detail.seriesInstanceUid; + that.UpdateIsSeriesComplete(studyInstanceUid, seriesInstanceUid); + }); } }); @@ -916,20 +937,6 @@ }); -window.addEventListener('ThumbnailLoaded', function(args) { - //var studyInstanceUid = args.detail.studyInstanceUid; - var seriesInstanceUid = args.detail.seriesInstanceUid; - app.UpdateSeriesThumbnail(seriesInstanceUid); -}); - - -window.addEventListener('MetadataLoaded', function(args) { - var studyInstanceUid = args.detail.studyInstanceUid; - var seriesInstanceUid = args.detail.seriesInstanceUid; - app.UpdateIsSeriesComplete(studyInstanceUid, seriesInstanceUid); -}); - - window.addEventListener('StoneException', function() { console.error('Exception catched in Stone'); });
--- a/Applications/StoneWebViewer/WebApplication/index.html Fri Nov 27 13:57:28 2020 +0100 +++ b/Applications/StoneWebViewer/WebApplication/index.html Fri Nov 27 16:36:43 2020 +0100 @@ -187,57 +187,93 @@ <div class="wvStudyIsland__main"> <ul class="wvSerieslist"> - <li class="wvSerieslist__seriesItem" - v-bind:class="{ highlighted : GetActiveSeries().includes(series[seriesIndex].tags['0020,000e']), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" - v-on:dragstart="SeriesDragStart($event, seriesIndex)" - v-on:click="ClickSeries(seriesIndex)" - v-for="seriesIndex in study.series"> - <div class="wvSerieslist__picture" style="z-index:0" - draggable="true" - v-if="series[seriesIndex].type != stone.ThumbnailType.UNKNOWN" - > - <div v-if="series[seriesIndex].type == stone.ThumbnailType.LOADING"> - <img src="img/loading.gif" - style="vertical-align:baseline" - width="65px" height="65px" - /> + + <!-- Series without multiple multiframe instances --> + <span v-for="seriesIndex in study.series"> + <li class="wvSerieslist__seriesItem" + v-bind:class="{ highlighted : GetActiveSeries().includes(series[seriesIndex].tags['0020,000e']), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" + v-on:dragstart="SeriesDragStart($event, seriesIndex)" + v-on:click="ClickSeries(seriesIndex)" + v-if="series[seriesIndex].multiframeInstances === null"> + <div class="wvSerieslist__picture" style="z-index:0" + draggable="true" + v-if="series[seriesIndex].type != stone.ThumbnailType.UNKNOWN" + > + <div v-if="series[seriesIndex].type == stone.ThumbnailType.LOADING"> + <img src="img/loading.gif" + style="vertical-align:baseline" + width="65px" height="65px" + /> + </div> + + <i v-if="series[seriesIndex].type == stone.ThumbnailType.PDF" + class="wvSerieslist__placeholderIcon fa fa-file-pdf"></i> + + <i v-if="series[seriesIndex].type == stone.ThumbnailType.VIDEO" + class="wvSerieslist__placeholderIcon fa fa-video-video"></i> + + <div v-if="[stone.ThumbnailType.IMAGE, stone.ThumbnailType.NO_PREVIEW].includes(series[seriesIndex].type)" + class="wvSerieslist__placeholderIcon" + v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']"> + <i v-if="series[seriesIndex].type == stone.ThumbnailType.NO_PREVIEW" + class="fa fa-eye-slash"></i> + + <img v-if="series[seriesIndex].type == stone.ThumbnailType.IMAGE" + v-bind:src="series[seriesIndex].thumbnail" + style="vertical-align:baseline" + width="65px" height="65px" + v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']" + /> + + <div v-bind:class="'wvSerieslist__badge--' + study.color" + v-if="series[seriesIndex].numberOfFrames != 0">{{ series[seriesIndex].numberOfFrames }}</div> + </div> </div> - <i v-if="series[seriesIndex].type == stone.ThumbnailType.PDF" - class="wvSerieslist__placeholderIcon fa fa-file-pdf"></i> - - <i v-if="series[seriesIndex].type == stone.ThumbnailType.VIDEO" - class="wvSerieslist__placeholderIcon fa fa-video-video"></i> + <div v-if="leftMode == 'full'" class="wvSerieslist__information" + draggable="true" + v-on:dragstart="SeriesDragStart($event, seriesIndex)" + v-on:click="ClickSeries(seriesIndex)"> + <p class="wvSerieslist__label"> + [{{ series[seriesIndex].tags['0008,0060'] }}] + {{ series[seriesIndex].tags['0008,103e'] }} + </p> + </div> + </li> - - <div v-if="[stone.ThumbnailType.IMAGE, stone.ThumbnailType.NO_PREVIEW].includes(series[seriesIndex].type)" - class="wvSerieslist__placeholderIcon" - v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']"> - <i v-if="series[seriesIndex].type == stone.ThumbnailType.NO_PREVIEW" - class="fa fa-eye-slash"></i> + <!-- Series with multiple multiframe instances (CINE) --> + <li class="wvSerieslist__seriesItem" + v-bind:class="{ highlighted : GetActiveSeries().includes(series[seriesIndex].tags['0020,000e']), 'wvSerieslist__seriesItem--list' : leftMode != 'grid', 'wvSerieslist__seriesItem--grid' : leftMode == 'grid' }" + v-on:dragstart="SeriesDragStart($event, seriesIndex)" + v-on:click="ClickSeries(seriesIndex)" + v-for="(numberOfFrames, sopInstanceUid) in series[seriesIndex].multiframeInstances"> + <div class="wvSerieslist__picture" style="z-index:0" + draggable="true"> <img v-if="series[seriesIndex].type == stone.ThumbnailType.IMAGE" - v-bind:src="series[seriesIndex].thumbnail" + v-bind:src="sopInstanceUid in multiframeInstanceThumbnails ? multiframeInstanceThumbnails[sopInstanceUid] : series[seriesIndex].thumbnail" style="vertical-align:baseline" width="65px" height="65px" v-bind:title="leftMode == 'full' ? null : '[' + series[seriesIndex].tags['0008,0060'] + '] ' + series[seriesIndex].tags['0008,103e']" /> - <div v-bind:class="'wvSerieslist__badge--' + study.color" - v-if="series[seriesIndex].numberOfFrames != 0">{{ series[seriesIndex].numberOfFrames }}</div> + <div v-bind:class="'wvSerieslist__badge--' + study.color"> + {{ numberOfFrames }} + </div> </div> - </div> - <div v-if="leftMode == 'full'" class="wvSerieslist__information" - draggable="true" - v-on:dragstart="SeriesDragStart($event, seriesIndex)" - v-on:click="ClickSeries(seriesIndex)"> - <p class="wvSerieslist__label"> - [{{ series[seriesIndex].tags['0008,0060'] }}] - {{ series[seriesIndex].tags['0008,103e'] }} - </p> - </div> - </li> + <div v-if="leftMode == 'full'" class="wvSerieslist__information" + draggable="true" + v-on:dragstart="SeriesDragStart($event, seriesIndex)" + v-on:click="ClickSeries(seriesIndex)"> + <p class="wvSerieslist__label"> + [{{ series[seriesIndex].tags['0008,0060'] }}] + {{ series[seriesIndex].tags['0008,103e'] }} + </p> + </div> + </li> + + </span> </ul> </div> </div> @@ -550,8 +586,8 @@ 'wvSplitpane__cellBorder--yellow' : series.color == 'yellow', 'wvSplitpane__cellBorder--violet' : series.color == 'violet' }" - v-on:dragover="SeriesDragAccept($event)" - v-on:drop="SeriesDragDrop($event)" + ondragover="event.preventDefault()" + v-on:drop="DragDrop($event)" style="width:100%;height:100%"> <div class="wvSplitpane__cell" v-on:click="MakeActive()">
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Nov 27 13:57:28 2020 +0100 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Fri Nov 27 16:36:43 2020 +0100 @@ -195,6 +195,9 @@ virtual void SignalSeriesPdfLoaded(const std::string& studyInstanceUid, const std::string& seriesInstanceUid, const std::string& pdf) = 0; + + virtual void SignalMultiframeInstanceThumbnailLoaded(const std::string& sopInstanceUid, + const std::string& jpeg) = 0; }; private: @@ -207,6 +210,7 @@ boost::shared_ptr<OrthancStone::DicomResourcesLoader> resourcesLoader_; boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader> thumbnailsLoader_; boost::shared_ptr<OrthancStone::SeriesMetadataLoader> metadataLoader_; + std::set<std::string> scheduledMultiframeInstances_; explicit ResourcesLoader(OrthancStone::ILoadersContext& context, const OrthancStone::DicomSource& source) : @@ -361,6 +365,44 @@ } } + void FetchInstanceThumbnail(const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) + { + if (scheduledMultiframeInstances_.find(sopInstanceUid) == scheduledMultiframeInstances_.end()) + { + scheduledMultiframeInstances_.insert(sopInstanceUid); + + std::map<std::string, std::string> arguments; + std::map<std::string, std::string> headers; + arguments["viewport"] = ( + boost::lexical_cast<std::string>(thumbnailsLoader_->GetThumbnailWidth()) + "," + + boost::lexical_cast<std::string>(thumbnailsLoader_->GetThumbnailHeight())); + headers["Accept"] = Orthanc::MIME_JPEG; + + const std::string uri = ("studies/" + studyInstanceUid + "/series/" + seriesInstanceUid + + "/instances/" + sopInstanceUid + "/frames/1/rendered"); + + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule( + GetSharedObserver(), PRIORITY_LOW + 2, source_.CreateDicomWebCommand( + uri, arguments, headers, new Orthanc::SingleValueObject<std::string>(sopInstanceUid))); + } + } + } + + void HandleInstanceThumbnail(const OrthancStone::HttpCommand::SuccessMessage& message) + { + if (observer_.get() != NULL) + { + const std::string& sopInstanceUid = + dynamic_cast<const Orthanc::SingleValueObject<std::string>&>( + message.GetOrigin().GetPayload()).GetValue(); + observer_->SignalMultiframeInstanceThumbnailLoaded(sopInstanceUid, message.GetAnswer()); + } + } + public: static boost::shared_ptr<ResourcesLoader> Create(OrthancStone::ILoadersContext::ILock& lock, const OrthancStone::DicomSource& source) @@ -382,7 +424,10 @@ loader->Register<OrthancStone::ParseDicomSuccessMessage>( lock.GetOracleObservable(), &ResourcesLoader::Handle); - + + loader->Register<OrthancStone::HttpCommand::SuccessMessage>( + lock.GetOracleObservable(), &ResourcesLoader::HandleInstanceThumbnail); + return loader; } @@ -428,13 +473,13 @@ } void GetStudy(Orthanc::DicomMap& target, - size_t i) + size_t i) const { target.Assign(studies_->GetResource(i)); } void GetSeries(Orthanc::DicomMap& target, - size_t i) + size_t i) const { target.Assign(series_->GetResource(i)); @@ -449,26 +494,65 @@ OrthancStone::SeriesThumbnailType GetSeriesThumbnail(std::string& image, std::string& mime, - const std::string& seriesInstanceUid) + const std::string& seriesInstanceUid) const { return thumbnailsLoader_->GetSeriesThumbnail(image, mime, seriesInstanceUid); } void FetchSeriesMetadata(int priority, const std::string& studyInstanceUid, - const std::string& seriesInstanceUid) + const std::string& seriesInstanceUid) const { metadataLoader_->ScheduleLoadSeries(priority, source_, studyInstanceUid, seriesInstanceUid); } - bool IsSeriesComplete(const std::string& seriesInstanceUid) + bool IsSeriesComplete(const std::string& seriesInstanceUid) const { OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid); return accessor.IsComplete(); } + bool LookupMultiframeSeries(std::map<std::string, unsigned int>& numberOfFramesPerInstance, + const std::string& seriesInstanceUid) + { + numberOfFramesPerInstance.clear(); + + OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid); + if (accessor.IsComplete() && + accessor.GetInstancesCount() >= 2) + { + bool isMultiframe = false; + + for (size_t i = 0; i < accessor.GetInstancesCount(); i++) + { + OrthancStone::DicomInstanceParameters p(accessor.GetInstance(i)); + numberOfFramesPerInstance[p.GetSopInstanceUid()] = p.GetNumberOfFrames(); + + if (p.GetNumberOfFrames() > 1) + { + isMultiframe = true; + } + } + + if (isMultiframe) + { + for (size_t i = 0; i < accessor.GetInstancesCount(); i++) + { + OrthancStone::DicomInstanceParameters p(accessor.GetInstance(i)); + FetchInstanceThumbnail(p.GetStudyInstanceUid(), p.GetSeriesInstanceUid(), p.GetSopInstanceUid()); + } + } + + return isMultiframe; + } + else + { + return false; + } + } + bool SortSeriesFrames(OrthancStone::SortedFrames& target, - const std::string& seriesInstanceUid) + const std::string& seriesInstanceUid) const { OrthancStone::SeriesMetadataLoader::Accessor accessor(*metadataLoader_, seriesInstanceUid); @@ -2674,6 +2758,24 @@ pdf.empty() ? 0 : reinterpret_cast<intptr_t>(pdf.c_str()), // Explicit conversion to an integer pdf.size()); } + + + virtual void SignalMultiframeInstanceThumbnailLoaded(const std::string& sopInstanceUid, + const std::string& jpeg) ORTHANC_OVERRIDE + { + std::string dataUriScheme; + Orthanc::Toolbox::EncodeDataUriScheme(dataUriScheme, "image/jpeg", jpeg); + + EM_ASM({ + const customEvent = document.createEvent("CustomEvent"); + customEvent.initCustomEvent("MultiframeInstanceThumbnailLoaded", false, false, + { "sopInstanceUid" : UTF8ToString($0), + "thumbnail" : UTF8ToString($1) }); + window.dispatchEvent(customEvent); + }, + sopInstanceUid.c_str(), + dataUriScheme.c_str()); + } }; @@ -3246,4 +3348,32 @@ } EXTERN_CATCH_EXCEPTIONS; } + + + EMSCRIPTEN_KEEPALIVE + int LoadMultiframeInstancesFromSeries(const char* seriesInstanceUid) + { + try + { + std::map<std::string, unsigned int> numberOfFramesPerInstance; + if (GetResourcesLoader().LookupMultiframeSeries(numberOfFramesPerInstance, seriesInstanceUid)) + { + Json::Value json = Json::objectValue; + for (std::map<std::string, unsigned int>::const_iterator it = + numberOfFramesPerInstance.begin(); it != numberOfFramesPerInstance.end(); ++it) + { + json[it->first] = it->second; + } + + stringBuffer_ = json.toStyledString(); + return true; + } + else + { + return false; + } + } + EXTERN_CATCH_EXCEPTIONS; + return false; + } }
--- a/OrthancStone/Sources/Loaders/SeriesThumbnailsLoader.h Fri Nov 27 13:57:28 2020 +0100 +++ b/OrthancStone/Sources/Loaders/SeriesThumbnailsLoader.h Fri Nov 27 16:36:43 2020 +0100 @@ -213,6 +213,16 @@ unsigned int height); void Clear(); + + unsigned int GetThumbnailWidth() const + { + return width_; + } + + unsigned int GetThumbnailHeight() const + { + return height_; + } SeriesThumbnailType GetSeriesThumbnail(std::string& image, std::string& mime,