]> Creatis software - creaImageIO.git/blob - src/creaImageIOMultiThreadImageReader.cpp
Clean Code
[creaImageIO.git] / src / creaImageIOMultiThreadImageReader.cpp
1 /*
2 # ---------------------------------------------------------------------
3 #
4 # Copyright (c) CREATIS (Centre de Recherche en Acquisition et Traitement de l'Image 
5 #                        pour la Santé)
6 # Authors : Eduardo Davila, Frederic Cervenansky, Claire Mouton
7 # Previous Authors : Laurent Guigues, Jean-Pierre Roux
8 # CreaTools website : www.creatis.insa-lyon.fr/site/fr/creatools_accueil
9 #
10 #  This software is governed by the CeCILL-B license under French law and 
11 #  abiding by the rules of distribution of free software. You can  use, 
12 #  modify and/ or redistribute the software under the terms of the CeCILL-B 
13 #  license as circulated by CEA, CNRS and INRIA at the following URL 
14 #  http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.html 
15 #  or in the file LICENSE.txt.
16 #
17 #  As a counterpart to the access to the source code and  rights to copy,
18 #  modify and redistribute granted by the license, users are provided only
19 #  with a limited warranty  and the software's author,  the holder of the
20 #  economic rights,  and the successive licensors  have only  limited
21 #  liability. 
22 #
23 #  The fact that you are presently reading this means that you have had
24 #  knowledge of the CeCILL-B license and that you accept its terms.
25 # ------------------------------------------------------------------------
26 */
27
28
29 #include <vtkVersionMacros.h>
30 #include <creaImageIOMultiThreadImageReader.h>
31 #include <creaImageIOImageReader.h>
32 #include <wx/utils.h>
33 #include <creaImageIOSystem.h>
34
35         #include <creaImageIODicomImageReader.h>
36
37
38 #include <creaImageIOGimmick.h>
39 #ifdef _DEBUG
40 #define new DEBUG_NEW
41 #endif
42 namespace creaImageIO
43 {
44
45   //=====================================================================
46   void MultiThreadImageReaderUser::MultiThreadImageReaderSendEvent
47   ( const std::string& filename,
48     EventType type,
49     vtkImageData* image)
50   {
51     wxMutexLocker lock(mMultiThreadImageReaderUserMutex);
52     this->OnMultiThreadImageReaderEvent(filename,type,image);
53   }
54   //=====================================================================
55
56   class wxThreadEED2
57   { 
58   public:
59           void Delete() {  printf("EED wxThreadEED2::Delete() \n"); }
60           int Run() { printf("EED wxThreadEED2::Run() \n"); return 0;}
61           void Pause() { printf("EED wxThreadEED2::Pause() \n"); }
62           void Create() { printf("EED wxThreadEED2::Create() \n"); }
63           bool IsAlive() { printf("EED wxThreadEED2::IsAlive() \n"); return false; }
64           bool TestDestroy() { printf("EED wxThreadEED2::TestDestroy() \n"); return false; }
65           int GetCurrentId() { printf("EED wxThreadEED2::GetCurrentId() \n"); return -999; }      
66   };
67   
68   //=====================================================================
69 //EED 2018-08-20
70 //  class ThreadedImageReader: public wxThread
71   class ThreadedImageReader: public wxThreadEED2
72   {
73   public:
74     ThreadedImageReader(MultiThreadImageReader* tir) :
75       mMultiThreadImageReader(tir)
76     {}
77     void* Entry();
78     void  OnExit();
79     vtkImageData* Read(const std::string& filename);
80         struct deleter
81         {
82                 void operator()(ThreadedImageReader* p)
83                 {
84                         p->Delete();
85                 }
86         };
87         friend struct deleter;
88
89   private:
90     ImageReader mReader;
91     MultiThreadImageReader* mMultiThreadImageReader;
92   };
93
94   //=====================================================================
95
96   
97   //=====================================================================
98   MultiThreadImageReader::MultiThreadImageReader(int number_of_threads)
99     : //mDoNotSignal(false),
100       mReader(NULL),
101       mTotalMem(0),
102       mTotalMemMax(1000000)
103   {
104     //    std::cout << "#### MultiThreadImageReader::MultiThreadImageReader("
105     //        << " #threads= " << number_of_threads <<" )"<<std::endl;
106
107           mDone = false;
108     // Create the threads
109         
110     for (int i=0; i<number_of_threads; i++) 
111     {
112                 //ThreadedImageReader* t = new ThreadedImageReader(this);
113                 boost::shared_ptr<ThreadedImageReader> t(new ThreadedImageReader(this), ThreadedImageReader::deleter());
114                 mThreadedImageReaderList.push_back(t);
115                 std::cout << "  ===> Thread "<<i <<" successfully added"<< std::endl;
116       } // for
117     mNumberOfThreadedReadersRunning = 0;
118     // Init the queue
119     mQueue.set(mComparator);
120     mQueue.set(mIndexer);
121     // 
122     // no thread : alloc self reader
123 //    if (number_of_threads==0)
124 //      {
125         mReader = new ImageReader();
126 //      }
127   }
128   //=====================================================================
129
130
131   //=====================================================================
132   bool MultiThreadImageReader::Start()
133   {
134     //    std::cout << "#### MultiThreadImageReader::Start()"
135     //                <<std::endl;
136           if (mNumberOfThreadedReadersRunning > 0) return true;
137           
138     ThreadedImageReaderListType::iterator i;
139     for (i =mThreadedImageReaderList.begin();
140          i!=mThreadedImageReaderList.end();
141          i++)
142     {
143                 (*i)->Create();
144                 if ( (*i)->Run() != wxTHREAD_NO_ERROR )
145                   {
146                         std::cout << "ERROR starting a thread"<< std::endl;
147                         return false;
148                   }     else  {
149                                         std::cout << "  ===> Thread "<<(*i)->GetCurrentId()
150                                                   <<" successfully created"<< std::endl;
151                   } // if
152                 } // for
153                 wxMutexLocker locker(GetMultiThreadImageReaderUserMutex());
154                 //    std::cout << "EO Start : #Threads running = "
155                 //                    << mNumberOfThreadedReadersRunning<<std::endl;
156     return true;
157   }
158   //=====================================================================
159
160   //=====================================================================
161   void MultiThreadImageReader::Stop()
162   { 
163 //                  std::cout << "#### MultiThreadImageReader::Stop()"
164 //            <<std::endl;
165   //  std::cout << "Sending stop order to the threads..."<<std::endl;
166   
167           if (mDone) return;
168
169     ThreadedImageReaderListType::iterator i;
170     for (i =mThreadedImageReaderList.begin();
171          i!=mThreadedImageReaderList.end();
172          i++)
173     { 
174                 std::cout << "  ===> Thread "<<(*i)->GetCurrentId()
175                               <<" successfully stopped"<< std::endl;
176                   if((*i)->IsAlive())
177                   {
178                           (*i)->Pause();
179                           (*i).reset();
180 //                        (*i)->Delete();
181                   } // if i
182     } // for
183    mThreadedImageReaderList.clear();
184     // Wait a little to be sure that all threads have stopped
185     // A better way to do this ?
186     //    wxMilliSleep(1000);
187     // New method : the threads generate a stop event when they have finished
188     // We wait until all threads have stopped
189 //        std::cout << "Waiting for stop signals..."<<std::endl;
190     do 
191       {
192         // Sleep a little
193                 wxMilliSleep(10);
194         // Lock
195         {
196           wxMutexLocker locker(GetMultiThreadImageReaderUserMutex());
197 //                std::cout << "#Threads running = "
198 //                          << mNumberOfThreadedReadersRunning<<std::endl;
199           // Break if all readers have stopped
200           if (mNumberOfThreadedReadersRunning <= 0) 
201             {
202               break;
203             }
204         }
205       } 
206     while (true);
207 //        std::cout << "All threads stopped : OK "<<std::endl;
208
209
210         CleanMImagesMap();
211         
212         mDone = true;
213
214   }
215   //=====================================================================
216
217   
218    void MultiThreadImageReader::CleanMImagesMap()
219   {
220      ImageMapType::iterator j;
221      for (j=mImages.begin(); j!=mImages.end(); ++j)
222          {
223                 delete j->first;
224      } //for 
225      mImages.clear();
226   }
227   
228   //=====================================================================
229   MultiThreadImageReader::~MultiThreadImageReader()
230   {
231     //    std::cout << "#### MultiThreadImageReader::~MultiThreadImageReader()"
232     //        <<std::endl;
233     Stop();
234     if (mReader) delete mReader;
235         mThreadedImageReaderList.clear();
236
237         CleanMImagesMap();
238         
239 /*      
240         ImageMapType::iterator it;
241         for (it=mImages.begin() ; it!=mImages.end(); it++)
242         {
243                         printf("MultiThreadImageReader::~MultiThreadImageReader  %s ", it.first->GetFilename() );
244         } // for it
245 */
246         
247   }
248   //=====================================================================
249
250   //=====================================================================
251   void MultiThreadImageReader::UpdateUnloadPriority(ImageToLoadPtr p, 
252                                                     int priority)
253   {
254     // not in unload queue : ciao
255     if (p->UnloadIndex()<0) return;
256     int old_prio = p->GetPriority();
257     if (priority > old_prio) 
258       {
259         p->SetPriority(priority);
260         mUnloadQueue.downsort(p->UnloadIndex());
261       }
262     else if ( old_prio > priority )
263       {
264         p->SetPriority(priority);
265         mUnloadQueue.upsort(p->UnloadIndex());
266      }
267   }
268   //=====================================================================
269   // function to read attributes for a file
270   void MultiThreadImageReader::getAttributes(const std::string filename, 
271           std::map <std::string , std::string> &infos,std::vector<std::string> i_attr)
272   {
273           mReader->getAttributes(filename, infos, i_attr);
274   }
275
276   //=====================================================================
277   void MultiThreadImageReader::Request( MultiThreadImageReaderUser* user,
278                                         const std::string& filename, 
279                                         int priority )
280   {
281         wxMutexLocker lock(GetMultiThreadImageReaderUserMutex()); //mMutex);
282
283           if (mNumberOfThreadedReadersRunning==0)
284 //    if (mThreadedImageReaderList.size()==0) 
285       {
286         // no detached reader : use self reader
287         ImageToLoad itl(user,filename);
288         ImageMapType::iterator i = mImages.find(&itl);
289         if (i!=mImages.end())
290           {
291             ImageToLoadPtr pitl = const_cast<ImageToLoadPtr>(i->first);
292             // Already inserted
293             if (pitl->GetImage() != 0)
294               {
295                 // Already read
296                 pitl->SetUser(user);
297                 UpdateUnloadPriority(pitl,priority);
298                 SignalImageRead(pitl,false);
299                 return; // pitl->GetImage();
300               }
301           }
302         ImageToLoadPtr pitl = new ImageToLoad(user,filename,0);
303         mImages[pitl] = 0;
304         pitl->SetImage(mReader->ReadImage(filename));
305         UpdateUnloadPriority(pitl,priority);
306         SignalImageRead(pitl,true);
307         //      return pitl->GetImage();
308         return;
309       }
310
311     ImageToLoad itl(user,filename);
312     ImageMapType::iterator i = mImages.find(&itl);
313     if (i!=mImages.end())
314       {
315         // Already inserted
316         if (i->first->GetImage() != 0)
317           {
318             // Already read : ok :signal the user
319             UpdateUnloadPriority(i->first,priority);
320             SignalImageRead(i->first,false);
321             return;
322           }
323         /// Already requested : change the priority
324         ImageToLoadPtr pitl = const_cast<ImageToLoadPtr>(i->first);
325         pitl->SetPriority(priority);
326         // Already in queue
327         if (pitl->Index()>=0) 
328           {
329             // Re-sort the queue
330             mQueue.upsort(pitl->Index());
331           }
332         // Not read but not in queue = being read = ok
333         else 
334           {
335             
336           }
337       }
338     else 
339       {
340         // Never requested before or unloaded 
341         ImageToLoadPtr pitl = new ImageToLoad(user,filename,priority);
342         mImages[pitl] = 0;
343         mQueue.insert(pitl);
344       }
345   }
346   //=====================================================================
347
348   //=====================================================================
349   void MultiThreadImageReader::OnMultiThreadImageReaderEvent
350   (const std::string& filename,
351    MultiThreadImageReaderUser::EventType e,
352    vtkImageData* image)
353   {
354     if ((e==MultiThreadImageReaderUser::ImageLoaded) &&
355         (filename == mRequestedFilename))
356       {
357         mRequestedImage = image;
358       }
359     else if (e==MultiThreadImageReaderUser::ThreadedReaderStarted)
360       {
361         mNumberOfThreadedReadersRunning++;
362         //      std::cout << "#TR=" << mNumberOfThreadedReadersRunning << std::endl;
363       }
364     else if (e==MultiThreadImageReaderUser::ThreadedReaderStopped)
365       {
366         
367                  mNumberOfThreadedReadersRunning--;
368         //      std::cout << "#TR=" << mNumberOfThreadedReadersRunning << std::endl;
369       }
370   }
371   //=====================================================================
372
373   //=====================================================================
374   vtkImageData* MultiThreadImageReader::GetImage(const std::string& filename)
375   {
376
377     do 
378       {
379         //      wxMutexLocker lock(GetMultiThreadImageReaderUserMutex()); //mMutex);
380                 
381         //     std::cout << "** MultiThreadImageReader::GetImage('"<<filename
382         //             <<"') lock ok"
383         //               <<std::endl;
384     
385         //                if (mNumberOfThreadedReadersRunning==0)
386         //      if (mThreadedImageReaderList.size()==0)
387         if (true)
388         {
389             ImageToLoad itl(this,filename);
390             ImageMapType::iterator i = mImages.find(&itl);
391             if (i!=mImages.end())
392             {
393                         ImageToLoadPtr pitl = const_cast<ImageToLoadPtr>(i->first);
394                         // Already inserted
395                         if (pitl->GetImage() != NULL)
396                         {
397                                 // Already read
398                                 UpdateUnloadPriority( pitl, GetMaximalPriorityWithoutLocking()+1 );
399                                 return pitl->GetImage();
400                         } // if pitl->GetImage()
401             } // if i
402                 
403             ImageToLoadPtr pitl = new ImageToLoad(this,filename,0);
404             mImages[pitl]               = NULL;
405             pitl->SetImage( mReader->ReadImage(filename) );
406             UpdateUnloadPriority( pitl, GetMaximalPriorityWithoutLocking()+1 );
407             return pitl->GetImage();
408         } // if true
409         
410         /*      
411         mRequestedFilename = filename;
412         mRequestedImage = 0;
413         ImageToLoad itl(this,filename);
414         ImageMapType::iterator i = mImages.find(&itl);
415         if (i!=mImages.end())
416           {
417             // Already inserted in queue
418             if (i->first->GetImage() != 0)
419               {
420                 // Already read : ok : return it 
421                 return i->first->GetImage();
422               }
423             /// Already requested : change the priority
424               ImageToLoadPtr pitl = const_cast<ImageToLoadPtr>(i->first);
425               pitl->SetPriority( GetMaximalPriorityWithoutLocking() + 1 );
426               pitl->SetUser( this );
427               // Already in queue
428               if (pitl->Index()>=0) 
429                 {
430                   // Re-sort the queue
431                   mQueue.upsort(pitl->Index());
432                 }
433               // Not read but not in queue = being read = ok
434               else 
435                 {
436                   pitl->SetUser( this );
437                 }
438           }
439         else 
440           {
441             
442             // Never requested before or unloaded 
443             ImageToLoadPtr pitl = 
444               new ImageToLoad(this,filename,
445                               GetMaximalPriorityWithoutLocking() + 1);
446             mImages[pitl] = 0;
447             mQueue.insert(pitl);
448           }
449         */
450       }
451     while (0);
452
453     //    std::cout << "Waiting..."<<std::endl;
454
455     /*
456     // Waiting that it is read
457     int n = 0;
458     do 
459       {
460         //      std::cout << n++ << std::endl;
461         wxMilliSleep(10);
462         do 
463           {
464             //      wxMutexLocker lock(mMutex);
465             wxMutexLocker lock(GetMultiThreadImageReaderUserMutex());
466             if (mRequestedImage!=0) 
467               {
468                 return mRequestedImage;
469               } 
470           }
471         while (0);
472       }
473     while (true);
474     // 
475     */
476         
477   }
478   //=====================================================================
479   
480   //=====================================================================
481   void MultiThreadImageReader::SignalImageRead(ImageToLoadPtr p, 
482                                                bool purge)
483   {
484     
485 //    std::cout << "MultiThreadImageReader::SignalImageRead" <<std::endl;
486     //    std::cout << "this="<<this <<std::endl;
487     //    std::cout << "user="<<p->GetUser() <<std::endl;
488
489     if ( p->GetUser() == this ) 
490         {
491       GetMultiThreadImageReaderUserMutex().Unlock();
492         }
493
494     p->GetUser()->MultiThreadImageReaderSendEvent
495       (p->GetFilename(),
496        MultiThreadImageReaderUser::ImageLoaded,
497        p->GetImage());
498
499     /*
500       AN ATTEMPT TO UNLOAD OLDEST IMAGE IF EXCEEDED A CERTAIN MEMORY QUOTA
501       BUGGY : TO FIX 
502     */
503     if (!purge)  return;
504     GimmickMessage(5,"Image '"<<p->GetFilename()<<"' read"<<std::endl);
505
506     //    wxMutexLocker lock(GetMultiThreadImageReaderUserMutex());
507            
508     mUnloadQueue.insert(p);
509
510
511 //EED 2017-01-01 Migration VTK7
512 #if VTK_MAJOR_VERSION <= 5
513     p->GetImage()->UpdateInformation();
514     p->GetImage()->PropagateUpdateExtent();
515     long ImMem = p->GetImage()->GetEstimatedMemorySize();
516 #else
517         int ext[6];
518         int dim[3];
519         p->GetImage()->GetExtent(ext);
520         dim[0]          = ext[1]-ext[0]+1;
521         dim[1]          = ext[3]-ext[2]+1;
522         dim[2]          = ext[5]-ext[4]+1;
523     long ImMem  = dim[0]*dim[1]*dim[2]*p->GetImage()->GetScalarSize();;
524 #endif
525     mTotalMem += ImMem;
526
527     GimmickMessage(5,"==> Image in memory = "<<mUnloadQueue.size()<<std::endl);
528     GimmickMessage(5,"==> Total mem       = "<<mTotalMem<<" Ko"<<std::endl);
529
530     //  return;
531
532     while (mTotalMem > mTotalMemMax)
533       {
534         GimmickMessage(5,
535                        "   ! Exceeded max of "
536                        << mTotalMemMax << " Ko : unloading oldest image ... "
537                        << std::endl);
538         if ( mUnloadQueue.size() <= 1 ) 
539           {
540              GimmickMessage(5,
541                             "   Only one image : cannot load AND unload it !!"
542                             <<std::endl);
543             break; 
544             
545           }
546         ImageToLoadPtr unload = mUnloadQueue.remove_top();
547         MultiThreadImageReaderUser* user = unload->GetUser();
548
549         /*
550         if ((user!=0)&&(user!=this)) 
551           {
552             user->GetMultiThreadImageReaderUserMutex().Lock();
553           }
554         */
555
556         std::string filename = unload->GetFilename();
557
558         GimmickMessage(5,"'" << filename << "'" << std::endl);
559
560 //EED 2017-01-01 Migration VTK7
561 #if VTK_MAJOR_VERSION <= 5
562         mTotalMem -= unload->GetImage()->GetEstimatedMemorySize();
563 #else
564         int ext[6];
565         int dim[3];
566         unload->GetImage()->GetExtent(ext);
567         dim[0]          = ext[1]-ext[0]+1;
568         dim[1]          = ext[3]-ext[2]+1;
569         dim[2]          = ext[5]-ext[4]+1;
570         mTotalMem -= dim[0]*dim[1]*dim[2]*unload->GetImage()->GetScalarSize();
571 #endif
572
573         GimmickMessage(5," ==> Total mem = "<<mTotalMem<<" Ko "<<std::endl);
574
575         if (user!=0) 
576           {
577             //      std::cout << "unlock..."<<std::endl;
578             //   user->GetMultiThreadImageReaderUserMutex().Unlock();
579             //      std::cout << "event"<<std::endl;
580             user->MultiThreadImageReaderSendEvent
581               (filename,
582                MultiThreadImageReaderUser::ImageUnloaded,
583                0);
584             //      std::cout << "event ok"<<std::endl;
585           }     
586
587         if (unload->Index()>=0)
588           {
589             // GimmickMessage(5,"still in queue"<<std::endl);
590           }
591         unload->Index() = -1;
592
593
594         ImageMapType::iterator it = mImages.find(unload);
595         if (it!=mImages.end())
596           {
597             mImages.erase(it);
598           }
599         //          std::cout << "delete..."<<std::endl;
600         delete unload;
601         //          std::cout << "delete ok."<<std::endl;
602
603       }
604   }
605   //=====================================================================
606
607   //=====================================================================
608   int MultiThreadImageReader::GetMaximalPriority()
609   { 
610     wxMutexLocker lock(GetMultiThreadImageReaderUserMutex()); //mMutex);
611     return GetMaximalPriorityWithoutLocking();
612   }
613   //=====================================================================
614
615
616   //=====================================================================
617   int MultiThreadImageReader::GetMaximalPriorityWithoutLocking()
618   { 
619     long max = 0;
620     if (mQueue.size()>0) 
621       {
622         max = mQueue.top()->GetPriority();
623       }
624     if (mUnloadQueue.size()>0)
625       {
626         int max2 = mUnloadQueue.top()->GetPriority();
627         if (max2>max) max=max2;
628       }
629     return max;
630   }
631   //=====================================================================
632
633
634   //=====================================================================
635   //=====================================================================
636   //=====================================================================
637   //=====================================================================
638
639   //=====================================================================
640   void*  ThreadedImageReader::Entry()
641   {
642     //    std::cout << "### Thread "<<GetCurrentId()<<"::Entry()"
643     //                << std::endl;
644
645     mMultiThreadImageReader->MultiThreadImageReaderSendEvent
646       ("",
647        MultiThreadImageReaderUser::ThreadedReaderStarted,
648        0);
649
650     // While was not deleted 
651     while (!TestDestroy())
652       {
653                 //std::cout << "### Thread "<<GetCurrentId()<<" still alive"  << std::endl;
654           
655         // Lock the mutex
656         mMultiThreadImageReader->MultiThreadImageReaderEventLock();
657         //mMutex.Lock();
658         // If image in queue
659         if (mMultiThreadImageReader->mQueue.size()>0)
660           {
661             MultiThreadImageReader::ImageToLoadPtr i = 
662               mMultiThreadImageReader->mQueue.remove_top();
663
664             mMultiThreadImageReader->MultiThreadImageReaderEventUnlock();
665             //mMutex.Unlock();
666
667             
668             //      std::cout << "### Thread "<<GetCurrentId()<<" : reading '"
669             //                << i->GetFilename() << "'" << std::endl;
670             
671             // Do the job
672             vtkImageData* im = Read(i->GetFilename());
673
674             // Store it in the map
675             mMultiThreadImageReader->MultiThreadImageReaderEventLock();
676             //mMutex.Lock();
677             MultiThreadImageReader::ImageToLoad itl(0,i->GetFilename());
678             MultiThreadImageReader::ImageMapType::iterator it = 
679               mMultiThreadImageReader->mImages.find(&itl);
680             MultiThreadImageReader::ImageToLoadPtr 
681               pitl = const_cast<MultiThreadImageReader::ImageToLoadPtr>
682               (it->first);
683             pitl->SetImage(im);
684             mMultiThreadImageReader->SignalImageRead(pitl,true);//i->GetFilename());
685             mMultiThreadImageReader->MultiThreadImageReaderEventUnlock();           //mMutex.Unlock();
686             
687             //      std::cout << "### Thread "<<GetCurrentId()<<" : reading '"
688             //                << i->GetFilename() << "' : DONE" << std::endl;
689             
690           }     else  {
691             mMultiThreadImageReader->MultiThreadImageReaderEventUnlock();
692             //mMutex.Unlock();
693             // Wait a little to avoid blocking 
694             wxMilliSleep(10);
695           }
696       };
697     //    std::cout << "### Thread "<<GetCurrentId()<<" stopping"
698     //                << std::endl;
699        
700     return 0;
701   }
702   //=====================================================================
703
704   //=====================================================================
705   void ThreadedImageReader::OnExit()
706   {
707     mMultiThreadImageReader->MultiThreadImageReaderSendEvent
708       ("",
709        MultiThreadImageReaderUser::ThreadedReaderStopped,
710        0);
711   }
712   //=====================================================================
713
714   //=====================================================================
715   vtkImageData* ThreadedImageReader::Read(const std::string& filename)
716   {
717     return mReader.ReadImage(filename);
718   }
719   //=====================================================================
720
721 } // namespace creaImageIO