initially-uploaded version

master
Jacob Gustafson 2016-08-04 22:39:36 -04:00
parent 1789927e5e
commit f6b5ac2ddc
11 changed files with 1096 additions and 2 deletions

View File

@ -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 <sequenceName> is sequence name (such as, if mygreatvideo0000.png is first frame, <sequenceName> is mygreatvideo)
<sequenceName> [folder]
rotocanvas.yml [not yet implemented]
frames [folder]
<frameNumber> [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]
<layerNumber>.png
<layerNumber>.yml [not yet implemented]

View File

@ -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

View File

@ -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

11
main.cpp Normal file
View File

@ -0,0 +1,11 @@
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}

255
mainwindow.cpp Normal file
View File

@ -0,0 +1,255 @@
#include <QtWidgets>
#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<QAction *>(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("<p><b>RotoCanvas Paint</b> is a manual rotoscoping (frame by "
"frame painting) application.</p>"
"<p>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.</p>"
"<p>This program uses the <b>RotoCanvas</b> widget based on the <b>Scribble</b> Qt Example.</p>"));
}
//! [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]

61
mainwindow.h Normal file
View File

@ -0,0 +1,61 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QList>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QLineEdit>
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<QAction *> saveAsActs;
QAction *exitAct;
QAction *brushColorAct;
QAction *brushWidthAct;
QAction *brushHardnessAct;
QAction *brushOpacityAct;
QAction *printAct;
QAction *clearScreenAct;
QAction *aboutAct;
QAction *aboutQtAct;
};
//! [0]
#endif

15
rcpaint.pro Normal file
View File

@ -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

574
rotocanvas.cpp Normal file
View File

@ -0,0 +1,574 @@
#include <QtWidgets>
#ifndef QT_NO_PRINTER
#include <QPrinter>
#include <QPrintDialog>
#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 <sequenceName>/frames/<frameNumber>/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:"<<framesPath;
QDir framesDir(framesPath);
if (createEnable) {
if (!framesDir.exists()) seqDir.mkdir("frames");
}
QString thisFramePath=framesDir.filePath(QString::number(frameNumber));
qInfo()<<"thisFramePath:"<<thisFramePath;
QDir thisFrameDir=QDir(thisFramePath);
if (createEnable) {
if (!thisFrameDir.exists()) framesDir.mkdir(QString::number(frameNumber));
}
QString layersPath=QDir(thisFramePath).filePath("layers");
qInfo()<<"layersPath:"<<layersPath;
QDir layersDir=QDir(layersPath);
if (createEnable) {
if (!layersDir.exists()) thisFrameDir.mkdir("layers");
}
return layersPath;
}
QString RotoCanvas::getLayersFolderPath(int frameNumber, bool createEnable)
{
if (this->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()<minDigitCount) {
result=zeroString+result;
}
return result;
}
//! [0]
//! [1]
bool RotoCanvas::openImage(const QString &fileName)
//! [1] //! [2]
{
QImage loadedImage;
if (!loadedImage.load(fileName))
return false;
this->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: "<<thisLayerPath;
//QImage newImage(loadedImage.size(), QImage::Format_RGB32);
//QImage* thisLayerImagePtr=new QImage();
//thisLayerImagePtr->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()<selectedLayerIndex+1) {
RotoCanvasLayer* newLayer;
newLayer = new RotoCanvasLayer;
newLayer->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<rectBrush.bottom(); y++) {
for (int x=rectBrush.left(); x<rectBrush.right(); x++) {
//todo: distance from line instead of points
if (x>=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; y<thisImage->height(); y++) {
for (int x=0; x<thisImage->width(); 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]

102
rotocanvas.h Normal file
View File

@ -0,0 +1,102 @@
#ifndef ROTOCANVAS_H
#define ROTOCANVAS_H
#include <QColor>
#include <QImage>
#include <QPoint>
#include <QWidget>
#include <QList>
#include <QFileInfo>
#include <rotocanvaslayer.h>
//! [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<RotoCanvasLayer*> layerPtrs;
QPoint lastPoint;
//project info:
//QString formatString;
};
//! [0]
#endif

7
rotocanvaslayer.cpp Normal file
View File

@ -0,0 +1,7 @@
#include "rotocanvaslayer.h"
RotoCanvasLayer::RotoCanvasLayer(QObject *parent) : QObject(parent)
{
isModified=true;
frameNumber=-1;
}

23
rotocanvaslayer.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef ROTOCANVASLAYER_H
#define ROTOCANVASLAYER_H
#include <QObject>
#include <QImage>
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