# HG changeset patch # User Sebastien Jodogne # Date 1717081650 -7200 # Node ID 1a02e758eaf48d9a832fd787e85eaef27b885b4e # Parent c1ed80be71380940ddc776a0a896e27966828eb8# Parent 16c01cc201e70d9636bc8aba4eb4b41b3f046580 integration mainline->dicom-sr diff -r 16c01cc201e7 -r 1a02e758eaf4 Applications/StoneWebViewer/WebApplication/configuration.json --- 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: diff -r 16c01cc201e7 -r 1a02e758eaf4 Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp --- 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 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 sr_; + std::vector 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 tmp; + sr_->ExportReferencedFrames(tmp); + + frames_.reserve(tmp.size()); + for (std::list::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 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(structure); + + if (source.GetSize() > 1) + { + std::unique_ptr 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 studies_; boost::shared_ptr series_; + boost::shared_ptr instances_; boost::shared_ptr resourcesLoader_; boost::shared_ptr thumbnailsLoader_; boost::shared_ptr metadataLoader_; @@ -417,13 +658,17 @@ VirtualSeries virtualSeries_; std::vector skipSeriesFromModalities_; + typedef std::map > 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 seriesIdsToRemove; - if (payload.GetValue() == Orthanc::ResourceType_Series) { // the 'dicom' var is actually equivalent to the 'series_' member in this case + std::vector 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 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(message.GetOrigin().GetPayload()); + const InstanceInfo& info = dynamic_cast(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 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 tags; + + resourcesLoader_->ScheduleQido( + instances_, PRIORITY_NORMAL, source_, Orthanc::ResourceType_Instance, filter, tags, + new Orthanc::SingleValueObject(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 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 structuredReportAnnotations; + + if (frames_.get() != NULL) + { + structuredReportAnnotations.reset(frames_->ExtractAnnotations(instance.GetSopInstanceUid(), frameIndex, + layer->GetOriginX(), layer->GetOriginY(), + layer->GetPixelSpacingX(), layer->GetPixelSpacingY())); + } + + { std::unique_ptr 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_) diff -r 16c01cc201e7 -r 1a02e758eaf4 OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp diff -r 16c01cc201e7 -r 1a02e758eaf4 OrthancStone/Sources/Toolbox/DicomStructuredReport.h