SOL9 Sample: ImageStitcher

SOL9 2.0 Samples

1 Screenshot


2 Source code

/*
 * ImageStitcher.cpp 
 * Copyright (c) 2019 Antillia.com TOSHIYUKI ARAI. ALL RIGHTS RESERVED. 
 */


//2017/09/25

// See: http://docs.opencv.org/3.2.0/d2/d8d/classcv_1_1Stitcher.html
// See: http://docs.opencv.org/3.2.0/d8/d19/tutorial_stitcher.html
//2017/12/01 Added save, saveAs methods to MainView.


#define _CONSOLE_

#include <sol/ModuleFileName.h>
#include <sol/Pair.h>
#include <sol/DropFiles.h>
#include <sol/LabeledTrackBar.h>
#include <sol/LabeledComboBox.h>
#include <sol/PushButton.h>

#include <sol/FileDialog.h>
#include <sol/opencv/OpenCVApplicationView.h>
#include <sol/opencv/OpenCVScrolledImageView.h>
#include <sol/opencv/OpenCVScaleComboBox.h>

#include "resource.h"

namespace SOL {

class MainView :public OpenCVApplicationView {

private:
  ////////////////////////////////////////////////////////////////////////////////////////
  //Inner classes start.
  class SourceImageView :public OpenCVScrolledImageView {
  private:
    cv::Mat originalImage;
    
  public:
    cv::Mat getClone()
    {
      return originalImage.clone();
    }

  public:
    SourceImageView(View* parent, const char* name, Args& args)
    :OpenCVScrolledImageView(parent, name, args)
    {
      try {
        const char* filename = (const char*)args.get(XmNimageFileName);
        int imageLoadingFlag = args.get(XmNimageLoadingFlag);
        int scalingRatio     = args.get(XmNimageScalingRatio);
        
        loadImage(filename, imageLoadingFlag, scalingRatio);
      } catch (SOL::Exception ex) {
        caught(ex);
      }
    }
    
    ~SourceImageView()
    {
    }
    
    void rescaleImage(int scalingRatio)
    {
      setScaledImage(originalImage,  scalingRatio);      
    }
    
    void loadImage(const char* filename, int imageLoadingFlag= CV_LOAD_IMAGE_COLOR, int scalingRatio=100)
    {
      try {
        originalImage = readImage(filename, imageLoadingFlag);        
        setScaledImage(originalImage,  scalingRatio);      
        refresh();
      } catch (Exception& ex) {
        caught(ex);
      }
    } 
  };
  
  
  class StitchedImageView :public OpenCVScrolledImageView {
  private:
    cv::Mat originalImage;
    
  public:
    StitchedImageView(View* parent, const char* name, Args& args)
    :OpenCVScrolledImageView(parent, name, args)
    {
      try {
        ;//Do nothing here.
      } catch (SOL::Exception ex) {
        caught(ex);
      }
    }
    
    ~StitchedImageView()
    {
    }
    
    void rescaleImage(int scalingRatio)
    {
      if (!originalImage.empty()) {
        setScaledImage(originalImage,  scalingRatio);
      }
    }
    
    void setStitchedImage(cv::Mat stitchedImage, int scalingRatio)
    {   
      try {
        originalImage = stitchedImage;
        setScaledImage(originalImage, scalingRatio);
        
        refresh();
      } catch (Exception& ex) {
        caught(ex);
      }
    }
  };
  //Inner classes end.
  ////////////////////////////////////////////////////////////////////////////////////////
  
  
  static const int              IMAGES_COUNT = 2;
  
  static const int              FIRST  = 0;
  
  static const int              SECOND = 1;

  StringT<char>                 imageFiles[IMAGES_COUNT];

  StringT<char>                 savedImageFile;

  int                           imageLoadingFlag;

  int                           imageScalingRatio; //percentage 
  
  SmartPtr<SourceImageView>     sourceImageView[IMAGES_COUNT];

  SmartPtr<StitchedImageView>   stitchedImageView;

  int                           stitcherModeIndex;

  cv::Stitcher::Mode            mode;

  SmartPtr<OpenCVScaleComboBox> scaleComboBox;
  
  //2018/12/30 bool                          tryToUseGPU;

  cv::Ptr<Stitcher>             stitcher;

  SmartPtr<LabeledComboBox>     stitcherModeComboBox;

  SmartPtr<PushButton>          clearButton;

  SmartPtr<PushButton>          stitchButton;
 
  FileDialog                    filedlg;

  cv::Stitcher::Mode getStitcherMode(int index) 
  {
    int n = 0;
    Pair<int, cv::Stitcher::Mode> types[] = {
      {n++, cv::Stitcher::PANORAMA  },
      {n++, cv::Stitcher::SCANS     },
    };
    mode = cv::Stitcher::PANORAMA; 
    if (index >= 0 && index <n) {
      mode = types[index].second;
    } 
    return mode;
  }
  
  void updateCaption()
  {
    char caption[MAX_PATH];
    sprintf_s(caption, CountOf(caption), "%s %s- %s", 
        (const char*)imageFiles[FIRST],
      (const char*)imageFiles[SECOND],
        getAppName() ); 
       
    setText(caption);
  }

  void scaleChanged(Action& action)
  {
    imageScalingRatio = scaleComboBox->getScale();
    sourceImageView[FIRST]   -> rescaleImage(imageScalingRatio);
    sourceImageView[SECOND]  -> rescaleImage(imageScalingRatio);
    stitchedImageView        -> rescaleImage(imageScalingRatio);
  }
  
  void stitcherModeChanged(Action& event)
  {
    int index = stitcherModeComboBox -> getCurSel();

    if (stitcherModeIndex != index) {
      stitcherModeIndex = index;
      mode = getStitcherMode(index);
      //Rebuild stitcher.
      stitcher = cv::Stitcher::create(mode);//2018/12/30 , tryToUseGPU);
      stitchImages();
    }
  }
  
  void resize(int w, int h)
  {
    try {
    if (sourceImageView[FIRST] && sourceImageView[SECOND] && stitchedImageView ) {
      static const int CPW = 170;
      
      sourceImageView[FIRST]   -> reshape(          2,   2,   (w-CPW)/2-2, h/2-4);
      sourceImageView[SECOND]  -> reshape((w-CPW)/2+1,   2,   (w-CPW)/2-2, h/2-4);
      stitchedImageView        -> reshape(          2,   h/2, w-CPW-2    , h/2-4);

      scaleComboBox            -> reshape((w-CPW)+4,      2,   CPW-10,        50);

      stitcherModeComboBox     -> reshape((w-CPW)+4,     60,   CPW-10,        50);

      //clearButton            -> reshape(w-160+4, 250, 140, 30);
      stitchButton             -> reshape(w-CPW+10,     250,   CPW-20,        30); 
    }
    } catch (SOL::Exception& ex) {
      caught(ex);
    }
  }


  void openFile(int index, const char* filename)
  {
    try {
      if (index >= FIRST && index <= SECOND) {
        printf("filename: %s\n", filename);
       // sourceImageView[fileOpenIndex]->invalidate();
        sourceImageView[index]->loadImage(filename, 
                       imageLoadingFlag,
                   imageScalingRatio);
        imageFiles[index] = filename;
        updateCaption();
        
        savedImageFile = "";
        
      }
    } catch (Exception& ex) {
      caught(ex);
    }
  }
  
  
  //Event handler for WM_DROPFILES
  long dropFiles(Event& event)
  {
    View* view = (View*)event.getData();
    char fileName[MAX_PATH] = { 0 };
    DropFiles drop((HDROP)event.getWParam());
    //fileName[0] = ZERO;
    int num = drop.queryFile(0, fileName, CountOf(fileName));
    if(num > 0) {
      if (filedlg.isImageFileName(fileName)) {
        if (view == sourceImageView[FIRST]) {
          openFile(FIRST, fileName);
        } else if (view == sourceImageView[SECOND]) {
          openFile(SECOND, fileName);          
        }
        bringUp();
      } else {        
        bringUp(); 
        showErrorDialog("Invalid image filename", fileName,  MB_OK);
      }
    }    
    return 0;
  }


  void confirm(Action& action)
  {
    int rc = MessageBox(NULL, "Are you sure to close this window?", "Confirmation", 
                MB_OKCANCEL|MB_ICONEXCLAMATION);
    if (rc == IDOK) {
      exit(action);
    }
  }
  
  void stitch(Action& action)
  {
    stitchImages();
  }

/*
enum   Status {  
  OK = 0, 
   ERR_NEED_MORE_IMGS = 1, 
   ERR_HOMOGRAPHY_EST_FAIL = 2, 
   ERR_CAMERA_PARAMS_ADJUST_FAIL = 3 
  */

  const char* getErrorMessage(cv::Stitcher::Status status)
  {
    static const Pair<const char*,  cv::Stitcher::Status> errors[] = {
      {"Need more images",          cv::Stitcher::Status::ERR_NEED_MORE_IMGS},
      {"Homography est fail",       cv::Stitcher::Status::ERR_HOMOGRAPHY_EST_FAIL},
      {"Camera params adjust fail", cv::Stitcher::Status::ERR_CAMERA_PARAMS_ADJUST_FAIL},
    };
    const char* err = "";
    for (int i = 0; i <CountOf(errors); i++) {
      if (status == errors[i].second) {
        err = errors[i].first;
        break;
      }
    }
    return err;
  }
  
  void stitchImages()
  {
    //Detect connectedCommponentsWithStats on destImage 
    Mat labels, stats, centroids;
    cv::Mat sourceImage[IMAGES_COUNT];
    
    std::vector<cv::Mat> images;
    images.push_back( sourceImageView[FIRST  ]->getClone()  );
    images.push_back( sourceImageView[SECOND  ]->getClone() );

    cv::Mat panorama;
    cv::Stitcher::Status status = stitcher->stitch(images, panorama);
    if (status == cv::Stitcher::Status::OK) {
      stitchedImageView->setStitchedImage(panorama, imageScalingRatio);
    } else {
      char message[1024];
      const char* err = getErrorMessage(status);
      sprintf_s(message, CountOf(message), "Failed to Stitcher::stitch method error=%s", err);
      
      MessageBox(NULL, message, "Confirmation", 
                MB_OK|MB_ICONERROR);

    }
  }
  
public:
  MainView(OpenCVApplication& applet, const char* name, Args& args)
  :OpenCVApplicationView(applet, name, args)
  ,imageScalingRatio(50)
  ,mode (cv::Stitcher::PANORAMA)
  //,tryToUseGPU(false)
  {  
    try {
      stitcher = cv::Stitcher::create(mode); //2018/12/30 , tryToUseGPU);

      imageFiles[FIRST]  = "../images/Lake2.png";
      imageFiles[SECOND] = "../images/Lake1.png";

      imageLoadingFlag = CV_LOAD_IMAGE_COLOR;

      //1 Create two source image views.
      Args ar;
      for (int i = 0; i<IMAGES_COUNT; i++) {
        ar.reset();
        ar.set(XmNimageFileName, (const char*)imageFiles[i]);
        ar.set(XmNimageLoadingFlag, imageLoadingFlag);
        ar.set(XmNimageScalingRatio, imageScalingRatio);
        sourceImageView[i] = new SourceImageView(this, "", ar);
      }
      
      //2 Create a stitched image view.
      ar.reset();
      stitchedImageView = new StitchedImageView(this, "", ar);

      //3 Create a scale combobox
      ar.reset();
      ar.set(XmNdefaultScale, "50%");
      scaleComboBox = new OpenCVScaleComboBox(this, "Scale", ar);
      
      //Add a selChanged callback.
      scaleComboBox -> addCallback(XmNselChangeCallback, this,
        (Callback)&MainView::scaleChanged, NULL);
     

      //4 Create a stitch mode combobox
      ar.reset();
      stitcherModeComboBox = new LabeledComboBox(this, "StitcherMode", ar);
      const char* types[] = {
        "Panorama",
        "Scans",
      };
      for (int i = 0; i<CountOf(types); i++) {
        stitcherModeComboBox -> addString(types[i]);
      }
      stitcherModeIndex = 0; //Panorama
      
      stitcherModeComboBox -> setCurSel(stitcherModeIndex);
      //Add a selChanged callback.
      stitcherModeComboBox -> addCallback(XmNselChangeCallback, this,
        (Callback)&MainView::stitcherModeChanged, NULL);
     

      //5 Create a stitchButton.
      ar.reset();
      stitchButton = new PushButton(this, "Stitch", ar);
      stitchButton -> addCallback(XmNactivateCallback, this,
        (Callback)&MainView::stitch, NULL);
 
      //6 If you wouldn't call the stitchImages method, since it will take a few seconds, 
      // you comment out the following line, and let the users click stitchButton.
      stitchImages();
   
      updateCaption();  

      ar.reset();
      ar.set(XmNfilter, FileDialog::getImageFilesFilter());
      filedlg.create(this, "OpenFile", ar);

      addCallback(XmNmenuCallback, IDM_EXIT, this,
          (Callback)&MainView::confirm, NULL);

      addCallback(XmNmenuCallback, IDM_OPEN_FIRST_FILE, this,
          (Callback)&MainView::openFile, (void*)FIRST);
      
      addCallback(XmNmenuCallback, IDM_OPEN_SECOND_FILE, this,
          (Callback)&MainView::openFile, (void*)SECOND);
      
      bringUp();
      
      postResizeRequest();
      
    } catch (Exception& ex) {
      caught(ex);
    }
  }

  ~MainView()
  {
  }

  void openFile(Action& action)
  {
    Args ar;
    int index = (int)action.getData();
    
    char dir[MAX_PATH] = { 0 };
    if (restoreFileFolder(dir, CountOf(dir))) {
      ar.set(XmNdirectory, dir);
      filedlg.setValues(ar);
    }
    
    try {    
      if(filedlg.open()) {
    
        const char* filename = filedlg.getFileName();
        saveFileFolder(filename);
       
        openFile(index, filename);
        
      }
    } catch (Exception& ex) {
      caught(ex);
    }
  } 
  
  //2017/12/01
  void save(Action& action)
  {
    try {
      if (!savedImageFile.isEmpty()) {
        //Write stitchedImage into the filename.
        stitchedImageView->writeImage(savedImageFile);
      } else {
        saveAs(action);
      }
    } catch (Exception& ex) {
      caught(ex);
    }
  }
  
  //2017/12/01
  void saveAs(Action& action)
  {
    Args ar;
    
    char dir[MAX_PATH] = { 0 };
    if (restoreFileFolder(dir, CountOf(dir))) {
      ar.set(XmNdirectory, dir);
      filedlg.setValues(ar);
    }
    
    try {    
      if(filedlg.save()) {
    
        const char* filename = filedlg.getFileName();
        saveFileFolder(filename);
        
        //Write stitchedImage into the filename.
        stitchedImageView->writeImage(filename);
        savedImageFile = filename;
      }
    } catch (Exception& ex) {
      caught(ex);
    }
  } 
};
}

//
void main(int argc, char** argv) 
{
  try {
    ModuleFileName module(argv[0]);
    
    const char*  name = module.getAppName();
        
    OpenCVApplication applet(name, argc, argv);

    Args args;
    args.set(XmNwidth,  1000);
    args.set(XmNheight, 600);  
    MainView view(applet, name, args);
    view.realize();

    applet.run();
    
  } catch (SOL::Exception& ex) {
    caught(ex);
  }
}


Last modified: 1 Jan. 2019

Copyright (c) 2019 Antillia.com ALL RIGHTS RESERVED.