diff --git a/README.md b/README.md index bd6288c..0e357d3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ -# RotoCanvasPaint -This is a (open source) manual rotoscoping (frame painting) application with disregard to unavoidable caveats of such a program (see Caveats in README); written in Qt. +# RotoCanvas + +## Purpose +This is a manual rotoscoping (frame by frame painting) application. Rotoscoping is the only accurate way to achieve effects such as manual object removal (such as removing wires for flying scenes), cartoons mixed with live action, correction of layer order errors (such as if the wrong fingers get in the way of a virtual object that is held), doing energy effects without 3D mockups of characters (3D mockups are normally needed in order to address situations where part or all of the energy effect goes behind a character or some of the character's fingers), or detailed junk matte tweaking (such as is needed when all or part of an actor appears in front of a studio wall instead of a green screen). Before the digital age, these issues were address by hand, and rotoscoping was considered an essential part of post effects to address a whole domain of issues. The fact that there are so few applications like this is a shame. Ideally this project will be used as a basis to create plugins (see Developer Notes) so that more video editing applications will include rotoscoping, however, RotoCanvas is being tested and compiled as standalone application so that the software can be used immediately. + +### Why aren't there more rotoscoping applications? +Possibly, rotoscoping applications are not considered commercially viable since there is unavoidable lag situations that can't be reasonably blamed on developers but inevitably are (see "Caveats" below). Unavoidable lag and format complexity issues could be reasons for the discontinuation of such programs as Ulead ® VideoPaint ®. Another reason could be that rotoscoping is highly dependent on the source frame remaining the same, whereas MPEG (variants of which are used almost everywhere) has inherently inexact frame seeking. These issues may be partially or completely resolved by advanced caching (for the lag issue) and advanced frame seeking algorithms (for the accuracy issue), either of which are not easily achieved but both of which are needed in a professional video application (since video, which has slow seeking would be required, and MPEG, which is normally inaccurate would be required). Even if image sequences are used to resolve the seek lag and seek accuracy issues, seek lag normally remains simply because the edits have to be re-applied or re-loaded (basically, a multi-layer image project needs to be loaded each time seeking to a different frame). Leaving rotoscoping out entirely is often seen as the only way to avoid these issues. This program aims to implement rotoscoping regardless of the possibility of seek lag or requiring image sequences (to avoid seek accuracy issues), since in this project, rotoscoping is considered to be an essential feature. + +## Caveats +* Lag during frame loading cannot be avoided, since each video frame must be loaded at full quality, which at 1080p takes up an unavoidable 8100kb per layer. Maximum performance could be achieved when one or more frames in either direction of the current frame are cached, in their edited form. However, upon editing, the cache will have to be updated and the image, redrawn. To prevent further lag in that situation, the source frame (base layer) could be cached so that editing layers can be applied without reloading the frame from the source video file. +* At this time, image sequences are required. MPEG-derived formats may or may not ever be added, since MPEG-style frame seeking is inexact and rotoscoping is highly dependent on the source frame remaining the same. + +## Known Issues +* not yet done first working version (expected August 2016) +* initially will only input and output png sequences. +* layer cache (purpose for unused variable cacheMaxMB) is not yet implemented + +## Developer Notes +The RotoCanvas (based on ScribbleArea from Qt Examples) class is modular, with hopes that it can be used by various video editing applications in the future. The recommended use of RotoCanvas in a video editing application is for applying effects to source media as a preprocessing step before they are trimmed or other effects are added, since rotoscoping is highly dependent on the source frame (base layer) remaining the same. Using the RotoCanvas as a post-processing effect is possible, but accurate frame seeking must be assured somehow (such as by a frame-accurate video editing engine), and further edits to the previous layers will in some cases cause the rotoscoped parts (parts of the image edited by RotoCanvas) to no longer make sense (such as, if a lens pinch effect is added to a scene where there was a layer order error that has been rotoscoped out, instead of the error being rotoscoped out, there will be both the error and a corrected blotch that is the error's original position & shape), which in such cases would require redoing the rotoscoping. + +### Storage Method +where is sequence name (such as, if mygreatvideo0000.png is first frame, is mygreatvideo) + [folder] + rotocanvas.yml [not yet implemented] + frames [folder] + [folder; only exists if frame is a keyframe] + alpha.png [file where only alpha channel is used (and applied to background upon export)] + layers [folder] + .png + .yml [not yet implemented] + + \ No newline at end of file diff --git a/examples/movie - Shortcut.blnk b/examples/movie - Shortcut.blnk new file mode 100644 index 0000000..6580f06 --- /dev/null +++ b/examples/movie - Shortcut.blnk @@ -0,0 +1,8 @@ +Content-Type: text/blnk +Type:Directory +NoDisplay:true +Terminal:false +Name:movie - Shortcut +Encoding:UTF-8 +Comment:File or folder shortcut generated by lnk-to-blnk +Exec:C:\Qt\Examples\Qt-5.7\widgets\widgets\movie diff --git a/examples/scribble - Shortcut.blnk b/examples/scribble - Shortcut.blnk new file mode 100644 index 0000000..c963197 --- /dev/null +++ b/examples/scribble - Shortcut.blnk @@ -0,0 +1,8 @@ +Content-Type: text/blnk +Type:Directory +NoDisplay:true +Terminal:false +Name:scribble - Shortcut +Encoding:UTF-8 +Comment:File or folder shortcut generated by lnk-to-blnk +Exec:C:\Qt\Examples\Qt-5.7\widgets\widgets\scribble diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..086deab --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + MainWindow window; + window.show(); + return app.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..54340fb --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,255 @@ +#include + +#include "mainwindow.h" +#include "rotocanvas.h" + +//! [0] +MainWindow::MainWindow() +{ + rotocanvas = nullptr; + vBoxLayout = nullptr; + statusLineEdit = nullptr; + + bool statusBarEnable=false; + + rotocanvas = new RotoCanvas; + + if (statusBarEnable) { + vBoxLayout = new QVBoxLayout; + vBoxLayout->addWidget(rotocanvas,1,Qt::AlignJustify); + + statusLineEdit = new QLineEdit; + vBoxLayout->addWidget(statusLineEdit,1,Qt::AlignBottom); + + //vBoxLayout->setSizeConstraint(QLayout::); + setLayout(vBoxLayout); + } + else setCentralWidget(rotocanvas); + + createActions(); + createMenus(); + + setWindowTitle(tr("RotoCanvas Paint")); + resize(500, 500); +} +//! [0] + +//! [1] +void MainWindow::closeEvent(QCloseEvent *event) +//! [1] //! [2] +{ + if (maybeSave()) { + event->accept(); + } else { + event->ignore(); + } +} +//! [2] + +//! [3] +void MainWindow::open() +//! [3] //! [4] +{ + if (maybeSave()) { + QString path=QDir::currentPath(); + QString tryPath="C:\\Users\\Owner\\Videos\\NTWAOG (Music Video)\\Media\\Sequence 00092 hovering\\00092a"; + QDir defaultDir=QDir(tryPath); + if (defaultDir.exists()) path=tryPath; + QString fileName = QFileDialog::getOpenFileName(this, + tr("Open File"), path); + if (!fileName.isEmpty()) + rotocanvas->openImage(fileName); + } +} +//! [4] + +//! [5] +void MainWindow::save() +//! [5] //! [6] +{ + QAction *action = qobject_cast(sender()); + QByteArray fileFormat = action->data().toByteArray(); + saveFile(fileFormat); +} +//! [6] + +//! [7] +void MainWindow::askBrushColor() +//! [7] //! [8] +{ + QColor newColor = QColorDialog::getColor(rotocanvas->getBrushColor()); + if (newColor.isValid()) + rotocanvas->setBrushColor(newColor); +} + +void MainWindow::askBrushOpacity() +{ + bool ok; + double new_value = QInputDialog::getDouble(this, tr("RotoCanvas Paint"), + tr("Select brush opacity (0.0 to 1.0):"), + rotocanvas->getBrushOpacity(), + 0.0, 1.0, 2, &ok); + if (ok) + rotocanvas->setBrushOpacity(new_value); +} +//! [8] + +//! [9] +void MainWindow::askBrushRadius() +//! [9] //! [10] +{ + bool ok; + double new_value = QInputDialog::getDouble(this, tr("RotoCanvas Paint"), + tr("Select brush radius:"), + rotocanvas->getBrushRadius(), + 0.0, 1.0, 2, &ok); + if (ok) + rotocanvas->setBrushRadius(new_value); +} +//! [10] + +void MainWindow::askBrushHardness() +{ + bool ok; + double new_value = QInputDialog::getDouble(this, tr("RotoCanvas Paint"), + tr("Select brush hardness (0 to 1):"), + rotocanvas->getBrushHardness(), + 0.0, 1.0, 2, &ok); + if (ok) + rotocanvas->setBrushHardness(new_value); +} + +//! [11] +void MainWindow::about() +//! [11] //! [12] +{ + QMessageBox::about(this, tr("About RotoCanvas Paint"), + tr("

RotoCanvas Paint is a manual rotoscoping (frame by " + "frame painting) application.

" + "

Lag when starting to paint is normal due to loading or " + "initialization of layers, even though when seeking to a frame " + "previously cached, the cached (layerless) frame is initially used.

" + "

This program uses the RotoCanvas widget based on the Scribble Qt Example.

")); +} +//! [12] + +//! [13] +void MainWindow::createActions() +//! [13] //! [14] +{ + openAct = new QAction(tr("&Open..."), this); + openAct->setShortcuts(QKeySequence::Open); + connect(openAct, SIGNAL(triggered()), this, SLOT(open())); + + foreach (QByteArray format, QImageWriter::supportedImageFormats()) { + QString text = tr("%1...").arg(QString(format).toUpper()); + + QAction *action = new QAction(text, this); + action->setData(format); + connect(action, SIGNAL(triggered()), this, SLOT(save())); + saveAsActs.append(action); + } + + printAct = new QAction(tr("&Print..."), this); + connect(printAct, SIGNAL(triggered()), rotocanvas, SLOT(print())); + + exitAct = new QAction(tr("E&xit"), this); + exitAct->setShortcuts(QKeySequence::Quit); + connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); + + brushColorAct = new QAction(tr("&Brush Color..."), this); + connect(brushColorAct, SIGNAL(triggered()), this, SLOT(askBrushColor())); + + brushWidthAct = new QAction(tr("Brush &Radius..."), this); + connect(brushWidthAct, SIGNAL(triggered()), this, SLOT(askBrushRadius())); + + brushHardnessAct = new QAction(tr("Brush &Hardness..."), this); + connect(brushHardnessAct, SIGNAL(triggered()), this, SLOT(askBrushHardness())); + + brushOpacityAct = new QAction(tr("Brush &Opacity..."), this); + connect(brushOpacityAct, SIGNAL(triggered()), this, SLOT(askOpacity())); + + clearScreenAct = new QAction(tr("&Clear Screen"), this); + clearScreenAct->setShortcut(tr("Ctrl+L")); + connect(clearScreenAct, SIGNAL(triggered()), + rotocanvas, SLOT(clearImage())); + + aboutAct = new QAction(tr("&About"), this); + connect(aboutAct, SIGNAL(triggered()), this, SLOT(about())); + + aboutQtAct = new QAction(tr("About &Qt"), this); + connect(aboutQtAct, SIGNAL(triggered()), qApp, SLOT(aboutQt())); +} +//! [14] + +//! [15] +void MainWindow::createMenus() +//! [15] //! [16] +{ + saveAsMenu = new QMenu(tr("&Save As"), this); + foreach (QAction *action, saveAsActs) + saveAsMenu->addAction(action); + + fileMenu = new QMenu(tr("&File"), this); + fileMenu->addAction(openAct); + fileMenu->addMenu(saveAsMenu); + fileMenu->addAction(printAct); + fileMenu->addSeparator(); + fileMenu->addAction(exitAct); + + optionMenu = new QMenu(tr("&Options"), this); + optionMenu->addAction(brushColorAct); + optionMenu->addAction(brushWidthAct); + optionMenu->addAction(brushHardnessAct); + optionMenu->addSeparator(); + optionMenu->addAction(clearScreenAct); + + helpMenu = new QMenu(tr("&Help"), this); + helpMenu->addAction(aboutAct); + helpMenu->addAction(aboutQtAct); + + menuBar()->addMenu(fileMenu); + menuBar()->addMenu(optionMenu); + menuBar()->addMenu(helpMenu); +} +//! [16] + +//! [17] +bool MainWindow::maybeSave() +//! [17] //! [18] +{ + if (rotocanvas->getIsModified()) { + QMessageBox::StandardButton ret; + ret = QMessageBox::warning(this, tr("RotoCanvas Paint"), + tr("The image has been modified.\n" + "Do you want to save your changes?"), + QMessageBox::Save | QMessageBox::Discard + | QMessageBox::Cancel); + if (ret == QMessageBox::Save) { + return saveFile("png"); + } else if (ret == QMessageBox::Cancel) { + return false; + } + } + return true; +} +//! [18] + +//! [19] +bool MainWindow::saveFile(const QByteArray &fileFormat) +//! [19] //! [20] +{ + QString initialPath = QDir::currentPath() + "/untitled." + fileFormat; + + QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), + initialPath, + tr("%1 Files (*.%2);;All Files (*)") + .arg(QString::fromLatin1(fileFormat.toUpper())) + .arg(QString::fromLatin1(fileFormat))); + if (fileName.isEmpty()) { + return false; + } else { + return rotocanvas->saveImage(fileName, fileFormat.constData()); + } +} +//! [20] diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..103117b --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,61 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include + +class RotoCanvas; + +//! [0] +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(); + +protected: + void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; + +private slots: + void open(); + void save(); + void askBrushColor(); + void askBrushOpacity(); + void askBrushRadius(); + void askBrushHardness(); + void about(); + +private: + void createActions(); + void createMenus(); + bool maybeSave(); + bool saveFile(const QByteArray &fileFormat); + + QVBoxLayout *vBoxLayout; + + RotoCanvas *rotocanvas; + QLineEdit *statusLineEdit; + + QMenu *saveAsMenu; + QMenu *fileMenu; + QMenu *optionMenu; + QMenu *helpMenu; + + QAction *openAct; + QList saveAsActs; + QAction *exitAct; + QAction *brushColorAct; + QAction *brushWidthAct; + QAction *brushHardnessAct; + QAction *brushOpacityAct; + QAction *printAct; + QAction *clearScreenAct; + QAction *aboutAct; + QAction *aboutQtAct; +}; +//! [0] + +#endif diff --git a/rcpaint.pro b/rcpaint.pro new file mode 100644 index 0000000..9191d42 --- /dev/null +++ b/rcpaint.pro @@ -0,0 +1,15 @@ +QT += widgets +qtHaveModule(printsupport): QT += printsupport + +HEADERS = mainwindow.h \ + rotocanvas.h \ + rotocanvaslayer.h +SOURCES = main.cpp \ + mainwindow.cpp \ + rotocanvas.cpp \ + rotocanvaslayer.cpp + +# install +# target.path = $$[QT_INSTALL_EXAMPLES]/widgets/widgets/scribble + +INSTALLS += target diff --git a/rotocanvas.cpp b/rotocanvas.cpp new file mode 100644 index 0000000..4d63551 --- /dev/null +++ b/rotocanvas.cpp @@ -0,0 +1,574 @@ +#include +#ifndef QT_NO_PRINTER +#include +#include +#endif + +#include "rotocanvas.h" + +//! [0] +RotoCanvas::RotoCanvas(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_StaticContents); + isModified = false; + isToolActive = false; + selectedLayerIndex = -1; + //frameNumber = -1; + //minDigitCount = 4; + cacheMaxMB = 800.0; //NOTE: 1920*1080*4 = 8294400 = ~7.91 MB, so 800MB is around 100 HD frames + brushRadius = 5.0; + brushHardRadius = 0.0; + brushOpacity = 1.0; + //formatString = ""; + brushColor = QColor(35,97,20,255); //Qt::green; + //Pigment Ajans green screen color: 0,203,27 + //Expert Multimedia Green Screen Color (LED lighting, muslin green screen): 35,97,20 + checkerDarkColor = QColor(128,128,128); + checkerLightColor = QColor(192,192,192); + outputSize.setWidth(0); + outputSize.setHeight(0); + loadedFI=nullptr; + selectedLayerIndex=0; +} + +QString RotoCanvas::getSeqName(QString framePath) +{//static + //QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getSeqName(frameFI); +} + +QString RotoCanvas::getSeqName(QFileInfo frameFI) +{ + QString result; + QString baseName=frameFI.completeBaseName(); //baseName would get file from file.tar.gz, completeBaseName gets file.tar + result=baseName; + return result; +} + +QString RotoCanvas::getSeqName() +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getSeqName(*this->loadedFI); + else return ""; +} + +QString RotoCanvas::getSeqFormatString(QString framePath) +{ + //QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getSeqFormatString(frameFI); +} + +QString RotoCanvas::getSeqFormatString(QFileInfo frameFI) +{ + QString result=frameFI.suffix(); + return result; +} + +QString RotoCanvas::getSeqFormatString() +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getSeqFormatString(*this->loadedFI); + else return ""; +} + +QString RotoCanvas::getFolderPath(QString framePath) +{ + QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getFolderPath(frameFI); +} + +QString RotoCanvas::getFolderPath(QFileInfo frameFI) +{ + QString result=frameFI.absoluteDir().path(); + return result; +} + +QString RotoCanvas::getFolderPath() +{ + if (this->loadedFI!=nullptr) RotoCanvas::getFolderPath(*this->loadedFI); + else return ""; +} + +QString RotoCanvas::getSeqPaddedFrameNumber(QString framePath) +{ + //QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getSeqPaddedFrameNumber(frameFI); +} + +QString RotoCanvas::getSeqPaddedFrameNumber(QFileInfo frameFI) +{ + QString baseName=frameFI.completeBaseName(); + int digitCount=RotoCanvas::getSeqDigitCount(frameFI); + QString result=baseName.right(digitCount); + return result; +} + +QString RotoCanvas::getSeqPaddedFrameNumber() +{ + if (this->loadedFI!=nullptr) RotoCanvas::getSeqPaddedFrameNumber(*this->loadedFI); + else return ""; +} + +QString RotoCanvas::getLayersFolderPath(QString framePath, int frameNumber, bool createEnable) +{ + //QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getLayersFolderPath(frameFI, frameNumber, createEnable); +} + +QString RotoCanvas::getLayersFolderPath(QFileInfo frameFI, int frameNumber, bool createEnable) +{ + //such as /frames//layers + QString seqName=RotoCanvas::getSeqName(frameFI); + qInfo() << "seqName:" << seqName; //see also qInfo,qDebug,qWarning,qCritical,qFatal + QString seqPath=frameFI.dir().filePath(seqName); + QDir seqDir(seqPath); + if (createEnable) { + if (!seqDir.exists()) frameFI.dir().mkdir(seqName); + } + QString framesPath=frameFI.dir().filePath("frames"); + qInfo()<<"framesPath:"<loadedFI!=nullptr) return RotoCanvas::getLayersFolderPath(*this->loadedFI, frameNumber, createEnable); + else return ""; +} + +QString RotoCanvas::getLayerImagePathMostRecent(QString framePath, int frameNumber, int layerNumber) +{ + //QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getLayerImagePathMostRecent(frameFI, frameNumber, createEnable); +} + +QString RotoCanvas::getLayerImagePathMostRecent(QFileInfo frameFI, int frameNumber, int layerNumber) +{ + QString result=""; + QString layersPath=RotoCanvas::getLayersFolderPath(frameFI,frameNumber,false); + QDir layersDir(layersPath); + QString thisLayerImagePath=layersDir.filePath(QString::number(layerNumber)+".png"); + QFileInfo thisLayerImageFI(thisLayerImagePath); + int thisFrameNumber=frameNumber; + while (thisFrameNumber>=0 && !thisLayerImageFI.exists()) { + layersPath=RotoCanvas::getLayersFolderPath(frameFI,frameNumber,false); + layersDir=QDir(layersPath); + thisLayerImagePath=layersDir.filePath(QString::number(layerNumber)+".png"); + thisLayerImageFI=QFileInfo(thisLayerImagePath); + thisFrameNumber--; + } + if (thisLayerImageFI.exists()) result=thisLayerImageFI.path(); + return result; +} + +QString RotoCanvas::getLayerImagePathMostRecent(int frameNumber, int layerNumber) +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getLayerImagePathMostRecent(*this->loadedFI, frameNumber, layerNumber); + else return ""; +} + +int RotoCanvas::getSeqFrameNumber(QString framePath) +{ + int result=-1; + QString resultString=RotoCanvas::getSeqPaddedFrameNumber(framePath); + while (resultString.length()>0 && resultString.left(1)=="0") { + resultString=resultString.right(resultString.length()-1); + } + bool ok=false; + result = resultString.toInt(&ok); + if (!ok) result=-1; + return result; +} + +int RotoCanvas::getSeqFrameNumber(QFileInfo frameFI) +{ + int result=-1; + QString resultString=RotoCanvas::getSeqPaddedFrameNumber(frameFI); + while (resultString.length()>0 && resultString.left(1)=="0") { + resultString=resultString.right(resultString.length()-1); + } + bool ok=false; + result = resultString.toInt(&ok); + if (!ok) result=-1; + return result; +} + +int RotoCanvas::getSeqFrameNumber() +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getSeqFrameNumber(*this->loadedFI); + else return -1; +} + +int RotoCanvas::getSeqDigitCount(QString framePath) +{ + QString result; + QFileInfo frameFI(framePath); + return RotoCanvas::getSeqDigitCount(frameFI); +} + +int RotoCanvas::getSeqDigitCount(QFileInfo frameFI) +{ + int result=-1; + QString baseName=frameFI.completeBaseName(); + int digitCount=0; + int index=baseName.length()-1; + while (index>=0) { + if (baseName.at(index).isDigit()) { + index--; + digitCount++; + } + else { + break; + } + } + if (digitCount>0) result=baseName.left(baseName.length()-digitCount).toInt(); + return result; +} + +int RotoCanvas::getSeqDigitCount() +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getSeqDigitCount(*this->loadedFI); + else return -1; +} + +QString RotoCanvas::getSeqFramePath(QString folderPath, QString seqName, int frameNumber, int minDigitCount, QString format) +{ + QString fileName=seqName+RotoCanvas::getZeroPadded(frameNumber,minDigitCount)+"."+format; + return QDir::cleanPath(folderPath+QDir::separator()+fileName); +} + +QString RotoCanvas::getSeqFramePath(int frameNumber) +{ + if (this->loadedFI!=nullptr) return RotoCanvas::getSeqFramePath(this->loadedFI->dir().path(), getSeqName(),getSeqDigitCount(),getSeqFormatString()); + else return ""; +} + +QString RotoCanvas::getZeroPadded(int frameNumber, int minDigitCount) +{ + QString result=QString::number(frameNumber); + QString zeroString=QString::number(0); + while (result.length()outputSize = loadedImage.size(); + if (this->loadedFI!=nullptr) { + delete this->loadedFI; + this->loadedFI=nullptr; + } + this->loadedFI = new QFileInfo(fileName); + + //this->formatString = loadedFI.suffix(); + QSize newSize = loadedImage.size().expandedTo(size()); + //resizeImage(&loadedImage, this->outputSize); + resizeImage(&displayImage, newSize); //resizeImage(&) + //backgroundImage = loadedImage; + backgroundImage = QImage(loadedImage.size(),QImage::Format_RGB32); + + backgroundImage.fill(Qt::transparent); + QPainter bgPainter(&backgroundImage); + bgPainter.drawImage(QPoint(0,0),loadedImage); + + QPainter painter(&displayImage); + + isModified = false; + while (layerPtrs.length()>0) { + RotoCanvasLayer* thisLayer=layerPtrs.takeLast(); + if (thisLayer!=nullptr) delete thisLayer; + } + QString layersPath=getLayersFolderPath(getSeqFrameNumber(),false); + QDir layersDir=QDir(layersPath); + int thisLayerNumber=0; + QString thisLayerPath=layersDir.filePath(QString::number(thisLayerNumber)+".png"); + QFileInfo thisLayerFI=QFileInfo(thisLayerPath); + int thisLayerFrameNumber=getSeqFrameNumber(); + while (thisLayerFI.exists()) { + qInfo()<<"Found layer file: "<load(thisLayerPath); + RotoCanvasLayer* thisLayerPtr=new RotoCanvasLayer(); + thisLayerPtr->frameNumber=thisLayerFrameNumber; + thisLayerPtr->image.load(thisLayerPath); + //TODO: seek backward to get image from previous keyframe + layerPtrs.append(thisLayerPtr); + painter.drawImage(QPoint(0, 0), thisLayerPtr->image); + thisLayerNumber++; + thisLayerPath=layersDir.filePath(QString::number(thisLayerNumber)); + thisLayerFI=QFileInfo(thisLayerPath); + } + this->selectedLayerIndex=0; + update(); + return true; +} +//! [2] + +//! [3] +bool RotoCanvas::saveImage(const QString &fileName, const char *fileFormat) +//! [3] //! [4] +{ + QImage visibleImage = displayImage; + resizeImage(&visibleImage, size()); + + if (visibleImage.save(fileName, fileFormat)) { + isModified = false; + return true; + } else { + return false; + } +} +//! [4] + +//! [5] +void RotoCanvas::setBrushColor(const QColor &newColor) +//! [5] //! [6] +{ + brushColor = newColor; +} +//! [6] + +//! [7] +void RotoCanvas::setBrushRadius(double newWidth) +//! [7] //! [8] +{ + double temp_hardness = getBrushHardness(); + brushRadius = newWidth; + setBrushHardness(temp_hardness); //repair brushHardRadius +} +//! [8] + +void RotoCanvas::setBrushHardness(double new_value) +{ + if (new_value>1.0) new_value=1.0; + else if (new_value<0.0) new_value=0.0f; + brushHardRadius = brushRadius-(brushRadius*(1.0-new_value)); +} + +void RotoCanvas::setBrushOpacity(double new_value) +{ + if (new_value>1.0) new_value=1.0; + else if (new_value<0.0) new_value=0.0f; + brushOpacity = new_value; +} + +//! [9] +void RotoCanvas::clearImage() +//! [9] //! [10] +{ + displayImage.fill(qRgb(255, 255, 255)); + isModified = true; + update(); +} +//! [10] + +//! [11] +void RotoCanvas::mousePressEvent(QMouseEvent *event) +//! [11] //! [12] +{ + if (event->button() == Qt::LeftButton) { + lastPoint = event->pos(); + isToolActive = true; + } +} + +void RotoCanvas::mouseMoveEvent(QMouseEvent *event) +{ + if ((event->buttons() & Qt::LeftButton) && isToolActive) + drawLineTo(event->pos()); +} + +void RotoCanvas::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton && isToolActive) { + drawLineTo(event->pos()); + isToolActive = false; + } +} + +//! [12] //! [13] +void RotoCanvas::paintEvent(QPaintEvent *event) +//! [13] //! [14] +{ + QPainter painter(this); + QRect dirtyRect = event->rect(); + painter.drawImage(dirtyRect, displayImage, dirtyRect); +} +//! [14] + +//! [15] +void RotoCanvas::resizeEvent(QResizeEvent *event) +//! [15] //! [16] +{ + if (width() > displayImage.width() || height() > displayImage.height()) { + int newWidth = qMax(width() + 128, displayImage.width()); + int newHeight = qMax(height() + 128, displayImage.height()); + resizeImage(&displayImage, QSize(newWidth, newHeight)); + update(); + } + QWidget::resizeEvent(event); +} +//! [16] + +//! [17] +void RotoCanvas::drawLineTo(const QPoint &endPoint) +//! [17] //! [18] +{ + if (outputSize.width()>0&&outputSize.height()>0) { + if (selectedLayerIndex>=0) { + QString layersPath=getLayersFolderPath(getSeqFrameNumber(),true); + QDir layersDir=QDir(layersPath); + int thisLayerNumber=selectedLayerIndex; + QString thisLayerPath=layersDir.filePath(QString::number(thisLayerNumber)+".png"); + QFileInfo thisLayerFI=QFileInfo(thisLayerPath); + + while (layerPtrs.length()frameNumber=getSeqFrameNumber(); + newLayer->image=QImage(outputSize, QImage::Format_RGB32); + layerPtrs.append(newLayer); + } + QPainter painter(&displayImage); + int rad = (brushRadius) + 2; + QRect rectBrush = QRect(lastPoint, endPoint).normalized() + .adjusted(-rad, -rad, +rad, +rad); + //QPen pen; + //pen.setStyle(Qt::SolidLine); + //pen.setWidth(1); + //pen.setColor(brushColor); + painter.setPen(brushColor); + QColor thisColor; + + QPoint thisPoint; + brushColor.setAlpha(this->brushOpacity); + for (int y=rectBrush.top(); y=0 && y>=0) { + //thisPoint.setX(x); + //thisPoint.setY(y); + double this_distance = sqrt(pow(endPoint.x()-x, 2) + pow(endPoint.y()-y, 2)); + double fade_length = brushRadius-brushHardRadius; + double this_opacity = (fade_length>0.0) ? ((brushRadius-this_distance) / fade_length) : ((brushRadius-this_distance) / 0.000001) ; + if (this_opacity>1.0) this_opacity=1.0; + else if (this_opacity<0.0) this_opacity=0.0; + QColor destColor = displayImage.pixelColor(x,y); + //do alpha formula (byte)((source-dest)*alpha/255.0f+dest+.5f) + int r = (int)((this->brushColor.red()-destColor.red())*this_opacity+destColor.red()+.5); //+.5 for rounding + int g = (int)((this->brushColor.green()-destColor.green())*this_opacity+destColor.green()+.5); //+.5 for rounding + int b = (int)((this->brushColor.blue()-destColor.blue())*this_opacity+destColor.blue()+.5); //+.5 for rounding + int a = (int)((this->brushColor.alpha()-destColor.alpha())*this_opacity+destColor.alpha()+.5); //+.5 for rounding + thisColor.setRed(r); + thisColor.setGreen(g); + thisColor.setBlue(b); + thisColor.setAlpha(a); + displayImage.setPixelColor(x, y, thisColor); + //painter.setOpacity(this_opacity); + //painter.drawPoint(x,y); + } + } + } + isModified = true; + update(rectBrush); + lastPoint = endPoint; + } + //else no layer is selected + } + //else a dimension is 0--no video loaded +} + +void RotoCanvas::fillCheckered(QImage *thisImage) +{ + if (thisImage!=nullptr) { + //QSize newSize=thisImage->size(); + thisImage->fill(this->checkerDarkColor); //newImage.fill(qRgb(255, 255, 255)); + for (int y=0; yheight(); y++) { + for (int x=0; xwidth(); x++) { + //debug optimization: try lines instead of pixel + if (y%2==1) { + if (x%2==1) thisImage->setPixelColor(x, y, this->checkerLightColor); + } + else { + if ((x+1)%2==1) thisImage->setPixelColor(x, y, this->checkerLightColor); + } + } + } + } + else QDebug("fillCheckered Error: thisImage is nullptr"); +} +//! [18] + +//! [19] +void RotoCanvas::resizeImage(QImage *image, const QSize &newSize) +//! [19] //! [20] +{ + if (image->size() == newSize) + return; + + QImage newImage(newSize, QImage::Format_RGB32); + fillCheckered(&newImage); + + QPainter painter(&newImage); + painter.drawImage(QPoint(0, 0), *image); + *image = newImage; +} + +QImage RotoCanvas::getCacheableImage(QString filePath) +{ + //TODO: load from cache instead if possible + QImage thisImage(filePath); + return thisImage; +} +//! [20] + +//! [21] +void RotoCanvas::print() +{ +#if !defined(QT_NO_PRINTER) && !defined(QT_NO_PRINTDIALOG) + QPrinter printer(QPrinter::HighResolution); + + QPrintDialog printDialog(&printer, this); +//! [21] //! [22] + if (printDialog.exec() == QDialog::Accepted) { + QPainter painter(&printer); + QRect rect = painter.viewport(); + QSize size = displayImage.size(); + size.scale(rect.size(), Qt::KeepAspectRatio); + painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); + painter.setWindow(displayImage.rect()); + painter.drawImage(0, 0, displayImage); + } +#endif // QT_NO_PRINTER +} +//! [22] diff --git a/rotocanvas.h b/rotocanvas.h new file mode 100644 index 0000000..9d09e0a --- /dev/null +++ b/rotocanvas.h @@ -0,0 +1,102 @@ +#ifndef ROTOCANVAS_H +#define ROTOCANVAS_H + +#include +#include +#include +#include +#include +#include +#include +//! [0] +class RotoCanvas : public QWidget +{ + Q_OBJECT + +public: + RotoCanvas(QWidget *parent = 0); + + static QString getSeqName(QString framePath); + static QString getSeqName(QFileInfo frameFI); + QString getSeqName(); + static QString getSeqFormatString(QString framePath); + static QString getSeqFormatString(QFileInfo frameFI); + QString getSeqFormatString(); + static QString getFolderPath(QString framePath); + static QString getFolderPath(QFileInfo frameFI); + QString getFolderPath(); + static int getSeqFrameNumber(QString framePath); + static int getSeqFrameNumber(QFileInfo frameFI); + int getSeqFrameNumber(); + static int getSeqDigitCount(QString framePath); + static int getSeqDigitCount(QFileInfo frameFI); + int getSeqDigitCount(); + static QString getSeqPaddedFrameNumber(QString framePath); + static QString getSeqPaddedFrameNumber(QFileInfo frameFI); + QString getSeqPaddedFrameNumber(); + static QString getLayersFolderPath(QString framePath, int frameNumber, bool createEnable); + static QString getLayersFolderPath(QFileInfo frameFI, int frameNumber, bool createEnable); + QString getLayersFolderPath(int frameNumber, bool createEnable); + static QString getLayerImagePathMostRecent(QString framePath, int frameNumber, int layerNumber); + static QString getLayerImagePathMostRecent(QFileInfo frameFI, int frameNumber, int layerNumber); + QString getLayerImagePathMostRecent(int frameNumber, int layerNumber); + + static QString getSeqFramePath(QString folderPath, QString seqName, int frameNumber, int minDigitCount, QString format); + QString getSeqFramePath(int frameNumber); + static QString getZeroPadded(int frameNumber, int minDigitCount); + + + bool openImage(const QString &fileName); + bool saveImage(const QString &fileName, const char *fileFormat); + void setBrushColor(const QColor &newColor); + void setBrushRadius(double newWidth); + void setBrushHardness(double newHardness); + void setBrushOpacity(double newOpacity); + + bool getIsModified() const { return isModified; } + QColor getBrushColor() const { return brushColor; } + double getBrushRadius() const { return brushRadius; } + double getBrushHardness() const { return (brushRadius-brushHardRadius)/brushRadius; } + double getBrushOpacity() const { return brushOpacity; } + +public slots: + void clearImage(); + void print(); + +protected: + void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + void mouseMoveEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; + +private: + void drawLineTo(const QPoint &endPoint); + void fillCheckered(QImage *thisImage); + void resizeImage(QImage *thisImage, const QSize &newSize); + QImage getCacheableImage(QString filePath); + + bool isModified; + bool isToolActive; + int selectedLayerIndex; + double cacheMaxMB; + //int minDigitCount; // use getSeqDigitCount; formerly PROJECT INFO + double brushRadius; // PROJECT INFO + double brushHardRadius; // PROJECT INFO + double brushOpacity; // PROJECT INFO + QFileInfo* loadedFI; // PROJECT INFO; STATE INFO: background layer + QSize outputSize; // PROJECT INFO size of video (same as layers, but different from image) + QColor brushColor; // PROJECT INFO + QColor checkerDarkColor; // PROJECT INFO + QColor checkerLightColor; // PROJECT INFO + QImage backgroundImage; + QImage displayImage; //just for display (not saved anywhere)--includes matte, canvas (checkerboard), and interface + QList layerPtrs; + QPoint lastPoint; + + //project info: + //QString formatString; +}; +//! [0] + +#endif diff --git a/rotocanvaslayer.cpp b/rotocanvaslayer.cpp new file mode 100644 index 0000000..ccea882 --- /dev/null +++ b/rotocanvaslayer.cpp @@ -0,0 +1,7 @@ +#include "rotocanvaslayer.h" + +RotoCanvasLayer::RotoCanvasLayer(QObject *parent) : QObject(parent) +{ + isModified=true; + frameNumber=-1; +} diff --git a/rotocanvaslayer.h b/rotocanvaslayer.h new file mode 100644 index 0000000..bb75276 --- /dev/null +++ b/rotocanvaslayer.h @@ -0,0 +1,23 @@ +#ifndef ROTOCANVASLAYER_H +#define ROTOCANVASLAYER_H + +#include +#include + +class RotoCanvasLayer : public QObject +{ + Q_OBJECT +public: + explicit RotoCanvasLayer(QObject *parent = 0); + //bool loadFrame(QString setSequenceName, int setFrameNumber, int minDigitCount); + bool isModified; + int frameNumber; //source of keyframe in case an earlier keyframe is still visible + QImage image; +signals: + +public slots: + +private: +}; + +#endif // ROTOCANVASLAYER_H