diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index 6a0bb686..c4d2a2da 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -111,6 +111,7 @@ jobs: - name: Change Permissions run: | chmod +x relightlab_linux_portable/usr/bin/relight + chmod +x relightlab_linux_portable/usr/bin/relightlab chmod +x relightlab_linux_portable/usr/bin/relight-cli chmod +x relightlab_linux_portable/usr/bin/relight-merge chmod +x relightlab_linux_portable/AppRun @@ -144,6 +145,7 @@ jobs: path: relightlab_macos_portable - name: Change Permissions run: | + chmod +x relightlab_macos_portable/RelightLab*.app/Contents/MacOS/relightlab chmod +x relightlab_macos_portable/RelightLab*.app/Contents/MacOS/relight chmod +x relightlab_macos_portable/RelightLab*.app/Contents/MacOS/relight-cli chmod +x relightlab_macos_portable/RelightLab*.app/Contents/MacOS/relight-merge diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a21ae39..d70a931a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,10 +45,17 @@ endif() if(MSVC) set(JPEGTURBO_HOME ${CMAKE_CURRENT_SOURCE_DIR}/external/libjpeg-turbo-2.0.6) set(JPEG_INCLUDE_DIR ${JPEGTURBO_HOME}/include) - set(JPEG_LIBRARIES ${JPEGTURBO_HOME}/lib/jpeg.lib) + set(JPEG_LIBRARIES ${JPEGTURBO_HOME}/lib/jpeg.lib) else() find_package (JPEG REQUIRED) endif() + +find_package (TIFF) +if(NOT TIFF_FOUND) + add_subdirectory( ${CMAKE_CURRENT_SOURCE_DIR}/external/tiff-4.7.0) + add_library(TIFF::TIFF ALIAS tiff) +endif() + find_package (TIFF) if(NOT TIFF_FOUND) @@ -78,5 +85,6 @@ endif() include(GNUInstallDirs) add_subdirectory(relight) +add_subdirectory(relightlab) add_subdirectory(relight-cli) add_subdirectory(relight-merge) diff --git a/README.md b/README.md index ffc84071..091fd86b 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,6 @@ $ cmake . $ make ``` - ### MacOS Installed tools: diff --git a/build_scripts/Linux/1_build.sh b/build_scripts/Linux/1_build.sh index 202a0a99..33558e14 100644 --- a/build_scripts/Linux/1_build.sh +++ b/build_scripts/Linux/1_build.sh @@ -4,7 +4,7 @@ # Requires a Qt environment which is set-up properly, and an accessible # cmake binary. # -# Without given arguments, relight will be built in the relight/build +# Without given arguments, relightlab will be built in the relightlab/build # directory, and installed in $BUILD_PATH/../install. # # You can give as argument the BUILD_PATH and the INSTALL_PATH in the diff --git a/build_scripts/Linux/2_deploy.sh b/build_scripts/Linux/2_deploy.sh index 9eef5078..62f41d9a 100644 --- a/build_scripts/Linux/2_deploy.sh +++ b/build_scripts/Linux/2_deploy.sh @@ -1,11 +1,11 @@ #!/bin/bash # This is a script shell for deploying a relight-portable folder, and an appimage. -# Requires a properly built relight (see 1_build.sh). +# Requires a properly built relightlab (see 1_build.sh). # -# Without given arguments, the folder that will be deployed is relight/install, which -# should be the path where relight has been installed (default output of 1_build.sh). +# Without given arguments, the folder that will be deployed is relightlab/install, which +# should be the path where relightlab has been installed (default output of 1_build.sh). # -# You can give as argument the path where you installed relight. +# You can give as argument the path where you installed relightlab. SCRIPTS_PATH="$(dirname "$(realpath "$0")")" INSTALL_PATH=$SCRIPTS_PATH/../../install diff --git a/build_scripts/Linux/internal/2a_make_bundle.sh b/build_scripts/Linux/internal/2a_make_bundle.sh index e6ac7c3e..274a3528 100644 --- a/build_scripts/Linux/internal/2a_make_bundle.sh +++ b/build_scripts/Linux/internal/2a_make_bundle.sh @@ -21,8 +21,10 @@ done mkdir -p $INSTALL_PATH/usr/share/applications/ mkdir -p $INSTALL_PATH/usr/share/icons/Yaru/512x512/apps/ cp $SCRIPTS_PATH/resources/relight.desktop $INSTALL_PATH/usr/share/applications/relight.desktop +cp $SCRIPTS_PATH/resources/relightlab.desktop $INSTALL_PATH/usr/share/applications/relightlab.desktop cp $SCRIPTS_PATH/../relight.png $INSTALL_PATH/usr/share/icons/Yaru/512x512/apps/relight.png chmod +x $INSTALL_PATH/usr/bin/relight +chmod +x $INSTALL_PATH/usr/bin/relightlab chmod +x $INSTALL_PATH/usr/bin/relight-cli chmod +x $INSTALL_PATH/usr/bin/relight-merge diff --git a/build_scripts/Linux/internal/2c_portable.sh b/build_scripts/Linux/internal/2c_portable.sh index 8b546c1f..f2d55daa 100644 --- a/build_scripts/Linux/internal/2c_portable.sh +++ b/build_scripts/Linux/internal/2c_portable.sh @@ -33,7 +33,7 @@ fi export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$INSTALL_PATH/usr/lib if $SCRIPTS_PATH/resources/linuxdeploy --appdir=$INSTALL_PATH --plugin qt; then - echo "$INSTALL_PATH is now a self contained meshlab application" + echo "$INSTALL_PATH is now a self contained relight application" else echo "linuxdeploy failed with error code $?. Script was not completed successfully." exit 1 diff --git a/build_scripts/Linux/make_it.sh b/build_scripts/Linux/make_it.sh index 0b8c0901..03a54380 100644 --- a/build_scripts/Linux/make_it.sh +++ b/build_scripts/Linux/make_it.sh @@ -4,12 +4,12 @@ # Requires a Qt environment which is set-up properly, and an accessible # cmake binary. # -# Without given arguments, ReLight will be built in the relight/build, -# the folder relight/install will be a portable version of ReLight and -# the AppImage will be placed in the relight folder. +# Without given arguments, RelightLab will be built in the relightlab/build, +# the folder relightlab/install will be a portable version of RelightLab and +# the AppImage will be placed in the relightlab folder. # # You can give as argument the build path, the install path (that will contain -# the portable version of ReLight), and the number of cores to use to build ReLight +# the portable version of RelightLab), and the number of cores to use to build RelightLab # (default: 4). # The AppImage will be placed in the parent directory of the install path. # diff --git a/build_scripts/Linux/resources/relight.desktop b/build_scripts/Linux/resources/relightlab.desktop similarity index 80% rename from build_scripts/Linux/resources/relight.desktop rename to build_scripts/Linux/resources/relightlab.desktop index 202fcc84..ec28ef67 100644 --- a/build_scripts/Linux/resources/relight.desktop +++ b/build_scripts/Linux/resources/relightlab.desktop @@ -4,8 +4,8 @@ Type=Application Name=RelightLab GenericName=Image relighting Comment=Create and view relightable images -Exec=relight %F +Exec=relightlab %F Icon=relight Terminal=false Categories=Graphics;3DGraphics;Viewer;Qt; -Name[en_US]=relight.desktop +Name[en_US]=relightlab.desktop diff --git a/build_scripts/Windows/1_build.sh b/build_scripts/Windows/1_build.sh index 890c5f54..98c8e74d 100644 --- a/build_scripts/Windows/1_build.sh +++ b/build_scripts/Windows/1_build.sh @@ -3,7 +3,7 @@ # Requires a VS >= 2017 and Qt environments which are set-up properly, # and an accessible cmake binary. # -# Without given arguments, relight will be built in the relight/build +# Without given arguments, relight will be built in the relightlab/build # directory, and installed in $BUILD_PATH/../install. # # You can give as argument the BUILD_PATH and the INSTALL_PATH in the diff --git a/build_scripts/Windows/2_deploy.sh b/build_scripts/Windows/2_deploy.sh index 805ea967..c749a3da 100644 --- a/build_scripts/Windows/2_deploy.sh +++ b/build_scripts/Windows/2_deploy.sh @@ -1,10 +1,10 @@ #!/bin/bash # This is a script shell for deploying a relight-portable folder. -# Requires a properly built relight (see 1_build.sh). +# Requires a properly built relightlab (see 1_build.sh). # -# Without given arguments, the folder that will be deployed is relight/install. +# Without given arguments, the folder that will be deployed is relightlab/install. # -# You can give as argument the path where relight is installed. +# You can give as argument the path where relightlab is installed. SCRIPTS_PATH="$(dirname "$(realpath "$0")")" INSTALL_PATH=$SCRIPTS_PATH/../../install diff --git a/build_scripts/Windows/internal/2a_portable.sh b/build_scripts/Windows/internal/2a_portable.sh index 30d36a9e..11dc36ae 100644 --- a/build_scripts/Windows/internal/2a_portable.sh +++ b/build_scripts/Windows/internal/2a_portable.sh @@ -23,6 +23,8 @@ esac done ${QT_DIR}windeployqt $INSTALL_PATH/relight.exe +${QT_DIR}windeployqt $INSTALL_PATH/relightlab.exe + # remove all .lib files for file in $(find $INSTALL_PATH -name '*.lib'); diff --git a/build_scripts/macOS/0_setup_env.sh b/build_scripts/macOS/0_setup_env.sh index 0998298c..df01b347 100644 --- a/build_scripts/macOS/0_setup_env.sh +++ b/build_scripts/macOS/0_setup_env.sh @@ -23,7 +23,8 @@ esac done -brew install coreutils cmake ninja libomp eigen libjpeg +brew install coreutils cmake ninja libomp eigen libjpeg libtiff + npm install -g appdmg if [ "$DONT_INSTALL_QT" = false ] ; then diff --git a/build_scripts/macOS/1_build.sh b/build_scripts/macOS/1_build.sh index 4718cae8..67ece412 100644 --- a/build_scripts/macOS/1_build.sh +++ b/build_scripts/macOS/1_build.sh @@ -1,9 +1,9 @@ #!/bin/bash -# this is a script shell for compiling relight in a MacOS environment. +# this is a script shell for compiling relightlab in a MacOS environment. # Requires a Qt environment which is set-up properly, and an accessible # cmake binary. # -# Without given arguments, relight will be built in the meshlab/src/build +# Without given arguments, relightlab will be built in the relightlab/src/build # directory, and installed in $BUILD_PATH/../install. # # You can give as argument the BUILD_PATH and the INSTALL_PATH in the diff --git a/build_scripts/macOS/internal/2a_appbundle.sh b/build_scripts/macOS/internal/2a_appbundle.sh index 156e67d0..2811e5f4 100644 --- a/build_scripts/macOS/internal/2a_appbundle.sh +++ b/build_scripts/macOS/internal/2a_appbundle.sh @@ -4,7 +4,7 @@ SCRIPTS_PATH="$(dirname "$(realpath "$0")")"/.. INSTALL_PATH=$SCRIPTS_PATH/../../install QT_DIR="" -APPNAME="relight.app" +APPNAME="relightlab.app" #checking for parameters for i in "$@" diff --git a/build_scripts/macOS/internal/2b_sign_appbundle.sh b/build_scripts/macOS/internal/2b_sign_appbundle.sh index 9b52ca61..fa853389 100644 --- a/build_scripts/macOS/internal/2b_sign_appbundle.sh +++ b/build_scripts/macOS/internal/2b_sign_appbundle.sh @@ -4,7 +4,7 @@ SCRIPTS_PATH="$(dirname "$(realpath "$0")")"/.. INSTALL_PATH=$SCRIPTS_PATH/../../install CERT_ID="" -APPNAME="relight.app" +APPNAME="relightlab.app" #checking for parameters for i in "$@" diff --git a/build_scripts/macOS/internal/2c_notarize_appbundle.sh b/build_scripts/macOS/internal/2c_notarize_appbundle.sh index 764dcbee..812d6085 100644 --- a/build_scripts/macOS/internal/2c_notarize_appbundle.sh +++ b/build_scripts/macOS/internal/2c_notarize_appbundle.sh @@ -6,7 +6,7 @@ INSTALL_PATH=$SCRIPTS_PATH/../../install NOTAR_USER="" NOTAR_PASSWORD="" NOTAR_TEAM_ID="" -APPNAME="relight.app" +APPNAME="relightlab.app" #checking for parameters for i in "$@" diff --git a/build_scripts/macOS/internal/2d_dmg.sh b/build_scripts/macOS/internal/2d_dmg.sh index a1496f0e..11110175 100644 --- a/build_scripts/macOS/internal/2d_dmg.sh +++ b/build_scripts/macOS/internal/2d_dmg.sh @@ -33,7 +33,7 @@ sed "s%INST_PATH%$INSTALL_PATH%g" $SCRIPTS_PATH/resources/dmg_latest.json > $SCR sed -i '' "s%RL_VERSION%$RL_VERSION%g" $SCRIPTS_PATH/resources/dmg_final.json sed -i '' "s%SOURCE_PATH%$SOURCE_PATH%g" $SCRIPTS_PATH/resources/dmg_final.json -mv $INSTALL_PATH/relight.app $INSTALL_PATH/RelightLab$RL_VERSION.app +mv $INSTALL_PATH/relightlab.app $INSTALL_PATH/RelightLab$RL_VERSION.app mkdir $PACKAGES_PATH diff --git a/build_scripts/macOS/make_it.sh b/build_scripts/macOS/make_it.sh index f46c9ff5..61ae4053 100644 --- a/build_scripts/macOS/make_it.sh +++ b/build_scripts/macOS/make_it.sh @@ -1,5 +1,5 @@ #!/bin/bash -# This is a script shell for compiling and deploying ReLight in a MacOS environment. +# This is a script shell for compiling and deploying RelightLab in a MacOS environment. # # Requires a Qt environment which is set-up properly, and an accessible # cmake binary. @@ -41,4 +41,4 @@ esac done bash $SCRIPTS_PATH/1_build.sh -b=$BUILD_PATH -i=$INSTALL_PATH $QT_DIR_OPTION $CORES -bash $SCRIPTS_PATH/2_deploy.sh -i=$INSTALL_PATH $QT_DIR_OPTION \ No newline at end of file +bash $SCRIPTS_PATH/2_deploy.sh -i=$INSTALL_PATH $QT_DIR_OPTION diff --git a/build_scripts/relightlab.icns b/build_scripts/relightlab.icns new file mode 100644 index 00000000..1146266e Binary files /dev/null and b/build_scripts/relightlab.icns differ diff --git a/normal_integration/normal_integration.pro b/normal_integration/normal_integration.pro index ed241ec3..83a21493 100644 --- a/normal_integration/normal_integration.pro +++ b/normal_integration/normal_integration.pro @@ -15,11 +15,12 @@ HEADERS += \ ../src/bni_normal_integration.h +unix:INCLUDEPATH += /usr/include/eigen3 win32:INCLUDEPATH += ../external/eigen-3.4.0/ unix:INCLUDEPATH += /usr/include/eigen3 unix:INCLUDEPATH += ../external/eigen-3.4.0/ -unix:LIBS += -lgomp #-liomp5 +unix:LIBS += -lgomp -ltiff #-liomp5 unix:QMAKE_CXXFLAGS += -fopenmp diff --git a/relight-cli/main.cpp b/relight-cli/main.cpp index f8da7993..bd7798ec 100644 --- a/relight-cli/main.cpp +++ b/relight-cli/main.cpp @@ -137,15 +137,16 @@ int main(int argc, char *argv[]) { cerr << "Invalid resolution (must be 0 or >= 2 && <= 20)!\n" << endl; return 1; } - break; - } - case 'P': - builder.pixelSize = atof(optarg); - if(builder.pixelSize <= 0) { - cerr << "Invalid parameter pixelSize (-P): " << optarg << endl; - return 1; - } - break; + break; + } + case 'P': + builder.pixelSize = atof(optarg); + if(builder.pixelSize <= 0) { + cerr << "Invalid parameter pixelSize (-P): " << optarg << endl; + return 1; + } + + break; case 'b': { string b = optarg; if(b == "rbf") { diff --git a/relight/dstretchtask.cpp b/relight/dstretchtask.cpp index 85bde7b6..38f30b80 100644 --- a/relight/dstretchtask.cpp +++ b/relight/dstretchtask.cpp @@ -14,20 +14,19 @@ void DStretchTask::run() { - uint32_t minSamples; + uint32_t minSamples; std::function callback = [this](QString s, int n)->bool { return this->progressed(s, n); }; - status = RUNNING; + status = RUNNING; - // Get sample rate - if (hasParameter("min_samples")) - minSamples = (*this)["min_samples"].value.toInt(); - else { - error = "Unspecified sample rate"; - status = FAILED; - return; - } + // Get sample rate + if (hasParameter("min_samples")) + minSamples = (*this)["min_samples"].value.toInt(); + else { + error = "Unspecified sample rate"; + status = FAILED; + return; + } - dstretchImage(input_folder, output, minSamples, callback); - status = DONE; + dstretchImage(input_folder, output, minSamples, callback); + status = DONE; } - diff --git a/relight/dstretchtask.h b/relight/dstretchtask.h index 9ea3a326..292a2890 100644 --- a/relight/dstretchtask.h +++ b/relight/dstretchtask.h @@ -8,9 +8,9 @@ class DStretchTask : public Task { public: DStretchTask(QObject *parent) : Task(parent) {} + virtual ~DStretchTask(){} virtual void run() override; - }; #endif // DSTRETCHTASK_H diff --git a/relight/imagecropper.cpp b/relight/imagecropper.cpp index 69f6f5aa..cb4635e1 100644 --- a/relight/imagecropper.cpp +++ b/relight/imagecropper.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace std; namespace { @@ -20,7 +21,8 @@ ImageCropper::ImageCropper(QWidget* parent): QWidget(parent) { ImageCropper::~ImageCropper() {} void ImageCropper::setImage(const QPixmap& _image) { - imageForCropping = _image; + image = _image; + realSizeRect = QRect(QPoint(0, 0), image.size()); update(); } @@ -29,8 +31,7 @@ void ImageCropper::setBackgroundColor(const QColor& _backgroundColor) { update(); } -void ImageCropper::setCroppingRectBorderColor(const QColor& _borderColor) -{ +void ImageCropper::setCroppingRectBorderColor(const QColor& _borderColor) { croppingRectBorderColor = _borderColor; update(); } @@ -45,26 +46,7 @@ void ImageCropper::setProportion(const QSizeF& _proportion) { } if ( isProportionFixed ) { - float croppintRectSideRelation = - (float)croppingRect.width() / croppingRect.height(); - float proportionSideRelation = - (float)proportion.width() / proportion.height(); - if (croppintRectSideRelation != proportionSideRelation) { - - float area = croppingRect.width() * croppingRect.height(); - croppingRect.setWidth(sqrt(area / deltas.height())); - croppingRect.setHeight(sqrt(area / deltas.width())); - bool widthShotrerThenHeight = - croppingRect.width() < croppingRect.height(); - if (widthShotrerThenHeight) { - croppingRect.setHeight( - croppingRect.width() * deltas.height()); - } else { - croppingRect.setWidth( - croppingRect.height() * deltas.width()); - } - update(); - } + enforceBounds(realSizeRect, CursorPositionUndefined); } emit areaChanged(croppedRect()); } @@ -77,51 +59,79 @@ void ImageCropper::setProportionFixed(const bool _isFixed) } } -QRect ImageCropper::croppedRect() { - QRectF &r = croppingRect; - QRectF realSizeRect; - realSizeRect.setLeft((r.left() - leftDelta) * xScale); - realSizeRect.setTop ((r.top() - topDelta) * yScale); - - realSizeRect.setWidth(r.width() * xScale); - realSizeRect.setHeight(r.height() * yScale); +//returns cropped rect in image ccords +QRectF ImageCropper::imageCroppedRect() { + QRectF r; + r.setLeft(realSizeRect.left()/xScale + leftDelta); + r.setTop(realSizeRect.top()/yScale + topDelta); + r.setWidth(realSizeRect.width()/xScale); + r.setHeight(realSizeRect.height()/yScale); + return r; +} - if(!imageForCropping.isNull()) - realSizeRect = realSizeRect.intersected(QRectF(0, 0, imageForCropping.width(), imageForCropping.height())); - return realSizeRect.toRect(); +QRect ImageCropper::croppedRect() { + return realSizeRect; } void ImageCropper::setCrop(QRect rect) { if(!rect.isValid()) return; - QRectF &r = croppingRect; - r.setLeft(rect.left()/xScale + leftDelta); - r.setTop(rect.top()/yScale + topDelta); - r.setWidth(rect.width()/xScale); - r.setHeight(rect.height()/yScale); - update(); + enforceBounds(rect, CursorPositionMiddle); } void ImageCropper::resetCrop() { - setCrop(imageForCropping.rect()); + setCrop(image.rect()); +} + +void ImageCropper::setWidth(int w) { + QRect target = realSizeRect; + if(target.width() == w) return; + target.setWidth(w); + enforceBounds(target, CursorPositionRight); +} +void ImageCropper::setHeight(int h) { + QRect target = realSizeRect; + if(target.height() == h) return; + target.setHeight(h); + enforceBounds(target, CursorPositionBottom); +} + +void ImageCropper::setTop(int t) { + QRect target = realSizeRect; + if(target.top() == t) return; + target.moveTop(t); + enforceBounds(target, CursorPositionMiddle); +} +void ImageCropper::setLeft(int l) { + QRect target = realSizeRect; + if(target.left() == l) return; + target.moveLeft(l); + enforceBounds(target, CursorPositionMiddle); } +void ImageCropper::maximizeCrop() { + setCrop(image.rect()); +} + +void ImageCropper::centerCrop() { + QRect target = realSizeRect; + target.moveCenter(image.rect().center()); + setCrop(target); +} + void ImageCropper::updateDeltaAndScale() { - //TODO don't resize an image just to compute a proportion. - QSize scaledImageSize = imageForCropping.scaled( - this->size(), Qt::KeepAspectRatio, Qt::FastTransformation - ).size(); + QSize initial = image.size(); + QSize scaledImageSize = initial.scaled(this->size(), Qt::KeepAspectRatio); + leftDelta = 0.0f; topDelta = 0.0f; if (this->size().height() == scaledImageSize.height()) { leftDelta = (this->width() - scaledImageSize.width()) / 2.0f; } else { - if(this->size().width() != scaledImageSize.width()) - throw "Should never happen this"; topDelta = (this->height() - scaledImageSize.height()) / 2.0f; } - xScale = (float)imageForCropping.width() / scaledImageSize.width(); - yScale = (float)imageForCropping.height() / scaledImageSize.height(); + xScale = (float)image.width() / scaledImageSize.width(); + yScale = (float)image.height() / scaledImageSize.height(); } void ImageCropper::resizeEvent(QResizeEvent *event) { @@ -141,18 +151,14 @@ void ImageCropper::hideHandle() { update(); } -// ******** -// Protected section - -void ImageCropper::paintEvent(QPaintEvent* _event) -{ +void ImageCropper::paintEvent(QPaintEvent* _event) { QWidget::paintEvent( _event ); - // + + QRectF croppingRect = imageCroppedRect(); QPainter widgetPainter(this); { - QPixmap scaledImage = - imageForCropping.scaled(this->size(), Qt::KeepAspectRatio, Qt::FastTransformation); + QPixmap scaledImage = image.scaled(this->size(), Qt::KeepAspectRatio, Qt::FastTransformation); widgetPainter.fillRect( this->rect(), backgroundColor ); @@ -182,11 +188,9 @@ void ImageCropper::paintEvent(QPaintEvent* _event) widgetPainter.drawPath(p); widgetPainter.setPen(croppingRectBorderColor); + widgetPainter.setBrush(QBrush(Qt::transparent)); + widgetPainter.drawRect(croppingRect); - { - widgetPainter.setBrush(QBrush(Qt::transparent)); - widgetPainter.drawRect(croppingRect); - } { widgetPainter.setBrush(QBrush(croppingRectBorderColor)); @@ -198,9 +202,9 @@ void ImageCropper::paintEvent(QPaintEvent* _event) int topYCoord = croppingRect.top() - 2; int middleYCoord = croppingRect.center().y() - 3; int bottomYCoord = croppingRect.bottom() - 2; - // + const QSize pointSize(6, 6); - // + QVector points; points @@ -215,7 +219,7 @@ void ImageCropper::paintEvent(QPaintEvent* _event) << QRect( QPoint(rightXCoord, topYCoord), pointSize ) << QRect( QPoint(rightXCoord, middleYCoord), pointSize ) << QRect( QPoint(rightXCoord, bottomYCoord), pointSize ); - // + widgetPainter.drawRects( points ); } @@ -233,121 +237,182 @@ void ImageCropper::paintEvent(QPaintEvent* _event) QPoint(croppingRect.right(), croppingRect.center().y()) ); } } - // + widgetPainter.end(); } -void ImageCropper::mousePressEvent(QMouseEvent* _event) -{ - if (_event->button() == Qt::LeftButton) { +void ImageCropper::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { isMousePressed = true; - startMousePos = _event->pos(); - lastStaticCroppingRect = croppingRect; + startMousePos = event->pos(); + lastStaticCroppingRect = realSizeRect; } // - updateCursorIcon(_event->pos()); + updateCursorIcon(event->pos()); } -void ImageCropper::mouseMoveEvent(QMouseEvent* _event) +void ImageCropper::mouseMoveEvent(QMouseEvent* event) { - QPointF mousePos = _event->pos(); - // + QPointF mousePos = event->pos(); + if (!isMousePressed) { + QRectF croppingRect = imageCroppedRect(); _cursorPosition = cursorPosition(croppingRect, mousePos); updateCursorIcon(mousePos); + } else if (_cursorPosition != CursorPositionUndefined) { - QPointF mouseDelta; - mouseDelta.setX( mousePos.x() - startMousePos.x() ); - mouseDelta.setY( mousePos.y() - startMousePos.y() ); - // - QRectF &r = croppingRect; + QPoint mouseDelta; + mouseDelta.setX( round((mousePos.x() - startMousePos.x())*xScale) ); + mouseDelta.setY( round((mousePos.y() - startMousePos.y())*yScale) ); + + QRect target = lastStaticCroppingRect; if (_cursorPosition != CursorPositionMiddle) { - - QRectF newGeometry = - calculateGeometry( - lastStaticCroppingRect, - _cursorPosition, - mouseDelta); - - if (!newGeometry.isNull()) { - r = newGeometry; - - if(r.left() < leftDelta) r.setLeft(leftDelta); - if(r.top() < topDelta) r.setTop(topDelta); - float rightEdge = imageForCropping.width()/xScale + leftDelta; - if(r.right() > rightEdge) r.setRight(rightEdge); - - float bottomEdge = imageForCropping.height()/yScale + topDelta; - if(r.bottom() > bottomEdge) r.setBottom(bottomEdge); + if(_cursorPosition & CursorPositionLeft) { + target.setLeft(target.left() + mouseDelta.x()); + } + if(_cursorPosition & CursorPositionRight) { + target.setRight(target.right() + mouseDelta.x()); } + if( _cursorPosition & CursorPositionTop) { + target.setTop(target.top() + mouseDelta.y()); + } + if(_cursorPosition & CursorPositionBottom) { + target.setBottom(target.bottom() + mouseDelta.y()); + } } else { + target.moveTo( lastStaticCroppingRect.topLeft() + mouseDelta ); + } + enforceBounds(target, _cursorPosition); + emit areaChanged(croppedRect()); + update(); + } +} + +void ImageCropper::enforceBounds(QRect target, CursorPosition position) { + if(!target.isValid()) + return; + + if(position == CursorPositionUndefined) { + //make sure it is of the right shape + if(isProportionFixed) { + //keep area more or less the same + float area = target.width()*target.width(); + QPoint center = target.center(); + target.setWidth(round(sqrt(area*proportion.width()/proportion.height()))); + target.setHeight(round(sqrt(area*proportion.height()/proportion.width()))); + target.moveCenter(center); + } + target = ensureCropFits(target); - r.moveTo( lastStaticCroppingRect.topLeft() + mouseDelta ); + } + - if(r.left() < leftDelta) r.moveLeft(leftDelta); - if(r.top() < topDelta) r.moveTop(topDelta); + if(position == CursorPositionMiddle) { + if(target.left() < 0) target.moveLeft(0); + if(target.top() < 0) target.moveTop(0); + if(target.right() >= image.width()) target.moveRight(image.width()-1); + if(target.bottom() >= image.height()) target.moveBottom(image.height()-1); - float rightEdge = imageForCropping.width()/xScale + leftDelta; - if(r.right() > rightEdge) r.moveRight(rightEdge); + } else { + if(position & CursorPositionLeft) { + if(target.left() < 0) target.setLeft(0); + } + if(position & CursorPositionRight) { + if(target.right() >= image.width()) target.setRight(image.width()-1); + } - float bottomEdge = imageForCropping.height()/yScale + topDelta; - if(r.bottom() > bottomEdge) r.moveBottom(bottomEdge); + if(position & CursorPositionTop) { + if(target.top() < 0) target.setTop(0); + } + if(position & CursorPositionBottom) { + if(target.bottom() >= image.height()) target.setBottom(image.height()-1); } - emit areaChanged(croppedRect()); - update(); } + + target = ensureCropFits(target); + if(isProportionFixed) { + float h_over_w = proportion.height() / proportion.width(); + if(position & (CursorPositionLeft | CursorPositionRight)) { + int height = round(target.width() * h_over_w); + int delta = target.height() - height; + target.setHeight(height); + target.moveTop(target.top() + delta/2); + if(target.top() < 0) target.moveTop(0); + if(target.bottom() >= image.height()) + target.moveBottom(image.height()-1); + } + if(position & (CursorPositionTop | CursorPositionBottom)) { + int width = round(target.height() / h_over_w); + int delta = target.width() - width; + target.setWidth(width); + target.moveLeft(target.left() + delta/2); + if(target.left() < 0) target.moveLeft(0); + if(target.right() >= image.width()) + target.moveRight(image.width()-1); + } + } + realSizeRect = target; + emit areaChanged(croppedRect()); + update(); } -void ImageCropper::mouseReleaseEvent(QMouseEvent* _event) -{ +QRect ImageCropper::ensureCropFits(QRect target) { + QSizeF proportion = this->proportion; + if(!isProportionFixed) { + proportion.setWidth(target.width()); + proportion.setHeight(target.height()); + } + + float h_over_w = proportion.height() / proportion.width(); + int max_width = std::min(image.width(), (int)round(image.height()/h_over_w)); + int max_height = std::min(image.height(), (int)round(image.width()*h_over_w)); + if(target.width() > max_width) { + target.setWidth(max_width); + target.setHeight(image.height()); + } + if(target.height() > max_height) { + target.setHeight(max_height); + target.setWidth(image.width()); + } + if(target.left() < 0) target.moveLeft(0); + if(target.top() < 0) target.moveTop(0); + + return target; +} + +void ImageCropper::mouseReleaseEvent(QMouseEvent* _event) { isMousePressed = false; updateCursorIcon(_event->pos()); } -// ******** -// Private section -CursorPosition ImageCropper::cursorPosition(const QRectF& _cropRect, const QPointF& _mousePosition) -{ - // - float x = _mousePosition.x(); - float y = _mousePosition.y(); - QRectF outside = _cropRect.adjusted(-handleMargin, -handleMargin, handleMargin, handleMargin); - if(!outside.contains(_mousePosition)) +CursorPosition ImageCropper::cursorPosition(const QRectF& cropRect, const QPointF& mousePosition) { + + float x = mousePosition.x(); + float y = mousePosition.y(); + QRectF outside = cropRect.adjusted(-handleMargin, -handleMargin, handleMargin, handleMargin); + if(!outside.contains(mousePosition)) return CursorPositionUndefined; - int top = fabs(_cropRect.top() - y) < handleMargin; - int bottom = fabs(_cropRect.bottom() - y) < handleMargin; - int left = fabs(_cropRect.left() - x) < handleMargin; - int right = fabs(_cropRect.right() - x) < handleMargin; - - if(top && left) - return CursorPositionTopLeft; - if(bottom && left) - return CursorPositionBottomLeft; - if(top && right) - return CursorPositionTopRight; - if(bottom && right) - return CursorPositionBottomRight; - - if(left) - return CursorPositionLeft; - if(right) - return CursorPositionRight; - if(top) - return CursorPositionTop; - if(bottom) - return CursorPositionBottom; - - return CursorPositionMiddle; + int top = 2*(fabs(cropRect.top() - y) < handleMargin); + int bottom = 8*(fabs(cropRect.bottom() - y) < handleMargin); + int left = 1*(fabs(cropRect.left() - x) < handleMargin); + int right = 4*(fabs(cropRect.right() - x) < handleMargin); + + int position = top + bottom + left + right; + if(!position) + return CursorPositionMiddle; + + return (CursorPosition(position)); } -void ImageCropper::updateCursorIcon(const QPointF& _mousePosition) +void ImageCropper::updateCursorIcon(const QPointF& mousePosition) { QCursor cursorIcon; - // - switch (cursorPosition(croppingRect, _mousePosition)) + QRectF croppingRect = imageCroppedRect(); + + switch (cursorPosition(croppingRect, mousePosition)) { case CursorPositionTopRight: case CursorPositionBottomLeft: @@ -377,150 +442,3 @@ void ImageCropper::updateCursorIcon(const QPointF& _mousePosition) } this->setCursor(cursorIcon); } - -const QRectF ImageCropper::calculateGeometry( - const QRectF& _sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF& _mouseDelta - ) -{ - QRectF resultGeometry; - // - if ( isProportionFixed ) { - resultGeometry = - calculateGeometryWithFixedProportions( - _sourceGeometry, _cursorPosition, _mouseDelta, deltas); - } else { - resultGeometry = - calculateGeometryWithCustomProportions( - _sourceGeometry, _cursorPosition, _mouseDelta); - } - - if ((resultGeometry.left() >= resultGeometry.right()) || - (resultGeometry.top() >= resultGeometry.bottom())) { - resultGeometry = QRect(); - } - - //ensure geometry fits in the image. - if(resultGeometry.left() < 0) resultGeometry.setLeft(0); - if(resultGeometry.top() < 0) resultGeometry.setTop(0); - - // - return resultGeometry; -} - -const QRectF ImageCropper::calculateGeometryWithCustomProportions( - const QRectF& _sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF& _mouseDelta - ) -{ - QRectF resultGeometry = _sourceGeometry; - // - switch ( _cursorPosition ) - { - case CursorPositionTopLeft: - resultGeometry.setLeft( _sourceGeometry.left() + _mouseDelta.x() ); - resultGeometry.setTop ( _sourceGeometry.top() + _mouseDelta.y() ); - break; - case CursorPositionTopRight: - resultGeometry.setTop ( _sourceGeometry.top() + _mouseDelta.y() ); - resultGeometry.setRight( _sourceGeometry.right() + _mouseDelta.x() ); - break; - case CursorPositionBottomLeft: - resultGeometry.setBottom( _sourceGeometry.bottom() + _mouseDelta.y() ); - resultGeometry.setLeft ( _sourceGeometry.left() + _mouseDelta.x() ); - break; - case CursorPositionBottomRight: - resultGeometry.setBottom( _sourceGeometry.bottom() + _mouseDelta.y() ); - resultGeometry.setRight ( _sourceGeometry.right() + _mouseDelta.x() ); - break; - case CursorPositionTop: - resultGeometry.setTop( _sourceGeometry.top() + _mouseDelta.y() ); - break; - case CursorPositionBottom: - resultGeometry.setBottom( _sourceGeometry.bottom() + _mouseDelta.y() ); - break; - case CursorPositionLeft: - resultGeometry.setLeft( _sourceGeometry.left() + _mouseDelta.x() ); - break; - case CursorPositionRight: - resultGeometry.setRight( _sourceGeometry.right() + _mouseDelta.x() ); - break; - default: - break; - } - // - return resultGeometry; -} - -const QRectF ImageCropper::calculateGeometryWithFixedProportions( - const QRectF& _sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF& _mouseDelta, - const QSizeF& _deltas - ) -{ - QRectF resultGeometry = _sourceGeometry; - // - switch (_cursorPosition) - { - case CursorPositionLeft: - resultGeometry.setTop(_sourceGeometry.top() + _mouseDelta.x() * _deltas.height()); - resultGeometry.setLeft(_sourceGeometry.left() + _mouseDelta.x()); - break; - case CursorPositionRight: - resultGeometry.setTop(_sourceGeometry.top() - _mouseDelta.x() * _deltas.height()); - resultGeometry.setRight(_sourceGeometry.right() + _mouseDelta.x()); - break; - case CursorPositionTop: - resultGeometry.setTop(_sourceGeometry.top() + _mouseDelta.y()); - resultGeometry.setRight(_sourceGeometry.right() - _mouseDelta.y() * _deltas.width()); - break; - case CursorPositionBottom: - resultGeometry.setBottom(_sourceGeometry.bottom() + _mouseDelta.y()); - resultGeometry.setRight(_sourceGeometry.right() + _mouseDelta.y() * _deltas.width()); - break; - case CursorPositionTopLeft: - if ((_mouseDelta.x() * _deltas.height()) < (_mouseDelta.y())) { - resultGeometry.setTop(_sourceGeometry.top() + _mouseDelta.x() * _deltas.height()); - resultGeometry.setLeft(_sourceGeometry.left() + _mouseDelta.x()); - } else { - resultGeometry.setTop(_sourceGeometry.top() + _mouseDelta.y()); - resultGeometry.setLeft(_sourceGeometry.left() + _mouseDelta.y() * _deltas.width()); - } - break; - case CursorPositionTopRight: - if ((_mouseDelta.x() * _deltas.height() * -1) < (_mouseDelta.y())) { - resultGeometry.setTop(_sourceGeometry.top() - _mouseDelta.x() * _deltas.height()); - resultGeometry.setRight(_sourceGeometry.right() + _mouseDelta.x() ); - } else { - resultGeometry.setTop(_sourceGeometry.top() + _mouseDelta.y()); - resultGeometry.setRight(_sourceGeometry.right() - _mouseDelta.y() * _deltas.width()); - } - break; - case CursorPositionBottomLeft: - if ((_mouseDelta.x() * _deltas.height()) < (_mouseDelta.y() * -1)) { - resultGeometry.setBottom(_sourceGeometry.bottom() - _mouseDelta.x() * _deltas.height()); - resultGeometry.setLeft(_sourceGeometry.left() + _mouseDelta.x()); - } else { - resultGeometry.setBottom(_sourceGeometry.bottom() + _mouseDelta.y()); - resultGeometry.setLeft(_sourceGeometry.left() - _mouseDelta.y() * _deltas.width()); - } - break; - case CursorPositionBottomRight: - if ((_mouseDelta.x() * _deltas.height()) > (_mouseDelta.y())) { - resultGeometry.setBottom(_sourceGeometry.bottom() + _mouseDelta.x() * _deltas.height()); - resultGeometry.setRight(_sourceGeometry.right() + _mouseDelta.x()); - } else { - resultGeometry.setBottom(_sourceGeometry.bottom() + _mouseDelta.y()); - resultGeometry.setRight(_sourceGeometry.right() + _mouseDelta.y() * _deltas.width()); - } - break; - default: - break; - } - // - return resultGeometry; -} - diff --git a/relight/imagecropper.h b/relight/imagecropper.h index 9e6827f6..aba9f645 100644 --- a/relight/imagecropper.h +++ b/relight/imagecropper.h @@ -4,28 +4,20 @@ #include #include -namespace { - const QRect INIT_CROPPING_RECT = QRect(); - const QSizeF INIT_PROPORTION = QSizeF(1.0, 1.0); -} - enum CursorPosition { - CursorPositionUndefined, - CursorPositionMiddle, - CursorPositionTop, - CursorPositionBottom, - CursorPositionLeft, - CursorPositionRight, - CursorPositionTopLeft, - CursorPositionTopRight, - CursorPositionBottomLeft, - CursorPositionBottomRight + CursorPositionUndefined = 0, + CursorPositionMiddle = 1 | 2 | 4 | 8, + CursorPositionLeft = 1, + CursorPositionTop = 2, + CursorPositionRight = 4, + CursorPositionBottom = 8, + CursorPositionTopLeft = 1 | 2, + CursorPositionTopRight = 2 | 4, + CursorPositionBottomLeft = 8 | 1, + CursorPositionBottomRight = 8 | 4 }; - - -class ImageCropper : public QWidget -{ +class ImageCropper : public QWidget { Q_OBJECT public: @@ -36,20 +28,31 @@ public slots: void setImage(const QPixmap& _image); void setBackgroundColor(const QColor& _backgroundColor); void setCroppingRectBorderColor(const QColor& _borderColor); + void setProportion(const QSizeF& _proportion); void setProportionFixed(const bool _isFixed); + void showHandle(bool _show = true); void hideHandle(); + void setCrop(QRect rect); + void setWidth(int w); + void setHeight(int h); + void setTop(int t); + void setLeft(int l); void resetCrop(); + void maximizeCrop(); + void centerCrop(); signals: void areaChanged(QRect rect); public: bool handleShown() { return show_handle; } + QRectF imageCroppedRect(); //return cropped rect in image ccords QRect croppedRect(); - + void enforceBounds(QRect rect, CursorPosition position); //makes sure the rectangle is inside the image. + QRect ensureCropFits(QRect rect); protected: virtual void resizeEvent(QResizeEvent *event); virtual void paintEvent(QPaintEvent* _event); @@ -60,36 +63,22 @@ public slots: private: CursorPosition cursorPosition(const QRectF& _cropRect, const QPointF& _mousePosition); void updateCursorIcon(const QPointF& _mousePosition); - - const QRectF calculateGeometry( - const QRectF& _sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF& _mouseDelta - ); - const QRectF calculateGeometryWithCustomProportions( - const QRectF& _sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF& _mouseDelta - ); - const QRectF calculateGeometryWithFixedProportions( - const QRectF &_sourceGeometry, - const CursorPosition _cursorPosition, - const QPointF &_mouseDelta, - const QSizeF &_deltas - ); - void updateDeltaAndScale(); private: bool show_handle = true; + //TODO invert logic: realSize rect is stored, cropping rect is computed instead. float leftDelta = 0, topDelta = 0; float xScale = 1, yScale = 1; - QPixmap imageForCropping; - QRectF croppingRect; - QRectF lastStaticCroppingRect; + QPixmap image; + //QRectF croppingRect; //in image coords //j + + QRect realSizeRect; + QRect lastStaticCroppingRect; + CursorPosition _cursorPosition = CursorPositionUndefined; bool isMousePressed = false; bool isProportionFixed = false; diff --git a/relight/normalstask.cpp b/relight/normalstask.cpp index 5b32bf69..81431ce7 100644 --- a/relight/normalstask.cpp +++ b/relight/normalstask.cpp @@ -51,7 +51,7 @@ void NormalsTask::run() { imageSet.crop(rect.left(), rect.top(), rect.width(), rect.height()); } -// Normals vector + // Normals vector int start = clock(); imageSet.setCallback(nullptr); @@ -64,12 +64,12 @@ void NormalsTask::run() { pool.start(QThread::idealThreadCount()); for (int i=0; i normalmap(imageSet.width * imageSet.height * 3); - for(size_t i = 0; i < normals.size(); i++) - normalmap[i] = floor(((normals[i] + 1.0f) / 2.0f) * 255); + std::vector normalmap(imageSet.width * imageSet.height * 3); + for(size_t i = 0; i < normals.size(); i++) + normalmap[i] = floor(((normals[i] + 1.0f) / 2.0f) * 255); - // Save the final result - QImage img(normalmap.data(), imageSet.width, imageSet.height, imageSet.width*3, QImage::Format_RGB888); + // Save the final result + QImage img(normalmap.data(), imageSet.width, imageSet.height, imageSet.width*3, QImage::Format_RGB888); // Set spatial resolution if known. Need to convert as pixelSize stored in mm/pixel whereas QImage requires pixels/m if( pixelSize > 0 ) { int dotsPerMeter = round(1000.0/pixelSize); img.setDotsPerMeterX(dotsPerMeter); img.setDotsPerMeterY(dotsPerMeter); } - img.save(output); + img.save(output); std::function callback = [this](QString s, int n)->bool { return this->progressed(s, n); }; @@ -133,26 +133,26 @@ void NormalsTask::run() { return; std::vector z; bni_integrate(callback, imageSet.width, imageSet.height, normals, z, exportK); - if(z.size() == 0) { - error = "Failed to integrate normals"; - status = FAILED; - return; - } - QString filename = output.left(output.size() -4) + ".ply"; + if(z.size() == 0) { + error = "Failed to integrate normals"; + status = FAILED; + return; + } + QString filename = output.left(output.size() -4) + ".ply"; - if(!progressed("Saving surface...", 99)) - return; - if(exportSurface) - savePly(filename, imageSet.width, imageSet.height, z); + if(!progressed("Saving surface...", 99)) + return; + if(exportSurface) + savePly(filename, imageSet.width, imageSet.height, z); - filename = output.left(output.size() -4) + ".tif"; + filename = output.left(output.size() -4) + ".tif"; - if(exportDepthmap) + if(exportDepthmap) saveTiff(filename + ".tif", imageSet.width, imageSet.height, z); - } - int end = clock(); - qDebug() << "Time: " << ((double)(end - start) / CLOCKS_PER_SEC); - progressed("Finished", 100); + } + int end = clock(); + qDebug() << "Time: " << ((double)(end - start) / CLOCKS_PER_SEC); + progressed("Finished", 100); } /** @@ -163,24 +163,24 @@ void NormalsTask::run() { void NormalsWorker::run() { - switch (solver) - { - // L2 solver - case NORMALS_L2: - solveL2(); - break; - // SBL solver - case NORMALS_SBL: - solveSBL(); - break; - // RPCA solver - case NORMALS_RPCA: - solveRPCA(); - break; - } + switch (solver) + { + // L2 solver + case NORMALS_L2: + solveL2(); + break; + // SBL solver + case NORMALS_SBL: + solveSBL(); + break; + // RPCA solver + case NORMALS_RPCA: + solveRPCA(); + break; + } - // Deallocate line (TODO: useless?) - std::vector().swap(m_Row); + // Deallocate line (TODO: useless?) + std::vector().swap(m_Row); } @@ -188,23 +188,23 @@ void NormalsWorker::solveL2() { std::vector &m_Lights = m_Imageset.lights; std::vector &m_Lights3d = m_Imageset.lights3d; - // Pixel data + // Pixel data Eigen::MatrixXd mLights(m_Lights.size(), 3); Eigen::MatrixXd mPixel(m_Lights.size(), 1); - Eigen::MatrixXd mNormals; + Eigen::MatrixXd mNormals; - unsigned int normalIdx = 0; + unsigned int normalIdx = 0; - // Fill the lights matrix + // Fill the lights matrix for (size_t i = 0; i < m_Lights.size(); i++) for (int j = 0; j < 3; j++) - mLights(i, j) = m_Lights[i][j]; + mLights(i, j) = m_Lights[i][j]; - // For each pixel in the line solve the system - //TODO do it in a single pass, it's faster. + // For each pixel in the line solve the system + //TODO do it in a single pass, it's faster. for (size_t p = 0; p < m_Row.size(); p++) { - // Fill the pixel vector + // Fill the pixel vector for (size_t m = 0; m < m_Lights.size(); m++) mPixel(m, 0) = m_Row[p][m].mean(); @@ -221,9 +221,10 @@ void NormalsWorker::solveL2() mNormals = (mLights.transpose() * mLights).ldlt().solve(mLights.transpose() * mPixel); mNormals.col(0).normalize(); - m_Normals[normalIdx+0] = mNormals(0,0); - m_Normals[normalIdx+1] = mNormals(1,0); - m_Normals[normalIdx+2] = mNormals(2,0); + m_Normals[normalIdx+0] = mNormals(0, 0); + m_Normals[normalIdx+1] = mNormals(1, 0); + m_Normals[normalIdx+2] = mNormals(2, 0); + normalIdx += 3; } } diff --git a/relight/normalstask.h b/relight/normalstask.h index 6585a660..3b5d55f2 100644 --- a/relight/normalstask.h +++ b/relight/normalstask.h @@ -20,20 +20,22 @@ enum FlatMethod { NONE, RADIAL, FOURIER }; class NormalsTask : public Task { public: - NormalSolver solver; + NormalSolver solver; FlatMethod flatMethod; double flat_radius = 0.5; - bool exportSurface = false; + bool exportSurface = false; bool exportDepthmap = false; - bool exportK = 2.0; - QRect m_Crop; + bool exportK = 2.0; + QRect m_Crop; + float pixelSize = 0.0f; NormalsTask(Project *_project, NormalSolver _solver, FlatMethod _flatMethod) : project(_project), solver(_solver), flatMethod(_flatMethod) { } - virtual ~NormalsTask() {}; - virtual void run() override; + + virtual ~NormalsTask(){}; + virtual void run() override; private: //TODO remove dependency on project! @@ -47,16 +49,16 @@ class NormalsWorker NormalsWorker(NormalSolver _solver, int _row, PixelArray& toProcess, float* normals, ImageSet &imageset) : solver(_solver), row(_row), m_Row(toProcess), m_Normals(normals), m_Imageset(imageset){} - void run(); + void run(); private: - void solveL2(); - void solveSBL(); - void solveRPCA(); -private: - NormalSolver solver; + void solveL2(); + void solveSBL(); + void solveRPCA(); + + NormalSolver solver; int row; - PixelArray m_Row; - float* m_Normals; + PixelArray m_Row; + float* m_Normals; ImageSet &m_Imageset; - QMutex m_Mutex; + QMutex m_Mutex; }; diff --git a/relight/processqueue.cpp b/relight/processqueue.cpp index 67377281..b03c523b 100644 --- a/relight/processqueue.cpp +++ b/relight/processqueue.cpp @@ -92,7 +92,7 @@ void ProcessQueue::removeTask(int id) { if(index < 0) return; - Task *task = queue.takeAt(index); + Task *task = queue.takeAt(index); //processqueue is never the owner! //delete task; emit update(); diff --git a/relight/task.cpp b/relight/task.cpp index 34f139c9..4af21e97 100644 --- a/relight/task.cpp +++ b/relight/task.cpp @@ -63,13 +63,13 @@ void Task::runScript(QString program, QString script, QStringList arguments, QSt int pos = line.indexOf(re); if(pos >= 0) { - QString text = re.match(line).capturedTexts()[1]; + QString text = re.match(line).capturedTexts()[1]; int percent = re.match(line).capturedTexts()[2].toInt(); emit progress(text, percent); } } if(err.size()) - qDebug() << "Err: " << qPrintable(err) << "\n"; + qDebug() << "Err: " << qPrintable(err) << "\n"; if(out.size()) qDebug() << "Out: " << qPrintable(out) << "\n"; if(status == PAUSED) { @@ -92,23 +92,23 @@ void Task::runScript(QString program, QString script, QStringList arguments, QSt } void Task::pause() { - mutex.lock(); - status = PAUSED; + mutex.lock(); + status = PAUSED; } void Task::resume() { - if(status == PAUSED) { - status = RUNNING; - mutex.unlock(); - } + if(status == PAUSED) { + status = RUNNING; + mutex.unlock(); + } } void Task::stop() { - if(status == PAUSED) { //we were already locked then. - status = STOPPED; - mutex.unlock(); - } - status = STOPPED; + if(status == PAUSED) { //we were already locked then. + status = STOPPED; + mutex.unlock(); + } + status = STOPPED; } @@ -122,3 +122,4 @@ bool Task::progressed(QString s, int percent) { return false; return true; } + diff --git a/relight/zoom.h b/relight/zoom.h index efc88d0e..95c497ed 100644 --- a/relight/zoom.h +++ b/relight/zoom.h @@ -5,7 +5,7 @@ #include #include #include - + #include #include #include diff --git a/relightlab/.gitignore b/relightlab/.gitignore new file mode 100644 index 00000000..fab7372d --- /dev/null +++ b/relightlab/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/relightlab/CMakeLists.txt b/relightlab/CMakeLists.txt new file mode 100644 index 00000000..5c8f6dea --- /dev/null +++ b/relightlab/CMakeLists.txt @@ -0,0 +1,211 @@ +cmake_minimum_required(VERSION 3.12) +project(relightlab) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_FIND_FRAMEWORK LAST) + +find_package( + ${RELIGHT_QT} + COMPONENTS Core Gui Widgets Concurrent Xml + REQUIRED) +find_package(OpenMP) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +if (APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version" FORCE) + SET(CMAKE_INSTALL_RPATH $ORIGIN/../Frameworks) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() + +set (RELIGHT_HEADERS + processqueue.h + ../src/align.h + ../src/dome.h + ../src/exif.h + ../src/image.h + ../src/lens.h + ../src/lp.h + ../src/measure.h + ../src/project.h + ../src/sphere.h + ../src/white.h + ../src/bni_normal_integration.h + alignrow.h + alignframe.h + alignframe.h + canvas.h + lightgeometry.h + mainwindow.h + mainwindow.h + preferences.h + recentprojects.h + reflectionview.h + relightapp.h + sphererow.h + tabwidget.h + imageframe.h + imageview.h + homeframe.h + imagelist.h + flowlayout.h + imagegrid.h + lightsframe.h + markerdialog.h + domepanel.h + directionsview.h + rtiexportdialog.h + spherepanel.h + spherepicking.h + spheredialog.h + task.h + verifyview.h + verifydialog.h + helpbutton.h + cropframe.h + rtiframe.h + rticard.h + rtitask.h + ../relight/imagecropper.h + ../relight-cli/rtibuilder.h + ../src/rti.h + ../src/legacy_rti.h + ../src/imageset.h + ../src/jpeg_encoder.h + ../src/jpeg_decoder.h + ../src/flatnormals.h + ../relight/httpserver.h + queueframe.h + queueitem.h + normalsframe.h + normalstask.h + scaleframe.h +) + +set (RELIGHTLAB_SOURCES + main.cpp + processqueue.cpp + ../src/align.cpp + ../src/dome.cpp + ../src/exif.cpp + ../src/image.cpp + ../src/lens.cpp + ../src/lp.cpp + ../src/measure.cpp + ../src/project.cpp + ../src/sphere.cpp + ../src/white.cpp + ../src/bni_normal_integration.cpp + alignframe.cpp + alignpicking.cpp + alignrow.cpp + canvas.cpp + domepanel.cpp + lightgeometry.cpp + mainwindow.cpp + preferences.cpp + recentprojects.cpp + reflectionview.cpp + relightapp.cpp + sphererow.cpp + tabwidget.cpp + imageframe.cpp + imageview.cpp + homeframe.cpp + imagelist.cpp + flowlayout.cpp + imagegrid.cpp + lightsframe.cpp + markerdialog.cpp + directionsview.cpp + spherepanel.cpp + spherepicking.cpp + spheredialog.cpp + task.cpp + verifyview.cpp + verifydialog.cpp + helpbutton.cpp + cropframe.cpp + rtiframe.cpp + rtirecents.cpp + rtiplan.cpp + rticard.cpp + rtiexportdialog.cpp + rtitask.cpp + ../relight/imagecropper.cpp + ../relight-cli/rtibuilder.cpp + ../src/rti.cpp + ../src/legacy_rti.cpp + ../src/imageset.cpp + ../src/jpeg_encoder.cpp + ../src/jpeg_decoder.cpp + ../src/flatnormals.cpp + ../relight/httpserver.cpp + queueframe.cpp + queueitem.cpp + normalsframe.cpp + normalstask.cpp + scaleframe.cpp +) + + +set (RELIGHTLAB_RESOURCES + res.qrc +) + +add_executable(relightlab ${MACOSX_EXE_TARGET_OPTION} ${RELIGHTLAB_HEADERS} ${RELIGHTLAB_SOURCES} ${RELIGHTLAB_RESOURCES}) +target_include_directories( + relightlab PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${JPEG_INCLUDE_DIR} + TIFF::TIFF + ${EIGEN3_INCLUDE_DIR} + ) + +target_link_libraries( + relightlab PUBLIC + ${JPEG_LIBRARIES} + TIFF::TIFF + OpenMP::OpenMP_CXX + ${RELIGHT_QT}::Core + ${RELIGHT_QT}::Gui + ${RELIGHT_QT}::Widgets + ${RELIGHT_QT}::Concurrent + ${RELIGHT_QT}::Xml + ) + +target_compile_definitions(relightlab PUBLIC _USE_MATH_DEFINES NOMINMAX) + +target_compile_definitions(relightlab + PUBLIC + RELIGHT_VERSION=${RELIGHT_VERSION}) + +if (APPLE) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/../build_scripts/relightlab.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/../build_scripts/relightlab.icns DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/relightlab.app/Contents/Resources/") + set_target_properties(relightlab PROPERTIES + MACOSX_BUNDLE_ICON_FILE relightlab.icns + MACOSX_BUNDLE_BUNDLE_VERSION "${RELIGHT_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${RELIGHT_VERSION}" + MACOSX_BUNDLE_INFO_STRING "Relight ${RELIGHT_VERSION}" + MACOSX_BUNDLE_COPYRIGHT "Copyright VCG-ISTI-CNR © 2005-2023. All rights reserved." + ) + + set_additional_settings_info_plist( + TARGET relightlab + FILE ${CMAKE_CURRENT_BINARY_DIR}/relightlab.app/Contents/Info.plist) +endif() + +if (INSTALL_TO_UNIX_LAYOUT) + set(RELIGHT_INSTALL_BIN_DIR ${CMAKE_INSTALL_BINDIR}) +else() + set(RELIGHT_INSTALL_BIN_DIR .) +endif() + +install (TARGETS relightlab DESTINATION ${RELIGHT_INSTALL_BIN_DIR}) + +if (WIN32) + install(FILES ${JPEGTURBO_HOME}/bin/jpeg62.dll DESTINATION .) +endif() diff --git a/relightlab/alignframe.cpp b/relightlab/alignframe.cpp new file mode 100644 index 00000000..b0bb638a --- /dev/null +++ b/relightlab/alignframe.cpp @@ -0,0 +1,91 @@ +#include "alignframe.h" +#include "imageview.h" +#include "flowlayout.h" +#include "relightapp.h" +#include "markerdialog.h" +#include "alignrow.h" +#include "../src/align.h" + +#include +#include +#include +#include + +AlignFrame::AlignFrame(QWidget *parent): QFrame(parent) { + QVBoxLayout *content = new QVBoxLayout(this); + + content->addSpacing(10); + QPushButton *new_align = new QPushButton("New align..."); + new_align->setProperty("class", "large"); + content->addWidget(new_align); + new_align->setMinimumWidth(200); + new_align->setMaximumWidth(300); + + QFrame *aligns_frame = new QFrame; + content->addWidget(aligns_frame); + aligns = new QVBoxLayout(aligns_frame); + + //content->addStretch(); + connect(new_align, SIGNAL(clicked()), this, SLOT(newAlign())); +} + +void AlignFrame::clear() { + while(aligns->count() > 0) { + QLayoutItem *item = aligns->takeAt(0); + AlignRow *row = dynamic_cast(item->widget()); + //row->stopDetecting(); + delete row; + } +} + +void AlignFrame::init() { + for(Align *align: qRelightApp->project().aligns) { + AlignRow * row = addAlign(align); +// row->detectHighlights(false); + } +} + +/* on user button press */ +void AlignFrame::newAlign() { + if(!marker_dialog) + marker_dialog = new MarkerDialog(MarkerDialog::ALIGN, this); + + //TODO ACTUALLY images might be skipped! + Align *align = new Align(qRelightApp->project().images.size()); + marker_dialog->setAlign(align); + int answer = marker_dialog->exec(); + if(answer == QDialog::Rejected) { + delete align; + return; + } + qRelightApp->project().aligns.push_back(align); + AlignRow *row = addAlign(align); + //row->detectHighlights(); +} + +AlignRow *AlignFrame::addAlign(Align *align) { + AlignRow *row = new AlignRow(align); + aligns->addWidget(row); + + + connect(row, SIGNAL(removeme(AlignRow *)), this, SLOT(removeAlign(AlignRow *))); + connect(row, SIGNAL(updated()), this, SIGNAL(updated())); + return row; +} + +void AlignFrame::removeAlign(AlignRow *row) { + layout()->removeWidget(row); + +// row->stopDetecting(); + + Align *align = row->align; + auto &aligns = qRelightApp->project().aligns; + + auto it = std::find(aligns.begin(), aligns.end(), align); + +// assert(it != aligns.end()); + + delete align; + aligns.erase(it); + delete row; +} diff --git a/relightlab/alignframe.h b/relightlab/alignframe.h new file mode 100644 index 00000000..d3c53a15 --- /dev/null +++ b/relightlab/alignframe.h @@ -0,0 +1,31 @@ +#ifndef ALIGNFRAME_H +#define ALIGNFRAME_H + +#include + +class ImageViewer; +class QGraphicsRectItem; +class Align; +class AlignRow; +class MarkerDialog; +class QVBoxLayout; + +class AlignFrame: public QFrame { +Q_OBJECT +public: + AlignFrame(QWidget *parent = nullptr); + void clear(); + void init(); + AlignRow *addAlign(Align *align); + +public slots: + void newAlign(); + void removeAlign(AlignRow *align); + + +private: + MarkerDialog *marker_dialog = nullptr; + QVBoxLayout *aligns = nullptr; +}; + +#endif // ALIGNFRAME_H diff --git a/relightlab/alignpicking.cpp b/relightlab/alignpicking.cpp new file mode 100644 index 00000000..f0a8c2ed --- /dev/null +++ b/relightlab/alignpicking.cpp @@ -0,0 +1,51 @@ +#include "alignpicking.h" +#include "canvas.h" +#include "../src/align.h" +#include "relightapp.h" + +#include +#include +#include + +AlignPicking::AlignPicking(QWidget *parent): ImageViewer(parent) { + + marker_side = 40; + + connect(view, SIGNAL(clicked(QPoint)), this, SLOT(click(QPoint))); +} + + +void AlignPicking::clear() { + scene().clear(); + rect = nullptr; +} + +void AlignPicking::setAlign(Align *a) { + clear(); + align = a; + + rect = scene().addRect(a->rect, QPen(Qt::yellow), Qt::red); + + showImage(0); + fit(); +} + + + +void AlignPicking::click(QPoint p) { + clear(); + + QSize imgsize = qRelightApp->project().imgsize; + QPointF pos = view->mapToScene(p); + +//ensure that the marker is inside the image + pos.setX(std::max(marker_side/2.0, pos.x())); + pos.setY(std::max(marker_side/2.0, pos.y())); + + + pos.setX(std::min(imgsize.width()-marker_side/2.0, pos.x())); + pos.setY(std::min(imgsize.height()-marker_side/2.0, pos.y())); + + align->rect = QRect(pos.x()-marker_side/2.0, pos.y()-marker_side/2.0, marker_side, marker_side); + rect = scene().addRect(align->rect, QPen(Qt::yellow), Qt::red); +} diff --git a/relightlab/alignpicking.h b/relightlab/alignpicking.h new file mode 100644 index 00000000..caa6aa59 --- /dev/null +++ b/relightlab/alignpicking.h @@ -0,0 +1,30 @@ +#ifndef ALIGN_PICKING_H +#define ALIGN_PICKING_H + +#include "imageview.h" + +class QGraphicsRectItem; +class Canvas; +class Align; + + +class AlignPicking: public ImageViewer { + Q_OBJECT +public: + int marker_side = 40; + Align *align = nullptr; + + QGraphicsRectItem *rect = nullptr; + + + AlignPicking(QWidget *parent = nullptr); + void setAlign(Align *sphere); + void updateAlign(); + void clear(); + +public slots: + void click(QPoint); + +}; + +#endif diff --git a/relightlab/alignrow.cpp b/relightlab/alignrow.cpp new file mode 100644 index 00000000..bface326 --- /dev/null +++ b/relightlab/alignrow.cpp @@ -0,0 +1,140 @@ +#include "alignrow.h" +#include "markerdialog.h" +#include "relightapp.h" +#include "verifydialog.h" +#include "reflectionview.h" +#include "../src/project.h" +#include "../src/sphere.h" +#include "processqueue.h" + +#include +#include +#include +#include +#include + +FindAlignment::FindAlignment(Align *_align, bool update) { + align = _align; + update_positions = update; +} + +void FindAlignment::run() { + mutex.lock(); + status = RUNNING; + mutex.unlock(); +/* + Project &project = qRelightApp->project(); + for(size_t i = 0; i < project.images.size(); i++) { + + Image &image = project.images[i]; + if(image.skip) continue; + + QImage img(image.filename); + sphere->findHighlight(img, i, update_positions); + + int progress = std::min(99, (int)(100*(i+1) / project.images.size())); + progressed(QString("Detecting highlights"), progress); + } */ + progressed(QString("Done"), 100); + mutex.lock(); + status = DONE; + mutex.unlock(); +} + + +AlignRow::AlignRow(Align *_align, QWidget *parent): QWidget(parent) { + align = _align; + QHBoxLayout *columns = new QHBoxLayout(this); + columns->setSpacing(20); + + columns->addWidget(thumb = new QLabel()); +/* position = new PositionView(sphere, rowHeight); + position->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + columns->addWidget(position); + + + reflections = new ReflectionView(sphere, rowHeight); + reflections->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + columns->addWidget(reflections); */ + + QVBoxLayout *status_layout = new QVBoxLayout; + columns->addLayout(status_layout, 2); + status_layout->addStretch(); + status = new QLabel("Locating highlights..."); + status_layout->addWidget(status); + progress = new QProgressBar; + progress->setValue(0); + status_layout->addWidget(progress); + status_layout->addStretch(); + + QPushButton *edit = new QPushButton(QIcon::fromTheme("edit"), "Edit..."); + columns->addWidget(edit, 1); + QPushButton *verify = new QPushButton(QIcon::fromTheme("check"), "Verify..."); + columns->addWidget(verify, 1); + QPushButton *remove = new QPushButton(QIcon::fromTheme("trash-2"), "Delete"); + columns->addWidget(remove, 1); + + connect(edit, SIGNAL(clicked()), this, SLOT(edit())); + connect(remove, SIGNAL(clicked()), this, SLOT(remove())); + connect(verify, SIGNAL(clicked()), this, SLOT(verify())); + +} +void AlignRow::edit() { + MarkerDialog *marker_dialog = new MarkerDialog(MarkerDialog::ALIGN, this); + marker_dialog->setAlign(align); + int answer = marker_dialog->exec(); + if(answer == QDialog::Accepted) { + //position->update(); + //reflections->init(); + //detectHighlights(); + } +} + +void AlignRow::verify() { + std::vector centers; + std::vector thumbs; +// assert(0); //todo needs to initialize those vaules and update align. + VerifyDialog *verify_dialog = new VerifyDialog(thumbs, centers, this); + verify_dialog->exec(); +} + +void AlignRow::remove() { + emit removeme(this); +} + +void AlignRow::updateStatus(QString msg, int percent) { + status->setText(msg); + progress->setValue(percent); + reflections->update(); + if(percent == 100) { + emit updated(); + } +} + +void AlignRow::findAlignment(bool update) { +/* if(sphere->center.isNull()) { + status->setText("Needs at least 3 points."); + return; + } + if(!detect_highlights) { + detect_highlights = new DetectHighlights(sphere, update); + connect(detect_highlights, &DetectHighlights::progress, this, &SphereRow::updateStatus); //, Qt::QueuedConnection); + } + detect_highlights->stop(); + + ProcessQueue &queue = ProcessQueue::instance(); + queue.removeTask(detect_highlights); + queue.addTask(detect_highlights); + queue.start(); */ +} + +void AlignRow::stopFinding() { + /* + if(detect_highlights) { + if(detect_highlights->isRunning()) { + detect_highlights->stop(); + detect_highlights->wait(); + } + detect_highlights->deleteLater(); + } */ +} diff --git a/relightlab/alignrow.h b/relightlab/alignrow.h new file mode 100644 index 00000000..3449cb59 --- /dev/null +++ b/relightlab/alignrow.h @@ -0,0 +1,52 @@ +#ifndef ALIGNROW_H +#define ALIGNROW_H + +#include "task.h" +#include + + + + +class Align; +class QLabel; +class QProgressBar; +class ReflectionView; +class QGraphicsPixmapItem; + +class FindAlignment: public Task { +public: + Align *align; + bool update_positions; + + FindAlignment(Align *align, bool update = true); + virtual void run() override; + +}; + +class AlignRow: public QWidget { + Q_OBJECT +public: + Align *align; + int rowHeight = 92; + QLabel *thumb; + ReflectionView *reflections; + QLabel *status = nullptr; + QProgressBar *progress = nullptr; + FindAlignment *find_alignment = nullptr; + + AlignRow(Align *align, QWidget *parent = nullptr); + void findAlignment(bool update = true); + void stopFinding(); + +signals: + void removeme(AlignRow *row); + void updated(); //emit when status changes + +public slots: + void edit(); + void remove(); + void verify(); + void updateStatus(QString msg, int percent); +}; + +#endif // SPHEREROW_H diff --git a/relightlab/canvas.cpp b/relightlab/canvas.cpp new file mode 100644 index 00000000..ea5d98a5 --- /dev/null +++ b/relightlab/canvas.cpp @@ -0,0 +1,140 @@ +#include "canvas.h" + +#include +#include +#include +#include + +#include + +#include +using namespace std; + +Canvas::Canvas(QWidget *parent): QGraphicsView(parent) { + viewport()->installEventFilter(this); + setMouseTracking(true); + setDragMode(QGraphicsView::ScrollHandDrag); + setInteractive(true); + + _modifiers = Qt::NoModifier; //Qt::ControlModifier; + _zoom_factor_base = 1.0015; +} + +void Canvas::gentle_zoom(double factor) { + double currentScale = transform().m11(); + if(currentScale * factor < min_scale) + factor = min_scale/currentScale; + + if(currentScale *factor > max_scale) + factor = max_scale/currentScale; + + scale(factor, factor); + centerOn(target_scene_pos); + QPointF delta_viewport_pos = target_viewport_pos - QPointF(viewport()->width() / 2.0, + viewport()->height() / 2.0); + QPointF viewport_center = mapFromScene(target_scene_pos) - delta_viewport_pos; + centerOn(mapToScene(viewport_center.toPoint())); + emit zoomed(); +} + +void Canvas::set_modifiers(Qt::KeyboardModifiers modifiers) { + _modifiers = modifiers; + +} + +void Canvas::set_zoom_factor_base(double value) { + _zoom_factor_base = value; +} + +void Canvas::setCursor(Qt::CursorShape c) { + view_cursor = c; + viewport()->setCursor(view_cursor); +} + +bool Canvas::eventFilter(QObject *object, QEvent *event) { + QMouseEvent* mouse_event = static_cast(event); + + if (event->type() == QEvent::MouseMove) { + QPointF delta = target_viewport_pos - mouse_event->pos(); + if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) { + target_viewport_pos = mouse_event->pos(); + target_scene_pos = mapToScene(mouse_event->pos()); + } + + } else if (event->type() == QEvent::Wheel) { + QWheelEvent* wheel_event = static_cast(event); + if (QApplication::keyboardModifiers() == _modifiers) { + if (wheel_event->angleDelta().y() != 0) { + double angle = wheel_event->angleDelta().y(); + double factor = qPow(_zoom_factor_base, angle); + gentle_zoom(factor); + return true; + } + } + + } else if(event->type() == QEvent::MouseButtonPress) { + pressPosition = mouse_event->pos(); + + } else if(event->type() == QEvent::MouseButtonRelease) { + QPoint p = mouse_event->pos(); + pressPosition -= p; + if(pressPosition.manhattanLength() < click_threshold) + emit clicked(p); + + } else if(event->type() == QEvent::MouseButtonDblClick) { + QMouseEvent* mouse_event = static_cast(event); + QPoint p = mouse_event->pos(); + + emit dblClicked(p); + return true; + } + + Q_UNUSED(object) + return false; +} + +void Canvas::mouseReleaseEvent(QMouseEvent *event) { + QGraphicsView::mouseReleaseEvent(event); + viewport()->setCursor(view_cursor); +} + + +void Canvas::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode) { + if(!resized) { + rect_fit = rect; + aspect_fit = aspectRadioMode; + needs_fit = true; + return; + } + QGraphicsView::fitInView(rect, aspectRadioMode); +} + +void Canvas::resizeEvent(QResizeEvent *event) { + + QSize old = event->oldSize(); + QGraphicsView::resizeEvent(event); + + if(!old.isValid()) { + if(needs_fit) { + QGraphicsView::fitInView(rect_fit, aspect_fit); + } + resized = true; + return; + } + //preservinca scale + double sx = event->size().width()/(double)event->oldSize().width(); + double sy = event->size().height()/(double)event->oldSize().height(); + double s = std::min(sx, sy); + scale(s, s); +} + + + +void Canvas::zoomIn() { + gentle_zoom(1.19706); +} + +void Canvas::zoomOut() { + gentle_zoom(1/1.19706); +} + diff --git a/relightlab/canvas.h b/relightlab/canvas.h new file mode 100644 index 00000000..99ce6f8b --- /dev/null +++ b/relightlab/canvas.h @@ -0,0 +1,49 @@ +#ifndef CANVAS_H +#define CANVAS_H + +#include + +class Canvas : public QGraphicsView { + Q_OBJECT +public: + Canvas(QWidget *parent = nullptr); + void gentle_zoom(double factor); + void set_modifiers(Qt::KeyboardModifiers modifiers); + void set_zoom_factor_base(double value); + void setCursor(Qt::CursorShape cursor); + + double min_scale = 0.0f; + double max_scale = 4.0f; + void fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRadioMode = Qt::KeepAspectRatio); + +protected: + void resizeEvent(QResizeEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + +private: + Qt::KeyboardModifiers _modifiers; + double _zoom_factor_base; + QPointF target_scene_pos, target_viewport_pos; + QPoint pressPosition; + double click_threshold = 2; + Qt::CursorShape view_cursor = Qt::OpenHandCursor; + + //this stuff is needed to call fitinview before the image is properly resized. + bool needs_fit = false; + bool resized = false; + QRectF rect_fit; + Qt::AspectRatioMode aspect_fit; + + bool eventFilter(QObject* object, QEvent* event); + +public slots: + void zoomIn(); + void zoomOut(); + +signals: + void zoomed(); + void clicked(QPoint); + void dblClicked(QPoint); +}; + +#endif // CANVAS_H diff --git a/relightlab/creatertidialog.cpp b/relightlab/creatertidialog.cpp new file mode 100644 index 00000000..baa8f1bc --- /dev/null +++ b/relightlab/creatertidialog.cpp @@ -0,0 +1,6 @@ +#include "creatertidialog.h" + +CreateRtiDialog::CreateRtiDialog() +{ + +} diff --git a/relightlab/creatertidialog.h b/relightlab/creatertidialog.h new file mode 100644 index 00000000..59c8df67 --- /dev/null +++ b/relightlab/creatertidialog.h @@ -0,0 +1,11 @@ +#ifndef CREATERTIDIALOG_H +#define CREATERTIDIALOG_H + + +class CreateRtiDialog +{ +public: + CreateRtiDialog(); +}; + +#endif // CREATERTIDIALOG_H \ No newline at end of file diff --git a/relightlab/cropframe.cpp b/relightlab/cropframe.cpp new file mode 100644 index 00000000..2948695c --- /dev/null +++ b/relightlab/cropframe.cpp @@ -0,0 +1,165 @@ +#include "cropframe.h" +#include "../relight/imagecropper.h" +#include "relightapp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CropFrame::CropFrame(QWidget *parent): QFrame(parent) { + + QVBoxLayout *content = new QVBoxLayout(this); + + QHBoxLayout *image_layout = new QHBoxLayout; + content->addLayout(image_layout); + cropper = new ImageCropper; + + image_layout->addWidget(cropper, 4); + + QVBoxLayout *right_side = new QVBoxLayout; + image_layout->addLayout(right_side); + + QGroupBox *bounds = new QGroupBox("Area"); + bounds->setMinimumWidth(300); + right_side->addWidget(bounds, 0); + + QGridLayout * area_layout = new QGridLayout(bounds); + area_layout->setSpacing(10); + + area_layout->addWidget(new QLabel("Width"), 0, 0); + area_layout->addWidget(crop_width = new QSpinBox, 0, 1); + + area_layout->addWidget(new QLabel("Height"), 1, 0); + area_layout->addWidget(crop_height = new QSpinBox, 1, 1); + + area_layout->addWidget(new QLabel("Top"), 2, 0); + area_layout->addWidget(crop_top = new QSpinBox, 2, 1); + + area_layout->addWidget(new QLabel("Left"), 3, 0); + area_layout->addWidget(crop_left = new QSpinBox, 3, 1); + + crop_width->setMaximum(65535); + crop_height->setMaximum(65535); + crop_top->setMaximum(65535); + crop_left->setMaximum(65535); + + right_side->addSpacing(10); + QHBoxLayout *maximize_layout = new QHBoxLayout; + right_side->addLayout(maximize_layout); + maximize_layout->setSpacing(20); + + QPushButton *maximize = new QPushButton("Maximize"); + maximize->setStyleSheet("text-align: center;"); + maximize->setProperty("class", "large"); + maximize_layout->addWidget(maximize); + + QPushButton *center = new QPushButton("Center"); + center->setProperty("class", "large"); + center->setStyleSheet("text-align: center;"); + maximize_layout->addWidget(center); + + right_side->addSpacing(10); + + QGroupBox *aspect_box = new QGroupBox("Aspect ratio"); + right_side->addWidget(aspect_box); + + QGridLayout *aspect_layout = new QGridLayout(aspect_box); + aspect_layout->setSpacing(10); + + aspect_combo = new QComboBox; + aspect_combo->addItem("None"); //0 + aspect_combo->addItem("Custom"); //1 + aspect_combo->addItem("Square"); //2 + aspect_combo->addItem("4:3 Photo"); //3 + aspect_combo->addItem("3:2 Postcard"); //4 + aspect_combo->addItem("16:10 Widescreen"); //5 + aspect_combo->addItem("16:9 Widescreen"); //6 + aspect_combo->addItem("2:3 Postcard portrait"); //7 + aspect_combo->addItem("3:4 Photo portrait"); //8 + + aspect_layout->addWidget(aspect_combo, 0, 0, 1, 2); + + + aspect_layout->addWidget(aspect_width = new QSpinBox, 1, 0); + aspect_layout->addWidget(aspect_height = new QSpinBox, 1, 1); + aspect_width->setRange(1, 65535); + aspect_height->setRange(1, 65535); + + right_side->addStretch(2); + + connect(aspect_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(setAspectRatio())); + connect(aspect_width, SIGNAL(valueChanged(int)), this, SLOT(setAspectRatio())); + connect(aspect_height, SIGNAL(valueChanged(int)), this, SLOT(setAspectRatio())); + connect(cropper, SIGNAL(areaChanged(QRect)), this, SLOT(setArea(QRect))); + + connect(crop_width, SIGNAL(valueChanged(int)), cropper, SLOT(setWidth(int))); + connect(crop_height, SIGNAL(valueChanged(int)), cropper, SLOT(setHeight(int))); + connect(crop_top, SIGNAL(valueChanged(int)), cropper, SLOT(setTop(int))); + connect(crop_left, SIGNAL(valueChanged(int)), cropper, SLOT(setLeft(int))); + + connect(maximize, SIGNAL(clicked()), cropper, SLOT(maximizeCrop())); + connect(center, SIGNAL(clicked()), cropper, SLOT(centerCrop())); + + connect(cropper, SIGNAL(areaChanged(QRect)), this, SLOT(areaChanged(QRect))); +} + +void CropFrame::areaChanged(QRect rect) { + Project &project = qRelightApp->project(); + project.crop = rect; + +} +void CropFrame::clear() { + cropper->setImage(QPixmap()); +} + +void CropFrame::init() { + Project &project = qRelightApp->project(); + QString filename = project.images[0].filename; + int count = 0; + while(project.images[count].skip == true) { + count++; + if(count >= project.images.size()) + break; + filename = project.images[count].filename; + } + + QImage img(project.dir.filePath(filename)); + if(img.isNull()) { + QMessageBox::critical(this, "Houston we have a problem!", "Could not load image " + filename); + return; + } + cropper->setImage(QPixmap::fromImage(img)); + cropper->setCrop(project.crop); +} + +void CropFrame::setAspectRatio() { + int aspect = aspect_combo->currentIndex(); + double aspects[9][2] = { {1, 1}, {1, 1}, {1, 1}, {4, 3} , {3, 2}, {16, 10}, {16, 9}, {2, 3}, {3, 4} }; + + switch(aspect) { + case 0: return; //none + case 1: + aspects[1][0] = aspect_width->value(); + aspects[1][1] = aspect_height->value(); + break; + } + + double *s = aspects[aspect]; + cropper->setProportion(QSizeF(s[0], s[1])); + cropper->setProportionFixed(aspect > 0); +} + +void CropFrame::setArea(QRect rect) { + crop_width->setValue(rect.width()); + crop_height->setValue(rect.height()); + crop_top->setValue(rect.top()); + crop_left->setValue(rect.left()); +} + diff --git a/relightlab/cropframe.h b/relightlab/cropframe.h new file mode 100644 index 00000000..b4633def --- /dev/null +++ b/relightlab/cropframe.h @@ -0,0 +1,35 @@ +#ifndef CROPFRAME_H +#define CROPFRAME_H + +#include + +class ImageCropper; +class QSpinBox; +class QComboBox; + +class CropFrame: public QFrame { + Q_OBJECT +public: + CropFrame(QWidget *parent = nullptr); + void clear(); + void init(); + +public slots: + void setAspectRatio(); + void setArea(QRect rect); + void areaChanged(QRect rect); + +private: + ImageCropper *cropper = nullptr; + + QSpinBox *crop_width = nullptr; + QSpinBox *crop_height = nullptr; + QSpinBox *crop_top = nullptr; + QSpinBox *crop_left = nullptr; + + QComboBox *aspect_combo = nullptr; + QSpinBox *aspect_width = nullptr; + QSpinBox *aspect_height = nullptr; +}; + +#endif // CROPFRAME_H diff --git a/relightlab/css/style.qss b/relightlab/css/style.qss new file mode 100644 index 00000000..ec21859d --- /dev/null +++ b/relightlab/css/style.qss @@ -0,0 +1,67 @@ +/* QWidget { font-size: 11pt; } */ +#home { padding-top:30px } + +QPushButton.large, QToolButton.large { + text-align: left; + padding-left:12px; + padding-top:8px; + padding-bottom:8px; + max-width:200px; +} +QComboBox.large { + padding-left:12px; + padding-top:8px; + padding-bottom:8px; +} + +QPushButton:checked { + background-color: rgba(42, 130, 218, 255); +} + +QCommandLinkButton { + font-size:15px; +} + + +QLabel.recent { + font-size:9pt; + padding:4px; +} + +QLabel.recent:hover{ + background-color: palette(mid); +} + + +QSpinBox, QDoubleSpinBox { + border: 1px solid palette(dark); + border-radius: 3px; + padding: 2px; + height:20px; + min-width:100px; +} + +QSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { + width: 16px; + height: 16px; + padding:4px; + + subcontrol-origin: border; + subcontrol-position: top right; + border: 1px solid palette(dark); + border-radius: 3px; + + background-color: palette(button); +} + +QSpinBox::up-button, QDoubleSpinBox::up-button { + image: url(:/icons/dark/scalable/plus.svg); +} + +QSpinBox::down-button, QDoubleSpinBox::down-button { + image: url(:/icons/dark/scalable/minus.svg); + margin-right: 24px; + border-radius:0px; +} + +` diff --git a/relightlab/directionsview.cpp b/relightlab/directionsview.cpp new file mode 100644 index 00000000..44e0b784 --- /dev/null +++ b/relightlab/directionsview.cpp @@ -0,0 +1,41 @@ +#include "directionsview.h" +#include "../src/dome.h" + +#include +#include + +DirectionsView::DirectionsView(QWidget *parent): QGraphicsView(parent) { + setScene(&scene); +} + +void DirectionsView::initFromDome(Dome &dome) { + scene.clear(); + + + qreal scale = width(); + //scene goes from [-1, +1]x[-1, +1], view will just zoom on it + qreal diameter = lightSize; + + int count = 0; + for(Vector3f dir: dome.directions) { + dir.normalize(); + QGraphicsEllipseItem *e = scene.addEllipse(dir[0]*scale, dir[1]*scale, diameter, diameter); + e->setToolTip(QString::number(count++)); + e->setBrush(Qt::white); + } + + qreal margin = scale/10; + qreal side = scale + margin; + fitInView(QRectF(-side, -side, 2*side, 2*side), Qt::KeepAspectRatio); +} + +void DirectionsView::highlight(int n) { + for(int i = 0; i < scene.items().size(); i++) { + QGraphicsEllipseItem *item = (QGraphicsEllipseItem *)(scene.items()[i]); + item->setBrush(i == n? Qt::red : Qt::white); + } +} + +void DirectionsView::clear() { + scene.clear(); +} diff --git a/relightlab/directionsview.h b/relightlab/directionsview.h new file mode 100644 index 00000000..a646038a --- /dev/null +++ b/relightlab/directionsview.h @@ -0,0 +1,25 @@ +#ifndef DIRECTIONSVIEW_H +#define DIRECTIONSVIEW_H + +#include +#include + +class Dome; + +class DirectionsView: public QGraphicsView { + Q_OBJECT +public: + double lightSize = 10.0; + + DirectionsView(QWidget *parent = nullptr); + void initFromDome(Dome &dome); + +public slots: + void highlight(int n); + void clear(); + +private: + QGraphicsScene scene; +}; + +#endif // DIRECTIONSVIEW_H diff --git a/relightlab/docs/formats/config.md b/relightlab/docs/formats/config.md new file mode 100644 index 00000000..9920b05e --- /dev/null +++ b/relightlab/docs/formats/config.md @@ -0,0 +1,19 @@ +## Configuration + +# Appearance + +# Casting + +# Performances + +# Cache + + +# Defaults + +Defaults will be usually overwritten when value is changed in a dialog. + +Jpeg Quality (95). + +Web format (deep zoom) + diff --git a/relightlab/docs/formats/dome.md b/relightlab/docs/formats/dome.md new file mode 100644 index 00000000..a1b2fb4f --- /dev/null +++ b/relightlab/docs/formats/dome.md @@ -0,0 +1,44 @@ +# Dome configuration file format + +Dome configuration is saved in a JSON file (.dome extension) and contains all the informations +related to geometry and calibration of the lights, dimensions, offsets etc. + +| key | value | +| --- | --- | +| path | path and filename of the dome config | +| diameter | Dome diameter in mm, and yes, metric and only mm. | +| imageWidth | Default width in mm of the area covered by the image, this is useful for domes having a fixed optic. | +| verticalOffset | Default vertical offset from the origin of the coordinated +(usually the center of the sphere which is the most common geometry for domes). Positive if the center is above the surface. | +| positions | Array of light positions, in mm, where z is up, x is right and y is positive going toward the top of the image, optional | +| directions | Array of light directions, a reasonable approximation if the imageWidth is small relative to the diameter. Either positions or directions needs to be defined. | +| lightsCalibration | Array. Leds can have different colors, intensity and even non uniform emissions, hence the need for calibration | + +## Lights calibration + +Each element in this array is defined as such: +Spatial calibration is a grid sampling of the light cone intensity of each led, +centered on the coordinate origin (the center of the sphere dome, or [0,0,0] +when each led is assigned a position. (not usable id directions only are given). +Width and height define the size of the grid in mm, rows and cols the number of +divisions in the grid, number of coefficients is cols*rows and the values. +The distance between the samples on the x axis is width/(cols-1). + +Spatial calibration includes the distance factor; if no spatial calibration +is given the 1/d^2 formula is to be used, with d distance from the light to the point. + +The formula is: + +R = R * whiteBalance.r * bilinearInterpolation(spatialCalibration.intensity) + +or + +R = R * whiteBalance.r * intensity*(1/d^2) + +| key | value | +| --- | --- | +| whiteBalance | RGB correction coefficients { r: 1.0, g:1.0, b:1.0 } | +| intensity | Intensity correction coefficient. Each component is multiplied by this factor. | +| spatialCalibration | { width: 8.5, height: 6.1, rows: 8, cols: 7, intensity: [ 1.0, 0.9, 0.8... ] } intensity correction values on a grid wxh, the 0, 0 is a the top left corner of image | + + diff --git a/relightlab/docs/formats/relight.md b/relightlab/docs/formats/relight.md new file mode 100644 index 00000000..d3feb6e6 --- /dev/null +++ b/relightlab/docs/formats/relight.md @@ -0,0 +1,68 @@ +### Relight project file format + +RelightLab saves a project in a JSON file (.relight extension), containing the list of images, +lighting directions or posittions and all other attributed required for processing. + +## Metadata +| key | value | +| ---- | ---- | +| application | "ReglightLab", so we can check it is actually from this software | +| version | "RELIGHT_VERSION" | +| created | date of creation | +| authors | | + +## images +| key | value | +| --- | --- | +| folder | Images folder relative to he position of the project file | +| width | Width of the images in pixels | +| height | Height of the images in pixels | +| images | Array of image properties | + +## image + +| key | value | +| --- | --- | +| filename | Filename of the image | +| direction | x, y, z object | +| position | x, y, z object, one of the two needs to be present for processing | +| lightCalibration | see Dome definition. | + +## Geometry +| key | value | +| ---- | ---- | +| diameter | Dome diameter in mm, and yes, metric and only mm. | +| imageWidth | Default width in mm of the area covered by the image, this is useful for domes having a fixed optic. | +| verticalOffset | Default vertical offset from the origin of the coordinated +(usually the center of the sphere which is the most common geometry for domes). Positive if the center is above the surface. | + + +## Reflective pheres + +| key | value | +| ---- | ---- | +| border| Array of the points used to fit a circle or an ellipse (in pixels 0,0 is top left corner) | +| lights | Array of coordinates of the reflections, one for each image, [0, 0] if not found | + +### Deprecated? +This values can all be simply computed from the border values. + +| key | value | +| --- | --- | +| center | [x, y] coordinates of the center of the sphere/ellipse | +| inner | Bounding box of the part of the sphere where light reflections are supposed to be | +| radius | For the sphere fitting | +| majorAxisLenght | For ellipse fitting (in pixels) | +| minorAxisLenght | For ellipse fitting (in pixels) | +| axisAngle | Rotation of the major axis in radians | + + + +## If a dome is specified, it is saved here + +| key | value | +| ---- | ---- | +| dome | Dome json config | + +Dome configuration (geometry, LED calibrations etc.) are copied inside here, not to depend on external files. +See [dome format](dome_format.md) specifications. diff --git a/relightlab/docs/home.md b/relightlab/docs/home.md new file mode 100644 index 00000000..2a01d645 --- /dev/null +++ b/relightlab/docs/home.md @@ -0,0 +1,11 @@ +### RelightLab + +A selection of topics, links to to tutorials, etc. + +RTI introduction. + +Publishing on the web. + + + + diff --git a/relightlab/docs/index.md b/relightlab/docs/index.md new file mode 100644 index 00000000..5fe1c132 --- /dev/null +++ b/relightlab/docs/index.md @@ -0,0 +1,15 @@ +### RelightLab + +## Index + +# Interface + + New project + Open project + +# File formats + + Relight file format + Relight config file + Dome file format + diff --git a/relightlab/docs/interface/new_project.md b/relightlab/docs/interface/new_project.md new file mode 100644 index 00000000..544bd391 --- /dev/null +++ b/relightlab/docs/interface/new_project.md @@ -0,0 +1,8 @@ +### New Project + + +A dialog will ask you so select a folder containing the images to be processed. + +All same format (which are supported) and resolution. + + diff --git a/relightlab/docs/interface/open_project.md b/relightlab/docs/interface/open_project.md new file mode 100644 index 00000000..06dc8a42 --- /dev/null +++ b/relightlab/docs/interface/open_project.md @@ -0,0 +1,5 @@ +### Open project... + +A dialog will ask you for a .relight file. + +see test diff --git a/relightlab/domepanel.cpp b/relightlab/domepanel.cpp new file mode 100644 index 00000000..0e997762 --- /dev/null +++ b/relightlab/domepanel.cpp @@ -0,0 +1,163 @@ +#include "lightgeometry.h" +#include "relightapp.h" +#include "directionsview.h" +#include "domepanel.h" +#include "../src/lp.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +DomePanel::DomePanel(QWidget *parent): QFrame(parent) { + +// setContentsMargins(10, 10, 10, 10); + QHBoxLayout *content = new QHBoxLayout(this); + //content->setHorizontalSpacing(20); + + QPushButton *sphere = new QPushButton(QIcon::fromTheme("folder"), "New reflective sphere..."); + sphere->setProperty("class", "large"); + sphere->setMinimumWidth(200); + sphere->setMaximumWidth(300); + connect(sphere, SIGNAL(clicked()), parent, SLOT(newSphere())); + content->addWidget(sphere, 0, Qt::AlignTop); + + QPushButton *save = new QPushButton(QIcon::fromTheme("save"), "Export dome..."); + save->setProperty("class", "large"); + save->setMinimumWidth(200); + save->setMaximumWidth(300); + connect(save, SIGNAL(clicked()), this, SLOT(exportDome())); + content->addWidget(save, 0, Qt::AlignTop); + + QPushButton *load = new QPushButton(QIcon::fromTheme("folder"), "Load dome file..."); + load->setProperty("class", "large"); + load->setMinimumWidth(200); + load->setMaximumWidth(300); + connect(load, SIGNAL(clicked()), this, SLOT(loadDomeFile())); + content->addWidget(load, 0, Qt::AlignTop); + + + dome_list = new QComboBox; + dome_list->setProperty("class", "large"); + content->addWidget(dome_list, 1, Qt::AlignTop); + connect(dome_list, SIGNAL(currentIndexChanged(int)), this, SLOT(setDome(int))); + init(); +} + +void DomePanel::init() { + dome = qRelightApp->project().dome; + updateDomeList(); +} + +void DomePanel::updateDomeList() { + //dome_labels.clear(); + dome_paths.clear(); + dome_list->clear(); + dome_list->addItem("Select a recent dome..."); + //get list of existing domes + QStringList paths = qRelightApp->domes(); + for(QString path: paths) { + Dome dome; //yep, same name for a class member + try { + dome.load(path); + } catch (QString error) { + qDebug() << error; + } + + QFileInfo info(path); + dome.label = info.filePath(); + + dome_labels.append(dome.label); + dome_paths.append(path); + dome_list->addItem(dome.label); + } +} + +void DomePanel::setDome(int index) { + if(index <= 0) + return; + loadDomeFile(dome_paths[index-1]); //First index is "Seelect a recent dome..." +} +void DomePanel::loadDomeFile() { + QString path = QFileDialog::getOpenFileName(this, "Load a .lp or .dome file", QDir::currentPath(), "Light directions and domes (*.lp *.dome )"); + if(path.isNull()) + return; + loadDomeFile(path); +} + +void DomePanel::loadDomeFile(QString path) { + if(path.endsWith(".lp")) + loadLP(path); + if(path.endsWith(".dome")) + loadDome(path); +// dome_list->clearSelection(); +} + +void DomePanel::exportDome() { + QString filename = QFileDialog::getSaveFileName(this, "Select a dome file", qRelightApp->lastProjectDir(), "*.dome"); + if(filename.isNull()) + return; + if(!filename.endsWith(".dome")) + filename += ".dome"; + //TODO Basic checks, label is a problem (use filename! + Dome &dome = qRelightApp->project().dome; + dome.save(filename); + qRelightApp->addDome(filename); +} + + +void DomePanel::loadLP(QString path) { + std::vector filenames; + dome.lightConfiguration = Dome::DIRECTIONAL; + + try { + parseLP(path, dome.directions, filenames); + } catch(QString error) { + QMessageBox::critical(this, "Loading .lp file failed", error); + return; + } + QFileInfo info(path); + dome.label = info.filePath(); + qRelightApp->addDome(path); + + updateDomeList(); + + emit accept(dome); +} + +void DomePanel::loadDome(QString path) { + float imageWidth = dome.imageWidth; + try { + dome.load(path); + } catch (QString error) { + QMessageBox::critical(this, "Loading .dome file failed", error); + return; + } + + QFileInfo info(path); + dome.label = info.filePath(); + qRelightApp->addDome(path); + //preserve image width if we actually have a measurement. + if(imageWidth != 0 && qRelightApp->project().measures.size() != 0) + dome.imageWidth = imageWidth; + updateDomeList(); + emit accept(dome); +} + + + +/*void DomePanel::setSelectedDome() { + auto list = dome_list->selectedItems(); + if(!list.size()) + return; + int pos = dome_list->row(list[0]); + loadDomeFile(dome_paths[pos]); +}*/ diff --git a/relightlab/domepanel.h b/relightlab/domepanel.h new file mode 100644 index 00000000..c503016e --- /dev/null +++ b/relightlab/domepanel.h @@ -0,0 +1,41 @@ +#ifndef DOMEPANEL_H +#define DOMEPANEL_H + +#include "../src/dome.h" + +#include +#include + +class QLabel; +class QComboBox; + +class DomePanel: public QFrame { + Q_OBJECT +public: + DomePanel(QWidget *parent = nullptr); + void init(); + void loadDomeFile(QString path); + +public slots: + void loadDomeFile(); + void exportDome(); + void setDome(int); + void updateDomeList(); + +signals: + void accept(Dome dome); + +private: + Dome dome; + QStringList dome_labels; + QStringList dome_paths; + QComboBox *dome_list; + + + //QListWidget *dome_list; + + void loadLP(QString filename); + void loadDome(QString filename); +}; + +#endif // DOMEPANEL_H diff --git a/relightlab/flowlayout.cpp b/relightlab/flowlayout.cpp new file mode 100644 index 00000000..cfcb4016 --- /dev/null +++ b/relightlab/flowlayout.cpp @@ -0,0 +1,140 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include + +#include "flowlayout.h" + +FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) { + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) + : m_hSpace(hSpacing), m_vSpace(vSpacing) { + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::~FlowLayout() { + clear(); +} + +void FlowLayout::clear() { + QLayoutItem *item; + while ((item = takeAt(0))) + delete item; +} + +void FlowLayout::addItem(QLayoutItem *item) { + itemList.append(item); +} + +int FlowLayout::horizontalSpacing() const { + if (m_hSpace >= 0) { + return m_hSpace; + } else { + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); + } +} + +int FlowLayout::verticalSpacing() const { + if (m_vSpace >= 0) { + return m_vSpace; + } else { + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); + } +} + +int FlowLayout::count() const { + return itemList.size(); +} + +QLayoutItem *FlowLayout::itemAt(int index) const { + return itemList.value(index); +} + +QLayoutItem *FlowLayout::takeAt(int index) { + if (index >= 0 && index < itemList.size()) + return itemList.takeAt(index); + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const { + return { }; +} + +bool FlowLayout::hasHeightForWidth() const { + return true; +} + +int FlowLayout::heightForWidth(int width) const { + int height = doLayout(QRect(0, 0, width, 0), true); + return height; +} + +void FlowLayout::setGeometry(const QRect &rect) { + QLayout::setGeometry(rect); + doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const { + return minimumSize(); +} + +QSize FlowLayout::minimumSize() const { + QSize size; + for (const QLayoutItem *item : itemList) + size = size.expandedTo(item->minimumSize()); + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const { + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + + for (QLayoutItem *item : itemList) { + const QWidget *wid = item->widget(); + int spaceX = horizontalSpacing(); + if (spaceX == -1) + spaceX = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal); + int spaceY = verticalSpacing(); + if (spaceY == -1) + spaceY = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical); + + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + bottom; +} + +int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const { + QObject *parent = this->parent(); + if (!parent) { + return -1; + } else if (parent->isWidgetType()) { + QWidget *pw = static_cast(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(parent)->spacing(); + } +} diff --git a/relightlab/flowlayout.h b/relightlab/flowlayout.h new file mode 100644 index 00000000..39b8d8a7 --- /dev/null +++ b/relightlab/flowlayout.h @@ -0,0 +1,42 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef FLOWLAYOUT_H +#define FLOWLAYOUT_H + +#include +#include +#include + +class FlowLayout : public QLayout +{ +public: + explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1); + explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); + ~FlowLayout(); + + void clear(); + void addItem(QLayoutItem *item) override; + int horizontalSpacing() const; + int verticalSpacing() const; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int) const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +private: + int doLayout(const QRect &rect, bool testOnly) const; + int smartSpacing(QStyle::PixelMetric pm) const; + + QList itemList; + int m_hSpace; + int m_vSpace; +}; + + +#endif // FLOWLAYOUT_H diff --git a/relightlab/helpbutton.cpp b/relightlab/helpbutton.cpp new file mode 100644 index 00000000..cb7a3557 --- /dev/null +++ b/relightlab/helpbutton.cpp @@ -0,0 +1,164 @@ +#include "helpbutton.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +HelpDialog* HelpDialog::m_instance = nullptr; // Initialize static instance to nullptr + + +HelpedButton::HelpedButton(QAction *action, QString url, QWidget *parent): QWidget(parent) { + init(url); + button->setText(action->text()); + button->setIcon(action->icon()); + connect(button, SIGNAL(clicked(bool)), action, SLOT(trigger())); +} + +HelpedButton::HelpedButton(QString id, QIcon icon, QString text, QWidget *parent): QWidget(parent) { + init(id); + setToolTip(id); + + button->setIcon(icon); + button->setText(text); +} + +void HelpedButton::init(QString id) { + QHBoxLayout *layout = new QHBoxLayout(this); + button = new QPushButton; + button->setProperty("class", "large"); + + help = new HelpButton(id); + + layout->addWidget(button,1); + layout->addWidget(help); + layout->addStretch(1); +} + + +HelpButton::HelpButton(QString id, QWidget *parent): QToolButton(parent) { + setCursor(Qt::PointingHandCursor); + setIcon(QIcon::fromTheme("help-circle")); + //setStyleSheet("QToolButton { color: #ff00ff; }"); + connect(this, SIGNAL(clicked()), this, SLOT(showHelp())); + + setStyleSheet("QToolButton { border: none; outline: none; }"); +} +void HelpButton::setId(QString id) { + setObjectName(id); + setToolTip(id); +} + +void HelpButton::showHelp() { + QString id = objectName(); + HelpDialog &dialog = HelpDialog::instance(); + dialog.showPage(id); + //dialog.raise(); + //dialog.exec(); +} + +HelpLabel::HelpLabel(QString txt, QString help_id, QWidget *parent): QWidget(parent) { + QHBoxLayout *layout = new QHBoxLayout(this); + label = new QLabel(txt); + layout->addWidget(label); + layout->addStretch(); + help = new HelpButton(help_id); + layout->addWidget(help); +} + +HelpRadio::HelpRadio(QString txt, QString help_id, QWidget *parent): QWidget(parent) { + QHBoxLayout *layout = new QHBoxLayout(this); + radio = new QRadioButton(txt); + layout->addWidget(radio); + layout->addStretch(); + HelpButton *help = new HelpButton(help_id); + layout->addWidget(help); + layout->setContentsMargins(0, 0, 0, 0); +} + +HelpCheckBox::HelpCheckBox(QString txt, QString help_id, QWidget *parent): QWidget(parent) { + QHBoxLayout *layout = new QHBoxLayout(this); + checkBox = new QCheckBox(txt); + layout->addWidget(checkBox); + layout->addStretch(); + HelpButton *help = new HelpButton(help_id); + layout->addWidget(help); + layout->setContentsMargins(0, 0, 0, 0); +} + +HelpDialog::HelpDialog(QWidget *parent): QDialog(parent) { + + QSettings settings; + QRect region = settings.value("help_position", QRect(100, 100, 600, 600)).toRect(); + setGeometry(region); + + QVBoxLayout *content = new QVBoxLayout(this); + + QToolBar *toolbar = new QToolBar; + toolbar->addAction(QIcon::fromTheme("home"), "Home", this, SLOT(home())); + toolbar->addSeparator(); + toolbar->addAction(QIcon::fromTheme("chevron-left"), "Back", this, SLOT(backward())); + toolbar->addAction(QIcon::fromTheme("chevron-right"), "Forward", this, SLOT(forward())); + + content->addWidget(toolbar); + + browser = new QTextBrowser; + content->addWidget(browser); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + + content->addWidget(buttonBox); +} + +HelpDialog& HelpDialog::instance() { + if (!m_instance) { + m_instance = new HelpDialog(); // Create the instance if it doesn't exist + } + return *m_instance; +} + +void HelpDialog::accept() { + QSettings().setValue("help_position", geometry()); + QDialog::accept(); +} + +void HelpDialog::home() { + showPage("index"); +} + +void HelpDialog::forward() { + browser->forward(); +} + +void HelpDialog::backward() { + browser->backward(); +} + +void HelpDialog::showPage(QString id) { + QUrl url("qrc:/docs/" + id + ".md"); +#if QT_VERSION > QT_VERSION_CHECK(5, 15, 0) + browser->setSource(url, QTextDocument::MarkdownResource); +#else + browser->setSource(url); +#endif + setGeometry(QSettings().value("help_position", QRect(100, 100, 600, 600)).toRect()); + show(); +} + diff --git a/relightlab/helpbutton.h b/relightlab/helpbutton.h new file mode 100644 index 00000000..97a4379c --- /dev/null +++ b/relightlab/helpbutton.h @@ -0,0 +1,87 @@ +#ifndef HELPBUTTON_H +#define HELPBUTTON_H + +#include +#include + +class QPushButton; +class QIcon; +class QAction; +class QRadioButton; +class QCheckBox; + +class HelpedButton: public QWidget { + Q_OBJECT +public: + HelpedButton(QAction *action, QString url, QWidget *parent = nullptr); + HelpedButton(QString id, QIcon icon, QString text, QWidget *parent = nullptr); + //void setDefaultAction(QAction &a); +signals: + void clicked(); + +private: + QPushButton *button = nullptr; + QToolButton *help = nullptr; + + void init(QString id); +}; + +class HelpButton: public QToolButton { + Q_OBJECT +public: + HelpButton(QString id, QWidget *parent = nullptr); + void setId(QString id); + +public slots: + void showHelp(); +}; + +class QLabel; + +class HelpLabel: public QWidget { + public: + HelpLabel(QString txt, QString help_id, QWidget *parent = nullptr); + QLabel *label = nullptr; + HelpButton *help = nullptr; +}; + +class HelpRadio: public QWidget { + public: + HelpRadio(QString txt, QString help_id, QWidget *parent = nullptr); + QRadioButton *radioButton() { return radio; } + private: + QRadioButton *radio = nullptr; +}; + +class HelpCheckBox: public QWidget { + public: + HelpCheckBox(QString txt, QString help_id, QWidget *parent = nullptr); + QCheckBox *checkBoxButton() { return checkBox; } + private: + QCheckBox *checkBox = nullptr; +}; + + +class QTextBrowser; + +class HelpDialog: public QDialog { + Q_OBJECT +public: + void showPage(QString id); + static HelpDialog& instance(); // Static method to get the instance + +public slots: + void accept(); + void home(); //get to initial documentation page + void forward(); + void backward(); +private: + QTextBrowser *browser = nullptr; + + explicit HelpDialog(QWidget *parent = nullptr); + HelpDialog(const HelpDialog&) = delete; // Disable copy constructor + HelpDialog& operator=(const HelpDialog&) = delete; // Disable assignment operator + + static HelpDialog* m_instance; // Static instance of Dialog +}; +#endif // HELPBUTTON_H diff --git a/relightlab/homeframe.cpp b/relightlab/homeframe.cpp new file mode 100644 index 00000000..5cb8fa20 --- /dev/null +++ b/relightlab/homeframe.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include +#include + +#include "homeframe.h" +#include "relightapp.h" +#include "recentprojects.h" +#include "helpbutton.h" + +void setDefaultAction(QPushButton *button, QAction *action) { + QObject::connect(button, SIGNAL(clicked(bool)), action, SLOT(trigger())); + button->setText(action->text()); + button->setIcon(action->icon()); +} + + +HomeFrame::HomeFrame() { + setObjectName("home"); + + //setStyleSheet(".home { background:red; padding-top:100px }"); + QHBoxLayout *contentLayout = new QHBoxLayout(this); + contentLayout->addStretch(1); + + // Left column + QVBoxLayout *leftColumnLayout = new QVBoxLayout(); + + // Title label + QLabel *titleLabel = new QLabel("

RelightLab

"); + titleLabel->setMinimumWidth(200); + + leftColumnLayout->addWidget(titleLabel); + + HelpedButton *new_project = new HelpedButton(qRelightApp->action("new_project"), "interface/new_project"); + leftColumnLayout->addWidget(new_project); + + HelpedButton *open_project = new HelpedButton(qRelightApp->action("open_project"), "interface/open_project"); + leftColumnLayout->addWidget(open_project); + + QLabel *recentLabel = new QLabel("

Recent projects:

"); + leftColumnLayout->addSpacing(20); + leftColumnLayout->addWidget(recentLabel); + + + for(QString filename: recentProjects()) { + QLabel *label = new QLabel("" + filename + ""); + label->setProperty("class", "recent"); + label->setWordWrap(true); + leftColumnLayout->addWidget(label); + connect(label, SIGNAL(linkActivated(QString)), qRelightApp, SLOT(openProject(QString))); + } + leftColumnLayout->addStretch(1); + + // Add columns to the content layout + contentLayout->addLayout(leftColumnLayout, 2); + + // Right column + QTextBrowser *browser = new QTextBrowser(this); + browser->setStyleSheet("margin-left:40px; margin-top: 40px; background:transparent;"); + browser->setAlignment(Qt::AlignTop); +#if QT_VERSION > QT_VERSION_CHECK(5, 15, 0) + browser->setSource(QUrl("qrc:/docs/home.md"), QTextDocument::MarkdownResource); +#else + browser->setSource(QUrl("qrc:/docs/home.md")); +#endif + browser->setMinimumWidth(400); + contentLayout->addWidget(browser, 2); + + contentLayout->addStretch(1); + + // Set layout margins and spacing + contentLayout->setContentsMargins(20, 20, 20, 20); + contentLayout->setSpacing(20); + +} diff --git a/relightlab/homeframe.h b/relightlab/homeframe.h new file mode 100644 index 00000000..3b4727c4 --- /dev/null +++ b/relightlab/homeframe.h @@ -0,0 +1,11 @@ +#ifndef HOMEWIDGET_H +#define HOMEWIDGET_H + +#include + +class HomeFrame: public QFrame { +public: + HomeFrame(); +}; + +#endif // HOMEWIDGET_H diff --git a/relightlab/icons/dark/index.theme b/relightlab/icons/dark/index.theme new file mode 100644 index 00000000..c7e48198 --- /dev/null +++ b/relightlab/icons/dark/index.theme @@ -0,0 +1,10 @@ +[Icon Theme] +Name=dark +Comment=Derived from feather icons. +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 diff --git a/relightlab/icons/dark/scalable/camera.svg b/relightlab/icons/dark/scalable/camera.svg new file mode 100644 index 00000000..b46daebb --- /dev/null +++ b/relightlab/icons/dark/scalable/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/cancel.svg b/relightlab/icons/dark/scalable/cancel.svg new file mode 100644 index 00000000..f509571f --- /dev/null +++ b/relightlab/icons/dark/scalable/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/cast.svg b/relightlab/icons/dark/scalable/cast.svg new file mode 100644 index 00000000..83d8f0f0 --- /dev/null +++ b/relightlab/icons/dark/scalable/cast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/check.svg b/relightlab/icons/dark/scalable/check.svg new file mode 100644 index 00000000..27682f73 --- /dev/null +++ b/relightlab/icons/dark/scalable/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/chevron-left.svg b/relightlab/icons/dark/scalable/chevron-left.svg new file mode 100644 index 00000000..68c1c1ee --- /dev/null +++ b/relightlab/icons/dark/scalable/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/chevron-right.svg b/relightlab/icons/dark/scalable/chevron-right.svg new file mode 100644 index 00000000..1d9bd137 --- /dev/null +++ b/relightlab/icons/dark/scalable/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/chevrons-down.svg b/relightlab/icons/dark/scalable/chevrons-down.svg new file mode 100644 index 00000000..f16b0d8b --- /dev/null +++ b/relightlab/icons/dark/scalable/chevrons-down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/relightlab/icons/dark/scalable/chevrons-up.svg b/relightlab/icons/dark/scalable/chevrons-up.svg new file mode 100644 index 00000000..a68c747c --- /dev/null +++ b/relightlab/icons/dark/scalable/chevrons-up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/relightlab/icons/dark/scalable/crop.svg b/relightlab/icons/dark/scalable/crop.svg new file mode 100644 index 00000000..ab9476ef --- /dev/null +++ b/relightlab/icons/dark/scalable/crop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/download.svg b/relightlab/icons/dark/scalable/download.svg new file mode 100644 index 00000000..06b7876e --- /dev/null +++ b/relightlab/icons/dark/scalable/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/edit.svg b/relightlab/icons/dark/scalable/edit.svg new file mode 100644 index 00000000..66875f1b --- /dev/null +++ b/relightlab/icons/dark/scalable/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/file.svg b/relightlab/icons/dark/scalable/file.svg new file mode 100644 index 00000000..ac38a5d7 --- /dev/null +++ b/relightlab/icons/dark/scalable/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/folder.svg b/relightlab/icons/dark/scalable/folder.svg new file mode 100644 index 00000000..406872da --- /dev/null +++ b/relightlab/icons/dark/scalable/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/grid.svg b/relightlab/icons/dark/scalable/grid.svg new file mode 100644 index 00000000..037193c8 --- /dev/null +++ b/relightlab/icons/dark/scalable/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/help-circle.svg b/relightlab/icons/dark/scalable/help-circle.svg new file mode 100644 index 00000000..aba74a57 --- /dev/null +++ b/relightlab/icons/dark/scalable/help-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/highlight.svg b/relightlab/icons/dark/scalable/highlight.svg new file mode 100644 index 00000000..37270a02 --- /dev/null +++ b/relightlab/icons/dark/scalable/highlight.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/dark/scalable/home.svg b/relightlab/icons/dark/scalable/home.svg new file mode 100644 index 00000000..d269bb04 --- /dev/null +++ b/relightlab/icons/dark/scalable/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/image.svg b/relightlab/icons/dark/scalable/image.svg new file mode 100644 index 00000000..00bab9b8 --- /dev/null +++ b/relightlab/icons/dark/scalable/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/list.svg b/relightlab/icons/dark/scalable/list.svg new file mode 100644 index 00000000..1c2ecba4 --- /dev/null +++ b/relightlab/icons/dark/scalable/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/loader.svg b/relightlab/icons/dark/scalable/loader.svg new file mode 100644 index 00000000..df71e64d --- /dev/null +++ b/relightlab/icons/dark/scalable/loader.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/relightlab/icons/dark/scalable/maximize.svg b/relightlab/icons/dark/scalable/maximize.svg new file mode 100644 index 00000000..aac6fa2a --- /dev/null +++ b/relightlab/icons/dark/scalable/maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/minus.svg b/relightlab/icons/dark/scalable/minus.svg new file mode 100644 index 00000000..f9404af3 --- /dev/null +++ b/relightlab/icons/dark/scalable/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/pause.svg b/relightlab/icons/dark/scalable/pause.svg new file mode 100644 index 00000000..593baa21 --- /dev/null +++ b/relightlab/icons/dark/scalable/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/play.svg b/relightlab/icons/dark/scalable/play.svg new file mode 100644 index 00000000..864b1c90 --- /dev/null +++ b/relightlab/icons/dark/scalable/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/plus.svg b/relightlab/icons/dark/scalable/plus.svg new file mode 100644 index 00000000..a3900c92 --- /dev/null +++ b/relightlab/icons/dark/scalable/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/rotate-ccw b/relightlab/icons/dark/scalable/rotate-ccw new file mode 100644 index 00000000..b94a7c97 --- /dev/null +++ b/relightlab/icons/dark/scalable/rotate-ccw @@ -0,0 +1,47 @@ + + + + + + + diff --git a/relightlab/icons/dark/scalable/rotate-ccw.svg b/relightlab/icons/dark/scalable/rotate-ccw.svg new file mode 100644 index 00000000..bdf2c65e --- /dev/null +++ b/relightlab/icons/dark/scalable/rotate-ccw.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/dark/scalable/rotate-cw b/relightlab/icons/dark/scalable/rotate-cw new file mode 100644 index 00000000..6425ffc8 --- /dev/null +++ b/relightlab/icons/dark/scalable/rotate-cw @@ -0,0 +1,47 @@ + + + + + + + diff --git a/relightlab/icons/dark/scalable/rotate-cw.svg b/relightlab/icons/dark/scalable/rotate-cw.svg new file mode 100644 index 00000000..ccc1f1f2 --- /dev/null +++ b/relightlab/icons/dark/scalable/rotate-cw.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/dark/scalable/ruler.svg b/relightlab/icons/dark/scalable/ruler.svg new file mode 100644 index 00000000..3b795af1 --- /dev/null +++ b/relightlab/icons/dark/scalable/ruler.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/relightlab/icons/dark/scalable/save.svg b/relightlab/icons/dark/scalable/save.svg new file mode 100644 index 00000000..67588097 --- /dev/null +++ b/relightlab/icons/dark/scalable/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/settings.svg b/relightlab/icons/dark/scalable/settings.svg new file mode 100644 index 00000000..16adb31f --- /dev/null +++ b/relightlab/icons/dark/scalable/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/sliders.svg b/relightlab/icons/dark/scalable/sliders.svg new file mode 100644 index 00000000..ea6d962c --- /dev/null +++ b/relightlab/icons/dark/scalable/sliders.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/square.svg b/relightlab/icons/dark/scalable/square.svg new file mode 100644 index 00000000..5d72cc6d --- /dev/null +++ b/relightlab/icons/dark/scalable/square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/sun.svg b/relightlab/icons/dark/scalable/sun.svg new file mode 100644 index 00000000..8f41bcf2 --- /dev/null +++ b/relightlab/icons/dark/scalable/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/trash-2.svg b/relightlab/icons/dark/scalable/trash-2.svg new file mode 100644 index 00000000..2568f87c --- /dev/null +++ b/relightlab/icons/dark/scalable/trash-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/zoom-in.svg b/relightlab/icons/dark/scalable/zoom-in.svg new file mode 100644 index 00000000..392d6d2f --- /dev/null +++ b/relightlab/icons/dark/scalable/zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/dark/scalable/zoom-out.svg b/relightlab/icons/dark/scalable/zoom-out.svg new file mode 100644 index 00000000..1b83b4c2 --- /dev/null +++ b/relightlab/icons/dark/scalable/zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/index.theme b/relightlab/icons/light/index.theme new file mode 100644 index 00000000..03e7d2fe --- /dev/null +++ b/relightlab/icons/light/index.theme @@ -0,0 +1,10 @@ +[Icon Theme] +Name=light +Comment=Derived from feather icons. +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 diff --git a/relightlab/icons/light/scalable/camera.svg b/relightlab/icons/light/scalable/camera.svg new file mode 100644 index 00000000..0e7f0603 --- /dev/null +++ b/relightlab/icons/light/scalable/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/cancel.svg b/relightlab/icons/light/scalable/cancel.svg new file mode 100644 index 00000000..7d5875ca --- /dev/null +++ b/relightlab/icons/light/scalable/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/cast.svg b/relightlab/icons/light/scalable/cast.svg new file mode 100644 index 00000000..63c954d9 --- /dev/null +++ b/relightlab/icons/light/scalable/cast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/check.svg b/relightlab/icons/light/scalable/check.svg new file mode 100644 index 00000000..1c209899 --- /dev/null +++ b/relightlab/icons/light/scalable/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/chevron-left.svg b/relightlab/icons/light/scalable/chevron-left.svg new file mode 100644 index 00000000..747d46d9 --- /dev/null +++ b/relightlab/icons/light/scalable/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/chevron-right.svg b/relightlab/icons/light/scalable/chevron-right.svg new file mode 100644 index 00000000..258de414 --- /dev/null +++ b/relightlab/icons/light/scalable/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/chevrons-down.svg b/relightlab/icons/light/scalable/chevrons-down.svg new file mode 100644 index 00000000..54d23aba --- /dev/null +++ b/relightlab/icons/light/scalable/chevrons-down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/relightlab/icons/light/scalable/chevrons-up.svg b/relightlab/icons/light/scalable/chevrons-up.svg new file mode 100644 index 00000000..da8c10c4 --- /dev/null +++ b/relightlab/icons/light/scalable/chevrons-up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/relightlab/icons/light/scalable/crop.svg b/relightlab/icons/light/scalable/crop.svg new file mode 100644 index 00000000..ffbfd045 --- /dev/null +++ b/relightlab/icons/light/scalable/crop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/download.svg b/relightlab/icons/light/scalable/download.svg new file mode 100644 index 00000000..76767a92 --- /dev/null +++ b/relightlab/icons/light/scalable/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/edit.svg b/relightlab/icons/light/scalable/edit.svg new file mode 100644 index 00000000..ec7b4ca2 --- /dev/null +++ b/relightlab/icons/light/scalable/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/file.svg b/relightlab/icons/light/scalable/file.svg new file mode 100644 index 00000000..378519ab --- /dev/null +++ b/relightlab/icons/light/scalable/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/folder.svg b/relightlab/icons/light/scalable/folder.svg new file mode 100644 index 00000000..134458b9 --- /dev/null +++ b/relightlab/icons/light/scalable/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/grid.svg b/relightlab/icons/light/scalable/grid.svg new file mode 100644 index 00000000..8ef2e9d8 --- /dev/null +++ b/relightlab/icons/light/scalable/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/help-circle.svg b/relightlab/icons/light/scalable/help-circle.svg new file mode 100644 index 00000000..51fddd80 --- /dev/null +++ b/relightlab/icons/light/scalable/help-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/highlight.svg b/relightlab/icons/light/scalable/highlight.svg new file mode 100644 index 00000000..9794e9c2 --- /dev/null +++ b/relightlab/icons/light/scalable/highlight.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/light/scalable/home.svg b/relightlab/icons/light/scalable/home.svg new file mode 100644 index 00000000..7bb31b23 --- /dev/null +++ b/relightlab/icons/light/scalable/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/image.svg b/relightlab/icons/light/scalable/image.svg new file mode 100644 index 00000000..a7d84b98 --- /dev/null +++ b/relightlab/icons/light/scalable/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/list.svg b/relightlab/icons/light/scalable/list.svg new file mode 100644 index 00000000..5ce38eaa --- /dev/null +++ b/relightlab/icons/light/scalable/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/loader.svg b/relightlab/icons/light/scalable/loader.svg new file mode 100644 index 00000000..293bb70d --- /dev/null +++ b/relightlab/icons/light/scalable/loader.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/relightlab/icons/light/scalable/maximize.svg b/relightlab/icons/light/scalable/maximize.svg new file mode 100644 index 00000000..fc305189 --- /dev/null +++ b/relightlab/icons/light/scalable/maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/minus.svg b/relightlab/icons/light/scalable/minus.svg new file mode 100644 index 00000000..93cc7340 --- /dev/null +++ b/relightlab/icons/light/scalable/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/pause.svg b/relightlab/icons/light/scalable/pause.svg new file mode 100644 index 00000000..4e78038d --- /dev/null +++ b/relightlab/icons/light/scalable/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/play.svg b/relightlab/icons/light/scalable/play.svg new file mode 100644 index 00000000..fd76e30d --- /dev/null +++ b/relightlab/icons/light/scalable/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/plus.svg b/relightlab/icons/light/scalable/plus.svg new file mode 100644 index 00000000..703c5b7b --- /dev/null +++ b/relightlab/icons/light/scalable/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/rotate-ccw (copy).svg b/relightlab/icons/light/scalable/rotate-ccw (copy).svg new file mode 100644 index 00000000..a2340f2f --- /dev/null +++ b/relightlab/icons/light/scalable/rotate-ccw (copy).svg @@ -0,0 +1,47 @@ + + + + + + + diff --git a/relightlab/icons/light/scalable/rotate-ccw.svg b/relightlab/icons/light/scalable/rotate-ccw.svg new file mode 100644 index 00000000..113d3c3b --- /dev/null +++ b/relightlab/icons/light/scalable/rotate-ccw.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/light/scalable/rotate-cw (copy).svg b/relightlab/icons/light/scalable/rotate-cw (copy).svg new file mode 100644 index 00000000..e637a3fd --- /dev/null +++ b/relightlab/icons/light/scalable/rotate-cw (copy).svg @@ -0,0 +1,47 @@ + + + + + + + diff --git a/relightlab/icons/light/scalable/rotate-cw.svg b/relightlab/icons/light/scalable/rotate-cw.svg new file mode 100644 index 00000000..e973d364 --- /dev/null +++ b/relightlab/icons/light/scalable/rotate-cw.svg @@ -0,0 +1 @@ + diff --git a/relightlab/icons/light/scalable/ruler.svg b/relightlab/icons/light/scalable/ruler.svg new file mode 100644 index 00000000..8e6deb14 --- /dev/null +++ b/relightlab/icons/light/scalable/ruler.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/relightlab/icons/light/scalable/save.svg b/relightlab/icons/light/scalable/save.svg new file mode 100644 index 00000000..46c72990 --- /dev/null +++ b/relightlab/icons/light/scalable/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/settings.svg b/relightlab/icons/light/scalable/settings.svg new file mode 100644 index 00000000..19c27265 --- /dev/null +++ b/relightlab/icons/light/scalable/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/sliders.svg b/relightlab/icons/light/scalable/sliders.svg new file mode 100644 index 00000000..19c93852 --- /dev/null +++ b/relightlab/icons/light/scalable/sliders.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/square.svg b/relightlab/icons/light/scalable/square.svg new file mode 100644 index 00000000..6eabc77d --- /dev/null +++ b/relightlab/icons/light/scalable/square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/sun.svg b/relightlab/icons/light/scalable/sun.svg new file mode 100644 index 00000000..7f51b94d --- /dev/null +++ b/relightlab/icons/light/scalable/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/trash-2.svg b/relightlab/icons/light/scalable/trash-2.svg new file mode 100644 index 00000000..f24d55bf --- /dev/null +++ b/relightlab/icons/light/scalable/trash-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/zoom-in.svg b/relightlab/icons/light/scalable/zoom-in.svg new file mode 100644 index 00000000..da4572d2 --- /dev/null +++ b/relightlab/icons/light/scalable/zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light/scalable/zoom-out.svg b/relightlab/icons/light/scalable/zoom-out.svg new file mode 100644 index 00000000..fd678d72 --- /dev/null +++ b/relightlab/icons/light/scalable/zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/relightlab/icons/light2dark.sh b/relightlab/icons/light2dark.sh new file mode 100644 index 00000000..8ae5d92b --- /dev/null +++ b/relightlab/icons/light2dark.sh @@ -0,0 +1,6 @@ +for i in light/scalable/*.svg; do + file="$(basename -- $i)" + echo $file + sed 's/currentColor/white/' "$i" > dark/scalable/"$file"; +done; + diff --git a/relightlab/imageframe.cpp b/relightlab/imageframe.cpp new file mode 100644 index 00000000..638a18c8 --- /dev/null +++ b/relightlab/imageframe.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "relightapp.h" +#include "imageframe.h" +//#include "canvas.h" +#include "imageview.h" +#include "imagelist.h" +#include "imagegrid.h" + +#include +using namespace std; + +ImageFrame::ImageFrame(QWidget *parent): QFrame(parent) { + QVBoxLayout *container = new QVBoxLayout(this); + + QHBoxLayout *toolbars = new QHBoxLayout(); + + left_toolbar = new QToolBar; + + left_toolbar->addAction(qRelightApp->action("rotate_left")); + left_toolbar->addAction(qRelightApp->action("rotate_right")); + + + toolbars->addWidget(left_toolbar, 0, Qt::AlignLeft); + + center_toolbar = new QToolBar; + center_toolbar->addAction(qRelightApp->action("zoom_fit")); + center_toolbar->addAction(qRelightApp->action("zoom_one")); + center_toolbar->addAction(qRelightApp->action("zoom_in")); + center_toolbar->addAction(qRelightApp->action("zoom_out")); + center_toolbar->addAction(qRelightApp->action("previous_image")); + center_toolbar->addAction(qRelightApp->action("next_image")); + toolbars->addWidget(center_toolbar, 0, Qt::AlignCenter); + + + right_toolbar = new QToolBar; + right_toolbar->addAction(qRelightApp->action("show_image")); + right_toolbar->addAction(qRelightApp->action("show_list")); + right_toolbar->addAction(qRelightApp->action("show_grid")); + toolbars->addWidget(right_toolbar, 0, Qt::AlignRight); + + container->addLayout(toolbars); + + QHBoxLayout *content = new QHBoxLayout; + + image_list = new ImageList(); + image_list->hide(); + content->addWidget(image_list, 0); + + image_grid = new ImageGrid(); + connect(qRelightApp, SIGNAL(updateThumbnail(int)), image_grid, SLOT(updateThumbnail(int))); + image_grid->hide(); + content->addWidget(image_grid, 1); + + content->addWidget(image_view = new ImageView, 1); + + connect(image_list, SIGNAL(skipChanged(int,bool)), image_grid, SLOT(setSkipped(int,bool))); + connect(image_list, SIGNAL(skipChanged(int,bool)), image_view, SLOT(setSkipped(int,bool))); + connect(image_grid, SIGNAL(skipChanged(int,bool)), image_list, SLOT(setSkipped(int,bool))); + connect(image_grid, SIGNAL(skipChanged(int,bool)), image_view, SLOT(setSkipped(int,bool))); + connect(image_view, SIGNAL(skipChanged(int,bool)), image_grid, SLOT(setSkipped(int,bool))); + connect(image_view, SIGNAL(skipChanged(int,bool)), image_list, SLOT(setSkipped(int,bool))); + + connect(qRelightApp->action("zoom_fit"), SIGNAL(triggered(bool)), image_view, SLOT(fit())); + connect(qRelightApp->action("zoom_one"), SIGNAL(triggered(bool)), image_view, SLOT(one())); + connect(qRelightApp->action("zoom_in"), SIGNAL(triggered(bool)), image_view, SLOT(zoomIn())); + connect(qRelightApp->action("zoom_out"), SIGNAL(triggered(bool)), image_view, SLOT(zoomOut())); + connect(qRelightApp->action("previous_image"), SIGNAL(triggered(bool)), this, SLOT(previousImage())); + connect(qRelightApp->action("next_image"), SIGNAL(triggered(bool)), this, SLOT(nextImage())); + + connect(image_list, SIGNAL(itemPressed(QListWidgetItem*)), this, SLOT(showImageItem(QListWidgetItem*))); + + connect(qRelightApp->action("show_image"), SIGNAL(triggered(bool)), this, SLOT(imageMode())); + connect(qRelightApp->action("show_list"), SIGNAL(triggered(bool)), this, SLOT(listMode())); + connect(qRelightApp->action("show_grid"), SIGNAL(triggered(bool)), this, SLOT(gridMode())); + + container->addLayout(content); + + status = new QStatusBar(); + container->addWidget(status); +} + +void ImageFrame::clear() { + image_list->clear(); + image_grid->clear(); + image_view->clear(); +} + +void ImageFrame::init() { + image_list->init(); + image_grid->init(); + image_view->clear(); + + if(qRelightApp->project().images.size()) { + showImage(0); + image_view->fit(); + } + listMode(); //TODO actually use last used mode used by the user but only in imageframe +} + +int ImageFrame::currentImage() { + //TODO not properly elegant.... + return image_list->currentRow(); +} + +void ImageFrame::showImage(int id) { + + Project &project = qRelightApp->project(); + + image_list->setCurrentRow(id); + qRelightApp->action("previous_image")->setEnabled(id != 0); + qRelightApp->action("next_image")->setEnabled(id != (int)project.images.size()-1); + + image_view->showImage(id); + + int w = project.imgsize.width(); + int h = project.imgsize.height(); + status->showMessage(QString("%1x%2 %3").arg(w).arg(h).arg(QFileInfo(project.images[id].filename).canonicalFilePath())); +} + +void ImageFrame::previousImage() { + int current = image_list->currentRow(); + if(current-- <= 0) + return; + image_list->setCurrentRow(current); + showImage(current); +} + +void ImageFrame::nextImage() { + int current = image_list->currentRow(); + if(current++ == (int)qRelightApp->project().images.size()-1) + return; + image_list->setCurrentRow(current); + showImage(current); + //TODO enable and disable previous//next maybe in showImage +} + +void ImageFrame::showImageItem(QListWidgetItem *item) { + int id =item->listWidget()->currentRow(); + showImage(id); +} + +void ImageFrame::imageMode() { + image_view->show(); + image_list->hide(); + image_grid->hide(); +} +void ImageFrame::listMode() { + image_view->show(); + image_list->show(); + image_grid->hide(); + +} +void ImageFrame::gridMode() { + image_view->hide(); + image_list->hide(); + image_grid->show(); +} + diff --git a/relightlab/imageframe.h b/relightlab/imageframe.h new file mode 100644 index 00000000..7117025d --- /dev/null +++ b/relightlab/imageframe.h @@ -0,0 +1,49 @@ +#ifndef IMAGEFRAME_H +#define IMAGEFRAME_H + +#include +#include + +class Canvas; +class ImageList; +class ImageGrid; +class ImageView; + +class QStatusBar; +class QGraphicsPixmapItem; +class QGraphicsScene; +class QListWidgetItem; +class QToolBar; +class FlowLayout; + + +class ImageFrame: public QFrame { + Q_OBJECT +public: + enum Mode { SINGLE, LIST, THUMBNAILS }; + ImageList *image_list = nullptr; + ImageGrid *image_grid = nullptr; + ImageView *image_view = nullptr; + //Canvas *canvas = nullptr; + QStatusBar *status = nullptr; + QToolBar *left_toolbar; + QToolBar *center_toolbar; + QToolBar *right_toolbar; + + ImageFrame(QWidget *parent = nullptr); + void clear(); + void init(); + int currentImage(); + void showImage(int id); //new project loaded. + +public slots: + void previousImage(); + void nextImage(); + void showImageItem(QListWidgetItem *item); + + void imageMode(); + void listMode(); + void gridMode(); +}; + +#endif // IMAGEFRAME_H diff --git a/relightlab/imagegrid.cpp b/relightlab/imagegrid.cpp new file mode 100644 index 00000000..9e030f82 --- /dev/null +++ b/relightlab/imagegrid.cpp @@ -0,0 +1,85 @@ +#include "imagegrid.h" +#include "relightapp.h" +#include "flowlayout.h" + +#include +#include +#include +#include +#include + +#include + +#include +using namespace std; + +ImageThumb::ImageThumb(QImage img, const QString& text, bool skip, QWidget* parent): QWidget(parent) { + + QVBoxLayout* layout = new QVBoxLayout(this); + + QLabel *label = new QLabel; + label->setPixmap(QPixmap::fromImage(img)); + layout->addWidget(label); + + QCheckBox *checkbox = new QCheckBox(text); + + checkbox->setChecked(!skip); + connect(checkbox, SIGNAL(stateChanged(int)), this, SIGNAL(skipChanged(int))); + layout->addWidget(checkbox); + + layout->setSpacing(5); + layout->setContentsMargins(5, 5, 5, 5); +} + +void ImageThumb::setSkipped(bool skip) { + findChild()->setChecked(!skip); +} + +void ImageThumb::setThumbnail(QImage thumb) { + findChild()->setPixmap(QPixmap::fromImage(thumb)); + +} + +ImageGrid::ImageGrid(QWidget *parent): QScrollArea(parent) { + + flowlayout = new FlowLayout(); + setWidgetResizable(true); + setWidget(new QWidget); + widget()->setLayout(flowlayout); + setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); +} + +void ImageGrid::clear() { + flowlayout->clear(); +} + +void ImageGrid::init() { + Project &project = qRelightApp->project(); + std::vector &thumbnails = qRelightApp->thumbnails(); + + assert(project.images.size() == thumbnails.size()); + + for(size_t i = 0; i < project.images.size(); i++) { + QImage thumbnail = thumbnails[i]; + Image &image = project.images[i]; + QFileInfo info(image.filename); + ImageThumb *thumb = new ImageThumb(thumbnail, info.fileName(), image.skip); + connect(thumb, &ImageThumb::skipChanged, [this, i, &image](int state){ + image.skip = (state==0); + this->emit skipChanged(i, image.skip); + }); + flowlayout->addWidget(thumb); + } +} + +void ImageGrid::setSkipped(int image, bool skip) { + ImageThumb *thumb = dynamic_cast(flowlayout->itemAt(image)->widget()); + thumb->setSkipped(skip); +} + +void ImageGrid::updateThumbnail(int pos) { + ImageThumb *thumb = dynamic_cast(flowlayout->itemAt(pos)->widget()); + + QMutexLocker lock(&qRelightApp->thumbails_lock); + thumb->setThumbnail(qRelightApp->thumbnails()[pos]); +} diff --git a/relightlab/imagegrid.h b/relightlab/imagegrid.h new file mode 100644 index 00000000..104864a1 --- /dev/null +++ b/relightlab/imagegrid.h @@ -0,0 +1,43 @@ +#ifndef IMAGEGRID_H +#define IMAGEGRID_H + +#include +#include + + +class FlowLayout; + + +class ImageThumb : public QWidget { + Q_OBJECT +public: + ImageThumb(QImage img, const QString& text, bool skip, QWidget* parent = nullptr); + void setSkipped(bool skip); + void setThumbnail(QImage thumb); +signals: + void skipChanged(int state); + + +}; + + +class ImageGrid: public QScrollArea { + Q_OBJECT +public: + ImageGrid(QWidget *parent = nullptr); + + void clear(); + void init(); + +public slots: + void setSkipped(int image, bool skip); + void updateThumbnail(int pos); + +signals: + void skipChanged(int image, bool skip); + +private: + FlowLayout *flowlayout = nullptr; +}; + +#endif // IMAGEGRID_H diff --git a/relightlab/imagelist.cpp b/relightlab/imagelist.cpp new file mode 100644 index 00000000..5faeb3ea --- /dev/null +++ b/relightlab/imagelist.cpp @@ -0,0 +1,39 @@ +#include "imagelist.h" +#include "relightapp.h" + +#include +#include + +void ImageList::init() { + Project &project = qRelightApp->project(); + clear(); + int count =0; + for(Image &img: project.images) { + QFileInfo info(img.filename); + QListWidgetItem *item = new QListWidgetItem(QString("%1 - %2").arg(count+1).arg(info.fileName())); + item->setData(Qt::UserRole, count); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable); + item->setCheckState(img.skip? Qt::Unchecked : Qt::Checked); + addItem(item); + } + + connect(this, SIGNAL(itemChanged(QListWidgetItem *)), this, SLOT(verifyItem(QListWidgetItem *))); +} + +void ImageList::verifyItem(QListWidgetItem *item) { + int img_number = item->data(Qt::UserRole).toInt(); + bool skip = item->checkState() != Qt::Checked; + + Project &project = qRelightApp->project(); + assert(img_number >= 0 && img_number < project.images.size()); + + project.images[img_number].skip = skip; + + emit skipChanged(img_number, skip); + +} + +void ImageList::setSkipped(int img_number, bool skip) { + QListWidgetItem *item = this->item(img_number); + item->setCheckState(skip? Qt::Unchecked : Qt::Checked); +} diff --git a/relightlab/imagelist.h b/relightlab/imagelist.h new file mode 100644 index 00000000..603292a6 --- /dev/null +++ b/relightlab/imagelist.h @@ -0,0 +1,21 @@ +#ifndef IMAGELIST_H +#define IMAGELIST_H + +#include + +class ImageList: public QListWidget { + Q_OBJECT +public: + ImageList(QWidget *parent = nullptr): QListWidget(parent) {} + + void init(); + +public slots: + void setSkipped(int image, bool skip); + void verifyItem(QListWidgetItem *item); + +signals: + void skipChanged(int image, bool skip); +}; + +#endif // IMAGELIST_H diff --git a/relightlab/imageview.cpp b/relightlab/imageview.cpp new file mode 100644 index 00000000..6581fb87 --- /dev/null +++ b/relightlab/imageview.cpp @@ -0,0 +1,105 @@ +#include "imageview.h" +#include "relightapp.h" +#include "../src/project.h" + +#include +#include +#include +#include + + +ImageView::ImageView(QWidget *parent): Canvas(parent) { + setScene(&scene); + +} + +void ImageView::clear() { + //remove all elements from scene. + scene.clear(); + imagePixmap = nullptr; +} + +void ImageView::showImage(int id) { + Project &project = qRelightApp->project(); + if(project.images.size() <= id) + return; + + QString filename = project.images[id].filename; + + QImage img(project.dir.filePath(filename)); + if(img.isNull()) { + QMessageBox::critical(this, "Houston we have a problem!", "Could not load image " + filename); + return; + } + imagePixmap = new QGraphicsPixmapItem(QPixmap::fromImage(img)); + imagePixmap->setZValue(-1); + scene.addItem(imagePixmap); + + int w = project.imgsize.width(); + int h = project.imgsize.height(); + double sx = double(width()) / w; + double sy = double(height()) / h; + double min_scale = std::min(1.0, std::min(sx, sy)); + min_scale = min_scale; + + current_image = id; +} + +void ImageView::setSkipped(int image, bool skip) { + if(image != current_image) + return; +} + + + +void ImageView::fit() { + if(imagePixmap) + fitInView(imagePixmap->boundingRect()); +} + +void ImageView::one() { + double current_scale = transform().m11(); + double s = 1/current_scale; + scale(s, s); +} + +void ImageView::next() { + if(current_image < qRelightApp->project().images.size()-1) + showImage(current_image+1); +} + +void ImageView::prev() { + if(current_image > 0) + showImage(current_image-1); +} + + +ImageViewer::ImageViewer(QWidget *parent): QFrame(parent) { + QVBoxLayout *layout = new QVBoxLayout(this); + + layout->addWidget(toolbar = new QToolBar(), 0, Qt::AlignCenter); + layout->addWidget(view = new ImageView()); + + QAction *fit = qRelightApp->action("zoom_fit"); + toolbar->addAction(fit->icon(), fit->text(), view, SLOT(fit())); + + QAction *one = qRelightApp->action("zoom_one"); + toolbar->addAction(one->icon(), one->text(), view, SLOT(one())); + + QAction *in = qRelightApp->action("zoom_in"); + toolbar->addAction(in->icon(), in->text(), view, SLOT(zoomIn())); + + QAction *out = qRelightApp->action("zoom_out"); + toolbar->addAction(out->icon(), out->text(), view, SLOT(zoomOut())); + + QAction *prev = qRelightApp->action("previous_image"); + toolbar->addAction(prev->icon(), prev->text(), view, SLOT(prev())); + + QAction *next = qRelightApp->action("next_image"); + toolbar->addAction(next->icon(), next->text(), view, SLOT(next())); +} + +void ImageViewer::showImage(int id) { + view->showImage(id); +} + diff --git a/relightlab/imageview.h b/relightlab/imageview.h new file mode 100644 index 00000000..c1a6853c --- /dev/null +++ b/relightlab/imageview.h @@ -0,0 +1,60 @@ +#ifndef IMAGEVIEW_H +#define IMAGEVIEW_H + +#include "canvas.h" +#include + +class Canvas; +class QGraphicsPixmapItem; + +class QToolBar; + + +class ImageView: public Canvas { + Q_OBJECT +public: + QGraphicsScene scene; + int current_image = -1; + + ImageView(QWidget *parent = nullptr); + void clear(); + + +public slots: + void showImage(int id); + void setSkipped(int image, bool skip); + + void fit(); //fit image on screen + void one(); //scale to 1:1 zoom + void next(); //show next image + void prev(); //show previous image + +signals: + void skipChanged(int image, bool skip); + +protected: + QGraphicsPixmapItem *imagePixmap = nullptr; +}; + +class ImageViewer: public QFrame { + Q_OBJECT +public: + ImageView *view; + + ImageViewer(QWidget *parent = nullptr); + QGraphicsScene &scene() { return view->scene; } + + virtual void showImage(int id); + int currentImage() { return view->current_image; } + +public slots: + void fit() { view->fit(); } + void one() { view->one(); } + void next() { view->next(); } + void prev() { view->prev(); } + +protected: + QToolBar *toolbar; +}; + +#endif // IMAGEVIEW_H diff --git a/relightlab/lightgeometry.cpp b/relightlab/lightgeometry.cpp new file mode 100644 index 00000000..60950fa5 --- /dev/null +++ b/relightlab/lightgeometry.cpp @@ -0,0 +1,160 @@ +#include "lightgeometry.h" +#include "relightapp.h" +#include "helpbutton.h" +#include "directionsview.h" +#include "../src/sphere.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +LightsGeometry::~LightsGeometry() { if(group) delete group; } + + +LightsGeometry::LightsGeometry(QWidget *parent): QFrame(parent) { + + QVBoxLayout *page = new QVBoxLayout(this); + + page->addWidget(new QLabel("

Current dome configuration

")); + //page->addSpacing(10); + + QGridLayout * content = new QGridLayout(); + page->addLayout(content); + + content->addWidget( new QLabel("Filename:"), 0, 0); + content->addWidget(filename = new QLineEdit, 0, 1); + + content->addWidget( new QLabel("Number of images:"), 1, 0); + content->addWidget(images_number = new QSpinBox, 1, 1); + images_number->setRange(1, 1024); + images_number->setEnabled(false); + + content->addWidget(new QLabel("Notes:"), 2, 0); + content->addWidget(notes = new QTextEdit, 2, 1); + notes->setMaximumHeight(100); + connect(notes, &QTextEdit::textChanged, [&]() { qRelightApp->project().dome.notes = notes->toPlainText(); }); + + + + group = new QButtonGroup; + + content->addWidget(directional = new HelpRadio("Directional Lights", "lights/directional"), 3, 0); + content->addWidget(sphere_approx = new HelpRadio("3D light positions on a sphere", "lights/3dsphere"), 4, 0); + content->addWidget(lights3d = new HelpRadio("3D light positions", "lights/3dposition"), 5, 0); + group->addButton(directional->radioButton(), Dome::DIRECTIONAL); + group->addButton(sphere_approx->radioButton(), Dome::SPHERICAL); + group->addButton(lights3d->radioButton(), Dome::LIGHTS3D); + + connect(group, SIGNAL(buttonClicked(QAbstractButton *)), this, SLOT(setSpherical(QAbstractButton *))); + + QFrame *geometry = new QFrame; + geometry->setFrameShape(QFrame::StyledPanel); + + content->addWidget(geometry, 3, 1, 3, 1); + + QGridLayout *grid = new QGridLayout(geometry); + grid->setColumnMinimumWidth(0, 200); + grid->addWidget(new QLabel("Image width:"), 2, 0); + grid->addWidget(image_width = new QDoubleSpinBox, 2, 1); + image_width->setRange(0, 1000); + grid->addWidget(new QLabel("mm"), 2, 2); + connect(image_width, QOverload::of(&QDoubleSpinBox::valueChanged), [&](double v) { qRelightApp->project().dome.imageWidth = v; }); + + grid->addWidget(new QLabel("Diameter:"), 3, 0); + grid->addWidget(diameter = new QDoubleSpinBox, 3, 1); + diameter->setRange(0, 1000); + grid->addWidget(new QLabel("mm"), 3, 2); + connect(diameter, QOverload::of(&QDoubleSpinBox::valueChanged), [&](double v) { qRelightApp->project().dome.domeDiameter = v; }); + + grid->addWidget(new QLabel("Vertical offset:"), 4, 0); + grid->addWidget(vertical_offset = new QDoubleSpinBox, 4, 1); + vertical_offset->setRange(-1000, 1000); + grid->addWidget(new QLabel("mm"), 4, 2); + connect(vertical_offset, QOverload::of(&QDoubleSpinBox::valueChanged), [&](double v) { qRelightApp->project().dome.verticalOffset = v; }); + + + /* it seems basically impossible to have a widget scale while preserving aspect ratio, bummer */ + + content->setSpacing(20); + directions_view = new DirectionsView; + content->addWidget(directions_view, 0, 2, 3, 1, Qt::AlignBottom); + directions_view->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + directions_view->setMaximumSize(200, 200); + directions_view->setMinimumSize(200, 200); + + page->addSpacing(30); +} + +void LightsGeometry::setSpherical(QAbstractButton *button) { + Dome &dome = qRelightApp->project().dome; + dome.lightConfiguration = Dome::DIRECTIONAL; + + bool spherical = (button == sphere_approx->radioButton()); + diameter->setEnabled(spherical == Dome::SPHERICAL); + vertical_offset->setEnabled(spherical == Dome::SPHERICAL); + + if(button == sphere_approx->radioButton()) { + dome.lightConfiguration = Dome::SPHERICAL; + } else if(button == lights3d->radioButton()) { + dome.lightConfiguration = Dome::LIGHTS3D; + } + + init(); +} + +void LightsGeometry::init() { + Dome &dome = qRelightApp->project().dome; + + filename->setText(dome.label); + notes->setText(dome.notes); + images_number->setValue(dome.imagesCount()); + group->button(dome.lightConfiguration)->setChecked(true); + + bool spherical = dome.lightConfiguration == Dome::SPHERICAL; + diameter->setEnabled(spherical); + vertical_offset->setEnabled(spherical); + + image_width->setValue(dome.imageWidth); + diameter->setValue(dome.domeDiameter); + vertical_offset->setValue(dome.verticalOffset); + directions_view->initFromDome(dome); +} + +void LightsGeometry::setDome(Dome dome) { + qRelightApp->project().dome = dome; + init(); +} + +void LightsGeometry::setFromSpheres() { + //get spheres & lens from project + Project &project = qRelightApp->project(); + //call appropriate compute directions/positions + Dome &dome = project.dome; + dome.label = ""; + dome.fromSpheres(project.spheres, project.lens); + + init(); +} + +void LightsGeometry::exportDome() { + QString filename = QFileDialog::getSaveFileName(this, "Select a dome file", qRelightApp->lastProjectDir(), "*.dome"); + if(filename.isNull()) + return; + if(!filename.endsWith(".dome")) + filename += ".dome"; + //TODO Basic checks, label is a problem (use filename! + Dome &dome = qRelightApp->project().dome; + dome.save(filename); + qRelightApp->addDome(filename); +} diff --git a/relightlab/lightgeometry.h b/relightlab/lightgeometry.h new file mode 100644 index 00000000..ddb58694 --- /dev/null +++ b/relightlab/lightgeometry.h @@ -0,0 +1,52 @@ +#ifndef LIGHTGEOMETRY_H +#define LIGHTGEOMETRY_H + +#include "../src/dome.h" +#include + +class QDoubleSpinBox; +class QButtonGroup; +class QSpinBox; +class QTextEdit; +class QLineEdit; +class QAbstractButton; +class HelpRadio; +class DirectionsView; + +/* This class display the current dome parameters (see dome.h for details + * + * The dome is computed using: + * 1) reflective spheres. The project computes the dome using the spheres. + * 2) lp: again the dome is computed on load, but no 3d positions. + */ + +class LightsGeometry: public QFrame { + Q_OBJECT +public: + QSpinBox *images_number; + QTextEdit *notes; + QLineEdit *filename; + HelpRadio *directional; + HelpRadio *sphere_approx; + HelpRadio *lights3d; + + QDoubleSpinBox *image_width; + QDoubleSpinBox *vertical_offset; + QDoubleSpinBox *diameter; + DirectionsView *directions_view; + + QButtonGroup *group = nullptr; + + LightsGeometry(QWidget *parent = nullptr); + ~LightsGeometry(); + + void init(); + + +public slots: + void setDome(Dome dome); //when a dome is selected + void setFromSpheres(); //when reflective spheres reflections have been processed + void setSpherical(QAbstractButton *button); + void exportDome(); +}; +#endif // LIGHTGEOMETRY_H diff --git a/relightlab/lightsframe.cpp b/relightlab/lightsframe.cpp new file mode 100644 index 00000000..4873020d --- /dev/null +++ b/relightlab/lightsframe.cpp @@ -0,0 +1,62 @@ +#include "lightsframe.h" +#include "relightapp.h" +#include "domepanel.h" +#include "spherepanel.h" +#include "lightgeometry.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LightsFrame::LightsFrame() { + QHBoxLayout *page = new QHBoxLayout(this); + QVBoxLayout *content = new QVBoxLayout; + page->addStretch(1); + page->addLayout(content, 5); + page->addStretch(1); + + content->addWidget(new QLabel("

Lights direction setup

")); + content->addSpacing(30); + + content->addWidget(dome_panel = new DomePanel(this)); + + content->addWidget(sphere_panel = new SpherePanel(this)); + content->addSpacing(30); + + geometry = new LightsGeometry(this); + content->addWidget(geometry); + + content->addStretch(); + + connect(sphere_panel, SIGNAL(updated()), geometry, SLOT(setFromSpheres())); + connect(dome_panel, SIGNAL(accept(Dome)), geometry, SLOT(setDome(Dome))); +} + +void LightsFrame::clear() { + sphere_panel->clear(); +} + +void LightsFrame::newSphere() { + sphere_panel->newSphere(); +} + +void LightsFrame::init() { +// bool useSphere = qRelightApp->project().spheres.size(); + + sphere_panel->init(); + dome_panel->init(); + geometry->init(); +} +void LightsFrame::setPixelSize() { + geometry->image_width->setValue(qRelightApp->project().dome.imageWidth); +} + + + diff --git a/relightlab/lightsframe.h b/relightlab/lightsframe.h new file mode 100644 index 00000000..98306435 --- /dev/null +++ b/relightlab/lightsframe.h @@ -0,0 +1,38 @@ +#ifndef LIGHTSFRAME_H +#define LIGHTSFRAME_H + +#include "../src/dome.h" + +#include + +class QLabel; +class QTabWidget; +class SpherePanel; +class DomePanel; +class LightsGeometry; + +/* Dome holds the geometri config + SpherePanel uses the reflective spheres to calculate the light direction. (stored in the project)*/ + + +class LightsFrame: public QFrame { + Q_OBJECT +public: + LightsFrame(); + +public slots: + void newSphere(); + void clear(); + void init(); + void setPixelSize(); + +private: + QTabWidget *choice = nullptr; + SpherePanel *sphere_panel = nullptr; + DomePanel *dome_panel = nullptr; + LightsGeometry *geometry = nullptr; +}; + + + +#endif // LIGHTSFRAME_H diff --git a/relightlab/main.cpp b/relightlab/main.cpp new file mode 100644 index 00000000..bda7af66 --- /dev/null +++ b/relightlab/main.cpp @@ -0,0 +1,31 @@ +#include + +#include +#include +#include + +#include "relightapp.h" +#include "mainwindow.h" + +#include + +#define RELIGHT_STRINGIFY0(v) #v +#define RELIGHT_STRINGIFY(v) RELIGHT_STRINGIFY0(v) + +Project project; + +int main(int argc, char *argv[]) { + setlocale(LC_ALL, ".UTF8"); + + RelightApp app(argc, argv); + + QCoreApplication::setOrganizationName("VCG"); + QCoreApplication::setOrganizationDomain("vcg.isti.cnr.it"); + QCoreApplication::setApplicationName("RelightLab"); + QCoreApplication::setApplicationVersion(RELIGHT_STRINGIFY(RELIGHT_VERSION)); + + app.run(); + + return app.exec(); +} + diff --git a/relightlab/mainwindow.cpp b/relightlab/mainwindow.cpp new file mode 100644 index 00000000..3bb07a21 --- /dev/null +++ b/relightlab/mainwindow.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include + +#include "mainwindow.h" +#include "relightapp.h" +#include "recentprojects.h" +#include "tabwidget.h" +#include "homeframe.h" +#include "imageframe.h" +#include "alignframe.h" +#include "scaleframe.h" +#include "lightsframe.h" +#include "cropframe.h" +#include "rtiframe.h" +#include "normalsframe.h" +#include "queueframe.h" + + +#include +#include +#include +#include + +#include +using namespace std; + +MainWindow::MainWindow() { + setWindowTitle("RelightLab"); + + createMenu(); + + tabs = new TabWidget; + + tabs->addTab(home_frame = new HomeFrame, "Home"); + tabs->addTab(image_frame = new ImageFrame, "Images"); + tabs->addTab(align_frame = new AlignFrame, "Align"); + tabs->addTab(scale_frame = new ScaleFrame, "Scale"); + tabs->addTab(lights_frame = new LightsFrame, "Lights"); + tabs->addTab(crop_frame = new CropFrame, "Crop"); + tabs->addTab(rti_frame = new RtiFrame, "RTI"); + tabs->addTab(normals_frame = new NormalsFrame, "Normals"); + tabs->addTab(queue_frame = new QueueFrame, "Queue"); + + connect(scale_frame, SIGNAL(pixelSizeChanged()), lights_frame, SLOT(setPixelSize())); + connect(rti_frame, SIGNAL(processStarted()), this, SLOT(showQueue())); + connect(normals_frame, SIGNAL(processStarted()), this, SLOT(showQueue())); + + setCentralWidget(tabs); +} + +void MainWindow::showQueue() { + tabs->setCurrentIndex(8); +} + +void MainWindow::setTabIndex(int index) { + tabs->setCurrentIndex(index); +} +void MainWindow::setTabWidget(QWidget *widget) { + tabs->setCurrentWidget(widget); +} + +void MainWindow::createMenu() { + QMenuBar *menubar = new QMenuBar(this);// +// menubar->setGeometry(QRect(0, 0, 1069, 22)); + + QMenu *menuFile = new QMenu(menubar); + menuFile->setTitle("File"); + menubar->addAction(menuFile->menuAction()); + + + menuFile->addAction(qRelightApp->action("new_project")); + menuFile->addAction(qRelightApp->action("open_project")); + + recentMenu = new QMenu("&Recent Projects", this); + menuFile->addMenu(recentMenu); + updateRecentProjectsMenu(); + + menuFile->addSeparator(); + menuFile->addAction(qRelightApp->action("save_project")); + menuFile->addAction(qRelightApp->action("save_project_as")); + menuFile->addSeparator(); + menuFile->addAction(qRelightApp->action("preferences")); + menuFile->addSeparator(); + menuFile->addAction(qRelightApp->action("exit")); + + setMenuBar(menubar); +} + +void MainWindow::updateRecentProjectsMenu() { + recentMenu->clear(); + + QStringList recents = recentProjects(); + + int count = 1; + for (const QString& project : recents) { + QAction *action = new QAction(QString::number(count++) + " | " + project, this); + action->setProperty("filename", project); + connect(action, &QAction::triggered, this, &MainWindow::openRecentProject); + recentMenu->addAction(action); + } +} + +void MainWindow::openRecentProject() { + QAction *action = qobject_cast(sender()); + if (action) { + QString projectFilename = action->property("filename").toString(); + if(!qRelightApp->needsSavingProceed()) + return; + qRelightApp->openProject(projectFilename); + } +} + +void MainWindow::clear() { + image_frame->clear(); + scale_frame->clear(); + lights_frame->clear(); + crop_frame->clear(); +} + +void MainWindow::init() { + image_frame->init(); + scale_frame->init(); + lights_frame->init(); + crop_frame->init(); +} + + diff --git a/relightlab/mainwindow.h b/relightlab/mainwindow.h new file mode 100644 index 00000000..de599864 --- /dev/null +++ b/relightlab/mainwindow.h @@ -0,0 +1,52 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +#include "normalsframe.h" +class QStackedWidget; +class TabWidget; +class HomeFrame; +class ImageFrame; +class AlignFrame; +class ScaleFrame; +class LightsFrame; +class CropFrame; +class RtiFrame; +class NormalsFrame; +class QueueFrame; + + +class MainWindow: public QMainWindow { + Q_OBJECT +public: + MainWindow(); + void createMenu(); + void updateRecentProjectsMenu(); + void openRecentProject(); + + void clear(); //clean up interface and stops project related tasks + void init(); //initialize interface using the current project + void setTabIndex(int index); + void setTabWidget(QWidget *widget); + +public slots: + void showQueue(); + +protected: + + TabWidget *tabs = nullptr; + HomeFrame *home_frame = nullptr; + ImageFrame *image_frame = nullptr; + AlignFrame *align_frame = nullptr; + ScaleFrame *scale_frame = nullptr; + LightsFrame *lights_frame = nullptr; + CropFrame *crop_frame = nullptr; + RtiFrame *rti_frame = nullptr; + NormalsFrame *normals_frame = nullptr; + QueueFrame *queue_frame = nullptr; + + QMenu *recentMenu = nullptr; +}; + +#endif // MAINWINDOW_H diff --git a/relightlab/markerdialog.cpp b/relightlab/markerdialog.cpp new file mode 100644 index 00000000..8d28471d --- /dev/null +++ b/relightlab/markerdialog.cpp @@ -0,0 +1,50 @@ +#include "markerdialog.h" +#include "spherepicking.h" +#include "alignpicking.h" +#include "imageview.h" + +#include +#include + +MarkerDialog::MarkerDialog(Marker marker, QWidget *parent): QDialog(parent) { + + QVBoxLayout *content = new QVBoxLayout(this); + + setModal(true); + switch(marker) { + case SPHERE: + sphere_picking = new SpherePicking(); + content->addWidget(sphere_picking); + + break; + case ALIGN: + align_picking = new AlignPicking(); + content->addWidget(align_picking); + break; + } + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + content->addWidget(buttonBox); + showMaximized(); +} + +void MarkerDialog::setAlign(Align *align) { + align_picking->setAlign(align); +} + +void MarkerDialog::setSphere(Sphere *sphere) { + sphere_picking->setSphere(sphere); +} + + +void MarkerDialog::accept() { + QDialog::accept(); +} + +void MarkerDialog::reject() { + QDialog::reject(); +} diff --git a/relightlab/markerdialog.h b/relightlab/markerdialog.h new file mode 100644 index 00000000..cec6f4ce --- /dev/null +++ b/relightlab/markerdialog.h @@ -0,0 +1,28 @@ +#ifndef MARKERDIALOG_H +#define MARKERDIALOG_H + +#include + +class SpherePicking; +class AlignPicking; +class Align; +class Sphere; + +class MarkerDialog: public QDialog { + Q_OBJECT +public: + enum Marker { SPHERE, ALIGN }; + MarkerDialog(Marker marker, QWidget *parent = nullptr); + void setAlign(Align *align); + void setSphere(Sphere *sphere); + +public slots: + void accept(); + void reject(); + +private: + SpherePicking *sphere_picking = nullptr; + AlignPicking *align_picking = nullptr; +}; + +#endif // SPHEREDIALOG_H diff --git a/relightlab/normalsframe.cpp b/relightlab/normalsframe.cpp new file mode 100644 index 00000000..c2e23415 --- /dev/null +++ b/relightlab/normalsframe.cpp @@ -0,0 +1,100 @@ +#include "normalsframe.h" +#include "normalstask.h" +#include "relightapp.h" +#include "processqueue.h" +#include "helpbutton.h" +#include "../src/project.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +NormalsFrame::NormalsFrame(QWidget *parent): QFrame(parent) { + QVBoxLayout *content = new QVBoxLayout(this); + content->addWidget(new QLabel("

Export normals

")); + content->addSpacing(30); + + content->addWidget(jpg = new QRadioButton("JPEG: normalmap")); + content->addWidget(png = new QRadioButton("PNG: normalmap")); + QButtonGroup *group = new QButtonGroup(this); + group->addButton(jpg); + group->addButton(png); + jpg->setChecked(true); + + content->addSpacing(30); + content->addWidget(new QLabel("

Export 3D surface

")); + + content->addSpacing(30); + QHBoxLayout *discontinuity_layout = new QHBoxLayout; + content->addLayout(discontinuity_layout); + + discontinuity_layout->addWidget(new HelpLabel("Disconitnuity parameter:", "normals/disconituity")); + discontinuity_layout->addWidget(discontinuity = new QDoubleSpinBox); + discontinuity->setRange(0.0, 4.0); + discontinuity->setValue(2.0); + + content->addWidget(tif = new QCheckBox("TIF: depthmap")); + content->addWidget(ply = new QCheckBox("PLY: mesh")); + + content->addWidget(new QLabel("

Flatten normals

")); + content->addWidget(radial = new QCheckBox("Radial")); + content->addWidget(fourier = new QCheckBox("Fourier")); + QHBoxLayout *fourier_layout = new QHBoxLayout; + content->addLayout(fourier_layout); + + fourier_layout->addWidget(new HelpLabel("Fourier image percent: ", "normals/flatten")); + fourier_layout->addWidget(fourier_radius = new QSpinBox); + fourier_radius->setRange(0, 100); + + + QPushButton *save = new QPushButton("Export"); + content->addWidget(save); + + connect(save, SIGNAL(clicked()), this, SLOT(save())); + + content->addStretch(); +} + +void NormalsFrame::save() { + if(qRelightApp->project().dome.directions.size() == 0) { + QMessageBox::warning(this, "Missing light directions.", "You need light directions for this dataset to build a normalmap.\n" + "You can either load a dome or .lp file or mark a reflective sphere in the 'Lights' tab."); + return; + } + QString filter = jpg->isChecked() ? "JPEG Images (*.jpg)" : "PNG Images (*.png)"; + Project &project = qRelightApp->project(); + QString output = QFileDialog::getSaveFileName(this, "Select a filename for the normal map.", project.dir.path(), filter); + if(output.isNull()) + return; + QString extension = jpg->isChecked() ? ".jpg" : ".png"; + if(!output.endsWith(extension)) + output += extension; + + NormalsTask *task = new NormalsTask(output); + if(ply->isChecked()) + task->exportPly = true; + if(tif->isChecked()) + task->exportTiff = true; + + if(radial->isChecked()) + task->flatMethod = RADIAL; + + if(fourier->isChecked()) { + task->flatMethod = FOURIER; + task->m_FlatRadius = fourier_radius->value()/100.0f; + } + + task->initFromProject(project); + + ProcessQueue &queue = ProcessQueue::instance(); + queue.addTask(task); + + emit processStarted(); +} diff --git a/relightlab/normalsframe.h b/relightlab/normalsframe.h new file mode 100644 index 00000000..e93f154d --- /dev/null +++ b/relightlab/normalsframe.h @@ -0,0 +1,33 @@ +#ifndef NORMALSFRAME_H +#define NORMALSFRAME_H + +#include + +class QCheckBox; +class QRadioButton; +class QSpinBox; +class QDoubleSpinBox; + +class NormalsFrame: public QFrame { + Q_OBJECT +public: + NormalsFrame(QWidget *parent = nullptr); + +public slots: + void save(); + +signals: + void processStarted(); + +private: + QRadioButton *jpg = nullptr; + QRadioButton *png = nullptr; + QCheckBox *tif = nullptr; + QCheckBox *ply = nullptr; + QDoubleSpinBox *discontinuity = nullptr; + QCheckBox *radial = nullptr; + QCheckBox *fourier = nullptr; + QSpinBox *fourier_radius = nullptr; +}; + +#endif // NORMALSFRAME_H diff --git a/relightlab/normalstask.cpp b/relightlab/normalstask.cpp new file mode 100644 index 00000000..b6471b19 --- /dev/null +++ b/relightlab/normalstask.cpp @@ -0,0 +1,221 @@ +#include "normalstask.h" +#include "../src/jpeg_decoder.h" +#include "../src/jpeg_encoder.h" +#include "../src/imageset.h" +#include "../src/relight_threadpool.h" +#include "../src/bni_normal_integration.h" +#include "../src/flatnormals.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////// NORMALS TASK ////////////////////////////////////////////////////////// +/// \brief NormalsTask: Takes care of creating the normals from the images given in a folder (inputFolder) and saves the file +/// in the outputFolder. After applying the crop described in the QRect passed as an argument to the constructor, +/// the NormalsTask creates a NormalsWorker for each line in the final image. +/// That NormalsWorker fills a vector with the colors of the normals in that line. +/// + +void NormalsTask::initFromProject(Project &project) { + lens = project.lens; + imageset.width = imageset.image_width = project.lens.width; + imageset.height = imageset.image_height = project.lens.height; + + imageset.images = project.getImages(); + imageset.initImages(project.dir.absolutePath().toStdString().c_str()); + imageset.initLightsFromDome(project.dome); + assert(imageset.lights.size() == imageset.images.size()); + QRect &crop = project.crop; + if(!crop.isNull()) { + imageset.crop(crop.left(), crop.top(), crop.width(), crop.height()); + } + pixelSize = project.pixelSize; +} + +void NormalsTask::run() { + status = RUNNING; + + + imageset.setCallback(nullptr); + + std::vector normals(imageset.width * imageset.height * 3); + + + RelightThreadPool pool; + PixelArray line; + + pool.start(QThread::idealThreadCount()); + + for (int i = 0; i < imageset.height; i++) { + // Read a line + imageset.readLine(line); + + // Create the normal task and get the run lambda + uint32_t idx = i * 3 * imageset.width; + //uint8_t* data = normals.data() + idx; + float* data = &normals[idx]; + + NormalsWorker *task = new NormalsWorker(solver, i, line, data, imageset, lens); + + std::function run = [this, task](void)->void { + task->run(); + delete task; + }; + + // Launch the task + pool.queue(run); + pool.waitForSpace(); + + bool proceed = progressed("Computing normals...", ((float)i / imageset.height) * 100); + if(!proceed) + return; + } + + // Wait for the end of all the threads + pool.finish(); + + if(flatMethod != NONE) { + //TODO: do we really need double precision? + std::vector normalsd(normals.size()); + for(uint32_t i = 0; i < normals.size(); i++) + normalsd[i] = (double)normals[i]; + + NormalsImage ni; + ni.load(normalsd, imageset.width, imageset.height); + switch(flatMethod) { + case NONE: break; + case RADIAL: + ni.flattenRadial(); + break; + case FOURIER: + //convert radius to frequencies + double sigma = 100/m_FlatRadius; + ni.flattenFourier(imageset.width/10, sigma); + break; + } + normalsd = ni.normals; + for(uint32_t i = 0; i < normals.size(); i++) + normals[i] = (float)ni.normals[i]; + + } + + + std::vector normalmap(imageset.width * imageset.height * 3); + for(size_t i = 0; i < normals.size(); i++) + normalmap[i] = floor(((normals[i] + 1.0f) / 2.0f) * 255); + + // Save the final result + QImage img(normalmap.data(), imageset.width, imageset.height, imageset.width*3, QImage::Format_RGB888); + + // Set spatial resolution if known. Need to convert as pixelSize stored in mm/pixel whereas QImage requires pixels/m + if( pixelSize > 0 ) { + int dotsPerMeter = round(1000.0/pixelSize); + img.setDotsPerMeterX(dotsPerMeter); + img.setDotsPerMeterY(dotsPerMeter); + } + img.save(output); + std::function callback = [this](QString s, int n)->bool { return this->progressed(s, n); }; + + if(exportPly) { + bool proceed = progressed("Integrating normals...", 0); + if(!proceed) + return; + std::vector z; + bni_integrate(callback, imageset.width, imageset.height, normals, z, bni_k); + if(z.size() == 0) { + error = "Failed to integrate normals"; + status = FAILED; + return; + } + //TODO remove extension properly + QString filename = output.left(output.size() -4) + ".ply"; + + progressed("Saving surface...", 99); + savePly(filename, imageset.width, imageset.height, z); + } + progressed("Done", 100); +} + + +void NormalsWorker::run() { + switch (solver) + { + // L2 solver + case NORMALS_L2: + solveL2(); + break; + // SBL solver + case NORMALS_SBL: + solveSBL(); + break; + // RPCA solver + case NORMALS_RPCA: + solveRPCA(); + break; + } + +} + + +void NormalsWorker::solveL2() +{ + std::vector &m_Lights = m_Imageset.lights; + std::vector &m_Lights3d = m_Imageset.lights3d; + // Pixel data + Eigen::MatrixXd mLights(m_Lights.size(), 3); + Eigen::MatrixXd mPixel(m_Lights.size(), 1); + Eigen::MatrixXd mNormals; + + unsigned int normalIdx = 0; + + + // Fill the lights matrix + for (size_t i = 0; i < m_Lights.size(); i++) + for (int j = 0; j < 3; j++) + mLights(i, j) = m_Lights[i][j]; + + // For each pixel in the line solve the system + //TODO do it in a single large matrix, it should be faster. + for (size_t p = 0; p < m_Row.size(); p++) { + // Fill the pixel vector + for (size_t m = 0; m < m_Lights.size(); m++) + mPixel(m, 0) = m_Row[p][m].mean(); + + if(m_Imageset.light3d) { + for(size_t i = 0; i < m_Lights3d.size(); i++) { + Vector3f light = m_Imageset.relativeLight(m_Lights3d[i], p, m_Imageset.height - row); + light.normalize(); + for (int j = 0; j < 3; j++) + mLights(i, j) = light[j]; + } + } + + mNormals = (mLights.transpose() * mLights).ldlt().solve(mLights.transpose() * mPixel); + mNormals.col(0).normalize(); + assert(!isnan(mNormals.col(0)[0])); + assert(!isnan(mNormals.col(0)[1])); + assert(!isnan(mNormals.col(0)[2])); + m_Normals[normalIdx+0] = float(mNormals.col(0)[0]); + m_Normals[normalIdx+1] = float(mNormals.col(0)[1]); + m_Normals[normalIdx+2] = float(mNormals.col(0)[2]); + + normalIdx += 3; + } +} +void NormalsWorker::solveSBL() +{ +} + +void NormalsWorker::solveRPCA() +{ +} + diff --git a/relightlab/normalstask.h b/relightlab/normalstask.h new file mode 100644 index 00000000..0b852c12 --- /dev/null +++ b/relightlab/normalstask.h @@ -0,0 +1,72 @@ +#ifndef NORMALSTASK_H +#define NORMALSTASK_H + +#include +#include +#include +#include "../src/relight_vector.h" +#include "task.h" +#include "../src/project.h" +#include "../src/imageset.h" +#include + +enum NormalSolver { NORMALS_L2, NORMALS_SBL, NORMALS_RPCA }; +enum FlatMethod { NONE, RADIAL, FOURIER }; + + +class NormalsTask : public Task { +public: + NormalSolver solver = NORMALS_L2; + FlatMethod flatMethod = NONE; + double m_FlatRadius = 0.5; + + bool exportJpeg = true; + int quality = 95; + bool exportPng = false; + bool exportTiff = false; + + bool exportPly = false; + bool bni_k = 2.0; + ImageSet imageset; + Lens lens; + float pixelSize = 0.0f; + + NormalsTask(const QString &outputPath) { + output = outputPath; + } + + virtual ~NormalsTask(){}; + void initFromProject(Project &project); + virtual void run() override; +}; + +class NormalsWorker +{ +public: + NormalsWorker(NormalSolver _solver, int _row, const PixelArray& toProcess, float* normals, ImageSet &imageset, Lens &_lens) : + solver(_solver), row(_row), m_Row(toProcess), m_Normals(normals), m_Imageset(imageset), lens(_lens){ + m_Row.resize(toProcess.npixels(), toProcess.nlights); + for(size_t i = 0; i < m_Row.size(); i++) + m_Row[i] = toProcess[i]; + } + + void run(); +private: + void solveL2(); + void solveSBL(); + void solveRPCA(); +private: + + NormalSolver solver; + int row; + PixelArray m_Row; + + float* m_Normals; + ImageSet &m_Imageset; + Lens &lens; + QMutex m_Mutex; +}; + + + +#endif // NORMALSTASK_H diff --git a/relightlab/preferences.cpp b/relightlab/preferences.cpp new file mode 100644 index 00000000..dd284a27 --- /dev/null +++ b/relightlab/preferences.cpp @@ -0,0 +1,43 @@ +#include "preferences.h" +#include "tabwidget.h" +#include "relightapp.h" + +#include +#include +#include + +Preferences::Preferences(QWidget *parent): QDialog(parent) { + setWindowTitle("Preferences - RelightLab"); + setModal(true); + + tabs = new TabWidget; + + QWidget *appearance = buildAppearance(); + tabs->addTab(appearance, "Appearance"); + + QWidget *performances = new QWidget; + tabs->addTab(performances, "Performances"); + + QWidget *casting = new QWidget; + tabs->addTab(casting, "Casting"); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(tabs); + + QPushButton *closeButton = new QPushButton("Close", this); + layout->addWidget(closeButton); + + connect(closeButton, &QPushButton::clicked, this, &QDialog::accept); +} + +QWidget *Preferences::buildAppearance() { + QWidget *widget = new QWidget; + QVBoxLayout *content = new QVBoxLayout; + QCheckBox *dark = new QCheckBox("Dark theme (requires restart)"); + dark->setChecked(QSettings().value("dark", true).toBool()); + content->addWidget(dark); + widget->setLayout(content); + + connect(dark, SIGNAL(toggled(bool)), qRelightApp, SLOT(setDarkTheme(bool))); + return widget; +} diff --git a/relightlab/preferences.h b/relightlab/preferences.h new file mode 100644 index 00000000..d6b28437 --- /dev/null +++ b/relightlab/preferences.h @@ -0,0 +1,17 @@ +#ifndef PREFERENCES_H +#define PREFERENCES_H + +#include + +class TabWidget; + +class Preferences: public QDialog { +public: + Preferences(QWidget *parent = nullptr); + +private: + TabWidget *tabs; + QWidget *buildAppearance(); +}; + +#endif // PREFERENCES_H diff --git a/relightlab/processqueue.cpp b/relightlab/processqueue.cpp new file mode 100644 index 00000000..f3afbcb8 --- /dev/null +++ b/relightlab/processqueue.cpp @@ -0,0 +1,168 @@ +#include "processqueue.h" +#include "relightapp.h" + +#include +#include +#include + +#include +using namespace std; + + +ProcessQueue::~ProcessQueue() { + if(!isRunning()) + return; + { + QMutexLocker locker(&lock); + stopped = true; + } + + wait(); +} + +void ProcessQueue::run() { + while(1) { + lock.lock(); + if(stopped) { + lock.unlock(); + break; + } + + + if(!task) { + if(queue.size() == 0) { + lock.unlock(); + sleep(1); + } else { + startNewProcess(); + lock.unlock(); + } + continue; + } + lock.unlock(); + + task->wait(100); + + if(task->isFinished()) { + QString msg = task->status == Task::DONE ? "Successfully finished" : task->error; + msg = task->output + "\n" + msg; + emit finished(task->label, msg); + + past.push_back(task); + task = nullptr; + emit update(); + } + } + +} + + +/* mutexed! */ +void ProcessQueue::startNewProcess() { + task = queue.front(); + queue.pop_front(); + + task->start(); + task->status = Task::RUNNING; + emit update(); +} + +bool ProcessQueue::hasTasks() { + QMutexLocker locker(&lock); + if(task) return true; + return queue.size() > 0; +} + +void ProcessQueue::addTask(Task *a, bool paused) { + if(paused) + a->pause(); + a->id = newId(); + QMutexLocker locker(&lock); + queue.push_back(a); + emit update(); +} + +void ProcessQueue::removeTask(Task *a) { + QMutexLocker locker(&lock); + int index = queue.indexOf(a); + if(index < 0) + return; + + Task *task = queue.takeAt(index); + emit update(); +} + +void ProcessQueue::removeTask(int id) { + QMutexLocker locker(&lock); + int index = indexOf(id); + if(index < 0) + return; + + Task *task = queue.takeAt(index); + //processqueue is never the owner! + //delete task; + emit update(); +} + +void ProcessQueue::pushFront(int id) { + QMutexLocker locker(&lock); + int index = indexOf(id); + if(index < 0) + return; + + Task *p = queue.takeAt(index); + queue.push_front(p); + emit update(); +} + +void ProcessQueue::pushBack(int id) { + QMutexLocker locker(&lock); + int index = indexOf(id); + if(index < 0) + return; + + Task *p = queue.takeAt(index); + queue.push_back(p); + emit update(); +} + +void ProcessQueue::clear() { + QMutexLocker locker(&lock); + queue.clear(); + emit update(); +} + +void ProcessQueue::start() { + QMutexLocker locker(&lock); + stopped = false; + if(task) + task->resume(); + if(!isRunning()) + QThread::start(); + emit update(); +} + +void ProcessQueue::pause() { + QMutexLocker locker(&lock); + stopped = true; + if(task) + task->pause(); + emit update(); +} + +void ProcessQueue::stop() { + QMutexLocker locker(&lock); + stopped = true; + if(task) + task->stop(); + emit update(); +} + +int ProcessQueue::indexOf(int id) { + for(int i = 0; i < queue.size(); i++) { + int pid = queue[i]->id; + if(pid == id) + return i; + } + return -1; +} diff --git a/relightlab/processqueue.h b/relightlab/processqueue.h new file mode 100644 index 00000000..21290181 --- /dev/null +++ b/relightlab/processqueue.h @@ -0,0 +1,64 @@ +#ifndef PROCESSQUEUE_H +#define PROCESSQUEUE_H + + +#include +#include +#include +#include +#include +#include + +#include "task.h" + +#include +using namespace std; + + +class ProcessQueue: public QThread { + Q_OBJECT +public: + ~ProcessQueue(); + int nthreads = 4; + + Task *task = nullptr; //task is removed from the queue when executing. + bool stopped = false; + + QList queue; + QList past; + QMutex lock; + + static ProcessQueue &instance() { + static ProcessQueue single; + return single; + } + + bool hasTasks(); + void addTask(Task *a, bool paused = false); + void removeTask(Task *a); + void removeTask(int id); + void pushFront(int id); + void pushBack(int id); + void clear(); + + void start(); //start the queue if not start + void pause(); //pause current process. + void stop(); //stop cuirrent process and pause the queue. + +public slots: + +signals: + void finished(QString title, QString msg); + void update(); //a task was added, or started, or finished. + +protected: + void run(); + void startNewProcess(); + int indexOf(int id); + int newId() { + static int id = 0; + return id++; + } +}; + +#endif // PROCESSQUEUE_H diff --git a/relightlab/qlabelbutton.cpp b/relightlab/qlabelbutton.cpp new file mode 100644 index 00000000..aecc9b55 --- /dev/null +++ b/relightlab/qlabelbutton.cpp @@ -0,0 +1,2 @@ +#include "qlabelbutton.h" + diff --git a/relightlab/qlabelbutton.h b/relightlab/qlabelbutton.h new file mode 100644 index 00000000..68779026 --- /dev/null +++ b/relightlab/qlabelbutton.h @@ -0,0 +1,17 @@ +#ifndef QLABELBUTTON_H +#define QLABELBUTTON_H + +#include +#include +#include + +class QLabelButton: public QCommandLinkButton { +public: + QLabelButton(QString text, QString description = "", QWidget *parent = nullptr): QCommandLinkButton(text, description, parent) { + setCheckable(true); + setIcon(QIcon()); + setMinimumWidth(200); + } +}; + +#endif // QLABELBUTTON_H diff --git a/relightlab/queueframe.cpp b/relightlab/queueframe.cpp new file mode 100644 index 00000000..4e0121e1 --- /dev/null +++ b/relightlab/queueframe.cpp @@ -0,0 +1,192 @@ +#include "queueframe.h" + +#include "processqueue.h" +#include "queueitem.h" +#include "relightapp.h" + + +#include +#include +#include +#include +#include + +#include +using namespace std; + + + +QueueFrame::QueueFrame(QWidget *parent): QFrame(parent) { + + QVBoxLayout *vbox = new QVBoxLayout(this); + + + toolbar = new QToolBar; + vbox->addWidget(toolbar); + + toolbar->addAction(actionStart = qRelightApp->addAction("queue_start", "Start", "play", "")); + toolbar->addAction(actionPause = qRelightApp->addAction("queue_pause", "Pause", "pause", "")); + toolbar->addAction(actionStop = qRelightApp->addAction("queue_stop", "Stop", "stop", "")); + toolbar->addAction(actionToBottom = qRelightApp->addAction("queue_bottom", "Send to bottom", "chevrons-down", "")); + toolbar->addAction(actionToTop = qRelightApp->addAction("queue_top", "Send to top", "chevrons-up", "")); + toolbar->addAction(actionRemove = qRelightApp->addAction("queue_remove", "Remove", "trash-2", "")); + toolbar->addAction(actionOpenFolder = qRelightApp->addAction("queue_open", "Open folder", "folder", "")); + toolbar->addAction(actionInfo = qRelightApp->addAction("queue_info", "Info", "info", "")); + + + + list = new QListWidget; + vbox->addWidget(list); + + list->setSelectionMode(QAbstractItemView::ExtendedSelection); + connect(list->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + setToolsStatus(); + + connect(actionStart, SIGNAL(triggered(bool)), this, SLOT(start())); + connect(actionPause, SIGNAL(triggered(bool)), this, SLOT(pause())); + connect(actionStop, SIGNAL(triggered(bool)), this, SLOT(stop())); + connect(actionToBottom, SIGNAL(triggered(bool)), this, SLOT(sendToTop())); + connect(actionToTop, SIGNAL(triggered(bool)), this, SLOT(sendToBottom())); + connect(actionRemove, SIGNAL(triggered(bool)), this, SLOT(remove())); + + ProcessQueue &queue = ProcessQueue::instance(); + connect(&queue, SIGNAL(update()), this, SLOT(update())); + + update(); +} + + +void QueueFrame::setToolsStatus() { + ProcessQueue &queue = ProcessQueue::instance(); + actionStart->setEnabled(queue.stopped == true); + actionPause->setEnabled(queue.stopped == false); + actionStop->setEnabled(queue.stopped == false || queue.task != nullptr); + + + bool empty_selection = list->selectedItems().size() == 0; + actionToBottom->setDisabled(empty_selection); + actionToTop->setDisabled(empty_selection); + actionRemove->setDisabled(empty_selection); + actionInfo->setDisabled(empty_selection); + //actionOpenFolder->setDisabled(empty_selection); +} + + +void QueueFrame::start() { + ProcessQueue &queue = ProcessQueue::instance(); + queue.start(); + setToolsStatus(); + +} +void QueueFrame::pause() { + ProcessQueue &queue = ProcessQueue::instance(); + queue.pause(); + setToolsStatus(); + +} +void QueueFrame::stop() { + ProcessQueue &queue = ProcessQueue::instance(); + queue.stop(); + setToolsStatus(); +} + +void QueueFrame::sendToTop() { + +} + +void QueueFrame::sendToBottom() { + +} + +void QueueFrame::remove() { + QStringList tasks; + for(QModelIndex index: list->selectionModel()->selectedRows()) { + QueueItem *item = (QueueItem *)list->item(index.row()); + QString label = item->task->label; + if(item->task->status == Task::PAUSED) + label += " (PAUSED)"; + if(item->task->status == Task::RUNNING) + label += " (RUNNING)"; + tasks.push_back(item->task->label); + } + int answer = QMessageBox::question(this, "Removing tasks", + QString("Are you sure you want to remove this task:?\n") + tasks.join("\n"), + QMessageBox::Cancel, QMessageBox::Ok); + if(answer == QMessageBox::No) + return; + ProcessQueue &queue = ProcessQueue::instance(); + + QModelIndexList selection = list->selectionModel()->selection().indexes(); + std::sort(selection.begin(), selection.end(), + [](const QModelIndex &a, const QModelIndex &b) -> bool { return a.row() < b.row(); }); + + while(!selection.isEmpty()) { + QModelIndex i = selection.takeLast(); + QueueItem *item = (QueueItem *)list->item(i.row()); + if(item->task->status == Task::PAUSED ||item->task->status == Task::RUNNING) { + queue.stop(); + } + queue.removeTask(item->id); + list->takeItem(i.row()); + delete item; + } +} + +void QueueFrame::removeTask(Task *task) { + ProcessQueue &queue = ProcessQueue::instance(); + queue.removeTask(task->id); + for(int i = 0; i < list->count(); i++) { + QueueItem *item = dynamic_cast(list->item(i)); + if(item->id == task->id) { + list->removeItemWidget(item); + //should remove task from queue? + } + } +} + +void QueueFrame::selectionChanged(const QItemSelection & selected, const QItemSelection & deselected) { + for(QModelIndex index: deselected.indexes()) { + int row = index.row(); + QueueItem *item = (QueueItem *)list->item(row); + item->setSelected(false); + } + for(QModelIndex index: selected.indexes()) { + int row = index.row(); + QueueItem *item = (QueueItem *)list->item(row); + item->setSelected(true); + } + setToolsStatus(); +} + + +void QueueFrame::update() { + QSet tasks; + //check status of each widget + for(int i = 0; i < list->count(); i++) { + QueueItem *item = (QueueItem *)list->item(i); + item->update(); + tasks.insert(item->id); + } + ProcessQueue &queue = ProcessQueue::instance(); + + //add all task not already present. + for(Task *task: queue.queue) { + if(!tasks.contains(task->id) && task->visible) { + QueueItem *item = new QueueItem(task, list); + list->addItem(item); + } + } + if(queue.task && !tasks.contains(queue.task->id) && queue.task->visible) { + QueueItem *item = new QueueItem(queue.task, list); + list->addItem(item); + } +/* task might have been deleted already. + for(Task *task: queue.past) { + if(!tasks.contains(task->id) && task->visible) { + QueueItem *item = new QueueItem(task, list); + list->addItem(item); + } + } */ + +} diff --git a/relightlab/queueframe.h b/relightlab/queueframe.h new file mode 100644 index 00000000..9655ed0d --- /dev/null +++ b/relightlab/queueframe.h @@ -0,0 +1,50 @@ +#ifndef QUEUEFRAME_H +#define QUEUEFRAME_H + +#include + +class QItemSelection; +class QGridLayout; +class QListWidget; +class QAction; +class QToolBar; +class Task; + +class QueueFrame: public QFrame { + Q_OBJECT +public: + QueueFrame(QWidget *parent = nullptr); + + void setToolsStatus(); + void removeTask(Task *task); + +public slots: + void selectionChanged(const QItemSelection & selected, const QItemSelection & deselected); + void update(); + + void start(); + void pause(); + void stop(); + + void sendToTop(); + void sendToBottom(); + void remove(); + +private: + QToolBar *toolbar; + QAction *actionStart; + QAction *actionPause; + QAction *actionStop; + QAction *actionToTop; + QAction *actionToBottom; + QAction *actionRemove; + QAction *actionOpenFolder; + QAction *actionInfo; + + QWidget *centralwidget; + QGridLayout *gridLayout; + QListWidget *list; + +}; + +#endif // QUEUEFRAME_H diff --git a/relightlab/queueitem.cpp b/relightlab/queueitem.cpp new file mode 100644 index 00000000..78535e6c --- /dev/null +++ b/relightlab/queueitem.cpp @@ -0,0 +1,139 @@ +#include "queueitem.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "task.h" +#include "../relight/httpserver.h" + + +#include +using namespace std; + +QueueItem::QueueItem(Task *_task, QListWidget *parent): QListWidgetItem(parent) { + style[Task::ON_QUEUE] = "background-color:#323232; color:#b1b1b1"; + style[Task::RUNNING] = "background-color:#328232; color:#b1b1b1"; + style[Task::PAUSED] = "background-color:#323232; color:#b1b1b1"; + style[Task::STOPPED] = "background-color:#823232; color:#b1b1b1"; + style[Task::DONE] = "background-color:#323282; color:#b1b1b1"; + style[Task::FAILED] = "background-color:#823232; color:#b1b1b1"; + + task = _task; + connect(task, SIGNAL(progress(QString,int)), this, SLOT(progress(QString,int))); + + id = task->id; + widget = new QWidget; + widget->setObjectName("task"); + + QGridLayout *grid = new QGridLayout(); + + QLabel *label = new QLabel(task->label); + grid->addWidget(label, 0, 0, 1, 2); + + QFont font = label->font(); + font.setPointSize(10); + + QLabel *input = new QLabel(task->input_folder); + input->setFont(font); + grid->addWidget(input, 1, 0, 1, 2); + + QLabel *output = new QLabel(task->output); + output->setFont(font); + grid->addWidget(output, 2, 0, 1, 2); + + status = new QLabel(); + status->setMinimumWidth(250); +// status->hide(); + grid->addWidget(status, 3, 0, 1, 1); + + progressbar = new QProgressBar(); + progressbar->setValue(0); +// progressbar->hide(); + grid->addWidget(progressbar, 3, 1, 1, 1); + + + cast = new QPushButton(); + cast->setIcon(QIcon::fromTheme("cast")); + cast->setEnabled(false); + grid->addWidget(cast, 0, 2, 4, 1); + + connect(cast, SIGNAL(clicked(bool)), this, SLOT(casting())); + + folder = new QPushButton(); + folder->setIcon(QIcon::fromTheme("folder")); + folder->setEnabled(true); + grid->addWidget(folder, 0, 3, 4, 1); + + connect(folder, SIGNAL(clicked(bool)), this, SLOT(openFolder())); + + widget->setLayout(grid); + + setSizeHint(widget->minimumSizeHint()); + parent->setItemWidget(this, widget); + + update(); +} + +void QueueItem::progress(QString text, int percent) { + status->setText(text); + progressbar->setValue(percent); +} + +void QueueItem::update() { + switch(task->status) { + case Task::PAUSED: + status->setText("Paused"); + case Task::ON_QUEUE: + case Task::RUNNING: + break; + case Task::DONE: + status->setText("Done"); + progressbar->setValue(100); + { + QFileInfo info(task->output); + if(info.isDir()) + cast->setEnabled(true); + } + cast->setEnabled(true); + break; + case Task::STOPPED: + status->setText("Stopped"); + break; + case Task::FAILED: + status->setText(task->error); + progressbar->setValue(0); + } + widget->setStyleSheet(style[task->status]); +} + +void QueueItem::setSelected(bool selected) { + if(selected) + widget->setStyleSheet("background-color:#ffa02f; color: #ffffff;"); + else + widget->setStyleSheet(style[task->status]); +} + +void QueueItem::casting() { + try { + HttpServer &server = HttpServer::instance(); + server.stop(); + server.port = 8880; + server.start(task->output); + server.show(); + } catch(QString error) { + QMessageBox::critical(nullptr, "Could not cast!", error); + } +} + +void QueueItem::openFolder() { + QFileInfo fileInfo(task->output); + QString path = fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.absolutePath(); + + QUrl folderUrl = QUrl::fromLocalFile(path); + QDesktopServices::openUrl(folderUrl); +} diff --git a/relightlab/queueitem.h b/relightlab/queueitem.h new file mode 100644 index 00000000..4aa92580 --- /dev/null +++ b/relightlab/queueitem.h @@ -0,0 +1,35 @@ +#ifndef QUEUEITEM_H +#define QUEUEITEM_H + +#include + +class QLabel; +class QPushButton; +class QProgressBar; +class Task; + +class QueueItem: public QObject, public QListWidgetItem { + Q_OBJECT +public: + int id; + Task *task; + + QWidget *widget = nullptr; + QLabel *status = nullptr; + QPushButton *cast = nullptr; + QPushButton *folder = nullptr; + QProgressBar *progressbar = nullptr; + + QueueItem(Task *task, QListWidget *parent); + void update(); + +public slots: + void setSelected(bool selected); + void progress(QString text, int percent); + void casting(); + void openFolder(); + +private: + QMap style; +}; +#endif // QUEUEITEM_H diff --git a/relightlab/recentprojects.cpp b/relightlab/recentprojects.cpp new file mode 100644 index 00000000..90037ca2 --- /dev/null +++ b/relightlab/recentprojects.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include +#include "recentprojects.h" + +QStringList recentProjects() { + return QSettings().value("recent-projects", QStringList()).toStringList(); +} + +void addRecentProject(const QString &filename) { + QStringList recents = recentProjects(); + int index = recents.indexOf(filename); + if (index != -1) { // String found in the list + recents.removeAt(index); // Remove the string from its current position + } + recents.prepend(filename); // Add the string to the front + QSettings().setValue("recent-projects", recents); +} + +void clearRecentProjects() { + QSettings().setValue("recent-projects", QStringList()); +} + + diff --git a/relightlab/recentprojects.h b/relightlab/recentprojects.h new file mode 100644 index 00000000..0a04b27b --- /dev/null +++ b/relightlab/recentprojects.h @@ -0,0 +1,10 @@ +#ifndef RECENTPROJECTS_H +#define RECENTPROJECTS_H + +#include + +QStringList recentProjects(); +void addRecentProject(const QString &filename); +void clearRecentProjects(); + +#endif // RECENTPROJECTS_H diff --git a/relightlab/reflectionview.cpp b/relightlab/reflectionview.cpp new file mode 100644 index 00000000..9fb9ae71 --- /dev/null +++ b/relightlab/reflectionview.cpp @@ -0,0 +1,114 @@ +#include "reflectionview.h" +#include "relightapp.h" +#include "../src/sphere.h" + +#include +#include +#include +#include + +PositionView::PositionView(Sphere *_sphere, int _height, QWidget *parent): QGraphicsView(parent) { + sphere = _sphere; + height = _height; + setScene(&scene); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + QPixmap pix = QPixmap::fromImage(qRelightApp->thumbnails()[0]); + img_item = scene.addPixmap(pix); + + setFixedSize(pix.width()*height/pix.height(), height); + + update(); +} + +void PositionView::resizeEvent(QResizeEvent */*event*/) { + fitInView(scene.sceneRect()); //img_item); +} + +void PositionView::update() { +// scene.clear(); + if(ellipse) + scene.removeItem(ellipse); + + QSizeF size = img_item->boundingRect().size(); + + double scale = size.width()/(double)qRelightApp->project().imgsize.width(); + + double radius = sphere->radius*scale; + QPointF scaled_center = sphere->center*scale; + ellipse = scene.addEllipse(QRectF(scaled_center - QPointF(radius, radius), QSize(2*radius, 2*radius)), Qt::NoPen, Qt::green); +} + + +ReflectionView::ReflectionView(Sphere *_sphere, int _height, QWidget *parent ): QGraphicsView(parent) { + sphere = _sphere; + height = _height; + setScene(&scene); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + verticalScrollBar()->blockSignals(true); + horizontalScrollBar()->blockSignals(true); + + init(); + update(); +} + +ReflectionView::~ReflectionView() { + for(auto l: lights) { + scene.removeItem(l); + delete l; + } + lights.clear(); +} + +void ReflectionView::init() { + scene.clear(); + lights.clear(); + + QPixmap pix = QPixmap::fromImage(sphere->sphereImg);//.scaledToHeight(height); + img_item = scene.addPixmap(pix); + setFixedSize(pix.width()*height/pix.height(), height); + //setFixedSize(pix.width(), pix.height()); + + QPointF c = sphere->center - sphere->inner.topLeft(); + double w = sphere->eWidth; + double h = sphere->eHeight; + + w *= sphere->smallradius/sphere->radius; + h *= sphere->smallradius/sphere->radius; + area = scene.addEllipse(c.x() - w, c.y() - h, 2*w, 2*h, QPen(Qt::yellow)); + area->setTransformOriginPoint(c); + area->setRotation(sphere->eAngle); +} + +void ReflectionView::update() { + for(auto l: lights) { + scene.removeItem(l); + delete l; + } + lights.clear(); + + QPixmap pix = QPixmap::fromImage(sphere->sphereImg); + double scale = transform().m11(); + img_item->setPixmap(pix); + + double side = lightRadius/scale; + for(QPointF p: sphere->lights) { + if(p.isNull()) + continue; + p.setX(p.x() - sphere->inner.left()); + p.setY(p.y() - sphere->inner.top()); + + auto ellipse = scene.addEllipse(p.x()-side, p.y() - side, 2*side, 2*side, Qt::NoPen, Qt::green); + lights.push_back(ellipse); + } +} + +void ReflectionView::resizeEvent(QResizeEvent *event) { + fitInView(img_item->boundingRect()); //.sceneRect()); //img_item); +} + + diff --git a/relightlab/reflectionview.h b/relightlab/reflectionview.h new file mode 100644 index 00000000..f252010f --- /dev/null +++ b/relightlab/reflectionview.h @@ -0,0 +1,53 @@ +#ifndef REFLECTIONVIEW_H +#define REFLECTIONVIEW_H + + +#include +#include +#include + +class Sphere; +class QGraphicsEllipseItem; + +class PositionView: public QGraphicsView { + Q_OBJECT +public: + PositionView(Sphere *sphere, int height, QWidget *parent = nullptr); + void update(); +protected: + void resizeEvent(QResizeEvent *event); +private: + int height; + Sphere *sphere; + QGraphicsScene scene; + QGraphicsPixmapItem *img_item = nullptr; + QGraphicsEllipseItem *ellipse = nullptr; +}; + +class ReflectionView: public QGraphicsView { + Q_OBJECT +public: + double lightRadius = 2.0; + + ReflectionView(Sphere *sphere, int height, QWidget *parent = nullptr); + ~ReflectionView(); + void init(); //call this when the sphere changes position + void update(); //this when detecting highlights. + +//public slots: + +protected: + void resizeEvent(QResizeEvent *event); + +private: + int height; + Sphere *sphere; + QGraphicsScene scene; + QGraphicsPixmapItem *img_item = nullptr; + QGraphicsEllipseItem *area = nullptr; + std::vector lights; + +}; + + +#endif // REFLECTIONVIEW_H diff --git a/relightlab/relightapp.cpp b/relightlab/relightapp.cpp new file mode 100644 index 00000000..19a8338c --- /dev/null +++ b/relightlab/relightapp.cpp @@ -0,0 +1,423 @@ +#include "relightapp.h" +#include "processqueue.h" +#include "imageframe.h" +#include "recentprojects.h" +#include "mainwindow.h" +#include "preferences.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +using namespace std; + + + +QIcon ProxyStyle::standardIcon(StandardPixmap standardIcon, + const QStyleOption *option, + const QWidget *widget) const { + switch(standardIcon) { + case QStyle::SP_DialogOkButton: return QIcon::fromTheme("check"); + case QStyle::SP_DialogCancelButton: return QIcon::fromTheme("cancel"); + case QStyle::SP_DialogHelpButton: return QIcon::fromTheme("help-circle"); + case QStyle::SP_DialogSaveButton: return QIcon::fromTheme("save"); + default: return QProxyStyle::standardIcon(standardIcon, option, widget); + } +}; + + +RelightApp::RelightApp(int &argc, char **argv): QApplication(argc, argv) { + QTemporaryDir tmp; + if(!tmp.isValid()) { + QMessageBox::critical(nullptr, "Temporary folder is needed", "Could not create a temporary file for the scripts.\nSelect a folder in File->Preferences"); + } + + + + QFile style(":/css/style.qss"); + style.open(QFile::ReadOnly); + setStyleSheet(style.readAll()); + //Default font size can be read using QApplication::font().pointSize(), not pointPixel + + dark_palette.setColor(QPalette::Window,QColor(53,53,53)); + dark_palette.setColor(QPalette::WindowText,Qt::white); + dark_palette.setColor(QPalette::Disabled,QPalette::WindowText,QColor(127,127,127)); + dark_palette.setColor(QPalette::Disabled,QPalette::Text,QColor(127,127,127)); + dark_palette.setColor(QPalette::Base,QColor(42,42,42)); + dark_palette.setColor(QPalette::AlternateBase,QColor(66,66,66)); + dark_palette.setColor(QPalette::Button,QColor(53,53,53)); + dark_palette.setColor(QPalette::ToolTipBase,QColor(53,53,53)); + dark_palette.setColor(QPalette::ToolTipText,Qt::white); + dark_palette.setColor(QPalette::Text,Qt::white); + dark_palette.setColor(QPalette::Dark,QColor(35,35,35)); + dark_palette.setColor(QPalette::Shadow,QColor(20,20,20)); + + dark_palette.setColor(QPalette::ButtonText,Qt::white); + dark_palette.setColor(QPalette::Disabled,QPalette::ButtonText,QColor(127,127,127)); + dark_palette.setColor(QPalette::BrightText,Qt::red); + dark_palette.setColor(QPalette::Link,QColor(42,130,218)); + dark_palette.setColor(QPalette::Highlight,QColor(42,130,218)); + dark_palette.setColor(QPalette::Disabled,QPalette::Highlight,QColor(80,80,80)); + dark_palette.setColor(QPalette::HighlightedText,Qt::white); + dark_palette.setColor(QPalette::Disabled,QPalette::HighlightedText,QColor(127,127,127)); + + this->setStyle(new ProxyStyle(QStyleFactory::create("Fusion"))); + + setAttribute(Qt::AA_DontShowIconsInMenus); + QIcon::setThemeSearchPaths(QStringList() << ":/icons"); + + + ProcessQueue &queue = ProcessQueue::instance(); + connect(&queue, SIGNAL(finished(QString title, QString msg)), this, SLOT(notify(QString title, QString msg))); + queue.start(); + + + + addAction("new_project", "New project..", "file", "Ctrl+N", SLOT(newProject())); + addAction("open_project", "Open project...", "folder", "Ctrl+O", SLOT(openProject())); + addAction("save_project", "Save project", "save", "Ctrl+S", SLOT(saveProject())); + addAction("save_project_as", "Save project as...", "", "Shift-Ctrl+S", SLOT(saveProjectAs())); + + addAction("preferences", "Preferences...", "", "Shift-Ctrl-P", SLOT(openPreferences())); + addAction("exit", "Exit", "", "Alt-F4", SLOT(close())); + + //imagesframe + addAction("zoom_fit", "Fit", "maximize", "="); + addAction("zoom_one", "Zoom 1x", "", "1"); + addAction("zoom_in", "Zoom in", "zoom-in", "+"); + addAction("zoom_out", "Zoom out", "zoom-out", "-"); + + addAction("previous_image", "Previous image", "chevron-left", "left"); + addAction("next_image", "Next image", "chevron-right", "right"); + + addAction("rotate_left", "Rotate left", "rotate-ccw", ""); + addAction("rotate_right", "Rotate right", "rotate-cw", ""); + + addAction("show_image", "Show image", "image", ""); + addAction("show_list", "Show list", "list", ""); + addAction("show_grid", "Show grid", "grid", ""); + + + if(QSystemTrayIcon::isSystemTrayAvailable()) { + QIcon icon(":/relight.png"); + systemTray = new QSystemTrayIcon(icon, this); + systemTray->show(); + systemTray->setVisible(false); + } + + +} +void RelightApp::notify(const QString &title, const QString &msg, int ms) { + if(!systemTray) + return; + QIcon icon(":/relight.png"); + systemTray->setVisible(true); + systemTray->showMessage(title, msg, icon, 5000); + systemTray->setVisible(false); + +} + +void RelightApp::run() { + //qDebug() << "Settings: " << QSettings().allKeys(); + bool dark = QSettings().value("dark", false).toBool(); + if(dark) { + QIcon::setThemeName("dark"); + setPalette(dark_palette); + } else { + QIcon::setThemeName("light"); + } + + + mainwindow = new MainWindow; + mainwindow->showMaximized(); +} + +void RelightApp::setDarkTheme(bool dark) { + QSettings().setValue("dark", dark); +} + +void RelightApp::setProject(const Project &_project) { + //cleanup interface and stop (and remove) project related tasks. + mainwindow->clear(); + + m_project = _project; + loadThumbnails(); + + mainwindow->init(); + mainwindow->setTabIndex(1); + qRelightApp->setLastProjectDir(m_project.dir.path()); +} + +void RelightApp::newProject() { + if(!needsSavingProceed()) + return; + + QString dir = QFileDialog::getExistingDirectory(mainwindow, "Choose picture folder", qRelightApp->lastProjectDir()); + if(dir.isNull()) return; + + + Project project; + project.setDir(QDir(dir)); + bool ok = project.scanDir(); + if(!project.size()) { + QMessageBox::critical(mainwindow, "Houston we have a problem!", "Could not find images in directory: " + project.dir.path()); + return; + } + + if(!ok) { + //check if we can rotate a few images. + bool canrotate = false; + for(Image &image: project.images) { + if(image.size == project.imgsize) + continue; + + if(image.isRotated(project.imgsize)) + canrotate = true; + } + if(canrotate) { + int answer = QMessageBox::question(mainwindow, "Some images are rotated.", "Do you wish to uniform image rotation?", QMessageBox::Yes, QMessageBox::No); + if(answer != QMessageBox::No) + project.rotateImages(); + } else + QMessageBox::critical(mainwindow, "Resolution problem", "Not all of the images in the folder have the same resolution,\nyou might need to fix this problem manually."); + } + + + qRelightApp->setProject(project); + + //Check for .lp files in the folder + QStringList img_ext; + img_ext << "*.lp"; + QStringList lps = QDir(dir).entryList(img_ext); + if(lps.size() > 0) { + int answer = QMessageBox::question(mainwindow, "Found an .lp file: " + lps[0], "Do you wish to load " + lps[0] + "?", QMessageBox::Yes, QMessageBox::No); + if(answer != QMessageBox::No) + m_project.loadLP(lps[0]); + } +} + +void RelightApp::openProject() { + if(!needsSavingProceed()) + return; + QString filename = QFileDialog::getOpenFileName(mainwindow, "Select a project", qRelightApp->lastProjectDir(), "*.relight"); + if(filename.isNull()) + return; + openProject(filename); +} + +void RelightApp::openProject(const QString &filename) { + Project project; + try { + project.load(filename); + } catch(QString e) { + QMessageBox::critical(mainwindow, "Could not load the project: " + filename, "Error: " + e); + return; + } + + QFileInfo info(filename); + QDir::setCurrent(info.canonicalPath()); + + while(project.missing.size() != 0) { + + QString msg = "Could not find this images:\n"; + for(int i: project.missing) + msg += "\t" + project.images[i].filename + "\n"; + + QMessageBox box(mainwindow); + box.setText(msg); + box.setWindowTitle("Missing images"); + box.addButton("Ignore missing images", QMessageBox::AcceptRole); + box.addButton("Select a different folder...", QMessageBox::ActionRole); + box.addButton("Cancel", QMessageBox::RejectRole); + int ret = box.exec(); + + switch(ret) { + case 1: { + QString imagefolder = QFileDialog::getExistingDirectory(mainwindow, "Could not find the images, please select the image folder:", project.dir.absolutePath()); + if(imagefolder.isNull()) { + QMessageBox::critical(mainwindow, "No folder selected", "No folder selected."); + return; + } + project.dir.setPath(imagefolder); + QDir::setCurrent(imagefolder); + project.checkMissingImages(); + project.checkImages(); + } + break; + case 2: //cancel + return; + case 3: //ignore + project.missing.clear(); + break; + } + } + + qRelightApp->setProject(project); + + project_filename = filename; //project.dir.relativeFilePath(filename); + addRecentProject(filename); + mainwindow->updateRecentProjectsMenu(); +} + +void RelightApp::saveProject() { + + if(project_filename.isNull()) { + QString filename = QFileDialog::getSaveFileName(mainwindow, "Save file: ", qRelightApp->lastProjectDir(), "*.relight"); + if(filename.isNull()) + return; + if(!filename.endsWith((".relight"))) + filename += ".relight"; + project_filename = filename; + } + + m_project.save(project_filename); + + QFileInfo info(project_filename); + mainwindow->setWindowTitle("Relight - " + info.fileName()); + addRecentProject(project_filename); + mainwindow->updateRecentProjectsMenu(); +} + +void RelightApp::saveProjectAs() { + QString filename = QFileDialog::getSaveFileName(mainwindow, "Save file: ", qRelightApp->lastProjectDir(), "*.relight"); + if(filename.isNull()) + return; + if(!filename.endsWith((".relight"))) + filename += ".relight"; + project_filename = filename; + + m_project.save(project_filename); + QFileInfo info(project_filename); + mainwindow->setWindowTitle("Relight - " + info.fileName()); + addRecentProject(project_filename); + mainwindow->updateRecentProjectsMenu(); +} + +void RelightApp::loadThumbnails() { + //if loading thumbails kill thumbnails + if(loader) { + loader->stop(); + loader->wait(); + delete loader; + loader = nullptr; + } + m_thumbnails.resize(m_project.images.size()); + QStringList paths; + for(size_t i = 0; i < m_project.images.size(); i++) { + Image &image = m_project.images[i]; + if(i == 0) { + QImage img(image.filename); + m_thumbnails[i] = img.scaledToHeight(256); + } else { + QImage img(m_thumbnails[0].size().scaled(2560, 256, Qt::KeepAspectRatio), QImage::Format_ARGB32); + img.fill(Qt::black); + m_thumbnails[i] = img; + paths.push_back(image.filename); + } + } + + //start a thumbnail loading thread and ask her to signal when something has been loaded. + loader = new ThumbailLoader(paths); + connect(loader, SIGNAL(update(int)), this, SIGNAL(updateThumbnail(int))); + QObject::connect(this, &QCoreApplication::aboutToQuit, [&]() { + if(loader) { + loader->stop(); + loader->wait(); + delete loader; + loader = nullptr; + } + }); + loader->start(); + +} + +void RelightApp::openPreferences() { + if(!preferences) + preferences = new Preferences(mainwindow); + preferences->show(); +} + +void RelightApp::close() { + if(!needsSavingProceed()) + return; + auto &q = ProcessQueue::instance(); + if(q.hasTasks()) { + auto answer = QMessageBox::critical(mainwindow, "Process queue", "There are some processes in the queue, do you want to terminate them?"); + if(answer != QMessageBox::Yes) + return; + } + q.stop(); + q.wait(); + exit(0); +} + + +bool RelightApp::needsSavingProceed() { + if(!m_project.needs_saving) + return true; + auto answer = QMessageBox::question(mainwindow, "Current project is unsaved", "Do you want to proceed without saving?"); + return answer == QMessageBox::Yes; +} + +QStringList RelightApp::domes() { + return QSettings().value("domes", QVariant(QStringList())).toStringList(); +} + +void RelightApp::addDome(QString filename) { + QStringList d = domes(); + if(d.contains(filename)) + return; + d.append(filename); + QSettings().setValue("domes", d); +} + +void RelightApp::removeDome(QString filename) { + QStringList d = domes(); + d.removeAll(filename); + QSettings().setValue("domes", d); +} + + +QAction *RelightApp::addAction(const QString &id, const QString &label, const QString &icon, const QString &shortcut, const char *method) { + QAction *a = new QAction(label); + a->setShortcut(shortcut); + if(icon != "") { + //a->setIcon(QIcon(icon)); + a->setIcon(QIcon::fromTheme(icon)); + } + a->setObjectName(id); + actions[id] = a; + if(method) + connect(a, SIGNAL(triggered()), this, method); + return a; +} + +ThumbailLoader::ThumbailLoader(QStringList &images) { + QDir current = QDir::current(); + for(QString filename: images) + paths.push_back(current.absoluteFilePath(filename)); +} + +void ThumbailLoader::run() { + int count = 1; + for(QString path: paths) { + if(stop_request) + break; + QImage img(path); + if(img.isNull()) //TODO shoudl actually warn! + break; + { + QMutexLocker lock(&qRelightApp->thumbails_lock); + qRelightApp->thumbnails()[count] = img.scaledToHeight(256);; + } + emit update(count); + count++; + } +} diff --git a/relightlab/relightapp.h b/relightlab/relightapp.h new file mode 100644 index 00000000..7f0f4f4a --- /dev/null +++ b/relightlab/relightapp.h @@ -0,0 +1,115 @@ +#ifndef RELIGHTAPP_H +#define RELIGHTAPP_H + +#include "../src/project.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +#define qRelightApp (static_cast(QCoreApplication::instance())) + +class Preferences; +class MainWindow; +class Task; +class QSystemTrayIcon; + +/* customize standard icons */ +class ProxyStyle : public QProxyStyle { + Q_OBJECT + +public: + ProxyStyle(QStyle *style = 0) : QProxyStyle(style) { } + +public slots: + QIcon standardIcon(StandardPixmap standardIcon, + const QStyleOption *option = 0, + const QWidget *widget = 0) const; +}; + +class ThumbailLoader: public QThread { + Q_OBJECT +public: + ThumbailLoader(QStringList &images); + void stop() { stop_request = true; } +signals: + void update(int); //something has been loaded. + +protected: + virtual void run(); + + QStringList paths; + bool stop_request = false; +}; + +class RelightApp: public QApplication { + Q_OBJECT +public: + Project m_project; + std::vector m_thumbnails; + + QMap actions; + MainWindow *mainwindow = nullptr; + Preferences *preferences = nullptr; + QSystemTrayIcon *systemTray = nullptr; + + RelightApp(int &argc, char **argv); + virtual ~RelightApp() {} + void run(); + +public slots: + void newProject(); + void openProject(); + void openProject(const QString &filename); + void saveProject(); + void saveProjectAs(); + void close(); + + void openPreferences(); + void setDarkTheme(bool on); + void notify(const QString &title, const QString &msg, int ms = 4000); +signals: + void updateThumbnail(int pos); + +public: + void setProject(const Project &project); + Project &project() { return m_project; } + + QMutex thumbails_lock; + std::vector &thumbnails() { return m_thumbnails; } + + QAction *addAction(const QString &id, const QString &label, const QString &icon, const QString &shortcut, const char *method = nullptr); + QAction *action(const QString &id) { return actions[id]; } + QString lastProjectDir() { + return QSettings().value("LastProjectDir", QDir::homePath()).toString(); + } + void setLastProjectDir(QString dir) { + QSettings().setValue("LastProjectDir", dir); + } + bool needsSavingProceed(); + + QStringList domes(); + void addDome(QString filename); + void removeDome(QString filename); + void loadThumbnails(); + +private: + + + //keep memory of current project filename for quick saving. + QString project_filename; + QPalette dark_palette; + ThumbailLoader *loader = nullptr; +}; + + + +#endif diff --git a/relightlab/relightlab.pro b/relightlab/relightlab.pro new file mode 100644 index 00000000..e87c401a --- /dev/null +++ b/relightlab/relightlab.pro @@ -0,0 +1,186 @@ +QT += widgets xml concurrent +CONFIG += c++17 + +#TODO: this might be needed in CMake +#find_package(Qt5Svg REQUIRED) +#target_link_libraries( ${APP_NAME} Qt5::Svg ) + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += _USE_MATH_DEFINES +DEFINES += NOMINMAX + +win32:INCLUDEPATH += ../external/libjpeg-turbo-2.0.6/include \ + ../external/eigen-3.3.9/ \ + ../src/ +win32:LIBS += ../external/libjpeg-turbo-2.0.6/lib/jpeg-static.lib + +unix:INCLUDEPATH += /usr/include/eigen3 +unix:LIBS += -ljpeg -ltiff -lgomp +unix:QMAKE_CXXFLAGS += -fopenmp + + +mac:INCLUDEPATH += /usr/local/Cellar/jpeg-turbo/2.0.6/include \ + /usr/local/include \ + /usr/local/include/eigen3 +mac:LIBS += -L/usr/local/Cellar/jpeg-turbo/2.0.6/lib/ -ljpeg +mac:LIBS += -framework Accelerate +mac:QMAKE_CXXFLAGS += -fopenmp +mac:QMAKE_CXXFLAGS += -Xpreprocessor -fopenmp -lomp -I/usr/local/include +mac:QMAKE_LFLAGS += -lomp +mac:LIBS += -L /usr/local/lib /usr/local/lib/libomp.dylib + + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +SOURCES += main.cpp \ + ../relight-cli/convert_rti.cpp \ + ../relight-cli/rtibuilder.cpp \ + ../src/flatnormals.cpp \ + processqueue.cpp \ + ../src/align.cpp \ + ../src/dome.cpp \ + ../src/exif.cpp \ + ../src/image.cpp \ + ../src/imageset.cpp \ + ../src/jpeg_decoder.cpp \ + ../src/jpeg_encoder.cpp \ + ../src/legacy_rti.cpp \ + ../src/lens.cpp \ + ../src/measure.cpp \ + ../src/project.cpp \ + ../src/rti.cpp \ + ../src/sphere.cpp \ + ../src/white.cpp \ + alignframe.cpp \ + alignpicking.cpp \ + alignrow.cpp \ + canvas.cpp \ + domepanel.cpp \ + imageview.cpp \ + lightgeometry.cpp \ + mainwindow.cpp \ + markerdialog.cpp \ + preferences.cpp \ + qlabelbutton.cpp \ + recentprojects.cpp \ + reflectionview.cpp \ + relightapp.cpp \ + rticard.cpp \ + rtiframe.cpp \ + rtiplan.cpp \ + rtirecents.cpp \ + rtirow.cpp \ + spherepicking.cpp \ + sphererow.cpp \ + tabwidget.cpp \ + imageframe.cpp \ + homeframe.cpp \ + imagelist.cpp \ + flowlayout.cpp \ + imagegrid.cpp \ + lightsframe.cpp \ + ../src/lp.cpp \ + directionsview.cpp \ + spherepanel.cpp \ + spheredialog.cpp \ + verifyview.cpp \ + verifydialog.cpp \ + helpbutton.cpp \ + cropframe.cpp \ + ../relight/imagecropper.cpp \ + creatertidialog.cpp \ + rtiexportdialog.cpp \ + rtitask.cpp \ + task.cpp \ + queueitem.cpp \ + queueframe.cpp \ + ../relight/httpserver.cpp \ + normalsframe.cpp \ + normalstask.cpp \ + ../src/bni_normal_integration.cpp \ + scaleframe.cpp + +RESOURCES += \ + res.qrc + + +HEADERS += \ + ../relight-cli/rtibuilder.h \ + ../src/flatnormals.h \ + processqueue.h \ + ../src/align.h \ + ../src/dome.h \ + ../src/exif.h \ + ../src/image.h \ + ../src/imageset.h \ + ../src/jpeg_decoder.h \ + ../src/jpeg_encoder.h \ + ../src/legacy_rti.h \ + ../src/lens.h \ + ../src/measure.h \ + ../src/project.h \ + ../src/rti.h \ + ../src/sphere.h \ + ../src/white.h \ + alignframe.h \ + alignpicking.h \ + alignrow.h \ + canvas.h \ + imageview.h \ + lightgeometry.h \ + mainwindow.h \ + mainwindow.h \ + markerdialog.h \ + preferences.h \ + qlabelbutton.h \ + recentprojects.h \ + reflectionview.h \ + relightapp.h \ + rticard.h \ + rtiframe.h \ + rtiplan.h \ + rtirecents.h \ + rtirow.h \ + spherepicking.h \ + sphererow.h \ + tabwidget.h \ + imageframe.h \ + homeframe.h \ + imagelist.h \ + flowlayout.h \ + imagegrid.h \ + lightsframe.h \ + ../src/lp.h \ + domepanel.h \ + directionsview.h \ + spherepanel.h \ + spheredialog.h \ + verifyview.h \ + verifydialog.h \ + helpbutton.h \ + cropframe.h \ + ../relight/imagecropper.h \ + creatertidialog.h \ + rtiexportdialog.h \ + rtitask.h \ + task.h \ + queueitem.h \ + queueframe.h \ + ../relight/httpserver.h \ + ../relight/httplib.h \ + normalsframe.h \ + normalstask.h \ + ../src/bni_normal_integration.h \ + scaleframe.h \ + ../src/flatnormals.h + +FORMS += + +DISTFILES += \ + roadmap.md \ + ../build_scripts/relight.png + + diff --git a/relightlab/res.qrc b/relightlab/res.qrc new file mode 100644 index 00000000..8295551f --- /dev/null +++ b/relightlab/res.qrc @@ -0,0 +1,89 @@ + + + css/style.qss + icons/dark/scalable/camera.svg + icons/dark/scalable/cast.svg + icons/dark/scalable/check.svg + icons/dark/scalable/chevron-left.svg + icons/dark/scalable/chevron-right.svg + icons/dark/scalable/crop.svg + icons/dark/scalable/download.svg + icons/dark/scalable/edit.svg + icons/dark/scalable/file.svg + icons/dark/scalable/folder.svg + icons/dark/scalable/highlight.svg + icons/dark/scalable/home.svg + icons/dark/scalable/image.svg + icons/dark/scalable/list.svg + icons/dark/scalable/maximize.svg + icons/dark/scalable/pause.svg + icons/dark/scalable/play.svg + icons/dark/scalable/ruler.svg + icons/dark/scalable/save.svg + icons/dark/scalable/settings.svg + icons/dark/scalable/trash-2.svg + icons/dark/scalable/zoom-in.svg + icons/dark/index.theme + icons/dark/scalable/grid.svg + icons/dark/scalable/help-circle.svg + icons/dark/scalable/rotate-ccw + icons/dark/scalable/rotate-ccw.svg + icons/dark/scalable/rotate-cw + icons/dark/scalable/rotate-cw.svg + icons/dark/scalable/sliders.svg + icons/dark/scalable/square.svg + icons/dark/scalable/sun.svg + icons/dark/scalable/zoom-out.svg + icons/dark/scalable/minus.svg + icons/dark/scalable/plus.svg + icons/dark/scalable/cancel.svg + icons/dark/scalable/loader.svg + icons/light/scalable/camera.svg + icons/light/index.theme + icons/light/scalable/cast.svg + icons/light/scalable/check.svg + icons/light/scalable/chevron-left.svg + icons/light/scalable/chevron-right.svg + icons/light/scalable/crop.svg + icons/light/scalable/download.svg + icons/light/scalable/edit.svg + icons/light/scalable/file.svg + icons/light/scalable/folder.svg + icons/light/scalable/grid.svg + icons/light/scalable/help-circle.svg + icons/light/scalable/highlight.svg + icons/light/scalable/home.svg + icons/light/scalable/image.svg + icons/light/scalable/list.svg + icons/light/scalable/maximize.svg + icons/light/scalable/pause.svg + icons/light/scalable/play.svg + icons/light/scalable/rotate-ccw.svg + icons/light/scalable/rotate-cw.svg + icons/light/scalable/ruler.svg + icons/light/scalable/save.svg + icons/light/scalable/settings.svg + icons/light/scalable/sliders.svg + icons/light/scalable/square.svg + icons/light/scalable/sun.svg + icons/light/scalable/trash-2.svg + icons/light/scalable/zoom-in.svg + icons/light/scalable/zoom-out.svg + icons/light/scalable/cancel.svg + icons/light/scalable/loader.svg + icons/light/scalable/minus.svg + icons/light/scalable/plus.svg + docs/home.md + docs/index.md + docs/interface/new_project.md + docs/interface/open_project.md + docs/formats/dome.md + docs/formats/relight.md + docs/formats/config.md + icons/dark/scalable/chevrons-down.svg + icons/dark/scalable/chevrons-up.svg + icons/light/scalable/chevrons-down.svg + icons/light/scalable/chevrons-up.svg + ../build_scripts/relight.png + + diff --git a/relightlab/roadmap.md b/relightlab/roadmap.md new file mode 100644 index 00000000..848ef120 --- /dev/null +++ b/relightlab/roadmap.md @@ -0,0 +1,47 @@ +# Roadmap for RelightLab. + + + +## Reinstate existing functions: + +* lp saving +* normals filtering/flattening + +## New Features + +* consider eventual modifications to the file format to mirror interface parameters. +* manual alignment +* ellipse reflections geometrically accurate +* cancel button on sphere reflection processing. +* better info on queue items. +* if scale exists don't overwrite imageWith with dome scale. +* remove relight_vector and jus use eigen. +* settings: notifications +* allow resize for normals integration (it's too slow...). + +## Long term new features + +* preprocessing files and caching +* TIF/CR2/JPEGXL support +* floating point processing +* shadow and burned pixels filtering +* Assisted/automated alignment +* PBR texture creation +* shadow removal +* 3d lights on sphere should take into account sphere posistion/ +* allow rescaling when building rti and normals + + +## Bugs + +* dark theme on windows conflicts +* Accessing the crop tab before loading the project causes it to stop working. +* ptm and hsh number of planes cannot be changed. +* check for invalid inner circle when creating a sphere. +* test pause/stop/play in queue. +* deal with #lights different from #images + +## Small Bugs + +* Check version is properly written and read from the .relight file + diff --git a/relightlab/rticard.cpp b/relightlab/rticard.cpp new file mode 100644 index 00000000..501cb65d --- /dev/null +++ b/relightlab/rticard.cpp @@ -0,0 +1,130 @@ +#include "rticard.h" +#include "helpbutton.h" +#include "rtiexportdialog.h" + +#include +#include +#include +#include +#include +#include + +RtiCard::RtiCard(Rti::Type _type, Rti::ColorSpace _colorspace, int _nplanes, int _nchroma, QWidget *parent): + QFrame(parent), type(_type), colorspace(_colorspace), nplanes(_nplanes), nchroma(_nchroma) { + + + + QStringList titles; + titles << + "PTM" << + "HSH" << + "RBF" << + "BNL" << + "Neural"; + + QStringList tooltips; + tooltips << + "Polynomial Texture Maps" << + "HemiSpherical Harmonics" << + "Radial Basis Functions" << + "Bilinear sampling" << + "Convolutional neural network"; + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(title_label = new HelpLabel("", "rti/" + titles[(int)type].toLower())); + updateTitle(); + + //QImage thumb = qRelightApp->thumbnails()[0]; + QLabel *img = new QLabel(); + img->setMinimumSize(300, 300); + img->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + layout->addWidget(img); + + if(type == Rti::HSH) { + QComboBox *combo = new QComboBox; + combo->addItems(QStringList() << "4 harmonics" << "9 harmonics"); + connect(combo, QOverload::of(&QComboBox::currentIndexChanged), [&] (int h) { + nplanes = h == 0? 12 : 27; + updateTitle(); + }); + layout->addWidget(combo); + } + if(type == Rti::RBF || type == Rti::BILINEAR) { + QComboBox *combo = new QComboBox; + combo->addItems(QStringList() << "12 coefficients" << "15 coefficients" << "18 coefficients" << "21 coefficients" << "24 coefficients" << "27 coefficients"); + combo->setCurrentIndex(nplanes/3 - 4); + + layout->addWidget(combo); + + QComboBox *chroma = new QComboBox; + chroma->addItems(QStringList() << "0 chroma dedicated" << "1 chroma dedicated" << "2 chroma dedicated"); + chroma->setCurrentIndex(nchroma); + layout->addWidget(chroma); + } + + QHBoxLayout *hbox = new QHBoxLayout; + layout->addLayout(hbox); + + //hbox->addWidget(new QPushButton("Preview"), 1); + QPushButton *create = new QPushButton("Create"); + hbox->addWidget(create, 1); + + if(type == Rti::PTM) { + QPushButton *legacy = new QPushButton("Export .ptm for RtiViewer"); + hbox->addWidget(legacy, 1); + } + if(type == Rti::HSH) { + QPushButton *legacy = new QPushButton("Export .rti for RtiViewer"); + hbox->addWidget(legacy, 1); + } + + + + + connect(create, SIGNAL(clicked()), this, SLOT(rtiExport())); + + setFrameStyle(QFrame::StyledPanel); + setAutoFillBackground(true); + setBackgroundRole(QPalette::AlternateBase); +} + +void RtiCard::updateTitle() { + QStringList titles; + titles << + "PTM" << + "HSH" << + "RBF" << + "BNL" << + "Neural"; + + QString title = titles[(int)type]; + if(colorspace == Rti::LRGB) { + title = "L" + title; + } + + title += QString(" %1").arg(nplanes); + if(nchroma > 0) { + title += QString(".%1").arg(nchroma); + } + title_label->label->setText(title); +} + +void RtiCard::rtiExport() { + RtiExportDialog *dialog = new RtiExportDialog(this); + dialog->exec(); +} + +void RtiCard::mousePressEvent(QMouseEvent *event) { + if(checkable) { + setChecked(!checked); + emit toggled(checked); + } + QFrame::mousePressEvent(event); + +} + +void RtiCard::setChecked(bool _checked) { + checked = _checked; + setFrameStyle(checked ? QFrame::Panel : QFrame::StyledPanel); + setBackgroundRole(checked ? QPalette::Base : QPalette::AlternateBase); +} diff --git a/relightlab/rticard.h b/relightlab/rticard.h new file mode 100644 index 00000000..dd012cc5 --- /dev/null +++ b/relightlab/rticard.h @@ -0,0 +1,40 @@ +#ifndef RTICARD_H +#define RTICARD_H + +#include +#include "../src/rti.h" + + +class HelpLabel; + +class RtiCard: public QFrame { + Q_OBJECT +public: + Rti::Type type; + Rti::ColorSpace colorspace; + int nplanes; + int nchroma; + + RtiCard(Rti::Type type, Rti::ColorSpace colorspace, int nplanes, int nchroma, QWidget *parent = nullptr); + void setCheckable(bool); + +public slots: + void setChecked(bool); + void updateTitle(); + void rtiExport(); +signals: + void toggled(bool checked); + +protected: + //manage on click event + void mousePressEvent(QMouseEvent *event) override; + +private: + HelpLabel *title_label = nullptr; + bool checkable = false; + bool checked = false; + +}; + + +#endif // RTICARD_H diff --git a/relightlab/rtiexportdialog.cpp b/relightlab/rtiexportdialog.cpp new file mode 100644 index 00000000..a57f735e --- /dev/null +++ b/relightlab/rtiexportdialog.cpp @@ -0,0 +1,81 @@ +#include "rtiexportdialog.h" +#include "helpbutton.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +RtiExportDialog::RtiExportDialog(QWidget *parent): QDialog(parent) { + setWindowTitle("Export RTI"); + setMinimumWidth(400); + + QVBoxLayout *content = new QVBoxLayout(this); + + QGroupBox *format = new QGroupBox("Format"); + content->addWidget(format); + QVBoxLayout *format_layout = new QVBoxLayout(format); + format_layout->addWidget(new HelpRadio("Plain images", "format/relight")); + format_layout->addWidget(new HelpRadio("Deepzoom", "format/deepzoom")); + format_layout->addWidget(new HelpRadio("Tarzoom", "format/tarzoom")); + format_layout->addWidget(new HelpRadio("Interleaved Tarzoom", "format/itarzoom")); + + QHBoxLayout *quality_box = new QHBoxLayout; + content->addLayout(quality_box); + quality_box->addWidget(new QLabel("Jpeg quality")); + quality_box->addWidget(quality = new QSpinBox); + quality->setRange(75, 100); + + + content->addSpacing(20); + content->addWidget(new QLabel("Directory:")); + + QHBoxLayout *dir = new QHBoxLayout; + content->addLayout(dir); + dir->addWidget(new QLineEdit()); + dir->addWidget(new QPushButton("...")); + + QHBoxLayout *buttons = new QHBoxLayout; + content->addLayout(buttons); + buttons->addStretch(); + buttons->addWidget(new QPushButton("Create")); + buttons->addWidget(new QPushButton("Cancel")); + + //seupt connections + //connect(buttons->itemAt(1)->widget(), &QPushButton::clicked, this, &RtiExportDialog::accept); + //connect(buttons->itemAt(2)->widget(), &QPushButton::clicked, this, &RtiExportDialog::reject); + + //init values from settings + //int quality = QSettings().value("rti/defaults/quality", 95).toInt(); +} + +LegacyExportDialog::LegacyExportDialog(QWidget *parent): QDialog(parent) { + setWindowTitle("Export legacy RTI"); + setMinimumWidth(400); + + QVBoxLayout *content = new QVBoxLayout(this); + + content->addWidget(new HelpRadio("Uncompressed", "format/compression")); + content->addWidget(new HelpRadio("JPEG", "format/compressiob")); + + QHBoxLayout *quality_box = new QHBoxLayout; + content->addLayout(quality_box); + quality_box->addWidget(new QLabel("Jpeg quality")); + quality_box->addWidget(quality = new QSpinBox); + quality->setRange(75, 100); + + QHBoxLayout *filename = new QHBoxLayout; + content->addLayout(filename); + filename->addWidget(new QLineEdit()); + filename->addWidget(new QPushButton("...")); + + QHBoxLayout *buttons = new QHBoxLayout; + content->addLayout(buttons); + buttons->addStretch(); + buttons->addWidget(new QPushButton("Create")); + buttons->addWidget(new QPushButton("Cancel")); +} diff --git a/relightlab/rtiexportdialog.h b/relightlab/rtiexportdialog.h new file mode 100644 index 00000000..502c7fac --- /dev/null +++ b/relightlab/rtiexportdialog.h @@ -0,0 +1,20 @@ +#ifndef RTIEXPORTDIALOG_H +#define RTIEXPORTDIALOG_H + +#include + +class QSpinBox; + +class RtiExportDialog: public QDialog { +public: + RtiExportDialog(QWidget *parent = nullptr); + QSpinBox *quality = nullptr; +}; + +class LegacyExportDialog: public QDialog { +public: + LegacyExportDialog(QWidget *parent = nullptr); + QSpinBox *quality = nullptr; +}; + +#endif // RTIEXPORTDIALOG_H diff --git a/relightlab/rtiframe.cpp b/relightlab/rtiframe.cpp new file mode 100644 index 00000000..e719c062 --- /dev/null +++ b/relightlab/rtiframe.cpp @@ -0,0 +1,166 @@ +#include "rtiframe.h" +#include "helpbutton.h" +#include "relightapp.h" +#include "rticard.h" +#include "flowlayout.h" +#include "rtiplan.h" +#include "rtirow.h" +#include "rtirecents.h" +#include "rtiframe.h" +#include "qlabelbutton.h" +#include "rtitask.h" + +#include "processqueue.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rtiplan.h" + + +RtiFrame::RtiFrame(QWidget *parent): QFrame(parent) { + QVBoxLayout *content = new QVBoxLayout(this); + + content->addWidget(new QLabel("

Build relightable images

")); + content->addSpacing(20); + + content->addWidget(recents = new RtiRecents); + + content->addWidget(rti_plan = new RtiPlan, 1); + connect(rti_plan, SIGNAL(exportRti()), this, SLOT(exportRti())); + return; + +/* content->addWidget(new PtmRow()); + content->addWidget(new HshRow()); + content->addWidget(new RbfRow()); */ + + + QScrollArea *area = new QScrollArea; + area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + area->setWidgetResizable(true); + content->addWidget(area, 1); + + QWidget *widget = new QWidget; + area->setWidget(widget); + + + FlowLayout *sample_layout = new FlowLayout(area->widget()); + + sample_layout->addWidget(new RtiCard(Rti::PTM, Rti::LRGB, 9, 0)); + sample_layout->addWidget(new RtiCard(Rti::PTM, Rti::RGB, 18, 0)); + sample_layout->addWidget(new RtiCard(Rti::HSH, Rti::RGB, 27, 0)); + + + sample_layout->addWidget(new RtiCard(Rti::RBF, Rti::MRGB, 18, 0)); + sample_layout->addWidget(new RtiCard(Rti::BILINEAR, Rti::MRGB, 18, 0)); + + + + /* + QGroupBox *model = new QGroupBox("Model"); + content->addWidget(model); + QVBoxLayout *model_layout = new QVBoxLayout(model); + model_layout->addWidget(new HelpRadio("Polynomial Texture Maps (PTM)", "rti/ptm")); + model_layout->addWidget(new HelpRadio("HemiSpherical Harmonics (HSH)", "rti/hsh")); + model_layout->addWidget(new HelpRadio("Radial Basis Functions (RBF)", "rti/rbf")); + model_layout->addWidget(new HelpRadio("Bilinear sampling (BNL)", "rti/bln")); + model_layout->addWidget(new HelpRadio("Neural network", "rti/neural")); + + QGroupBox *colorspace= new QGroupBox("Colorspace"); + content->addWidget(colorspace); + QVBoxLayout *colorspace_layout = new QVBoxLayout(colorspace); + colorspace_layout->addWidget(new HelpRadio("RGB", "rti/rgb")); + colorspace_layout->addWidget(new HelpRadio("LRGB", "rti/lrgb")); + colorspace_layout->addWidget(new HelpRadio("MRGB", "rti/mrgb")); + colorspace_layout->addWidget(new HelpRadio("YCC", "rti/ycc")); + + QGroupBox *planes = new QGroupBox("Planes"); + content->addWidget(planes); + + QGridLayout *planes_layout = new QGridLayout(planes); + planes_layout->addWidget(new HelpLabel("Total number of planes:", "rti/planes"), 0, 0); + + QSpinBox *total_planes = new QSpinBox; + planes_layout->addWidget(total_planes, 0, 1); + + planes_layout->addWidget(new HelpLabel("Number of luminance planes:", "rti/luminance"), 1, 0); + + QSpinBox *luminance_planes = new QSpinBox; + planes_layout->addWidget(luminance_planes, 1, 1); +*/ +/* + QGroupBox *legacy = new QGroupBox("Export .rti for RtiViewer"); + content->addWidget(legacy); + + QGridLayout *legacy_layout = new QGridLayout(legacy); + + legacy_layout->addWidget(new HelpRadio("Lossless (heavy!)", "rti/legacy"), 0, 0); + + legacy_layout->addWidget(new HelpRadio("JPEG", "rti/legacy"), 1, 0); + legacy_layout->addWidget(new HelpLabel("Quality:", "rti/legacy"), 1, 1); + QSpinBox *quality = new QSpinBox; + legacy_layout->addWidget(quality, 1, 2); + + legacy_layout->addWidget(new QLabel("Filename:"), 2, 0); + legacy_layout->addWidget(new QLineEdit(), 2, 1); + legacy_layout->addWidget(new QPushButton("..."), 2, 2); + + legacy_layout->addWidget(new QPushButton("Export"), 3, 2); */ + + content->addStretch(); +} + +void RtiFrame::exportRti() { + + //check for lights + if(qRelightApp->project().dome.directions.size() == 0) { + QMessageBox::warning(this, "Missing light directions.", "You need light directions for this dataset to build an RTI.\n" + "You can either load a dome or .lp file or mark a reflective sphere in the 'Lights' tab."); + return; + } + RtiParameters ¶meters = rti_plan->parameters; + //get folder if not legacy. + QString output; + if(parameters.format == RtiParameters::RTI) { + QString extension; + QString label; + + if(parameters.basis == Rti::HSH) { + extension = ".rti"; + label = "RTI file (*.rti)"; + } else if(parameters.basis == Rti::PTM) { + extension = ".ptm"; + label = "PTM file (*.ptm)"; + } + output = QFileDialog::getSaveFileName(this, "Select a file name", QString(), label); + if(output.isNull()) return; + + if(!output.endsWith(extension)) + output += extension; + + } else { + output = QFileDialog::getSaveFileName(this, "Select an output folder", QString()); + if(output.isNull()) return; + } + parameters.path = output; + + RtiTask *rti_task = new RtiTask(qRelightApp->project()); + rti_task->parameters = parameters; + + ProcessQueue &queue = ProcessQueue::instance(); + queue.addTask(rti_task); + + emit processStarted(); +} + + diff --git a/relightlab/rtiframe.h b/relightlab/rtiframe.h new file mode 100644 index 00000000..7beb3e7f --- /dev/null +++ b/relightlab/rtiframe.h @@ -0,0 +1,31 @@ +#ifndef RTIFRAME_H +#define RTIFRAME_H + +#include +//TODO we should separate RTI definitions from actual implementation (materials etc). +#include "../src/rti.h" + +class RtiPlan; +class RtiCard; +class RtiRecents; +class RtiParameters; + +class RtiFrame: public QFrame { + Q_OBJECT + +public: + RtiFrame(QWidget *parent = nullptr); + void init(); + +public slots: + void exportRti(); + +signals: + void processStarted(); + +private: + RtiRecents *recents; + RtiPlan *rti_plan; +}; + +#endif // RTIFRAME_H diff --git a/relightlab/rtiplan.cpp b/relightlab/rtiplan.cpp new file mode 100644 index 00000000..1b0aeff6 --- /dev/null +++ b/relightlab/rtiplan.cpp @@ -0,0 +1,421 @@ +#include "rtiplan.h" +#include "rtiframe.h" +#include "qlabelbutton.h" +#include "helpbutton.h" + +#include +#include +#include +#include +#include +#include +#include + +RtiPlanRow::RtiPlanRow(RtiParameters ¶m, QFrame *parent): QFrame(parent), parameters(param) { + QHBoxLayout *layout = new QHBoxLayout(this); + + label = new HelpLabel("", ""); + label->setFixedWidth(200); + layout->addWidget(label, 0, Qt::AlignLeft); + //layout->setSpacing(20); + + layout->addStretch(1); + + QFrame *buttonsFrame = new QFrame; + buttonsFrame->setMinimumWidth(860); + buttonsFrame->setFrameStyle(QFrame::Box); + + layout->addWidget(buttonsFrame); + + buttons = new QHBoxLayout(buttonsFrame); + + layout->addStretch(1); +} + + +RtiBasisRow::RtiBasisRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + label->label->setText("Basis:"); + label->help->setId("rti/basis"); + + ptm = new QLabelButton("PTM", "Polynomial Texture Map"); + hsh = new QLabelButton("HSH", "HemiSpherical Harmonics"); + rbf = new QLabelButton("RBF", "Radial Basis Functions"); + bln = new QLabelButton("BNL", "Bilinear interplation"); + + buttons->addWidget(ptm, 0, Qt::AlignCenter); + buttons->addWidget(hsh, 0, Qt::AlignCenter); + buttons->addWidget(rbf, 0, Qt::AlignCenter); + buttons->addWidget(bln, 0, Qt::AlignCenter); + + connect(ptm, &QAbstractButton::clicked, this, [this](){ setBasis(Rti::PTM, true); }); + connect(hsh, &QAbstractButton::clicked, this, [this](){ setBasis(Rti::HSH, true); }); + connect(rbf, &QAbstractButton::clicked, this, [this](){ setBasis(Rti::RBF, true); }); + connect(bln, &QAbstractButton::clicked, this, [this](){ setBasis(Rti::BILINEAR, true); }); + + QButtonGroup *group = new QButtonGroup(this); + + group->addButton(ptm); + group->addButton(hsh); + group->addButton(rbf); + group->addButton(bln); + setBasis(parameters.basis); +} + +void RtiBasisRow::setBasis(Rti::Type basis, bool emitting) { + parameters.basis = basis; + + if(emitting) { + emit basisChanged(); + return; + } + + switch(basis) { + case Rti::PTM: ptm->setChecked(true); break; + case Rti::HSH: hsh->setChecked(true); break; + case Rti::RBF: rbf->setChecked(true); break; + case Rti::BILINEAR: bln->setChecked(true); break; + default: break; + } +} + +RtiColorSpaceRow::RtiColorSpaceRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + + label->label->setText("Colorspace:"); + label->help->setId("rti/colorspace"); + + rgb = new QLabelButton("RGB", "Standard"); + lrgb = new QLabelButton("LRGB", "Albedo * Luminance."); + mrgb = new QLabelButton("MRGB", "Standard"); + ycc = new QLabelButton("YCC", "Dedicated chroma coefficients."); + + buttons->addWidget(rgb, 0, Qt::AlignLeft); + buttons->addWidget(lrgb, 0, Qt::AlignRight); + buttons->addWidget(mrgb, 0, Qt::AlignLeft); + buttons->addWidget(ycc, 0, Qt::AlignRight); + + connect(rgb, &QAbstractButton::clicked, this, [this](){ setColorspace(Rti::RGB, true); }); + connect(lrgb, &QAbstractButton::clicked, this, [this](){ setColorspace(Rti::LRGB, true); }); + connect(mrgb, &QAbstractButton::clicked, this, [this](){ setColorspace(Rti::MRGB, true); }); + connect(ycc, &QAbstractButton::clicked, this, [this](){ setColorspace(Rti::YCC, true); }); + + QButtonGroup *group = new QButtonGroup(this); + group->addButton(rgb); + group->addButton(lrgb); + group->addButton(mrgb); + group->addButton(ycc); + + setColorspace(parameters.colorspace); +} + + +void RtiColorSpaceRow::setColorspace(Rti::ColorSpace colorspace, bool emitting) { + parameters.colorspace = colorspace; + + bool pca = parameters.basis == Rti::RBF || parameters.basis == Rti::BILINEAR; + + rgb->setEnabled(!pca); + lrgb->setEnabled(!pca); + mrgb->setEnabled(pca); + ycc->setEnabled(pca); + + if(emitting) { + emit colorspaceChanged(); + return; + } + switch(colorspace) { + case Rti::RGB: rgb->setChecked(true); break; + case Rti::LRGB: lrgb->setChecked(true); break; + case Rti::MRGB: mrgb->setChecked(true); break; + case Rti::YCC: ycc->setChecked(true); break; + } +} + + +RtiPlanesRow::RtiPlanesRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + + label->label->setText("Planes:"); + label->help->setId("rti/planes"); + + + buttons->addWidget(new QLabel("Total number of images:")); + nplanesbox = new QComboBox; + nplanesbox->setFixedWidth(100); + for(int i = 0; i < 7; i++) { + nplanesbox->addItem(QString::number(nimages[i])); + } + connect(nplanesbox, static_cast(&QComboBox::currentIndexChanged), [this](int n) { setNPlanes(nimages[n]*3, true); }); + buttons->addWidget(nplanesbox); + + buttons->addStretch(1); + buttons->addWidget(new QLabel("Number of dedicated chroma images:")); + nchromabox = new QComboBox; + nchromabox->setFixedWidth(100); + + for(int i = 0; i < 3; i++) { + nchromabox->addItem(QString::number(nchromas[i])); + } + connect(nchromabox, static_cast(&QComboBox::currentIndexChanged), [this](int n) { setNChroma(nchromas[n], true); }); + buttons->addWidget(nchromabox); + + setNPlanes(parameters.nplanes); + setNChroma(parameters.nchroma); +} + +void RtiPlanesRow::setNPlanes(int nplanes, bool emitting) { + nchromabox->setEnabled(parameters.colorspace == Rti::YCC); + + parameters.nplanes = nplanes; + if(emitting) { + emit nplanesChanged(); + return; + } + for(int i = 0; i < 7; i++) { + if(nimages[i] == nplanes/3) + nplanesbox->setCurrentIndex(i); + } +} + +void RtiPlanesRow::setNChroma(int nchroma, bool emitting) { + nchromabox->setEnabled(parameters.colorspace == Rti::YCC); + + parameters.nchroma = nchroma; + if(emitting) { + emit nplanesChanged(); + return; + } + + for(int i = 0; i < 3; i++) { + if(nchromas[i] == nchroma) + nchromabox->setCurrentIndex(i); + } +} + + +RtiFormatRow::RtiFormatRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + label->label->setText("Format:"); + label->help->setId("rti/format"); + + + rti = new QLabelButton("Legacy", ".rti, .ptm"); + web = new QLabelButton("Web", ".json, .jpg"); + iip = new QLabelButton("IIP", ".tiff"); + + buttons->addWidget(rti, Qt::AlignLeft); + buttons->addWidget(web, Qt::AlignLeft); + buttons->addWidget(iip, Qt::AlignLeft); + + connect(rti, &QAbstractButton::clicked, [this]() { setFormat(RtiParameters::RTI, true); }); + connect(web, &QAbstractButton::clicked, [this]() { setFormat(RtiParameters::WEB, true); }); + connect(iip, &QAbstractButton::clicked, [this]() { setFormat(RtiParameters::IIP, true); }); + + QButtonGroup *group = new QButtonGroup(this); + group->addButton(rti); + group->addButton(web); + group->addButton(iip); + + allowLegacy(parameters.basis == Rti::PTM || parameters.basis == Rti::HSH); + setFormat(parameters.format); +} + +void RtiFormatRow::allowLegacy(bool legacy) { + rti->setEnabled(legacy); +} + +void RtiFormatRow::setFormat(RtiParameters::Format format, bool emitting) { + parameters.format = format; + + if(emitting) { + emit formatChanged(); + return; + } + switch(format) { + case RtiParameters::RTI: rti->setChecked(true); break; + case RtiParameters::WEB: web->setChecked(true); break; + case RtiParameters::IIP: iip->setChecked(true); break; + } + +} + +RtiQualityRow::RtiQualityRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + label->label->setText("Image quality:"); + label->help->setId("rti/quality"); + + buttons->addWidget(losslessbox = new QCheckBox(" Lossless"), Qt::AlignLeft); + + QLabel *qualitylabel = new QLabel("Quality:"); + qualitylabel->setMaximumWidth(200); + buttons->addWidget(qualitylabel, Qt::AlignRight); + + qualitybox = new QSpinBox; + qualitybox->setMaximumWidth(200); + qualitybox->setMinimum(75); + qualitybox->setMaximum(100); + qualitybox->setValue(parameters.quality); + buttons->addWidget(qualitybox, Qt::AlignRight); +} + +void RtiQualityRow::setQuality(int quality, bool emitting) { + parameters.quality = quality; + if(quality == 0) + losslessbox->setChecked(true); + + if(emitting) + emit qualityChanged(); + else + qualitybox->setValue(quality); +} +void RtiQualityRow::allowLossless(bool allow) { + losslessbox->setEnabled(allow); +} + +RtiWebLayoutRow::RtiWebLayoutRow(RtiParameters ¶meters, QFrame *parent): RtiPlanRow(parameters, parent) { + label->label->setText("Web layout:"); + label->help->setId("rti/web_layout"); + + image = new QLabelButton("Images", ""); + deepzoom = new QLabelButton("Deepzoom", "Pyramidal, lot's of files."); + tarzoom = new QLabelButton("Tarzoom", "Pyramidal, few files but 206"); + itarzoom = new QLabelButton("ITarzoom", "Pyramidal, few files and requests but 206"); + + buttons->addWidget(image); + buttons->addWidget(deepzoom); + buttons->addWidget(tarzoom); + buttons->addWidget(itarzoom); + + setWebLayout(parameters.web_layout); + + connect(image, &QAbstractButton::clicked, [this]() { setWebLayout(RtiParameters::PLAIN); }); + connect(deepzoom, &QAbstractButton::clicked, [this]() { setWebLayout(RtiParameters::DEEPZOOM); }); + connect(tarzoom, &QAbstractButton::clicked, [this]() { setWebLayout(RtiParameters::TARZOOM); }); + connect(itarzoom, &QAbstractButton::clicked, [this]() { setWebLayout(RtiParameters::ITARZOOM); }); + + QButtonGroup *group = new QButtonGroup(this); + group->addButton(image); + group->addButton(deepzoom); + group->addButton(tarzoom); + group->addButton(itarzoom); +} + +void RtiWebLayoutRow::setWebLayout(RtiParameters::WebLayout layout, bool emitting) { + parameters.web_layout = layout; + + if(emitting) { + emit layoutChanged(); + return; + } + + image->setChecked(layout == RtiParameters::PLAIN); + deepzoom->setChecked(layout == RtiParameters::DEEPZOOM); + tarzoom->setChecked(layout == RtiParameters::TARZOOM); + itarzoom->setChecked(layout == RtiParameters::ITARZOOM); +} + + + +RtiPlan::RtiPlan(QWidget *parent): QFrame(parent) { + QVBoxLayout *layout = new QVBoxLayout(this); + + basis_row = new RtiBasisRow(parameters, this); + colorspace_row = new RtiColorSpaceRow(parameters, this); + planes_row = new RtiPlanesRow(parameters, this); + format_row = new RtiFormatRow(parameters, this); + quality_row = new RtiQualityRow(parameters, this); + layout_row = new RtiWebLayoutRow(parameters, this); + + + layout->addWidget(basis_row); + layout->addWidget(colorspace_row); + layout->addWidget(planes_row); + layout->addWidget(format_row); + layout->addWidget(quality_row); + layout->addWidget(layout_row); + + QHBoxLayout *save_row = new QHBoxLayout; + QPushButton *save = new QPushButton("Export RTI...", this); + save->setProperty("class", "large"); + save_row->addWidget(save); + + connect(save, &QPushButton::clicked, [this]() { emit exportRti(); }); + + layout->addLayout(save_row); + layout->addStretch(); + + connect(basis_row, &RtiBasisRow::basisChanged, this, &RtiPlan::basisChanged); + connect(colorspace_row, &RtiColorSpaceRow::colorspaceChanged, this, &RtiPlan::colorspaceChanged); + connect(planes_row, &RtiPlanesRow::nplanesChanged, this, &RtiPlan::nplanesChanged); + connect(format_row, &RtiFormatRow::formatChanged, this, &RtiPlan::formatChanged); + connect(layout_row, &RtiWebLayoutRow::layoutChanged, this, &RtiPlan::layoutChanged); + connect(quality_row, &RtiQualityRow::qualityChanged, this, &RtiPlan::qualityChanged); +} + + +void RtiPlan::basisChanged() { + //when basis is changed we try to change the other values as little as possible. + //if the new basis is not compatible with the current color space we change it to the default one + //colorspace: + auto &basis = parameters.basis; + bool pca = basis == Rti::RBF || basis == Rti::BILINEAR; + + // COLORSPACE + + auto &colorspace = parameters.colorspace; + if(!pca) { + if(colorspace != Rti::RGB && colorspace != Rti::LRGB) + colorspace = Rti::RGB; + + } else { + if(colorspace != Rti::MRGB && colorspace != Rti::YCC) + colorspace = Rti::MRGB; + } + colorspace_row->setColorspace(colorspace); + + // PLANES + + auto &nplanes = parameters.nplanes; + auto &nchroma = parameters.nchroma; + + switch(basis) { + case Rti::PTM: nplanes = 18; nchroma = 0; break; + case Rti::HSH: if(nplanes != 12 && nplanes != 27) nplanes = 27; nchroma = 0; break; + case Rti::RBF: + case Rti::BILINEAR: + if(colorspace != Rti::YCC) nchroma = 0; + } + + planes_row->setNPlanes(nplanes); + planes_row->setNChroma(nchroma); + + //FORMAT + format_row->allowLegacy(!pca); + + if(pca && parameters.format == RtiParameters::RTI) { + format_row->setFormat(RtiParameters::WEB); //emit and cascade update other rows. + } +} + +void RtiPlan::colorspaceChanged() { + planes_row->setNChroma(parameters.nchroma); + + bool pca = parameters.basis == Rti::RBF || parameters.basis == Rti::BILINEAR; + if(pca && parameters.format == RtiParameters::RTI) { + format_row->setFormat(RtiParameters::WEB); + } +} + +void RtiPlan::nplanesChanged() { + //actually nothing happens! +} + +void RtiPlan::formatChanged() { + bool legacy = quality_row->parameters.format == RtiParameters::RTI; + //only RTI allows for lossless. + quality_row->allowLossless(legacy); + layout_row->setEnabled(quality_row->parameters.format == RtiParameters::WEB); +} + +void RtiPlan::qualityChanged() { +} + +void RtiPlan::layoutChanged() { +} + diff --git a/relightlab/rtiplan.h b/relightlab/rtiplan.h new file mode 100644 index 00000000..39ba009e --- /dev/null +++ b/relightlab/rtiplan.h @@ -0,0 +1,142 @@ +#ifndef RTIPLAN_H +#define RTIPLAN_H + +#include +#include "../src/rti.h" +#include "rtitask.h" + +class QComboBox; +class QCheckBox; +class QSpinBox; +class QLabelButton; +class HelpLabel; +class QHBoxLayout; + +class RtiPlanRow: public QFrame { + Q_OBJECT +public: + RtiPlanRow(RtiParameters ¶meters, QFrame *parent = nullptr); + + RtiParameters ¶meters; + HelpLabel *label = nullptr; + QHBoxLayout *buttons = nullptr; +}; + + +class RtiBasisRow: public RtiPlanRow { + Q_OBJECT +public: + RtiBasisRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setBasis(Rti::Type basis, bool emitting = false); + +private: + QLabelButton *ptm, *hsh, *rbf, *bln; + +signals: + void basisChanged(); +}; + + +class RtiColorSpaceRow: public RtiPlanRow { + Q_OBJECT +public: + RtiColorSpaceRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setColorspace(Rti::ColorSpace colorspace, bool emitting = false); + +private: + QLabelButton *rgb, *lrgb, *mrgb, *ycc; + +signals: + void colorspaceChanged(); +}; + + +class RtiPlanesRow: public RtiPlanRow { + Q_OBJECT +public: + RtiPlanesRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setNPlanes(int nplanes, bool emitting = false); + void setNChroma(int nchroma, bool emitting = false); + +private: + QComboBox *nplanesbox, *nchromabox; + int nimages[7] = { 3, 4, 5, 6, 7, 8, 9 }; + int nchromas[3] = { 1, 2, 3 }; +signals: + void nplanesChanged(); +}; + + +class RtiFormatRow: public RtiPlanRow { + Q_OBJECT +public: + RtiFormatRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setFormat(RtiParameters::Format format, bool emitting = false); + void allowLegacy(bool legacy); +private: + RtiParameters::Format format; + QLabelButton *rti, *web, *iip; + +signals: + void formatChanged(); +}; + + +class RtiQualityRow: public RtiPlanRow { + Q_OBJECT +public: + RtiQualityRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setQuality(int quality, bool emitting = false); //0 stands for lossless. + void allowLossless(bool allow); + +private: + QCheckBox *losslessbox; + QSpinBox *qualitybox; + +signals: + void qualityChanged(); +}; + + +class RtiWebLayoutRow: public RtiPlanRow { + Q_OBJECT +public: + RtiWebLayoutRow(RtiParameters ¶meters, QFrame *parent = nullptr); + void setWebLayout(RtiParameters::WebLayout layout, bool emitting = false); //0 stands for lossless. +private: + QLabelButton *image, *deepzoom, *tarzoom, *itarzoom; +signals: + void layoutChanged(); + +}; + + +class RtiPlan: public QFrame { + Q_OBJECT +public: + RtiPlan(QWidget *parent = nullptr); + void setParameters(RtiParameters parameters); + + RtiParameters parameters; + +public slots: + void basisChanged(); + void colorspaceChanged(); + void nplanesChanged(); + void formatChanged(); + void qualityChanged(); + void layoutChanged(); + +signals: + void exportRti(); + +private: + RtiBasisRow *basis_row = nullptr; + RtiColorSpaceRow *colorspace_row = nullptr; + RtiPlanesRow *planes_row = nullptr; + RtiFormatRow *format_row = nullptr; + RtiQualityRow *quality_row = nullptr; + RtiWebLayoutRow *layout_row = nullptr; +}; + +#endif // RTIPLAN_H diff --git a/relightlab/rtirecents.cpp b/relightlab/rtirecents.cpp new file mode 100644 index 00000000..30e94728 --- /dev/null +++ b/relightlab/rtirecents.cpp @@ -0,0 +1,76 @@ +#include "rtirecents.h" +#include "qlabelbutton.h" +#include "recentprojects.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +vector recentRtis() { + vector params; + + QStringList recents = QSettings().value("recent-rtis", QStringList()).toStringList(); + for(QString &s: recents) { + QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8()); + QJsonObject obj = doc.object(); + RtiParameters p; + p.basis = (Rti::Type)obj["basis"].toInt(); + p.colorspace = (Rti::ColorSpace) obj["colorspace"].toInt(); + p.nplanes = obj["nplanes"].toInt(); + p.nchroma = obj["nchroma"].toInt(); + p.format = (RtiParameters::Format) obj["format"].toInt(); + + params.push_back(p); + } + return params; +} + +void addRecentRti(const RtiParameters ¶ms) { + QSettings settings; + QStringList recents = settings.value("recent-rtis", QStringList()).toStringList(); + QJsonObject obj; + obj["basis"] = params.basis; + obj["colorspace"] = params.colorspace; + obj["nplanes"] = params.nplanes; + obj["nchroma"] = params.nchroma; + obj["format"] = params.format; + QJsonDocument doc(obj); + recents.prepend(doc.toJson()); + while(recents.size() > 5) { + recents.removeLast(); + } + settings.setValue("recent-rtis", recents); +} + + +RtiRecents::RtiRecents(QFrame *parent): QFrame(parent) { + QHBoxLayout *content = new QHBoxLayout(this); + + std::vector params = recentRtis(); + for(RtiParameters &p: params) { + QString basisLabels[] = { "PTM", "HSH", "RBF", "BLN", "NEURAL" }; + QString colorspaceLabels[] = { "RGB", "LRGB", "YCC", "RGB", "YCC" }; + QString formatLabels[] = { "", "relight", "deepzoom", "tarzoom", "itarzoom", "tiff" }; + + QString basis = basisLabels[p.basis]; + QString colorspace = colorspaceLabels[p.colorspace]; + QString planes = QString::number(p.nplanes); + if(p.nchroma) { + planes += "." + QString::number(p.nchroma); + } + QString format; + if(p.format == RtiParameters::RTI) + format = p.basis == Rti::PTM ? ".ptm" : ".rti"; + else + format = formatLabels[p.format]; + + QString txt = QString("

%1 (%2) %3

" + "

%4

").arg(basis).arg(colorspace).arg(planes).arg(format); + content->addWidget(new QLabelButton(txt)); + } +} diff --git a/relightlab/rtirecents.h b/relightlab/rtirecents.h new file mode 100644 index 00000000..a13ba6dd --- /dev/null +++ b/relightlab/rtirecents.h @@ -0,0 +1,17 @@ +#ifndef RTIRECENTS_H +#define RTIRECENTS_H + +#include "rtitask.h" + +#include +#include + +std::vector recentRtis(); +void addRecentRti(const RtiParameters ¶ms); + +class RtiRecents: public QFrame { +public: + RtiRecents(QFrame *parent = nullptr); +}; + +#endif // RTIRECENTS_H diff --git a/relightlab/rtirow.cpp b/relightlab/rtirow.cpp new file mode 100644 index 00000000..c006e788 --- /dev/null +++ b/relightlab/rtirow.cpp @@ -0,0 +1,63 @@ +#include "rtirow.h" +#include "helpbutton.h" + +#include +#include +#include +#include + +RtiRow::RtiRow(QWidget *parent): QFrame(parent) { + row = new QHBoxLayout(this); + +} + +PtmRow::PtmRow(QWidget *parent): RtiRow(parent) { + row->addWidget(new HelpLabel("PTM: Polynomial Texture Maps", "rti/ptm"), 1); + row->addStretch(1); + QCheckBox *lrgb = new QCheckBox("LRGB"); + row->addWidget(lrgb, 1); + + + row->addWidget(new QPushButton("Create"), 1); + row->addWidget(new QPushButton("Create .ptm"), 1); +} + + +HshRow::HshRow(QWidget *parent): RtiRow(parent) { + row->addWidget(new HelpLabel("HSH: emiSpherical Harmonics", "rti/hsh"), 1); + QComboBox *nharmonics = new QComboBox(); + nharmonics->addItem("4 harmonics"); + nharmonics->addItem("9 harmonics"); + + row->addWidget(nharmonics, 1); + + QCheckBox *lrgb = new QCheckBox("LRGB"); + row->addWidget(lrgb, 1); + + row->addWidget(new QPushButton("Create"), 1); + row->addWidget(new QPushButton("Create .rti"), 1); +} + +RbfRow::RbfRow(QWidget *parent): RtiRow(parent) { + row->addWidget(new HelpLabel("RBF: Radial Basis Functions", "rti/rbf"), 1); + QComboBox *nplanes = new QComboBox(); + nplanes->addItem("9 planes"); + nplanes->addItem("12 planes "); + nplanes->addItem("15 planes "); + nplanes->addItem("18 planes "); + nplanes->addItem("21 planes "); + nplanes->addItem("24 planes "); + nplanes->addItem("27 planes "); + + row->addWidget(nplanes, 1); + + QComboBox *chroma = new QComboBox(); + chroma->addItem("0 chroma reserved"); + chroma->addItem("1 chroma reserved"); + chroma->addItem("2 chroma reserved"); + chroma->addItem("3 chroma reserved"); + + row->addWidget(chroma, 1); + + row->addWidget(new QPushButton("Create"), 2); +} diff --git a/relightlab/rtirow.h b/relightlab/rtirow.h new file mode 100644 index 00000000..648d6b51 --- /dev/null +++ b/relightlab/rtirow.h @@ -0,0 +1,32 @@ +#ifndef RTIROW_H +#define RTIROW_H + +#include + +class QHBoxLayout; + +class RtiRow: public QFrame { +public: + RtiRow(QWidget *parent = nullptr); + +protected: + QHBoxLayout *row = nullptr; +}; + +class PtmRow: public RtiRow { +public: + PtmRow(QWidget *parent = nullptr); + +}; + +class HshRow: public RtiRow { +public: + HshRow(QWidget *parent = nullptr); + +}; + +class RbfRow: public RtiRow { +public: + RbfRow(QWidget *parent = nullptr); +}; +#endif // RTIROW_H diff --git a/relightlab/rtitask.cpp b/relightlab/rtitask.cpp new file mode 100644 index 00000000..fafb5c32 --- /dev/null +++ b/relightlab/rtitask.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rtitask.h" +#include "../relight/zoom.h" +#include "../src/rti.h" +#include "../relight-cli/rtibuilder.h" + + +#include +using namespace std; + +int convertToRTI(const char *filename, const char *output); +int convertRTI(const char *file, const char *output, int quality); + +void setupLights(ImageSet &imageset, Dome &dome); +RtiTask::RtiTask(const Project &_project): Task(), project(_project) {} + +RtiTask::~RtiTask() { + if(builder) + delete builder; +} + +void RtiTask::run() { + status = RUNNING; + std::function callback = [this](QString s, int n)->bool { return this->progressed(s, n); }; + + builder = new RtiBuilder; + builder->pixelSize = project.pixelSize; + + builder->nworkers = QSettings().value("nworkers", 8).toInt(); + builder->samplingram = QSettings().value("ram", 512).toInt(); + + builder->type = parameters.basis; + builder->colorspace = parameters.colorspace; + builder->nplanes = parameters.nplanes; + builder->yccplanes[0] = parameters.nchroma; + + if( builder->colorspace == Rti::MYCC) { + builder->yccplanes[1] = builder->yccplanes[2] = (builder->nplanes - builder->yccplanes[0])/2; + builder->nplanes = builder->yccplanes[0] + 2*builder->yccplanes[1]; + } + ImageSet &imageset = builder->imageset; + imageset.images = project.getImages(); + imageset.initLightsFromDome(project.dome); + imageset.initImages(input_folder.toStdString().c_str()); + + if(!crop.isNull()) { + builder->crop[0] = crop.left(); + builder->crop[1] = crop.top(); + builder->crop[2] = crop.width(); + builder->crop[3] = crop.height(); + imageset.crop(crop.left(), crop.top(), crop.width(), crop.height()); + } + + builder->lights = imageset.lights; + builder->width = imageset.width; + builder->height = imageset.height; + + QString output = parameters.path; //masking Task::output. + try { + if(!builder->init(&callback)) { + error = builder->error.c_str(); + status = FAILED; + return; + } + if(parameters.format == RtiParameters::RTI) { + if(builder->type == Rti::HSH) + builder->saveUniversal(output.toStdString()); + else if(builder->type == Rti::PTM) + builder->savePTM(output.toStdString()); + else + throw "Legacy RTI and PTM formats are supported only for HSH and PTM basis"; + } else + builder->save(output.toStdString(), parameters.quality); + + } catch(std::string e) { + error = e.c_str(); + status = STOPPED; + return; + } + +/* + else if(step == "fromRTI") + fromRTI(); + //TODO! deepZOOM should set error and status? + else if(step == "deepzoom") { + if ((err = deepZoom(output, output, 95, 0, 256, callback)).compare("OK") != 0) { + error = err; + status = FAILED; + } + } + else if(step == "tarzoom") { + if ((err = tarZoom(output, output, callback)).compare("OK") != 0) { + error = err; + status = FAILED; + } + } + else if(step == "itarzoom") { + if ((err = itarZoom(output, output, callback)).compare("OK") != 0) { + error = err; + status = FAILED; + } + } + else if(step == "openlime") + openlime(); + } */ + if(status != FAILED) + status = DONE; +} +/* +void RtiTask::relight(bool commonMinMax, bool saveLegacy) { + builder = new RtiBuilder; + builder->pixelSize =(*this)["pixelSize"].value.toDouble(); + builder->commonMinMax = commonMinMax; + + builder->nworkers = QSettings().value("nworkers", 8).toInt(); + builder->samplingram = QSettings().value("ram", 512).toInt(); + + builder->samplingram = (*this)["ram"].value.toInt(); + builder->type = Rti::Type((*this)["type"].value.toInt()); + builder->colorspace = Rti::ColorSpace((*this)["colorspace"].value.toInt()); + builder->nplanes = (*this)["nplanes"].value.toInt(); + builder->yccplanes[0] = (*this)["yplanes"].value.toInt(); + //builder->sigma = + + if( builder->colorspace == Rti::MYCC) { + builder->yccplanes[1] = builder->yccplanes[2] = (builder->nplanes - builder->yccplanes[0])/2; + builder->nplanes = builder->yccplanes[0] + 2*builder->yccplanes[1]; + } + + imageset.images = (*this)["images"].value.toStringList(); + QList qlights = (*this)["lights"].value.toList(); + std::vector lights(qlights.size()/3); + for(int i = 0; i < qlights.size(); i+= 3) + for(int k = 0; k < 3; k++) + lights[i/3][k] = qlights[i+k].toDouble(); + builder->lights = imageset.lights = lights; + imageset.light3d = project.dome.lightConfiguration != Dome::DIRECTIONAL; + imageset.dome_radius = project.dome.domeDiameter/2.0; + imageset.vertical_offset = project.dome.verticalOffset; + imageset.initLights(); + imageset.initImages(input_folder.toStdString().c_str()); + + + if(hasParameter("crop")) { + QRect rect = (*this)["crop"].value.toRect(); + builder->crop[0] = rect.left(); + builder->crop[1] = rect.top(); + builder->crop[2] = rect.width(); + builder->crop[3] = rect.height(); + imageset.crop(rect.left(), rect.top(), rect.width(), rect.height()); + } + builder->width = imageset.width; + builder->height = imageset.height; + int quality= (*this)["quality"].value.toInt(); + + std::function callback = [this](QString s, int n)->bool { return this->progressed(s, n); }; + + try { + if(!builder->init(&callback)) { + error = builder->error.c_str(); + status = FAILED; + return; + } + if(saveLegacy) { + if(builder->type == Rti::HSH) + builder->saveUniversal(output.toStdString()); + else if(builder->type == Rti::PTM) + builder->savePTM(output.toStdString()); + else + throw "Legacy RTI and PTM formats are supported only for HSH and PTM basis"; + } else + builder->save(output.toStdString(), quality); + + } catch(std::string e) { + error = e.c_str(); + status = STOPPED; + return; + } +} +*/ + +/* not used anymore: build temporary rti and convert to legacy format */ +/* void RtiTask::toRTI() { + QString filename = output; + QTemporaryDir tmp; + if(!tmp.isValid()) { + cerr << "OOOPSS" << endl; + return; + } + output = tmp.path(); + relight(true); + try { + convertToRTI(tmp.filePath("info.json").toLatin1().data(), filename.toLatin1().data()); + } catch(QString err) { + error = err; + status = FAILED; + } +} */ + +/* +void RtiTask::fromRTI() { + QString input = (*this)["input"].value.toString(); + int quality= (*this)["quality"].value.toInt(); + try { + convertRTI(input.toLatin1().data(), output.toLatin1().data(), quality); + } catch(QString err) { + error = err; + status = FAILED; + } +} */ + + +void RtiTask::openlime() { + QStringList files = QStringList() << ":/demo/index.html" + << ":/demo/openlime.min.js" + << ":/demo/skin.css" + << ":/demo/skin.svg"; + QDir dir(output); + for(QString file: files) { + QFile fp(file); + fp.open(QFile::ReadOnly); + QFileInfo info(file); + QFile copy(dir.filePath(info.fileName())); + copy.open(QFile::WriteOnly); + copy.write(fp.readAll()); + } +} diff --git a/relightlab/rtitask.h b/relightlab/rtitask.h new file mode 100644 index 00000000..cbf2b0f7 --- /dev/null +++ b/relightlab/rtitask.h @@ -0,0 +1,57 @@ +#ifndef RTITASK_H +#define RTITASK_H + +#include "task.h" +#include "../src/project.h" +#include "../src/rti.h" +#include + +class RtiBuilder; + +class RtiParameters { +public: + + enum Format { RTI = 0, WEB = 1, IIP = 2 }; + enum WebLayout { PLAIN = 0, DEEPZOOM = 1, TARZOOM = 2, ITARZOOM = 3 }; + + Rti::Type basis = Rti::PTM; + Rti::ColorSpace colorspace = Rti::RGB; + int nplanes = 18; + int nchroma = 0; + + Format format = WEB; + WebLayout web_layout = PLAIN; + + bool lossless = false; //used only for RTI format; + + bool iiif_manifest = false; //TODO + bool openlime = true; //include openlime viewer //TODO: might want different interfaces. + + int quality = 95; + QString path; +}; + +class RtiTask: public Task { + Q_OBJECT +public: + Project project; + RtiParameters parameters; + QRect crop; + + RtiTask(const Project &_project); + virtual ~RtiTask(); + virtual void run() override; + +public slots: + + //void relight(bool commonMinMax = false, bool saveLegacy = false); //use true for .rti and .ptm + //void toRTI(); + //void fromRTI(); + void openlime(); + +private: + RtiBuilder *builder = nullptr; + +}; + +#endif // RTITASK_H diff --git a/relightlab/scaleframe.cpp b/relightlab/scaleframe.cpp new file mode 100644 index 00000000..365640ea --- /dev/null +++ b/relightlab/scaleframe.cpp @@ -0,0 +1,204 @@ +#include "scaleframe.h" +#include "imageview.h" +#include "relightapp.h" +#include "../src/measure.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +ScaleFrame::ScaleFrame(QWidget *parent): QFrame(parent) { + + QVBoxLayout *content = new QVBoxLayout(this); + + QHBoxLayout *controls = new QHBoxLayout; + content->addLayout(controls); + + QPushButton *take = new QPushButton("Take a new measurement..."); + controls->addWidget(take); + + scale = new QDoubleSpinBox; + controls->addWidget(scale); + + pixelSize = new QLabel("mm"); + controls->addWidget(pixelSize); + + QPushButton *remove = new QPushButton(QIcon::fromTheme("trash-2"), ""); + controls->addWidget(remove); + + viewer = new ImageViewer; + viewer->showImage(0); + content->addWidget(viewer); + + QPainterPath path; + path.moveTo(-8, -8); + path.lineTo(-3, -3); + path.moveTo( 8, -8); + path.lineTo( 3, -3); + path.moveTo( 8, 8); + path.lineTo( 3, 3); + path.moveTo(-8, 8); + path.lineTo(-3, 3); + + first = new QGraphicsPathItem(path); + second = new QGraphicsPathItem(path); + line = new QGraphicsLineItem(); + text = new QGraphicsTextItem(); + QPen pen; + pen.setColor(Qt::yellow); + pen.setWidth(1); + + first->setPen(pen); + second->setPen(pen); + line->setPen(pen); + text->setDefaultTextColor(Qt::yellow); + text->setFont(QFont("Arial", 16)); + + + connect(take, SIGNAL(clicked()), this, SLOT(startPicking())); + connect(viewer->view, SIGNAL(clicked(QPoint)), this, SLOT(click(QPoint))); + connect(scale, SIGNAL(valueChanged(double)), this, SLOT(setLength(double))); + connect(remove, SIGNAL(clicked()), this, SLOT(removeScale())); +} + +ScaleFrame::~ScaleFrame() { + clear(); + + delete first; + delete second; + delete line; + delete text; +} + +void ScaleFrame::clear() { + auto &scene = viewer->scene(); + + if(first->scene()) + scene.removeItem(first); + if(second->scene()) + scene.removeItem(second); + if(line->scene()) + scene.removeItem(line); + if(text->scene()) + scene.removeItem(text); + scale->setValue(0.0); + status = NOTHING; +} + +void ScaleFrame::init() { + viewer->showImage(0); + + Project &project = qRelightApp->project(); + if(project.measures.size()) { + Measure *measure = project.measures[0]; + setFirst(measure->first); + setSecond(measure->second); + setLengthLabel(measure->length); + status = DONE; + } +} + +void ScaleFrame::removeScale() { + QMessageBox::StandardButton proceed = QMessageBox::question(this, "Removing measurement", "You are removing a measurement. Proceed?"); + if(proceed != QMessageBox::Yes) + return; + + Project &project = qRelightApp->project(); + for(Measure *m: project.measures) + delete m; + project.measures.clear(); + + clear(); +} + +void ScaleFrame::startPicking() { + if(status == DONE) { + QMessageBox::StandardButton proceed = QMessageBox::question(this, "Taking a new measurement", "You are removing a measurement and taking a new one. Proceed?"); + if(proceed != QMessageBox::Yes) + return; + } + clear(); + status = FIRST_POINT; + QApplication::setOverrideCursor(Qt::CrossCursor); +} + +void ScaleFrame::cancelPicking() { + clear(); + QApplication::restoreOverrideCursor(); +} + +void ScaleFrame::click(QPoint q) { + QPointF p = viewer->view->mapToScene(q); + + if(status == FIRST_POINT) { + setFirst(p); + status = SECOND_POINT; + } else if(status == SECOND_POINT) { + setSecond(p); + status = DONE; + QApplication::restoreOverrideCursor(); + } + +} + +void ScaleFrame::setFirst(QPointF p) { + auto &scene = viewer->scene(); + + first->setPos(p); + scene.addItem(first); +} + +void ScaleFrame::setSecond(QPointF p) { + auto &scene = viewer->scene(); + + second->setPos(p); + line->setLine(QLineF(first->pos(), p)); + text->setPos((first->pos() + second->pos())/2); + + scene.addItem(second); + scene.addItem(line); +} + +void ScaleFrame::setLengthLabel(double length) { + auto &scene = viewer->scene(); + + text->setPlainText(QString::number(length) + "mm"); + text->setPos((first->pos() + second->pos())/2); + if(!text->scene()) + scene.addItem(text); + + scale->setValue(length); + pixelSize->setText(QString("Pixel size in mm: %1").arg(qRelightApp->project().pixelSize)); + +} + +void ScaleFrame::setLength(double length) { + auto &scene = viewer->scene(); + + text->setPlainText(QString::number(length) + "mm"); + if(!text->scene()) + scene.addItem(text); + + Project &project = qRelightApp->project(); + for(Measure *m: project.measures) + delete m; + project.measures.clear(); + + Measure *measure = new Measure; + measure->first = first->pos(); + measure->second = second->pos(); + measure->length = scale->value(); + project.measures.push_back(measure); + project.computePixelSize(); + pixelSize->setText(QString("Pixel size in mm: %1").arg(qRelightApp->project().pixelSize)); + emit pixelSizeChanged(); +} + + diff --git a/relightlab/scaleframe.h b/relightlab/scaleframe.h new file mode 100644 index 00000000..6a366f3d --- /dev/null +++ b/relightlab/scaleframe.h @@ -0,0 +1,53 @@ +#ifndef SCALEFRAME_H +#define SCALEFRAME_H + +#include + +class ImageViewer; +class QDoubleSpinBox; +class QLabel; +class QGraphicsPathItem; +class QGraphicsLineItem; +class QGraphicsTextItem; + + +/* mark a segment and specify a distance */ + + +class ScaleFrame: public QFrame { + Q_OBJECT +public: + ScaleFrame(QWidget *parent = nullptr); + ~ScaleFrame(); + void clear(); + void init(); + void setFirst(QPointF p); + void setSecond(QPointF p); + void setLengthLabel(double length); + +public slots: + void click(QPoint p); + void startPicking(); + void cancelPicking(); + void removeScale(); + void setLength(double length); //called when user modified the value in the spinbox. + +signals: + void pixelSizeChanged(); + + +protected: + enum Measuring { NOTHING = 0, FIRST_POINT = 1, SECOND_POINT = 2, DONE = 3 }; + Measuring status; + + QDoubleSpinBox *scale = nullptr; + QLabel *pixelSize = nullptr; + ImageViewer *viewer = nullptr; + + QGraphicsPathItem *first = nullptr; + QGraphicsPathItem *second = nullptr; + QGraphicsLineItem *line = nullptr; + QGraphicsTextItem *text = nullptr; +}; + +#endif // SCALEFRAME_H diff --git a/relightlab/spheredialog.cpp b/relightlab/spheredialog.cpp new file mode 100644 index 00000000..881d8328 --- /dev/null +++ b/relightlab/spheredialog.cpp @@ -0,0 +1,47 @@ +#include "spheredialog.h" +#include "spherepicking.h" +#include "imageview.h" +#include "../src/sphere.h" + +#include +#include +#include + +SphereDialog::SphereDialog(QWidget *parent): QDialog(parent) { + setModal(true); + sphere_picking = new SpherePicking; + sphere_picking->showImage(0); + QVBoxLayout *content = new QVBoxLayout(this); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + content->addWidget(sphere_picking); + content->addWidget(buttonBox); + showMaximized(); +} + +void SphereDialog::setSphere(Sphere *sphere) { + sphere_picking->setSphere(sphere); + sphere_picking->fit(); +} + + +void SphereDialog::accept() { + if(sphere_picking->sphere->border.size() < 3) { + QMessageBox::information(this, "Not enought points", "At least 3 points are needed to fit a circle"); + return; + } + if(!sphere_picking->sphere->fitted) { + QMessageBox::information(this, "Sphere not fit", "The sphere could not be fit, try to remove and add back the points"); + return; + } + //TODO check for a valid (inside the image) inner rectangle. + QDialog::accept(); +} + +void SphereDialog::reject() { + QDialog::reject(); +} diff --git a/relightlab/spheredialog.h b/relightlab/spheredialog.h new file mode 100644 index 00000000..8e0aebc6 --- /dev/null +++ b/relightlab/spheredialog.h @@ -0,0 +1,23 @@ +#ifndef SPHEREDIALOG_H +#define SPHEREDIALOG_H + +#include + +class SpherePicking; +class Sphere; + +class SphereDialog: public QDialog { + Q_OBJECT +public: + SphereDialog(QWidget *parent = nullptr); + void setSphere(Sphere *sphere); + +public slots: + void accept(); + void reject(); + +private: + SpherePicking *sphere_picking = nullptr; +}; + +#endif // SPHEREDIALOG_H diff --git a/relightlab/spheremarker.cpp b/relightlab/spheremarker.cpp new file mode 100644 index 00000000..e69de29b diff --git a/relightlab/spheremarker.h b/relightlab/spheremarker.h new file mode 100644 index 00000000..e69de29b diff --git a/relightlab/spherepanel.cpp b/relightlab/spherepanel.cpp new file mode 100644 index 00000000..a50b1947 --- /dev/null +++ b/relightlab/spherepanel.cpp @@ -0,0 +1,103 @@ +#include "spherepanel.h" +#include "sphererow.h" +#include "spheredialog.h" +#include "spherepicking.h" +#include "relightapp.h" +#include "../src/sphere.h" + +#include +#include +#include +#include + +#include + +SpherePanel::SpherePanel(QWidget *parent): QGroupBox("Reflective spheres", parent) { + //setFrameShape(QFrame::Box); + QVBoxLayout *content = new QVBoxLayout(this); + + content->addSpacing(10); + + QFrame *spheres_frame = new QFrame; + + QScrollArea *sphere_area = new QScrollArea; + sphere_area->setWidget(spheres_frame); + sphere_area->setWidgetResizable(true); + + content->addWidget(sphere_area); + + spheres = new QVBoxLayout(spheres_frame); + + QHBoxLayout *buttons = new QHBoxLayout; + content->addLayout(buttons); +} + +void SpherePanel::clear() { + setVisible(false); + while(spheres->count() > 0) { + QLayoutItem *item = spheres->takeAt(0); + SphereRow *row = dynamic_cast(item->widget()); + row->stopDetecting(); + delete row; + } +} + +void SpherePanel::init() { + auto &project_spheres = qRelightApp->project().spheres; + setVisible(project_spheres.size() > 0); + for(Sphere *sphere: project_spheres) { + sphere->fit(); + SphereRow * row = addSphere(sphere); + row->detectHighlights(false); + } +} + +/* on user button press */ +void SpherePanel::newSphere() { + if(!sphere_dialog) + sphere_dialog = new SphereDialog(this); + + //TODO ACTUALLY images might be skipped! + Sphere *sphere = new Sphere(qRelightApp->project().images.size()); + sphere_dialog->setSphere(sphere); + int answer = sphere_dialog->exec(); + if(answer == QDialog::Rejected) { + delete sphere; + return; + } + qRelightApp->project().spheres.push_back(sphere); + SphereRow *row = addSphere(sphere); + row->detectHighlights(); +} + +SphereRow *SpherePanel::addSphere(Sphere *sphere) { + setVisible(true); + SphereRow *row = new SphereRow(sphere); + spheres->addWidget(row); + + connect(row, SIGNAL(removeme(SphereRow *)), this, SLOT(removeSphere(SphereRow *))); + connect(row, SIGNAL(updated()), this, SIGNAL(updated())); + return row; +} + +void SpherePanel::removeSphere(SphereRow *row) { + layout()->removeWidget(row); + + row->stopDetecting(); + + Sphere *sphere = row->sphere; + auto &spheres = qRelightApp->project().spheres; + + + auto it = std::find(spheres.begin(), spheres.end(), sphere); + + assert(it != spheres.end()); + + delete sphere; + delete row; + + spheres.erase(it); + setVisible(spheres.size()); + +} + diff --git a/relightlab/spherepanel.h b/relightlab/spherepanel.h new file mode 100644 index 00000000..acb123af --- /dev/null +++ b/relightlab/spherepanel.h @@ -0,0 +1,37 @@ +#ifndef SPHEREPANEL_H +#define SPHEREPANEL_H + +#include +#include +#include "../src/dome.h" + +class SphereDialog; +class Sphere; +class QVBoxLayout; +class SphereRow; +class Dome; + + +class SpherePanel: public QGroupBox { + Q_OBJECT +public: + SpherePanel(QWidget *parent = nullptr); + void clear(); + void init(); + SphereRow *addSphere(Sphere *sphere); + +public slots: + void newSphere(); + void removeSphere(SphereRow *sphere); + +signals: + void updated(); + +private: + SphereDialog *sphere_dialog = nullptr; + QVBoxLayout *spheres = nullptr; +}; + + + +#endif // SPHEREPANEL_H diff --git a/relightlab/spherepicking.cpp b/relightlab/spherepicking.cpp new file mode 100644 index 00000000..dcc336e7 --- /dev/null +++ b/relightlab/spherepicking.cpp @@ -0,0 +1,273 @@ +#include "spherepicking.h" +#include "canvas.h" +#include "../src/sphere.h" + +#include +#include +#include +#include +#include +#include +#include + +SpherePicking::SpherePicking(QWidget *parent): ImageViewer(parent) { + + //status->showMessage("Double click on the boundary of the sphere."); + + + //create shpere marking items. + QPen outlinePen(Qt::yellow); + outlinePen.setCosmetic(true); + circle = new QGraphicsEllipseItem(0, 0, 1, 1); + circle->setPen(outlinePen); + + axis[0] = new QGraphicsLineItem(0, 0, 1, 1); + axis[1] = new QGraphicsLineItem(0, 0, 1, 1); + + QVector dashes; + dashes << 4 << 4; + outlinePen.setDashPattern(dashes); + smallcircle = new QGraphicsEllipseItem(0, 0, 1, 1); + smallcircle->setPen(outlinePen); + + highlight = new HighlightPoint(this, -4, -4, 4, 4); + + QPen pen(Qt::red); + pen.setWidth(2); + pen.setCosmetic(true); + highlight->setPen(pen); + highlight->setBrush(Qt::red); + highlight->setBrush(Qt::green); + highlight->setFlag(QGraphicsItem::ItemIsMovable); + highlight->setFlag(QGraphicsItem::ItemIsSelectable); + highlight->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + + + scene().addItem(circle); + scene().addItem(smallcircle); + scene().addItem(highlight); + scene().addItem(axis[0]); + scene().addItem(axis[1]); + + connect(view, SIGNAL(clicked(QPoint)), this, SLOT(click(QPoint))); + //TODO: rename something, it conflicts! + view->setCursor(Qt::CrossCursor); + + //QApplication::setOverrideCursor(Qt::CrossCursor); + +} + +void SpherePicking::showImage(int id) { + ImageViewer::showImage(id); + + //status->showMessage("Double click on the boundary of the sphere."); +} + + +/*void SpherePicking::updatePoint(QGraphicsEllipseItem *point) { + updatePoint(point); + sphere->fit(); + int sphere_id = project.indexOf(m->sphere); + assert(sphere_id != -1); + history.push(Action(Action::MOVE_BORDER, sphere_id, *(m->sphere))); + } +}*/ + + +QVariant BorderPoint::itemChange(GraphicsItemChange change, const QVariant &value) { + if ((change == ItemPositionChange && scene()) || change == ItemScenePositionHasChanged) { + picker->updateBorderPoints(); + } + return QGraphicsItem::itemChange(change, value); +} + +HighlightPoint::~HighlightPoint() {} + +QVariant HighlightPoint::itemChange(GraphicsItemChange change, const QVariant &value) { + if ((change == ItemPositionChange && scene()) || change == ItemScenePositionHasChanged) { + picker->updateHighlightPosition(); + } + return QGraphicsItem::itemChange(change, value); +} + +void SpherePicking::clear() { + circle->setVisible(false); + smallcircle->setVisible(false); + axis[0]->setVisible(false); + axis[1]->setVisible(false); + highlight->setVisible(false); + for(BorderPoint *b: border) { + scene().removeItem(b); + delete b; + } + border.clear(); +} + +void SpherePicking::setSphere(Sphere *s) { + sphere = s; + + //TODO uupdatescene, not set sphere + clear(); + + for(QPointF pos: sphere->border) + addBorderPoint(pos); + + updateSphere(); + + showImage(0); + fit(); +} + +void SpherePicking::updateSphere() { + if(sphere->center.isNull()) + return; + + QPointF c = sphere->center; + double R = double(sphere->radius); + double r = double(sphere->smallradius); + if(sphere->ellipse) { + double w = sphere->eWidth; + double h = sphere->eHeight; + circle->setRect(c.x() - w, c.y() - h, 2*w, 2*h); + circle->setTransformOriginPoint(c); + circle->setRotation(sphere->eAngle); + QPointF dir = { cos(sphere->eAngle*M_PI/180), sin(sphere->eAngle*M_PI/180) }; + axis[0]->setLine(c.x(), c.y(), c.x() + dir.x()*sphere->eWidth, c.y() + dir.y()*sphere->eWidth); + axis[1]->setLine(c.x(), c.y(), c.x() - dir.y()*sphere->eHeight, c.y() + dir.x()*sphere->eHeight); + + w *= sphere->smallradius/sphere->radius; + h *= sphere->smallradius/sphere->radius; + smallcircle->setRect(c.x() - w, c.y() - h, 2*w, 2*h); + smallcircle->setTransformOriginPoint(c); + smallcircle->setRotation(sphere->eAngle); + smallcircle->setVisible(true); + + } else { + circle->setRect(c.x()-R, c.y()-R, 2*R, 2*R); + smallcircle->setRect(c.x()-r, c.y()-r, 2*r, 2*r); + smallcircle->setVisible(true); + } + circle->setVisible(true); + +} + +void SpherePicking::fitSphere() { + if(sphere->border.size() < 3) + sphere->center = QPointF(); + else + sphere->fit(); + + updateSphere(); +} + + +void SpherePicking::click(QPoint p) { + QPointF pos = view->mapToScene(p); + + //min distance between border points in pixels. + double minBorderDist = 20; + for(QPointF p: sphere->border) { + if(sqrt(pow(p.x() - pos.x(), 2) + pow(p.y() - pos.y(), 2)) < minBorderDist) + return; + } + + addBorderPoint(pos); + sphere->border.push_back(pos); + fitSphere(); +} + +void SpherePicking::addBorderPoint(QPointF pos) { + + QBrush blueBrush(Qt::white); + QPen outlinePen(Qt::white); + outlinePen.setWidth(5); + outlinePen.setCosmetic(true); + + auto borderPoint = new BorderPoint(this, -3, -3, 6, 6); + borderPoint->setPos(pos); + borderPoint->setPen(outlinePen); + borderPoint->setBrush(blueBrush); + border.push_back(borderPoint); + scene().addItem(borderPoint); +} + +void SpherePicking::updateBorderPoints() { + for(size_t i = 0; i < border.size(); i++) { + sphere->border[i] = border[i]->pos(); + } + fitSphere(); +} + + + +void SpherePicking::showHighlight(size_t n) { + highlight->setVisible(sphere->fitted); + + if(!sphere->fitted) + return; + + highlight->setVisible(true); + QPen pen = highlight->pen(); + + if(!sphere->lights[n].isNull()) { + highlight->setPos(sphere->lights[n]); + highlight->setBrush(Qt::green); + pen.setColor(Qt::green); + } else { + highlight->setPos(sphere->inner.center()); + highlight->setBrush(Qt::red); + pen.setColor(Qt::red); + } + highlight->setPen(pen); +} + +void SpherePicking::updateHighlightPosition() { + highlight->setBrush(Qt::green); + QPen pen = highlight->pen(); + pen.setColor(Qt::green); + highlight->setPen(pen); + sphere->lights[this->currentImage()] = highlight->pos(); +} + +void SpherePicking::deleteSelected(int currentImage) { + size_t j = 0; + for(size_t i = 0; i < border.size(); i++, j++) { + if(i != j) { + border[j] = border[i]; + sphere->border[j] = sphere->border[i]; + } + if(border[i]->isSelected()) { + scene().removeItem(border[i]); + delete border[i]; + j--; + } + } + border.resize(j); + sphere->border.resize(j); + + if(highlight->isSelected()) { + sphere->resetHighlight(currentImage); + showHighlight(currentImage); + } + fitSphere(); +} + +void SpherePicking::keyReleaseEvent(QKeyEvent *e) { + switch(e->key()) { + case Qt::Key_Backspace: + case Qt::Key_Delete: + deleteSelected(this->currentImage()); + return; + case Qt::Key_Z: + if((e->modifiers() | Qt::ControlModifier) && border.size()) { + scene().removeItem(border.back()); + delete border.back(); + border.pop_back(); + sphere->border.pop_back(); + fitSphere(); + return; + } + break; + } + ImageViewer::keyPressEvent(e); +} diff --git a/relightlab/spherepicking.h b/relightlab/spherepicking.h new file mode 100644 index 00000000..1fdddc19 --- /dev/null +++ b/relightlab/spherepicking.h @@ -0,0 +1,96 @@ +#ifndef SPHERE_PICKING_H +#define SPHERE_PICKING_H + +#include "imageview.h" + +#include +#include +#include + +class Canvas; +class QGraphicsEllipseItem; +class QGraphicsLineItem; +class SpherePicking; + +class BorderPoint: public QGraphicsEllipseItem { +public: + BorderPoint(SpherePicking *_picker, qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent = Q_NULLPTR): + QGraphicsEllipseItem(x, y, w, h, parent), picker(_picker) { + setCursor(Qt::CrossCursor); + setFlag(QGraphicsItem::ItemIsMovable); + setFlag(QGraphicsItem::ItemIsSelectable); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + + } + virtual ~BorderPoint() {} + +protected: + SpherePicking *picker; + QVariant itemChange(GraphicsItemChange change, const QVariant &value); +}; + +class HighlightPoint: public QGraphicsPathItem { +public: + HighlightPoint(SpherePicking *_picker, qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent = Q_NULLPTR): + QGraphicsPathItem(parent), picker(_picker) { + QPainterPath path; + path.moveTo(x, y); + path.lineTo(w, h); + path.moveTo(x, h); + path.lineTo(w, y); + setPath(path); + } + virtual ~HighlightPoint(); + +protected: + SpherePicking *picker; + QVariant itemChange(GraphicsItemChange change, const QVariant &value); +}; + +class Sphere; + +class SpherePicking: public ImageViewer { + Q_OBJECT +public: + Sphere *sphere = nullptr; + + std::vector border; + QGraphicsEllipseItem *circle = nullptr; + QGraphicsEllipseItem *smallcircle = nullptr; + QGraphicsLineItem *axis[2] = { nullptr, nullptr }; + HighlightPoint *highlight = nullptr; + QGraphicsPixmapItem *pixmap = nullptr; + + + SpherePicking(QWidget *parent = nullptr); + void setSphere(Sphere *sphere); + void updateSphere(); + void clear(); + + void showHighlight(size_t n); + void updateHighlightPosition(); + + void addBorderPoint(QPointF pos); + void updateBorderPoints(); + + void deleteSelected(int currentImage); + void fitSphere(); + +/* virtual void doubleClick(QPointF pos) { click(pos); } + + virtual void cancelEditing() {} + + virtual void setSelected(bool value); */ + + +public slots: + void click(QPoint); + //void cancel(); + //void accept(); + void showImage(int id); + +protected: + void keyReleaseEvent(QKeyEvent *e); +}; + +#endif diff --git a/relightlab/sphererow.cpp b/relightlab/sphererow.cpp new file mode 100644 index 00000000..adc01e0b --- /dev/null +++ b/relightlab/sphererow.cpp @@ -0,0 +1,145 @@ +#include "sphererow.h" +#include "relightapp.h" +#include "spheredialog.h" +#include "verifydialog.h" +#include "reflectionview.h" +#include "../src/project.h" +#include "../src/sphere.h" +#include "processqueue.h" + +#include +#include +#include +#include +#include + +DetectHighlights::DetectHighlights(Sphere *_sphere, bool update) { + sphere = _sphere; + update_positions = update; + visible = false; + label = "Detecting sphere highlights."; +} + +void DetectHighlights::run() { + mutex.lock(); + status = RUNNING; + mutex.unlock(); + + Project &project = qRelightApp->project(); + for(size_t i = 0; i < project.images.size(); i++) { + + Image &image = project.images[i]; + if(image.skip) continue; + + QImage img(image.filename); + sphere->findHighlight(img, i, update_positions); + + int progress = std::min(99, (int)(100*(i+1) / project.images.size())); + progressed(QString("Detecting highlights"), progress); + } + progressed(QString("Done."), 100); + mutex.lock(); + status = DONE; + mutex.unlock(); +} + + +SphereRow::SphereRow(Sphere *_sphere, QWidget *parent): QWidget(parent) { + sphere = _sphere; + QHBoxLayout *columns = new QHBoxLayout(this); + columns->setSpacing(20); + + position = new PositionView(sphere, rowHeight); + position->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + columns->addWidget(position); + + + reflections = new ReflectionView(sphere, rowHeight); + reflections->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + columns->addWidget(reflections); + + //QVBoxLayout *status_layout = new QVBoxLayout; + //columns->addLayout(status_layout, 2); + //status_layout->addStretch(); + //status = new QLabel("Locating highlights..."); + //status_layout->addWidget(status); + progress = new QProgressBar; + progress->setValue(0); + //status_layout->addWidget(progress); + //status_layout->addStretch(); + columns->addWidget(progress, 2); + + QPushButton *edit = new QPushButton(QIcon::fromTheme("edit"), "Edit..."); + columns->addWidget(edit, 1); + QPushButton *verify = new QPushButton(QIcon::fromTheme("check"), "Verify..."); + columns->addWidget(verify, 1); + QPushButton *remove = new QPushButton(QIcon::fromTheme("trash-2"), "Delete"); + columns->addWidget(remove, 1); + + connect(edit, SIGNAL(clicked()), this, SLOT(edit())); + connect(remove, SIGNAL(clicked()), this, SLOT(remove())); + connect(verify, SIGNAL(clicked()), this, SLOT(verify())); + +} +void SphereRow::edit() { + SphereDialog *sphere_dialog = new SphereDialog(this); + sphere_dialog->setSphere(sphere); + int answer = sphere_dialog->exec(); + if(answer == QDialog::Accepted) { + position->update(); + reflections->init(); + detectHighlights(); + } +} + +void SphereRow::verify() { + std::vector &positions = sphere->lights; + for(QPointF &pos: positions) + pos -= sphere->inner.topLeft(); + + VerifyDialog *verify_dialog = new VerifyDialog(sphere->thumbs, positions, this); + verify_dialog->exec(); + + for(QPointF &pos: positions) + pos += sphere->inner.topLeft(); +} + +void SphereRow::remove() { + emit removeme(this); +} + +void SphereRow::updateStatus(QString msg, int percent) { +// status->setText(msg); + progress->setValue(percent); + reflections->update(); + if(percent == 100) { + emit updated(); + } +} + +void SphereRow::detectHighlights(bool update) { + if(sphere->center.isNull()) { +// status->setText("Needs at least 3 points."); + return; + } + if(!detect_highlights) { + detect_highlights = new DetectHighlights(sphere, update); + connect(detect_highlights, &DetectHighlights::progress, this, &SphereRow::updateStatus); //, Qt::QueuedConnection); + } + detect_highlights->stop(); + + ProcessQueue &queue = ProcessQueue::instance(); + queue.removeTask(detect_highlights); + queue.addTask(detect_highlights); + queue.start(); +} + +void SphereRow::stopDetecting() { + if(detect_highlights) { + if(detect_highlights->isRunning()) { + detect_highlights->stop(); + detect_highlights->wait(); + } + detect_highlights->deleteLater(); + } +} diff --git a/relightlab/sphererow.h b/relightlab/sphererow.h new file mode 100644 index 00000000..c6884f18 --- /dev/null +++ b/relightlab/sphererow.h @@ -0,0 +1,51 @@ +#ifndef SPHEREROW_H +#define SPHEREROW_H + +#include "task.h" +#include + + + +class Sphere; +class QLabel; +class QProgressBar; +class ReflectionView; +class PositionView; + +class DetectHighlights: public Task { +public: + Sphere *sphere; + bool update_positions; + + DetectHighlights(Sphere *sphere, bool update = true); + virtual void run() override; + +}; + +class SphereRow: public QWidget { + Q_OBJECT +public: + Sphere *sphere; + int rowHeight = 92; + PositionView *position; + ReflectionView *reflections; + QLabel *status = nullptr; + QProgressBar *progress = nullptr; + DetectHighlights *detect_highlights = nullptr; + + SphereRow(Sphere *sphere, QWidget *parent = nullptr); + void detectHighlights(bool update = true); + void stopDetecting(); + +signals: + void removeme(SphereRow *row); + void updated(); //emit when status changes + +public slots: + void edit(); + void remove(); + void verify(); + void updateStatus(QString msg, int percent); +}; + +#endif // SPHEREROW_H diff --git a/relightlab/tabwidget.cpp b/relightlab/tabwidget.cpp new file mode 100644 index 00000000..b96f1b25 --- /dev/null +++ b/relightlab/tabwidget.cpp @@ -0,0 +1,36 @@ +#include "tabwidget.h" + +QSize TabBar::tabSizeHint(int index) const{ + QSize s = QTabBar::tabSizeHint(index); + s.transpose(); + return s; +} +void TabBar::paintEvent(QPaintEvent * /*event*/){ + QStylePainter painter(this); + QStyleOptionTab opt; + + for(int i = 0;i < count();i++) + { + initStyleOption(&opt,i); + painter.drawControl(QStyle::CE_TabBarTabShape, opt); + painter.save(); + + QSize s = opt.rect.size(); + s.transpose(); + QRect r(QPoint(), s); + r.moveCenter(opt.rect.center()); + opt.rect = r; + + QPoint c = tabRect(i).center(); + painter.translate(c); + painter.rotate(90); + painter.translate(-c); + painter.drawControl(QStyle::CE_TabBarTabLabel,opt); + painter.restore(); + } +} + +TabWidget::TabWidget(QWidget *parent):QTabWidget(parent) { + setTabBar(new TabBar); + setTabPosition(QTabWidget::West); +} diff --git a/relightlab/tabwidget.h b/relightlab/tabwidget.h new file mode 100644 index 00000000..05db8c12 --- /dev/null +++ b/relightlab/tabwidget.h @@ -0,0 +1,23 @@ +#ifndef TABWIDGET_H +#define TABWIDGET_H + +#include +#include +#include +#include + +class TabBar: public QTabBar { +public: + QSize tabSizeHint(int index) const; + +protected: + void paintEvent(QPaintEvent * /*event*/); +}; + +class TabWidget: public QTabWidget { +public: + TabWidget(QWidget *parent = nullptr); +}; + + +#endif // TABWIDGET_H diff --git a/relightlab/task.cpp b/relightlab/task.cpp new file mode 100644 index 00000000..af74f815 --- /dev/null +++ b/relightlab/task.cpp @@ -0,0 +1,124 @@ +#include "task.h" + +#include +#include +#include +#include +#include +#include + +#include +using namespace std; + +void Task::runPythonScript(QString script, QStringList arguments, QString working) { + QString python = QSettings().value("python_path").toString(); + if(python.isNull()) { + error = "Python executable not set. See File -> Preferences -> Scripts."; + status = FAILED; + return; + } + runScript(python, script, arguments, working); +} + +void Task::runScript(QString program, QString script, QStringList arguments, QString working) { + QString scripts_path = QSettings().value("scripts_path").toString(); + + if(scripts_path.isEmpty()) + QSettings().value("tmp_scripts_path").toString(); + + QDir dir(scripts_path); + if(!dir.exists()) { + + error = "Could not find script folder: " + dir.path(); + status = FAILED; + return; + } + + QString script_path = dir.filePath(script); + QFileInfo info(script_path); + if(!info.exists()) { + error = "Could not find the script: " + script_path; + status = FAILED; + } + + + QProcess process; + if(!working.isNull()) + process.setWorkingDirectory(working); + process.setProgram(program); + process.setArguments(QStringList() << script_path << arguments); + process.start(); + + + QRegularExpression re("([^:]+):\\ (\\d+)\\%"); + + do { + QString err = process.readAllStandardError(); + QString out = process.readAllStandardOutput(); + log.append(err); + log.append(out); + + if(!out.isEmpty()) { + QString line = out.split("\n").front(); + int pos = line.indexOf(re); + + if(pos >= 0) { + QString text = re.match(line).capturedTexts()[1]; + int percent = re.match(line).capturedTexts()[2].toInt(); + emit progress(text, percent); + } + } + if(err.size()) + qDebug() << "Err: " << qPrintable(err) << "\n"; + if(out.size()) + qDebug() << "Out: " << qPrintable(out) << "\n"; + if(status == PAUSED) { + qDebug() << "DOn't know how to pause a process!" << "\n"; + } + if(status == STOPPED) { + process.kill(); + process.waitForFinished(); + break; + } + } while(!process.waitForFinished(100)); + log.append(process.readAllStandardError()); + log.append(process.readAllStandardOutput()); + + if(process.exitStatus() == QProcess::NormalExit) { + status = DONE; + } else + status = FAILED; + error = log; +} + +void Task::pause() { + mutex.lock(); + status = PAUSED; +} + +void Task::resume() { + if(status == PAUSED) { + status = RUNNING; + mutex.unlock(); + } +} + +void Task::stop() { + if(status == PAUSED) { //we were already locked then. + status = STOPPED; + mutex.unlock(); + } + status = STOPPED; +} + + +bool Task::progressed(QString s, int percent) { + emit progress(s, percent); + if(status == PAUSED) { + mutex.lock(); //mutex should be already locked, this stalls the queue. + mutex.unlock(); + } + if(status == STOPPED) + return false; + return true; +} diff --git a/relightlab/task.h b/relightlab/task.h new file mode 100644 index 00000000..ae7410eb --- /dev/null +++ b/relightlab/task.h @@ -0,0 +1,43 @@ +#ifndef TASK_H +#define TASK_H + +#include +#include + + +class Task: public QThread { + Q_OBJECT +public: + int id = 0; + QString label; + bool visible = true; //visible on the queueframe. + + QString input_folder; + QString output; + + enum Status { ON_QUEUE = 0, RUNNING = 1, PAUSED = 2, STOPPED = 3, DONE = 4, FAILED = 5}; + Status status = ON_QUEUE; + QString error; + QString log; + + Task(QObject *parent = Q_NULLPTR): QThread(parent) {} + virtual ~Task() {} + virtual void stop(); + virtual void pause(); + virtual void resume(); + + + void runPythonScript(QString script, QStringList arguments, QString workingdir = QString()); + void runScript(QString program, QString script, QStringList arguments, QString workingdir = QString()); +public slots: + void test() {} + virtual bool progressed(QString str, int percent); + +signals: + void progress(QString str, int n); + +protected: + QMutex mutex; +}; + +#endif // TASK_H diff --git a/relightlab/verifydialog.cpp b/relightlab/verifydialog.cpp new file mode 100644 index 00000000..6936353a --- /dev/null +++ b/relightlab/verifydialog.cpp @@ -0,0 +1,39 @@ +#include "verifydialog.h" +#include "../src/sphere.h" +#include "flowlayout.h" +#include "relightapp.h" +#include "verifyview.h" + +#include +#include +#include +#include +#include +#include "assert.h" + + +VerifyDialog::VerifyDialog(std::vector &_thumbs, std::vector &_positions, QWidget *parent): + QDialog(parent), thumbs(_thumbs), positions(_positions) { + setModal(true); + + showMaximized(); + QVBoxLayout *layout = new QVBoxLayout(this); + QScrollArea *area = new QScrollArea(this); + layout->addWidget(area); + + area->setWidgetResizable(true); + area->setWidget(new QWidget); + flowlayout = new FlowLayout(); + area->widget()->setLayout(flowlayout); + + area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + for(size_t i = 0; i < thumbs.size(); i++) { + VerifyView *thumb = new VerifyView(thumbs[i], positions[i], 192); + flowlayout->addWidget(thumb); + } + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); + layout->addWidget(buttonBox); + connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); +} diff --git a/relightlab/verifydialog.h b/relightlab/verifydialog.h new file mode 100644 index 00000000..695e84f6 --- /dev/null +++ b/relightlab/verifydialog.h @@ -0,0 +1,21 @@ +#ifndef SPHEREVERIFY_H +#define SPHEREVERIFY_H + +#include + +class FlowLayout; +class Sphere; + + +class VerifyDialog: public QDialog { +public: + VerifyDialog(std::vector &thumbs, std::vector &positions, QWidget *parent = nullptr); + +private: + FlowLayout *flowlayout = nullptr; + std::vector &thumbs; + std::vector &positions; + +}; + +#endif // SPHEREVERIFY_H diff --git a/relightlab/verifyview.cpp b/relightlab/verifyview.cpp new file mode 100644 index 00000000..1630bdfa --- /dev/null +++ b/relightlab/verifyview.cpp @@ -0,0 +1,92 @@ +#include "verifyview.h" + +#include "reflectionview.h" +#include "relightapp.h" +#include "../src/sphere.h" + +#include +#include +#include +#include + +ReflectionPoint::ReflectionPoint(VerifyView *_view, qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent): + QGraphicsEllipseItem(x, y, w, h, parent), view(_view) { + init(); +} +ReflectionPoint::ReflectionPoint(VerifyView *_view, QRectF rect, QGraphicsItem *parent): + QGraphicsEllipseItem(rect, parent), view(_view) { + + init(); +} + +void ReflectionPoint::init() { + setCursor(Qt::CrossCursor); + setFlag(QGraphicsItem::ItemIsMovable); + setFlag(QGraphicsItem::ItemIsSelectable); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges); +} + +QVariant ReflectionPoint::itemChange(GraphicsItemChange change, const QVariant &value) { + if ((change == ItemPositionChange && scene()) ) { + + QPointF newPos = value.toPointF(); + QRectF rect = scene()->sceneRect(); + if (!rect.contains(newPos)) { + // Keep the item inside the scene rect. + newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left()))); + newPos.setY(qMin(rect.bottom(), qMax(newPos.y(), rect.top()))); + } + return newPos; + } + + if(change == ItemScenePositionHasChanged) { + view->updateReflection(); + } + return QGraphicsItem::itemChange(change, value); +} + +VerifyView:: VerifyView(QImage &_image, QPointF &_pos, int _height, QWidget *parent): + QGraphicsView(parent), image(_image), pos(_pos) { + height = _height; + setScene(&scene); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + QPixmap pix = QPixmap::fromImage(image);//.scaledToHeight(height); + img_item = scene.addPixmap(pix); + double scale = height/(double)pix.height(); + setFixedSize(pix.width()*scale, height); + + int r = 8*scale; + reflection = new ReflectionPoint(this, QRectF(QPointF(-r, -r), QSize(2*r, 2*r))); + if(pos.isNull()) { + reflection->setPos(image.width()/2, image.height()/2); + reflection->setPen(QPen(Qt::red, 2)); + + } else { + reflection->setPos(pos); + reflection->setPen(QPen(Qt::green, 2)); + } + scene.addItem(reflection); +} + +void VerifyView::updateReflection() { + QPointF p = reflection->pos(); + if(!img_item->boundingRect().contains(p)) { + reflection->setPos(image.width()/2, image.height()/2); + reflection->setPen(QPen(Qt::red, 2)); + pos = QPointF(0, 0); //order is important: setPos triggers again. + + } else { + pos = p; + reflection->setPen(QPen(Qt::green, 2)); + } +} + + +void VerifyView::resizeEvent(QResizeEvent *) { + fitInView(img_item->boundingRect()); //.sceneRect()); //img_item); +} + + diff --git a/relightlab/verifyview.h b/relightlab/verifyview.h new file mode 100644 index 00000000..ee90b002 --- /dev/null +++ b/relightlab/verifyview.h @@ -0,0 +1,46 @@ +#ifndef VERIFYVIEW_H +#define VERIFYVIEW_H + +#include +#include +#include + +class Sphere; +class VerifyView; + +class ReflectionPoint: public QGraphicsEllipseItem { +public: + ReflectionPoint(VerifyView *_view, QRectF rect, QGraphicsItem *parent = Q_NULLPTR); + ReflectionPoint(VerifyView *_view, qreal x, qreal y, qreal w, qreal h, QGraphicsItem *parent = Q_NULLPTR); + void init(); + virtual ~ReflectionPoint() {} + +protected: + VerifyView *view; + QVariant itemChange(GraphicsItemChange change, const QVariant &value); +}; + + +class VerifyView: public QGraphicsView { + Q_OBJECT +public: + double lightSize = 10.0; +//id is just image number + VerifyView(QImage &image, QPointF &pos, int height, QWidget *parent = nullptr); + +public slots: + void updateReflection(); + +protected: + void resizeEvent(QResizeEvent *event); + +private: + QGraphicsPixmapItem *img_item; + ReflectionPoint *reflection; + QGraphicsScene scene; + QImage ℑ + QPointF &pos; + int id; + int height; +}; +#endif // VERIFYVIEW_H diff --git a/scripts/build_stack.py b/scripts/build_stack.py index 9b48d112..7e4bbd1b 100755 --- a/scripts/build_stack.py +++ b/scripts/build_stack.py @@ -91,6 +91,7 @@ # Save as a stacked tiled pyramid TIFF using SubIFDs if dpi == 0.0: + stack.tiffsave( args.output, pyramid=True, subifd=True, compression=args.compression, Q=args.quality, tile=True, tile_width=args.tile, tile_height=args.tile ) diff --git a/src/jpeg_encoder.cpp b/src/jpeg_encoder.cpp index d4ede09e..9541ece9 100644 --- a/src/jpeg_encoder.cpp +++ b/src/jpeg_encoder.cpp @@ -47,7 +47,7 @@ void JpegEncoder::setChromaSubsampling(bool subsample) { } void JpegEncoder::setDotsPerMeter(float dotsPerMeter) { - this->dotsPerCM = round( dotsPerMeter / 100.0 ); // JPEG requires a resolution in pixels/cm + this->dotsPerCM = round( dotsPerMeter / 100.0 ); // JPEG requires a resolution in pixels/cm } @@ -90,8 +90,8 @@ bool JpegEncoder::encode(uint8_t* img, int width, int height) { info.optimize_coding = (boolean)optimize; // Set our output resolution if provided in pixels/cm - if(dotsPerCM>0) { - info.X_density = dotsPerCM; + if(dotsPerCM > 0) { + info.X_density = dotsPerCM; info.Y_density = dotsPerCM; info.density_unit = 2; // 2 = pixels per cm } diff --git a/src/jpeg_encoder.h b/src/jpeg_encoder.h index 37a09422..503e8bff 100644 --- a/src/jpeg_encoder.h +++ b/src/jpeg_encoder.h @@ -24,7 +24,7 @@ class JpegEncoder { int getQuality() const; void setOptimize(bool optimize); void setChromaSubsampling(bool subsample); - void setDotsPerMeter(float dotsPerMeter); + void setDotsPerMeter(float dotsPerMeter); bool encode(uint8_t *img, int width, int height, FILE* file); bool encode(uint8_t *img, int width, int height, const char* path); @@ -51,7 +51,7 @@ class JpegEncoder { bool subsample = false; int quality = 95; - int dotsPerCM = 0; + int dotsPerCM = 0; }; #endif // JPEGENCODER_H_ diff --git a/src/project.cpp b/src/project.cpp index 9f645b72..90859ccb 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -219,13 +219,13 @@ double mutualInfo(QImage &a, QImage &b) { } void Project::rotateImages(bool clockwise) { - QTransform rotate; - rotate.rotate(clockwise ? 90 : -90); - for(Image &image: images) { - QImage source(image.filename); - QImage rotated = source.transformed(rotate); - rotated.save(image.filename, "jpg", 100); - } + QTransform rotate; + rotate.rotate(clockwise ? 90 : -90); + for(Image &image: images) { + QImage source(image.filename); + QImage rotated = source.transformed(rotate); + rotated.save(image.filename, "jpg", 100); + } needs_saving = true; } @@ -261,7 +261,7 @@ void Project::rotateImages() { QTransform final = right_mutual > left_mutual ? rot_left : rot_right; //TODO should be libjpeg to rotate. QImage rotated = source.transformed(final); - rotated.save(image.filename, "jpg", 100); + rotated.save(image.filename, "jpg", 100); image.size = imgsize; image.valid = true; @@ -369,7 +369,7 @@ void Project::checkMissingImages() { } } void Project::checkImages() { - for(Image &image:images) { + for(Image &image:images) { QImageReader reader(image.filename); QSize size = reader.size(); image.valid = (size == imgsize); diff --git a/src/sphere.cpp b/src/sphere.cpp index a120306f..ae08b03f 100644 --- a/src/sphere.cpp +++ b/src/sphere.cpp @@ -72,7 +72,7 @@ void Sphere::ellipseFit() { Eigen::VectorXd min_pos_eig = eigenvector.col(0); - for(int i = 0; i<3 ; i++){ + for(int i = 0; i<3; i++) { if(cond(i) > 0){ min_pos_eig = eigenvector.col(i); break; @@ -120,7 +120,7 @@ bool Sphere::fit() { if(border.size() >= 5) { ellipseFit(); if(isnan(eWidth)) { - ellipse = false; + ellipse = false; } else { radius = eWidth; assert(eWidth >= eHeight); @@ -227,8 +227,8 @@ void Sphere::findHighlight(QImage img, int n, bool update_positions) { if(!inEllipse(cx, cy, eWidth, eHeight, eAngle)) continue; } else { - float d = sqrt(cx*cx + cy*cy); - if(d > smallradius) continue; + float d = sqrt(cx*cx + cy*cy); + if(d > smallradius) continue; } QRgb c = img.pixel(x, y);