Mercurial > hg > orthanc-stone
changeset 2126:1a02e758eaf4 dicom-sr
integration mainline->dicom-sr
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 30 May 2024 17:07:30 +0200 |
parents | c1ed80be7138 (diff) 16c01cc201e7 (current diff) |
children | 6cc11bd11890 |
files | Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp OrthancStone/Sources/Toolbox/DicomStructuredReport.h |
diffstat | 2 files changed, 424 insertions(+), 32 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/configuration.json Thu May 30 17:00:29 2024 +0200 +++ b/Applications/StoneWebViewer/WebApplication/configuration.json Thu May 30 17:07:30 2024 +0200 @@ -120,7 +120,7 @@ /** * Define a list of modality type that the viewer will ignore. **/ - "SkipSeriesFromModalities": ["SR", "SEG", "PR"], + "SkipSeriesFromModalities": [ "SEG", "PR" ], /** * Whether to display the info panel at startup. Allowed values:
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Thu May 30 17:00:29 2024 +0200 +++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp Thu May 30 17:07:30 2024 +0200 @@ -82,6 +82,7 @@ #include "../../../OrthancStone/Sources/Scene2DViewport/ViewportController.h" #include "../../../OrthancStone/Sources/StoneException.h" #include "../../../OrthancStone/Sources/Toolbox/DicomInstanceParameters.h" +#include "../../../OrthancStone/Sources/Toolbox/DicomStructuredReport.h" #include "../../../OrthancStone/Sources/Toolbox/GeometryToolbox.h" #include "../../../OrthancStone/Sources/Toolbox/OsiriX/AngleAnnotation.h" #include "../../../OrthancStone/Sources/Toolbox/OsiriX/CollectionOfAnnotations.h" @@ -225,6 +226,13 @@ const OrthancStone::Vector& point, double maximumDistance) const = 0; + virtual OrthancStone::ISceneLayer* ExtractAnnotations(const std::string& sopInstanceUid, + unsigned int frameNumber, + double originX, + double originY, + double pixelSpacingX, + double pixelSpacingY) const = 0; + static OrthancStone::CoordinateSystem3D GetFrameGeometry(const IFramesCollection& frames, size_t frameIndex) { @@ -278,7 +286,239 @@ double maximumDistance) const ORTHANC_OVERRIDE { return frames_->FindClosestFrame(frameIndex, point, maximumDistance); + } + + virtual OrthancStone::ISceneLayer* ExtractAnnotations(const std::string& sopInstanceUid, + unsigned int frameNumber, + double originX, + double originY, + double pixelSpacingX, + double pixelSpacingY) const ORTHANC_OVERRIDE + { + return NULL; + } +}; + + +class DicomStructuredReportFrames : public IFramesCollection +{ +private: + class Frame : public boost::noncopyable + { + private: + OrthancStone::DicomStructuredReport::ReferencedFrame info_; + Orthanc::DicomMap tags_; + std::unique_ptr<OrthancStone::DicomInstanceParameters> parameters_; + + public: + Frame(const OrthancStone::DicomStructuredReport::ReferencedFrame& info, + const OrthancStone::LoadedDicomResources& instances) : + info_(info) + { + if (!instances.LookupResource(tags_, info.GetSopInstanceUid())) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem); + } + + parameters_.reset(new OrthancStone::DicomInstanceParameters(tags_)); + } + + const OrthancStone::DicomStructuredReport::ReferencedFrame& GetInformation() const + { + return info_; + } + + const Orthanc::DicomMap& GetTags() const + { + return tags_; + } + + const OrthancStone::DicomInstanceParameters& GetParameters() const + { + return *parameters_; + } }; + + std::unique_ptr<OrthancStone::DicomStructuredReport> sr_; + std::vector<Frame*> frames_; + + void Finalize() + { + for (size_t i = 0; i < frames_.size(); i++) + { + if (frames_[i] != NULL) + { + delete frames_[i]; + } + } + + frames_.clear(); + } + + const Frame& GetFrame(size_t index) const + { + if (index >= frames_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + else + { + assert(frames_[index] != NULL); + return *frames_[index]; + } + } + +public: + DicomStructuredReportFrames(const OrthancStone::DicomStructuredReport& sr, + const OrthancStone::LoadedDicomResources& instances) : + sr_(new OrthancStone::DicomStructuredReport(sr)) + { + std::list<OrthancStone::DicomStructuredReport::ReferencedFrame> tmp; + sr_->ExportReferencedFrames(tmp); + + frames_.reserve(tmp.size()); + for (std::list<OrthancStone::DicomStructuredReport::ReferencedFrame>::const_iterator + it = tmp.begin(); it != tmp.end(); ++it) + { + try + { + frames_.push_back(new Frame(*it, instances)); + } + catch (Orthanc::OrthancException&) + { + // An instance is not loaded yet + Finalize(); + throw; + } + } + } + + virtual ~DicomStructuredReportFrames() + { + Finalize(); + } + + virtual size_t GetFramesCount() const ORTHANC_OVERRIDE + { + return frames_.size(); + } + + virtual const OrthancStone::DicomInstanceParameters& GetInstanceOfFrame(size_t frameIndex) const ORTHANC_OVERRIDE + { + return GetFrame(frameIndex).GetParameters(); + } + + virtual unsigned int GetFrameNumberInInstance(size_t frameIndex) const ORTHANC_OVERRIDE + { + return GetFrame(frameIndex).GetInformation().GetFrameNumber(); + } + + virtual bool LookupFrame(size_t& frameIndex, + const std::string& sopInstanceUid, + unsigned int frameNumber) const ORTHANC_OVERRIDE + { + // TODO - Could be speeded up with an additional index + for (size_t i = 0; i < frames_.size(); i++) + { + if (frames_[i]->GetInformation().GetSopInstanceUid() == sopInstanceUid && + frames_[i]->GetInformation().GetFrameNumber() == frameNumber) + { + frameIndex = i; + return true; + } + } + + return false; + } + + virtual bool FindClosestFrame(size_t& frameIndex, + const OrthancStone::Vector& point, + double maximumDistance) const ORTHANC_OVERRIDE + { + bool found = false; + + for (size_t i = 0; i < GetFramesCount(); i++) + { + double distance = GetFrameGeometry(*this, i).ComputeDistance(point); + if (distance <= maximumDistance) + { + found = true; + frameIndex = i; + } + } + + return found; + } + + virtual OrthancStone::ISceneLayer* ExtractAnnotations(const std::string& sopInstanceUid, + unsigned int frameNumber, + double originX, + double originY, + double pixelSpacingX, + double pixelSpacingY) const ORTHANC_OVERRIDE + { + std::unique_ptr<OrthancStone::MacroSceneLayer> layer(new OrthancStone::MacroSceneLayer); + + const double x = originX - pixelSpacingX / 2.0; + const double y = originY - pixelSpacingY / 2.0; + + for (size_t i = 0; i < sr_->GetStructuresCount(); i++) + { + const OrthancStone::DicomStructuredReport::Structure& structure = sr_->GetStructure(i); + if (structure.GetSopInstanceUid() == sopInstanceUid && + (!structure.HasFrameNumber() || + structure.GetFrameNumber() == frameNumber)) + { + OrthancStone::Color color(0, 0, 255); + + if (structure.HasProbabilityOfCancer()) + { + if (structure.GetProbabilityOfCancer() > 50.0f) + { + color = OrthancStone::Color(255, 0, 0); + } + else + { + color = OrthancStone::Color(0, 255, 0); + } + } + + switch (structure.GetType()) + { + case OrthancStone::DicomStructuredReport::StructureType_Point: + // TODO + break; + + case OrthancStone::DicomStructuredReport::StructureType_Polyline: + { + const OrthancStone::DicomStructuredReport::Polyline& source = dynamic_cast<const OrthancStone::DicomStructuredReport::Polyline&>(structure); + + if (source.GetSize() > 1) + { + std::unique_ptr<OrthancStone::PolylineSceneLayer> target(new OrthancStone::PolylineSceneLayer); + + OrthancStone::PolylineSceneLayer::Chain chain; + chain.resize(source.GetSize()); + for (size_t i = 0; i < source.GetSize(); i++) + { + chain[i] = OrthancStone::ScenePoint2D(x + source.GetPoint(i).GetX() * pixelSpacingX, + y + source.GetPoint(i).GetY() * pixelSpacingY); + } + + target->AddChain(chain, false, color.GetRed(), color.GetGreen(), color.GetBlue()); + layer->AddLayer(target.release()); + } + break; + } + + default: + break; + } + } + } + + return layer.release(); + } }; @@ -410,6 +650,7 @@ size_t pending_; boost::shared_ptr<OrthancStone::LoadedDicomResources> studies_; boost::shared_ptr<OrthancStone::LoadedDicomResources> series_; + boost::shared_ptr<OrthancStone::LoadedDicomResources> instances_; boost::shared_ptr<OrthancStone::DicomResourcesLoader> resourcesLoader_; boost::shared_ptr<OrthancStone::SeriesThumbnailsLoader> thumbnailsLoader_; boost::shared_ptr<OrthancStone::SeriesMetadataLoader> metadataLoader_; @@ -417,13 +658,17 @@ VirtualSeries virtualSeries_; std::vector<std::string> skipSeriesFromModalities_; + typedef std::map<std::string, boost::shared_ptr<OrthancStone::DicomStructuredReport> > StructuredReports; + StructuredReports structuredReports_; + explicit ResourcesLoader(OrthancStone::ILoadersContext& context, const OrthancStone::DicomSource& source) : context_(context), source_(source), pending_(0), studies_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID)), - series_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID)) + series_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID)), + instances_(new OrthancStone::LoadedDicomResources(Orthanc::DICOM_TAG_SOP_INSTANCE_UID)) { } @@ -437,12 +682,12 @@ LOG(INFO) << "resources loaded: " << dicom.GetSize() << ", " << Orthanc::EnumerationToString(payload.GetValue()); - std::vector<std::string> seriesIdsToRemove; - if (payload.GetValue() == Orthanc::ResourceType_Series) { // the 'dicom' var is actually equivalent to the 'series_' member in this case + std::vector<std::string> seriesIdsToRemove; + for (size_t i = 0; i < dicom.GetSize(); i++) { std::string studyInstanceUid, seriesInstanceUid, modality; @@ -459,7 +704,6 @@ thumbnailsLoader_->ScheduleLoadThumbnail(source_, "", studyInstanceUid, seriesInstanceUid); metadataLoader_->ScheduleLoadSeries(PRIORITY_LOW + 1, source_, studyInstanceUid, seriesInstanceUid); } - else { seriesIdsToRemove.push_back(seriesInstanceUid); @@ -473,18 +717,28 @@ dicom.RemoveResource(seriesIdsToRemove[i]); } } - - if (pending_ == 0) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - else - { - pending_ --; - if (pending_ == 0 && - observer_.get() != NULL) + else if (payload.GetValue() == Orthanc::ResourceType_Instance) + { + // This occurs if loading DICOM-SR + + // TODO - Hide/show DICOM-SR once they have been loaded + } + + if (payload.GetValue() == Orthanc::ResourceType_Study || + payload.GetValue() == Orthanc::ResourceType_Series) + { + if (pending_ == 0) { - observer_->SignalResourcesLoaded(); + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + else + { + pending_ --; + if (pending_ == 0 && + observer_.get() != NULL) + { + observer_->SignalResourcesLoaded(); + } } } } @@ -500,6 +754,23 @@ void Handle(const OrthancStone::SeriesMetadataLoader::SuccessMessage& message) { + for (size_t i = 0; i < message.GetInstancesCount(); i++) + { + std::string sopInstanceUid, sopClassUid; + if (message.GetInstance(i).LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false) && + message.GetInstance(i).LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false) && + OrthancStone::StringToSopClassUid(sopClassUid) == OrthancStone::SopClassUid_ComprehensiveSR) + { + std::unique_ptr<OrthancStone::ILoadersContext::ILock> lock(context_.Lock()); + lock->Schedule( + GetSharedObserver(), PRIORITY_NORMAL, OrthancStone::ParseDicomFromWadoCommand::Create( + source_, message.GetStudyInstanceUid(), message.GetSeriesInstanceUid(), sopInstanceUid, + false /* no transcoding */, Orthanc::DicomTransferSyntax_LittleEndianExplicit /* dummy value */, + new InstanceInfo(message.GetStudyInstanceUid(), message.GetSeriesInstanceUid(), sopInstanceUid, Action_ComprehensiveSR))); + return; + } + } + if (observer_.get() != NULL) { observer_->SignalSeriesMetadataLoaded( @@ -545,19 +816,33 @@ pending_ += 2; } - - - class PdfInfo : public Orthanc::IDynamicObject + + + enum Action + { + Action_Pdf, + Action_ComprehensiveSR, + Action_ReferencedInstance + }; + + + class InstanceInfo : public Orthanc::IDynamicObject { private: std::string studyInstanceUid_; std::string seriesInstanceUid_; + std::string sopInstanceUid_; + Action action_; public: - PdfInfo(const std::string& studyInstanceUid, - const std::string& seriesInstanceUid) : + InstanceInfo(const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid, + Action action) : studyInstanceUid_(studyInstanceUid), - seriesInstanceUid_(seriesInstanceUid) + seriesInstanceUid_(seriesInstanceUid), + sopInstanceUid_(sopInstanceUid), + action_(action) { } @@ -570,23 +855,85 @@ { return seriesInstanceUid_; } + + const std::string& GetSopInstanceUid() const + { + return sopInstanceUid_; + } + + Action GetAction() const + { + return action_; + } }; void Handle(const OrthancStone::ParseDicomSuccessMessage& message) { - const PdfInfo& info = dynamic_cast<const PdfInfo&>(message.GetOrigin().GetPayload()); + const InstanceInfo& info = dynamic_cast<const InstanceInfo&>(message.GetOrigin().GetPayload()); if (observer_.get() != NULL) { - std::string pdf; - if (message.GetDicom().ExtractPdf(pdf)) + switch (info.GetAction()) { - observer_->SignalSeriesPdfLoaded(info.GetStudyInstanceUid(), info.GetSeriesInstanceUid(), pdf); - } - else - { - LOG(ERROR) << "Unable to extract PDF from series: " << info.GetSeriesInstanceUid(); + case Action_Pdf: + { + std::string pdf; + if (message.GetDicom().ExtractPdf(pdf)) + { + observer_->SignalSeriesPdfLoaded(info.GetStudyInstanceUid(), info.GetSeriesInstanceUid(), pdf); + } + else + { + LOG(ERROR) << "Unable to extract PDF from series: " << info.GetSeriesInstanceUid(); + } + + break; + } + + case Action_ComprehensiveSR: + { + try + { + boost::shared_ptr<OrthancStone::DicomStructuredReport> sr(new OrthancStone::DicomStructuredReport(message.GetDicom())); + + for (size_t i = 0; i < sr->GetReferencedInstancesCount(); i++) + { + std::string studyInstanceUid; + std::string seriesInstanceUid; + std::string sopInstanceUid; + std::string sopClassUid; + sr->GetReferencedInstance(studyInstanceUid, seriesInstanceUid, sopInstanceUid, sopClassUid, i); + + Orthanc::DicomMap filter; + filter.SetValue(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, studyInstanceUid, false); + filter.SetValue(Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, seriesInstanceUid, false); + filter.SetValue(Orthanc::DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid, false); + + std::set<Orthanc::DicomTag> tags; + + resourcesLoader_->ScheduleQido( + instances_, PRIORITY_NORMAL, source_, Orthanc::ResourceType_Instance, filter, tags, + new Orthanc::SingleValueObject<Orthanc::ResourceType>(Orthanc::ResourceType_Instance)); + } + + structuredReports_[info.GetSeriesInstanceUid()] = sr; + + if (observer_.get() != NULL) + { + observer_->SignalSeriesMetadataLoaded( + info.GetStudyInstanceUid(), info.GetSeriesInstanceUid()); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Cannot decode DICOM-SR: " << e.What(); + } + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } } } @@ -825,6 +1172,23 @@ if (accessor.IsComplete()) { + StructuredReports::const_iterator sr = structuredReports_.find(seriesInstanceUid); + if (sr != structuredReports_.end()) + { + assert(sr->second != NULL); + + try + { + return new DicomStructuredReportFrames(*sr->second, *instances_); + } + catch (Orthanc::OrthancException&) + { + // TODO + LOG(ERROR) << "Instances of the DICOM-SR are not available yet"; + return NULL; + } + } + std::unique_ptr<OrthancStone::SortedFrames> target(new OrthancStone::SortedFrames); target->Clear(); @@ -939,8 +1303,7 @@ GetSharedObserver(), PRIORITY_NORMAL, OrthancStone::ParseDicomFromWadoCommand::Create( source_, studyInstanceUid, seriesInstanceUid, sopInstanceUid, false /* no transcoding */, Orthanc::DicomTransferSyntax_LittleEndianExplicit /* dummy value */, - new PdfInfo(studyInstanceUid, seriesInstanceUid))); - + new InstanceInfo(studyInstanceUid, seriesInstanceUid, sopInstanceUid, Action_Pdf))); return; } } @@ -1688,6 +2051,7 @@ static const int LAYER_REFERENCE_LINES = 3; static const int LAYER_ANNOTATIONS_OSIRIX = 4; static const int LAYER_ANNOTATIONS_STONE = 5; + static const int LAYER_STRUCTURED_REPORT = 6; class ICommand : public Orthanc::IDynamicObject @@ -2372,6 +2736,16 @@ } + std::unique_ptr<OrthancStone::ISceneLayer> structuredReportAnnotations; + + if (frames_.get() != NULL) + { + structuredReportAnnotations.reset(frames_->ExtractAnnotations(instance.GetSopInstanceUid(), frameIndex, + layer->GetOriginX(), layer->GetOriginY(), + layer->GetPixelSpacingX(), layer->GetPixelSpacingY())); + } + + { std::unique_ptr<OrthancStone::IViewport::ILock> lock(viewport_->Lock()); @@ -2406,6 +2780,24 @@ scene.DeleteLayer(LAYER_ORIENTATION_MARKERS); } + if (orientationMarkers.get() != NULL) + { + scene.SetLayer(LAYER_ORIENTATION_MARKERS, orientationMarkers.release()); + } + else + { + scene.DeleteLayer(LAYER_ORIENTATION_MARKERS); + } + + if (structuredReportAnnotations.get() != NULL) + { + scene.SetLayer(LAYER_STRUCTURED_REPORT, structuredReportAnnotations.release()); + } + else + { + scene.DeleteLayer(LAYER_STRUCTURED_REPORT); + } + stoneAnnotations_->Render(scene); // Necessary for "FitContent()" to work if (fitNextContent_)