From a4e702cb573fa335385c8efd923a469420e4f00e Mon Sep 17 00:00:00 2001 From: BenJuan26 Date: Sat, 16 Sep 2017 09:59:42 -0400 Subject: [PATCH] Extreme improvement to calibration process * Calibration of the light frames now happens directly on the Bayer matrix * As a bonus, stacking of calibration frames is approximately 10x faster * Allow for saving and loading of the image list * Store more user preferences * Remove 3rdparty libs from the repo * Fix linux bug regarding file extensions * Restructure to facilitate the creation of deb packages --- .gitignore | 12 +- Doxyfile | 2 +- README.md | 6 +- src/{ => images}/OpenSkyStacker.icns | Bin src/{ => images}/OpenSkyStacker.ico | Bin src/{ => images}/dng-background.png | Bin .../openskystacker-logoabbr-128.png | Bin .../openskystacker-logoabbr-512.png | Bin src/model/imagetablemodel.cpp | 6 + src/model/imagetablemodel.h | 1 - src/processing/imagestacker.cpp | 158 ++++++---- src/processing/imagestacker.h | 3 + src/{ => scripts}/create-dmg.sh | 4 +- src/{ => scripts}/mac_deploy.sh | 2 +- src/src.pro | 47 +-- src/{ => translations}/openskystacker_es.qm | Bin src/{ => translations}/openskystacker_es.ts | 0 src/ui/mainwindow.cpp | 273 ++++++++++++++---- src/ui/mainwindow.h | 4 + src/ui/mainwindow.ui | 46 ++- 20 files changed, 416 insertions(+), 148 deletions(-) rename src/{ => images}/OpenSkyStacker.icns (100%) rename src/{ => images}/OpenSkyStacker.ico (100%) rename src/{ => images}/dng-background.png (100%) rename src/{ => images}/openskystacker-logoabbr-128.png (100%) rename src/{ => images}/openskystacker-logoabbr-512.png (100%) rename src/{ => scripts}/create-dmg.sh (98%) rename src/{ => scripts}/mac_deploy.sh (98%) rename src/{ => translations}/openskystacker_es.qm (100%) rename src/{ => translations}/openskystacker_es.ts (100%) diff --git a/.gitignore b/.gitignore index 0dd9180..9b7e7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -*/*.pro.user -*/*.pro.user* +*.pro.user +*.pro.user* src/*_resource.rc -src/*.lnk -*/.DS_Store +*.lnk +.DS_Store Makefile Makefile.* -src/*.dmg +*.dmg # Snapcraft src/*.snap @@ -19,4 +19,4 @@ bin .qmake.stash doc -*/*.autosave +*.autosave diff --git a/Doxyfile b/Doxyfile index 8997229..9015bd7 100644 --- a/Doxyfile +++ b/Doxyfile @@ -2,7 +2,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = OpenSkyStacker PROJECT_NUMBER = v0.1.1 PROJECT_BRIEF = "Multi-platform astroimaging stacker" -PROJECT_LOGO = src/openskystacker-logoabbr-128.png +PROJECT_LOGO = src/images/openskystacker-logoabbr-128.png OUTPUT_DIRECTORY = doc CREATE_SUBDIRS = NO ALLOW_UNICODE_NAMES = NO diff --git a/README.md b/README.md index e4dac66..3aa8d1f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Multi-platform astroimaging stacker. [![Build Status](https://travis-ci.org/BenJuan26/OpenSkyStacker.svg?branch=master)](https://travis-ci.org/BenJuan26/OpenSkyStacker) [![Documentation](https://codedocs.xyz/BenJuan26/OpenSkyStacker.svg)](https://codedocs.xyz/BenJuan26/OpenSkyStacker/) -OpenSkyStacker assists in the processing of deep-sky images. "Stacking" in this context means averaging several near-identical images to reduce the noise and boost the signal-to-noise ratio. This is especially helpful in the field of astrophotography because many objects of interest are so dim that, without processing, they might be indistinguishable from noise. +OpenSkyStacker assists in the processing of deep-sky images. *Stacking* in this context means taking the average of several exposures of the same object to reduce the noise and boost the signal-to-noise ratio. This is especially helpful in the field of astrophotography because many objects of interest are so dim that, without processing, they might be indistinguishable from noise. OpenSkyStacker is not unique in what it accomplishes, as there is other stacking software out there, but it is unique in that it is free, open-source, and available for nearly any operating system. ## Download -For Windows and Mac, OpenSkyStacker can be downloaded from the [releases page](https://github.com/BenJuan26/OpenSkyStacker/releases) of this repo. Releasing software for Linux is not a simple process, so for the time being Linux users must [compile from source](#build). +For Windows, Mac, and Ubuntu, OpenSkyStacker can be downloaded from the [releases page](https://github.com/BenJuan26/OpenSkyStacker/releases) of this repo. Releasing software for all the many Linux distros is not a simple process, so users on other distros can [compile from source](#build). ## Build @@ -46,7 +46,7 @@ This will compile the program to the `bin/` directory. ### Releasing for Mac -Qt provides the somewhat-helpful `macdeployqt` program to deploy Qt apps for Mac. However, it's not perfect: in my experience it doesn't correctly change the absolute paths of some libraries. For that reason, two scripts are provided for deploying on Mac: [mac_deploy.sh](src/mac_deploy.sh) and [create_dmg.sh](src/create_dmg.sh). The former will run `macdeployqt` and fix anything it may have missed, and the latter will create a pretty DMG image ready for release. +Qt provides the somewhat-helpful `macdeployqt` program to deploy Qt apps for Mac. However, it's not perfect: in my experience it doesn't correctly change the absolute paths of some libraries. For that reason, two scripts are provided for deploying on Mac: [mac_deploy.sh](src/scripts/mac_deploy.sh) and [create_dmg.sh](src/scripts/create_dmg.sh). The former will run `macdeployqt` and fix anything it may have missed, and the latter will create a pretty DMG image ready for release. ``` cd src diff --git a/src/OpenSkyStacker.icns b/src/images/OpenSkyStacker.icns similarity index 100% rename from src/OpenSkyStacker.icns rename to src/images/OpenSkyStacker.icns diff --git a/src/OpenSkyStacker.ico b/src/images/OpenSkyStacker.ico similarity index 100% rename from src/OpenSkyStacker.ico rename to src/images/OpenSkyStacker.ico diff --git a/src/dng-background.png b/src/images/dng-background.png similarity index 100% rename from src/dng-background.png rename to src/images/dng-background.png diff --git a/src/openskystacker-logoabbr-128.png b/src/images/openskystacker-logoabbr-128.png similarity index 100% rename from src/openskystacker-logoabbr-128.png rename to src/images/openskystacker-logoabbr-128.png diff --git a/src/openskystacker-logoabbr-512.png b/src/images/openskystacker-logoabbr-512.png similarity index 100% rename from src/openskystacker-logoabbr-512.png rename to src/images/openskystacker-logoabbr-512.png diff --git a/src/model/imagetablemodel.cpp b/src/model/imagetablemodel.cpp index 1739ef0..0e8cc95 100644 --- a/src/model/imagetablemodel.cpp +++ b/src/model/imagetablemodel.cpp @@ -104,6 +104,8 @@ void ImageTableModel::Append(ImageRecord *record) beginInsertRows({}, list.count(), list.count()); list.append(record); endInsertRows(); + + emit dataChanged({},{}); } ImageRecord *ImageTableModel::At(int i) @@ -116,6 +118,8 @@ void ImageTableModel::RemoveAt(int i) beginRemoveRows({}, i, i); list.removeAt(i); endRemoveRows(); + + emit dataChanged({},{}); } bool ImageTableModel::setData(const QModelIndex &index, const QVariant &value, int role) @@ -130,6 +134,8 @@ bool ImageTableModel::setData(const QModelIndex &index, const QVariant &value, i record->SetChecked(checked); } + emit dataChanged({},{}); + return true; } diff --git a/src/model/imagetablemodel.h b/src/model/imagetablemodel.h index 2289cf1..a4e6cfc 100644 --- a/src/model/imagetablemodel.h +++ b/src/model/imagetablemodel.h @@ -64,7 +64,6 @@ class ImageTableModel : public QAbstractTableModel /*! @param i The index of the ImageRecord to be removed. */ void RemoveAt(int i); - private: QList list; }; diff --git a/src/processing/imagestacker.cpp b/src/processing/imagestacker.cpp index a49f3ed..166e8b5 100644 --- a/src/processing/imagestacker.cpp +++ b/src/processing/imagestacker.cpp @@ -148,6 +148,18 @@ bool ImageStacker::FileHasRawExtension(QString filename) return std::find(RAW_EXTENSIONS.begin(), RAW_EXTENSIONS.end(), ext.toLower()) != RAW_EXTENSIONS.end(); } +int ImageStacker::GetTotalOperations() +{ + int ops = target_image_file_names_.length() * 2 + 1; + + if (use_bias_) ops += bias_frame_file_names_.length(); + if (use_darks_) ops += dark_frame_file_names_.length(); + if (use_dark_flats_) ops += dark_flat_frame_file_names_.length(); + if (use_flats_) ops += flat_frame_file_names_.length(); + + return ops; +} + void ImageStacker::Process() { bool raw = FileHasRawExtension(ref_image_file_name_); if (raw) { @@ -171,6 +183,61 @@ void ImageStacker::Process() { } } +cv::Mat ImageStacker::GetBayerMatrix(QString filename) { + LibRaw libraw; + libraw_data_t *imgdata = &libraw.imgdata; + libraw_output_params_t *params = &imgdata->params; + + // params for raw processing + params->use_auto_wb = 0; + params->use_camera_wb = 1; + params->no_auto_bright = 1; + params->output_bps = 16; + + libraw.open_file(filename.toUtf8().constData()); + libraw.unpack(); + + cv::Mat bayer(imgdata->sizes.raw_width, imgdata->sizes.raw_height, + CV_16UC1, imgdata->rawdata.raw_image); + return bayer.clone(); +} + +cv::Mat ImageStacker::GetCalibratedImage(QString filename) { + LibRaw libraw; + libraw_data_t *imgdata = &libraw.imgdata; + libraw_output_params_t *params = &imgdata->params; + + // params for raw processing + params->use_auto_wb = 0; + params->use_camera_wb = 1; + params->no_auto_bright = 1; + params->output_bps = 16; + + libraw.open_file(filename.toUtf8().constData()); + libraw.unpack(); + + // Edit the LibRaw Bayer matrix directly with a Mat + // That way we can still use LibRaw's debayering and processing algorithms + cv::Mat bayer(imgdata->sizes.raw_width, imgdata->sizes.raw_height, + CV_16UC1, imgdata->rawdata.raw_image); + + if (use_bias_) bayer -= master_bias_; + if (use_darks_) bayer -= master_dark_; + if (use_flats_) cv::divide(bayer, master_flat_, bayer, 1.0, CV_16U); + + // process raw into readable BGR bitmap + libraw.dcraw_process(); + + libraw_processed_image_t *proc = libraw.dcraw_make_mem_image(); + cv::Mat image = cv::Mat(cv::Size(imgdata->sizes.width, imgdata->sizes.height), + CV_16UC3, proc->data); + + if (bits_per_channel_ == BITS_32) + image.convertTo(image, CV_32F, 1/65535.0); + + return image.clone(); +} + void ImageStacker::ProcessRaw() { cancel_ = false; emit UpdateProgress(tr("Checking image sizes"), 0); @@ -182,12 +249,7 @@ void ImageStacker::ProcessRaw() { } current_operation_ = 0; - total_operations_ = target_image_file_names_.length() * 2 + 1; - - if (use_bias_) total_operations_ += bias_frame_file_names_.length(); - if (use_darks_) total_operations_ += dark_frame_file_names_.length(); - if (use_dark_flats_) total_operations_ += dark_flat_frame_file_names_.length(); - if (use_flats_) total_operations_ += flat_frame_file_names_.length(); + total_operations_ = GetTotalOperations(); if (use_bias_) StackBias(); if (use_darks_) StackDarks(); @@ -199,11 +261,7 @@ void ImageStacker::ProcessRaw() { 100*current_operation_/total_operations_); current_operation_++; - ref_image_ = ReadImage(ref_image_file_name_); - - if (use_bias_) ref_image_ -= master_bias_; - if (use_darks_) ref_image_ -= master_dark_; - if (use_flats_) ref_image_ /= master_flat_; + ref_image_ = GetCalibratedImage(ref_image_file_name_); // 32-bit float no matter what for the working image ref_image_.convertTo(working_image_, CV_32F); @@ -217,18 +275,7 @@ void ImageStacker::ProcessRaw() { qDebug() << message; if (total_operations_ != 0) emit UpdateProgress(message, 100*current_operation_/total_operations_); - cv::Mat targetImage = ReadImage(target_image_file_names_.at(k)); - - - // ------------- CALIBRATION -------------- - message = tr("Calibrating light frame %1 of %2").arg(QString::number(k+2), QString::number(target_image_file_names_.length() + 1)); - qDebug() << message; - if (total_operations_ != 0) emit UpdateProgress(message, 100*current_operation_/total_operations_); - - if (use_bias_) targetImage -= master_bias_; - if (use_darks_) targetImage -= master_dark_; - if (use_flats_) targetImage /= master_flat_; - + cv::Mat targetImage = GetCalibratedImage(target_image_file_names_.at(k)); // -------------- ALIGNMENT --------------- message = tr("Aligning image %1 of %2").arg(QString::number(k+2), QString::number(target_image_file_names_.length() + 1)); @@ -261,11 +308,13 @@ void ImageStacker::ProcessRaw() { working_image_ /= totalValidImages; - // only need to change the bit depth, no scaling if (bits_per_channel_ == BITS_16) { working_image_.convertTo(working_image_, CV_16U); } + // LibRaw works in RGB while OpenCV works in BGR + cv::cvtColor(working_image_, working_image_, CV_RGB2BGR); + emit Finished(working_image_, tr("Stacking completed")); } @@ -383,8 +432,6 @@ int ImageStacker::ValidateImageSizes() height = ref.rows; } - qDebug() << "target" << i << ":" << width << height; - if (width != refWidth || height != refHeight) { return -1; } @@ -531,15 +578,16 @@ QImage ImageStacker::Mat2QImage(const cv::Mat &src) void ImageStacker::StackDarks() { - cv::Mat dark1 = ReadImage(dark_frame_file_names_.at(0)); + cv::Mat dark1 = GetBayerMatrix(dark_frame_file_names_.at(0)); if (use_bias_) dark1 -= master_bias_; - cv::Mat result = dark1.clone(); + cv::Mat result; + dark1.convertTo(result, CV_32F); QString message; for (int i = 1; i < dark_frame_file_names_.length() && !cancel_; i++) { - cv::Mat dark = ReadImage(dark_frame_file_names_.at(i)); + cv::Mat dark = GetBayerMatrix(dark_frame_file_names_.at(i)); message = tr("Stacking dark frame %1 of %2").arg(QString::number(i+1), QString::number(dark_frame_file_names_.length())); qDebug() << message; @@ -547,25 +595,26 @@ void ImageStacker::StackDarks() if (total_operations_ != 0) emit UpdateProgress(message, 100*current_operation_/total_operations_); if (use_bias_) dark -= master_bias_; - result += dark; + cv::add(result, dark, result, cv::noArray(), CV_32F); } result /= dark_frame_file_names_.length(); - master_dark_ = result; + result.convertTo(master_dark_, CV_16U); } void ImageStacker::StackDarkFlats() { - cv::Mat darkFlat1 = ReadImage(dark_flat_frame_file_names_.at(0)); + cv::Mat darkFlat1 = GetBayerMatrix(dark_flat_frame_file_names_.at(0)); if (use_bias_) darkFlat1 -= master_bias_; - cv::Mat result = darkFlat1.clone(); + cv::Mat result; + darkFlat1.convertTo(result, CV_32F); QString message; for (int i = 1; i < dark_flat_frame_file_names_.length() && !cancel_; i++) { - cv::Mat dark = ReadImage(dark_flat_frame_file_names_.at(i)); + cv::Mat dark = GetBayerMatrix(dark_flat_frame_file_names_.at(i)); message = tr("Stacking dark flat frame %1 of %2").arg(QString::number(i+1), QString::number(dark_flat_frame_file_names_.length())); qDebug() << message; @@ -573,27 +622,28 @@ void ImageStacker::StackDarkFlats() if (total_operations_ != 0) emit UpdateProgress(message, 100*current_operation_/total_operations_); if (use_bias_) dark -= master_bias_; - result += dark; + cv::add(result, dark, result, cv::noArray(), CV_32F); } result /= dark_flat_frame_file_names_.length(); - master_dark_flat_ = result; + result.convertTo(master_dark_flat_, CV_16U); } void ImageStacker::StackFlats() { // most algorithms compute the median, but we will stick with mean for now - cv::Mat flat1 = ReadImage(flat_frame_file_names_.at(0)); - cv::Mat result = flat1.clone(); + cv::Mat flat1 = GetBayerMatrix(flat_frame_file_names_.at(0)); + if (use_bias_) flat1 -= master_bias_; + if (use_dark_flats_) flat1 -= master_dark_flat_; - if (use_bias_) result -= master_bias_; - if (use_dark_flats_) result -= master_dark_flat_; + cv::Mat result; + flat1.convertTo(result, CV_32F); QString message; for (int i = 1; i < flat_frame_file_names_.length() && !cancel_; i++) { - cv::Mat flat = ReadImage(flat_frame_file_names_.at(i)); + cv::Mat flat = GetBayerMatrix(flat_frame_file_names_.at(i)); message = tr("Stacking flat frame %1 of %2").arg(QString::number(i+1), QString::number(flat_frame_file_names_.length())); qDebug() << message; @@ -602,7 +652,7 @@ void ImageStacker::StackFlats() if (use_bias_) flat -= master_bias_; if (use_dark_flats_) flat -= master_dark_flat_; - result += flat; + cv::add(result, flat, result, cv::noArray(), CV_32F); } result /= flat_frame_file_names_.length(); @@ -610,8 +660,7 @@ void ImageStacker::StackFlats() // scale the flat frame so that the average value is 1.0 // since we're dividing, flat values darker than the average value will brighten the image // and values brighter than the average will darken the image, flattening the field - cv::Scalar meanScalar = cv::mean(result); - float avg = (meanScalar.val[0] + meanScalar.val[1] + meanScalar.val[2])/3; + float avg = cv::mean(result)[0]; if (bits_per_channel_ == BITS_16) result.convertTo(master_flat_, CV_32F, 1.0/avg); @@ -621,25 +670,26 @@ void ImageStacker::StackFlats() void ImageStacker::StackBias() { - cv::Mat bias1 = ReadImage(bias_frame_file_names_.at(0)); - cv::Mat result = bias1.clone(); + cv::Mat bias1 = GetBayerMatrix(bias_frame_file_names_.at(0)); + cv::Mat result; + bias1.convertTo(result, CV_32F); QString message; for (int i = 1; i < bias_frame_file_names_.length() && !cancel_; i++) { - cv::Mat bias = ReadImage(bias_frame_file_names_.at(i)); + cv::Mat bias = GetBayerMatrix(bias_frame_file_names_.at(i)); message = tr("Stacking bias frame %1 of %2").arg(QString::number(i+1), QString::number(bias_frame_file_names_.length())); qDebug() << message; current_operation_++; if (total_operations_ != 0) emit UpdateProgress(message, 100*current_operation_/total_operations_); - result += bias; + cv::add(result, bias, result, cv::noArray(), CV_32F); } result /= bias_frame_file_names_.length(); - master_bias_ = result; + result.convertTo(master_bias_, CV_16U); } cv::Mat ImageStacker::ConvertAndScaleImage(cv::Mat image) @@ -701,12 +751,14 @@ cv::Mat ImageStacker::ConvertAndScaleImage(cv::Mat image) cv::Mat ImageStacker::RawToMat(QString filename) { LibRaw processor; + libraw_data_t *imgdata = &processor.imgdata; + libraw_output_params_t *params = &imgdata->params; // params for raw processing - processor.imgdata.params.use_auto_wb = 0; - processor.imgdata.params.use_camera_wb = 1; - processor.imgdata.params.no_auto_bright = 1; - processor.imgdata.params.output_bps = 16; + params->use_auto_wb = 0; + params->use_camera_wb = 1; + params->no_auto_bright = 1; + params->output_bps = 16; processor.open_file(filename.toUtf8().constData()); processor.unpack(); diff --git a/src/processing/imagestacker.h b/src/processing/imagestacker.h index d27837a..d49a42e 100644 --- a/src/processing/imagestacker.h +++ b/src/processing/imagestacker.h @@ -128,6 +128,7 @@ public slots: void ProcessRaw(); void ProcessNonRaw(); bool FileHasRawExtension(QString filename); + int GetTotalOperations(); int ValidateImageSizes(); @@ -135,6 +136,8 @@ public slots: cv::Mat ConvertAndScaleImage(cv::Mat image); cv::Mat RawToMat(QString filename); + cv::Mat GetCalibratedImage(QString filename); + cv::Mat GetBayerMatrix(QString filename); cv::Mat GenerateAlignedImageOld(cv::Mat ref, cv::Mat target); void StackDarks(); diff --git a/src/create-dmg.sh b/src/scripts/create-dmg.sh similarity index 98% rename from src/create-dmg.sh rename to src/scripts/create-dmg.sh index a8876d9..d0c9739 100755 --- a/src/create-dmg.sh +++ b/src/scripts/create-dmg.sh @@ -12,8 +12,8 @@ fi # set up your app name, version number, and background image file name APP_NAME="OpenSkyStacker" VERSION="v0.1.1-alpha" -DMG_BACKGROUND_IMG="dng-background.png" -APP_PATH="../bin" +DMG_BACKGROUND_IMG="../images/dng-background.png" +APP_PATH="../../bin" # you should not need to change these APP_EXE="${APP_NAME}.app/Contents/MacOS/${APP_NAME}" diff --git a/src/mac_deploy.sh b/src/scripts/mac_deploy.sh similarity index 98% rename from src/mac_deploy.sh rename to src/scripts/mac_deploy.sh index 6549f21..89bb2a3 100755 --- a/src/mac_deploy.sh +++ b/src/scripts/mac_deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -DEFAULT_APP_PATH=../bin/OpenSkyStacker.app +DEFAULT_APP_PATH=../../bin/OpenSkyStacker.app APP_PATH=$1 if [ "$1" = "" ]; then diff --git a/src/src.pro b/src/src.pro index 64328c2..49f03a2 100644 --- a/src/src.pro +++ b/src/src.pro @@ -1,13 +1,6 @@ - -#------------------------------------------------- -# -# Project created by QtCreator 2017-03-14T21:16:33 -# -#------------------------------------------------- - QT += core gui -TRANSLATIONS += openskystacker_es.ts +TRANSLATIONS += translations/openskystacker_es.ts greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -78,30 +71,40 @@ test { } win32 { - RC_ICONS = $$PWD/OpenSkyStacker.ico + RC_ICONS = $$PWD/images/OpenSkyStacker.ico + + OPENCV_DIR = $$(OPENCV_DIR) + OPENCV_VER = $$(OPENCV_VER) + LIBRAW_DIR = $$(LIBRAW_DIR) + + isEmpty(OPENCV_DIR) { + error(Must define the env var OPENCV_DIR that points to the OpenCV build directory.) + } + isEmpty(OPENCV_VER) { + error(Must define the env var OPENCV_VER indicating the OpenCV version (ex. "320").) + } + isEmpty(LIBRAW_DIR) { + error(Must define the env var LIBRAW_DIR that points to the LibRaw build directory.) + } - INCLUDEPATH += $$PWD/3rdparty/opencv/build/include - INCLUDEPATH += $$PWD/3rdparty/libraw/libraw + INCLUDEPATH += $$OPENCV_DIR/include + INCLUDEPATH += $$LIBRAW_DIR/libraw LIBS += -lucrt LIBS += -lucrtd - LIBS += -L$$PWD/3rdparty/opencv/build/lib/Release - LIBS += -lopencv_core320 - LIBS += -lopencv_highgui320 - LIBS += -lopencv_imgcodecs320 - LIBS += -lopencv_imgproc320 - LIBS += -lopencv_features2d320 - LIBS += -lopencv_calib3d320 - LIBS += -lopencv_video320 + LIBS += -L$$OPENCV_DIR/lib/Release + LIBS += -lopencv_core$$OPENCV_VER + LIBS += -lopencv_imgcodecs$$OPENCV_VER + LIBS += -lopencv_imgproc$$OPENCV_VER LIBS += $$PWD/3rdparty/focas/win64/hfti.o LIBS += $$PWD/3rdparty/focas/win64/h12.o LIBS += $$PWD/3rdparty/focas/win64/diff.o - LIBS += -L$$PWD/3rdparty/libraw/lib + LIBS += -L$$LIBRAW_DIR/lib LIBS += -llibraw LIBS += -lWS2_32 } macx { - ICON = $$PWD/OpenSkyStacker.icns + ICON = $$PWD/images/OpenSkyStacker.icns CONFIG += link_pkgconfig PKGCONFIG += libraw @@ -121,7 +124,7 @@ macx { linux { - INCLUDE += /usr/include /usr/local/include /usr/include/x86_64-linux-gnu + INCLUDEPATH += /usr/include /usr/local/include /usr/include/x86_64-linux-gnu CONFIG += link_pkgconfig PKGCONFIG += libraw diff --git a/src/openskystacker_es.qm b/src/translations/openskystacker_es.qm similarity index 100% rename from src/openskystacker_es.qm rename to src/translations/openskystacker_es.qm diff --git a/src/openskystacker_es.ts b/src/translations/openskystacker_es.ts similarity index 100% rename from src/openskystacker_es.ts rename to src/translations/openskystacker_es.ts diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 712ff06..a316b38 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -8,12 +8,18 @@ #include #include #include +#include +#include +#include +#include +#include + #include #include #include -#include -#include + #include + #ifdef WIN32 #include #include @@ -43,6 +49,8 @@ MainWindow::MainWindow(QWidget *parent) : QTableView *table = ui_->imageListView; table->setModel(&table_model_); + connect(&table_model_, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), this, + SLOT(checkTableData())); table->setColumnWidth(0,25); // checked table->setColumnWidth(1,260); // filename table->setColumnWidth(2,80); // type @@ -76,6 +84,10 @@ MainWindow::MainWindow(QWidget *parent) : SLOT(handleButtonStack())); connect(ui_->buttonOptions, SIGNAL(released()), this, SLOT(handleButtonOptions())); + connect(ui_->buttonSaveList, SIGNAL(released()), this, + SLOT(handleButtonSaveList())); + connect(ui_->buttonLoadList, SIGNAL(released()), this, + SLOT(handleButtonLoadList())); // Signals / slots for stacker connect(this, SIGNAL (stackImages()), stacker_, @@ -118,7 +130,6 @@ void MainWindow::finishedStacking(cv::Mat image) { void MainWindow::updateProgress(QString message, int percentComplete) { Q_UNUSED(message); - Q_UNUSED(percentComplete); #ifdef WIN32 QWinTaskbarButton *button = new QWinTaskbarButton(this); button->setWindow(this->windowHandle()); @@ -148,16 +159,6 @@ void MainWindow::showTableContextMenu(QPoint pos) QMenu *menu = new QMenu(this); QTableView *table = ui_->imageListView; - QAction *setAsReferenceAction = new QAction(tr("Set As Reference"), this); - connect(setAsReferenceAction, SIGNAL(triggered(bool)), this, - SLOT(setFrameAsReference())); - menu->addAction(setAsReferenceAction); - - QAction *removeImageAction = new QAction(tr("Remove"), this); - connect(removeImageAction, SIGNAL(triggered(bool)), this, - SLOT(removeSelectedImages())); - menu->addAction(removeImageAction); - QAction *checkImageAction = new QAction(tr("Check"), this); connect(checkImageAction, SIGNAL(triggered(bool)), this, SLOT(checkImages())); @@ -168,6 +169,16 @@ void MainWindow::showTableContextMenu(QPoint pos) SLOT(uncheckImages())); menu->addAction(uncheckImageAction); + QAction *setAsReferenceAction = new QAction(tr("Set As Reference"), this); + connect(setAsReferenceAction, SIGNAL(triggered(bool)), this, + SLOT(setFrameAsReference())); + menu->addAction(setAsReferenceAction); + + QAction *removeImageAction = new QAction(tr("Remove"), this); + connect(removeImageAction, SIGNAL(triggered(bool)), this, + SLOT(removeSelectedImages())); + menu->addAction(removeImageAction); + // Show menu where the user clicked menu->popup(table->viewport()->mapToGlobal(pos)); } @@ -209,10 +220,6 @@ void MainWindow::removeSelectedImages() for (int i = 0; i < rows.count(); i++) { table_model_.RemoveAt(rows.at(i).row() - i); } - - if (table_model_.rowCount() == 0) { - ui_->buttonStack->setEnabled(false); - } } void MainWindow::imageSelectionChanged() @@ -247,10 +254,6 @@ void MainWindow::checkImages() ImageRecord *record = table_model_.At(rows.at(i).row()); record->SetChecked(true); } - - if (rows.count() > 0) { - ui_->buttonStack->setEnabled(true); - } } void MainWindow::uncheckImages() @@ -263,16 +266,7 @@ void MainWindow::uncheckImages() record->SetChecked(false); } - bool hasChecked = false; - for (int i = 0; i < table_model_.rowCount(); i++) { - if (table_model_.At(i)->IsChecked()) { - hasChecked = true; - } - } - - if (!hasChecked) { - ui_->buttonStack->setEnabled(false); - } + checkTableData(); } void MainWindow::processingError(QString message) @@ -298,6 +292,13 @@ void MainWindow::handleButtonStack() { return; } + // Linux doesn't force the proper extension unlike Windows and Mac + QRegularExpression regex(".tif$"); + if (!regex.match(saveFilePath).hasMatch()) { + qDebug() << "Filename was missing extension, adding it"; + saveFilePath += ".tif"; + } + QFileInfo info(saveFilePath); settings.setValue("files/savePath", info.absoluteFilePath()); stacker_->SetSaveFilePath(saveFilePath); @@ -330,17 +331,28 @@ void MainWindow::handleButtonStack() { void MainWindow::handleButtonLightFrames() { QSettings settings("OpenSkyStacker", "OpenSkyStacker"); - QDir dir = QDir(settings.value("files/lightFramesDir", + QDir dir = QDir(settings.value("files/lights/dir", QDir::homePath()).toString()); QFileDialog dialog(this); dialog.setDirectory(dir); dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setNameFilters(image_file_filter_); + dialog.setWindowTitle(tr("Select Light Frames")); - if (!dialog.exec()) return; + QString filter = settings.value("files/lights/filter", QString()).toString(); + if (!filter.isNull()) { + dialog.selectNameFilter(filter); + } + + if (!dialog.exec()) + return; QStringList targetImageFileNames = dialog.selectedFiles(); + QFileInfo info(targetImageFileNames.at(0)); + settings.setValue("files/lights/dir", info.absolutePath()); + QString newFilter = dialog.selectedNameFilter(); + settings.setValue("files/lights/filter", newFilter); for (int i = 0; i < targetImageFileNames.length(); i++) { ImageRecord *record = stacker_->GetImageRecord( @@ -349,27 +361,33 @@ void MainWindow::handleButtonLightFrames() { table_model_.Append(record); } - QFileInfo info(targetImageFileNames.at(0)); - settings.setValue("files/lightFramesDir", info.absolutePath()); - emit readQImage(targetImageFileNames.at(0)); - - ui_->buttonStack->setEnabled(true); } void MainWindow::handleButtonDarkFrames() { QSettings settings("OpenSkyStacker", "OpenSkyStacker"); - QDir dir = QDir(settings.value("files/darkFramesDir", - settings.value("files/lightFramesDir", QDir::homePath())).toString()); + QDir dir = QDir(settings.value("files/darks/dir", + settings.value("files/lights/dir", QDir::homePath())).toString()); QFileDialog dialog(this); dialog.setDirectory(dir); dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setNameFilters(image_file_filter_); + dialog.setWindowTitle(tr("Select Dark Frames")); + + QString filter = settings.value("files/darks/filter", + settings.value("files/lights/filter",QString())).toString(); + if (!filter.isNull()) { + dialog.selectNameFilter(filter); + } if (!dialog.exec()) return; QStringList darkFrameFileNames = dialog.selectedFiles(); + QFileInfo info(darkFrameFileNames.at(0)); + settings.setValue("files/darks/dir", info.absolutePath()); + QString newFilter = dialog.selectedNameFilter(); + settings.setValue("files/darks/filter", newFilter); for (int i = 0; i < darkFrameFileNames.length(); i++) { ImageRecord *record = stacker_->GetImageRecord( @@ -377,24 +395,32 @@ void MainWindow::handleButtonDarkFrames() { record->SetType(ImageRecord::DARK); table_model_.Append(record); } - - QFileInfo info(darkFrameFileNames.at(0)); - settings.setValue("files/darkFramesDir", info.absolutePath()); } void MainWindow::handleButtonDarkFlatFrames() { QSettings settings("OpenSkyStacker", "OpenSkyStacker"); - QDir dir = QDir(settings.value("files/darkFlatFramesDir", - settings.value("files/lightFramesDir", QDir::homePath())).toString()); + QDir dir = QDir(settings.value("files/darkflats/dir", + settings.value("files/lights/dir", QDir::homePath())).toString()); QFileDialog dialog(this); dialog.setDirectory(dir); dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setNameFilters(image_file_filter_); + dialog.setWindowTitle(tr("Select Dark Flat Frames")); + + QString filter = settings.value("files/darkflats/filter", + settings.value("files/lights/filter",QString())).toString(); + if (!filter.isNull()) { + dialog.selectNameFilter(filter); + } if (!dialog.exec()) return; QStringList darkFlatFrameFileNames = dialog.selectedFiles(); + QFileInfo info(darkFlatFrameFileNames.at(0)); + settings.setValue("files/darkflats/dir", info.absolutePath()); + QString newFilter = dialog.selectedNameFilter(); + settings.setValue("files/darkflats/filter", newFilter); for (int i = 0; i < darkFlatFrameFileNames.length(); i++) { ImageRecord *record = stacker_->GetImageRecord( @@ -402,24 +428,32 @@ void MainWindow::handleButtonDarkFlatFrames() { record->SetType(ImageRecord::DARK_FLAT); table_model_.Append(record); } - - QFileInfo info(darkFlatFrameFileNames.at(0)); - settings.setValue("files/darkFlatFramesDir", info.absolutePath()); } void MainWindow::handleButtonFlatFrames() { QSettings settings("OpenSkyStacker", "OpenSkyStacker"); - QDir dir = QDir(settings.value("files/flatFramesDir", - settings.value("files/lightFramesDir", QDir::homePath())).toString()); + QDir dir = QDir(settings.value("files/flats/dir", + settings.value("files/lights/dir", QDir::homePath())).toString()); QFileDialog dialog(this); dialog.setDirectory(dir); dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setNameFilters(image_file_filter_); + dialog.setWindowTitle(tr("Select Flat Frames")); + + QString filter = settings.value("files/flats/filter", + settings.value("files/lights/filter",QString())).toString(); + if (!filter.isNull()) { + dialog.selectNameFilter(filter); + } if (!dialog.exec()) return; QStringList flatFrameFileNames = dialog.selectedFiles(); + QFileInfo info(flatFrameFileNames.at(0)); + settings.setValue("files/flats/dir", info.absolutePath()); + QString newFilter = dialog.selectedNameFilter(); + settings.setValue("files/flats/filter", newFilter); for (int i = 0; i < flatFrameFileNames.length(); i++) { ImageRecord *record = stacker_->GetImageRecord( @@ -427,25 +461,33 @@ void MainWindow::handleButtonFlatFrames() { record->SetType(ImageRecord::FLAT); table_model_.Append(record); } - - QFileInfo info(flatFrameFileNames.at(0)); - settings.setValue("files/flatFramesDir", info.absolutePath()); } void MainWindow::handleButtonBiasFrames() { QSettings settings("OpenSkyStacker", "OpenSkyStacker"); - QDir dir = QDir(settings.value("files/biasFramesDir", - settings.value("files/lightFramesDir", QDir::homePath())).toString()); + QDir dir = QDir(settings.value("files/bias/dir", + settings.value("files/lights/dir", QDir::homePath())).toString()); QFileDialog dialog(this); dialog.setDirectory(dir); dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setNameFilters(image_file_filter_); + dialog.setWindowTitle(tr("Select Bias Frames")); + + QString filter = settings.value("files/bias/filter", + settings.value("files/lights/filter",QString())).toString(); + if (!filter.isNull()) { + dialog.selectNameFilter(filter); + } if (!dialog.exec()) return; QStringList biasFrameFileNames = dialog.selectedFiles(); + QFileInfo info(biasFrameFileNames.at(0)); + settings.setValue("files/bias/dir", info.absolutePath()); + QString newFilter = dialog.selectedNameFilter(); + settings.setValue("files/bias/filter", newFilter); for (int i = 0; i < biasFrameFileNames.length(); i++) { ImageRecord *record = stacker_->GetImageRecord( @@ -453,9 +495,6 @@ void MainWindow::handleButtonBiasFrames() record->SetType(ImageRecord::BIAS); table_model_.Append(record); } - - QFileInfo info(biasFrameFileNames.at(0)); - settings.setValue("files/biasFramesDir", info.absolutePath()); } void MainWindow::handleButtonOptions() @@ -471,6 +510,108 @@ void MainWindow::handleButtonOptions() delete dialog; } +void MainWindow::handleButtonSaveList() +{ + QSettings settings("OpenSkyStacker", "OpenSkyStacker"); + QString filename = QFileDialog::getSaveFileName(this, tr("Save List"), + settings.value("files/listDir", settings.value( + "files/LightFramesDir", QDir::homePath())).toString(), + "JSON document (*.json)"); + if (filename.isEmpty()) + return; + + // Linux doesn't force the proper extension unlike Windows and Mac + QRegularExpression regex(".json$"); + if (!regex.match(filename).hasMatch()) { + qDebug() << "Filename was missing extension, adding it"; + filename += ".json"; + } + + QJsonArray images; + for (int i = 0; i < table_model_.rowCount(); i++) { + ImageRecord *record = table_model_.At(i); + QJsonObject image; + image.insert("filename", record->GetFilename()); + image.insert("type", record->GetType()); + image.insert("checked", record->IsChecked()); + + images.insert(images.size(), image); + } + + QJsonDocument doc(images); + QByteArray fileContents = doc.toJson(); + QFile file(filename); + if (file.open(QIODevice::ReadWrite)) { + QTextStream stream(&file); + stream << fileContents; + } + + QFileInfo info(file); + QString dir = info.absolutePath(); + settings.setValue("files/listDir", dir); +} + +void MainWindow::handleButtonLoadList() +{ + QSettings settings("OpenSkyStacker", "OpenSkyStacker"); + QString dir = settings.value("files/listDir", settings.value( + "files/LightFramesDir", QDir::homePath())).toString(); + QString filename = QFileDialog::getOpenFileName(this, tr("Load List"), dir, "JSON file (*.json)"); + if (filename.isEmpty()) { + return; + } + + QFile file(filename); + QString contents; + if (file.open(QIODevice::ReadOnly)) { + QTextStream in(&file); + contents = in.readAll(); + } + + QJsonDocument doc = QJsonDocument::fromJson(contents.toUtf8()); + if (!doc.isArray()) { + QMessageBox::information(this, tr("Error loading list"), + tr("Couldn't read the list file. Top level object is not an array.")); + return; + } + + for (int i = 0; i < table_model_.rowCount(); i++) { + table_model_.RemoveAt(0); + } + + QJsonArray list = doc.array(); + for (int i = 0; i < list.size(); i++) { + QJsonValue val = list.at(i); + QJsonObject img = val.toObject(); + if (img.isEmpty()) { + QMessageBox::information(this, tr("Error loading list"), + tr("Couldn't read the list file. Object at index %1 is not a JSON object.").arg(i)); + return; + } + + QString imageFileName = img.value("filename").toString(); + if (imageFileName.isNull()) { + QMessageBox::information(this, tr("Error loading list"), + tr("Couldn't read the list file. Object at index %1 has no valid filename.").arg(i)); + return; + } + + int type = img.value("type").toInt(-1); + if (type < 0) { + QMessageBox::information(this, tr("Error loading list"), + tr("Couldn't read the list file. Object at index %1 has no valid type.").arg(i)); + return; + } + + bool checked = img.value("checked").toBool(); + ImageRecord *record = stacker_->GetImageRecord(imageFileName); + record->SetType(static_cast(type)); + record->SetChecked(checked); + + table_model_.Append(record); + } +} + QImage MainWindow::Mat2QImage(const cv::Mat &src) { QImage dest(src.cols, src.rows, QImage::Format_RGB32); int r, g, b; @@ -525,6 +666,22 @@ void MainWindow::closeEvent(QCloseEvent *event) { Q_UNUSED(event); } +void MainWindow::checkTableData() +{ + int lightsChecked = 0; + for (int i = 0; i < table_model_.rowCount(); i++) { + if (table_model_.At(i)->IsChecked() && table_model_.At(i)->GetType() == ImageRecord::LIGHT) { + lightsChecked++; + } + } + + if (lightsChecked < 2) { + ui_->buttonStack->setEnabled(false); + } else { + ui_->buttonStack->setEnabled(true); + } +} + void MainWindow::setFileImage(QString filename) { QGraphicsScene* scene = new QGraphicsScene(this); diff --git a/src/ui/mainwindow.h b/src/ui/mainwindow.h index 160b2ef..9512958 100644 --- a/src/ui/mainwindow.h +++ b/src/ui/mainwindow.h @@ -81,6 +81,8 @@ public slots: void closeEvent(QCloseEvent *event); + void checkTableData(); + private slots: void handleButtonLightFrames(); void handleButtonStack(); @@ -89,6 +91,8 @@ private slots: void handleButtonFlatFrames(); void handleButtonBiasFrames(); void handleButtonOptions(); + void handleButtonSaveList(); + void handleButtonLoadList(); private: void setFileImage(QString filename); diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui index 459f1af..8f1a1cc 100644 --- a/src/ui/mainwindow.ui +++ b/src/ui/mainwindow.ui @@ -150,6 +150,50 @@ QPushButton:disabled { + + + + QWidget { + color: white; +} + +QPushButton { + color: white; + background-color: #222; + border-radius: 5px; + padding: 4px 12px; +} + +QPushButton:hover { + background-color: #2d74da; +} + +QPushButton:disabled { + color: #444; + background-color: #777; +} + + + Image List + + + + + + Load List + + + + + + + Save List + + + + + + @@ -226,7 +270,7 @@ QPushButton:disabled { 0 0 1024 - 22 + 21