From 39575f566d8d93729b0d6c9ffd20617e66864f58 Mon Sep 17 00:00:00 2001 From: manongjohn Date: Tue, 22 Feb 2022 11:55:37 -0500 Subject: [PATCH] stop motion webcam calibration Co-Authored-By: shun-iwasawa --- .../camera calibration/checkerboard.tif | Bin 0 -> 145060 bytes stuff/library/camera calibration/readme.txt | 19 + toonz/sources/stopmotion/stopmotion.cpp | 13 +- toonz/sources/stopmotion/stopmotion.h | 16 + .../stopmotion/stopmotioncontroller.cpp | 389 +++++++++++++++++- .../sources/stopmotion/stopmotioncontroller.h | 42 ++ toonz/sources/stopmotion/webcam.cpp | 11 +- toonz/sources/stopmotion/webcam.h | 4 +- 8 files changed, 490 insertions(+), 4 deletions(-) create mode 100644 stuff/library/camera calibration/checkerboard.tif create mode 100644 stuff/library/camera calibration/readme.txt diff --git a/stuff/library/camera calibration/checkerboard.tif b/stuff/library/camera calibration/checkerboard.tif new file mode 100644 index 0000000000000000000000000000000000000000..b3d30a2ee3c628a3a5a46c0eb649518a9e5f6e6a GIT binary patch literal 145060 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 n7$5&PA~ literal 0 HcmV?d00001 diff --git a/stuff/library/camera calibration/readme.txt b/stuff/library/camera calibration/readme.txt new file mode 100644 index 00000000..d609373a --- /dev/null +++ b/stuff/library/camera calibration/readme.txt @@ -0,0 +1,19 @@ +### How to calibrate camera + +1. Prepare the checkerboard + + - Print out "checkerboard.tif" found in "tahomastuff/library/camera calibration". + - It would be better to paste the checkerboard pattern on some flat panel like cardboard. + +2. Capture the pattern + + - In the Stop Motion Controller's "Settings" tab, enable "Calibration" box + - Click "Start calibration". + - Take 10 snapshots of the checkerboard pattern in various positions and angles. + - Once the capturing is done, calibration will be automatically applied whenever the "Calibration" box is checked. + +3. Export & load calibration settings + + - Calibration settings will be saved under "tahomastuff/library/camera calibration" with name "[MACHINENAME]_[CAMERANAME]_[RESOLUTION].xml" + - The settings file will be overwritten after finishing the new calibration. + - You can export the file for backup, or load it afterwards. diff --git a/toonz/sources/stopmotion/stopmotion.cpp b/toonz/sources/stopmotion/stopmotion.cpp index e6ba55ba..f8524eb7 100644 --- a/toonz/sources/stopmotion/stopmotion.cpp +++ b/toonz/sources/stopmotion/stopmotion.cpp @@ -1391,8 +1391,19 @@ void StopMotion::onTimeout() { } #endif } else { - bool success = m_webcam->getWebcamImage(m_liveViewImage); + bool calibrateImage = !m_calibration.captureCue && + m_calibration.isValid && m_calibration.isEnabled; + bool success = + m_webcam->getWebcamImage(m_liveViewImage, calibrateImage, + m_calibration.mapX, m_calibration.mapY); if (success) { + // capture calibration reference + if (m_calibration.captureCue) { + m_calibration.captureCue = false; + emit(calibrationImageCaptured()); + return; + } + setLiveViewImage(); } else { m_hasLiveViewImage = false; diff --git a/toonz/sources/stopmotion/stopmotion.h b/toonz/sources/stopmotion/stopmotion.h index 8c5525fe..75b6f711 100644 --- a/toonz/sources/stopmotion/stopmotion.h +++ b/toonz/sources/stopmotion/stopmotion.h @@ -131,6 +131,19 @@ public: // captured images. TPointD m_liveViewDpi = TPointD(0.0, 0.0); + struct CalibrationData { + // Parameters + QString filePath; + bool captureCue = false; + cv::Size boardSize = {10, 7}; + int refCaptured = 0; + std::vector> obj_points; + std::vector> image_points; + cv::Mat mapX, mapY; + bool isValid = false; + bool isEnabled = false; + } m_calibration; + // files and frames void setXSheetFrameNumber(int frameNumber); int getXSheetFrameNumber() { return m_xSheetFrameNumber; } @@ -283,6 +296,9 @@ signals: // test shots void updateTestShots(); + + // Calibration + void calibrationImageCaptured(); }; #endif // STOPMOTION_H \ No newline at end of file diff --git a/toonz/sources/stopmotion/stopmotioncontroller.cpp b/toonz/sources/stopmotion/stopmotioncontroller.cpp index 66d19f63..12534d8e 100644 --- a/toonz/sources/stopmotion/stopmotioncontroller.cpp +++ b/toonz/sources/stopmotion/stopmotioncontroller.cpp @@ -32,6 +32,7 @@ #include "flipbook.h" #include "iocommand.h" #include "tlevel_io.h" +#include "filebrowser.h" // TnzQt includes #include "toonzqt/filefield.h" @@ -64,6 +65,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include @@ -82,6 +85,7 @@ TEnv::StringVar CamCapSaveInPopupScene("CamCapSaveInPopupScene", "1"); TEnv::IntVar CamCapSaveInPopupAutoSubName("CamCapSaveInPopupAutoSubName", 1); TEnv::IntVar CamCapSaveInPopupCreateSceneInFolder( "CamCapSaveInPopupCreateSceneInFolder", 0); +TEnv::IntVar CamCapDoCalibration("CamCapDoCalibration", 0); namespace { @@ -1233,6 +1237,26 @@ StopMotionController::StopMotionController(QWidget *parent) : QWidget(parent) { innerSettingsLayout->addWidget(m_dslrFrame); m_dslrFrame->hide(); + // Calibration + m_calibrationUI.groupBox = new QGroupBox(tr("Calibration"), this); + m_calibrationUI.capBtn = new QPushButton(tr("Capture"), this); + m_calibrationUI.cancelBtn = new QPushButton(tr("Cancel"), this); + m_calibrationUI.newBtn = new QPushButton(tr("Start calibration"), this); + m_calibrationUI.loadBtn = new QPushButton(tr("Load"), this); + m_calibrationUI.exportBtn = new QPushButton(tr("Export"), this); + m_calibrationUI.label = new QLabel(this); + m_calibrationUI.groupBox->setCheckable(true); + m_calibrationUI.groupBox->setChecked(CamCapDoCalibration); + QAction *calibrationHelp = + new QAction(tr("Open Readme.txt for Camera calibration...")); + m_calibrationUI.groupBox->addAction(calibrationHelp); + m_calibrationUI.groupBox->setContextMenuPolicy(Qt::ActionsContextMenu); + m_calibrationUI.capBtn->hide(); + m_calibrationUI.cancelBtn->hide(); + m_calibrationUI.label->hide(); + m_calibrationUI.exportBtn->setEnabled(false); + connect(calibrationHelp, SIGNAL(triggered()), this, SLOT(onCalibReadme())); + QVBoxLayout *webcamSettingsLayout = new QVBoxLayout; webcamSettingsLayout->setSpacing(0); webcamSettingsLayout->setMargin(5); @@ -1360,6 +1384,27 @@ StopMotionController::StopMotionController(QWidget *parent) : QWidget(parent) { imageFrame->setLayout(imageLay); webcamGridLay->addWidget(imageFrame, 6, 0, 1, 2); + // Calibration + QGridLayout *calibLay = new QGridLayout(); + calibLay->setMargin(8); + calibLay->setHorizontalSpacing(3); + calibLay->setVerticalSpacing(5); + { + calibLay->addWidget(m_calibrationUI.newBtn, 0, 0); + calibLay->addWidget(m_calibrationUI.loadBtn, 0, 1); + calibLay->addWidget(m_calibrationUI.exportBtn, 0, 2); + QHBoxLayout *lay = new QHBoxLayout(); + lay->setMargin(0); + lay->setSpacing(5); + lay->addWidget(m_calibrationUI.capBtn, 1); + lay->addWidget(m_calibrationUI.label, 0); + lay->addWidget(m_calibrationUI.cancelBtn, 1); + calibLay->addLayout(lay, 1, 0, 1, 3); + } + calibLay->setColumnStretch(0, 1); + m_calibrationUI.groupBox->setLayout(calibLay); + webcamGridLay->addWidget(m_calibrationUI.groupBox, 7, 0, 1, 2); + webcamSettingsLayout->addLayout(webcamGridLay); webcamSettingsLayout->addStretch(); @@ -1941,6 +1986,24 @@ StopMotionController::StopMotionController(QWidget *parent) : QWidget(parent) { ret = ret && connect(m_stopMotion->m_webcam, SIGNAL(updateHistogram(cv::Mat)), this, SLOT(onUpdateHistogramCalled(cv::Mat))); + // Calibration + ret = ret && connect(m_calibrationUI.groupBox, &QGroupBox::toggled, + [&](bool checked) { + CamCapDoCalibration = checked; + m_stopMotion->m_calibration.isEnabled = checked; + resetCalibSettingsFromFile(); + }); + ret = ret && connect(m_calibrationUI.capBtn, SIGNAL(clicked()), this, + SLOT(onCalibCapBtnClicked())); + ret = ret && connect(m_calibrationUI.newBtn, SIGNAL(clicked()), this, + SLOT(onCalibNewBtnClicked())); + ret = ret && connect(m_calibrationUI.cancelBtn, SIGNAL(clicked()), this, + SLOT(resetCalibSettingsFromFile())); + ret = ret && connect(m_calibrationUI.loadBtn, SIGNAL(clicked()), this, + SLOT(onCalibLoadBtnClicked())); + ret = ret && connect(m_calibrationUI.exportBtn, SIGNAL(clicked()), this, + SLOT(onCalibExportBtnClicked())); + // Lighting Connections ret = ret && connect(m_screen1ColorFld, SIGNAL(colorChanged(const TPixel32 &, bool)), @@ -2024,6 +2087,9 @@ StopMotionController::StopMotionController(QWidget *parent) : QWidget(parent) { ret = ret && connect(m_stopMotion, SIGNAL(updateTestShots()), this, SLOT(onRefreshTests())); + // Calibration + ret = ret && connect(m_stopMotion, SIGNAL(calibrationImageCaptured()), this, + SLOT(onCalibImageCaptured())); assert(ret); m_placeOnXSheetCB->setChecked( @@ -2051,6 +2117,8 @@ StopMotionController::StopMotionController(QWidget *parent) : QWidget(parent) { m_stopMotion->setToNextNewLevel(); m_saveInFileFld->setPath(m_stopMotion->getFilePath()); + m_stopMotion->m_calibration.isEnabled = m_calibrationUI.groupBox->isChecked(); + #ifndef _WIN32 m_directShowCB->hide(); #endif @@ -2773,6 +2841,12 @@ void StopMotionController::onCameraListComboActivated(int comboIndex) { m_stopMotion->changeCameras(comboIndex); m_stopMotion->updateStopMotionControls(m_stopMotion->m_usingWebcam); + + if (m_calibrationUI.groupBox->isChecked() && comboIndex > 0) { + m_stopMotion->m_calibration.isValid = false; + m_calibrationUI.exportBtn->setEnabled(false); + if (m_stopMotion->m_usingWebcam) resetCalibSettingsFromFile(); + } } //----------------------------------------------------------------------------- @@ -2862,6 +2936,10 @@ void StopMotionController::onNewWebcamResolutionSelected(int index) { void StopMotionController::onResolutionComboActivated(const QString &itemText) { m_stopMotion->setWebcamResolution(itemText); + + m_stopMotion->m_calibration.isValid = false; + m_calibrationUI.exportBtn->setEnabled(false); + if (m_stopMotion->m_usingWebcam) resetCalibSettingsFromFile(); } //----------------------------------------------------------------------------- @@ -3908,4 +3986,313 @@ void StopMotionController::clearTests() { } m_testHBoxes.clear(); } -} \ No newline at end of file +} + +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibCapBtnClicked() { + if (!m_stopMotion->m_hasLiveViewImage || + m_stopMotion->m_liveViewStatus != + m_stopMotion->LiveViewStatus::LiveViewOpen) { + DVGui::warning(tr("Cannot capture image unless live view is active.")); + return; + } + m_stopMotion->m_calibration.captureCue = true; +} + +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibNewBtnClicked() { + if (m_stopMotion->m_calibration.isValid) { + QString question = tr("Do you want to restart camera calibration?"); + int ret = + DVGui::MsgBox(question, QObject::tr("Restart"), QObject::tr("Cancel")); + if (ret == 0 || ret == 2) return; + } + // initialize calibration parameter + m_stopMotion->m_calibration.filePath = getCurrentCalibFilePath(); + m_stopMotion->m_calibration.captureCue = false; + m_stopMotion->m_calibration.refCaptured = 0; + m_stopMotion->m_calibration.obj_points.clear(); + m_stopMotion->m_calibration.image_points.clear(); + m_stopMotion->m_calibration.isValid = false; + + // initialize label + m_calibrationUI.label->setText( + QString("%1/%2").arg(m_stopMotion->m_calibration.refCaptured).arg(10)); + // swap UIs + m_calibrationUI.newBtn->hide(); + m_calibrationUI.loadBtn->hide(); + m_calibrationUI.exportBtn->hide(); + m_calibrationUI.label->show(); + m_calibrationUI.capBtn->show(); + m_calibrationUI.cancelBtn->show(); +} + +//----------------------------------------------------------------------------- + +void StopMotionController::resetCalibSettingsFromFile() { + if (m_calibrationUI.capBtn->isVisible()) { + // swap UIs + m_calibrationUI.label->hide(); + m_calibrationUI.capBtn->hide(); + m_calibrationUI.cancelBtn->hide(); + m_calibrationUI.newBtn->show(); + m_calibrationUI.loadBtn->show(); + m_calibrationUI.exportBtn->show(); + } + if (m_calibrationUI.groupBox->isChecked() && + !m_stopMotion->m_calibration.isValid) { + QString calibFp = getCurrentCalibFilePath(); + std::cout << calibFp.toStdString() << std::endl; + if (!calibFp.isEmpty() && QFileInfo(calibFp).exists()) { + cv::Mat intrinsic, distCoeffs, new_intrinsic; + cv::FileStorage fs(calibFp.toStdString(), cv::FileStorage::READ); + if (!fs.isOpened()) return; + std::string identifierStr; + fs["identifier"] >> identifierStr; + if (identifierStr != "OpenToonzCameraCalibrationSettings") return; + cv::Size resolution; + QSize currentResolution(m_stopMotion->m_webcam->getWebcamWidth(), + m_stopMotion->m_webcam->getWebcamHeight()); + fs["resolution"] >> resolution; + if (currentResolution != QSize(resolution.width, resolution.height)) + return; + fs["instrinsic"] >> intrinsic; + fs["distCoeffs"] >> distCoeffs; + fs["new_intrinsic"] >> new_intrinsic; + fs.release(); + + cv::Mat mapR = cv::Mat::eye(3, 3, CV_64F); + cv::initUndistortRectifyMap( + intrinsic, distCoeffs, mapR, new_intrinsic, + cv::Size(currentResolution.width(), currentResolution.height()), + CV_32FC1, m_stopMotion->m_calibration.mapX, + m_stopMotion->m_calibration.mapY); + + m_stopMotion->m_calibration.isValid = true; + m_stopMotion->m_calibration.filePath = calibFp; + m_calibrationUI.exportBtn->setEnabled(true); + } + } +} + +//----------------------------------------------------------------------------- + +void StopMotionController::captureCalibrationRefImage(cv::Mat &image) { + cv::cvtColor(image, image, cv::COLOR_RGB2GRAY); + std::vector corners; + bool found = cv::findChessboardCorners( + image, m_stopMotion->m_calibration.boardSize, corners, + cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_FILTER_QUADS); + if (found) { + // compute corners in detail + cv::cornerSubPix( + image, corners, cv::Size(11, 11), cv::Size(-1, -1), + cv::TermCriteria(cv::TermCriteria::EPS | cv::TermCriteria::MAX_ITER, 30, + 0.1)); + // count up + m_stopMotion->m_calibration.refCaptured++; + // register corners + m_stopMotion->m_calibration.image_points.push_back(corners); + // register 3d points in real world space + std::vector obj; + for (int i = 0; i < m_stopMotion->m_calibration.boardSize.width * + m_stopMotion->m_calibration.boardSize.height; + i++) + obj.push_back(cv::Point3f(i / m_stopMotion->m_calibration.boardSize.width, + i % m_stopMotion->m_calibration.boardSize.width, + 0.0f)); + m_stopMotion->m_calibration.obj_points.push_back(obj); + + // needs 10 references + if (m_stopMotion->m_calibration.refCaptured < 10) { + // update label + m_calibrationUI.label->setText( + QString("%1/%2") + .arg(m_stopMotion->m_calibration.refCaptured) + .arg(10)); + } else { + // swap UIs + m_calibrationUI.label->hide(); + m_calibrationUI.capBtn->hide(); + m_calibrationUI.cancelBtn->hide(); + m_calibrationUI.newBtn->show(); + m_calibrationUI.loadBtn->show(); + m_calibrationUI.exportBtn->show(); + + cv::Mat intrinsic = cv::Mat(3, 3, CV_32FC1); + intrinsic.ptr(0)[0] = 1.f; + intrinsic.ptr(1)[1] = 1.f; + cv::Mat distCoeffs; + std::vector rvecs; + std::vector tvecs; + cv::calibrateCamera(m_stopMotion->m_calibration.obj_points, + m_stopMotion->m_calibration.image_points, + image.size(), intrinsic, distCoeffs, rvecs, tvecs); + + cv::Mat mapR = cv::Mat::eye(3, 3, CV_64F); + cv::Mat new_intrinsic = cv::getOptimalNewCameraMatrix( + intrinsic, distCoeffs, image.size(), + 0.0); // setting the last argument to 1.0 will include all source + // pixels in the frame + cv::initUndistortRectifyMap( + intrinsic, distCoeffs, mapR, new_intrinsic, image.size(), CV_32FC1, + m_stopMotion->m_calibration.mapX, m_stopMotion->m_calibration.mapY); + + // save calibration settings + QString calibFp = getCurrentCalibFilePath(); + cv::FileStorage fs(calibFp.toStdString(), cv::FileStorage::WRITE); + if (!fs.isOpened()) { + DVGui::warning( + tr("Failed to save calibration settings to %1.").arg(calibFp)); + return; + } + fs << "identifier" + << "OpenToonzCameraCalibrationSettings"; + fs << "resolution" << cv::Size(m_stopMotion->m_webcam->getWebcamWidth(), + m_stopMotion->m_webcam->getWebcamHeight()); + fs << "instrinsic" << intrinsic; + fs << "distCoeffs" << distCoeffs; + fs << "new_intrinsic" << new_intrinsic; + fs.release(); + + m_stopMotion->m_calibration.isValid = true; + m_calibrationUI.exportBtn->setEnabled(true); + } + } +} + +//----------------------------------------------------------------------------- + +QString StopMotionController::getCurrentCalibFilePath() { + QString cameraName = m_cameraListCombo->currentText(); + if (cameraName.isEmpty()) return QString(); + QString resolution = m_resolutionCombo->currentText(); + QString hostName = QHostInfo::localHostName(); + TFilePath folderPath = ToonzFolder::getLibraryFolder() + "camera calibration"; + return folderPath.getQString() + "\\" + hostName + "_" + cameraName + "_" + + resolution + ".xml"; +} + +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibLoadBtnClicked() { + LoadCalibrationFilePopup popup(this); + + QString fp = popup.getPath().getQString(); + if (fp.isEmpty()) return; + try { + cv::FileStorage fs(fp.toStdString(), cv::FileStorage::READ); + if (!fs.isOpened()) + throw TException(fp.toStdWString() + L": Can't open file"); + + std::string identifierStr; + fs["identifier"] >> identifierStr; + if (identifierStr != "OpenToonzCameraCalibrationSettings") + throw TException(fp.toStdWString() + L": Identifier does not match"); + cv::Size resolution; + QSize currentResolution(m_stopMotion->m_webcam->getWebcamWidth(), + m_stopMotion->m_webcam->getWebcamHeight()); + fs["resolution"] >> resolution; + if (currentResolution != QSize(resolution.width, resolution.height)) + throw TException(fp.toStdWString() + L": Resolution does not match"); + } catch (const TException &se) { + DVGui::warning(QString::fromStdWString(se.getMessage())); + return; + } catch (...) { + DVGui::error(tr("Couldn't load %1").arg(fp)); + return; + } + + if (m_stopMotion->m_calibration.isValid) { + QString question = tr("Overwriting the current calibration. Are you sure?"); + int ret = DVGui::MsgBox(question, QObject::tr("OK"), QObject::tr("Cancel")); + if (ret == 0 || ret == 2) return; + m_stopMotion->m_calibration.isValid = false; + } + + QString calibFp = getCurrentCalibFilePath(); + TSystem::copyFile(TFilePath(calibFp), TFilePath(fp), true); + resetCalibSettingsFromFile(); +} + +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibExportBtnClicked() { + // just in case + if (!m_stopMotion->m_calibration.isValid) return; + if (!QFileInfo(getCurrentCalibFilePath()).exists()) return; + + ExportCalibrationFilePopup popup(this); + + QString fp = popup.getPath().getQString(); + if (fp.isEmpty()) return; + + try { + { + QFileInfo fs(fp); + if (fs.exists() && !fs.isWritable()) { + throw TSystemException( + TFilePath(fp), + L"The file cannot be saved: it is a read only file."); + } + } + TSystem::copyFile(TFilePath(fp), TFilePath(getCurrentCalibFilePath()), + true); + } catch (const TSystemException &se) { + DVGui::warning(QString::fromStdWString(se.getMessage())); + } catch (...) { + DVGui::error(tr("Couldn't save %1").arg(fp)); + } +} + +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibReadme() { + TFilePath readmeFp = + ToonzFolder::getLibraryFolder() + "camera calibration" + "readme.txt"; + if (!TFileStatus(readmeFp).doesExist()) return; + if (TSystem::isUNC(readmeFp)) + QDesktopServices::openUrl(QUrl(readmeFp.getQString())); + else + QDesktopServices::openUrl(QUrl::fromLocalFile(readmeFp.getQString())); +} +//----------------------------------------------------------------------------- + +void StopMotionController::onCalibImageCaptured() { + cv::Mat camImage = m_stopMotion->m_webcam->getWebcamImage(); + captureCalibrationRefImage(camImage); +} + +//============================================================================= + +ExportCalibrationFilePopup::ExportCalibrationFilePopup(QWidget *parent) + : GenericSaveFilePopup(tr("Export Camera Calibration Settings")) { + Qt::WindowFlags flags = windowFlags(); + setParent(parent); + setWindowFlags(flags); + m_browser->enableGlobalSelection(false); + setFilterTypes(QStringList("xml")); +} + +void ExportCalibrationFilePopup::showEvent(QShowEvent *e) { + FileBrowserPopup::showEvent(e); + setFolder(ToonzFolder::getLibraryFolder() + "camera calibration"); +} + +//============================================================================= + +LoadCalibrationFilePopup::LoadCalibrationFilePopup(QWidget *parent) + : GenericLoadFilePopup(tr("Load Camera Calibration Settings")) { + Qt::WindowFlags flags = windowFlags(); + setParent(parent); + setWindowFlags(flags); + m_browser->enableGlobalSelection(false); + setFilterTypes(QStringList("xml")); +} + +void LoadCalibrationFilePopup::showEvent(QShowEvent *e) { + FileBrowserPopup::showEvent(e); + setFolder(ToonzFolder::getLibraryFolder() + "camera calibration"); +} diff --git a/toonz/sources/stopmotion/stopmotioncontroller.h b/toonz/sources/stopmotion/stopmotioncontroller.h index bb1ee165..0310b453 100644 --- a/toonz/sources/stopmotion/stopmotioncontroller.h +++ b/toonz/sources/stopmotion/stopmotioncontroller.h @@ -10,6 +10,7 @@ #include "tfilepath.h" #include "toonz/tproject.h" +#include "filebrowserpopup.h" // TnzQt includes #include "toonzqt/tabbar.h" @@ -236,6 +237,17 @@ class StopMotionController final : public QWidget { QVBoxLayout *m_testsInsideLayout; int m_testsImagesPerRow; + // calibration feature + struct CalibrationUI { + QPushButton *capBtn, *newBtn, *loadBtn, *cancelBtn, *exportBtn; + QLabel *label; + QGroupBox *groupBox; + } m_calibrationUI; + + void captureCalibrationRefImage(cv::Mat &procImage); + + QString getCurrentCalibFilePath(); + public: StopMotionController(QWidget *parent = 0); ~StopMotionController(); @@ -415,8 +427,38 @@ protected slots: void onRefreshTests(); void clearTests(); + void onCalibCapBtnClicked(); + void onCalibNewBtnClicked(); + void resetCalibSettingsFromFile(); + void onCalibLoadBtnClicked(); + void onCalibExportBtnClicked(); + void onCalibImageCaptured(); + void onCalibReadme(); + public slots: void openSaveInFolderPopup(); }; +//============================================================================= + +class ExportCalibrationFilePopup final : public GenericSaveFilePopup { + Q_OBJECT +public: + ExportCalibrationFilePopup(QWidget *parent); + +protected: + void showEvent(QShowEvent *) override; +}; + +//============================================================================= + +class LoadCalibrationFilePopup final : public GenericLoadFilePopup { + Q_OBJECT +public: + LoadCalibrationFilePopup(QWidget *parent); + +protected: + void showEvent(QShowEvent *) override; +}; + #endif // STOPMOTIONCONTROLLER_H diff --git a/toonz/sources/stopmotion/webcam.cpp b/toonz/sources/stopmotion/webcam.cpp index d5511cb7..3f665f17 100644 --- a/toonz/sources/stopmotion/webcam.cpp +++ b/toonz/sources/stopmotion/webcam.cpp @@ -92,7 +92,8 @@ int Webcam::getIndexOfResolution() { //----------------------------------------------------------------- -bool Webcam::getWebcamImage(TRaster32P& tempImage) { +bool Webcam::getWebcamImage(TRaster32P& tempImage, bool useCalibration, + cv::Mat calibrationMapX, cv::Mat calibrationMapY) { bool error = false; cv::Mat imgOriginal; cv::Mat imgCorrected; @@ -146,6 +147,14 @@ bool Webcam::getWebcamImage(TRaster32P& tempImage) { cv::cvtColor(imgCorrected, imgCorrected, cv::COLOR_GRAY2BGRA); } + // perform calibration + if (useCalibration) { + cv::remap(imgCorrected, imgCorrected, calibrationMapX, calibrationMapY, + cv::INTER_LINEAR); + } + + m_webcamImage = imgCorrected; + int width = m_cvWebcam.get(3); int height = m_cvWebcam.get(4); int size = imgCorrected.total() * imgCorrected.elemSize(); diff --git a/toonz/sources/stopmotion/webcam.h b/toonz/sources/stopmotion/webcam.h index 5a14b888..a20acffb 100644 --- a/toonz/sources/stopmotion/webcam.h +++ b/toonz/sources/stopmotion/webcam.h @@ -41,7 +41,9 @@ public: QCamera* getWebcam() { return m_webcam; } void setWebcam(QCamera* camera); bool initWebcam(int index = 0); - bool getWebcamImage(TRaster32P& tempImage); + bool getWebcamImage(TRaster32P& tempImage, bool useCalibration = false, + cv::Mat calibrationMapX = cv::Mat(), + cv::Mat calibrationMapY = cv::Mat()); bool translateIndex(int index);