changeset 1672:570398585b5f

start support of cine sequences
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 23 Nov 2020 15:39:27 +0100
parents 2c2512918a0f
children dd50f8a1a2be
files Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 3 files changed, 240 insertions(+), 61 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/StoneWebViewer/WebApplication/app.js	Fri Nov 20 10:14:36 2020 +0100
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Mon Nov 23 15:39:27 2020 +0100
@@ -48,17 +48,39 @@
   data: function () {
     return {
       stone: stone,  // To access global object "stone" from "index.html"
-      status: 'waiting'
+      status: 'waiting',
+      cineControls: false,
+      cineIncrement: 0,
+      cineFramesPerSecond: 30,
+      cineTimeoutId: null,
+      cineLoadingFrame: false
     }
   },
-  watch: { 
+  watch: {
+    currentFrame: function(newVal, oldVal) {
+      /**
+       * The "FrameUpdated" event has been received, which indicates
+       * that the schedule frame has been displayed: The cine loop can
+       * proceed to the next frame (check out "CineCallback()").
+       **/
+      this.cineLoadingFrame = false;
+    },
     series: function(newVal, oldVal) {
       this.status = 'loading';
+      this.cineControls = false;
+      this.cineMode = '';
+      this.cineLoadingFrame = false;
+      this.cineRate = 30;   // Default value
+      
+      if (this.cineTimeoutId !== null) {
+        clearTimeout(this.cineTimeoutId);
+        this.cineTimeoutId = null;
+      }
 
       var studyInstanceUid = newVal.tags[STUDY_INSTANCE_UID];
       var seriesInstanceUid = newVal.tags[SERIES_INSTANCE_UID];
       stone.SpeedUpFetchSeriesMetadata(studyInstanceUid, seriesInstanceUid);
-      
+
       if ((newVal.type == stone.ThumbnailType.IMAGE ||
            newVal.type == stone.ThumbnailType.NO_PREVIEW) &&
           newVal.complete) {
@@ -96,7 +118,14 @@
   },
   mounted: function() {
     var that = this;
-    
+
+    window.addEventListener('SeriesDetailsReady', function(args) {
+      var canvasId = args.detail.canvasId;
+      if (canvasId == that.canvasId) {
+        that.cineFramesPerSecond = stone.GetCineRate(canvasId);
+      }
+    });
+
     window.addEventListener('PdfLoaded', function(args) {
       var studyInstanceUid = args.detail.studyInstanceUid;
       var seriesInstanceUid = args.detail.seriesInstanceUid;
@@ -137,11 +166,72 @@
     MakeActive: function() {
       this.$emit('selected-viewport');
     },
-    DecrementFrame: function() {
-      stone.DecrementFrame(this.canvasId);
+    DecrementFrame: function(isCircular) {
+      return stone.DecrementFrame(this.canvasId, isCircular);
+    },
+    IncrementFrame: function(isCircular) {
+      return stone.IncrementFrame(this.canvasId, isCircular);
+    },
+    CinePlay: function() {
+      this.cineControls = true;
+      this.cineIncrement = 1;
+      this.UpdateCine();
+    },
+    CinePause: function() {
+      if (this.cineIncrement == 0) {
+        // Two clicks on the "pause" button will hide the playback control
+        this.cineControls = !this.cineControls;
+      } else {
+        this.cineIncrement = 0;
+        this.UpdateCine();
+      }
+    },
+    CineBackward: function() {
+      this.cineControls = true;
+      this.cineIncrement = -1;
+      this.UpdateCine();
     },
-    IncrementFrame: function() {
-      stone.IncrementFrame(this.canvasId);
+    UpdateCine: function() {
+      // Cancel the previous cine loop, if any
+      if (this.cineTimeoutId !== null) {
+        clearTimeout(this.cineTimeoutId);
+        this.cineTimeoutId = null;
+      }
+      
+      this.cineLoadingFrame = false;
+
+      if (this.cineIncrement != 0) {
+        this.CineCallback();
+      }
+    },
+    CineCallback: function() {
+      var reschedule;
+      
+      if (this.cineLoadingFrame) {
+        /**
+         * Wait until the frame scheduled by the previous call to
+         * "CineCallback()" is actually displayed (i.e. we monitor the
+         * "FrameUpdated" event). Otherwise, the background loading
+         * process of the DICOM frames in C++ might be behind the
+         * advancement of the current frame, which freezes the
+         * display.
+         **/
+        reschedule = true;
+      } else {
+        this.cineLoadingFrame = true;
+        
+        if (this.cineIncrement == 1) {
+          reschedule = this.DecrementFrame(true /* circular */);
+        } else if (this.cineIncrement == -1) {
+          reschedule = this.IncrementFrame(true /* circular */);
+        } else {
+          reschedule = false;  // Increment is zero, this test is just for safety
+        }
+      }
+      
+      if (reschedule) {
+        this.cineTimeoutId = setTimeout(this.CineCallback, 1000.0 / this.cineFramesPerSecond);
+      }     
     }
   }
 });
--- a/Applications/StoneWebViewer/WebApplication/index.html	Fri Nov 20 10:14:36 2020 +0100
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Mon Nov 23 15:39:27 2020 +0100
@@ -556,28 +556,50 @@
                       style="position:absolute; left:0; top:0; width:100%; height:100%"
                       oncontextmenu="return false"></canvas>
 
-              <div v-if="'tags' in series" v-show="showInfo">
+              <div v-show="showInfo">
                 <div class="wv-overlay">
-                  <div class="wv-overlay-topleft">
+                  <div v-if="'tags' in series" class="wv-overlay-topleft">
                     {{ series.tags['0010,0010'] }}<br/>
                     {{ series.tags['0010,0020'] }}
                   </div>
-                  <div class="wv-overlay-topright">
+                  <div v-if="'tags' in series" class="wv-overlay-topright">
                     {{ series.tags['0008,1030'] }}<br/>
                     {{ series.tags['0008,0020'] }}<br/>
                     {{ series.tags['0020,0011'] }} | {{ series.tags['0008,103e'] }}
                   </div>
-                  <div class="wv-overlay-bottomleft wvPrintExclude"
-                       v-show="framesCount != 0">
-                    <button class="btn btn-primary" @click="DecrementFrame()">
-                      <i class="fa fa-chevron-circle-left"></i>
-                    </button>
-                    &nbsp;&nbsp;{{ currentFrame }} / {{ framesCount }}&nbsp;&nbsp;
-                    <button class="btn btn-primary" @click="IncrementFrame()">
-                      <i class="fa fa-chevron-circle-right"></i>
-                    </button>
+                  <div class="wv-overlay-timeline-wrapper wvPrintExclude">
+                    <div style="text-align:left; padding:5px" v-show="framesCount != 0">
+                      <div style="width: 12em; padding: 1em;" v-show="cineControls">
+                        <label>
+                          Frame rate
+                          <span class="wv-play-button-config-framerate-wrapper">
+                            <input type="range" min="1" max="60" v-model="cineFramesPerSecond"
+                                   class="wv-play-button-config-framerate">
+                          </span>
+                          {{ cineFramesPerSecond }} fps
+                        </label>
+                      </div>
+                      <button class="btn btn-primary btn-sm" @click="DecrementFrame()">
+                        <i class="fa fa-chevron-circle-left"></i>
+                      </button>
+                      &nbsp;&nbsp;{{ currentFrame }} / {{ framesCount }}&nbsp;&nbsp;
+                      <button class="btn btn-primary btn-sm" @click="IncrementFrame()">
+                        <i class="fa fa-chevron-circle-right"></i>
+                      </button>
+                      <div class="btn-group btn-group-sm" role="group">                        
+                        <button type="button" class="btn btn-primary" @click="CinePlay()">
+                          <i class="fas fa-play fa-flip-horizontal"></i>
+                        </button>
+                        <button type="button" class="btn btn-primary" @click="CinePause()">
+                          <i class="fas fa-pause"></i>
+                        </button>
+                        <button type="button" class="btn btn-primary" @click="CineBackward()">
+                          <i class="fas fa-play"></i>
+                        </button>
+                      </div>
+                    </div>
                   </div>
-                  <div class="wv-overlay-bottomright wvPrintExclude">
+                  <div class="wv-overlay-bottomright wvPrintExclude" style="bottom: 0px">
                     <div v-show="quality == stone.DisplayedFrameQuality.NONE"
                          style="display:block;background-color:red;width:1em;height:1em" />
                     <div v-show="quality == stone.DisplayedFrameQuality.LOW"
@@ -599,7 +621,7 @@
             </div>
                 
             <div v-if="status == 'video'" class="wvPaneOverlay">
-              [ this viewer cannot play videos ]
+              [ videos are not supported yet ]
               <!--video class="wvVideo" autoplay="" loop="" controls="" preload="auto" type="video/mp4"
                      src="http://viewer-pro.osimis.io/instances/e465dd27-83c96343-96848735-7035a133-1facf1a0/frames/0/raw">
               </video-->
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Fri Nov 20 10:14:36 2020 +0100
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Mon Nov 23 15:39:27 2020 +0100
@@ -167,6 +167,9 @@
 static const unsigned int QUALITY_JPEG = 0;
 static const unsigned int QUALITY_FULL = 1;
 
+static const unsigned int DEFAULT_CINE_RATE = 30;
+
+
 class ResourcesLoader : public OrthancStone::ObserverBase<ResourcesLoader>
 {
 public:
@@ -687,12 +690,13 @@
   std::vector<size_t>  prefetch_;
   int                  framesCount_;
   int                  currentFrame_;
-  bool                 isCircular_;
+  bool                 isCircularPrefetch_;
   int                  fastDelta_;
   Action               lastAction_;
 
   int ComputeNextFrame(int currentFrame,
-                       Action action) const
+                       Action action,
+                       bool isCircular) const
   {
     if (framesCount_ == 0)
     {
@@ -727,7 +731,7 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
     }
 
-    if (isCircular_)
+    if (isCircular)
     {
       while (nextFrame < 0)
       {
@@ -797,31 +801,31 @@
         {
           case Action_None:
           case Action_Plus:
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus));
             break;
           
           case Action_Minus:
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus));
             break;
 
           case Action_FastPlus:
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus));
             break;
               
           case Action_FastMinus:
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus), Action_FastMinus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus), Action_FastPlus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus), Action_Minus));
-            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus), Action_Plus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastMinus, isCircularPrefetch_), Action_FastMinus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_FastPlus, isCircularPrefetch_), Action_FastPlus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Minus, isCircularPrefetch_), Action_Minus));
+            queue.push_back(std::make_pair(ComputeNextFrame(frame, Action_Plus, isCircularPrefetch_), Action_Plus));
             break;
 
           default:
@@ -843,16 +847,16 @@
   explicit SeriesCursor(size_t framesCount) :
     framesCount_(framesCount),
     currentFrame_(framesCount / 2),  // Start at the middle frame    
-    isCircular_(false),
+    isCircularPrefetch_(false),
     lastAction_(Action_None)
   {
     SetFastDelta(framesCount / 20);
     UpdatePrefetch();
   }
 
-  void SetCircular(bool isCircular)
+  void SetCircularPrefetch(bool isCircularPrefetch)
   {
-    isCircular_ = isCircular;
+    isCircularPrefetch_ = isCircularPrefetch;
     UpdatePrefetch();
   }
 
@@ -886,9 +890,10 @@
     return static_cast<size_t>(currentFrame_);
   }
 
-  void Apply(Action action)
+  void Apply(Action action,
+             bool isCircular)
   {
-    currentFrame_ = ComputeNextFrame(currentFrame_, action);
+    currentFrame_ = ComputeNextFrame(currentFrame_, action, isCircular);
     lastAction_ = action;
     UpdatePrefetch();
   }
@@ -990,6 +995,8 @@
     {
     }
 
+    virtual void SignalSeriesDetailsReady(const ViewerViewport& viewport) = 0;
+
     virtual void SignalFrameUpdated(const ViewerViewport& viewport,
                                     size_t currentFrame,
                                     size_t countFrames,
@@ -1045,10 +1052,10 @@
     }
   };
 
-  class SetDefaultWindowingCommand : public ICommand
+  class LoadSeriesDetailsFromInstance : public ICommand
   {
   public:
-    explicit SetDefaultWindowingCommand(boost::shared_ptr<ViewerViewport> viewport) :
+    explicit LoadSeriesDetailsFromInstance(boost::shared_ptr<ViewerViewport> viewport) :
       ICommand(viewport)
     {
     }
@@ -1082,7 +1089,28 @@
         }
       }
 
+      uint32_t cineRate;
+      if (dicom.ParseUnsignedInteger32(cineRate, Orthanc::DICOM_TAG_CINE_RATE) &&
+          cineRate > 0)
+      {
+        /**
+         * If we detect a cine sequence, start on the first frame
+         * instead of on the middle frame.
+         **/
+        GetViewport().cursor_->SetCurrentIndex(0);
+        GetViewport().cineRate_ = cineRate;
+      }
+      else
+      {
+        GetViewport().cineRate_ = DEFAULT_CINE_RATE;
+      }
+
       GetViewport().Redraw();
+
+      if (GetViewport().observer_.get() != NULL)
+      {
+        GetViewport().observer_->SignalSeriesDetailsReady(GetViewport());
+      }
     }
   };
 
@@ -1297,6 +1325,7 @@
   float                                        windowingWidth_;
   float                                        defaultWindowingCenter_;
   float                                        defaultWindowingWidth_;
+  unsigned int                                 cineRate_;
   bool                                         inverted_;
   bool                                         flipX_;
   bool                                         flipY_;
@@ -1719,11 +1748,11 @@
     {
       if (wheelEvent->deltaY < 0)
       {
-        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus);
+        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastMinus : SeriesCursor::Action_Minus, false /* not circular */);
       }
       else if (wheelEvent->deltaY > 0)
       {
-        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus);
+        that.ChangeFrame(that.isCtrlDown_ ? SeriesCursor::Action_FastPlus : SeriesCursor::Action_Plus, false /* not circular */);
       }
     }
     
@@ -1788,7 +1817,8 @@
     flipX_ = false;
     flipY_ = false;
     fitNextContent_ = true;
-
+    cineRate_ = DEFAULT_CINE_RATE;
+    
     frames_.reset(frames);
     cursor_.reset(new SeriesCursor(frames_->GetFramesCount()));
 
@@ -1821,14 +1851,14 @@
           uid != OrthancStone::SopClassUid_RTStruct &&
           GetSeriesThumbnailType(uid) != OrthancStone::SeriesThumbnailType_Video)
       {
-        // Fetch the default windowing for the central instance
+        // Fetch the details of the series from the central instance
         const std::string uri = ("studies/" + frames_->GetStudyInstanceUid() +
                                  "/series/" + frames_->GetSeriesInstanceUid() +
                                  "/instances/" + centralInstance.GetSopInstanceUid() + "/metadata");
         
         loader_->ScheduleGetDicomWeb(
           boost::make_shared<OrthancStone::LoadedDicomResources>(Orthanc::DICOM_TAG_SOP_INSTANCE_UID),
-          0, source_, uri, new SetDefaultWindowingCommand(GetSharedObserver()));
+          0, source_, uri, new LoadSeriesDetailsFromInstance(GetSharedObserver()));
       }
     }
 
@@ -1911,21 +1941,27 @@
   }
 
 
-  void ChangeFrame(SeriesCursor::Action action)
+  // Returns "true" iff the frame has indeed changed
+  bool ChangeFrame(SeriesCursor::Action action,
+                   bool isCircular)
   {
     if (cursor_.get() != NULL)
     {
       size_t previous = cursor_->GetCurrentIndex();
       
-      cursor_->Apply(action);
+      cursor_->Apply(action, isCircular);
       
       size_t current = cursor_->GetCurrentIndex();
       if (previous != current)
       {
         Redraw();
+        return true;
       }
     }
+
+    return false;
   }
+  
 
   bool GetCurrentFrameOfReferenceUid(std::string& frameOfReferenceUid) const
   {
@@ -2213,6 +2249,11 @@
       Redraw();
     }
   }
+
+  unsigned int GetCineRate() const
+  {
+    return cineRate_;
+  }
 };
 
 
@@ -2297,6 +2338,18 @@
     }
   }
 
+  virtual void SignalSeriesDetailsReady(const ViewerViewport& viewport) ORTHANC_OVERRIDE
+  {
+    EM_ASM({
+        const customEvent = document.createEvent("CustomEvent");
+        customEvent.initCustomEvent("SeriesDetailsReady", false, false,
+                                    { "canvasId" : UTF8ToString($0) });
+        window.dispatchEvent(customEvent);
+      },
+      viewport.GetCanvasId().c_str()
+      );
+  }
+
   virtual void SignalFrameUpdated(const ViewerViewport& viewport,
                                   size_t currentFrame,
                                   size_t countFrames,
@@ -2654,26 +2707,28 @@
 
 
   EMSCRIPTEN_KEEPALIVE
-  void DecrementFrame(const char* canvas,
-                      int fitContent)
+  int DecrementFrame(const char* canvas,
+                     int isCircular)
   {
     try
     {
-      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus);
+      return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Minus, isCircular) ? 1 : 0;
     }
     EXTERN_CATCH_EXCEPTIONS;
+    return 0;
   }
 
 
   EMSCRIPTEN_KEEPALIVE
-  void IncrementFrame(const char* canvas,
-                      int fitContent)
+  int IncrementFrame(const char* canvas,
+                     int isCircular)
   {
     try
     {
-      GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus);
+      return GetViewport(canvas)->ChangeFrame(SeriesCursor::Action_Plus, isCircular) ? 1 : 0;
     }
     EXTERN_CATCH_EXCEPTIONS;
+    return 0;
   }  
 
 
@@ -2866,4 +2921,16 @@
     }
     EXTERN_CATCH_EXCEPTIONS;
   }
+
+
+  EMSCRIPTEN_KEEPALIVE
+  unsigned int GetCineRate(const char* canvas)
+  {
+    try
+    {
+      return GetViewport(canvas)->GetCineRate();
+    }
+    EXTERN_CATCH_EXCEPTIONS;
+    return 0;
+  }
 }