Compare commits
275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71486efeef | |||
| 8213f6213f | |||
| f8c9fec77e | |||
| af4be850cb | |||
| ca1a13ed9d | |||
| 1873da6c49 | |||
| 92345f82ca | |||
| 3c8f49e932 | |||
| 37dd97e361 | |||
| da31187aa3 | |||
| 4801338160 | |||
| fb9d026ff5 | |||
| c2810faf8f | |||
| d3d302fd38 | |||
| a0497c7d19 | |||
| 8818e25eda | |||
| c3baa18087 | |||
| 66f0c05a48 | |||
| 461ffea874 | |||
| 7535ad87e7 | |||
| 273aef1594 | |||
| 9519c9830c | |||
| 342e5cc5db | |||
| ae84cbdfe0 | |||
| 933fd4a2a0 | |||
| c3588e1c36 | |||
| 174134a9ee | |||
| bbc13ec8a5 | |||
| 9f7e2ab6b4 | |||
| 4fe56acbd9 | |||
| f35db9d1af | |||
| 81d138f799 | |||
| ae07d4793b | |||
| dc2a781d3b | |||
| 90035f44ed | |||
| 53c9a58125 | |||
| 3f7e3689e8 | |||
| af9187737f | |||
| 4e952873e3 | |||
| fb24800050 | |||
| ea0dcc226a | |||
| 6a7b677b95 | |||
| 0cee4c9c53 | |||
| d5f2351905 | |||
| 18732a8cbf | |||
| 8c9c1d8d06 | |||
| e5f425ff8d | |||
| 428f9c360a | |||
| a8a1509db7 | |||
| 6539c78c57 | |||
| 0e0d29320e | |||
| 1efe8e6974 | |||
| dae10182d1 | |||
| ed5fc9c1c2 | |||
| cd6a64a98b | |||
| 67355a82b7 | |||
| 8fc2078a3a | |||
| da9b389409 | |||
| 7818b8d3e9 | |||
| 11294bfcb0 | |||
| faecb385aa | |||
| e5be04926b | |||
| eaf2c7094b | |||
| aef41f5f6b | |||
| 2134f13b06 | |||
| 0e9c980325 | |||
| b9bf6bf183 | |||
| 50c070b169 | |||
| cfee287bfa | |||
| 61e0c542f5 | |||
| a42abb05ea | |||
| 5c6df4a59f | |||
| 35d5934227 | |||
| 8e3c1b35db | |||
| 544e4abf92 | |||
| e97e10fb5b | |||
| 2608a1bc79 | |||
| fa69f17e51 | |||
| 4a9d720343 | |||
| d462ece7c9 | |||
| 46b0210078 | |||
| 0a803ace10 | |||
| 0c2c5f908c | |||
| c2197298a7 | |||
| e8630330b2 | |||
| 5955a02175 | |||
| c0b9194ecc | |||
| 5f27acbfd1 | |||
| f1a2aae9b6 | |||
| 9ffbdcee30 | |||
| d9b1c253db | |||
| 7e39304799 | |||
| 31cf1ee2b1 | |||
| ab245f0484 | |||
| 77c312800a | |||
| 21e90b92dc | |||
| 2817d3c7c9 | |||
| a51b0ef02c | |||
| 7b19230366 | |||
| 26666ee36d | |||
| 74aee15f80 | |||
| fde1594086 | |||
| a9783f6030 | |||
| 1ce4f95f53 | |||
| 1400fd2e32 | |||
| cc7f56fc53 | |||
| 8921ef9c63 | |||
| 576df9c196 | |||
| c47ecbedb8 | |||
| c7f4e3747a | |||
| 304cd33f34 | |||
| abc813ddbb | |||
| 88f449d971 | |||
| 295ddb8daf | |||
| 1a220bc3ed | |||
| f67539a3a1 | |||
| 468bcb5abb | |||
| fd49ba9a44 | |||
| 22e3b06fdd | |||
| 01febbf421 | |||
| 7690496cf5 | |||
| 743a5f50c4 | |||
| eac534352f | |||
| 57bdc74ef6 | |||
| fd1fd7ff08 | |||
| c1aca3ca65 | |||
| 5cc8fdd83d | |||
| 66e13529be | |||
| e1bed8e1cb | |||
| 151f521688 | |||
| 926647e1a7 | |||
| 71fc1f2bd1 | |||
| f1ff04382b | |||
| ad91adf1d9 | |||
| b7369c2501 | |||
| 79ed6b2059 | |||
| 380974a088 | |||
| b1ad56ca1f | |||
| 58abef5a72 | |||
| 5427ff57cb | |||
| 6a2fa3f656 | |||
| c62ec7db8c | |||
| d5eb0fdce5 | |||
| 6d25919e1f | |||
| 26d1af6077 | |||
| 44d8a8b856 | |||
| dab6c1f79d | |||
| ce6a4cc40c | |||
| 0368c1f1dc | |||
| a1e98d818b | |||
| f3f194bcef | |||
| efd36f96c3 | |||
| 37923b37b3 | |||
| 900453577e | |||
| 34d466c3e0 | |||
| 9e98127084 | |||
| ba6062b925 | |||
| 6411b7cd15 | |||
| 223f7cd0ea | |||
| f8f9ee08b3 | |||
| af5aed7ef8 | |||
| 8f5249b142 | |||
| a7dc942c62 | |||
| 1a1399434b | |||
| be567841bf | |||
| 62d2671112 | |||
| 1f8923512e | |||
| 455c3b2d64 | |||
| 4fe546f0e5 | |||
| 95c6fc5343 | |||
| 2bc54ea0cc | |||
| be6e472081 | |||
| 9746f8f653 | |||
| b51a305c63 | |||
| 39775b5e98 | |||
| 93b56e2966 | |||
| 2e41464ff4 | |||
| b6ae7d4cdb | |||
| 1bd48e8fb4 | |||
| 1d65eda490 | |||
| 97346df596 | |||
| a157b274a2 | |||
| 6466702819 | |||
| b4ea65b42a | |||
| 1682de4e1b | |||
| 00872b31df | |||
| 2884787916 | |||
| 86ea9fc137 | |||
| 67199a033d | |||
| 0f182900c2 | |||
| 19ed5ae1a4 | |||
| 46215c7a7d | |||
| c346487504 | |||
| 5b6fead6f1 | |||
| 3e94aa0eda | |||
| 08e70cdb52 | |||
| 04e587b51c | |||
| 42dd55244a | |||
| 6b9ea5e4b9 | |||
| 701a425cc7 | |||
| 9cd2ae14b3 | |||
| f7e4e1874f | |||
| e6749fc487 | |||
| dc6aa6baa8 | |||
| c8a70d22f8 | |||
| dbb533176c | |||
| 032f5b0577 | |||
| eb417010c3 | |||
| 5b44d2ac69 | |||
| d1df789691 | |||
| ab7d04b625 | |||
| a4cfc65d4b | |||
| b4746be190 | |||
| 9ceb7556f9 | |||
| 41b29f0701 | |||
| 67ae2d4b62 | |||
| b6b6863331 | |||
| 571fa57af2 | |||
| abb3d631bf | |||
| b65911943e | |||
| 9d9f8db499 | |||
| 3060b17c0c | |||
| fcf336d63a | |||
| 54ef8e990c | |||
| 94466a6b9b | |||
| b84d8127ad | |||
| c971a919ec | |||
| 8c248b7cfc | |||
| 43b510a78c | |||
| 105fba814d | |||
| 555e6c11c8 | |||
| 748f5fac03 | |||
| adc7d07b75 | |||
| ba450ee554 | |||
| cc69b7bc2d | |||
| 42b619641a | |||
| 9af4fa1b99 | |||
| cbc6775756 | |||
| cee6979ece | |||
| 903ec65d52 | |||
| 95e4774507 | |||
| 2410c51d5d | |||
| 17bca74362 | |||
| c70123cf7b | |||
| 12c6385f77 | |||
| 39f3ec7d30 | |||
| 8b968ddcb1 | |||
| da1843e48c | |||
| e0d473c8c8 | |||
| 92f9920f24 | |||
| f68a9c4d7c | |||
| 027a38cb42 | |||
| 47d5a9fc96 | |||
| 061bb3892e | |||
| b0b1a3a14b | |||
| ea834ebd16 | |||
| ce836a8ff3 | |||
| a1848b27bf | |||
| fabf3f0c1a | |||
| cba8a0bb9c | |||
| 4e6230eef2 | |||
| 2c95364fc4 | |||
| 26be690c70 | |||
| 56d6db8bc3 | |||
| a44c456cab | |||
| 0fb27266c7 | |||
| e5cd25cc77 | |||
| c51be18d28 | |||
| 61a618b2fd | |||
| 84a71896f8 | |||
| 0ba02d4070 | |||
| eff0780014 | |||
| 7665ea76b6 | |||
| 9ba2515aa8 | |||
| 9b1f165381 |
@@ -0,0 +1,3 @@
|
||||
[submodule "libXISF"]
|
||||
path = libXISF
|
||||
url = https://gitea.nouspiro.space/nou/libXISF.git
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required (VERSION 3.15)
|
||||
|
||||
project(Tenmon)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
@@ -12,42 +12,111 @@ set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s")
|
||||
|
||||
find_package(Qt5 COMPONENTS Widgets Sql OpenGL REQUIRED)
|
||||
find_package(OpenCV REQUIRED)
|
||||
option(SANITIZE_ADDRESS_LEAK "Enable -fsanitize=address -fsanitize=leak" OFF)
|
||||
if(SANITIZE_ADDRESS_LEAK)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak")
|
||||
endif(SANITIZE_ADDRESS_LEAK)
|
||||
|
||||
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml REQUIRED)
|
||||
find_library(GSL_LIB gsl REQUIRED)
|
||||
find_library(GSLCBLAS_LIB gslcblas REQUIRED)
|
||||
find_library(EXIF_LIB exif REQUIRED)
|
||||
find_library(FITS_LIB cfitsio REQUIRED)
|
||||
find_library(RAW_LIB NAMES raw_r raw REQUIRED)
|
||||
find_library(RAW_LIB NAMES raw_r REQUIRED)
|
||||
find_library(WCS_LIB wcs wcslib PATHS REQUIRED)
|
||||
|
||||
add_subdirectory(libXISF)
|
||||
|
||||
set(TENMON_SRC
|
||||
database.cpp
|
||||
databaseview.cpp
|
||||
filesystemwidget.cpp
|
||||
imageinfo.cpp
|
||||
imageringlist.cpp
|
||||
about.cpp about.h
|
||||
batchprocessing.cpp batchprocessing.h batchprocessing.ui
|
||||
database.cpp database.h
|
||||
databaseview.cpp databaseview.h
|
||||
delete.cpp
|
||||
filesystemwidget.cpp filesystemwidget.h
|
||||
histogram.cpp histogram.h
|
||||
imageinfo.cpp imageinfo.h
|
||||
imageringlist.cpp imageringlist.h
|
||||
imagescrollarea.cpp
|
||||
imagescrollareagl.cpp
|
||||
loadrunable.cpp
|
||||
imagescrollareagl.cpp imagescrollareagl.h
|
||||
loadrunable.cpp loadrunable.h
|
||||
main.cpp
|
||||
mainwindow.cpp
|
||||
rawimage.cpp
|
||||
starfit.cpp
|
||||
stfslider.cpp
|
||||
stretchpanel.cpp
|
||||
mainwindow.cpp mainwindow.h
|
||||
markedfiles.cpp markedfiles.h
|
||||
rawimage.cpp rawimage.h
|
||||
rawimage_sse.cpp
|
||||
scriptengine.cpp scriptengine.h
|
||||
settingsdialog.cpp settingsdialog.h
|
||||
starfit.cpp starfit.h
|
||||
statusbar.cpp statusbar.h
|
||||
stfslider.cpp stfslider.h
|
||||
stretchtoolbar.cpp stretchtoolbar.h
|
||||
)
|
||||
|
||||
qt5_add_resources(TENMON_SRC resources.qrc)
|
||||
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
|
||||
|
||||
if(COLOR_MANAGMENT)
|
||||
add_compile_definitions("COLOR_MANAGMENT")
|
||||
endif(COLOR_MANAGMENT)
|
||||
|
||||
qt_add_resources(TENMON_SRC resources/resources.qrc)
|
||||
qt_add_resources(TENMON_SRC shaders/shaders.qrc)
|
||||
qt_add_resources(TENMON_SRC scripts/scripts.qrc)
|
||||
if(WIN32)
|
||||
list(APPEND TENMON_SRC icon.rc)
|
||||
endif(WIN32)
|
||||
list(APPEND TENMON_SRC resources/icon.rc)
|
||||
set(tenmon_ICON "")
|
||||
elseif(APPLE)
|
||||
set(tenmon_ICON ${CMAKE_CURRENT_SOURCE_DIR}/resources/tenmon.icns)
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
set_source_files_properties(${tenmon_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
else()
|
||||
set(tenmon_ICON "")
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_search_module(GIO REQUIRED gio-2.0)
|
||||
endif()
|
||||
|
||||
add_executable(tenmon ${TENMON_SRC})
|
||||
qt_add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
|
||||
|
||||
if(WIN32)
|
||||
find_path(FITS_INCLUDE cfitsio/fitsio2.h REQUIRED)
|
||||
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE}/cfitsio)
|
||||
endif(WIN32)
|
||||
find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED)
|
||||
target_include_directories(tenmon PRIVATE ${FITS_INCLUDE} ${CMAKE_BINARY_DIR} ${libXISF_SOURCE_DIR})
|
||||
|
||||
target_include_directories(tenmon PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(tenmon Qt5::Widgets Qt5::Sql Qt5::OpenGL ${OpenCV_LIBS} ${GSL_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB})
|
||||
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} XISF)
|
||||
if(APPLE)
|
||||
target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation")
|
||||
elseif(UNIX)
|
||||
target_link_libraries(tenmon PRIVATE Qt6::DBus ${GIO_LDFLAGS})
|
||||
endif(APPLE)
|
||||
|
||||
if(LIBRAW_STATIC)
|
||||
add_compile_definitions("LIBRAW_NODLL")
|
||||
target_link_libraries(tenmon PRIVATE jasper)
|
||||
endif()
|
||||
|
||||
install(TARGETS tenmon BUNDLE DESTINATION .)
|
||||
if(UNIX AND NOT APPLE)
|
||||
include(GNUInstallDirs)
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
if(XDG-DESKTOP-MENU_EXECUTABLE)
|
||||
install(SCRIPT install.cmake)
|
||||
else()
|
||||
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
||||
install(FILES resources/space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
|
||||
install(FILES resources/space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
|
||||
endif()
|
||||
install(FILES space.nouspiro.tenmon.metainfo.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
|
||||
endif(UNIX AND NOT APPLE)
|
||||
|
||||
option(RELEASE_BUILD "Release build" OFF)
|
||||
if(RELEASE_BUILD)
|
||||
add_custom_target(tenmon_version COMMAND ${CMAKE_COMMAND} -Dlocal_dir="${CMAKE_CURRENT_SOURCE_DIR}" -Doutput_dir="${CMAKE_CURRENT_BINARY_DIR}"
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/gitversion.cmake")
|
||||
add_dependencies(tenmon tenmon_version)
|
||||
else()
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -Dlocal_dir=${CMAKE_CURRENT_SOURCE_DIR} -Doutput_dir=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/gitversion.cmake")
|
||||
endif()
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
Simple image viewer with multithreaded image loading
|
||||
FITS/XISF image viewer with multithreaded image loading
|
||||
|
||||
To get all dependencies install these packages
|
||||
|
||||
sudo apt install qtbase5-dev libraw-dev libexif-dev libcfitsio-dev
|
||||
sudo apt install qt6-base-dev qt6-declarative-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev libgsl-dev wcslib-dev cmake
|
||||
|
||||
Then to build run
|
||||
on OpenSUSE
|
||||
|
||||
qmake .
|
||||
make
|
||||
./tenmon
|
||||
sudo zypper install gsl-devel exif-devel libraw-devel wcslib-devel libqt6-qtbase-devel
|
||||
|
||||
MacOS X
|
||||
|
||||
To compile on MacOS install XCode first. Then install homebrew in x86_64 mode
|
||||
with "arch -i x86_64". Building on native ARM is not supported.
|
||||
|
||||
homebrew install qt6 libraw cfitsio libexif libgsl wcslib
|
||||
|
||||
You may need to set CMAKE_PREFIX_PATH for Qt6 so CMake can find them.
|
||||
|
||||
Then to build run standard cmake
|
||||
|
||||
cmake -B build -S .
|
||||
cmake --build build
|
||||
./build/tenmon
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "about.h"
|
||||
#include <QTextEdit>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFile>
|
||||
#include <QLocale>
|
||||
#include "gitversion.h"
|
||||
|
||||
About::About(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("About Tenmon"));
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
QLabel *label = new QLabel(this);
|
||||
|
||||
QFile tenmonText(":/about/tenmon");
|
||||
tenmonText.open(QIODevice::ReadOnly);
|
||||
QByteArray text = tenmonText.readAll();
|
||||
text.replace("@GITVERSION@", GITVERSION);
|
||||
label->setText(text);
|
||||
label->setOpenExternalLinks(true);
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
|
||||
layout->addWidget(label);
|
||||
layout->addWidget(buttonBox);
|
||||
}
|
||||
|
||||
HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Help"));
|
||||
resize(800, 600);
|
||||
|
||||
QLocale locale;
|
||||
QString l = QLocale::languageToString(locale.language());
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
QTextEdit *helpText = new QTextEdit(this);
|
||||
helpText->setReadOnly(true);
|
||||
|
||||
QFile tenmonText(":/help");
|
||||
tenmonText.open(QIODevice::ReadOnly);
|
||||
helpText->setHtml(tenmonText.readAll());
|
||||
|
||||
layout->addWidget(helpText);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef ABOUT_H
|
||||
#define ABOUT_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class About : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
About(QWidget *parent = nullptr);
|
||||
};
|
||||
|
||||
class HelpDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
HelpDialog(QWidget *parent = nullptr);
|
||||
};
|
||||
|
||||
#endif // ABOUT_H
|
||||
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<head>
|
||||
<style type="text/css">
|
||||
h1, h2, h3, h4 { padding:0px; margin:10px; }
|
||||
p { padding:0px; margin:5px; }
|
||||
img { margin: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Tenmon help</h2>
|
||||
|
||||
<p>Tenmon is intended primarily for viewing astro photos and images. It supports the following formats:
|
||||
<ul>
|
||||
<li>FITS 8, 16, 32 bit integer and 32, 64 bit float</li>
|
||||
<li>XISF 8, 16, 32 bit integer and 32, 64 bit float</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
|
||||
<li>CR2, CR3, NEF, DNG raw images</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Main windows</h3>
|
||||
<p>The main window shows the currently loaded image. On the left is the <i>Image info</i> panel which displays details about the loaded image.
|
||||
The <i>File system</i> panel shows other images in the same directory as the loaded image. At the top is the main menu and below that is the
|
||||
<i>Stretch panel</i> containing various options for auto-stretching linear image data.</p>
|
||||
<p>All panels in the interface can be moved around and/or closed. Any closed or non-visible panel can be
|
||||
re-opened through the <i>Docks</i> menu at the top.</p>
|
||||
<p>At bottom there is status bar that show current lightness or red, green, blue pixel value under mouse cursor then X and Y coordinates and
|
||||
if image contain World Coordinate System metadata it show celestial coordinates.</p>
|
||||
|
||||
<h3>Opening and saving images</h3>
|
||||
<p>To load an image select <i>File->Open</i> and choose the file. After a file is loaded, it becomes visible in the main image panel, and the
|
||||
<i>File system</i> panel will show the other images in the same directory.</p>
|
||||
<p>The loaded image can be exported to a different format with <i>File->Save as</i>. Any of the formats JPEG, PNG FITS and XISF can be selected.
|
||||
In the case of saving JPEG or PNG, the stretch function is applied to the saved image. FITS and XISF are saved/converted without applying the stretch.
|
||||
To open an image, you can also drag and drop it to main window.</p>
|
||||
|
||||
<h3>View</h3>
|
||||
<p>The <i>View</i> menu has options to control the size and scale of displayed images:
|
||||
<ul>
|
||||
<li><i>Zoom In</i> and <i>Zoom Out</i> magnify and shrink the image. The mouse wheel can be also used to zoom freely.</li>
|
||||
<li><i>Best fit</i> auto-zooms the image to fit the current size of the window.</li>
|
||||
<li><i>100%</i> will zoom to 1:1 scale.</li>
|
||||
<li><i>Fullscreen</i> enlarges the main window to the whole screen.</li>
|
||||
<li><i>Thumbnails</i> will display small thumbnails for all images in the current directory.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Stretch toolbar</h3>
|
||||
<p>This panel changes how images are displayed.
|
||||
<br><img src=":/about/stretch-panel.png"></p>
|
||||
<p>Starting on the left, there is slider scale with three adjustable points to manually control the stretch.
|
||||
<ul>
|
||||
<li>black point - all pixels with lower value (darker) than this setting will be clipped black</li>
|
||||
<li>mid point - defines the value to be stretched to 50% intensity</li>
|
||||
<li>white point - all pixels with higher value (brighter) than this will be clipped white</li>
|
||||
</ul>
|
||||
Following the slider are 7 buttons for automatic stretching:
|
||||
<ul>
|
||||
<li><i>Linked channels</i> toggle stretching each RGB channel individually.</li>
|
||||
<li><i>Auto Stretch</i> automatically apply black and mid points to render the image with optimal brightness.</li>
|
||||
<li><i>Reset</i> reset three values for black, mid and white point to default.</li>
|
||||
<li><i>Invert</i> invert colors to display the image as negative.</li>
|
||||
<li><i>False colors</i> show black and white in false colour palette.</li>
|
||||
<li><i>Debayer CFA</i> Demosaicing black and white image from CFA sensor to color one.</li>
|
||||
<li><i>Apply Auto stretch on load</i> toggle automatically applying Autostretch for each image when loaded.</p>
|
||||
</ul>
|
||||
|
||||
<h3>Marking images</h3>
|
||||
<p>Images can be marked in the <i>Select</i> menu. To show a list of only the marked images, use <i>Select->Show marked</i>.
|
||||
This dialog can be useful to clear marks from images. Marked images show a <b>*</b> character in the title bar of the main window.
|
||||
Marked images can be copied or moved to a selected directory with <i>File->Copy/Move marked files</i>.
|
||||
After copying or moving, the list of marked files is cleared. The list of marked files will be remembered after quitting the program.</p>
|
||||
<p>Another way to mark images is in database view where you can select rows and then select mark or unmark action in context menu. Marked
|
||||
files will be shown with bold text. Third way to mark files is from thumbnails view where you can press press <i>Shift</i> and click with left
|
||||
mouse button and drag across thumbnails to mark them. Holding <i>Ctrl</i> will unmark files.</p>
|
||||
|
||||
<h3>File system and tree</h3>
|
||||
<p>File system panel contain list of images in current opened directory. You can select file from this list and it will be displayed. It is also possible to
|
||||
use arrow keys to go back (left and up) and forth (right and down) between images.</p>
|
||||
<p>File tree show file system structure. You can right click to show context menu to perform various actions from <i>File</i> menu. There are also few others
|
||||
<ul>
|
||||
<li><i>Set as root directory</i> show only this directory and subdirectories</li>
|
||||
<li><i>Reset root directory</i> show whole file system</li>
|
||||
<li><i>Go up</i> show directory that is one level above current root directory</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Database of FITS/XISF files</h3>
|
||||
<p>Tenmon can scan a directory of FITS/XISF files and index metadata from FITS headers into it's internal database. This allows searching and sorting images based on that metadata.</p>
|
||||
<p>To populate the database, select a directory of FITS/XISF files with <i>File->Index directory</i>.
|
||||
After the selected directory is searched, metadata parsed from the images will be stored in the database.
|
||||
To refresh the database, run <i>File->Reindex</i>. This will update any changed metadata and remove any record of
|
||||
deleted files. To index new files, simply run <i>Index directory</i> again.</p>
|
||||
<p>The database is viewed through a panel which is not visible in the default layout. To add the database panel to the view,
|
||||
toggle <i>Docks->FITS/XISF database</i>. Once visible, database panel shows the database as a table with a column for each property.
|
||||
Below the table is button to select which columns/properties are displayed.</p>
|
||||
|
||||
<p>Also at the bottom of the database panel are three combo boxes and text inputs used for filtering.
|
||||
Select the property to filter on with the combo box and in the adjacent text box enter a string to search for in that property.
|
||||
These three combo box contain list of all properties that are found during indexing except first five. First one set searching in file name.
|
||||
Next two "RA pos" and "DEC pos" allow to filter out indexed images that contain point with entered RA/DEC coordinate. Expected format is three
|
||||
number separated by space. In case of "DEC pos" it also accept +- sign. Omitting one or two last number is also valid. Some examples "02 12 32" "-12 43 12" "+45 32" "13".
|
||||
So for RA it means hour, minutes and seconds while for DEC it is degrees, minutes and seconds.
|
||||
Setting both "RA pos" and "DEC pos" can return images that doesn't contain entered point as it search against minimum and maximum RA/DEC coordinates that images contain.
|
||||
"RA range" and "DEC range" filter out images which center coordinate is within entered range.
|
||||
Pressing Enter or clicking on <i>Filter</i> button will filter out database record according to search parameter.
|
||||
|
||||
<p>Wildcards:
|
||||
<ul>
|
||||
<li><b>%</b> (percent) is a wildcard representing zero or more of any characters.</li>
|
||||
<li><b>_</b> (underscore) is a wildcard for exactly one of any character.</i>
|
||||
<li>Without wildcard characters, the exact string must match.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br><img src=":/about/filter.png"><br>
|
||||
This example filters for files where: "Bias" is in the file name, the OBJECT property is "M_42" (where the underscore can be any single character), and the DATE property begins with "2022".
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Batch processing</h3>
|
||||
|
||||
This module allow to write scripts in JavaScript that process image files. Batch Processing window consist from three main parts. On top is list of input files and directories.
|
||||
You can add directories or individual files to this list. Directories are scanned recursively to find all files even non image files. This list of files is then passed to script in array named <b>files</b>.
|
||||
In script you can then iterate through files like this.
|
||||
<pre>for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
{
|
||||
core.log(file.fileName());
|
||||
file.convert(file.relativeFilePath(), "XISF");
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
<p>Bellow this list is output directory. All relative paths passed as arguments into methods like copy(), move() or convert() will be relative to this output directory. If you pass absolute path to methods then
|
||||
this output directory is ignored.</p>
|
||||
|
||||
<p>Bellow that is list of scripts. These scripts are located in application data directory. Select script which you want to run by clicking on it. Clicking on <i>Open scripts</i> will open directory with these scripts where you create new and edit them.
|
||||
Location of this directory is on Windows: "C:/Users/<USER>/AppData/Roaming/nou/Tenmon" Linux: "~/.local/share/nou/Tenmon/scripts" MacOS: "~/Library/Application Support/nou/Tenmon/scripts"</p>
|
||||
|
||||
<p>Next is Log windows that contain any messages that come from scripts. Mainly calls to <code>core.log()</code> At bottom there buttons that can start or stop execution of selected scripts.</p>
|
||||
|
||||
<h4>core</h4>
|
||||
There is global object called <b>core</b> that have these methods.
|
||||
<ul>
|
||||
<li><b>log(message)</b> print message to log window.</li>
|
||||
<li><b>mark(file)</b> mark file same way as in GUI. Takes object of type <i>File</i> as argument.</li>
|
||||
<li><b>unmark(file)</b> unmark file same was as in GUI. Takes object of type <i>File</i> as argument.</li>
|
||||
<li><b>isMarked(file)</b> check if file was marked. Takes object of type <i>File</i> as argument.</li>
|
||||
<li><b>setMaxThread(maxthread)</b> set maximum number of concurrent thread when doing asynchronous task.</li>
|
||||
<li><b>sync()</b> wait until all asynchronous tasks are done.</li>
|
||||
<li><b>getString(label = "", text = "")</b> show dialog box to get string value from user. String value passed in first argument is used as description label. Second argument text is default value in text box.
|
||||
Both parameters are optional so calling just <i>getString()</i> is valid. When cancel is pressed it return Undefined.</li>
|
||||
<li><b>getInt(label = "", value = 0)</b> show dialog box with input box to retrieve integer value. String value passed in first argument is used as description label.
|
||||
Second parameter is default value in input box. Both parameters are optional. When cancel is pressed it return Undefined.</li>
|
||||
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> show dialog box with input box to retrieve decimal value. String value passed in first argument is used as description label.
|
||||
Second parameter is default value in input box. All three parameters are optional. When cancel is pressed it return Undefined.</li>
|
||||
<li><b>getItem(items)</b> show selection dialog which allow to select one item from array of items. It return selected item as string. When cancel is pressed it return Undefined.</li>
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
In <b>files</b> array there are instances of type <b>File</b> objects that have these methods.
|
||||
<ul>
|
||||
<li><b>fileName()</b> returns the name of the file, excluding the path.</li>
|
||||
<li><b>absoluteFilePath()</b> returns an absolute path including the file name.</li>
|
||||
<li><b>absolutePath()</b> returns an absolute path without the file name</li>
|
||||
<li><b>relativeFilePath()</b> return relative path including file name relative to directory that was in list of directories to be scanned. For example you add C:/images as input directory. In this directory there
|
||||
is file <i>C:/images/lights/red/M42_001.fits</i> then this method will return <i>lights/red/M42_001.fits</i></li>
|
||||
<li><b>relativePath()</b> return same path as previous method just without file name. <i>lights/red</i></li>
|
||||
<li><b>baseName()</b> return file name up to the first dot. For example for <i>some.file.name.fits</i> it will return <i>some</i></li>
|
||||
<li><b>completeBaseName()</b> return file name up to the last dot. For example for <i>some.file.name.fits</i> it will return <i>some.file.name</i></li>
|
||||
<li><b>suffix()</b> return string after last dot in file name. For example <i>fits</i></li>
|
||||
<li><b>size()</b> return size of file in bytes.</li>
|
||||
<li><b>fitsKeywords()</b> return array of strings with every keyword that is in header. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
|
||||
<li><b>fitsValue(key)</b> return value for keyword. In case that there is multiple occurrences it return last one.</li>
|
||||
<li><b>fitsValues(key)</b> return array of values for keyword.</li>
|
||||
<li><b>fitsRecords()</b> return array of objects with properties <b>key, value</b> and <b>comment</b> </li>
|
||||
<li><b>modifyFITSRecords(FITSRecordModify)</b> modify FITS header by adding, removing or updating FITS record. Return true on success. Refer to <i>FITSRecordModify</i></li>
|
||||
<li><b>isMarked()</b> return true if file is marked.</li>
|
||||
<li><b>copy(newpath)</b> copy file to new location. It return instance of new <i>File<i> object that represent this copied file. This path can be relative or absolute. In case that <i>newpath</i> parameter is relative
|
||||
path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can absolute path. File is then copied to this path. In case that copy fail it return null.</li>
|
||||
<li><b>move(newpath)</b> move file to new location. It return false if move failed. This can happend if destination is not writable but also if destination file already exist. This functions does not overwrite existing file.
|
||||
This path can be relative or absolute. In case that <i>newpath</i> parameter is relative path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can be absolute path.
|
||||
File is then moved to this path.</li>
|
||||
<li><b>convert(outpath, format, params)</b> convert image file from any format that program is able to open into FITS, XISF, JPEG, PNG, BMP.
|
||||
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. <i>format</i> one of "FITS" "XISF", "JPG", "PNG" or "BMP". <i>params</i> object with attributes "compressionType" and "compressionLevel".
|
||||
Valid values for compressionType are be "gzip" or "rice" when converting to FITS. When converting to XISF compressionType can be "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
|
||||
It is recommended to use "+sh" variants of compression.
|
||||
XISF format also accept "compressionLevel" in range 0-100 where zero is fastest compression and 100 slowest. If you omit this attribute or set it to -1 then default compression level will be used.
|
||||
It return new instance of <i>File</i> that point to converted file.
|
||||
<pre>file.convert("converted_file.xisf", "xisf", {"compressionType": "zstd+sh", "compressionLevel": 70});
|
||||
file.convert("converted_file.fits", "fits", {"compressionType": "rice"});
|
||||
file.convert("converted_file.jpg", "png");</pre>
|
||||
</li>
|
||||
<li><b>convertAsync(outpath, format, params)</b> same as previous method but it does conversion in separated thread asynchronously and in parallel. Before calling any method on object returned by this method you must call
|
||||
<code>core.sync();</code> to ensure that conversion is done and destination file exists.
|
||||
<pre>let compression = {"compressionType": "zstd+sh"};
|
||||
let convertedFiles = [];
|
||||
for(file in files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
|
||||
}
|
||||
core.sync(); // ensure that files exist
|
||||
for(file of convertedFiles)// now we can iterate over the files
|
||||
{
|
||||
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
|
||||
}</pre></li>
|
||||
<li><b>stats()</b> calculate basic images statistics and return them as object with attributes "mean", "stddev", "median", "min", "max" and "mad".
|
||||
<pre>let s = file.stats();
|
||||
core.log("Median value is " + s.median);</pre></li>
|
||||
</ul>
|
||||
|
||||
<h4>FITSRecordModify</h4>
|
||||
This class is used to define modify operation FITS header in FITS and XISF files. It can remove update and add records. Order of operation is also remove then update and last add.
|
||||
The keyword names may be up to 8 characters long and can only contain uppercase letters, the digits 0-9, the hyphen, and the underscore character.
|
||||
<pre>let modify = new FITSRecordModify();
|
||||
modify.updateKeyword("OBJECT", "M42");
|
||||
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
|
||||
modify.removeKeyword("OBJECT");
|
||||
// doesn't matter that it is specified as last. This will first remove
|
||||
// existing OBJECT record and then add again OBJECT=M42
|
||||
for(file in files)
|
||||
{
|
||||
file.modifyFITSRecords(modify);
|
||||
}</pre>
|
||||
|
||||
<ul>
|
||||
<li><b>new FITSRecordModify()</b> create new instance of object.</li>
|
||||
<li><b>removeKeyword(key);</b> specify removing of record with <i>key</i> as keyword.</li>
|
||||
<li><b>updateKeyword(key, value, comment = "")</b> specify updating existing keyword with value and comment. Comment is optional parameter. If record with keyword doesn't exist then it will add new one.
|
||||
Unless you want to have multiple records with same keyword (for example HISTORY) always use this method and not addKeyword.</li>
|
||||
<li><b>addKeyword(key, value, comment = "")</b> specify adding new keyword</li>
|
||||
</ul>
|
||||
|
||||
<p><small>PS: Kanji in icon means astronomy in Japanese</small></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<head>
|
||||
<style type="text/css">
|
||||
h1, h2, h3, h4 { padding:0px; margin:10px; }
|
||||
p { padding:0px; margin:5px; }
|
||||
img { margin: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Aide de Tenmon</h2>
|
||||
|
||||
<p>Tenmon est destiné principalement à la visualisation de photos et d'images astronomique. Il prend en charge les formats suivants :
|
||||
<ul>
|
||||
<li>FITS 8, 16 bit entier et 32 bit point flottant</li>
|
||||
<li>XISF 8, 16 bit entier et 32 bit point flottant</li>
|
||||
<li>images JPEG, PNG, BMP, GIF, PBM, PGM, PPM et SVG</li>
|
||||
<li>images RAW CR2, CR3, NEF, DNG</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Fenêtre principale</h3>
|
||||
<p>La fenêtre principale affiche l'image actuellement chargée. Sur la gauche se trouve le panneau <i>Informations de l'image</i> qui affiche des détails sur l'image chargée.
|
||||
Le panneau <i>Système de fichier</i> affiche d'autres images dans le même répertoire que l'image chargée.
|
||||
En haut se trouve le menu principal et en dessous se trouve le panneau <i>Réglage de la luminosité</i> contenant diverses options pour l'étirement automatique des images linéaires.</p>
|
||||
<p>Tous les panneaux de l'interface peuvent être déplacés et/ou fermés. Tout panneau fermé ou non visible peut être rouvert via le menu <i>Fenêtres encrables</i> en haut.</p>
|
||||
<p>En bas, il y a une barre d'état qui affiche l'intensité ou la valeur du pixel rouge, vert, bleu sous le curseur de la souris, puis les coordonnées X et Y et,
|
||||
si l'image contient des métadonnées du World Coordinate System, elle affiche les coordonnées célestes.</p>
|
||||
|
||||
<h3>Ouvrir et enregistrer des images</h3>
|
||||
<p>Pour charger une image, sélectionnez <i>Fichier->Ouvrir</i> et choisissez le fichier. Une fois qu'un fichier est chargé,
|
||||
il devient visible dans le panneau d'image et le panneau du <i>système de fichiers</i> affiche les autres images dans le même répertoire.</p>
|
||||
<p>L'image chargée peut être exportée dans un format différent avec <i>Fichier->Enregistrer sous</i>. Tous les formats JPEG, PNG FITS et XISF
|
||||
peuvent être sélectionnés. Dans le cas d'un enregistrement JPEG ou PNG, la fonction d'étirement de la luminosité est appliquée à l'image enregistrée.
|
||||
FITS et XISF sont enregistrés/convertis sans appliquer l'étirement.
|
||||
Pour ouvrir une image, vous pouvez également la faire glisser et la déposer dans la fenêtre principale.</p>
|
||||
|
||||
<h3>Voir</h3>
|
||||
<p>Le menu <i>Voir</i> propose des options pour contrôler la taille et l'échelle des images affichées :
|
||||
<ul>
|
||||
<li><i>Zoom avant</i> et <i>Zoom arrière</i> agrandissent et rétrécissent l'image. La molette de la souris peut également être utilisée pour zoomer librement.</li>
|
||||
<li><i>Meilleur ajustement</i>, zoom automatiquement l'image pour l'adapter à la taille actuelle de la fenêtre.</li>
|
||||
<li><i>100 %</i>, zoom à l'échelle 1:1.</li>
|
||||
<li><i>Plein écran</i> agrandit la fenêtre principale sur tout l'écran.</li>
|
||||
<li><i>Vignettes</i>, affiche de petites vignettes pour toutes les images du répertoire actuel.</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
||||
<h3>Barre d'outils du réglage de la luminosité</h3>
|
||||
<p>Ces outils modifient la luminosité des images affichées.
|
||||
<br><img src=":/about/stretch-panel.png"></p>
|
||||
<p>À partir de la gauche, il y a une échelle de luminosité avec trois points réglables pour contrôler manuellement l'étirement.
|
||||
<ul>
|
||||
<li>point noir - tous les pixels avec une valeur inférieure (plus sombre) que ce paramètre seront écrêtés en noir</li>
|
||||
<li>point médian - définit la valeur à étirer à 50 % d'intensité</li>
|
||||
<li>point blanc - tous les pixels avec une valeur supérieure (plus lumineuse) que celle-ci seront écrêtés en blanc</li>
|
||||
</ul>
|
||||
Après le curseur se trouvent 5 boutons pour la luminosité automatique :
|
||||
<ul>
|
||||
<li><i>Luminosité automatique</i>, applique automatiquement les points noirs et moyens pour rendre l'image avec une luminosité optimale.</li>
|
||||
<li><i>Réinitialiser</i>, réinitialise les trois valeurs pour le point noir, moyen et blanc par défaut.</li>
|
||||
<li><i>Inverser</i>, inverse les couleurs pour afficher l'image en négatif.</li>
|
||||
<li><i>Super pixel CFA </i>, moyenne 2x2 pixels en un (adapté aux images d'une caméra couleur).</li>
|
||||
<li><i>Appliquer la luminosité automatique au chargement</i>, applique la luminosité automatique pour chaque image lors du chargement.</p>
|
||||
</ul>
|
||||
|
||||
<h3>Marquer les images</h3>
|
||||
<p>Les images peuvent être marquées dans le menu <i>Sélectionner</i>. Pour afficher une liste des seules images marquées, utilisez <i>Sélectionner->Afficher marqué</i>. Cette boîte de dialogue peut être utile pour effacer les marques des images. Les images marquées affichent un caractère <b>*</b> dans la barre de titre de la fenêtre principale. Les images marquées peuvent être copiées ou déplacées vers un répertoire sélectionné avec <i>Fichier->Copier/Déplacer les fichiers marqués</i>. Après la copie ou le déplacement, la liste des fichiers marqués est effacée. La liste des fichiers marqués sera mémorisée après avoir quitté le programme.</p>
|
||||
<p>Une autre façon de marquer des images est dans la vue de la base de données où vous pouvez sélectionner des lignes, puis sélectionner l'action marquer ou décocher dans le menu contextuel. Les fichiers marqués seront affichés en texte gras. La troisième façon de marquer les fichiers est de voir les vignettes où vous pouvez appuyer sur <i>Maj</i> et cliquer avec le bouton gauche de la souris et faire glisser sur les vignettes pour les marquer. Maintenir <i>Ctrl</i> décochera les fichiers.</p>
|
||||
|
||||
<h3>Système de fichier et arborescence</h3>
|
||||
<p>Le panneau du système de fichiers contient la liste des images du répertoire ouvert. Vous pouvez sélectionner un fichier dans cette liste et il sera affiché. Il est également possible
|
||||
d'utiliser les touches fléchées pour revenir en arrière (gauche et haut) et avancer (droite et bas) entre les images.</p>
|
||||
<p>L'arborescence des fichiers montre la structure du système de fichiers. Vous pouvez cliquer avec le bouton droit de la souris pour afficher un menu contextuel permettant d'effectuer diverses actions du menu <i>Fichier</i>. Il y a aussi les actions suivantes
|
||||
<ul>
|
||||
<li><i>Définir comme répertoire racine</i> afficher uniquement ce répertoire et ses sous-répertoires</li>
|
||||
<li><i>Réinitialiser la racine</i> afficher tout le système de fichiers</li>
|
||||
<li><i>Monter</i> afficher le répertoire qui est un niveau au-dessus du répertoire racine actuel</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Base de donnée de fichiers FITS/XISF</h3>
|
||||
<p>Tenmon peut analyser un répertoire de fichiers FITS/XISF et indexer les métadonnées des en-têtes FITS dans sa base de données interne. Cela permet de rechercher et de trier des images en fonction de ces métadonnées.</p>
|
||||
<p>Pour remplir la base de données, sélectionnez un répertoire de fichiers FITS/XISF avec <i>Fichier->Indexer le répertoire</i>. Une fois le répertoire parcouru, les métadonnées analysées à partir des images seront stockées dans la base de données. Pour actualiser la base de données, exécutez <i>Fichier-> Ré-indexer les fichiers</i>. Cela mettra à jour toutes les métadonnées modifiées et supprimera tout enregistrement de fichiers supprimés. Pour indexer de nouveaux fichiers, exécutez simplement à nouveau <i>Fichier->Indexer le répertoire</i>.</p>
|
||||
<p>La base de données est visualisée via un panneau qui n'est pas visible dans la mise en page par défaut. Pour ajouter le panneau de base de données à la vue, basculez <i>Fenêtres encrables->Base de données FITS/XISF</i>. Une fois visible, le panneau de la base de données affiche la base de données sous forme de tableau avec une colonne pour chaque propriété. Sous le tableau se trouve un bouton pour sélectionner les colonnes/propriétés à afficher.</p>
|
||||
|
||||
<p>Au bas du panneau de la base de données se trouvent également trois boîtes de liste déroulante et des entrées de texte utilisées pour le filtrage. Sélectionnez la propriété à filtrer dans une liste déroulante et dans la zone de texte adjacente, entrez un texte à rechercher pour cette propriété.
|
||||
Ces trois boîtes contiennent la liste de toutes les propriétés qui sont trouvées pendant l'indexation, sauf les trois premières. La première définit la recherche dans le nom du fichier.
|
||||
Les deux suivantes "RA pos" et "DEC pos" permettent de filtrer les images indexées qui contiennent un point avec les coordonnées RA/DEC entrée. Le format attendu est trois nombres séparés par un espace.
|
||||
Dans le cas de "DEC pos", il accepte également le signe +-. L'omission d'un ou deux derniers chiffres est également valable. Quelques exemples "02 12 32" "-12 43 12" "+45 32" "13".
|
||||
Le fait de définir à la fois "RA pos" et "DEC pos" peut renvoyer des images qui ne contiennent pas le point saisi, car la recherche est faite sur les coordonnées RA/DEC minimum et maximum que les images contiennent.
|
||||
"RA range" et "DEC range" filtrent les images dont les coordonnées centrale se trouvent dans la plage saisie.
|
||||
En appuyant sur la touche Enter ou en cliquant sur le bouton <i>Filtre</i>, les enregistrements de la base de données seront filtrés en fonction des paramètres de recherche.
|
||||
|
||||
<p>Caractères génériques :
|
||||
<ul>
|
||||
<li><b>%</b> (pourcentage) est un caractère générique représentant zéro ou plusieurs caractères.</li>
|
||||
<li><b>_</b> (trait de soulignement) est un caractère générique pour exactement un caractère quelconque.</li>
|
||||
<li>En l'absence de caractères génériques, le texte exacte doit correspondre.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<br><img src=":/about/filter.png"><br>
|
||||
Cet exemple filtre les fichiers où : "Bias" figure dans le nom de fichier, la propriété OBJECT est "M_42" (où le trait de soulignement peut être n'importe quel caractère) et la propriété DATE commence par "2022".
|
||||
</p>
|
||||
|
||||
<h3>Traitement par lot</h3>
|
||||
|
||||
Ce module permet d'écrire des scripts en JavaScript qui traitent des fichiers images. La fenêtre de traitement par lots se compose de trois parties principales. En haut se trouve la liste des fichiers et répertoires d'entrée.
|
||||
Vous pouvez ajouter des répertoires ou des fichiers individuels à cette liste. Les répertoires sont analysés de manière récursive pour trouver tous les fichiers, même les fichiers non image. Cette liste de fichiers est ensuite transmise au script dans un tableau nommé <b>files</b>.
|
||||
Dans le script, vous pouvez ensuite parcourir les fichiers comme ici.
|
||||
<pre>for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
{
|
||||
core.log(file.fileName());
|
||||
file.convert(file.relativeFilePath(), "XISF");
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h4>core</h4>
|
||||
Il existe un objet global appelé <b>core</b> qui possède ces méthodes.
|
||||
<ul>
|
||||
<li><b>log(message)</b> afficher le message dans la fenêtre du journal.</li>
|
||||
<li><b>mark(file)</b> marquer le fichier de la même manière que dans l'interface graphique. Prend un objet de type <i>File</i> comme argument.</li>
|
||||
<li><b>unmark(file)</b> décoche le fichier de la même manière que dans l'interface graphique. Prend un objet de type <i>File</i> comme argument.</li>
|
||||
<li><b>isMarked(file)</b> vérifie si le fichier a été marqué. Prend un objet de type <i>File</i> comme argument.</li>
|
||||
<li><b>setMaxThread(maxthread)</b> définir le nombre maximal de threads simultanés lors de l'exécution d'une tâche asynchrone.</li>
|
||||
<li><b>sync()</b> attendre que toutes les tâches asynchrones soient terminées.</li>
|
||||
<li><b>getString(label = "", text = "")</b> affiche la boîte de dialogue pour obtenir un text de l'utilisateur. La valeur text passée dans le premier argument est utilisée comme label de description. Le texte du deuxième argument est la valeur par défaut dans la zone de texte.
|
||||
Les deux paramètres sont facultatifs, donc l'appel à <i>getString()</i> est valide. Lorsque vous appuyez sur Annuler, il renvoie Undefined</li>
|
||||
<li><b>getInt(label = "", value = 0)</b> affiche une boîte de dialogue avec une zone de saisie pour récupérer une valeur entière. Le texte passé dans le premier argument est utilisé comme label de description.
|
||||
Le deuxième paramètre est la valeur par défaut dans la zone de saisie. Les deux paramètres sont facultatifs. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
|
||||
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> affiche une boîte de dialogue avec une zone de saisie pour récupérer une valeur décimale. Le texte passé dans le premier argument est utilisé comme label de description.
|
||||
Le deuxième paramètre est la valeur par défaut dans la zone de saisie. Les trois paramètres sont facultatifs. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
|
||||
<li><b>getItem(items)</b> affiche une boîte de dialogue de sélection qui permet de sélectionner un élément dans un tableau d'éléments. Lorsque vous appuyez sur Annuler, il renvoie Undefined.</li>
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
Dans le tableau <b>files</b>, il y a des instances d'objets de type <b>File</b> qui ont ces méthodes.
|
||||
<ul>
|
||||
<li><b>fileName()</b> renvoie le nom du fichier, à l'exclusion du chemin.</li>
|
||||
<li><b>absoluteFilePath()</b> renvoie un chemin absolu incluant le nom du fichier.</li>
|
||||
<li><b>absolutePath()</b> renvoie un chemin absolu sans le nom du fichier</li>
|
||||
<li><b>relativeFilePath()</b> renvoie le chemin relatif incluant le nom du fichier par rapport au répertoire qui était dans la liste des répertoires à analyser. Par exemple, vous ajoutez C:/images comme répertoire d'entrée. Dans ce répertoire, il y a
|
||||
le fichier <i>C:/images/lights/red/M42_001.fits</i>, alors cette méthode renverra <i>lights/red/M42_001.fits</i></li>
|
||||
<li><b>relativePath()</b> renvoie le même chemin que la méthode précédente, mais sans le nom de fichier. <i>lights/red</i></li>
|
||||
<li><b>baseName()</b> renvoie le nom du fichier jusqu'au premier point. Par exemple, pour <i>some.file.name.fits</i>, il renverra <i>some</i></li>
|
||||
<li><b>completeBaseName()</b> renvoie le nom du fichier jusqu'au dernier point. Par exemple, pour <i>some.file.name.fits</i>, il renverra <i>some.file.name</i></li>
|
||||
<li><b>suffix()</b> renvoie la chaîne après le dernier point du nom de fichier. Par exemple <i>fits</i></li>
|
||||
<li><b>size()</b> renvoie la taille du fichier en octets.</li>
|
||||
<li><b>fitsKeywords()</b> renvoie un tableau de chaînes avec chaque mot-clé présent dans l'en-tête. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
|
||||
<li><b>fitsValue(key)</b> renvoie la valeur pour le mot-clé. En cas d'occurrences multiples, la dernière est renvoyée.</li>
|
||||
<li><b>fitsValues(key)</b> renvoie un tableau de valeurs pour le mot clé.</li>
|
||||
<li><b>fitsRecords()</b> renvoie un tableau d'objets avec des propriétés <b>key, value</b> et <b>comment</b> </li>
|
||||
<li><b>modifyFITSRecords(FITSRecordModify)</b> modifier l'en-tête FITS en ajoutant, supprimant ou mettant à jour l'enregistrement FITS. Renvoie true en cas de succès. Reportez-vous à <i>FITSRecordModify</i></li>
|
||||
<li><b>isMarked()</b> renvoie true si le fichier est marqué.</li>
|
||||
<li><b>copy(newpath)</b> Copie le fichier vers un nouvel emplacement. Il renvoie une instance du nouvel objet <i>File<i> qui représente ce fichier copié. Ce chemin peut être relatif ou absolu. Dans le cas où le paramètre <i>newpath</i> est un chemin relatif, le "Répertoire de sortie" des fenêtres de l'interface graphique est utilisé comme répertoire de base. Le paramètre <i>newpath</i> peut être un chemin absolu. Le fichier est ensuite copié vers ce chemin. En cas d'échec de la copie, il renvoie null.</li>
|
||||
<li><b>move(newpath)</b> déplacer le fichier vers un nouvel emplacement. Il renvoie false si le déplacement a échoué. Cela peut se produire si la destination n'est pas accessible en écriture mais aussi si le fichier de destination existe déjà. Cette fonction n'écrase pas le fichier existant.
|
||||
Ce chemin peut être relatif ou absolu. Dans le cas où le paramètre <i>newpath</i> est un chemin relatif, le "répertoire de sortie" des fenêtres de l'interface graphique est utilisé comme répertoire de base. Le paramètre <i>newpath</i> peut être un chemin absolu.
|
||||
Le fichier est ensuite déplacé vers ce chemin.</li>
|
||||
<li><b>convert(outpath, format, params)</b> Convertir un fichier image à partir de n'importe quel format que le programme peut ouvrir en FITS, XISF, JPEG, PNG, BMP.
|
||||
Les paramètres sont : <i>outputpath</i> chemin où l'image convertie sera enregistrée. Il remplace automatiquement le suffixe en fonction du format. <i>format</i> l'un des éléments suivants : "FITS", "XISF", "JPG", "PNG" ou "BMP". <i>params</i> objet avec les attributs "compressionType" et "compressionLevel".
|
||||
Les valeurs valides pour compressionType sont "gzip" ou "rice" lors de la conversion en FITS. Lors de la conversion en XISF, compressionType peut être "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
|
||||
Il est recommandé d'utiliser les variantes de compression "+sh".
|
||||
Le format XISF accepte également les "compressionLevel" dans la plage 0-100, où zéro est la compression la plus rapide et 100 la plus lente. Si vous omettez cet attribut ou le définissez sur -1, le niveau de compression par défaut sera utilisé.
|
||||
Il renvoie une nouvelle instance de <i>File</i> qui pointe vers le fichier converti.
|
||||
<pre>file.convert("converted_file.xisf", "xisf", {"compressionType": "zstd+sh", "compressionLevel": 70});
|
||||
file.convert("converted_file.fits", "fits", {"compressionType": "rice"});
|
||||
file.convert("converted_file.jpg", "png");</pre>
|
||||
</li>
|
||||
<li><b>convertAsync(outpath, format, params)</b> même méthode que la précédente, mais effectue la conversion dans un thread séparé de manière asynchrone et en parallèle. Avant d'appeler une méthode sur un objet renvoyé par cette méthode, vous devez appeler
|
||||
<code>core.sync();</code> pour s'assurer que la conversion est effectuée et que le fichier de destination existe.
|
||||
<pre>let compression = {"compressionType": "zstd+sh"};
|
||||
let convertedFiles = [];
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
|
||||
}
|
||||
core.sync(); // ensure that files exist
|
||||
for(file of convertedFiles)// now we can iterate over the files
|
||||
{
|
||||
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
|
||||
}</pre></li>
|
||||
<li><b>stats()</b> calculer les statistiques d'images de base et les renvoyer sous forme d'objet avec des attributs "mean", "stddev", "median", "min", "max" et "mad".
|
||||
<pre>let s = file.stats();
|
||||
core.log("Median value is " + s.median);</pre></li>
|
||||
</ul>
|
||||
|
||||
<h4>FITSRecordModify</h4>
|
||||
Cette classe est utilisée pour définir l'en-tête FITS des opérations de modification dans les fichiers FITS et XISF. Elle peut supprimer, mettre à jour et ajouter des enregistrements. L'ordre des opérations est également le suivant : suppression, puis mise à jour et enfin ajout.
|
||||
Les noms des mots-clés peuvent comporter jusqu'à 8 caractères et ne peuvent contenir que des lettres majuscules, les chiffres de 0 à 9, le trait d'union et le caractère de soulignement.
|
||||
<pre>let modify = new FITSRecordModify();
|
||||
modify.updateKeyword("OBJECT", "M42");
|
||||
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
|
||||
modify.removeKeyword("OBJECT");
|
||||
// Peu importe qu'il soit spécifié comme dernier. Cela supprimera d'abord
|
||||
// l'enregistrement OBJECT existant, puis ajoutera à nouveau OBJECT=M42
|
||||
for(file in files)
|
||||
{
|
||||
file.modifyFITSRecords(modify);
|
||||
}</pre>
|
||||
|
||||
<ul>
|
||||
<li><b>new FITSRecordModify()</b> créer une nouvelle instance de l'objet.</li>
|
||||
<li><b>removeKeyword(key);</b> spécifier la suppression de l'enregistrement avec <i>key</i> comme mot-clé.</li>
|
||||
<li><b>updateKeyword(key, value, comment = "")</b> spécifiez la mise à jour du mot-clé existant avec la valeur et le commentaire. Le commentaire est un paramètre facultatif. Si l'enregistrement avec le mot-clé n'existe pas, il en ajoutera un nouveau.
|
||||
À moins que vous ne souhaitiez avoir plusieurs enregistrements avec le même mot-clé (par exemple HISTORY), utilisez toujours cette méthode et non addKeyword.</li>
|
||||
<li><b>addKeyword(key, value, comment = "")</b> spécifier l'ajout d'un nouveau mot-clé</li>
|
||||
</ul>
|
||||
|
||||
<p><small>PS: Le Kanji de icône (tenmon) signifie astronomie en japonais</small></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<head>
|
||||
<style type="text/css">
|
||||
h1, h2, h3, h4 { padding:0px; margin:10px; }
|
||||
p { padding:0px; margin:5px 5px 10px 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Tenmon pomocník</h2>
|
||||
|
||||
<p>Tenmon slúži primárne na zobrazenie astronomických fotiek a obrázkov. Dokáže otvoriť nasledovné formáty:
|
||||
<ul>
|
||||
<li>FITS 8, 16, 32 bitové celočíselné 32 a 64 bitové s plávajúcou čiarkou</li>
|
||||
<li>XISF 8, 16, 32 bitové celočíselné 32 a 64 bitové s plávajúcou čiarkou</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM a SVG obrázky</li>
|
||||
<li>CR2, CR3, NEF, DNG raw obrázky</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<h3>Hlavné okno</h3>
|
||||
<p>V hlavnom okne sa zobrazujú načítané obrázky. Naľavo sú potom <i>Informácie o obrázku</i> kde sa zobrazujú podrobné
|
||||
informácie o aktuálnom obrázku a <i>Zoznam súborov</i> kde sú všetky obrázky z adresára kde je aktuálne zobrazený obrázok.
|
||||
Hore je hlavné menu a pod ním je <i>Panel úrovní</i>. Všetky panely sa dajú zavrieť a presúvať. Zatvorený panel sa dá znova
|
||||
zobraziť v menu <i>Dokovacie panely</i>.
|
||||
</p>
|
||||
|
||||
<h3>Otváranie a ukladanie obrázkov</h3>
|
||||
<p>Otvoriť obrázok je možné v menu <i>Súbor->Otvoriť</i>. Po vybraní súboru ktorý sa má otvoriť je
|
||||
tento zobrazený v hlavnom okne. Taktiež sú v panely <i>Súborový systém</i> zobrazené ďalšie obrázky v
|
||||
adresári kde sa nachádza zobrazený obrázok.</p>
|
||||
<p>Aktuálne zobrazený obrázok je možné uložiť v inom formáte cez voľbu <i>Súbor->Ulož ako</i>. Dá sa vybrať
|
||||
formát JPEG, PNG, FITS a XISF. V prípade JPEG alebo PNG sa aplikuje funkcia na úpravu úrovní. Pri FITS a XISF
|
||||
sa dáta skonvertujú bez zmeny úrovní.</p>
|
||||
|
||||
<h3>Zobrazenie</h3>
|
||||
<p>Menu <i>Zobrazenie</i> ovplyvňuje veľkosť a škálu zobrazovaných obrázkov.
|
||||
<i>Priblížiť/Oddialiť</i> zväčší a zmenší obrázok. Na toto tiež slúži aj kolečko myši. <i>Najlepšia veľkosť</i> zobrazí
|
||||
obrázok tak aby bol zobrazený na celú plochu. <i>100%</i> zobrazí obrázok v pomere 1:1. <i>Celá obrazovka</i> zobrazí
|
||||
hlavné okno na celú obrazovku. <i>Náhľady</i> zobrazí malé náhľady pre všetky obrázky z aktuálneho adresára.
|
||||
</p>
|
||||
|
||||
<h3>Panel úrovní</h3>
|
||||
<p>
|
||||
Tento panel umožňuje upraviť spôsob ako sa zobrazujú obrazové dáta. Ako prvá je na tomto panely posuvná škála
|
||||
na ktorej sa dajú nastaviť tri body.
|
||||
<br><br><img src=":/about/stretch-panel.png">
|
||||
<ul>
|
||||
<li>čierny bod - všetky pixeli s hodnotou menšou ako nastavená budú zobrazené ako čierne</li>
|
||||
<li>stredný bod - pixeli s touto hodnotou budú zobrazené ako 50% šedá</li>
|
||||
<li>biely bod - pixeli nad touto hodnotou budú zobrazené ako biele</li>
|
||||
</ul>
|
||||
Prvé tlačidlo prepína prepojenie nastavenia čierneho, stretdného a bieleho bodu. Po prepnutí sa dá každý farebný kanál nastaviť
|
||||
samostatne.
|
||||
Nasleduje tlačidlo ktoré nastaví hodnoty čierneho a stredného bodu tak aby bol obrázok zobrazený optimálnym jasom.
|
||||
Druhé tlačidlo resetuje hodnoty pre čierny, stredný a biely bod na východzie hodnoty. Invertovanie farieb zobrazí obrázok ako negatív.
|
||||
Falošné farby zobraí čiernobiele obrázky vo farebnej škále.
|
||||
Prevoď CFA na farbu prevedie demozaikovanie čiernobieleho obrázku na farebný.
|
||||
Posledné tlačidlo zapína a vypína nastavovanie optimálnych hodnôt úrovní pre každý obrázok zvlášť.</p>
|
||||
|
||||
<h3>Označovanie obrázkov</h3>
|
||||
<p>Obrázky je možné si označiť cez menu <i>Výber</i>. Zoznam takto označených obrázkov sa dá zobraziť cez
|
||||
<i>Výber->Ukázať označené</i>. V tomto dialógu sa dá tiež tento zoznam upraviť. Okrem toho sa pri označených
|
||||
obrázkoch zobrazuje znak * v záhlaví hlavného okna. Takto označené obrázky je potom možné skopírovať alebo
|
||||
presunúť do vybraného adresára pomocou <i>Súbor->Skopírovať/Presunúť označené súbory</i>. Po skopírovaní alebo
|
||||
presunutú sa zoznam označených obrázkov vymaže. Program si tento zoznam pamätá aj po svojom ukončení.</p>
|
||||
<p>Ďalší spôsob ako označiť obrázky je cez databázu FITS/XISF kde je možné vybrať jednotlivé riadky. Potom stačí
|
||||
vybrať označit alebo odznačiť v kontextovom menu. Označené súbory budú zobrazené tučným textom. Tretí spôsob na označenie
|
||||
obrázkov je možné cez náhľady. Držaním <i>Shift</i> a následne kliknutím ľavým tlačítkom myši sa daný obrázok označí.
|
||||
Pre odznačenie je treba držať <i>Ctrl</i></p>
|
||||
|
||||
<h3>Databáza FITS/XISF súborov</h3>
|
||||
<p>Program vie prehľadať adresár a indexovať meta údaje z FIST a XISF obrázkov do internej databázy v ktorej sa dá
|
||||
následne vyhľadávať. Najprv je treba indexovať adresár s FIST/XISF obrázkami <i>Súbor->Indexovať adresár</i>.
|
||||
Vybraný adresár je prehľadaný a meta údaje z FIST a XISF obrázkov sú uložené do databázy. Na kontrolu a obnovu datábazy
|
||||
je možné spustiť re-indexáciu <i>Súbor->Reindex</i>. Toto obnoví zmenené údaje a odstráni záznamy o už neexistujúcich súboroch.
|
||||
Pre indexovanie nových súborov je treba znova pustiť indexáciu.</p>
|
||||
<p>Pre zobrazenie databázy je treba zobraziť jej panel cez <i>Dokovacie panely->Databáza FITS/XISF súborov</i>. Databáza je zobrazená vo forme tabuľky
|
||||
kde sú jednotlivé stĺpcoch zobrazené vybrané vlastnosti. V spodnej časti panelu je tlačidlo ktoré zobrazí dialóg na výber zobrazovaných
|
||||
sĺpcov. Nasledujú tri výberové a textové polia. Tieto slúžia na vyhľadávanie v databáze. Výberové pole určuje stĺpec v ktorom sa
|
||||
má vyhľadávať a do textového poľa sa zadáva hodnota na vyhľadanie.
|
||||
|
||||
<p>Zastupné znaky:
|
||||
<ul>
|
||||
<li><b>%</b> (percent) je zastupný znak reprezentujúci žiadný alebo hocikoľko znakov.</li>
|
||||
<li><b>_</b> (underscore) je zastupný znak nahrádzajúci presne jeden znak.</i>
|
||||
<li>Bez zástupných znako sa hľadá presná zhoda.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<br><img src=":/about/filter.png"><br>
|
||||
V nasledovnom príklade sa vyhľadajú súbory ktoré majú v mene súboru "Bias", OBJECT je M_42 a DATE začína reťazcom 2022. Znak % sa berie ako
|
||||
zástupný znak za hocijaký reťazec znakov aj žiadny. Znak _ je tiež zástupný znak zastupujúci práve jeden znak.
|
||||
Bez použitia zástupných znakov sa vyhľadá iba presný výskyt.</p>
|
||||
|
||||
<h3>Hromadné spracovanie</h3>
|
||||
|
||||
Tento modul umožnuje písanie skriptov v JavaScripte ktoré spracujú súbory obrázkov. Okno Hromadného spracovanie pozostáva z troch častí. Navrchu je zoznam vstupných súborov a adresárov.
|
||||
Do zoznamu môžete pridať adresáre alebo jednotlivé súbory. Adresáre sú rekurzívne prehľadané na všetky súbory. Zoznam súborov je potom predaný do skriptu ako pole nazvané <b>files</b>.
|
||||
V skripte potom cez toto pole iteruje nasledovne.
|
||||
<pre>for(file in files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
{
|
||||
core.log(file.fileName());
|
||||
file.convert(file.relativeFilePath(), "XISF");
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h4>core</h4>
|
||||
V skripte je dostupný globálny objekt nazvaný <b>core</b> ktorý má nasledovné metódy.
|
||||
<ul>
|
||||
<li><b>log(message)</b> vypíše message do okna záznamu.</li>
|
||||
<li><b>mark(file)</b> označí súbor rovnako ako cez GUI. Parameter je objekt typu <i>File</i>.</li>
|
||||
<li><b>unmark(file)</b> odznačí súbor rovnako ako cez GUI. Parameter je objekt typu <i>File</i>.</li>
|
||||
<li><b>isMarked(file)</b> overenie či je súbor označený. Parameter je objekt typu <i>File</i>.</li>
|
||||
<li><b>setMaxThread(maxthread)</b> nastavý maximálny počet paralelných vlákien pri vykonávaní asynchrónnych úloh.</li>
|
||||
<li><b>sync()</b> počká kým sa dokončia všetky asynchrónne úlohy.</li>
|
||||
<li><b>getString(label = "", text = "")</b> ukáže dialóg pre získanie textovej hodnoty od používateľa. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Obydva parametre sú voliteľné takže aj volanie <i>getString()</i> je valdiné.
|
||||
Metoda vracia textový retazec alebo Undefined ak bolo stlačené tlačidlo zrušiť.</li>
|
||||
<li><b>getInt(label = "", value = 0)</b> ukáže diálog pre získanie celočíselnej hodnoty. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Obydva parametre sú voliteľné. Vracia zadané číslo alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
|
||||
<li><b>getFloat(label = "", value = 0, decimals = 3)</b> ukáže diálog pre získanie reálneho čísla. Prvý parameter je textový popis. Druhý parameter je východzia hodnota. Tretí parameter je počet desatinných miest.
|
||||
Obydva parametre sú voliteľné. Vracia zadané číslo alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
|
||||
<li><b>getItem(items)</b> ukáže dialog pre výber jednej hodnoty z poľa hodnôt. Vracia vybranú hodnotu ako String alebo ak je stlačené tlačidlo zrušiť vráti Undefined.</li>
|
||||
</ul>
|
||||
|
||||
<h4>File</h4>
|
||||
V poli <b>files</b> sú inštancie objektu typu <b>File</b> ktorý ma nasledovné metódy.
|
||||
<ul>
|
||||
<li><b>fileName()</b> returns the name of the file, excluding the path.</li>
|
||||
<li><b>absoluteFilePath()</b> returns an absolute path including the file name.</li>
|
||||
<li><b>absolutePath()</b> returns an absolute path without the file name</li>
|
||||
<li><b>relativeFilePath()</b> return relative path including file name relative to directory that was in list of directories to be scanned. For example you add C:/images as input directory. In this directory there
|
||||
is file <i>C:/images/lights/red/M42_001.fits</i> then this method will return <i>lights/red/M42_001.fits</i></li>
|
||||
<li><b>relativePath()</b> return same path as previous method just without file name. <i>lights/red</i></li>
|
||||
<li><b>baseName()</b> return file name up to the first dot. For example for <i>some.file.name.fits</i> it will return <i>some</i></li>
|
||||
<li><b>completeBaseName()</b> return file name up to the last dot. For example for <i>some.file.name.fits</i> it will return <i>some.file.name</i></li>
|
||||
<li><b>suffix()</b> return string after last dot in file name. For example <i>fits</i></li>
|
||||
<li><b>size()</b> return size of file in bytes.</li>
|
||||
<li><b>fitsKeywords()</b> return array of strings with every keyword that is in header. <i>SIMPLE,BITPIX,NAXIS,NAXIS1,NAXIS2,EXTEND,COMMENT</i></li>
|
||||
<li><b>fitsValue(key)</b> return value for keyword. In case that there is multiple occurrences it return last one.</li>
|
||||
<li><b>fitsValues(key)</b> return array of values for keyword.</li>
|
||||
<li><b>fitsRecords()</b> return array of objects with properties <b>key, value</b> and <b>comment</b> </li>
|
||||
<li><b>modifyFITSRecords(FITSRecordModify)</b> modify FITS header by adding, removing or updating FITS record. Return true on success. Refer to <i>FITSRecordModify</i></li>
|
||||
<li><b>isMarked()</b> return true if file is marked.</li>
|
||||
<li><b>copy(newpath)</b> copy file to new location. It return instance of new <i>File<i> object that represent this copied file. This path can be relative or absolute. In case that <i>newpath</i> parameter is relative
|
||||
path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can absolute path. File is then copied to this path. In case that copy fail it return null.</li>
|
||||
<li><b>move(newpath)</b> move file to new location. It return false if move failed. This can happend if destination is not writable but also if destination file already exist. This functions does not overwrite existing file.
|
||||
This path can be relative or absolute. In case that <i>newpath</i> parameter is relative path then it "Output directory" from GUI windows is used as base directory. Parameter <i>newpath</i> can be absolute path.
|
||||
File is then moved to this path.</li>
|
||||
<li><b>convert(outpath, format, params)</b> convert image file from any format that program is able to open into FITS, XISF, JPEG, PNG, BMP.
|
||||
Parameters are: <i>outputpath</i> path where converted image will be saved. It automatically replace suffix according to format. <i>format</i> one of "FITS" "XISF", "JPG", "PNG" or "BMP". <i>params</i> object with attributes "compressionType" and "compressionLevel".
|
||||
Valid values for compressionType are be "gzip" or "rice" when converting to FITS. When converting to XISF compressionType can be "zlib", "lz4", "lz4hc", "zstd", "zlib+sh", "lz4+sh", "lz4hc+sh", "zstd+sh".
|
||||
It is recommended to use "+sh" variants of compression.
|
||||
XISF format also accept "compressionLevel" in range 0-100 where zero is fastest compression and 100 slowest. If you omit this attribute or set it to -1 then default compression level will be used.
|
||||
It return new instance of <i>File</i> that point to converted file.
|
||||
<pre>file.convert("converted_file.xisf", "xisf", {"compressionType": "zstd+sh", "compressionLevel": 70});
|
||||
file.convert("converted_file.fits", "fits", {"compressionType": "rice"});
|
||||
file.convert("converted_file.jpg", "png");</pre>
|
||||
</li>
|
||||
<li><b>convertAsync(outpath, format, params)</b> same as previous method but it does conversion in separated thread asynchronously and in parallel. Before calling any method on object returned by this method you must call
|
||||
<code>core.sync();</code> to ensure that conversion is done and destination file exists.
|
||||
<pre>let compression = {"compressionType": "zstd+sh"};
|
||||
let convertedFiles = [];
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits")
|
||||
convertedFiles.push(file.convertAsync("xisf/" + file.fileName(), "XISF", compression));
|
||||
}
|
||||
core.sync(); // ensure that files exist
|
||||
for(file of convertedFiles)// now we can iterate over the files
|
||||
{
|
||||
core.log(file.fileName() + " " + file.size()); // let print compressed file sizes
|
||||
}</pre></li>
|
||||
<li><b>stats()</b> calculate basic images statistics and return them as object with attributes "mean", "stddev", "median", "min", "max" and "mad".
|
||||
<pre>let s = file.stats();
|
||||
core.log("Median value is " + s.median);</pre></li>
|
||||
</ul>
|
||||
|
||||
<h4>FITSRecordModify</h4>
|
||||
This class is used to define modify operation FITS header in FITS and XISF files. It can remove update and add records. Order of operation is also remove then update and last add.
|
||||
The keyword names may be up to 8 characters long and can only contain uppercase letters, the digits 0-9, the hyphen, and the underscore character.
|
||||
<pre>let modify = new FITSRecordModify();
|
||||
modify.updateKeyword("OBJECT", "M42");
|
||||
modify.updateKeyword("MYTILE", "PART1", "adding custom keyword so WBPP can group it");
|
||||
modify.removeKeyword("OBJECT");
|
||||
// doesn't matter that it is specified as last. This will first remove
|
||||
// existing OBJECT record and then add again OBJECT=M42
|
||||
for(file in files)
|
||||
{
|
||||
file.modifyFITSRecords(modify);
|
||||
}</pre>
|
||||
|
||||
<ul>
|
||||
<li><b>new FITSRecordModify()</b> create new instance of object.</li>
|
||||
<li><b>removeKeyword(key);</b> specify removing of record with <i>key</i> as keyword.</li>
|
||||
<li><b>updateKeyword(key, value, comment = "")</b> specify updating existing keyword with value and comment. Comment is optional parameter. If record with keyword doesn't exist then it will add new one.
|
||||
Unless you want to have multiple records with same keyword (for example HISTORY) always use this method and not addKeyword.</li>
|
||||
<li><b>addKeyword(key, value, comment = "")</b> specify adding new keyword</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p><small>PS: Kanji v ikone programu znamená "astronomia" v Japončine</small></p>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,20 @@
|
||||
<table><tr>
|
||||
<td style="padding-right:10px"><img src=":/space.nouspiro.tenmon.png"></td>
|
||||
<td><h3>Tenmon</h3>
|
||||
Tenmon is FITS/XISF image viewer and converter. It also index FITS keywords.<br>
|
||||
v@GITVERSION@ Copyright © 2022 Dušan Poizl<br><br>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify<br>
|
||||
it under the terms of the GNU General Public License as published by<br>
|
||||
the Free Software Foundation, either version 3 of the License, or<br>
|
||||
(at your option) any later version.<br><br>
|
||||
|
||||
This program is distributed in the hope that it will be useful,<br>
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of<br>
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the<br>
|
||||
GNU General Public License for more details.<br><br>
|
||||
|
||||
You should have received a copy of the GNU General Public License<br>
|
||||
along with this program. If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
|
||||
</td>
|
||||
</tr></table>
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "batchprocessing.h"
|
||||
#include "ui_batchprocessing.h"
|
||||
#include <functional>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QStandardPaths>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QCloseEvent>
|
||||
#include <QMessageBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QInputDialog>
|
||||
#include "scriptengine.h"
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QCloseEvent>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#endif
|
||||
|
||||
QList<QPair<QString, QString>> scanDirectories(const QStringList &paths)
|
||||
{
|
||||
QList<QPair<QString, QString>> files;
|
||||
QStringList scannedDirs;
|
||||
|
||||
std::function<void(const QString &root, const QString &path)> scanDirectory = [&](const QString &root, const QString &path)
|
||||
{
|
||||
QFileInfo info(path);
|
||||
if(info.isDir() && !scannedDirs.contains(info.canonicalFilePath()))
|
||||
{
|
||||
scannedDirs.append(info.canonicalFilePath());
|
||||
QDir dir(path);
|
||||
QStringList entries = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
for(QString &entry : entries)
|
||||
scanDirectory(root, dir.absoluteFilePath(entry));
|
||||
}
|
||||
else if(info.isFile())
|
||||
{
|
||||
if(path == root)
|
||||
files.append({path, info.absolutePath()});
|
||||
else
|
||||
files.append({path, root});
|
||||
}
|
||||
};
|
||||
|
||||
for(const QString &path : paths)
|
||||
scanDirectory(path, path);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
void BatchProcessing::scanScriptDir()
|
||||
{
|
||||
QString current;
|
||||
if(_ui->scriptsList->currentItem())
|
||||
current = _ui->scriptsList->currentItem()->text();
|
||||
|
||||
_ui->scriptsList->clear();
|
||||
QDir dir(_scriptBasePath);
|
||||
QDir embededDir(":/scripts");
|
||||
QStringList scripts = dir.entryList(QDir::Files | QDir::Readable);
|
||||
scripts.append(embededDir.entryList(QDir::Files));
|
||||
scripts.removeDuplicates();
|
||||
_ui->scriptsList->addItems(scripts);
|
||||
|
||||
int idx = scripts.indexOf(current);
|
||||
if(idx>=0)_ui->scriptsList->setCurrentRow(idx);
|
||||
}
|
||||
|
||||
BatchProcessing::BatchProcessing(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
_ui = new Ui::BatchProcessing;
|
||||
_ui->setupUi(this);
|
||||
|
||||
QStringList scriptsPath = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
|
||||
if(scriptsPath.size())
|
||||
{
|
||||
QDir dir(scriptsPath.first());
|
||||
if(!dir.exists("scripts"))
|
||||
{
|
||||
if(!dir.mkpath("scripts"))
|
||||
qWarning() << "Failed to create scripts directory";
|
||||
}
|
||||
dir.cd("scripts");
|
||||
|
||||
_scriptBasePath = dir.absolutePath() + "/";
|
||||
scanScriptDir();
|
||||
_fileWatcher.addPath(_scriptBasePath);
|
||||
connect(&_fileWatcher, &QFileSystemWatcher::directoryChanged, this, &BatchProcessing::scanScriptDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to get app data location";
|
||||
}
|
||||
|
||||
connect(_ui->addFilesButton, &QPushButton::released, this, &BatchProcessing::addFiles);
|
||||
connect(_ui->addDirButton, &QPushButton::released, this, &BatchProcessing::addDir);
|
||||
connect(_ui->removeButton, &QPushButton::released, this, &BatchProcessing::removePath);
|
||||
connect(_ui->removeAllButton, &QPushButton::released, this, &BatchProcessing::removeAllPaths);
|
||||
connect(_ui->startButton, &QPushButton::released, this, &BatchProcessing::runScript);
|
||||
connect(_ui->stopButton, &QPushButton::released, this, &BatchProcessing::stopScript);
|
||||
connect(_ui->browseButton, &QPushButton::released, this, &BatchProcessing::browse);
|
||||
connect(_ui->openScriptsButton, &QPushButton::released, this, &BatchProcessing::openScriptDir);
|
||||
|
||||
_textColor = _ui->log->palette().text().color();
|
||||
|
||||
QSettings settings;
|
||||
_ui->outputPath->setText(settings.value("batchprocessing/outputpath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString());
|
||||
}
|
||||
|
||||
BatchProcessing::~BatchProcessing()
|
||||
{
|
||||
delete _engineThread;
|
||||
QSettings settings;
|
||||
settings.setValue("batchprocessing/outputpath", _ui->outputPath->text());
|
||||
delete _ui;
|
||||
}
|
||||
|
||||
void BatchProcessing::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
if(_engineThread)
|
||||
{
|
||||
QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Interrupt running script?"), tr("Interrupt running script?"));
|
||||
if(ret == QMessageBox::StandardButton::Yes)
|
||||
{
|
||||
_engineThread->interrupt();
|
||||
event->accept();
|
||||
}
|
||||
else
|
||||
{
|
||||
event->ignore();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
void BatchProcessing::addFiles()
|
||||
{
|
||||
QSettings settings;
|
||||
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
|
||||
if(!files.isEmpty())
|
||||
{
|
||||
_ui->pathsList->addItems(files);
|
||||
settings.setValue("batchprocessing/inputpath", QFileInfo(files.first()).absolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
void BatchProcessing::addDir()
|
||||
{
|
||||
QSettings settings;
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Select directory"), settings.value("batchprocessing/inputpath", QDir::homePath()).toString());
|
||||
if(!dir.isEmpty())
|
||||
{
|
||||
_ui->pathsList->addItem(dir);
|
||||
settings.setValue("batchprocessing/inputpath", dir);
|
||||
}
|
||||
}
|
||||
|
||||
void BatchProcessing::removePath()
|
||||
{
|
||||
for(auto &item : _ui->pathsList->selectedItems())
|
||||
delete item;
|
||||
}
|
||||
|
||||
void BatchProcessing::removeAllPaths()
|
||||
{
|
||||
_ui->pathsList->clear();
|
||||
}
|
||||
|
||||
void BatchProcessing::browse()
|
||||
{
|
||||
QString output = QFileDialog::getExistingDirectory(this, tr("Select output directory"), "/home/nou/Obrázky");
|
||||
if(!output.isEmpty())
|
||||
_ui->outputPath->setText(output);
|
||||
}
|
||||
|
||||
void BatchProcessing::openScriptDir()
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
QDBusConnection con = QDBusConnection::sessionBus();
|
||||
QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.FileManager1", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1", "ShowFolders");
|
||||
QList<QVariant> args = {QStringList(QUrl::fromLocalFile(_scriptBasePath).toString()), QString()};
|
||||
message.setArguments(args);
|
||||
con.call(message);
|
||||
#endif
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QProcess::startDetached("explorer.exe", {QDir::toNativeSeparators(_scriptBasePath)});
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(_scriptBasePath));
|
||||
#endif
|
||||
}
|
||||
|
||||
void BatchProcessing::runScript()
|
||||
{
|
||||
_ui->log->clear();
|
||||
auto selectedItems = _ui->scriptsList->selectedItems();
|
||||
if(selectedItems.size())
|
||||
{
|
||||
_engineThread = new Script::ScriptEngineThread(this);
|
||||
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
|
||||
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
|
||||
QStringList paths;
|
||||
for(int i=0; i<_ui->pathsList->count(); i++)
|
||||
paths.append(_ui->pathsList->item(i)->text());
|
||||
|
||||
QFileInfo outDir(_ui->outputPath->text());
|
||||
if(outDir.exists() && outDir.isWritable())
|
||||
{
|
||||
QString script = selectedItems.first()->text();
|
||||
if(QDir(_scriptBasePath).exists(script))
|
||||
script = _scriptBasePath + script;
|
||||
else
|
||||
script = ":/scripts/" + script;
|
||||
|
||||
_engineThread->setParams(script, scanDirectories(paths), _ui->outputPath->text());
|
||||
_engineThread->start();
|
||||
_ui->startButton->setEnabled(false);
|
||||
_ui->stopButton->setEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BatchProcessing::stopScript()
|
||||
{
|
||||
qDebug() << "Stop script";
|
||||
if(_engineThread)
|
||||
_engineThread->interrupt();
|
||||
}
|
||||
|
||||
void BatchProcessing::scriptFinished()
|
||||
{
|
||||
_ui->startButton->setEnabled(true);
|
||||
_ui->stopButton->setEnabled(false);
|
||||
qDebug() << "script finished";
|
||||
_engineThread->deleteLater();
|
||||
_engineThread = nullptr;
|
||||
}
|
||||
|
||||
void BatchProcessing::newMessage(const QString &message, bool error)
|
||||
{
|
||||
if(error)_ui->log->setTextColor(Qt::red);
|
||||
else _ui->log->setTextColor(_textColor);
|
||||
_ui->log->append(message);
|
||||
}
|
||||
|
||||
QJSValue BatchProcessing::getString(const QString &label, const QString &text)
|
||||
{
|
||||
bool ok = false;
|
||||
QString ret = QInputDialog::getText(this, tr("Enter text"), label, QLineEdit::Normal, text, &ok);
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
|
||||
QJSValue BatchProcessing::getInt(const QString &label, int value)
|
||||
{
|
||||
bool ok = false;
|
||||
int ret = QInputDialog::getInt(this, tr("Enter integer number"), label, value, INT_MIN, INT_MAX, 1, &ok);
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
|
||||
QJSValue BatchProcessing::getFloat(const QString &label, double value, int decimals)
|
||||
{
|
||||
bool ok = false;
|
||||
double ret = QInputDialog::getDouble(this, tr("Enter float number"), label, value, -INFINITY, INFINITY, decimals, &ok);
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
|
||||
QJSValue BatchProcessing::getItem(const QStringList &items, const QString &label, int current)
|
||||
{
|
||||
bool ok = false;
|
||||
QString ret = QInputDialog::getItem(this, tr("Select item"), label, items, current, false, &ok);
|
||||
return ok ? ret : QJSValue();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef BATCHPROCESSING_H
|
||||
#define BATCHPROCESSING_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFileSystemWatcher>
|
||||
#include "scriptengine.h"
|
||||
|
||||
namespace Ui { class BatchProcessing; }
|
||||
|
||||
class BatchProcessing : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Ui::BatchProcessing *_ui;
|
||||
QString _scriptBasePath;
|
||||
QFileSystemWatcher _fileWatcher;
|
||||
Script::ScriptEngineThread *_engineThread = nullptr;
|
||||
QColor _textColor;
|
||||
private slots:
|
||||
void scanScriptDir();
|
||||
public:
|
||||
explicit BatchProcessing(QWidget *parent = nullptr);
|
||||
~BatchProcessing();
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event);
|
||||
public slots:
|
||||
void addFiles();
|
||||
void addDir();
|
||||
void removePath();
|
||||
void removeAllPaths();
|
||||
void browse();
|
||||
void openScriptDir();
|
||||
void runScript();
|
||||
void stopScript();
|
||||
void scriptFinished();
|
||||
void newMessage(const QString &message, bool error);
|
||||
|
||||
QJSValue getString(const QString &label, const QString &text);
|
||||
QJSValue getInt(const QString &label, int value);
|
||||
QJSValue getFloat(const QString &label, double value, int decimals);
|
||||
QJSValue getItem(const QStringList &items, const QString &label, int current);
|
||||
};
|
||||
|
||||
#endif // BATCHPROCESSING_H
|
||||
@@ -0,0 +1,226 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BatchProcessing</class>
|
||||
<widget class="QDialog" name="BatchProcessing">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1024</width>
|
||||
<height>768</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Batch Processing</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Input files and directories</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="pathsList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::MultiSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="addFilesButton">
|
||||
<property name="text">
|
||||
<string>Add files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addDirButton">
|
||||
<property name="text">
|
||||
<string>Add directories</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeButton">
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeAllButton">
|
||||
<property name="text">
|
||||
<string>Remove all</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Output directory</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="outputPath">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="browseButton">
|
||||
<property name="text">
|
||||
<string>Browse</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Scripts</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="openScriptsButton">
|
||||
<property name="text">
|
||||
<string>Open scripts</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="scriptsList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="log">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>4</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>FreeMono</family>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="startButton">
|
||||
<property name="text">
|
||||
<string>Start script</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="stopButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stop script</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="closeButton">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>closeButton</sender>
|
||||
<signal>released()</signal>
|
||||
<receiver>BatchProcessing</receiver>
|
||||
<slot>close()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>973</x>
|
||||
<y>745</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>511</x>
|
||||
<y>383</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -10,26 +10,44 @@ Database::Database(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool Database::init()
|
||||
bool Database::init(const QLatin1String &connectionName)
|
||||
{
|
||||
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir dir(path);
|
||||
|
||||
m_database = QSqlDatabase::addDatabase("QSQLITE");
|
||||
QSqlDatabase m_database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
|
||||
|
||||
if(!dir.mkpath("."))
|
||||
return false;
|
||||
|
||||
if(m_database.isValid())
|
||||
{
|
||||
m_database.setDatabaseName(dir.absoluteFilePath("database.db"));
|
||||
m_database.setDatabaseName(dir.absoluteFilePath("database2.db"));
|
||||
if(m_database.open())
|
||||
{
|
||||
m_database.exec("PRAGMA foreign_keys = ON");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
|
||||
"key VARCHAR(81), value VARCHAR(81), comment VARCHAR(81), FOREIGN KEY(id_file) REFERENCES fits_files(id) ON DELETE CASCADE)");
|
||||
int version = checkVersion();
|
||||
if(version == 0)
|
||||
{
|
||||
m_database.exec("PRAGMA user_version = 1");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
|
||||
" minRa REAL, maxRa REAL, minDec REAL, maxDec REAL, crVal1 REAL, crVal2 REAL)");
|
||||
m_database.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
|
||||
"key VARCHAR(81), value VARCHAR(81), comment VARCHAR(81), FOREIGN KEY(id_file) REFERENCES fits_files(id) ON DELETE CASCADE)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
|
||||
m_database.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
|
||||
}
|
||||
else if(version > 1)
|
||||
{
|
||||
qDebug() << "Database version is too new";
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlError error = m_database.lastError();
|
||||
|
||||
if(error.type() == QSqlError::NoError)
|
||||
@@ -43,6 +61,8 @@ bool Database::init()
|
||||
|
||||
m_insertFile = QSqlQuery(m_database);
|
||||
m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
|
||||
m_insertFileWcs = QSqlQuery(m_database);
|
||||
m_insertFileWcs.prepare("INSERT INTO fits_files (file, mtime, minRa, maxRa, minDec, maxDec, crVal1, crVal2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
m_insertFitsHeader = QSqlQuery(m_database);
|
||||
m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
|
||||
m_checkFile = QSqlQuery(m_database);
|
||||
@@ -53,7 +73,6 @@ bool Database::init()
|
||||
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
|
||||
return true;
|
||||
}
|
||||
|
||||
qDebug() << error.text();
|
||||
}
|
||||
}
|
||||
@@ -64,27 +83,41 @@ bool Database::mark(const QString &filename)
|
||||
{
|
||||
m_markQuery.bindValue(0, filename);
|
||||
m_markQuery.exec();
|
||||
return checkError();
|
||||
return checkError(m_markQuery);
|
||||
}
|
||||
|
||||
bool Database::unmark(const QString &filename)
|
||||
{
|
||||
m_unmarkQuery.bindValue(0, filename);
|
||||
m_unmarkQuery.exec();
|
||||
return checkError();
|
||||
return checkError(m_unmarkQuery);
|
||||
}
|
||||
|
||||
bool Database::mark(const QStringList &filenames)
|
||||
{
|
||||
m_markQuery.bindValue(0, filenames);
|
||||
m_markQuery.execBatch();
|
||||
return checkError(m_markQuery);
|
||||
}
|
||||
|
||||
bool Database::unmark(const QStringList &filenames)
|
||||
{
|
||||
m_unmarkQuery.bindValue(0, filenames);
|
||||
m_unmarkQuery.execBatch();
|
||||
return checkError(m_unmarkQuery);
|
||||
}
|
||||
|
||||
bool Database::isMarked(const QString &filename)
|
||||
{
|
||||
m_isMarkedQuery.bindValue(":name", filename);
|
||||
m_isMarkedQuery.exec();
|
||||
checkError();
|
||||
checkError(m_isMarkedQuery);
|
||||
return m_isMarkedQuery.next();
|
||||
}
|
||||
|
||||
QStringList Database::getMarkedFiles()
|
||||
{
|
||||
QSqlQuery markedFiles("SELECT * from files", m_database);
|
||||
QSqlQuery markedFiles("SELECT * from files");
|
||||
|
||||
QStringList files;
|
||||
while(markedFiles.next())
|
||||
@@ -92,13 +125,17 @@ QStringList Database::getMarkedFiles()
|
||||
files << markedFiles.value("file").toString();
|
||||
}
|
||||
|
||||
qDebug() << files.size();
|
||||
return files;
|
||||
}
|
||||
|
||||
bool Database::checkError()
|
||||
void Database::clearMarkedFiles()
|
||||
{
|
||||
QSqlError error = m_database.lastError();
|
||||
QSqlDatabase::database().exec("DELETE FROM files");
|
||||
}
|
||||
|
||||
bool Database::checkError(QSqlQuery &query)
|
||||
{
|
||||
QSqlError error = query.lastError();
|
||||
if(error.type() == QSqlError::NoError)
|
||||
return true;
|
||||
else
|
||||
@@ -108,27 +145,79 @@ bool Database::checkError()
|
||||
}
|
||||
}
|
||||
|
||||
static QStringList nameFilters = {"*.fit", "*.fits"};
|
||||
|
||||
static int countFiles(const QDir &dir, int count = 0)
|
||||
int Database::checkVersion()
|
||||
{
|
||||
count += dir.entryList(nameFilters, QDir::Files).size();
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
QSqlQuery query = db.exec("PRAGMA user_version");
|
||||
if(query.next())
|
||||
return query.value(0).toInt();
|
||||
return -1;
|
||||
}
|
||||
|
||||
static QStringList nameFilters = {"*.fit", "*.fits", "*.xisf"};
|
||||
|
||||
static int countFiles(const QDir &dir, QStringList &scannedDirs)
|
||||
{
|
||||
if(scannedDirs.contains(dir.canonicalPath()))return 0;
|
||||
scannedDirs.append(dir.canonicalPath());
|
||||
|
||||
int count = dir.entryList(nameFilters, QDir::Files).size();
|
||||
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for(const QString &d : dirs)
|
||||
count += countFiles(dir.filePath(d));
|
||||
count += countFiles(dir.filePath(d), scannedDirs);
|
||||
return count;
|
||||
}
|
||||
|
||||
void Database::indexDir(const QDir &dir, QProgressDialog *progress)
|
||||
{
|
||||
m_progress = 0;
|
||||
int count = countFiles(dir);
|
||||
QStringList scannedDirs;
|
||||
int count = countFiles(dir, scannedDirs);
|
||||
progress->setMaximum(count);
|
||||
m_database.transaction();
|
||||
if(indexDir2(dir, progress))
|
||||
m_database.commit();
|
||||
QSqlDatabase database = QSqlDatabase::database();
|
||||
database.transaction();
|
||||
|
||||
scannedDirs.clear();
|
||||
if(indexDir2(dir, progress, scannedDirs))
|
||||
{
|
||||
database.commit();
|
||||
emit databaseChanged();
|
||||
}
|
||||
else
|
||||
m_database.rollback();
|
||||
{
|
||||
database.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
void Database::reindex(QProgressDialog *progress)
|
||||
{
|
||||
QVariantList deleteids;
|
||||
QSqlDatabase database = QSqlDatabase::database();
|
||||
database.transaction();
|
||||
QSqlQuery size = database.exec("SELECT COUNT(*) FROM fits_files");
|
||||
size.next();
|
||||
progress->setMaximum(size.value(0).toInt());
|
||||
QSqlQuery files = database.exec("SELECT id,file,mtime FROM fits_files");
|
||||
int i = 0;
|
||||
while(files.next())
|
||||
{
|
||||
QString path = files.value(1).toString();
|
||||
QFileInfo file(path);
|
||||
if(file.exists() && file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate) != files.value(2).toString())
|
||||
indexFile(file);
|
||||
if(!file.exists())
|
||||
deleteids.append(files.value(0));
|
||||
progress->setValue(i++);
|
||||
if(progress->wasCanceled())
|
||||
{
|
||||
database.rollback();
|
||||
return;
|
||||
}
|
||||
}
|
||||
QSqlQuery deleteFiles("DELETE FROM fits_files WHERE id = ?", database);
|
||||
deleteFiles.bindValue(0, deleteids);
|
||||
deleteFiles.execBatch();
|
||||
database.commit();
|
||||
}
|
||||
|
||||
QStringList Database::getFitsKeywords()
|
||||
@@ -142,48 +231,89 @@ QStringList Database::getFitsKeywords()
|
||||
return keywords;
|
||||
}
|
||||
|
||||
|
||||
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
|
||||
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
|
||||
{
|
||||
if(scannedDirs.contains(dir.canonicalPath()))return true;
|
||||
scannedDirs.append(dir.canonicalPath());
|
||||
|
||||
QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
|
||||
QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
for(const QString &d : dirs)
|
||||
{
|
||||
if(!indexDir2(dir.filePath(d), progress))
|
||||
if(!indexDir2(dir.filePath(d), progress, scannedDirs))
|
||||
return false;
|
||||
}
|
||||
ImageInfoData info;
|
||||
for(const QFileInfo &file : files)
|
||||
{
|
||||
progress->setValue(m_progress++);
|
||||
if(progress->wasCanceled())return false;
|
||||
QString filePath = file.absoluteFilePath();
|
||||
QString mtime = file.fileTime(QFileDevice::FileModificationTime).toString(Qt::ISODate);
|
||||
m_checkFile.bindValue(0, filePath);
|
||||
m_checkFile.exec();
|
||||
if(m_checkFile.next())
|
||||
if(!indexFile(file))return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Database::indexFile(const QFileInfo &file)
|
||||
{
|
||||
ImageInfoData info;
|
||||
QString filePath = file.absoluteFilePath();
|
||||
QString mtime = file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate);
|
||||
m_checkFile.bindValue(0, filePath);
|
||||
m_checkFile.exec();
|
||||
if(m_checkFile.next())
|
||||
{
|
||||
if(m_checkFile.value(1).toString() == mtime)
|
||||
return true;
|
||||
else
|
||||
{
|
||||
if(m_checkFile.value(1).toString() == file.fileTime(QFileDevice::FileModificationTime).toString(Qt::ISODate))
|
||||
continue;
|
||||
else
|
||||
m_deleteFile.bindValue(0, m_checkFile.value(0).toLongLong());
|
||||
m_deleteFile.exec();
|
||||
}
|
||||
}
|
||||
|
||||
bool ok;
|
||||
if(filePath.endsWith(".xisf", Qt::CaseInsensitive))
|
||||
ok = readXISFHeader(filePath, info);
|
||||
else
|
||||
ok = readFITSHeader(filePath, info);
|
||||
|
||||
qlonglong last_id = -1;
|
||||
if(ok)
|
||||
{
|
||||
if(info.wcs)
|
||||
{
|
||||
double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
|
||||
info.wcs->calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
|
||||
qDebug() << "bounds" << minRa << maxRa << minDec << maxDec;
|
||||
m_insertFileWcs.bindValue(0, filePath);
|
||||
m_insertFileWcs.bindValue(1, mtime);
|
||||
m_insertFileWcs.bindValue(2, minRa);
|
||||
m_insertFileWcs.bindValue(3, maxRa);
|
||||
m_insertFileWcs.bindValue(4, minDec);
|
||||
m_insertFileWcs.bindValue(5, maxDec);
|
||||
m_insertFileWcs.bindValue(6, crVal1);
|
||||
m_insertFileWcs.bindValue(7, crVal2);
|
||||
if(!m_insertFileWcs.exec())
|
||||
{
|
||||
m_deleteFile.bindValue(0, m_checkFile.value(0).toLongLong());
|
||||
m_deleteFile.exec();
|
||||
qDebug() << "Database error" << m_insertFileWcs.lastError();
|
||||
return false;
|
||||
}
|
||||
last_id = m_insertFileWcs.lastInsertId().toLongLong();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_insertFile.bindValue(0, filePath);
|
||||
m_insertFile.bindValue(1, mtime);
|
||||
if(!m_insertFile.exec())
|
||||
{
|
||||
qDebug() << "Database error" << m_insertFile.lastError();
|
||||
return false;
|
||||
}
|
||||
last_id = m_insertFile.lastInsertId().toLongLong();
|
||||
}
|
||||
|
||||
readFITSHeader(filePath, info);
|
||||
m_insertFile.bindValue(0, filePath);
|
||||
m_insertFile.bindValue(1, mtime);
|
||||
if(!m_insertFile.exec())
|
||||
{
|
||||
qDebug() << m_insertFile.lastError();
|
||||
return false;
|
||||
}
|
||||
qlonglong last_id = m_insertFile.lastInsertId().toLongLong();
|
||||
QVariantList file_id, keys, values, comments;
|
||||
for(auto &record : info.fitsHeader)
|
||||
for(const auto &record : info.fitsHeader)
|
||||
{
|
||||
file_id << last_id;
|
||||
keys << QString(record.key);
|
||||
@@ -196,12 +326,10 @@ bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
|
||||
m_insertFitsHeader.bindValue(3, comments);
|
||||
if(!m_insertFitsHeader.execBatch())
|
||||
{
|
||||
qDebug() << m_insertFitsHeader.lastError();
|
||||
qDebug() << "Database error" << m_insertFitsHeader.lastError();
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << filePath << last_id;
|
||||
info.fitsHeader.clear();
|
||||
}
|
||||
qDebug() << "Indexed" << filePath << last_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
class Database : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QSqlDatabase m_database;
|
||||
QSqlQuery m_markQuery;
|
||||
QSqlQuery m_unmarkQuery;
|
||||
QSqlQuery m_isMarkedQuery;
|
||||
|
||||
QSqlQuery m_insertFile;
|
||||
QSqlQuery m_insertFileWcs;
|
||||
QSqlQuery m_insertFitsHeader;
|
||||
QSqlQuery m_checkFile;
|
||||
QSqlQuery m_headerKeywords;
|
||||
@@ -24,17 +24,25 @@ class Database : public QObject
|
||||
int m_progress;
|
||||
public:
|
||||
explicit Database(QObject *parent = 0);
|
||||
bool init();
|
||||
bool init(const QLatin1String &connectionName = QLatin1String(QSqlDatabase::defaultConnection));
|
||||
bool mark(const QString &filename);
|
||||
bool unmark(const QString &filename);
|
||||
bool mark(const QStringList &filenames);
|
||||
bool unmark(const QStringList &filenames);
|
||||
bool isMarked(const QString &filename);
|
||||
QStringList getMarkedFiles();
|
||||
void clearMarkedFiles();
|
||||
|
||||
void indexDir(const QDir &dir, QProgressDialog *progress);
|
||||
void reindex(QProgressDialog *progress);
|
||||
QStringList getFitsKeywords();
|
||||
protected:
|
||||
bool indexDir2(const QDir &dir, QProgressDialog *progress);
|
||||
bool checkError();
|
||||
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
|
||||
bool indexFile(const QFileInfo &file);
|
||||
bool checkError(QSqlQuery &query);
|
||||
int checkVersion();
|
||||
signals:
|
||||
void databaseChanged();
|
||||
};
|
||||
|
||||
#endif // DATABASE_H
|
||||
|
||||
@@ -6,9 +6,36 @@
|
||||
#include <QHeaderView>
|
||||
#include <QSqlError>
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <iostream>
|
||||
|
||||
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
|
||||
|
||||
double RA(const QString &ra)
|
||||
{
|
||||
QRegularExpression reg("(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
|
||||
QRegularExpressionMatch match = reg.match(ra);
|
||||
double h = match.captured(1).toDouble();
|
||||
double m = match.captured(2).toDouble();
|
||||
double s = match.captured(3).toDouble();
|
||||
qDebug() << "RA" << match.capturedTexts() << h << m << s;
|
||||
return h*15 + m*0.25 + s*15/3600;
|
||||
}
|
||||
|
||||
double DEC(const QString &dec)
|
||||
{
|
||||
QRegularExpression reg("([\\+\\-])?(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
|
||||
QRegularExpressionMatch match = reg.match(dec);
|
||||
double sign = match.captured(1) == "-" ? -1 : 1;
|
||||
double d = match.captured(2).toDouble();
|
||||
double m = match.captured(3).toDouble();
|
||||
double s = match.captured(4).toDouble();
|
||||
qDebug() << "DEC" << match.capturedTexts() << sign << d << m << s;
|
||||
return sign * (d + m/60 + s/3600);
|
||||
}
|
||||
|
||||
SelectColumnsDialog::SelectColumnsDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
m_listWidget = new QListWidget(this);
|
||||
@@ -46,9 +73,9 @@ QStringList SelectColumnsDialog::selectedColumns()
|
||||
return ret;
|
||||
}
|
||||
|
||||
FITSFileModel::FITSFileModel(QObject *parent) : QSqlQueryModel(parent)
|
||||
FITSFileModel::FITSFileModel(Database *database, QObject *parent) : QSqlQueryModel(parent)
|
||||
, m_database(database)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void FITSFileModel::sort(int column, Qt::SortOrder order)
|
||||
@@ -57,8 +84,8 @@ void FITSFileModel::sort(int column, Qt::SortOrder order)
|
||||
m_sort.clear();
|
||||
else if(column <= m_columns.size())
|
||||
{
|
||||
if(column == 0)m_sort = " ORDER BY file ";
|
||||
else m_sort = QString(" ORDER BY \"%1\" ").arg(m_columns.at(column-1));
|
||||
if(column == 0)m_sort = " ORDER BY f.file ";
|
||||
else m_sort = QString(" ORDER BY \"h%1_value\" ").arg(column-1);
|
||||
m_sort += order == Qt::AscendingOrder ? "ASC" : "DESC";
|
||||
prepareQuery();
|
||||
}
|
||||
@@ -70,33 +97,135 @@ void FITSFileModel::setColumns(const QStringList &columns)
|
||||
prepareQuery();
|
||||
}
|
||||
|
||||
void FITSFileModel::setFilter(const QString &key, const QString &value)
|
||||
void FITSFileModel::setFilter(const QStringList &key, const QStringList &value, const QStringList &limit)
|
||||
{
|
||||
if(value.isEmpty())
|
||||
{
|
||||
m_having.clear();
|
||||
m_key.clear();
|
||||
m_value.clear();
|
||||
m_limit.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!m_columns.contains(key))m_columns.append(key);
|
||||
m_having = QString(" HAVING \"%1\" LIKE \"%2\"").arg(key).arg(value);
|
||||
m_key = key;
|
||||
m_value = value;
|
||||
m_limit = limit;
|
||||
}
|
||||
prepareQuery();
|
||||
}
|
||||
|
||||
QVariant FITSFileModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if(role == Qt::FontRole && index.column() == 0)
|
||||
{
|
||||
QFont font;
|
||||
QString file = index.data().toString();
|
||||
font.setBold(m_markedFiles.contains(file));
|
||||
return font;
|
||||
}
|
||||
if(role == Qt::ToolTipRole && index.column() == 0)
|
||||
{
|
||||
return QSqlQueryModel::data(index, Qt::DisplayRole);
|
||||
}
|
||||
return QSqlQueryModel::data(index, role);
|
||||
}
|
||||
|
||||
void FITSFileModel::filesMarked(const QModelIndexList &indexes)
|
||||
{
|
||||
for(auto &index : indexes)
|
||||
{
|
||||
QString file = index.data().toString();
|
||||
if(!m_markedFiles.contains(file))
|
||||
{
|
||||
m_markedFiles.insert(file);
|
||||
emit dataChanged(index, index, {Qt::FontRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FITSFileModel::filesUnmarked(const QModelIndexList &indexes)
|
||||
{
|
||||
for(auto &index : indexes)
|
||||
{
|
||||
QString file = index.data().toString();
|
||||
if(m_markedFiles.contains(file))
|
||||
{
|
||||
m_markedFiles.remove(file);
|
||||
emit dataChanged(index, index, {Qt::FontRole});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FITSFileModel::prepareQuery()
|
||||
{
|
||||
QString sql = "SELECT file,";
|
||||
QString cols;
|
||||
QString join;
|
||||
QStringList where;
|
||||
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
|
||||
for(int i=0; i<m_value.size(); i++)
|
||||
{
|
||||
if(m_key[i] == "file")
|
||||
where.append(QString(" f.file LIKE '%1' ").arg(m_value[i]));
|
||||
else if(m_key[i] == "RA pos")
|
||||
where.append(QString(" %1 BETWEEN f.minRa AND f.maxRa ").arg(RA(m_value[i])));
|
||||
else if(m_key[i] == "DEC pos")
|
||||
where.append(QString(" %1 BETWEEN f.minDec AND f.maxDec ").arg(DEC(m_value[i])));
|
||||
else if(m_key[i] == "RA range")
|
||||
where.append(QString(" crVal1 BETWEEN %1 AND %2 ").arg(RA(m_value[i])).arg(RA(m_limit[i])));
|
||||
else if(m_key[i] == "DEC range")
|
||||
where.append(QString(" crVal2 BETWEEN %1 AND %2 ").arg(DEC(m_value[i])).arg(DEC(m_limit[i])));
|
||||
else
|
||||
join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key='%2' AND s%1.value LIKE '%3'").arg(i).arg(m_key[i]).arg(m_value[i]);
|
||||
}
|
||||
int i=0;
|
||||
for(auto &column : m_columns)
|
||||
{
|
||||
sql += QString("GROUP_CONCAT(CASE WHEN key=\"%1\" THEN value END) AS \"%1\",").arg(column);
|
||||
cols += QString("GROUP_CONCAT(h%1.value) AS h%1_value,").arg(i);
|
||||
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key='%2'").arg(i).arg(column);
|
||||
i++;
|
||||
}
|
||||
sql.chop(1);
|
||||
sql += " FROM fits_files LEFT JOIN fits_headers ON fits_files.id=id_file GROUP BY fits_files.id" + m_having + m_sort;
|
||||
cols.chop(1);
|
||||
sql += cols;
|
||||
sql += " FROM fits_files AS f";
|
||||
sql += join;
|
||||
if(!where.isEmpty())sql += " WHERE " + where.join("AND");
|
||||
sql += " GROUP BY f.id" + m_sort;
|
||||
setQuery(sql);
|
||||
qDebug() << sql;
|
||||
setHeaderData(0, Qt::Horizontal, tr("File name"));
|
||||
i = 1;
|
||||
for(auto &column : m_columns)
|
||||
{
|
||||
setHeaderData(i++, Qt::Horizontal, column);
|
||||
}
|
||||
std::cout << sql.toStdString() << std::endl;
|
||||
if(lastError().type() != QSqlError::NoError)
|
||||
qDebug() << lastError();
|
||||
qDebug() << "Database error" << lastError();
|
||||
|
||||
QStringList list = m_database->getMarkedFiles();
|
||||
m_markedFiles = QSet<QString>(list.begin(), list.end());
|
||||
}
|
||||
|
||||
DatabaseTableView::DatabaseTableView(QWidget *parent) : QTableView(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
QMenu menu;
|
||||
QAction *mark = menu.addAction(tr("Mark"));
|
||||
QAction *unmark = menu.addAction(tr("Unmark"));
|
||||
|
||||
QAction *a = menu.exec(event->globalPos());
|
||||
if(a == nullptr)
|
||||
return;
|
||||
|
||||
QModelIndexList indexes = selectionModel()->selectedRows();
|
||||
|
||||
if(a == mark)
|
||||
emit filesMarked(indexes);
|
||||
else if(a == unmark)
|
||||
emit filesUnmarked(indexes);
|
||||
|
||||
}
|
||||
|
||||
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
|
||||
@@ -105,14 +234,15 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
m_tableView = new QTableView(this);
|
||||
m_tableView = new DatabaseTableView(this);
|
||||
m_tableView->verticalHeader()->setDefaultSectionSize(1);
|
||||
m_tableView->setSortingEnabled(true);
|
||||
m_tableView->horizontalHeader()->setSortIndicatorShown(true);
|
||||
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
layout->addWidget(m_tableView);
|
||||
connect(m_tableView, &QTableView::activated, this, &DataBaseView::itemActivated);
|
||||
|
||||
m_model = new FITSFileModel(this);
|
||||
m_model = new FITSFileModel(m_database, this);
|
||||
|
||||
QSettings settings;
|
||||
m_tableView->setModel(m_model);
|
||||
@@ -126,20 +256,67 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
|
||||
hlayout->addWidget(selectColumnsButton);
|
||||
connect(selectColumnsButton, &QPushButton::pressed, this, &DataBaseView::selectColumns);
|
||||
|
||||
m_filterKeyword = new QComboBox(this);
|
||||
m_filterKeyword->addItem("file");
|
||||
m_filterKeyword->addItems(m_database->getFitsKeywords());
|
||||
connect(m_tableView, &DatabaseTableView::filesMarked, [this](QModelIndexList indexes){
|
||||
QStringList files;
|
||||
for(auto &index : indexes)
|
||||
files.append(index.data().toString());
|
||||
m_database->mark(files);
|
||||
m_model->filesMarked(indexes);
|
||||
});
|
||||
connect(m_tableView, &DatabaseTableView::filesUnmarked, [this](QModelIndexList indexes){
|
||||
QStringList files;
|
||||
for(auto &index : indexes)
|
||||
files.append(index.data().toString());
|
||||
m_database->unmark(files);
|
||||
m_model->filesUnmarked(indexes);
|
||||
});
|
||||
|
||||
m_search = new QLineEdit(this);
|
||||
m_search->setPlaceholderText(tr("Text to search, you can % as wildcard"));
|
||||
connect(m_search, &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
|
||||
auto addFilterItems = [](QComboBox *combobox, const QStringList &fitsKeywords)
|
||||
{
|
||||
combobox->clear();
|
||||
combobox->addItem("file");
|
||||
combobox->addItem("RA pos");
|
||||
combobox->addItem("DEC pos");
|
||||
combobox->addItem("RA range");
|
||||
combobox->addItem("DEC range");
|
||||
combobox->addItems(fitsKeywords);
|
||||
};
|
||||
|
||||
QStringList fitsKeywords = m_database->getFitsKeywords();
|
||||
for(int i=0; i<3; i++)
|
||||
{
|
||||
m_filterKeyword[i] = new QComboBox(this);
|
||||
addFilterItems(m_filterKeyword[i], fitsKeywords);
|
||||
|
||||
|
||||
m_search[i] = new QLineEdit(this);
|
||||
m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));
|
||||
|
||||
m_limit[i] = new QLineEdit(this);
|
||||
m_limit[i]->hide();
|
||||
connect(m_filterKeyword[i], static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this, i](int index){
|
||||
if(index == 3 || index == 4)m_limit[i]->show();
|
||||
else m_limit[i]->hide();
|
||||
});
|
||||
|
||||
connect(m_search[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
|
||||
connect(m_limit[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
|
||||
hlayout->addWidget(m_filterKeyword[i]);
|
||||
hlayout->addWidget(m_search[i]);
|
||||
hlayout->addWidget(m_limit[i]);
|
||||
}
|
||||
|
||||
QPushButton *filterButton = new QPushButton(tr("Filter"), this);
|
||||
connect(filterButton, SIGNAL(pressed()), this, SLOT(applyFilter()));
|
||||
|
||||
hlayout->addWidget(m_filterKeyword);
|
||||
hlayout->addWidget(m_search);
|
||||
hlayout->addWidget(filterButton);
|
||||
|
||||
connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
|
||||
QStringList fitsKeywords = m_database->getFitsKeywords();
|
||||
for(int i=0; i<3; i++)
|
||||
addFilterItems(m_filterKeyword[i], fitsKeywords);
|
||||
|
||||
applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
DataBaseView::~DataBaseView()
|
||||
@@ -175,6 +352,53 @@ void DataBaseView::itemActivated(const QModelIndex &index)
|
||||
|
||||
void DataBaseView::applyFilter()
|
||||
{
|
||||
m_model->setFilter(m_filterKeyword->currentText(), m_search->text());
|
||||
QStringList keys;
|
||||
QStringList values;
|
||||
QStringList limits;
|
||||
for(int i=0; i<3; i++)
|
||||
{
|
||||
QString key = m_filterKeyword[i]->currentText();
|
||||
if(!m_search[i]->text().isEmpty() && !keys.contains(key))
|
||||
{
|
||||
keys.append(key);
|
||||
values.append(m_search[i]->text());
|
||||
limits.append(m_limit[i]->text());
|
||||
}
|
||||
}
|
||||
m_model->setFilter(keys, values, limits);
|
||||
}
|
||||
|
||||
bool DataBaseView::exportCSV(const QString &path)
|
||||
{
|
||||
QFile csv(path);
|
||||
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return false;
|
||||
|
||||
QSqlQuery sql = m_model->query();
|
||||
int colCount = m_model->columnCount();
|
||||
QStringList header;
|
||||
for(int i=0; i<colCount; i++)
|
||||
header.append(m_model->headerData(i, Qt::Horizontal).toString());
|
||||
|
||||
csv.write(header.join(",").toUtf8());
|
||||
csv.write("\n");
|
||||
|
||||
while(sql.next())
|
||||
{
|
||||
QStringList columns;
|
||||
for(int i=0; i<colCount; i++)
|
||||
{
|
||||
QString val = sql.value(i).toString();
|
||||
val.replace("\"", "\"\"");
|
||||
if(val.contains('"') || val.contains(','))
|
||||
{
|
||||
val.prepend('"');
|
||||
val.append('"');
|
||||
}
|
||||
columns.append(val);
|
||||
}
|
||||
csv.write(columns.join(",").toUtf8());
|
||||
csv.write("\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class SelectColumnsDialog : public QDialog
|
||||
Q_OBJECT
|
||||
QListWidget *m_listWidget;
|
||||
public:
|
||||
SelectColumnsDialog(QWidget *parent = nullptr);
|
||||
explicit SelectColumnsDialog(QWidget *parent = nullptr);
|
||||
void setColumns(QStringList columns);
|
||||
QStringList selectedColumns();
|
||||
};
|
||||
@@ -25,32 +25,53 @@ class FITSFileModel : public QSqlQueryModel
|
||||
Q_OBJECT
|
||||
QStringList m_columns;
|
||||
QString m_sort;
|
||||
QString m_having;
|
||||
QStringList m_key;
|
||||
QStringList m_value;
|
||||
QStringList m_limit;
|
||||
QSet<QString> m_markedFiles;
|
||||
Database *m_database;
|
||||
public:
|
||||
FITSFileModel(QObject *parent = nullptr);
|
||||
void sort(int column, Qt::SortOrder order);
|
||||
explicit FITSFileModel(Database *database, QObject *parent = nullptr);
|
||||
void sort(int column, Qt::SortOrder order) override;
|
||||
void setColumns(const QStringList &columns);
|
||||
void setFilter(const QString &key, const QString &value);
|
||||
void setFilter(const QStringList &key, const QStringList &value, const QStringList &limit);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
void filesMarked(const QModelIndexList &indexes);
|
||||
void filesUnmarked(const QModelIndexList &indexes);
|
||||
protected:
|
||||
void prepareQuery();
|
||||
};
|
||||
|
||||
class DatabaseTableView : public QTableView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DatabaseTableView(QWidget *parent = nullptr);
|
||||
protected:
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
signals:
|
||||
void filesMarked(QModelIndexList indexes);
|
||||
void filesUnmarked(QModelIndexList indexes);
|
||||
};
|
||||
|
||||
class DataBaseView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
Database *m_database;
|
||||
QTableView *m_tableView;
|
||||
DatabaseTableView *m_tableView;
|
||||
FITSFileModel *m_model;
|
||||
QComboBox *m_filterKeyword;
|
||||
QLineEdit *m_search;
|
||||
QComboBox *m_filterKeyword[3];
|
||||
QLineEdit *m_search[3];
|
||||
QLineEdit *m_limit[3];
|
||||
public:
|
||||
explicit DataBaseView(Database *database, QWidget *parent = nullptr);
|
||||
~DataBaseView();
|
||||
~DataBaseView() override;
|
||||
public slots:
|
||||
void selectColumns();
|
||||
void loadDatabase();
|
||||
void itemActivated(const QModelIndex &index);
|
||||
void applyFilter();
|
||||
bool exportCSV(const QString &path);
|
||||
signals:
|
||||
void loadFile(QString file);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifdef __linux__
|
||||
|
||||
#define QT_NO_KEYWORDS
|
||||
#include <QString>
|
||||
#include <iostream>
|
||||
#include <gio/gio.h>
|
||||
|
||||
//flatpak bug prevent to use QFile::moveToTrash
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
GFile *gfile = g_file_new_for_path(path.toLocal8Bit().data());
|
||||
GError *error = nullptr;
|
||||
g_file_trash(gfile, nullptr, &error);
|
||||
if(error)std::cerr << "failed to trash file " << error->code << " " << error->message << std::endl;
|
||||
g_clear_error(&error);
|
||||
g_object_unref(gfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
return QFile::moveToTrash(path);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "filesystemwidget.h"
|
||||
#include <QSettings>
|
||||
#include <QVBoxLayout>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
#include <QHeaderView>
|
||||
|
||||
FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) : QWidget(parent)
|
||||
, m_model(model)
|
||||
@@ -13,24 +17,144 @@ FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) :
|
||||
|
||||
setLayout(layout);
|
||||
|
||||
connect(m_listView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(fileClicked(QModelIndex)));
|
||||
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilesystemWidget::fileClicked);
|
||||
}
|
||||
|
||||
void FilesystemWidget::setDir(const QString &dir)
|
||||
void FilesystemWidget::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
//m_model->setRootPath(dir);
|
||||
//m_treeView->setRootIndex(m_model->index(m_model->rootPath()));
|
||||
QMenu menu;
|
||||
menu.addAction(tr("Sort by filename"), [this](){ emit sortChanged(QDir::Name); });
|
||||
menu.addAction(tr("Sort by time"), [this](){ emit sortChanged(QDir::Time); });
|
||||
menu.addAction(tr("Sort by size"), [this](){ emit sortChanged(QDir::Size); });
|
||||
menu.addAction(tr("Sort by type"), [this](){ emit sortChanged(QDir::Type); });
|
||||
menu.addAction(tr("Reverse"), [this](){ emit reverseSort(); });
|
||||
|
||||
menu.exec(event->globalPos());
|
||||
}
|
||||
|
||||
void FilesystemWidget::selectFile(int row)
|
||||
{
|
||||
QModelIndex index = m_model->index(row, 0);
|
||||
m_listView->selectionModel()->clearSelection();
|
||||
m_listView->selectionModel()->select(index, QItemSelectionModel::SelectCurrent);
|
||||
m_listView->scrollTo(index);
|
||||
}
|
||||
|
||||
void FilesystemWidget::fileClicked(const QModelIndex &index)
|
||||
void FilesystemWidget::fileClicked(const QModelIndex &index, const QModelIndex &)
|
||||
{
|
||||
emit fileSelected(index.row());
|
||||
if(index.isValid())
|
||||
emit fileSelected(index.row());
|
||||
}
|
||||
|
||||
QVariant FileSystemModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if(role == Qt::ToolTipRole && index.column() == 0)role = Qt::DisplayRole;
|
||||
return QFileSystemModel::data(index, role);
|
||||
}
|
||||
|
||||
Filetree::Filetree(QWidget *parent) : QTreeView(parent)
|
||||
{
|
||||
QSettings settings;
|
||||
m_rootDir = settings.value("filetree/rootDir", QDir::homePath()).toString();
|
||||
m_fileSystemModel = new FileSystemModel(this);
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
|
||||
m_fileSystemModel->setNameFilterDisables(false);
|
||||
if(settings.value("filetree/showHidden", false).toBool())
|
||||
m_fileSystemModel->setFilter(m_fileSystemModel->filter() | QDir::Hidden);
|
||||
|
||||
setModel(m_fileSystemModel);
|
||||
setRootIndex(m_fileSystemModel->index(m_rootDir));
|
||||
header()->restoreState(settings.value("filetree/header").toByteArray());
|
||||
}
|
||||
|
||||
Filetree::~Filetree()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("filetree/rootDir", m_rootDir);
|
||||
settings.setValue("filetree/header", header()->saveState());
|
||||
settings.setValue("filetree/showHidden", (bool)(m_fileSystemModel->filter() & QDir::Hidden));
|
||||
}
|
||||
|
||||
void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
QModelIndex index = indexAt(event->pos());
|
||||
QFileInfo info = m_fileSystemModel->fileInfo(index);
|
||||
QMenu menu;
|
||||
QAction *open = nullptr;
|
||||
QAction *setRoot = nullptr;
|
||||
QAction *copy = nullptr;
|
||||
QAction *move = nullptr;
|
||||
QAction *indexDir = nullptr;
|
||||
|
||||
if(info.isFile())
|
||||
open = menu.addAction(tr("Open"));
|
||||
|
||||
if(info.isDir())
|
||||
{
|
||||
open = menu.addAction(tr("Open"));
|
||||
setRoot = menu.addAction(tr("Set as root"));
|
||||
copy = menu.addAction(tr("Copy marked files"));
|
||||
move = menu.addAction(tr("Move marked files"));
|
||||
indexDir = menu.addAction(tr("Index directory"));
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
QAction *resetRoot = menu.addAction(tr("Reset root"));
|
||||
QAction *goUp = menu.addAction(tr("Go up"));
|
||||
QAction *showHidden = menu.addAction(tr("Show hidden files"));
|
||||
showHidden->setCheckable(true);
|
||||
showHidden->setChecked(m_fileSystemModel->filter() & QDir::Hidden);
|
||||
|
||||
QAction *a = menu.exec(event->globalPos());
|
||||
if(a == nullptr)
|
||||
return;
|
||||
|
||||
if(a == open)
|
||||
{
|
||||
emit fileSelected(m_fileSystemModel->filePath(index));
|
||||
}
|
||||
else if(a == setRoot && index.isValid())
|
||||
{
|
||||
setRootIndex(index);
|
||||
m_rootDir = m_fileSystemModel->filePath(index);
|
||||
}
|
||||
else if(a == resetRoot)
|
||||
{
|
||||
setRootIndex(QModelIndex());
|
||||
m_rootDir = QDir::rootPath();
|
||||
}
|
||||
else if(a == goUp)
|
||||
{
|
||||
setRootIndex(rootIndex().parent());
|
||||
m_rootDir = m_fileSystemModel->filePath(rootIndex().parent());
|
||||
}
|
||||
else if(a == copy)
|
||||
{
|
||||
emit copyFiles(m_fileSystemModel->filePath(index));
|
||||
}
|
||||
else if(a == move)
|
||||
{
|
||||
emit moveFiles(m_fileSystemModel->filePath(index));
|
||||
}
|
||||
else if(a == indexDir)
|
||||
{
|
||||
emit indexDirectory(m_fileSystemModel->filePath(index));
|
||||
}
|
||||
else if(a == showHidden)
|
||||
{
|
||||
auto filter = m_fileSystemModel->filter();
|
||||
filter ^= QDir::Hidden;
|
||||
m_fileSystemModel->setFilter(filter);
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
}
|
||||
}
|
||||
|
||||
void Filetree::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
QModelIndex index = indexAt(event->pos());
|
||||
QFileInfo info = m_fileSystemModel->fileInfo(index);
|
||||
if(info.isFile())
|
||||
emit fileSelected(info.filePath());
|
||||
else
|
||||
QTreeView::mouseDoubleClickEvent(event);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QWidget>
|
||||
#include <QFileSystemModel>
|
||||
#include <QListView>
|
||||
#include <QTreeView>
|
||||
|
||||
class FilesystemWidget : public QWidget
|
||||
{
|
||||
@@ -12,12 +13,38 @@ class FilesystemWidget : public QWidget
|
||||
QAbstractItemModel *m_model;
|
||||
public:
|
||||
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
|
||||
void setDir(const QString &dir);
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
private slots:
|
||||
void selectFile(int row);
|
||||
void fileClicked(const QModelIndex &index);
|
||||
void fileClicked(const QModelIndex &index, const QModelIndex &);
|
||||
signals:
|
||||
void fileSelected(int row);
|
||||
void sortChanged(QDir::SortFlag sort);
|
||||
void reverseSort();
|
||||
};
|
||||
|
||||
class FileSystemModel : public QFileSystemModel
|
||||
{
|
||||
public:
|
||||
explicit FileSystemModel(QObject *parent) : QFileSystemModel(parent){}
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
};
|
||||
|
||||
class Filetree : public QTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
FileSystemModel *m_fileSystemModel;
|
||||
QString m_rootDir;
|
||||
public:
|
||||
explicit Filetree(QWidget *parent = nullptr);
|
||||
~Filetree() override;
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
signals:
|
||||
void fileSelected(const QString &path);
|
||||
void copyFiles(const QString &path);
|
||||
void moveFiles(const QString &path);
|
||||
void indexDirectory(const QString &path);
|
||||
};
|
||||
|
||||
#endif // FILESYSTEMWIDGET_H
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
set(_build_version "unknown")
|
||||
|
||||
find_package(Git)
|
||||
if(GIT_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} describe --tags HEAD
|
||||
WORKING_DIRECTORY "${local_dir}"
|
||||
OUTPUT_VARIABLE _build_version
|
||||
ERROR_QUIET
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
message(STATUS "GIT hash: ${_build_version}")
|
||||
else()
|
||||
message(STATUS "GIT not found")
|
||||
endif()
|
||||
|
||||
message(STATUS "local:${local_dir} output:${output_dir}")
|
||||
|
||||
configure_file("${local_dir}/gitversion.h.in" "${output_dir}/gitversion.h" @ONLY)
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef GITVERSION_H
|
||||
#define GITVERSION_H
|
||||
|
||||
#define GITVERSION "@_build_version@"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "histogram.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <QPainter>
|
||||
#include <QDebug>
|
||||
|
||||
Histogram::Histogram(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setStyleSheet("QWidget { background: white; color: black; } ");
|
||||
}
|
||||
|
||||
void Histogram::imageLoaded(Image *img)
|
||||
{
|
||||
if(img && img->rawImage())
|
||||
{
|
||||
m_histogram = img->rawImage()->imageStats().m_histogram;
|
||||
m_points.clear();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void Histogram::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), Qt::black);
|
||||
|
||||
uint h = height();
|
||||
uint w = width();
|
||||
|
||||
if(m_histogram.size())
|
||||
{
|
||||
if(m_points.size() != w)
|
||||
{
|
||||
m_points.clear();
|
||||
for(uint64_t i = 0; i < w; i++)
|
||||
{
|
||||
uint32_t sum = 0;
|
||||
uint64_t start = i * m_histogram.size() / w;
|
||||
uint64_t end =(i+1) * m_histogram.size() / w;
|
||||
for(uint64_t o = start; o < end; o++)
|
||||
sum += m_histogram[o];
|
||||
if(start != end)
|
||||
m_points.push_back(sum);
|
||||
}
|
||||
float scale = *std::max_element(m_points.begin(), m_points.end());
|
||||
if(m_log)
|
||||
{
|
||||
scale = std::log(scale);
|
||||
std::for_each(m_points.begin(), m_points.end(), [scale](float &x){ x = (x > 0 ? std::log(x) : 0.0f) / scale; });
|
||||
}
|
||||
else
|
||||
{
|
||||
std::for_each(m_points.begin(), m_points.end(), [scale](float &x){ x /= scale; });
|
||||
}
|
||||
}
|
||||
std::vector<QPointF> points;
|
||||
points.push_back(QPointF(0, h));
|
||||
for(size_t i = 0; i < m_points.size(); i++)
|
||||
{
|
||||
points.push_back(QPointF((float)i * w / m_points.size(), h - m_points[i] * h));
|
||||
}
|
||||
points.push_back(QPoint(w, h));
|
||||
painter.setBrush(Qt::gray);
|
||||
painter.setPen(Qt::white);
|
||||
|
||||
painter.drawPolygon(&points[0], points.size());
|
||||
}
|
||||
|
||||
QStyleOptionButton button;
|
||||
button.initFrom(this);
|
||||
button.state = m_log ? QStyle::State_On : QStyle::State_Off;
|
||||
button.text = tr("Logarithmic scale");
|
||||
button.rect = style()->subElementRect(QStyle::SE_CheckBoxClickRect, &button, this);
|
||||
button.rect.moveTop(0);
|
||||
button.rect.moveRight(w);
|
||||
style()->drawControl(QStyle::CE_CheckBox, &button, &painter, this);
|
||||
}
|
||||
|
||||
void Histogram::mouseReleaseEvent(QMouseEvent *)
|
||||
{
|
||||
m_log = !m_log;
|
||||
m_points.clear();
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef HISTOGRAM_H
|
||||
#define HISTOGRAM_H
|
||||
|
||||
#include <QWidget>
|
||||
#include "imageringlist.h"
|
||||
|
||||
class Histogram : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
std::vector<uint32_t> m_histogram;
|
||||
std::vector<float> m_points;
|
||||
bool m_log = false;
|
||||
public:
|
||||
explicit Histogram(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
void imageLoaded(Image *img);
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mouseReleaseEvent(QMouseEvent *) override;
|
||||
};
|
||||
|
||||
#endif // HISTOGRAM_H
|
||||
@@ -1,27 +0,0 @@
|
||||
#version 130
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_TexCoord0;
|
||||
uniform vec3 mtf_param;
|
||||
uniform bool bw;
|
||||
uniform bool invert;
|
||||
out vec4 color;
|
||||
|
||||
vec4 MTF(vec4 x, vec3 m)
|
||||
{
|
||||
x = (x - m.x) / (m.z - m.x);
|
||||
x = clamp(x, vec4(0.0), vec4(1.0));
|
||||
return ((m.y - 1) * x) / ((2 * m.y - 1) * x - m.y);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
color = texture2D(qt_Texture0, qt_TexCoord0);
|
||||
if(bw)color = color.rrra;
|
||||
color = MTF(color, mtf_param);
|
||||
|
||||
if(invert)color = vec4(1.0) - color;
|
||||
|
||||
if(any(lessThan(qt_TexCoord0, vec2(0.0))) || any(greaterThan(qt_TexCoord0, vec2(1.0))))
|
||||
color = vec4(0.0);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
#include "imageinfo.h"
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <QHeaderView>
|
||||
#include <wcslib/wcshdr.h>
|
||||
#include <wcslib/wcsfix.h>
|
||||
#include <libxisf.h>
|
||||
|
||||
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
|
||||
|
||||
@@ -9,6 +13,52 @@ bool FITSRecord::editable() const
|
||||
return noEditableKey.count(key);
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment) :
|
||||
key(key), value(value), comment(comment)
|
||||
{
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
|
||||
{
|
||||
key = record.name.c_str();
|
||||
comment = record.comment.c_str();
|
||||
|
||||
QString string = record.value.c_str();
|
||||
if(string.startsWith('\'') && string.endsWith('\''))
|
||||
{
|
||||
string.chop(1);
|
||||
string.remove(0, 1);
|
||||
}
|
||||
bool isint;
|
||||
bool isdouble;
|
||||
double vald = string.toDouble(&isdouble);
|
||||
long long vall = string.toLongLong(&isint);
|
||||
if(isint)
|
||||
value = vall;
|
||||
else if(isdouble)
|
||||
value = vald;
|
||||
else if(string == "T" || string == "F")
|
||||
value = string == "T";
|
||||
else
|
||||
value = string;
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const LibXISF::Property &property)
|
||||
{
|
||||
key = property.id.c_str();
|
||||
value = QString::fromStdString(property.value.toString());
|
||||
comment = property.comment.c_str();
|
||||
xisf = true;
|
||||
}
|
||||
|
||||
QByteArray FITSRecord::valueToByteArray() const
|
||||
{
|
||||
if(value.type() == QVariant::Bool)
|
||||
return value.toBool() ? "T" : "F";
|
||||
else
|
||||
return value.toString().toLatin1();
|
||||
}
|
||||
|
||||
ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
|
||||
{
|
||||
setColumnCount(3);
|
||||
@@ -32,7 +82,7 @@ void ImageInfo::setInfo(const ImageInfoData &info)
|
||||
QTreeWidgetItem *fitsHeader = new QTreeWidgetItem({tr("FITS Header")});
|
||||
for(const FITSRecord &record : info.fitsHeader)
|
||||
{
|
||||
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString(), record.comment});
|
||||
new QTreeWidgetItem(fitsHeader, {record.key, record.value.toString().left(1024), record.comment});
|
||||
}
|
||||
addTopLevelItem(fitsHeader);
|
||||
}
|
||||
@@ -47,3 +97,190 @@ void ImageInfo::setInfo(const ImageInfoData &info)
|
||||
}
|
||||
expandAll();
|
||||
}
|
||||
|
||||
void WCSData::freeWCS()
|
||||
{
|
||||
wcsvfree(&nwcs, &wcs);
|
||||
nwcs = 0;
|
||||
wcs = nullptr;
|
||||
}
|
||||
|
||||
WCSData::WCSData(int width, int height, char *header, int nrec) :
|
||||
width(width),
|
||||
height(height)
|
||||
{
|
||||
int nreject = 0;
|
||||
int status = wcspih(header, nrec, 1, 0, &nreject, &nwcs, &wcs);
|
||||
if(status != 0)
|
||||
{
|
||||
freeWCS();
|
||||
return;
|
||||
}
|
||||
status = cdfix(wcs);
|
||||
if(status > 0 || wcs->crpix[0] == 0)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
WCSData::WCSData(int width, int height, const QVector<FITSRecord> &header) :
|
||||
width(width),
|
||||
height(height)
|
||||
{
|
||||
int status = 0;
|
||||
|
||||
QByteArray str;
|
||||
int nrec = 1;
|
||||
for(const FITSRecord &record : header)
|
||||
{
|
||||
if(record.key.startsWith("PV"))continue;
|
||||
|
||||
QByteArray rec;
|
||||
rec.append(record.key.leftJustified(8, ' '));
|
||||
rec.append("= ");
|
||||
rec.append(record.value.toString().toLatin1());
|
||||
rec.append(" / ");
|
||||
rec.append(record.comment);
|
||||
str.append(rec.leftJustified(80, ' ', true));
|
||||
nrec++;
|
||||
}
|
||||
str.append(QByteArray("END").leftJustified(80));
|
||||
|
||||
int nreject = 0;
|
||||
status = wcspih(str.data(), nrec, 1, 0, &nreject, &nwcs, &wcs);
|
||||
if(status != 0)
|
||||
{
|
||||
freeWCS();
|
||||
return;
|
||||
}
|
||||
status = cdfix(wcs);
|
||||
if(status > 0 || wcs->crpix[0] == 0)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
WCSData::~WCSData()
|
||||
{
|
||||
if(wcs)
|
||||
freeWCS();
|
||||
}
|
||||
|
||||
bool WCSData::pixelToWorld(const QPointF &pixel, SkyPoint &point) const
|
||||
{
|
||||
if(!valid())return false;
|
||||
|
||||
double pixcrd[2] = {pixel.x(), pixel.y()};
|
||||
double imgcrd[8] = {0};
|
||||
double phi = 0;
|
||||
double theta = 0;
|
||||
double world[8] = {0};
|
||||
int stat[NWCSFIX] = {0};
|
||||
int status = wcsp2s(wcs, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
|
||||
if(status == 0)
|
||||
{
|
||||
point = SkyPoint(world[0], world[1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WCSData::worldToPixel(const SkyPoint &point, QPointF &pixel) const
|
||||
{
|
||||
if(!valid())return false;
|
||||
|
||||
double world[2] = {point.RA(), point.DEC()};
|
||||
double phi = 0;
|
||||
double theta = 0;
|
||||
double imgcrd[8] = {0};
|
||||
double pixcrd[8] = {0};
|
||||
int stat[NWCSFIX] = {0};
|
||||
int status = wcss2p(wcs, 1, 2, world, &phi, &theta, imgcrd, pixcrd, stat);
|
||||
if(status == 0)
|
||||
{
|
||||
pixel = QPointF(pixcrd[0], pixcrd[1]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WCSData::calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const
|
||||
{
|
||||
if(wcs == nullptr)return;
|
||||
|
||||
minRa = 1000;
|
||||
maxRa = -1000;
|
||||
minDec = 1000;
|
||||
maxDec = -1000;
|
||||
|
||||
if(wcs->crval)
|
||||
{
|
||||
crVal1 = wcs->crval[0];
|
||||
crVal2 = wcs->crval[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
crVal1 = crVal2 = NAN;
|
||||
}
|
||||
|
||||
auto update = [&](const QPointF &pixel)
|
||||
{
|
||||
SkyPoint point;
|
||||
pixelToWorld(pixel, point);
|
||||
minRa = std::min(minRa, point.RA());
|
||||
maxRa = std::max(maxRa, point.RA());
|
||||
minDec = std::min(minDec, point.DEC());
|
||||
maxDec = std::max(maxDec, point.DEC());
|
||||
};
|
||||
|
||||
for(int x=0; x<width; x++)
|
||||
{
|
||||
update(QPointF(x, 0));
|
||||
update(QPointF(x, height - 1));
|
||||
}
|
||||
|
||||
for(int y=0; y<height; y++)
|
||||
{
|
||||
update(QPointF(0, y));
|
||||
update(QPointF(width - 1, y));
|
||||
}
|
||||
|
||||
QPointF ncp;
|
||||
QPointF scp;
|
||||
QRectF s(0, 0, width - 1, height - 1);
|
||||
if(worldToPixel(SkyPoint(0, 90), ncp))
|
||||
{
|
||||
if(s.contains(ncp))
|
||||
maxDec = 90;
|
||||
}
|
||||
|
||||
if(worldToPixel(SkyPoint(0, -90), scp))
|
||||
{
|
||||
if(s.contains(scp))
|
||||
minDec = -90;
|
||||
}
|
||||
}
|
||||
|
||||
SkyPoint::SkyPoint() : ra(NAN), dec(NAN)
|
||||
{
|
||||
}
|
||||
|
||||
SkyPoint::SkyPoint(double ra, double dec) : ra(ra), dec(dec)
|
||||
{
|
||||
}
|
||||
|
||||
void SkyPoint::set(double ra, double dec)
|
||||
{
|
||||
this->ra = ra;
|
||||
this->dec = dec;
|
||||
}
|
||||
|
||||
QString SkyPoint::toString() const
|
||||
{
|
||||
if(std::isnan(ra) || std::isnan(dec))
|
||||
return QString();
|
||||
|
||||
QTime t(0, 0);
|
||||
t = t.addSecs(ra * 240);
|
||||
|
||||
double deg, min, sec;
|
||||
min = std::abs(std::modf(dec, °) * 60);
|
||||
sec = std::modf(min, &min) * 60;
|
||||
return QString("RA: %1 DEC: %2° %3' %4\"").arg(t.toString("HH'h' mm'm' ss's'")).arg(deg, 2, 'f', 0, '0').arg(min, 2, 'f', 0, '0').arg(sec, 2, 'f', 0, '0');
|
||||
}
|
||||
|
||||
@@ -2,19 +2,62 @@
|
||||
#define IMAGEINFO_H
|
||||
|
||||
#include <QTreeWidget>
|
||||
#include <wcslib/wcs.h>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace LibXISF { struct FITSKeyword; struct Property; }
|
||||
|
||||
struct FITSRecord
|
||||
{
|
||||
QByteArray key;
|
||||
QVariant value;
|
||||
QByteArray comment;
|
||||
bool xisf = false;
|
||||
bool editable() const;
|
||||
FITSRecord(){}
|
||||
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
|
||||
FITSRecord(const LibXISF::FITSKeyword &record);
|
||||
FITSRecord(const LibXISF::Property &property);
|
||||
QByteArray valueToByteArray() const;
|
||||
};
|
||||
|
||||
class SkyPoint
|
||||
{
|
||||
double ra = NAN;
|
||||
double dec = NAN;
|
||||
public:
|
||||
SkyPoint();
|
||||
SkyPoint(double ra, double dec);
|
||||
void set(double ra, double dec);
|
||||
double RA() const { return ra; }
|
||||
double DEC() const { return dec; }
|
||||
QString toString() const;
|
||||
};
|
||||
|
||||
class WCSData
|
||||
{
|
||||
int nwcs = 0;
|
||||
struct wcsprm *wcs = nullptr;
|
||||
int width;
|
||||
int height;
|
||||
void freeWCS();
|
||||
public:
|
||||
WCSData(int width, int height, char *header, int nrec);
|
||||
WCSData(int width, int height, const QVector<FITSRecord> &header);
|
||||
WCSData(const WCSData &) = delete;
|
||||
~WCSData();
|
||||
bool pixelToWorld(const QPointF &pixel, SkyPoint &point) const;
|
||||
bool worldToPixel(const SkyPoint &point, QPointF &pixel) const;
|
||||
void calculateBounds(double &minRa, double &maxRa, double &minDec, double &maxDec, double &crVal1, double &crVal2) const;
|
||||
bool valid() const { return wcs; };
|
||||
};
|
||||
|
||||
struct ImageInfoData
|
||||
{
|
||||
QVector<FITSRecord> fitsHeader;
|
||||
QVector<QPair<QString, QString>> info;
|
||||
std::shared_ptr<WCSData> wcs;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ImageInfoData);
|
||||
@@ -32,7 +75,7 @@ class ImageInfo : public QTreeWidget
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImageInfo(QWidget *parent);
|
||||
~ImageInfo();
|
||||
~ImageInfo() override;
|
||||
public slots:
|
||||
void setInfo(const ImageInfoData &info);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#include "imageringlist.h"
|
||||
#include <functional>
|
||||
#include <QThreadPool>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include "loadrunable.h"
|
||||
#include "rawimage.h"
|
||||
#include "database.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
const int DEFAULT_WIDTH = 2;
|
||||
int DEFAULT_WIDTH = 2;
|
||||
|
||||
Image::Image(const QString name, ImageRingList *ringList) :
|
||||
Image::Image(const QString name, int number, ImageRingList *ringList) :
|
||||
m_loading(false),
|
||||
m_released(true),
|
||||
m_current(false),
|
||||
m_number(number),
|
||||
m_name(name),
|
||||
m_ringList(ringList)
|
||||
{
|
||||
@@ -29,6 +34,14 @@ void Image::load()
|
||||
emit pixmapLoaded(this);
|
||||
}
|
||||
|
||||
void Image::loadThumbnail(QThreadPool *pool)
|
||||
{
|
||||
if(!m_thumbnail)
|
||||
pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, true));
|
||||
else
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
|
||||
void Image::release()
|
||||
{
|
||||
m_rawImage.reset();
|
||||
@@ -41,9 +54,14 @@ QString Image::name() const
|
||||
return m_name;
|
||||
}
|
||||
|
||||
RawImage *Image::rawImage()
|
||||
std::shared_ptr<RawImage> Image::rawImage()
|
||||
{
|
||||
return m_rawImage.get();
|
||||
return m_rawImage;
|
||||
}
|
||||
|
||||
const RawImage *Image::thumbnail() const
|
||||
{
|
||||
return m_thumbnail.get();
|
||||
}
|
||||
|
||||
ImageInfoData Image::info() const
|
||||
@@ -56,50 +74,90 @@ bool Image::isCurrent() const
|
||||
return !m_released;
|
||||
}
|
||||
|
||||
void Image::imageLoaded(void *rawImage, ImageInfoData info)
|
||||
int Image::number() const
|
||||
{
|
||||
return m_number;
|
||||
}
|
||||
|
||||
void Image::clearThumbnail()
|
||||
{
|
||||
m_thumbnail.reset();
|
||||
}
|
||||
|
||||
void Image::imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info)
|
||||
{
|
||||
m_loading = false;
|
||||
if(!m_released)
|
||||
{
|
||||
m_rawImage.reset(static_cast<RawImage*>(rawImage));
|
||||
m_rawImage = rawImage;
|
||||
m_info = info;
|
||||
emit pixmapLoaded(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
delete static_cast<RawImage*>(rawImage);
|
||||
}
|
||||
}
|
||||
|
||||
ImageRingList::ImageRingList(QObject *parent) : QAbstractItemModel(parent)
|
||||
void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
|
||||
{
|
||||
m_thumbnail = rawImage;
|
||||
if(m_thumbnail)
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
|
||||
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractItemModel(parent)
|
||||
, m_liveMode(false)
|
||||
, m_analyzeLevel(None)
|
||||
, m_database(database)
|
||||
, m_nameFilter(nameFilter)
|
||||
{
|
||||
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
|
||||
m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
|
||||
m_thumbPool = new QThreadPool(this);
|
||||
|
||||
m_slideShowTimer = new QTimer(this);
|
||||
connect(m_slideShowTimer, &QTimer::timeout, this, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));
|
||||
}
|
||||
|
||||
ImageRingList::~ImageRingList()
|
||||
{
|
||||
QThreadPool::globalInstance()->clear();
|
||||
m_thumbPool->clear();
|
||||
|
||||
QThreadPool::globalInstance()->waitForDone();
|
||||
m_thumbPool->waitForDone();
|
||||
}
|
||||
|
||||
bool ImageRingList::setDir(const QString path, const QString ¤tFile)
|
||||
bool ImageRingList::setDir(const QString path, const QString ¤tFile, bool recursive)
|
||||
{
|
||||
QDir dir(path);
|
||||
|
||||
if(dir.exists())
|
||||
{
|
||||
QStringList nameFilter;
|
||||
nameFilter << "*.jpg" << "*.jpeg" << "*.png" << "*.cr2" << "*.fit" << "*.fits";
|
||||
|
||||
QStringList list = dir.entryList(nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name);
|
||||
QStringList scannedDirs;
|
||||
QStringList absolutePaths;
|
||||
foreach(const QString &file, list)
|
||||
std::function<void(const QString&)> scanDir = [&](const QString &path)
|
||||
{
|
||||
absolutePaths.append(dir.absoluteFilePath(file));
|
||||
}
|
||||
setFiles(absolutePaths, m_liveMode ? list.first() : currentFile);
|
||||
QDir dir(path);
|
||||
if(scannedDirs.contains(dir.canonicalPath()))return;
|
||||
scannedDirs.append(dir.canonicalPath());
|
||||
QDir::SortFlags sortFlags = m_liveMode ? QDir::Time : m_sort | QDir::IgnoreCase;
|
||||
if(m_reversed)sortFlags |= QDir::Reversed;
|
||||
|
||||
if(recursive)
|
||||
{
|
||||
QStringList dirs = dir.entryList(QDir::Readable | QDir::Dirs | QDir::NoDotAndDotDot, sortFlags);
|
||||
for(const QString &subdir : dirs)
|
||||
scanDir(dir.absoluteFilePath(subdir));
|
||||
}
|
||||
|
||||
QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, sortFlags);
|
||||
for(const QString &file : list)
|
||||
{
|
||||
absolutePaths.append(dir.absoluteFilePath(file));
|
||||
}
|
||||
};
|
||||
|
||||
scanDir(path);
|
||||
qDebug() << absolutePaths.size();
|
||||
setFiles(absolutePaths, m_liveMode ? absolutePaths.first() : currentFile);
|
||||
|
||||
m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
|
||||
m_fileSystemWatcher.addPath(path);
|
||||
@@ -112,7 +170,7 @@ void ImageRingList::setFile(const QString &file)
|
||||
{
|
||||
QFileInfo info(file);
|
||||
if(info.isDir())
|
||||
setDir(file);
|
||||
setDir(file, QString(), true);
|
||||
else
|
||||
setDir(info.absolutePath(), file);
|
||||
}
|
||||
@@ -129,6 +187,10 @@ void ImageRingList::increment()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
//don't increment if current image was not loaded yet
|
||||
if(!(*m_currImage)->rawImage())
|
||||
return;
|
||||
|
||||
(*m_firstImage)->release();
|
||||
m_firstImage = increment(m_firstImage);
|
||||
m_currImage = increment(m_currImage);
|
||||
@@ -180,42 +242,94 @@ void ImageRingList::loadFile(int row)
|
||||
{
|
||||
if(row < m_images.size())
|
||||
{
|
||||
m_firstImage = m_currImage = m_lastImage = m_images.begin()+row;
|
||||
if(m_images.empty())
|
||||
return;
|
||||
|
||||
(*m_currImage)->load();
|
||||
|
||||
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(m_liveMode)
|
||||
m_width = 0;
|
||||
|
||||
for(int i=0; i<m_width; i++)
|
||||
int diff = m_currImage != m_images.end() ? row - (m_currImage - m_images.begin()) : -100;
|
||||
if(diff == 1)
|
||||
increment();
|
||||
else if(diff == -1)
|
||||
decrement();
|
||||
else
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
}
|
||||
if(m_lastImage != m_firstImage)
|
||||
{
|
||||
QList<ImagePtr>::iterator iter = increment(m_lastImage);
|
||||
while(m_firstImage != iter)
|
||||
m_firstImage = m_currImage = m_lastImage = m_images.begin()+row;
|
||||
if(m_images.empty())
|
||||
return;
|
||||
|
||||
(*m_currImage)->load();
|
||||
|
||||
m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(m_liveMode)
|
||||
m_width = 0;
|
||||
|
||||
for(int i=0; i<m_width; i++)
|
||||
{
|
||||
(*iter)->release();
|
||||
iter = increment(iter);
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
}
|
||||
if(m_lastImage != m_firstImage)
|
||||
{
|
||||
QList<ImagePtr>::iterator iter = increment(m_lastImage);
|
||||
while(m_firstImage != iter)
|
||||
{
|
||||
(*iter)->release();
|
||||
iter = increment(iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::loadThumbnails()
|
||||
{
|
||||
for(auto &img : m_images)
|
||||
{
|
||||
img->loadThumbnail(m_thumbPool);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::stopLoading()
|
||||
{
|
||||
m_thumbPool->clear();
|
||||
m_thumbPool->waitForDone();
|
||||
}
|
||||
|
||||
int ImageRingList::imageCount() const
|
||||
{
|
||||
return m_images.size();
|
||||
}
|
||||
|
||||
QStringList ImageRingList::imageNames() const
|
||||
{
|
||||
QStringList ret;
|
||||
for(auto &img : m_images)
|
||||
ret.push_back(img->name());
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ImageRingList::updateMark()
|
||||
{
|
||||
if(m_images.size())
|
||||
{
|
||||
QModelIndex idx = index(m_currImage - m_images.begin(), 0);
|
||||
emit dataChanged(idx, idx, {Qt::FontRole});
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::clearThumbnails()
|
||||
{
|
||||
for(auto &img : m_images)
|
||||
img->clearThumbnail();
|
||||
}
|
||||
|
||||
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return createIndex(row, column, m_images.at(row).get());
|
||||
}
|
||||
|
||||
QModelIndex ImageRingList::parent(const QModelIndex &child) const
|
||||
{
|
||||
Q_UNUSED(child);
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
@@ -229,6 +343,7 @@ int ImageRingList::rowCount(const QModelIndex &parent) const
|
||||
|
||||
int ImageRingList::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -241,6 +356,13 @@ QVariant ImageRingList::data(const QModelIndex &index, int role) const
|
||||
QFileInfo info(m_images.at(index.row())->name());
|
||||
return info.fileName();
|
||||
}
|
||||
case Qt::FontRole:
|
||||
{
|
||||
bool marked = m_database->isMarked(m_images.at(index.row())->name());
|
||||
QFont font;
|
||||
font.setBold(marked);
|
||||
return font;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -255,16 +377,86 @@ QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void ImageRingList::setPreload(int width)
|
||||
{
|
||||
DEFAULT_WIDTH = width;
|
||||
if(m_images.size() == 0)return;
|
||||
|
||||
int newWidth = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(newWidth > m_width)
|
||||
{
|
||||
for(int i = newWidth - m_width; i>0; i--)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
}
|
||||
}
|
||||
if(newWidth < m_width)
|
||||
{
|
||||
for(int i = m_width - newWidth; i>0; i--)
|
||||
{
|
||||
(*m_firstImage)->release();
|
||||
m_firstImage = increment(m_firstImage);
|
||||
(*m_lastImage)->release();
|
||||
m_lastImage = decrement(m_lastImage);
|
||||
}
|
||||
}
|
||||
m_width = newWidth;
|
||||
}
|
||||
|
||||
void ImageRingList::setSort(QDir::SortFlag sort)
|
||||
{
|
||||
if(m_sort != sort)
|
||||
{
|
||||
m_sort = sort;
|
||||
if(m_images.size())
|
||||
{
|
||||
QString path = (*m_currImage)->name();
|
||||
setFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::reverseSort()
|
||||
{
|
||||
m_reversed = !m_reversed;
|
||||
if(m_images.size())
|
||||
{
|
||||
QString path = (*m_currImage)->name();
|
||||
setFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::toggleSlideshow(bool start)
|
||||
{
|
||||
if(start)
|
||||
{
|
||||
QSettings settings;
|
||||
int time = settings.value("settings/slideshowtime", 1.0).toDouble() * 1000;
|
||||
m_slideShowTimer->start(time);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_slideShowTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::setFiles(const QStringList files, const QString ¤tFile)
|
||||
{
|
||||
QThreadPool::globalInstance()->clear();
|
||||
m_thumbPool->clear();
|
||||
QThreadPool::globalInstance()->waitForDone();
|
||||
m_thumbPool->waitForDone();
|
||||
beginResetModel();
|
||||
m_images.clear();
|
||||
foreach(const QString &file, files)
|
||||
int i = 0;
|
||||
for(const QString &file : files)
|
||||
{
|
||||
ImagePtr ptr = make_shared<Image>(file, this);
|
||||
ImagePtr ptr = make_shared<Image>(file, i++, this);
|
||||
connect(ptr.get(), SIGNAL(pixmapLoaded(Image*)), this, SLOT(imageLoaded(Image*)));
|
||||
connect(ptr.get(), SIGNAL(thumbnailLoaded(Image*)), this, SIGNAL(thumbnailLoaded(Image*)));
|
||||
m_images.append(ptr);
|
||||
}
|
||||
|
||||
@@ -273,6 +465,7 @@ void ImageRingList::setFiles(const QStringList files, const QString ¤tFile
|
||||
index = 0;
|
||||
|
||||
endResetModel();
|
||||
m_currImage = m_images.end();
|
||||
loadFile(index);
|
||||
}
|
||||
|
||||
@@ -310,5 +503,6 @@ void ImageRingList::dirChanged(QString dir)
|
||||
currentFile = (*m_currImage)->name();
|
||||
|
||||
setDir(dir, currentFile);
|
||||
emit currentImageChanged(m_currImage-m_images.begin());
|
||||
if(m_images.size())
|
||||
emit currentImageChanged(m_currImage-m_images.begin());
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QList>
|
||||
#include <QPixmap>
|
||||
#include <QDir>
|
||||
#include <memory>
|
||||
#include "imageinfo.h"
|
||||
#include "rawimage.h"
|
||||
|
||||
class ImageRingList;
|
||||
class QThreadPool;
|
||||
|
||||
class Image : public QObject
|
||||
{
|
||||
@@ -17,26 +19,36 @@ class Image : public QObject
|
||||
bool m_loading;
|
||||
bool m_released;
|
||||
bool m_current;
|
||||
std::unique_ptr<RawImage> m_rawImage;
|
||||
int m_number;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::shared_ptr<RawImage> m_thumbnail;
|
||||
QString m_name;
|
||||
ImageInfoData m_info;
|
||||
ImageRingList *m_ringList;
|
||||
public:
|
||||
explicit Image(const QString name, ImageRingList *ringList);
|
||||
explicit Image(const QString name, int number, ImageRingList *ringList);
|
||||
void load();
|
||||
void loadThumbnail(QThreadPool *pool);
|
||||
void release();
|
||||
QString name() const;
|
||||
RawImage* rawImage();
|
||||
std::shared_ptr<RawImage> rawImage();
|
||||
const RawImage* thumbnail() const;
|
||||
ImageInfoData info() const;
|
||||
bool isCurrent() const;
|
||||
int number() const;
|
||||
void clearThumbnail();
|
||||
signals:
|
||||
void pixmapLoaded(Image *ptr);
|
||||
void thumbnailLoaded(Image *ptr);
|
||||
protected slots:
|
||||
void imageLoaded(void *rawImage, ImageInfoData info);
|
||||
void imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info);
|
||||
void thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage);
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<Image> ImagePtr;
|
||||
|
||||
class Database;
|
||||
|
||||
class ImageRingList : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -47,21 +59,31 @@ class ImageRingList : public QAbstractItemModel
|
||||
QList<ImagePtr>::iterator m_lastImage;
|
||||
QFileSystemWatcher m_fileSystemWatcher;
|
||||
bool m_liveMode;
|
||||
QDir::SortFlag m_sort = QDir::Name;
|
||||
bool m_reversed = false;
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
QThreadPool *m_thumbPool;
|
||||
Database *m_database;
|
||||
QStringList m_nameFilter;
|
||||
QTimer *m_slideShowTimer;
|
||||
public:
|
||||
explicit ImageRingList(QObject *parent = 0);
|
||||
~ImageRingList();
|
||||
bool setDir(const QString path, const QString ¤tFile = QString());
|
||||
explicit ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent = 0);
|
||||
~ImageRingList() override;
|
||||
bool setDir(const QString path, const QString ¤tFile = QString(), bool recursive = false);
|
||||
void setFile(const QString &file);
|
||||
ImagePtr currentImage();
|
||||
void increment();
|
||||
void decrement();
|
||||
void setLiveMode(bool live);
|
||||
void setCalculateStats(bool stats);
|
||||
void setFindPeaks(bool findPeaks);
|
||||
void setFindStars(bool findStars);
|
||||
AnalyzeLevel analyzeLevel() const;
|
||||
void loadFile(int row);
|
||||
void loadThumbnails();
|
||||
void stopLoading();
|
||||
int imageCount() const;
|
||||
QStringList imageNames() const;
|
||||
void updateMark();
|
||||
void clearThumbnails();
|
||||
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
@@ -69,12 +91,20 @@ public:
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
public slots:
|
||||
void setPreload(int width);
|
||||
void setSort(QDir::SortFlag sort);
|
||||
void reverseSort();
|
||||
void toggleSlideshow(bool start);
|
||||
void increment();
|
||||
void decrement();
|
||||
protected:
|
||||
void setFiles(const QStringList files, const QString ¤tFile = QString());
|
||||
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
|
||||
QList<ImagePtr>::iterator decrement(QList<ImagePtr>::iterator iter);
|
||||
signals:
|
||||
void pixmapLoaded(Image *image);
|
||||
void thumbnailLoaded(Image *image);
|
||||
void infoLoaded(ImageInfoData info);
|
||||
void currentImageChanged(int index);
|
||||
protected slots:
|
||||
|
||||
@@ -106,7 +106,7 @@ void ImageScrollArea::wheelEvent(QWheelEvent *event)
|
||||
m_scale = (float)size().width()/m_pixmap.size().width();
|
||||
|
||||
QPointF top(horizontalScrollBar()->value(), verticalScrollBar()->value());
|
||||
QPointF mousePos = (top + event->posF()) / m_scale;
|
||||
QPointF mousePos = (top + event->position()) / m_scale;
|
||||
|
||||
QPoint delta = event->angleDelta();
|
||||
if(delta.y() > 0)
|
||||
@@ -115,7 +115,7 @@ void ImageScrollArea::wheelEvent(QWheelEvent *event)
|
||||
setScale(m_scale - 0.1);
|
||||
|
||||
mousePos *= m_scale;
|
||||
top = mousePos - event->posF();
|
||||
top = mousePos - event->position();
|
||||
horizontalScrollBar()->setValue(top.x());
|
||||
verticalScrollBar()->setValue(top.y());
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ public slots:
|
||||
void bestFit();
|
||||
void oneToOne();
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event);
|
||||
void keyReleaseEvent(QKeyEvent *event);
|
||||
void mouseMoveEvent(QMouseEvent *event);
|
||||
void mousePressEvent(QMouseEvent *event);
|
||||
void resizeEvent(QResizeEvent *event);
|
||||
void wheelEvent(QWheelEvent *event);
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void keyReleaseEvent(QKeyEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // IMAGESCROLLAREA_H
|
||||
|
||||
@@ -1,59 +1,86 @@
|
||||
#include "imagescrollareagl.h"
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLVersionFunctionsFactory>
|
||||
#include <QDebug>
|
||||
#include <QKeyEvent>
|
||||
#include <QOpenGLDebugLogger>
|
||||
#include <QOpenGLPixelTransferOptions>
|
||||
#include <QOpenGLFramebufferObject>
|
||||
#include <QGridLayout>
|
||||
#include <QMimeData>
|
||||
#include <QMessageBox>
|
||||
#include <QCoreApplication>
|
||||
#include <QPainter>
|
||||
#include <QFileInfo>
|
||||
#include <cmath>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
int FILTERING = 1;
|
||||
|
||||
struct RawImageType
|
||||
{
|
||||
QOpenGLTexture::PixelFormat pixelFormat;
|
||||
QOpenGLTexture::TextureFormat textureFormat;
|
||||
QOpenGLTexture::PixelType dataType;
|
||||
bool bw;
|
||||
};
|
||||
|
||||
const RawImageType rawImageTypes[] = {
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R8_UNorm, QOpenGLTexture::UInt8, true},
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R16_UNorm, QOpenGLTexture::UInt16, true},
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R32F, QOpenGLTexture::Float32, true},
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::BGRA,QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
|
||||
};
|
||||
|
||||
void setScrollRange(QScrollBar *scrollBar, int newRange)
|
||||
RawImageType getRawImageType(const RawImage *img)
|
||||
{
|
||||
int page = scrollBar->pageStep();
|
||||
int pos = scrollBar->value() + page/2;
|
||||
int range = scrollBar->maximum() + page;
|
||||
float relPos = (float)pos/(float)range;
|
||||
RawImageType type;
|
||||
switch(img->type())
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
if(img->channels() >= 3)
|
||||
type.textureFormat = QOpenGLTexture::SRGB8_Alpha8;
|
||||
else
|
||||
type.textureFormat = QOpenGLTexture::R8_UNorm;
|
||||
type.dataType = QOpenGLTexture::UInt8;
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
if(img->channels() >= 3)
|
||||
type.textureFormat = QOpenGLTexture::RGBA16_UNorm;
|
||||
else
|
||||
type.textureFormat = QOpenGLTexture::R16_UNorm;
|
||||
type.dataType = QOpenGLTexture::UInt16;
|
||||
break;
|
||||
case RawImage::FLOAT32:
|
||||
if(img->channels() >= 3)
|
||||
type.textureFormat = QOpenGLTexture::RGBA32F;
|
||||
else
|
||||
type.textureFormat = QOpenGLTexture::R32F;
|
||||
type.dataType = QOpenGLTexture::Float32;
|
||||
break;
|
||||
default:
|
||||
qWarning() << "Invalid format" << img->type();
|
||||
break;
|
||||
}
|
||||
|
||||
if(page >= newRange)
|
||||
scrollBar->hide();
|
||||
if(img->channels() >= 3)
|
||||
type.pixelFormat = QOpenGLTexture::RGBA;
|
||||
else
|
||||
scrollBar->show();
|
||||
type.pixelFormat = QOpenGLTexture::Red;
|
||||
|
||||
scrollBar->setRange(0, newRange - page);
|
||||
scrollBar->setValue(relPos*newRange - page/2);
|
||||
return type;
|
||||
}
|
||||
|
||||
ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
|
||||
ImageWidget::ImageWidget(Database *database, QWidget *parent) : QOpenGLWidget(parent)
|
||||
, m_database(database)
|
||||
{
|
||||
setFocusPolicy(Qt::ClickFocus);
|
||||
m_range = UINT16_MAX;
|
||||
m_low = 0;
|
||||
m_mid = 0.5;
|
||||
m_high = 1;
|
||||
m_dx = m_dy = 0;
|
||||
m_scale = 1.0f;
|
||||
m_blockRepaint = false;
|
||||
m_range = UINT16_MAX;
|
||||
m_imgWidth = m_imgHeight = -1;
|
||||
m_superpixel = m_invert = false;
|
||||
m_updateTimer = new QTimer(this);
|
||||
m_updateTimer->setInterval(500);
|
||||
m_updateTimer->setSingleShot(true);
|
||||
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(update()));
|
||||
setAcceptDrops(true);
|
||||
QTimer::singleShot(1000, [this](){
|
||||
if(!isValid())
|
||||
{
|
||||
QMessageBox::critical(this, tr("OpenGL error"), tr("Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed."));
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
});
|
||||
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
ImageWidget::~ImageWidget()
|
||||
@@ -61,16 +88,40 @@ ImageWidget::~ImageWidget()
|
||||
makeCurrent();
|
||||
}
|
||||
|
||||
void ImageWidget::setImage(const RawImage *image)
|
||||
void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
|
||||
{
|
||||
if(image == nullptr)return;
|
||||
m_currentImg = index;
|
||||
|
||||
if(!image || !image->valid())
|
||||
{
|
||||
m_imgWidth = 0;
|
||||
m_imgHeight = 0;
|
||||
m_error = tr("Failed to load image");
|
||||
m_rawImage.reset();
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
m_error.clear();
|
||||
makeCurrent();
|
||||
m_rawImage = image;
|
||||
if((int)image->width() > m_maxTextureSize || (int)image->height() > m_maxTextureSize)
|
||||
m_rawImage->resize(std::min(m_maxTextureSize, (int)image->width()), std::min(m_maxTextureSize, (int)image->height()));
|
||||
|
||||
m_imgWidth = image->width();
|
||||
m_imgHeight = image->height();
|
||||
|
||||
const RawImageType &rawImageType = rawImageTypes[image->type()];
|
||||
|
||||
if(!m_image)return;
|
||||
|
||||
RawImageType rawImageType = getRawImageType(image.get());
|
||||
m_srgb = rawImageType.textureFormat == QOpenGLTexture::SRGB8_Alpha8;
|
||||
m_bwImg = image->channels() == 1;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
m_image->destroy();
|
||||
m_image->setAutoMipMapGenerationEnabled(false);
|
||||
m_image->setFormat(rawImageType.textureFormat);
|
||||
m_image->setSize(image->width(), image->height());
|
||||
m_image->setMipLevels([&](){ int c = 0; int s = std::min(m_imgWidth, m_imgHeight); while(s>>=1)c++; return c; }());
|
||||
@@ -78,60 +129,131 @@ void ImageWidget::setImage(const RawImage *image)
|
||||
m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
|
||||
m_image->setWrapMode(QOpenGLTexture::ClampToEdge);
|
||||
m_image->setBorderColor(0, 0, 0, 0);
|
||||
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, image->data(), m_transferOptions.get());
|
||||
m_image->setLevelOfDetailRange(m_superpixel ? 1 : 0, m_image->mipMaxLevel());
|
||||
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, (const void*)image->data(), m_transferOptions.get());
|
||||
m_image->generateMipMaps();
|
||||
m_bwImg = rawImageType.bw;
|
||||
qDebug() << "setImage" << timer.elapsed();
|
||||
|
||||
m_unit_scale[0] = 1.0f;
|
||||
m_unit_scale[1] = 0.0f;
|
||||
if(image->type() == RawImage::FLOAT32)
|
||||
{
|
||||
auto unitScaling = image->unitScale();
|
||||
m_unit_scale[0] = unitScaling.first;
|
||||
m_unit_scale[1] = unitScaling.second;
|
||||
}
|
||||
|
||||
if(m_debayerTex)
|
||||
{
|
||||
f->glDeleteTextures(1, &m_debayerTex);
|
||||
m_debayerTex = 0;
|
||||
}
|
||||
|
||||
if(m_bestFit)bestFit();
|
||||
else setOffset(m_dx, m_dy);
|
||||
}
|
||||
|
||||
void ImageWidget::setWCS(std::shared_ptr<WCSData> wcs)
|
||||
{
|
||||
m_wcs = wcs;
|
||||
}
|
||||
|
||||
void ImageWidget::zoom(int zoom, const QPointF &mousePos)
|
||||
{
|
||||
m_bestFit = false;
|
||||
if(zoom != 0)
|
||||
m_scaleStop = std::clamp(m_scaleStop + (zoom > 0 ? 1 : -1), -10, 10);
|
||||
else
|
||||
m_scaleStop = 0;
|
||||
|
||||
QPointF focus(m_width * 0.5f, m_height * 0.5f);
|
||||
if(!mousePos.isNull())
|
||||
focus = mousePos;
|
||||
|
||||
if(width() > m_image->width() * m_scale)
|
||||
m_dx = -width() * 0.5f + m_image->width() * m_scale * 0.5f;
|
||||
if(height() > m_image->height() * m_scale)
|
||||
m_dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
|
||||
|
||||
float newScale = std::sqrt(std::pow(2.0f, (float)m_scaleStop));
|
||||
float r = newScale / m_scale;
|
||||
m_scale = newScale;
|
||||
|
||||
setOffset(m_dx * r + focus.x() * (r - 1), m_dy * r + focus.y() * (r - 1));
|
||||
}
|
||||
|
||||
void ImageWidget::bestFit()
|
||||
{
|
||||
m_bestFit = true;
|
||||
m_scale = std::min((float)m_width/m_imgWidth, (float)m_height/m_imgHeight);
|
||||
setOffset(0, 0);
|
||||
}
|
||||
|
||||
void ImageWidget::allocateThumbnails(const QStringList &paths)
|
||||
{
|
||||
makeCurrent();
|
||||
int count = paths.size();
|
||||
m_thumbnailCount = count;
|
||||
m_thumnails.clear();
|
||||
QStringList marked = m_database->getMarkedFiles();
|
||||
for(auto &path : paths)
|
||||
{
|
||||
QString name = QFileInfo(path).fileName();
|
||||
m_thumnails.push_back({name, path, QSize(0, 0), marked.contains(path), false});
|
||||
}
|
||||
|
||||
m_thumbnailTexture->destroy();
|
||||
m_thumbnailTexture->create();
|
||||
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
|
||||
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
|
||||
m_thumbnailTexture->setLayers(std::min((int)paths.size(), m_maxArrayLayers));
|
||||
m_thumbnailTexture->allocateStorage();
|
||||
}
|
||||
|
||||
QVector2D ImageWidget::getImagePixelCoord(const QVector2D &pos)
|
||||
{
|
||||
float dx = m_dx;
|
||||
float dy = m_dy;
|
||||
if(m_width > m_image->width()*m_scale)
|
||||
dx = -width()*0.5f + m_image->width()*m_scale*0.5f;
|
||||
if(m_height > m_image->height()*m_scale)
|
||||
dy = -height()*0.5f + m_image->height()*m_scale*0.5f;
|
||||
|
||||
QVector2D offset(dx, dy);
|
||||
return (pos + offset) / m_scale;
|
||||
}
|
||||
|
||||
void ImageWidget::setBayerMask(int mask)
|
||||
{
|
||||
m_firstRed[0] = mask & 0x1;
|
||||
m_firstRed[1] = (mask & 0x2) >> 1;
|
||||
if(m_debayerTex)
|
||||
{
|
||||
f->glDeleteTextures(1, &m_debayerTex);
|
||||
m_debayerTex = 0;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::setImage(const QPixmap &pixmap)
|
||||
void ImageWidget::setMTFParams(const MTFParam ¶ms)
|
||||
{
|
||||
QImage img = pixmap.toImage();
|
||||
|
||||
m_imgWidth = pixmap.width();
|
||||
m_imgHeight = pixmap.height();
|
||||
|
||||
m_image->destroy();
|
||||
m_image->setData(img);
|
||||
m_image->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, QOpenGLTexture::Linear);
|
||||
m_image->setWrapMode(QOpenGLTexture::ClampToBorder);
|
||||
m_image->setBorderColor(0, 0, 0, 0);
|
||||
m_bwImg = false;
|
||||
m_mtfParams = params;
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::setScale(float scale)
|
||||
void ImageWidget::setOffset(float dx, float dy)
|
||||
{
|
||||
m_scale = scale;
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::blockRepaint(bool block)
|
||||
{
|
||||
m_blockRepaint = block;
|
||||
if(!block)update();
|
||||
}
|
||||
|
||||
void ImageWidget::setMTFParams(float low, float mid, float high)
|
||||
{
|
||||
m_low = low;
|
||||
m_mid = mid;
|
||||
m_high = high;
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::setOffset(int dx, int dy)
|
||||
{
|
||||
m_dx = dx;
|
||||
m_dy = dy;
|
||||
m_dx = std::clamp(dx, 0.0f, std::max(0.0f, m_imgWidth * m_scale - m_width));
|
||||
if(m_showThumbnails)
|
||||
m_dy = std::clamp(dy, 0.0f, std::max(0.0f, (float)((m_thumbnailCount / (m_width / THUMB_SIZE_BORDER) + 2) * THUMB_SIZE_BORDER_Y - m_height)));
|
||||
else
|
||||
m_dy = std::clamp(dy, 0.0f, std::max(0.0f, m_imgHeight * m_scale - m_height));
|
||||
updateScrollBars();
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::superPixel(bool enable)
|
||||
{
|
||||
m_superpixel = enable;
|
||||
m_image->setLevelOfDetailRange(enable ? 1 : 0, m_image->mipMaxLevel());
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -141,6 +263,12 @@ void ImageWidget::invert(bool enable)
|
||||
update();
|
||||
}
|
||||
|
||||
void ImageWidget::falseColor(bool enable)
|
||||
{
|
||||
m_falseColor = enable;
|
||||
update();
|
||||
}
|
||||
|
||||
QImage ImageWidget::renderToImage()
|
||||
{
|
||||
if(m_imgWidth < 0)return QImage();
|
||||
@@ -155,34 +283,142 @@ QImage ImageWidget::renderToImage()
|
||||
m_program->setUniformValue("offset", 0.0f, 0.0f);
|
||||
m_program->setUniformValue("zoom", 1.0f);
|
||||
|
||||
m_image->bind(0);
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.bindDefault();
|
||||
return fbo.toImage(true);
|
||||
}
|
||||
|
||||
void ImageWidget::thumbnailLoaded(const Image *image)
|
||||
{
|
||||
if(image->number() >= m_maxArrayLayers)
|
||||
return;
|
||||
|
||||
makeCurrent();
|
||||
const RawImage *raw = image->thumbnail();
|
||||
if(!raw || !raw->valid())return;
|
||||
m_thumbnailTexture->setData(0, image->number(), QOpenGLTexture::RGBA, QOpenGLTexture::UInt16, raw->data(), m_transferOptions.get());
|
||||
float a = raw->thumbAspect();
|
||||
int sizes[3] = { std::max(1, a > 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE * a)), std::max(1, a < 1.0f ? THUMB_SIZE : (int)(THUMB_SIZE / a)), image->number() };
|
||||
m_sizesDirty = true;
|
||||
m_thumnails[image->number()].size = QSize(sizes[0], sizes[1]);
|
||||
if(!m_updateTimer->isActive())m_updateTimer->start();
|
||||
}
|
||||
|
||||
void ImageWidget::showThumbnail(bool enable)
|
||||
{
|
||||
m_showThumbnails = enable;
|
||||
setOffset(m_dx, m_dy);
|
||||
}
|
||||
|
||||
void ImageWidget::paintGL()
|
||||
{
|
||||
if(m_blockRepaint)return;
|
||||
|
||||
float dx = m_dx;
|
||||
float dy = m_dy;
|
||||
if(width() > m_image->width()*m_scale)
|
||||
dx = -width()*0.5f + m_image->width()*m_scale*0.5f;
|
||||
if(height() > m_image->height()*m_scale)
|
||||
dy = -height()*0.5f + m_image->height()*m_scale*0.5f;
|
||||
if(m_width > m_image->width() * m_scale)
|
||||
dx = -width() * 0.5f + m_image->width() * m_scale * 0.5f;
|
||||
if(m_height > m_image->height() * m_scale)
|
||||
dy = -height() * 0.5f + m_image->height() * m_scale * 0.5f;
|
||||
QBrush highlight = style()->standardPalette().highlight();
|
||||
|
||||
m_program->bind();
|
||||
m_program->setUniformValue("viewport", (float)width(), (float)height());
|
||||
m_program->setUniformValue("offset", dx, dy);
|
||||
m_program->setUniformValue("mtf_param", m_low, m_mid, m_high);
|
||||
m_program->setUniformValue("zoom", 1.0f/m_scale);
|
||||
m_program->setUniformValue("bw", m_bwImg);
|
||||
m_program->setUniformValue("invert", m_invert);
|
||||
f->glClear(GL_COLOR_BUFFER_BIT);
|
||||
if(m_showThumbnails)
|
||||
{
|
||||
m_vaoThumb->bind();
|
||||
m_thumbnailTexture->bind(1);
|
||||
if(m_sizesDirty)
|
||||
{
|
||||
m_bufferSizes->bind();
|
||||
int i = 0;
|
||||
std::vector<int> sizes(m_thumbnailCount*3);
|
||||
for(auto &s : m_thumnails)
|
||||
{
|
||||
sizes[3*i] = s.size.width();
|
||||
sizes[3*i+1] = s.size.height();
|
||||
sizes[3*i+2] = i;
|
||||
i++;
|
||||
}
|
||||
m_bufferSizes->allocate(&sizes[0], sizes.size()*sizeof(int));
|
||||
m_sizesDirty = false;
|
||||
}
|
||||
m_thumbnailProgram->bind();
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("viewport_row"), width(), height(), width()/THUMB_SIZE_BORDER);
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("thumb_size"), THUMB_SIZE_BORDER/2, THUMB_SIZE_BORDER, THUMB_SIZE_BORDER_Y);
|
||||
m_thumbnailProgram->setUniformValueArray("mtf_param", m_mtfParams.blackPoint, 3, 3);
|
||||
m_thumbnailProgram->setUniformValue("invert", m_invert);
|
||||
m_thumbnailProgram->setUniformValue("offset", 0, m_dy);
|
||||
QMatrix4x4 mvp;
|
||||
mvp.ortho(rect());
|
||||
m_thumbnailProgram->setUniformValue("mvp", mvp);
|
||||
if(f3)f3->glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, m_thumbnailCount);
|
||||
|
||||
QPainter painter(this);
|
||||
const int w = width()/THUMB_SIZE_BORDER;
|
||||
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
|
||||
int start = std::max((int)(m_dy / THUMB_SIZE_BORDER_Y * w - w), 0);
|
||||
int end = std::min((int)(m_dy + m_height) / THUMB_SIZE_BORDER_Y * w + w, m_thumbnailCount);
|
||||
for(int i=start; i < end; i++)
|
||||
{
|
||||
float x = (i % w) * THUMB_SIZE_BORDER;
|
||||
float y = i / w * THUMB_SIZE_BORDER_Y + THUMB_SIZE - m_dy + off;
|
||||
QRectF rect(x, y, THUMB_SIZE_BORDER, 32);
|
||||
painter.drawText(rect, Qt::AlignCenter | Qt::TextWrapAnywhere, QString(m_thumnails[i].name));
|
||||
if(m_thumnails[i].selected)
|
||||
{
|
||||
painter.save();
|
||||
QRectF thumbRect;
|
||||
painter.setPen(Qt::red);
|
||||
thumbRect.setSize(m_thumnails[i].size);
|
||||
thumbRect.moveCenter(QPointF(x + THUMB_SIZE_BORDER / 2, y - THUMB_SIZE / 2));
|
||||
painter.drawRect(thumbRect);
|
||||
painter.restore();
|
||||
}
|
||||
if(m_currentImg == i)
|
||||
{
|
||||
painter.save();
|
||||
painter.setPen(QPen(highlight, 2.0));
|
||||
painter.drawRect((i % w) * THUMB_SIZE_BORDER + off, i / w * THUMB_SIZE_BORDER_Y - m_dy + off, THUMB_SIZE, THUMB_SIZE);
|
||||
painter.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(!m_error.isEmpty())
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setPen(Qt::red);
|
||||
painter.setFont(QFont("Sans", 24, QFont::Bold));
|
||||
painter.drawText(0, 0, width(), height(), Qt::AlignCenter | Qt::AlignHCenter, m_error);
|
||||
}
|
||||
else
|
||||
{
|
||||
debayer();
|
||||
|
||||
m_vao->bind();
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
|
||||
m_program->bind();
|
||||
m_program->setUniformValue("viewport", (float)width(), (float)height());
|
||||
m_program->setUniformValue("offset", std::floor(dx), std::floor(dy));
|
||||
m_program->setUniformValueArray("mtf_param", m_mtfParams.blackPoint, 3, 3);
|
||||
m_program->setUniformValue("unit_scale", m_unit_scale[0], m_unit_scale[1]);
|
||||
m_program->setUniformValue("zoom", 1.0f/m_scale);
|
||||
m_program->setUniformValue("bw", m_bwImg && !m_superpixel);
|
||||
m_program->setUniformValue("false_color", m_falseColor && m_bwImg);
|
||||
m_program->setUniformValue("invert", m_invert);
|
||||
m_program->setUniformValue("filtering", m_scale > 1.0f ? FILTERING : 1);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
m_program->setUniformValue("srgb", m_srgb);
|
||||
#endif
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
void ImageWidget::resizeGL(int w, int h)
|
||||
@@ -190,16 +426,23 @@ void ImageWidget::resizeGL(int w, int h)
|
||||
m_width = w;
|
||||
m_height = h;
|
||||
f->glViewport(0, 0, w, h);
|
||||
if(m_bestFit)bestFit();
|
||||
updateScrollBars();
|
||||
}
|
||||
|
||||
void ImageWidget::initializeGL()
|
||||
{
|
||||
f = context()->functions();
|
||||
f->glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
QOpenGLFunctions_3_2_Core *f3 = context()->versionFunctions<QOpenGLFunctions_3_2_Core>();
|
||||
f3 = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_3_3_Core>(context());
|
||||
|
||||
if(f3 == nullptr)
|
||||
QMessageBox::critical(this, tr("OpenGL error"), tr("Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed."));
|
||||
|
||||
m_vao = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
|
||||
m_vaoThumb = std::unique_ptr<QOpenGLVertexArrayObject>(new QOpenGLVertexArrayObject);
|
||||
m_vao->create();
|
||||
m_vaoThumb->create();
|
||||
m_vao->bind();
|
||||
|
||||
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
|
||||
@@ -210,9 +453,14 @@ void ImageWidget::initializeGL()
|
||||
qDebug() << message;
|
||||
});
|
||||
|
||||
qDebug() << (char*)f->glGetString(GL_VENDOR);
|
||||
qDebug() << (char*)f->glGetString(GL_RENDERER);
|
||||
qDebug() << (char*)f->glGetString(GL_VERSION);
|
||||
qDebug() << "Vendor:" << (char*)f->glGetString(GL_VENDOR);
|
||||
qDebug() << "Renderer:" << (char*)f->glGetString(GL_RENDERER);
|
||||
qDebug() << "Version:" << (char*)f->glGetString(GL_VERSION);
|
||||
f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_maxTextureSize);
|
||||
f->glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &m_maxArrayLayers);
|
||||
qDebug() << "Max texture size:" << m_maxTextureSize << "max layers:" << m_maxArrayLayers;
|
||||
|
||||
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
|
||||
|
||||
qDebug() << context()->format();
|
||||
|
||||
@@ -226,12 +474,12 @@ void ImageWidget::initializeGL()
|
||||
m_buffer->create();
|
||||
m_buffer->bind();
|
||||
m_buffer->allocate(vertexs, sizeof(vertexs));
|
||||
f->glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(float)*4, 0);
|
||||
// f->glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(float)*4, 0);
|
||||
|
||||
m_program = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/image.vert");
|
||||
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/image.frag");
|
||||
if(f3)f3->glBindFragDataLocation(m_program->programId(), 0, "color");
|
||||
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/image.vert");
|
||||
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/image.frag");
|
||||
|
||||
if(!m_program->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_program->log();
|
||||
@@ -245,6 +493,48 @@ void ImageWidget::initializeGL()
|
||||
m_program->setUniformValue("qt_Texture0", (GLuint)0);
|
||||
m_program->setUniformValue("scale", 1.0f, 0.0f);
|
||||
|
||||
m_debayerProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/debayer.vert");
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/debayer.frag");
|
||||
|
||||
m_debayerProgram->bind();
|
||||
m_debayerProgram->enableAttributeArray("qt_Vertex");
|
||||
m_debayerProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
m_debayerProgram->enableAttributeArray("qt_MultiTexCoord0");
|
||||
m_debayerProgram->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
|
||||
m_debayerProgram->setUniformValue("qt_Texture0", (GLuint)0);
|
||||
if(!m_debayerProgram->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_debayerProgram->log();
|
||||
}
|
||||
|
||||
m_vaoThumb->bind();
|
||||
|
||||
m_thumbnailProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/thumb.vert");
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/thumb.frag");
|
||||
|
||||
m_thumbnailProgram->bind();
|
||||
m_thumbnailProgram->enableAttributeArray("qt_Vertex");
|
||||
m_thumbnailProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
m_thumbnailProgram->enableAttributeArray("qt_MultiTexCoord0");
|
||||
m_thumbnailProgram->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
|
||||
if(!m_thumbnailProgram->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_thumbnailProgram->log();
|
||||
}
|
||||
m_thumbnailProgram->setUniformValue("qt_Texture0", (GLuint)1);
|
||||
|
||||
m_bufferSizes = std::unique_ptr<QOpenGLBuffer>(new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer));
|
||||
m_bufferSizes->setUsagePattern(QOpenGLBuffer::StaticDraw);
|
||||
m_bufferSizes->create();
|
||||
m_bufferSizes->bind();
|
||||
m_bufferSizes->allocate(12);
|
||||
|
||||
m_thumbnailProgram->enableAttributeArray("imageSize_num");
|
||||
f3->glVertexAttribIPointer(m_thumbnailProgram->attributeLocation("imageSize_num"), 3, GL_INT, 0, nullptr);
|
||||
f3->glVertexAttribDivisor(m_thumbnailProgram->attributeLocation("imageSize_num"), 1);
|
||||
|
||||
m_image = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2D));
|
||||
m_image->setFormat(QOpenGLTexture::RGB8U);
|
||||
m_image->allocateStorage();
|
||||
@@ -252,21 +542,208 @@ void ImageWidget::initializeGL()
|
||||
m_image->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
|
||||
m_image->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||
|
||||
m_thumbnailTexture = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(QOpenGLTexture::Target2DArray));
|
||||
m_thumbnailTexture->setFormat(QOpenGLTexture::RGB16_UNorm);
|
||||
m_thumbnailTexture->setSize(THUMB_SIZE, THUMB_SIZE);
|
||||
m_thumbnailTexture->setLayers(1);
|
||||
m_thumbnailTexture->allocateStorage();
|
||||
m_thumbnailTexture->bind(1);
|
||||
m_thumbnailTexture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
|
||||
|
||||
m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
|
||||
m_transferOptions->setAlignment(1);
|
||||
|
||||
if(m_rawImage)
|
||||
setImage(m_rawImage, m_currentImg);
|
||||
}
|
||||
|
||||
ImageScrollAreaGL::ImageScrollAreaGL(QWidget *parent) : QWidget(parent)
|
||||
void ImageWidget::dragEnterEvent(QDragEnterEvent *event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void ImageWidget::dropEvent(QDropEvent *event)
|
||||
{
|
||||
if(event->mimeData()->hasUrls() && event->proposedAction() & (Qt::CopyAction | Qt::MoveAction))
|
||||
{
|
||||
for(const QUrl &url : event->mimeData()->urls())
|
||||
{
|
||||
if(url.isLocalFile())
|
||||
{
|
||||
emit fileDropped(url.toLocalFile());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void ImageWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_showThumbnails && event->button() == Qt::LeftButton && event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier))
|
||||
m_selecting = true;
|
||||
|
||||
if(m_selecting)
|
||||
{
|
||||
thumbSelect(event);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(event->button() == Qt::LeftButton)
|
||||
m_lastPos = event->position();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_selecting)
|
||||
{
|
||||
thumbSelect(event);
|
||||
}
|
||||
else if(!m_lastPos.isNull())
|
||||
{
|
||||
QPointF off = event->position() - m_lastPos;
|
||||
m_lastPos = event->position();
|
||||
setOffset(m_dx - off.x(), m_dy - off.y());
|
||||
return;
|
||||
}
|
||||
|
||||
if(!m_showThumbnails && m_rawImage)
|
||||
{
|
||||
QVector2D pix = getImagePixelCoord(QVector2D(event->pos()));
|
||||
double r,g,b;
|
||||
|
||||
SkyPoint sky;
|
||||
if(m_wcs)
|
||||
{
|
||||
m_wcs->pixelToWorld(QPointF(pix.x(), pix.y()), sky);
|
||||
}
|
||||
|
||||
if(m_rawImage->pixel(pix.x(), pix.y(), r, g, b))
|
||||
{
|
||||
if(m_bwImg)
|
||||
emit status(tr("L:%1").arg(r), tr("X:%3 Y:%4").arg((int)pix.x()).arg((int)pix.y()), sky.toString());
|
||||
else
|
||||
emit status(tr("R:%1 G:%2 B:%3").arg(r).arg(g).arg(b), tr("X:%3 Y:%4").arg((int)pix.x()).arg((int)pix.y()), sky.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if(m_selecting)
|
||||
{
|
||||
m_selecting = false;
|
||||
event->accept();
|
||||
QStringList mark;
|
||||
QStringList unmark;
|
||||
for(auto &thumb : m_thumnails)
|
||||
{
|
||||
if(thumb.dirty)
|
||||
{
|
||||
if(thumb.selected)
|
||||
mark.append(thumb.path);
|
||||
else
|
||||
unmark.append(thumb.path);
|
||||
|
||||
thumb.dirty = false;
|
||||
}
|
||||
}
|
||||
if(!mark.isEmpty())
|
||||
m_database->mark(mark);
|
||||
if(!unmark.isEmpty())
|
||||
m_database->unmark(unmark);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_lastPos = QPointF();
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if(m_showThumbnails)
|
||||
{
|
||||
setOffset(0, m_dy - event->angleDelta().y());
|
||||
}
|
||||
else
|
||||
{
|
||||
if(std::abs(event->angleDelta().y()) > 15)
|
||||
zoom(event->angleDelta().y(), event->modifiers() & Qt::ShiftModifier ? QPointF() : event->position());
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::thumbSelect(QMouseEvent *event)
|
||||
{
|
||||
QPoint p = event->pos();
|
||||
const int off = (THUMB_SIZE_BORDER - THUMB_SIZE) / 2;
|
||||
p.ry() += m_dy;
|
||||
const int w = width()/THUMB_SIZE_BORDER;
|
||||
int x = p.x() / THUMB_SIZE_BORDER;
|
||||
int y = p.y() / THUMB_SIZE_BORDER_Y;
|
||||
int i = y * w + x;
|
||||
event->accept();
|
||||
QRect rect(x * THUMB_SIZE_BORDER + off, y * THUMB_SIZE_BORDER_Y + off, THUMB_SIZE, THUMB_SIZE);
|
||||
if(x < w && i < m_thumbnailCount && rect.contains(p, true))
|
||||
{
|
||||
bool oldVal = m_thumnails[i].selected;
|
||||
bool newVal = oldVal;
|
||||
if(event->modifiers() == Qt::ShiftModifier)
|
||||
newVal = true;
|
||||
if(event->modifiers() == Qt::ControlModifier)
|
||||
newVal = false;
|
||||
|
||||
if(newVal != oldVal)
|
||||
{
|
||||
m_thumnails[i].selected = newVal;
|
||||
m_thumnails[i].dirty = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::debayer()
|
||||
{
|
||||
if(m_debayerTex > 0 || !m_superpixel || !m_bwImg || m_imgWidth < 0)return;
|
||||
|
||||
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight, QOpenGLFramebufferObject::NoAttachment, GL_TEXTURE_2D, GL_RGBA16);
|
||||
fbo.bind();
|
||||
|
||||
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
|
||||
|
||||
m_debayerProgram->bind();
|
||||
f->glUniform2i(m_debayerProgram->uniformLocation("firstRed"), m_firstRed[0], m_firstRed[1]);
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.release();
|
||||
f->glViewport(0, 0, m_width, m_height);
|
||||
m_debayerTex = fbo.takeTexture();
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
f->glGenerateMipmap(GL_TEXTURE_2D);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
}
|
||||
|
||||
void ImageWidget::updateScrollBars()
|
||||
{
|
||||
if(m_showThumbnails)
|
||||
emit scrollBarsUpdate(0, 0, -1, m_dy, m_height, (m_thumbnailCount / (m_width / THUMB_SIZE_BORDER) + 2) * THUMB_SIZE_BORDER_Y - m_height);
|
||||
else
|
||||
emit scrollBarsUpdate(m_dx, m_width, m_imgWidth * m_scale - m_width, m_dy, m_height, m_imgHeight * m_scale - m_height);
|
||||
}
|
||||
|
||||
ImageScrollAreaGL::ImageScrollAreaGL(Database *database, QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
QGridLayout *layout = new QGridLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
m_imageWidget = new ImageWidget(this);
|
||||
m_imageWidget = new ImageWidget(database, this);
|
||||
|
||||
m_verticalScrollBar = new QScrollBar(Qt::Vertical, this);
|
||||
m_horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
|
||||
m_scale = 1.0f;
|
||||
m_bestFit = false;
|
||||
|
||||
layout->setSpacing(0);
|
||||
layout->addWidget(m_imageWidget, 0, 0);
|
||||
@@ -275,6 +752,7 @@ ImageScrollAreaGL::ImageScrollAreaGL(QWidget *parent) : QWidget(parent)
|
||||
|
||||
connect(m_verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
|
||||
connect(m_horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(scrollEvent()));
|
||||
connect(m_imageWidget, &ImageWidget::scrollBarsUpdate, this, &ImageScrollAreaGL::updateScrollbars);
|
||||
}
|
||||
|
||||
ImageScrollAreaGL::~ImageScrollAreaGL()
|
||||
@@ -282,22 +760,13 @@ ImageScrollAreaGL::~ImageScrollAreaGL()
|
||||
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::setImage(RawImage *image)
|
||||
void ImageScrollAreaGL::setImage(Image *image)
|
||||
{
|
||||
m_imageWidget->setImage(image);
|
||||
m_imgWidth = image->width();
|
||||
m_imgHeight = image->height();
|
||||
if(m_bestFit)bestFit();
|
||||
updateScrollbars();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::setImage(const QPixmap &pixmap)
|
||||
{
|
||||
m_imageWidget->setImage(pixmap);
|
||||
m_imgWidth = pixmap.width();
|
||||
m_imgHeight = pixmap.height();
|
||||
if(m_bestFit)bestFit();
|
||||
updateScrollbars();
|
||||
if(image && image->rawImage())
|
||||
{
|
||||
m_imageWidget->setImage(image->rawImage(), image->number());
|
||||
m_imageWidget->setWCS(image->info().wcs);
|
||||
}
|
||||
}
|
||||
|
||||
ImageWidget *ImageScrollAreaGL::imageWidget()
|
||||
@@ -305,83 +774,49 @@ ImageWidget *ImageScrollAreaGL::imageWidget()
|
||||
return m_imageWidget;
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::updateScrollbars(bool zoom)
|
||||
void ImageScrollAreaGL::updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV)
|
||||
{
|
||||
if(zoom)
|
||||
if(maxH > 0)
|
||||
{
|
||||
setScrollRange(m_verticalScrollBar, m_imgHeight*m_scale);
|
||||
setScrollRange(m_horizontalScrollBar, m_imgWidth*m_scale);
|
||||
m_horizontalScrollBar->show();
|
||||
m_horizontalScrollBar->setRange(0, maxH);
|
||||
m_horizontalScrollBar->setPageStep(stepH);
|
||||
m_horizontalScrollBar->setValue(valueH);
|
||||
}
|
||||
else
|
||||
m_horizontalScrollBar->hide();
|
||||
|
||||
if(maxV > 0)
|
||||
{
|
||||
m_verticalScrollBar->setRange(0, m_imgHeight*m_scale - m_verticalScrollBar->pageStep());
|
||||
m_horizontalScrollBar->setRange(0, m_imgWidth*m_scale - m_horizontalScrollBar->pageStep());
|
||||
m_verticalScrollBar->show();
|
||||
m_verticalScrollBar->setRange(0, maxV);
|
||||
m_verticalScrollBar->setPageStep(stepV);
|
||||
m_verticalScrollBar->setValue(valueV);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
m_verticalScrollBar->setPageStep(m_imageWidget->height());
|
||||
m_horizontalScrollBar->setPageStep(m_imageWidget->width());
|
||||
updateScrollbars();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QPoint delta = m_lastPos - event->pos();
|
||||
m_horizontalScrollBar->setValue(m_horizontalScrollBar->value() + delta.x());
|
||||
m_verticalScrollBar->setValue(m_verticalScrollBar->value() + delta.y());
|
||||
m_lastPos = event->pos();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
m_lastPos = event->pos();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
m_bestFit = false;
|
||||
if(event->angleDelta().y() != 0)
|
||||
zoom(event->angleDelta().y() / 1200.0f);
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::zoom(float delta)
|
||||
{
|
||||
if((m_scale >= 8.0f && delta > 0) || (m_scale <= 0.1f && delta < 0))return;
|
||||
|
||||
m_scale += delta;
|
||||
m_imageWidget->blockRepaint(true);
|
||||
m_imageWidget->setScale(m_scale);
|
||||
updateScrollbars(true);
|
||||
m_imageWidget->blockRepaint(false);
|
||||
else
|
||||
m_verticalScrollBar->hide();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::zoomIn()
|
||||
{
|
||||
zoom(0.1f);
|
||||
m_bestFit = false;
|
||||
m_imageWidget->zoom(1);
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::zoomOut()
|
||||
{
|
||||
zoom(-0.1f);
|
||||
m_bestFit = false;
|
||||
m_imageWidget->zoom(-1);
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::bestFit()
|
||||
{
|
||||
m_bestFit = true;
|
||||
m_scale = std::min((float)m_imageWidget->width()/m_imgWidth, (float)m_imageWidget->height()/m_imgHeight);
|
||||
zoom(0.0f);
|
||||
m_horizontalScrollBar->hide();
|
||||
m_verticalScrollBar->hide();
|
||||
m_imageWidget->bestFit();
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::oneToOne()
|
||||
{
|
||||
m_scale = 1.0f;
|
||||
zoom(0.0f);
|
||||
m_bestFit = false;
|
||||
m_imageWidget->zoom(0);
|
||||
}
|
||||
|
||||
void ImageScrollAreaGL::scrollEvent()
|
||||
|
||||
@@ -4,61 +4,108 @@
|
||||
#include <memory>
|
||||
#include <QObject>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QOpenGLFunctions_3_2_Core>
|
||||
#include <QOpenGLFunctions_3_3_Core>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLTexture>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include "rawimage.h"
|
||||
#include "imageringlist.h"
|
||||
#include "database.h"
|
||||
#include "stretchtoolbar.h"
|
||||
|
||||
typedef enum
|
||||
struct ImageThumb
|
||||
{
|
||||
Linear,
|
||||
Log,
|
||||
Sqrt,
|
||||
Power,
|
||||
Asinh,
|
||||
}StretchFunc;
|
||||
QString name;
|
||||
QString path;
|
||||
QSize size;
|
||||
bool selected;
|
||||
bool dirty;
|
||||
};
|
||||
|
||||
class ImageWidget : public QOpenGLWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
QOpenGLFunctions *f;
|
||||
QOpenGLFunctions *f = nullptr;
|
||||
QOpenGLFunctions_3_3_Core *f3 = nullptr;
|
||||
QTimer *m_updateTimer = nullptr;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_program;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_thumbnailProgram;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_debayerProgram;
|
||||
std::unique_ptr<QOpenGLBuffer> m_buffer;
|
||||
std::unique_ptr<QOpenGLBuffer> m_bufferSizes;
|
||||
std::unique_ptr<QOpenGLTexture> m_image;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vao;
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vaoThumb;
|
||||
std::unique_ptr<QOpenGLPixelTransferOptions> m_transferOptions;
|
||||
std::unique_ptr<QOpenGLTexture> m_thumbnailTexture;
|
||||
GLuint m_debayerTex = 0;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::shared_ptr<WCSData> m_wcs;
|
||||
int m_width, m_height;
|
||||
int m_imgWidth, m_imgHeight;
|
||||
float m_low;
|
||||
float m_mid;
|
||||
float m_high;
|
||||
float m_range;
|
||||
float m_dx, m_dy;
|
||||
float m_scale;
|
||||
bool m_blockRepaint;
|
||||
bool m_bwImg;
|
||||
bool m_invert;
|
||||
bool m_superpixel;
|
||||
int m_imgWidth = -1, m_imgHeight = -1;
|
||||
int m_currentImg = 0;
|
||||
MTFParam m_mtfParams;
|
||||
float m_unit_scale[2] = {1.0f, 0.0f}; // scale and offset
|
||||
float m_dx = 0, m_dy = 0;
|
||||
float m_scale = 1.0f;
|
||||
int m_scaleStop = 0;
|
||||
bool m_bestFit = false;
|
||||
bool m_bwImg = false;
|
||||
bool m_falseColor = false;
|
||||
bool m_invert = false;
|
||||
bool m_superpixel = false;
|
||||
bool m_showThumbnails = false;
|
||||
bool m_selecting = false;
|
||||
bool m_sizesDirty = false;
|
||||
bool m_srgb = false;
|
||||
int m_thumbnailCount = 0;
|
||||
int m_maxTextureSize = 0;
|
||||
int m_maxArrayLayers = 0;
|
||||
int m_firstRed[2] = {0, 0};
|
||||
QVector<ImageThumb> m_thumnails;
|
||||
Database *m_database = nullptr;
|
||||
QPointF m_lastPos;
|
||||
QString m_error;
|
||||
public:
|
||||
explicit ImageWidget(QWidget *parent = nullptr);
|
||||
~ImageWidget();
|
||||
void setImage(const RawImage *image);
|
||||
explicit ImageWidget(Database *database, QWidget *parent = nullptr);
|
||||
~ImageWidget() override;
|
||||
void setImage(std::shared_ptr<RawImage> image, int index);
|
||||
void setImage(const QPixmap &pixmap);
|
||||
void setScale(float scale);
|
||||
void blockRepaint(bool block);
|
||||
void setWCS(std::shared_ptr<WCSData> wcs);
|
||||
void zoom(int zoom, const QPointF &mousePos = QPointF());
|
||||
void bestFit();
|
||||
void allocateThumbnails(const QStringList &paths);
|
||||
QVector2D getImagePixelCoord(const QVector2D &pos);
|
||||
void setBayerMask(int mask);
|
||||
public slots:
|
||||
void setMTFParams(float low, float mid, float high);
|
||||
void setOffset(int dx, int dy);
|
||||
void setMTFParams(const MTFParam ¶ms);
|
||||
void setOffset(float dx, float dy);
|
||||
void superPixel(bool enable);
|
||||
void invert(bool enable);
|
||||
void falseColor(bool enable);
|
||||
QImage renderToImage();
|
||||
void thumbnailLoaded(const Image *image);
|
||||
void showThumbnail(bool enable);
|
||||
protected:
|
||||
void paintGL();
|
||||
void resizeGL(int w, int h);
|
||||
void initializeGL();
|
||||
void paintGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void initializeGL() override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void thumbSelect(QMouseEvent *event);
|
||||
void debayer();
|
||||
void updateScrollBars();
|
||||
signals:
|
||||
void fileDropped(const QString &path);
|
||||
void status(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
|
||||
void scrollBarsUpdate(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
|
||||
};
|
||||
|
||||
class ImageScrollAreaGL : public QWidget
|
||||
@@ -67,23 +114,13 @@ class ImageScrollAreaGL : public QWidget
|
||||
QScrollBar *m_verticalScrollBar;
|
||||
QScrollBar *m_horizontalScrollBar;
|
||||
ImageWidget *m_imageWidget;
|
||||
int m_imgWidth, m_imgHeight;
|
||||
QPoint m_lastPos;
|
||||
float m_scale;
|
||||
bool m_bestFit;
|
||||
public:
|
||||
explicit ImageScrollAreaGL(QWidget *parent = nullptr);
|
||||
~ImageScrollAreaGL();
|
||||
void setImage(RawImage *image);
|
||||
void setImage(const QPixmap &pixmap);
|
||||
explicit ImageScrollAreaGL(Database *database, QWidget *parent = nullptr);
|
||||
~ImageScrollAreaGL() override;
|
||||
void setImage(Image *image);
|
||||
ImageWidget* imageWidget();
|
||||
protected:
|
||||
void updateScrollbars(bool zoom = false);
|
||||
void resizeEvent(QResizeEvent *event);
|
||||
void mouseMoveEvent(QMouseEvent *event);
|
||||
void mousePressEvent(QMouseEvent *event);
|
||||
void wheelEvent(QWheelEvent *event);
|
||||
void zoom(float delta);
|
||||
void updateScrollbars(int valueH, int stepH, int maxH, int valueV, int stepV, int maxV);
|
||||
public slots:
|
||||
void zoomIn();
|
||||
void zoomOut();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
find_program(XDG-ICON-RESOURCE_EXECUTABLE xdg-icon-resource)
|
||||
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor space.nouspiro.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 resources/space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 resources/space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
@@ -7,15 +7,23 @@
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <libexif/exif-data.h>
|
||||
#include <fitsio2.h>
|
||||
#include <libxisf.h>
|
||||
#include "rawimage.h"
|
||||
#include "starfit.h"
|
||||
#include "wcslib/wcshdr.h"
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level) :
|
||||
#ifdef COLOR_MANAGMENT
|
||||
#include <QColorSpace>
|
||||
#endif
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
|
||||
m_file(file),
|
||||
m_receiver(receiver),
|
||||
m_analyzeLevel(level)
|
||||
m_analyzeLevel(level),
|
||||
m_thumbnail(thumbnail)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -75,51 +83,46 @@ void printStarModel(int radius, const std::vector<double> &data, const Star &sta
|
||||
std::cout << m.toStdString() << std::endl << std::endl;
|
||||
}
|
||||
|
||||
bool loadRAW(QString path, ImageInfoData &info, RawImage **image)
|
||||
bool loadRAW(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
|
||||
{
|
||||
if(!image)
|
||||
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
|
||||
raw->open_file(path.toLocal8Bit().data());
|
||||
raw->imgdata.params.half_size = true;
|
||||
raw->imgdata.params.use_camera_wb = true;
|
||||
raw->imgdata.params.user_flip = 0;
|
||||
if(raw->unpack())
|
||||
return false;
|
||||
|
||||
LibRaw raw;
|
||||
raw.open_file(path.toLocal8Bit().data());
|
||||
raw.imgdata.params.half_size = true;
|
||||
raw.imgdata.params.use_camera_wb = true;
|
||||
raw.imgdata.params.user_flip = 0;
|
||||
if(raw.unpack())
|
||||
return false;
|
||||
|
||||
if(image)
|
||||
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
|
||||
size_t size = rawdata.sizes.width*rawdata.sizes.height;
|
||||
|
||||
std::vector<uint16_t> out;
|
||||
out.resize(size);
|
||||
size_t d = 0;
|
||||
uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
|
||||
uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
|
||||
size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);
|
||||
|
||||
for(size_t i=rawdata.sizes.top_margin;i<h;i++)
|
||||
{
|
||||
libraw_rawdata_t rawdata = raw.imgdata.rawdata;
|
||||
size_t size = rawdata.sizes.width*rawdata.sizes.height;
|
||||
|
||||
std::vector<uint16_t> out;
|
||||
out.resize(size);
|
||||
size_t d = 0;
|
||||
uint h=rawdata.sizes.top_margin+rawdata.sizes.height;
|
||||
uint w=rawdata.sizes.left_margin+rawdata.sizes.width;
|
||||
size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t);
|
||||
|
||||
for(size_t i=rawdata.sizes.top_margin;i<h;i++)
|
||||
for(size_t o=rawdata.sizes.left_margin;o<w;o++)
|
||||
{
|
||||
for(size_t o=rawdata.sizes.left_margin;o<w;o++)
|
||||
{
|
||||
uint16_t p = rawdata.raw_image[i*pitch+o];
|
||||
out[d++] = p;
|
||||
}
|
||||
uint16_t p = rawdata.raw_image[i*pitch+o];
|
||||
out[d++] = p;
|
||||
}
|
||||
*image = new RawImage(rawdata.sizes.width, rawdata.sizes.height, RawImage::UINT16);
|
||||
memcpy((*image)->data(), &out[0], sizeof(uint16_t)*d);
|
||||
}
|
||||
image = std::make_shared<RawImage>(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16);
|
||||
memcpy(image->data(), &out[0], sizeof(uint16_t)*d);
|
||||
|
||||
QString shutterSpeed = QString::number(raw.imgdata.other.shutter);
|
||||
if(raw.imgdata.other.shutter < 1)
|
||||
QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
|
||||
if(raw->imgdata.other.shutter < 1)
|
||||
{
|
||||
shutterSpeed = QString("1/%1s").arg(1.0f/raw.imgdata.other.shutter);
|
||||
shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
|
||||
}
|
||||
//info.append(StringPair(QObject::tr("Width"), QString::number(rawImg->width)));
|
||||
//info.append(StringPair(QObject::tr("Height"), QString::number(rawImg->height)));
|
||||
info.info.append({QObject::tr("ISO"), QString::number(raw.imgdata.other.iso_speed)});
|
||||
info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)});
|
||||
info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)});
|
||||
info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
|
||||
info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
|
||||
#if LIBRAW_MINOR_VERSION>=19
|
||||
// info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
|
||||
@@ -129,6 +132,9 @@ bool loadRAW(QString path, ImageInfoData &info, RawImage **image)
|
||||
|
||||
int loadFITSHeader(fitsfile *file, ImageInfoData &info)
|
||||
{
|
||||
int imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
int nexist;
|
||||
int status = 0;
|
||||
char key[FLEN_KEYWORD];
|
||||
@@ -136,6 +142,7 @@ int loadFITSHeader(fitsfile *file, ImageInfoData &info)
|
||||
char comm[FLEN_COMMENT];
|
||||
char strval[FLEN_VALUE];
|
||||
QVariant var;
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
||||
fits_get_hdrspace(file, &nexist, nullptr, &status);
|
||||
for(int i=1; i<=nexist; i++)
|
||||
{
|
||||
@@ -154,57 +161,86 @@ int loadFITSHeader(fitsfile *file, ImageInfoData &info)
|
||||
var = vald;
|
||||
else if(status == VALUE_UNDEFINED)
|
||||
var = QVariant();
|
||||
else if(string == "T" || string == "F")
|
||||
var = string == "T";
|
||||
else
|
||||
var = strval;
|
||||
var = string;
|
||||
status = 0;
|
||||
info.fitsHeader.append({key, var, comm});
|
||||
info.fitsHeader.append(FITSRecord(key, var, comm));
|
||||
}
|
||||
else
|
||||
{
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
char *header = nullptr;
|
||||
int nrec = 0;
|
||||
const char *exclist[] = {"PV1_1", "PV1_2"};
|
||||
fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status);
|
||||
if(status == 0)
|
||||
{
|
||||
info.wcs = std::make_shared<WCSData>(naxes[0], naxes[1], header, nrec);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
fits_free_memory(header, &status);
|
||||
return status;
|
||||
}
|
||||
|
||||
bool loadFITS(QString path, ImageInfoData &info, RawImage **image)
|
||||
bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
|
||||
{
|
||||
if(!image)
|
||||
return false;
|
||||
|
||||
fitsfile *file;
|
||||
int status = 0;
|
||||
int type;
|
||||
fits_open_image(&file, path.toLocal8Bit().data(), READONLY, &status);
|
||||
fits_get_hdu_type(file, &type, &status);
|
||||
int type = -1;
|
||||
fits_open_diskfile(&file, path.toLocal8Bit().data(), READONLY, &status);
|
||||
int num = 0;
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
|
||||
if(type == IMAGE_HDU)
|
||||
int imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
for(int i=1; i <= num; i++)
|
||||
{
|
||||
int imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
fits_movabs_hdu(file, i, IMAGE_HDU, &status);
|
||||
fits_get_hdu_type(file, &type, &status);
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
||||
fits_get_img_equivtype(file, &imgtype, &status);
|
||||
|
||||
if(naxis >= 2 && naxis <= 3 && status == 0)
|
||||
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
{
|
||||
int cvtype;
|
||||
RawImage::DataType type;
|
||||
int fitstype;
|
||||
std::vector<cv::Mat> cvimg;
|
||||
long fpixel[3] = {1,1,1};
|
||||
switch(imgtype)
|
||||
{
|
||||
case BYTE_IMG:
|
||||
cvtype = CV_8U;
|
||||
type = RawImage::UINT8;
|
||||
fitstype = TBYTE;
|
||||
break;
|
||||
case SHORT_IMG:
|
||||
cvtype = CV_16U;
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TSHORT;
|
||||
break;
|
||||
case USHORT_IMG:
|
||||
type = RawImage::UINT16;
|
||||
fitstype = TUSHORT;
|
||||
break;
|
||||
case ULONG_IMG:
|
||||
type = RawImage::UINT32;
|
||||
fitstype = TUINT;
|
||||
break;
|
||||
case FLOAT_IMG:
|
||||
cvtype = CV_32F;
|
||||
type = RawImage::FLOAT32;
|
||||
fitstype = TFLOAT;
|
||||
break;
|
||||
case DOUBLE_IMG:
|
||||
type = RawImage::FLOAT64;
|
||||
fitstype = TDOUBLE;
|
||||
break;
|
||||
default:
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
goto noload;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t size = naxes[0]*naxes[1];
|
||||
@@ -214,28 +250,44 @@ bool loadFITS(QString path, ImageInfoData &info, RawImage **image)
|
||||
info.info.append({QObject::tr("Width"), QString::number(naxes[0])});
|
||||
info.info.append({QObject::tr("Height"), QString::number(naxes[1])});
|
||||
|
||||
RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type);
|
||||
uint8_t *data = static_cast<uint8_t*>(img.data());
|
||||
for (int i=1; i==1 || i<=naxes[2]; i++)
|
||||
{
|
||||
cv::Mat tmp(h, w, cvtype);
|
||||
fpixel[2] = i;
|
||||
fits_read_pix(file, fitstype, fpixel, size, NULL, tmp.ptr(), NULL, &status);
|
||||
cvimg.push_back(tmp);
|
||||
fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status);
|
||||
}
|
||||
if(fitstype == TSHORT)
|
||||
{
|
||||
uint16_t *s = static_cast<uint16_t*>(img.data());
|
||||
size_t size = img.size() * img.channels();
|
||||
for(size_t i=0; i<size; i++)
|
||||
s[i] -= INT16_MIN;
|
||||
}
|
||||
|
||||
if(cvimg.size() == 1)
|
||||
{
|
||||
*image = new RawImage(cvimg[0]);
|
||||
}
|
||||
if(cvimg.size() == 3)
|
||||
{
|
||||
cv::Mat rgb;
|
||||
cv::merge(cvimg, rgb);
|
||||
*image = new RawImage(rgb);
|
||||
}
|
||||
if(img.channels() == 1)
|
||||
image = std::make_shared<RawImage>(std::move(img));
|
||||
else
|
||||
image = RawImage::fromPlanar(img);
|
||||
|
||||
if(image)
|
||||
image->convertToGLFormat();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
noload:
|
||||
if(file)
|
||||
loadFITSHeader(file, info);
|
||||
|
||||
loadFITSHeader(file, info);
|
||||
if(image)
|
||||
{
|
||||
for(auto fits : info.fitsHeader)
|
||||
{
|
||||
if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP")
|
||||
image->flip();
|
||||
}
|
||||
}
|
||||
|
||||
fits_close_file(file, &status);
|
||||
if(status)
|
||||
@@ -243,117 +295,142 @@ bool loadFITS(QString path, ImageInfoData &info, RawImage **image)
|
||||
char err[100];
|
||||
fits_get_errstatus(status, err);
|
||||
info.info.append({QObject::tr("Error"), QString(err)});
|
||||
qDebug() << err;
|
||||
qDebug() << "Failed to load FITS file" << err;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &image)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path.toLocal8Bit().data());
|
||||
|
||||
const LibXISF::Image &xisfImage = xisf.getImage(0);
|
||||
|
||||
auto fitskeywords = xisfImage.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
auto imageproperties = xisfImage.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
|
||||
info.wcs = std::make_shared<WCSData>(xisfImage.width(), xisfImage.height(), info.fitsHeader);
|
||||
info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())});
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
|
||||
RawImage::DataType type;
|
||||
switch(xisfImage.sampleFormat())
|
||||
{
|
||||
case LibXISF::Image::UInt8: type = RawImage::UINT8; break;
|
||||
case LibXISF::Image::UInt16: type = RawImage::UINT16; break;
|
||||
case LibXISF::Image::UInt32: type = RawImage::UINT32; break;
|
||||
case LibXISF::Image::Float32: type = RawImage::FLOAT32; break;
|
||||
case LibXISF::Image::Float64: type = RawImage::FLOAT64; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
LibXISF::Image tmpImage = xisfImage;
|
||||
tmpImage.convertPixelStorageTo(LibXISF::Image::Planar);
|
||||
if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray)
|
||||
{
|
||||
image = std::make_shared<RawImage>(tmpImage.width(), tmpImage.height(), 1, type);
|
||||
std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount());
|
||||
}
|
||||
else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4)
|
||||
{
|
||||
image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type);
|
||||
}
|
||||
if(image)
|
||||
{
|
||||
image->convertToGLFormat();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
info.info.append(QPair<QString, QString>("Error", err.what()));
|
||||
qDebug() << "Failed to load XISF" << err.what();
|
||||
return false;
|
||||
}
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
return false;
|
||||
}
|
||||
|
||||
void LoadRunable::run()
|
||||
{
|
||||
if(!m_receiver->isCurrent())
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
QElapsedTimer timer;
|
||||
ImageInfoData info;
|
||||
QFileInfo finfo(m_file);
|
||||
info.info.append({QObject::tr("Filename"), finfo.fileName()});
|
||||
|
||||
RawImage *rawImage = nullptr;
|
||||
bool raw = false;
|
||||
if(m_file.endsWith(".CR2", Qt::CaseInsensitive))
|
||||
{
|
||||
timer.start();
|
||||
loadRAW(m_file, info, &rawImage);
|
||||
raw = true;
|
||||
qDebug() << "LoadRaw" << timer.elapsed();
|
||||
}
|
||||
else if(m_file.endsWith(".FIT", Qt::CaseInsensitive) || m_file.endsWith(".FITS", Qt::CaseInsensitive))
|
||||
{
|
||||
loadFITS(m_file, info, &rawImage);
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img(m_file);
|
||||
ExifData *exif = exif_data_new_from_file(m_file.toLocal8Bit().constData());
|
||||
info.info.append({QObject::tr("Width"), QString::number(img.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(img.height())});
|
||||
if(exif)
|
||||
if(!m_thumbnail && !m_receiver->isCurrent())
|
||||
{
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
|
||||
exif_data_free(exif);
|
||||
return;
|
||||
}
|
||||
rawImage = new RawImage(img);
|
||||
}
|
||||
QElapsedTimer timer;
|
||||
ImageInfoData info;
|
||||
QFileInfo finfo(m_file);
|
||||
info.info.append({QObject::tr("Filename"), finfo.fileName()});
|
||||
|
||||
if(rawImage && m_analyzeLevel >= Statistics)
|
||||
{
|
||||
double mean, median, min, max, mad;
|
||||
double stdDev;
|
||||
timer.start();
|
||||
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad);
|
||||
qDebug() << "image stats" << timer.restart();
|
||||
info.info.append({QObject::tr("Mean"), QString::number(mean)});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString::number(stdDev)});
|
||||
info.info.append({QObject::tr("Median"), QString::number(median)});
|
||||
info.info.append({QObject::tr("Minimum"), QString::number(min)});
|
||||
info.info.append({QObject::tr("Maximum"), QString::number(max)});
|
||||
info.info.append({QObject::tr("MAD"), QString::number(mad)});
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
if(!loadImage(m_file, info, rawImage))
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")});
|
||||
|
||||
if(m_analyzeLevel >= Peaks)
|
||||
if(rawImage && !m_thumbnail)
|
||||
{
|
||||
std::vector<Peak> peaks;
|
||||
if(raw) {
|
||||
rawImage->quarter();
|
||||
qDebug() << "quarter" << timer.restart();
|
||||
}
|
||||
RawImage *medianImage = rawImage->medianFilter();
|
||||
qDebug() << "median" << timer.restart();
|
||||
int numPeaks = medianImage->findPeaks(median+stdDev*2, 20, peaks);
|
||||
delete medianImage;
|
||||
qDebug() << "peaks" << timer.restart();
|
||||
//if(m_analyzeLevel == Peaks)
|
||||
// drawPeaks(img, peaks);
|
||||
qDebug() << "draw peaks" << timer.restart();
|
||||
info.info.append({QObject::tr("Peaks"), QString::number(numPeaks)});
|
||||
info.info.append({QObject::tr("Peaks draw"), QString::number(peaks.size())});
|
||||
|
||||
if(m_analyzeLevel>= Stars)
|
||||
timer.start();
|
||||
rawImage->calcStats();
|
||||
const RawImage::Stats &stats = rawImage->imageStats();
|
||||
qDebug() << "image stats" << timer.restart();
|
||||
if(rawImage->channels() == 1)
|
||||
{
|
||||
double fwhmX = 0;
|
||||
double fwhmY = 0;
|
||||
const int radius = 13;
|
||||
StarFit starFit(radius);
|
||||
std::vector<Star> stars;
|
||||
for(uint i=0; i<peaks.size(); i++)
|
||||
{
|
||||
Peak p = peaks[i];
|
||||
std::vector<double> r;
|
||||
int x = p.x();
|
||||
int y = p.y();
|
||||
rawImage->rect(x, y, radius, radius, r);
|
||||
Star star = starFit.fitStar(r, false);
|
||||
if(star.valid())
|
||||
{
|
||||
//printStarModel(radius, r, star);
|
||||
star.m_x += x;
|
||||
star.m_y += y;
|
||||
fwhmX += star.fwhmX();
|
||||
fwhmY += star.fwhmY();
|
||||
stars.push_back(star);
|
||||
}
|
||||
}
|
||||
//drawStars(img, stars);
|
||||
info.info.append({QObject::tr("FWHM X"), QString::number(fwhmX/stars.size())});
|
||||
info.info.append({QObject::tr("FWHM Y"), QString::number(fwhmY/stars.size())});
|
||||
info.info.append({QObject::tr("Mean"), QString::number(stats.m_mean[0])});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString::number(stats.m_stdDev[0])});
|
||||
info.info.append({QObject::tr("Median"), QString::number(stats.m_median[0])});
|
||||
info.info.append({QObject::tr("Minimum"), QString::number(stats.m_min[0])});
|
||||
info.info.append({QObject::tr("Maximum"), QString::number(stats.m_max[0])});
|
||||
info.info.append({QObject::tr("MAD"), QString::number(stats.m_mad[0])});
|
||||
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * stats.m_saturated[0] / rawImage->size()) + "%"});
|
||||
}
|
||||
qDebug() << "Star fit" << timer.restart();
|
||||
else
|
||||
{
|
||||
info.info.append({QObject::tr("Mean"), QString("%1 %2 %3").arg(stats.m_mean[0]).arg(stats.m_mean[1]).arg(stats.m_mean[2])});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString("%1 %2 %3").arg(stats.m_stdDev[0]).arg(stats.m_stdDev[1]).arg(stats.m_stdDev[2])});
|
||||
info.info.append({QObject::tr("Median"), QString("%1 %2 %3").arg(stats.m_median[0]).arg(stats.m_median[1]).arg(stats.m_median[2])});
|
||||
info.info.append({QObject::tr("Minimum"), QString("%1 %2 %3").arg(stats.m_min[0]).arg(stats.m_min[1]).arg(stats.m_min[2])});
|
||||
info.info.append({QObject::tr("Maximum"), QString("%1 %2 %3").arg(stats.m_max[0]).arg(stats.m_max[1]).arg(stats.m_max[2])});
|
||||
info.info.append({QObject::tr("MAD"), QString("%1 %2 %3").arg(stats.m_mad[0]).arg(stats.m_mad[1]).arg(stats.m_mad[2])});
|
||||
info.info.append({QObject::tr("Saturated"), QString("%1 %2 %3%").arg(100.0 * stats.m_saturated[0] / rawImage->size())
|
||||
.arg(100.0 * stats.m_saturated[1] / rawImage->size())
|
||||
.arg(100.0 * stats.m_saturated[2] / rawImage->size())});
|
||||
}
|
||||
}
|
||||
|
||||
if(m_thumbnail)
|
||||
{
|
||||
if(rawImage && rawImage->valid())
|
||||
{
|
||||
if(QUALITY_RESIZE)
|
||||
rawImage->resize(THUMB_SIZE, THUMB_SIZE);
|
||||
|
||||
rawImage->convertToThumbnail();
|
||||
}
|
||||
QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage));
|
||||
}
|
||||
else
|
||||
{
|
||||
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr<RawImage>, rawImage), Q_ARG(ImageInfoData, info));
|
||||
}
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(void*, rawImage), Q_ARG(ImageInfoData, info));
|
||||
catch(std::exception e)
|
||||
{
|
||||
qDebug() << m_file << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info)
|
||||
@@ -362,8 +439,300 @@ bool readFITSHeader(const QString &path, ImageInfoData &info)
|
||||
int status = 0;
|
||||
fits_open_diskfile(&fr, path.toLocal8Bit().data(), READONLY, &status);
|
||||
|
||||
status = loadFITSHeader(fr, info);
|
||||
|
||||
fits_close_file(fr, &status);
|
||||
if(fr && status == 0)
|
||||
{
|
||||
status = loadFITSHeader(fr, info);
|
||||
fits_close_file(fr, &status);
|
||||
}
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFReader xisf;
|
||||
xisf.open(path.toLocal8Bit().data());
|
||||
const LibXISF::Image &image = xisf.getImage(0, false);
|
||||
|
||||
auto fitskeywords = image.fitsKeywords();
|
||||
for(auto fits : fitskeywords)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
|
||||
auto imageproperties = image.imageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
info.wcs = std::make_shared<WCSData>(image.width(), image.height(), info.fitsHeader);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
catch (LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << err.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage)
|
||||
{
|
||||
bool ret = false;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadRAW(path, info, rawImage);
|
||||
qDebug() << "LoadRAW" << timer.elapsed();
|
||||
}
|
||||
else if(path.endsWith(".FIT", Qt::CaseInsensitive) || path.endsWith(".FITS", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadFITS(path, info, rawImage);
|
||||
qDebug() << "LoadFITS" << timer.elapsed();
|
||||
}
|
||||
else if(path.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
{
|
||||
ret = loadXISF(path, info, rawImage);
|
||||
qDebug() << "LoadXISF" << timer.elapsed();
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img(path);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb)
|
||||
img.convertToColorSpace(QColorSpace::SRgb);
|
||||
#endif
|
||||
|
||||
ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData());
|
||||
info.info.append({QObject::tr("Width"), QString::number(img.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(img.height())});
|
||||
if(exif)
|
||||
{
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS);
|
||||
loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE);
|
||||
exif_data_free(exif);
|
||||
}
|
||||
rawImage = std::make_shared<RawImage>(img);
|
||||
qDebug() << "LoadQImage" << timer.elapsed();
|
||||
ret = !img.isNull();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams ¶ms, QSemaphore *semaphore) :
|
||||
m_infile(in),
|
||||
m_outfile(out),
|
||||
m_format(format),
|
||||
m_params(params),
|
||||
m_semaphore(semaphore)
|
||||
{
|
||||
}
|
||||
|
||||
void writeFITSImage(fitsfile *fw, std::shared_ptr<RawImage> rawimage, ImageInfoData &imageinfo)
|
||||
{
|
||||
static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"};
|
||||
|
||||
int status = 0;
|
||||
long firstpix[3] = {1,1,1};
|
||||
|
||||
int channels = rawimage->channels();
|
||||
int naxis = channels == 1 ? 2 : 3;
|
||||
long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->channels()};
|
||||
|
||||
std::vector<RawImage> planes;
|
||||
if(channels == 1)
|
||||
planes.push_back(*rawimage);
|
||||
else
|
||||
planes = rawimage->split();
|
||||
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
fits_create_img(fw, BYTE_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TBYTE, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
fits_create_img(fw, USHORT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TUSHORT, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
case RawImage::FLOAT32:
|
||||
fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status);
|
||||
for(int i=0; i<channels; i++)
|
||||
{
|
||||
firstpix[2] = i+1;
|
||||
fits_write_pix(fw, TFLOAT, firstpix, rawimage->size(), planes[i].data(), &status);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
for(const FITSRecord &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(skipKeys.contains(record.key) || record.xisf)continue;
|
||||
|
||||
bool isdouble;
|
||||
bool isint;
|
||||
bool isbool = record.value.toString() == "T" || record.value.toString() == "F";
|
||||
double vald = record.value.toDouble(&isdouble);
|
||||
int valb = record.value.toString() == "T";
|
||||
long long vall = record.value.toLongLong(&isint);
|
||||
QByteArray str = record.value.toString().toLatin1();
|
||||
if(isdouble)
|
||||
fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isint)
|
||||
fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(isbool)
|
||||
fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "COMMENT")
|
||||
fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else if(record.key == "HISTORY")
|
||||
fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
else
|
||||
fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
}
|
||||
}
|
||||
|
||||
void ConvertRunable::run()
|
||||
{
|
||||
QSemaphoreReleaser release;
|
||||
if(m_semaphore)release = QSemaphoreReleaser(m_semaphore);
|
||||
|
||||
ImageInfoData imageinfo;
|
||||
std::shared_ptr<RawImage> rawimage;
|
||||
loadImage(m_infile, imageinfo, rawimage);
|
||||
QFileInfo info(m_outfile);
|
||||
info.dir().mkpath(".");
|
||||
|
||||
if(rawimage)
|
||||
{
|
||||
if(m_format == "xisf")
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFWriter xisf;
|
||||
int channelCount = rawimage->channels();
|
||||
LibXISF::Image::SampleFormat sampleFormat;
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8: sampleFormat = LibXISF::Image::UInt8; break;
|
||||
case RawImage::UINT16: sampleFormat = LibXISF::Image::UInt16; break;
|
||||
case RawImage::FLOAT32: sampleFormat = LibXISF::Image::Float32; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Planar);
|
||||
if(channelCount == 1)
|
||||
{
|
||||
std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize());
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t off = 0;
|
||||
std::vector<RawImage> planes = rawimage->split();
|
||||
for(const auto &plane : planes)
|
||||
{
|
||||
std::memcpy(image.imageData<uint8_t>() + off, plane.data(), plane.size() * RawImage::typeSize(plane.type()));
|
||||
off += plane.size() * RawImage::typeSize(plane.type());
|
||||
}
|
||||
}
|
||||
for(auto &record : imageinfo.fitsHeader)
|
||||
{
|
||||
if(record.xisf)continue;
|
||||
|
||||
if(record.value.typeId() == QMetaType::Bool)
|
||||
image.addFITSKeyword({record.key.toStdString(), record.value.toBool() ? "T" : "F", record.comment.toStdString()});
|
||||
else
|
||||
image.addFITSKeyword({record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});
|
||||
}
|
||||
|
||||
if(m_params.compressionType.startsWith("zstd") && LibXISF::DataBlock::CompressionCodecSupported(LibXISF::DataBlock::ZSTD))
|
||||
image.setCompression(LibXISF::DataBlock::ZSTD, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("lz4hc"))
|
||||
image.setCompression(LibXISF::DataBlock::LZ4HC, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("lz4"))
|
||||
image.setCompression(LibXISF::DataBlock::LZ4, m_params.compressionLevel);
|
||||
else if(m_params.compressionType.startsWith("zlib"))
|
||||
image.setCompression(LibXISF::DataBlock::Zlib, m_params.compressionLevel);
|
||||
|
||||
if(m_params.compressionType.endsWith("+sh"))
|
||||
image.setByteshuffling(true);
|
||||
|
||||
xisf.writeImage(image);
|
||||
xisf.save(m_outfile.toLocal8Bit().data());
|
||||
}
|
||||
catch(LibXISF::Error &err)
|
||||
{
|
||||
qDebug() << "Failed to save XISF image" << err.what();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(m_format == "fits")
|
||||
{
|
||||
int status = 0;
|
||||
fitsfile *fw;
|
||||
if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile);
|
||||
fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status);
|
||||
if(!m_params.compressionType.isEmpty())
|
||||
{
|
||||
if(m_params.compressionType == "gzip")
|
||||
fits_set_compression_type(fw, GZIP_1, &status);
|
||||
else if(m_params.compressionType == "rice")
|
||||
fits_set_compression_type(fw, RICE_1, &status);
|
||||
}
|
||||
writeFITSImage(fw, rawimage, imageinfo);
|
||||
fits_close_file(fw, &status);
|
||||
return;
|
||||
}
|
||||
|
||||
// if nothing else try QImage
|
||||
{
|
||||
QImage::Format format = QImage::Format_Invalid;
|
||||
int width = rawimage->widthBytes();
|
||||
switch(rawimage->type())
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
if(rawimage->channels() == 1)format = QImage::Format_Grayscale8;
|
||||
else if(rawimage->channels() == 3)format = QImage::Format_RGBX8888;
|
||||
else if(rawimage->channels() == 4)format = QImage::Format_RGBA8888;
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
if(rawimage->channels() == 1)format = QImage::Format_Grayscale16;
|
||||
else if(rawimage->channels() == 3)format = QImage::Format_RGBX64;
|
||||
else if(rawimage->channels() == 4)format = QImage::Format_RGBA64;
|
||||
width *= 2;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if(format == QImage::Format_Invalid)return;
|
||||
|
||||
QImage qimage(rawimage->width(), rawimage->height(), format);
|
||||
for(uint32_t i=0; i < rawimage->height(); i++)
|
||||
std::memcpy(qimage.scanLine(i), rawimage->data(i), width);
|
||||
qimage.save(m_outfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertParams::ConvertParams(const QVariantMap &map)
|
||||
{
|
||||
bool ok = false;
|
||||
if(map.contains("compressionLevel"))
|
||||
compressionLevel = std::clamp(map["compressionLevel"].toInt(&ok), -1, 100);
|
||||
|
||||
if(!ok)compressionLevel = -1;
|
||||
|
||||
if(map.contains("compressionType"))
|
||||
compressionType = map["compressionType"].toString();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
|
||||
#include <QRunnable>
|
||||
#include <QString>
|
||||
#include <QSemaphore>
|
||||
#include "imageinfo.h"
|
||||
|
||||
class RawImage;
|
||||
|
||||
bool readFITSHeader(const QString &path, ImageInfoData &info);
|
||||
bool readXISFHeader(const QString &path, ImageInfoData &info);
|
||||
bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage> &rawImage);
|
||||
|
||||
class Image;
|
||||
|
||||
@@ -14,9 +19,31 @@ class LoadRunable : public QRunnable
|
||||
QString m_file;
|
||||
Image *m_receiver;
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
bool m_thumbnail;
|
||||
public:
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level);
|
||||
void run();
|
||||
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail = false);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
|
||||
class ConvertRunable : public QRunnable
|
||||
{
|
||||
public:
|
||||
struct ConvertParams
|
||||
{
|
||||
int compressionLevel = -1;
|
||||
QString compressionType;
|
||||
ConvertParams(){}
|
||||
ConvertParams(const QVariantMap &map);
|
||||
};
|
||||
ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams ¶ms = ConvertParams(), QSemaphore *semaphore = nullptr);
|
||||
void run() override;
|
||||
private:
|
||||
QString m_infile;
|
||||
QString m_outfile;
|
||||
QString m_format;
|
||||
ConvertParams m_params;
|
||||
QSemaphore *m_semaphore;
|
||||
};
|
||||
|
||||
#endif // LOADRUNABLE_H
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QTranslator>
|
||||
#include <stdlib.h>
|
||||
#include "libxisf.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifdef __linux__
|
||||
setenv("LC_NUMERIC", "C", 1);
|
||||
#endif
|
||||
|
||||
QSurfaceFormat format;
|
||||
format.setMajorVersion(3);
|
||||
format.setMinorVersion(2);
|
||||
format.setOption(QSurfaceFormat::DebugContext);
|
||||
format.setMinorVersion(3);
|
||||
//format.setOption(QSurfaceFormat::DebugContext);
|
||||
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setOrganizationName("nou");
|
||||
a.setApplicationName("Tenmon");
|
||||
a.setWindowIcon(QIcon(":/icon.png"));
|
||||
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
|
||||
|
||||
QTranslator translator;
|
||||
QTranslator translator2;
|
||||
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
|
||||
a.installTranslator(&translator);
|
||||
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
|
||||
a.installTranslator(&translator2);
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
@@ -10,10 +10,22 @@
|
||||
#include <QProgressDialog>
|
||||
#include <QDebug>
|
||||
#include <QDockWidget>
|
||||
#include <QActionGroup>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QThreadPool>
|
||||
#include <QStatusBar>
|
||||
#include <QImageReader>
|
||||
#include <QMimeDatabase>
|
||||
#include "loadrunable.h"
|
||||
#include "markedfiles.h"
|
||||
#include "about.h"
|
||||
#include "statusbar.h"
|
||||
#include "settingsdialog.h"
|
||||
#include "histogram.h"
|
||||
#include "batchprocessing.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/ioctl.h>
|
||||
@@ -21,93 +33,190 @@
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
bool moveToTrash(const QString &path);
|
||||
|
||||
int MainWindow::socketPair[2] = {0, 0};
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
{
|
||||
qRegisterMetaType<ImageInfoData>("ImageInfoData");
|
||||
qRegisterMetaType<RawImage*>("RawImage");
|
||||
qRegisterMetaType<std::shared_ptr<RawImage>>("std::shared_ptr<RawImage>");
|
||||
|
||||
SettingsDialog::loadSettings();
|
||||
|
||||
QStringList nameFilter;
|
||||
_saveFilter = tr("FITS (*.fits *.fit);;XISF (*.xisf);;");
|
||||
_openFilter = tr("Images (");
|
||||
QMimeDatabase db;
|
||||
auto supportedFormats = QImageReader::supportedMimeTypes();
|
||||
QStringList filters;
|
||||
for(auto format : supportedFormats)
|
||||
{
|
||||
QMimeType mimeType = db.mimeTypeForName(format);
|
||||
_saveFilter.append(mimeType.filterString() + ";;");
|
||||
_openFilter.append("*.");
|
||||
_openFilter.append(mimeType.suffixes().join(" *."));
|
||||
_openFilter.append(" ");
|
||||
nameFilter.append(mimeType.suffixes());
|
||||
}
|
||||
_openFilter.append("*.fit *.fits *.xisf *.cr2 *.cr3 *.nef *.dng)");
|
||||
_openFilter.append(tr(";;All files (*)"));
|
||||
nameFilter.append({"fit", "fits", "xisf", "cr2", "cr3", "nef", "dng"});
|
||||
QImageReader::setAllocationLimit(0);
|
||||
|
||||
m_info = new ImageInfo(this);
|
||||
QDockWidget *infoDock = new QDockWidget(tr("Image info"), this);
|
||||
infoDock->setWidget(m_info);
|
||||
infoDock->setObjectName("infoDock");
|
||||
addDockWidget(Qt::LeftDockWidgetArea, infoDock);
|
||||
//m_image = new ImageScrollArea(this);
|
||||
//m_image->resize(0,0);
|
||||
//setCentralWidget(m_image);
|
||||
resize(800, 600);
|
||||
|
||||
m_imageGL = new ImageScrollAreaGL(this);
|
||||
setCentralWidget(m_imageGL);
|
||||
|
||||
m_stretchPanel = new StretchPanel(this);
|
||||
connect(m_stretchPanel, SIGNAL(paramChanged(float,float,float)), m_imageGL->imageWidget(), SLOT(setMTFParams(float,float,float)));
|
||||
connect(m_stretchPanel, &StretchPanel::autoStretch, [&](){ m_stretchPanel->stretchImage(m_ringList->currentImage().get()); });
|
||||
connect(m_stretchPanel, &StretchPanel::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
|
||||
connect(m_stretchPanel, &StretchPanel::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
|
||||
|
||||
m_ringList = new ImageRingList(this);
|
||||
m_filesystem = new FilesystemWidget(m_ringList, this);
|
||||
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
|
||||
setStatusBar(new QStatusBar(this));
|
||||
|
||||
m_database = new Database(this);
|
||||
if(!m_database->init())
|
||||
QMessageBox::critical(this, tr("Can't open DB"), tr("Can't open SQLITE database"));
|
||||
|
||||
m_imageGL = new ImageScrollAreaGL(m_database, this);
|
||||
setCentralWidget(m_imageGL);
|
||||
|
||||
StatusBar *statusBar = new StatusBar(this);
|
||||
setStatusBar(statusBar);
|
||||
connect(m_imageGL->imageWidget(), &ImageWidget::status, statusBar, &StatusBar::newStatus);
|
||||
|
||||
m_stretchPanel = new StretchToolbar(this);
|
||||
connect(m_stretchPanel, &StretchToolbar::paramChanged, m_imageGL->imageWidget(), &ImageWidget::setMTFParams);
|
||||
connect(m_stretchPanel, &StretchToolbar::autoStretch, [&](){ m_stretchPanel->stretchImage(m_ringList->currentImage().get()); });
|
||||
connect(m_stretchPanel, &StretchToolbar::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
|
||||
connect(m_stretchPanel, &StretchToolbar::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
|
||||
connect(m_stretchPanel, &StretchToolbar::falseColor, m_imageGL->imageWidget(), &ImageWidget::falseColor);
|
||||
|
||||
m_ringList = new ImageRingList(m_database, nameFilter, this);
|
||||
m_filesystem = new FilesystemWidget(m_ringList, this);
|
||||
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
|
||||
connect(m_filesystem, &FilesystemWidget::sortChanged, m_ringList, &ImageRingList::setSort);
|
||||
connect(m_filesystem, &FilesystemWidget::reverseSort, m_ringList, &ImageRingList::reverseSort);
|
||||
|
||||
m_filetree = new Filetree(this);
|
||||
connect(m_filetree, &Filetree::fileSelected, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
connect(m_filetree, &Filetree::copyFiles, [this](const QString &path){ copyOrMove(true, path); });
|
||||
connect(m_filetree, &Filetree::moveFiles, [this](const QString &path){ copyOrMove(false, path); });
|
||||
connect(m_filetree, &Filetree::indexDirectory, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::indexDir));
|
||||
|
||||
m_databaseView = new DataBaseView(m_database, this);
|
||||
connect(m_databaseView, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
|
||||
|
||||
QDockWidget *stretchDock = new QDockWidget(tr("Stretch"), this);
|
||||
stretchDock->setWidget(m_stretchPanel);
|
||||
stretchDock->setObjectName("strechDock");
|
||||
addDockWidget(Qt::TopDockWidgetArea, stretchDock);
|
||||
addToolBar(Qt::TopToolBarArea, m_stretchPanel);
|
||||
|
||||
QDockWidget *filesystemDock = new QDockWidget(tr("Filesystem"), this);
|
||||
filesystemDock->setWidget(m_filesystem);
|
||||
filesystemDock->setObjectName("filesystemDock");
|
||||
addDockWidget(Qt::LeftDockWidgetArea, filesystemDock);
|
||||
|
||||
QDockWidget *databaseViewDock = new QDockWidget(tr("FITS files database"), this);
|
||||
QDockWidget *databaseViewDock = new QDockWidget(tr("FITS/XISF files database"), this);
|
||||
databaseViewDock->setWidget(m_databaseView);
|
||||
databaseViewDock->setObjectName("databaseViewDock");
|
||||
databaseViewDock->hide();
|
||||
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
|
||||
|
||||
QDockWidget *filetreeDock = new QDockWidget(tr("File tree"), this);
|
||||
filetreeDock->setWidget(m_filetree);
|
||||
filetreeDock->setObjectName("filetreeDock");
|
||||
databaseViewDock->hide();
|
||||
addDockWidget(Qt::LeftDockWidgetArea, filetreeDock);
|
||||
|
||||
Histogram *histogram = new Histogram(this);
|
||||
QDockWidget *histogramDock = new QDockWidget(tr("Histogram"), this);
|
||||
histogramDock->setWidget(histogram);
|
||||
histogramDock->setObjectName("histogramDock");
|
||||
histogramDock->hide();
|
||||
addDockWidget(Qt::LeftDockWidgetArea, histogramDock);
|
||||
|
||||
setWindowTitle(tr("Tenmon"));
|
||||
|
||||
connect(m_ringList, SIGNAL(pixmapLoaded(Image*)), this, SLOT(pixmapLoaded(Image*)));
|
||||
connect(m_ringList, SIGNAL(currentImageChanged(int)), this, SLOT(updateWindowTitle()));
|
||||
connect(m_ringList, SIGNAL(infoLoaded(ImageInfoData)), m_info, SLOT(setInfo(const ImageInfoData&)));
|
||||
connect(m_ringList, SIGNAL(currentImageChanged(int)), m_filesystem, SLOT(selectFile(int)));
|
||||
connect(m_ringList, &ImageRingList::thumbnailLoaded, m_imageGL->imageWidget(), &ImageWidget::thumbnailLoaded);
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, m_stretchPanel, &StretchToolbar::imageLoaded);
|
||||
connect(m_ringList, &ImageRingList::pixmapLoaded, histogram, &Histogram::imageLoaded);
|
||||
connect(m_imageGL->imageWidget(), &ImageWidget::fileDropped, this, static_cast<void (MainWindow::*)(const QString &)>(&MainWindow::loadFile));
|
||||
|
||||
QMenu *fileMenu = new QMenu(tr("File"), this);
|
||||
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence("Ctrl+O"));
|
||||
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()));
|
||||
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence("Ctrl+S"));
|
||||
fileMenu->addAction(tr("Open"), QKeySequence::Open, this, SLOT(loadFile()));
|
||||
fileMenu->addAction(tr("Open directory recursively"), this, &MainWindow::loadDir);
|
||||
fileMenu->addAction(tr("Save as"), QKeySequence::Save, this, SLOT(saveAs()));
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, SLOT(copyMarked()));
|
||||
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, SLOT(moveMarked()));
|
||||
fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
||||
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
||||
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
||||
fileMenu->addAction(tr("Batch processing"), Qt::Key_B | Qt::CTRL, [this](){
|
||||
BatchProcessing *batchProcessing = new BatchProcessing(this);
|
||||
batchProcessing->exec();
|
||||
delete batchProcessing;
|
||||
});
|
||||
fileMenu->addSeparator();
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
||||
liveModeAction->setCheckable(true);
|
||||
fileMenu->addAction(tr("Exit"), this, SLOT(close()));
|
||||
QAction *exitAction = fileMenu->addAction(tr("Exit"), this, SLOT(close()));
|
||||
exitAction->setShortcut(QKeySequence::Quit);
|
||||
menuBar()->addMenu(fileMenu);
|
||||
|
||||
QMenu *editMenu = new QMenu(tr("Edit"), this);
|
||||
editMenu->addAction(tr("Settings"), this, &MainWindow::showSettingsDialog);
|
||||
menuBar()->addMenu(editMenu);
|
||||
|
||||
QMenu *viewMenu = new QMenu(tr("View"), this);
|
||||
viewMenu->addAction(tr("Zoom In"), m_imageGL, SLOT(zoomIn()), QKeySequence::ZoomIn);
|
||||
viewMenu->addAction(tr("Zoom Out"), m_imageGL, SLOT(zoomOut()), QKeySequence::ZoomOut);
|
||||
viewMenu->addAction(tr("Best Fit"), m_imageGL, SLOT(bestFit()), QKeySequence("Ctrl+1"));
|
||||
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn, m_imageGL, SLOT(zoomIn()));
|
||||
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut, m_imageGL, SLOT(zoomOut()));
|
||||
viewMenu->addAction(tr("Best Fit"), QKeySequence("Ctrl+1"), m_imageGL, SLOT(bestFit()));
|
||||
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
|
||||
viewMenu->addAction(tr("Fullscreen"), this, SLOT(toggleFullScreen()), QKeySequence::FullScreen);
|
||||
viewMenu->addSeparator();
|
||||
QMenu *bayerMenu = viewMenu->addMenu(tr("Bayer mask"));
|
||||
QActionGroup *bayerActionGroup = new QActionGroup(this);
|
||||
QAction *rggbAction = bayerActionGroup->addAction(tr("RGGB"));//0 0
|
||||
QAction *grbgAction = bayerActionGroup->addAction(tr("GRBG"));//1 0
|
||||
QAction *gbrgAction = bayerActionGroup->addAction(tr("GBRG"));//0 1
|
||||
QAction *bggrAction = bayerActionGroup->addAction(tr("BGGR"));//1 1
|
||||
rggbAction->setCheckable(true); rggbAction->setData(0); rggbAction->setIcon(QIcon(":/bayer.png"));
|
||||
grbgAction->setCheckable(true); grbgAction->setData(1); grbgAction->setIcon(QIcon(":/grbg.png"));
|
||||
gbrgAction->setCheckable(true); gbrgAction->setData(2); gbrgAction->setIcon(QIcon(":/gbrg.png"));
|
||||
bggrAction->setCheckable(true); bggrAction->setData(3); bggrAction->setIcon(QIcon(":/bggr.png"));
|
||||
bayerMenu->addActions({rggbAction, grbgAction, gbrgAction, bggrAction});
|
||||
viewMenu->addMenu(bayerMenu);
|
||||
connect(bayerActionGroup, &QActionGroup::triggered, [this](QAction *action){
|
||||
int data = action->data().toInt();
|
||||
m_imageGL->imageWidget()->setBayerMask(data);
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/bayermask", data);
|
||||
});
|
||||
|
||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), Qt::Key_F2, [this](bool checked){
|
||||
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
||||
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
|
||||
m_imageGL->imageWidget()->showThumbnail(checked);
|
||||
if(checked)m_ringList->loadThumbnails();
|
||||
else m_ringList->stopLoading();
|
||||
});
|
||||
thumbnailsAction->setCheckable(true);
|
||||
QAction *slideshowAction = viewMenu->addAction(tr("Slideshow"), Qt::Key_F3, m_ringList, &ImageRingList::toggleSlideshow);
|
||||
slideshowAction->setCheckable(true);
|
||||
menuBar()->addMenu(viewMenu);
|
||||
|
||||
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
||||
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F5);
|
||||
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
|
||||
selectMenu->addAction(tr("Mark"), Qt::Key_F7, this, SLOT(markImage()));
|
||||
selectMenu->addAction(tr("Unmark"), Qt::Key_F8, this, SLOT(unmarkImage()));
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
|
||||
selectMenu->addAction(tr("Unmark and next"), this, SLOT(unmarkAndNext()), Qt::Key_X);
|
||||
selectMenu->addAction(tr("Mark and next"), Qt::Key_M, this, SLOT(markAndNext()));
|
||||
selectMenu->addAction(tr("Unmark and next"), Qt::Key_X, this, SLOT(unmarkAndNext()));
|
||||
selectMenu->addAction(tr("Show marked"), this, &MainWindow::showMarkFilesDialog);
|
||||
menuBar()->addMenu(selectMenu);
|
||||
|
||||
QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
||||
/*QMenu *analyzeMenu = new QMenu(tr("Analyze"), this);
|
||||
QActionGroup *analyzeGroup = new QActionGroup(this);
|
||||
connect(analyzeGroup, &QActionGroup::triggered, [](QAction* action) {
|
||||
static QAction* lastAction = nullptr;
|
||||
@@ -122,7 +231,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
|
||||
QAction *statsAction = analyzeGroup->addAction(tr("Image statistics"));
|
||||
QAction *peakAction = analyzeGroup->addAction(tr("Peak finder"));
|
||||
QAction *starAction = analyzeGroup->addAction("Star finder");
|
||||
QAction *starAction = analyzeGroup->addAction(tr("Star finder"));
|
||||
statsAction->setCheckable(true);
|
||||
peakAction->setCheckable(true);
|
||||
starAction->setCheckable(true);
|
||||
@@ -130,26 +239,45 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
connect(peakAction, SIGNAL(toggled(bool)), this, SLOT(peakFinder(bool)));
|
||||
connect(starAction, SIGNAL(toggled(bool)), this, SLOT(starFinder(bool)));
|
||||
analyzeMenu->addActions({statsAction, peakAction, starAction});
|
||||
menuBar()->addMenu(analyzeMenu);
|
||||
menuBar()->addMenu(analyzeMenu);*/
|
||||
|
||||
QMenu *dockMenu = new QMenu(tr("Docks"), this);
|
||||
dockMenu->addAction(infoDock->toggleViewAction());
|
||||
dockMenu->addAction(stretchDock->toggleViewAction());
|
||||
dockMenu->addAction(m_stretchPanel->toggleViewAction());
|
||||
dockMenu->addAction(filesystemDock->toggleViewAction());
|
||||
dockMenu->addAction(databaseViewDock->toggleViewAction());
|
||||
dockMenu->addAction(filetreeDock->toggleViewAction());
|
||||
dockMenu->addAction(histogramDock->toggleViewAction());
|
||||
menuBar()->addMenu(dockMenu);
|
||||
|
||||
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
|
||||
helpMenu->addAction(tr("Help"), QKeySequence::HelpContents, [this]{ HelpDialog help(this); help.exec(); });
|
||||
helpMenu->addAction(tr("About Tenmon"), [this]{ About about(this); about.exec(); });
|
||||
helpMenu->addAction(tr("About Qt"), [this](){ QMessageBox::aboutQt(this); });
|
||||
|
||||
setupSigterm();
|
||||
QSettings settings;
|
||||
restoreGeometry(settings.value("mainwindow/geometry").toByteArray());
|
||||
restoreState(settings.value("mainwindow/state").toByteArray());
|
||||
int bayermask = settings.value("mainwindow/bayermask", 0).toInt();
|
||||
switch(bayermask)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
rggbAction->setChecked(true); break;
|
||||
case 1:
|
||||
grbgAction->setChecked(true); break;
|
||||
case 2:
|
||||
gbrgAction->setChecked(true); break;
|
||||
case 3:
|
||||
bggrAction->setChecked(true); break;
|
||||
}
|
||||
|
||||
QStringList standardLocations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
|
||||
if(standardLocations.size())
|
||||
_lastDir = standardLocations.first();
|
||||
|
||||
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
|
||||
m_filesystem->setDir(_lastDir);
|
||||
|
||||
QStringList args = QCoreApplication::arguments();
|
||||
args.removeFirst();
|
||||
@@ -167,6 +295,16 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
}
|
||||
|
||||
m_imageGL->setFocus();
|
||||
|
||||
// workaround for nasty wayland backend bug https://bugreports.qt.io/browse/QTBUG-87332
|
||||
if(static_cast<QGuiApplication*>(QCoreApplication::instance())->platformName() == "wayland")
|
||||
{
|
||||
infoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filesystemDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
databaseViewDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
m_stretchPanel->setFloatable(false);
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
@@ -179,9 +317,11 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_Left:
|
||||
case Qt::Key_Up:
|
||||
m_ringList->decrement();
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
case Qt::Key_Down:
|
||||
m_ringList->increment();
|
||||
break;
|
||||
default:
|
||||
@@ -232,6 +372,75 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::copyOrMove(bool copy)
|
||||
{
|
||||
QString dest = QFileDialog::getExistingDirectory(this,
|
||||
tr("Select destination"),
|
||||
_lastDir,
|
||||
QFileDialog::ShowDirsOnly);
|
||||
copyOrMove(copy, dest);
|
||||
}
|
||||
|
||||
void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
{
|
||||
QDir dir(dest);
|
||||
if(!dest.isEmpty() && dir.exists())
|
||||
{
|
||||
int i = 0;
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
QProgressDialog progress(copy ? tr("Copying") : tr("Moving"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.show();
|
||||
for(const QString &file : files)
|
||||
{
|
||||
bool result = false;
|
||||
QFileInfo info(file);
|
||||
QFile srcFile(file);
|
||||
QFile dstFile(dir.absoluteFilePath(info.fileName()));
|
||||
|
||||
if(dstFile.exists())
|
||||
continue;
|
||||
|
||||
if(progress.wasCanceled())
|
||||
return;
|
||||
#ifdef __linux__
|
||||
if(copy)
|
||||
{
|
||||
srcFile.open(QIODevice::ReadOnly);
|
||||
dstFile.open(QIODevice::WriteOnly);
|
||||
if(ioctl(dstFile.handle(), BTRFS_IOC_CLONE, srcFile.handle()) < 0)
|
||||
{
|
||||
dstFile.remove();
|
||||
dstFile.close();
|
||||
result = srcFile.copy(dstFile.fileName());
|
||||
}
|
||||
else result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = srcFile.rename(dstFile.fileName());
|
||||
}
|
||||
#else
|
||||
if(copy)
|
||||
result = srcFile.copy(dstFile.fileName());
|
||||
else
|
||||
result = srcFile.rename(dstFile.fileName());
|
||||
#endif
|
||||
if(!result)
|
||||
{
|
||||
QString t = copy ? tr("Failed to copy") : tr("Failed to move");
|
||||
QString m = copy ? tr("Failed to copy from %1 to %2") : tr("Failed to move from %1 to %2");
|
||||
QMessageBox::StandardButton button = QMessageBox::warning(this, t,
|
||||
m.arg(srcFile.fileName()).arg(dir.absolutePath()),
|
||||
QMessageBox::Ignore | QMessageBox::Abort);
|
||||
if(button == QMessageBox::Abort)return;
|
||||
}
|
||||
progress.setValue(i++);
|
||||
}
|
||||
m_database->clearMarkedFiles();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::socketNotify()
|
||||
{
|
||||
socketNotifier->setEnabled(false);
|
||||
@@ -243,20 +452,22 @@ void MainWindow::socketNotify()
|
||||
|
||||
void MainWindow::pixmapLoaded(Image *image)
|
||||
{
|
||||
//m_image->setImage(image->pixmap());
|
||||
if(image->rawImage())
|
||||
{
|
||||
m_imageGL->setImage(image->rawImage());
|
||||
m_imageGL->setImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadFile()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, tr("Open file"), _lastDir, tr("Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)"));
|
||||
QString file = QFileDialog::getOpenFileName(this,
|
||||
tr("Open file"),
|
||||
_lastDir,
|
||||
_openFilter);
|
||||
loadFile(file);
|
||||
}
|
||||
|
||||
void MainWindow::loadFile(const QString path)
|
||||
void MainWindow::loadFile(const QString &path)
|
||||
{
|
||||
if(!path.isEmpty())
|
||||
{
|
||||
@@ -269,7 +480,6 @@ void MainWindow::loadFile(const QString path)
|
||||
_lastDir = info.canonicalPath();
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
m_filesystem->setDir(_lastDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,9 +488,28 @@ void MainWindow::loadFile(int row)
|
||||
m_ringList->loadFile(row);
|
||||
}
|
||||
|
||||
void MainWindow::loadDir()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this,
|
||||
tr("Open directory recursively"),
|
||||
_lastDir);
|
||||
if(!dir.isEmpty())
|
||||
{
|
||||
_lastDir = dir;
|
||||
m_ringList->setDir(dir, QString(), true);
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::indexDir()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir);
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir, QFileDialog::ShowDirsOnly);
|
||||
indexDir(dir);
|
||||
}
|
||||
|
||||
void MainWindow::indexDir(const QString &dir)
|
||||
{
|
||||
if(!dir.isEmpty())
|
||||
{
|
||||
QProgressDialog progressDialog(tr("Indexing FITS files"), tr("Cancel"), 0, 1, this);
|
||||
@@ -289,17 +518,57 @@ void MainWindow::indexDir()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::reindex()
|
||||
{
|
||||
QProgressDialog progressDialog(tr("Indexing FITS files"), tr("Cancel"), 0, 1, this);
|
||||
progressDialog.setModal(true);
|
||||
m_database->reindex(&progressDialog);
|
||||
}
|
||||
|
||||
void MainWindow::saveAs()
|
||||
{
|
||||
QString file = QFileDialog::getSaveFileName(this, tr("Save as"), _lastDir, tr("Images (*.jpg *.png *.JPG *.PNG)"));
|
||||
QString selectedFilter;
|
||||
QString file = QFileDialog::getSaveFileName(this,
|
||||
tr("Save as"),
|
||||
_lastDir,
|
||||
_saveFilter,
|
||||
&selectedFilter);
|
||||
auto filterToFormat = [](const QString &file, const QString &filter) -> const char*
|
||||
{
|
||||
QString suffix = QFileInfo(file).suffix();
|
||||
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "JPEG";
|
||||
if(!suffix.compare("png", Qt::CaseInsensitive))return "PNG";
|
||||
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "FITS";
|
||||
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "XISF";
|
||||
if(filter.contains("png"))return "PNG";
|
||||
if(filter.contains("fits"))return "FITS";
|
||||
if(filter.contains("xisf"))return "XISF";
|
||||
return "JPEG";
|
||||
};
|
||||
|
||||
if(!file.isEmpty())
|
||||
{
|
||||
QImage img = m_imageGL->imageWidget()->renderToImage();
|
||||
if(!img.isNull())
|
||||
img.save(file);
|
||||
QString format = filterToFormat(file, selectedFilter);
|
||||
|
||||
if(format == "FITS" || format == "XISF")
|
||||
{
|
||||
convert(file, format);
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img = m_imageGL->imageWidget()->renderToImage();
|
||||
if(!img.isNull())
|
||||
img.save(file, filterToFormat(file, selectedFilter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::convert(const QString &outfile, const QString &format)
|
||||
{
|
||||
QString file = m_ringList->currentImage()->name();
|
||||
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format));
|
||||
}
|
||||
|
||||
void MainWindow::markImage()
|
||||
{
|
||||
ImagePtr ptr = m_ringList->currentImage();
|
||||
@@ -307,7 +576,10 @@ void MainWindow::markImage()
|
||||
{
|
||||
QString file = ptr->name();
|
||||
if(!file.isEmpty())
|
||||
{
|
||||
m_database->mark(file);
|
||||
m_ringList->updateMark();
|
||||
}
|
||||
|
||||
updateWindowTitle();
|
||||
}
|
||||
@@ -320,7 +592,10 @@ void MainWindow::unmarkImage()
|
||||
{
|
||||
QString file = ptr->name();
|
||||
if(!file.isEmpty())
|
||||
{
|
||||
m_database->unmark(file);
|
||||
m_ringList->updateMark();
|
||||
}
|
||||
|
||||
updateWindowTitle();
|
||||
}
|
||||
@@ -342,56 +617,40 @@ void MainWindow::unmarkAndNext()
|
||||
|
||||
void MainWindow::copyMarked()
|
||||
{
|
||||
QString dest = QFileDialog::getExistingDirectory(this, tr("Select destination"));
|
||||
QDir dir(dest);
|
||||
if(!dest.isEmpty() && dir.exists())
|
||||
{
|
||||
int i = 0;
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
QProgressDialog progress(tr("Copying"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.show();
|
||||
foreach(const QString &file, files)
|
||||
{
|
||||
QFileInfo info(file);
|
||||
QFile srcFile(file);
|
||||
QFile dstFile(dir.absoluteFilePath(info.fileName()));
|
||||
|
||||
if(dstFile.exists())
|
||||
continue;
|
||||
|
||||
if(progress.wasCanceled())
|
||||
break;
|
||||
#ifdef __linux__
|
||||
srcFile.open(QIODevice::ReadOnly);
|
||||
dstFile.open(QIODevice::WriteOnly);
|
||||
if(ioctl(dstFile.handle(), BTRFS_IOC_CLONE, srcFile.handle()) < 0)
|
||||
{
|
||||
dstFile.remove();
|
||||
dstFile.close();
|
||||
qDebug() << dstFile.fileName();
|
||||
srcFile.copy(dstFile.fileName());
|
||||
}
|
||||
#else
|
||||
srcFile.copy(dstFile.fileName());
|
||||
#endif
|
||||
progress.setValue(i++);
|
||||
}
|
||||
}
|
||||
copyOrMove(true);
|
||||
}
|
||||
|
||||
void MainWindow::toggleFullScreen()
|
||||
void MainWindow::moveMarked()
|
||||
{
|
||||
if(isFullScreen())
|
||||
copyOrMove(false);
|
||||
}
|
||||
|
||||
void MainWindow::deleteMarked()
|
||||
{
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
if(QMessageBox::question(this, tr("Move files to trash?"), tr("Do you want to move %1 files to trash?").arg(files.size())) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
QProgressDialog progress(tr("Moving marked files to trash"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.show();
|
||||
int i = 0;
|
||||
for(const QString &file : files)
|
||||
{
|
||||
showNormal();
|
||||
if(_maximized)showMaximized();
|
||||
}
|
||||
else
|
||||
{
|
||||
_maximized = isMaximized();
|
||||
showFullScreen();
|
||||
if(!QFile::exists(file))
|
||||
continue;
|
||||
|
||||
if(progress.wasCanceled())
|
||||
return;
|
||||
|
||||
if(!moveToTrash(file))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Failed to move file to trash"), tr("Failed to move file to trash %1").arg(file));
|
||||
return;
|
||||
}
|
||||
progress.setValue(i++);
|
||||
}
|
||||
m_database->clearMarkedFiles();
|
||||
}
|
||||
|
||||
void MainWindow::liveMode(bool active)
|
||||
@@ -414,6 +673,32 @@ void MainWindow::starFinder(bool findStars)
|
||||
m_ringList->setFindStars(findStars);
|
||||
}
|
||||
|
||||
void MainWindow::showMarkFilesDialog()
|
||||
{
|
||||
MarkedFiles markedFiles(this);
|
||||
markedFiles.exec();
|
||||
}
|
||||
|
||||
void MainWindow::showSettingsDialog()
|
||||
{
|
||||
SettingsDialog settingsDialog(this);
|
||||
connect(&settingsDialog, &SettingsDialog::preloadChanged, m_ringList, &ImageRingList::setPreload);
|
||||
settingsDialog.exec();
|
||||
}
|
||||
|
||||
void MainWindow::exportCSV()
|
||||
{
|
||||
QStringList documentDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
|
||||
if(documentDir.empty())documentDir.append("");
|
||||
QString file = QFileDialog::getSaveFileName(this,
|
||||
tr("Save as"),
|
||||
documentDir.first(),
|
||||
tr("CSV file (*.csv)"));
|
||||
|
||||
if(!file.isEmpty())
|
||||
m_databaseView->exportCSV(file);
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle()
|
||||
{
|
||||
ImagePtr ptr = m_ringList->currentImage();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "imageinfo.h"
|
||||
#include "imagescrollareagl.h"
|
||||
#include "filesystemwidget.h"
|
||||
#include "stretchpanel.h"
|
||||
#include "stretchtoolbar.h"
|
||||
#include "databaseview.h"
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
@@ -18,43 +18,56 @@ class MainWindow : public QMainWindow
|
||||
ImageScrollArea *m_image;
|
||||
ImageScrollAreaGL *m_imageGL;
|
||||
ImageRingList *m_ringList;
|
||||
StretchPanel *m_stretchPanel;
|
||||
StretchToolbar *m_stretchPanel;
|
||||
Database *m_database;
|
||||
ImageInfo *m_info;
|
||||
FilesystemWidget *m_filesystem;
|
||||
Filetree *m_filetree;
|
||||
DataBaseView *m_databaseView;
|
||||
static int socketPair[2];
|
||||
QSocketNotifier *socketNotifier;
|
||||
QString _lastDir;
|
||||
bool _maximized;
|
||||
QString _openFilter;
|
||||
QString _saveFilter;
|
||||
public:
|
||||
MainWindow(QWidget *parent = 0);
|
||||
~MainWindow();
|
||||
~MainWindow() override;
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event);
|
||||
void keyReleaseEvent(QKeyEvent *event);
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void keyReleaseEvent(QKeyEvent *event) override;
|
||||
void setupSigterm();
|
||||
static void signalHandler(int);
|
||||
void closeEvent(QCloseEvent *event);
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void copyOrMove(bool copy);
|
||||
void copyOrMove(bool copy, const QString &dest);
|
||||
protected slots:
|
||||
void socketNotify();
|
||||
void updateWindowTitle();
|
||||
void pixmapLoaded(Image *image);
|
||||
void loadFile();
|
||||
void loadFile(const QString path);
|
||||
void loadFile(const QString &path);
|
||||
void loadFile(int row);
|
||||
void loadDir();
|
||||
void indexDir();
|
||||
void indexDir(const QString &dir);
|
||||
void reindex();
|
||||
void saveAs();
|
||||
void convert(const QString &outfile, const QString &format);
|
||||
void markImage();
|
||||
void unmarkImage();
|
||||
void markAndNext();
|
||||
void unmarkAndNext();
|
||||
void copyMarked();
|
||||
void toggleFullScreen();
|
||||
void moveMarked();
|
||||
void deleteMarked();
|
||||
void liveMode(bool active);
|
||||
void imageStats(bool imageStats);
|
||||
void peakFinder(bool findPeaks);
|
||||
void starFinder(bool findStars);
|
||||
void showMarkFilesDialog();
|
||||
void showSettingsDialog();
|
||||
void exportCSV();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "markedfiles.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QTableView>
|
||||
#include <QSqlTableModel>
|
||||
#include <QPushButton>
|
||||
#include <QHeaderView>
|
||||
#include <QSqlQuery>
|
||||
|
||||
MarkedFiles::MarkedFiles(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Marked files"));
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
m_tableView = new QTableView(this);
|
||||
m_tableView->verticalHeader()->setDefaultSectionSize(1);
|
||||
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
m_model = new QSqlTableModel(this, db);
|
||||
|
||||
m_model->setTable("files");
|
||||
m_model->removeColumn(0);
|
||||
m_model->setHeaderData(0, Qt::Horizontal, tr("Filename"));
|
||||
m_model->select();
|
||||
|
||||
m_tableView->setModel(m_model);
|
||||
m_tableView->resizeColumnsToContents();
|
||||
m_tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
|
||||
QHBoxLayout *hlayout = new QHBoxLayout;
|
||||
QPushButton *clearSelectedButton = new QPushButton(tr("Clear selected"), this);
|
||||
QPushButton *clearAllButton = new QPushButton(tr("Clear all"), this);
|
||||
|
||||
connect(clearSelectedButton, &QPushButton::pressed, this, &MarkedFiles::clearSelected);
|
||||
connect(clearAllButton, &QPushButton::pressed, this, &MarkedFiles::clearAll);
|
||||
|
||||
layout->addWidget(m_tableView);
|
||||
layout->addLayout(hlayout);
|
||||
hlayout->addWidget(clearSelectedButton);
|
||||
hlayout->addWidget(clearAllButton);
|
||||
|
||||
resize(800, 600);
|
||||
}
|
||||
|
||||
void MarkedFiles::clearSelected()
|
||||
{
|
||||
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
QSqlQuery query("DELETE FROM files where file = ?", db);
|
||||
QModelIndexList rows = m_tableView->selectionModel()->selectedRows();
|
||||
QStringList files;
|
||||
for(const QModelIndex &row : rows)
|
||||
{
|
||||
files.append(row.data().toString());
|
||||
}
|
||||
query.bindValue(0, files);
|
||||
query.execBatch();
|
||||
m_model->select();
|
||||
}
|
||||
|
||||
void MarkedFiles::clearAll()
|
||||
{
|
||||
QSqlDatabase db = QSqlDatabase::database();
|
||||
db.exec("DELETE FROM files");
|
||||
m_model->select();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef MARKEDFILES_H
|
||||
#define MARKEDFILES_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableView>
|
||||
#include <QSqlTableModel>
|
||||
|
||||
class MarkedFiles : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
QTableView *m_tableView;
|
||||
QSqlTableModel *m_model;
|
||||
public:
|
||||
MarkedFiles(QWidget *parent = nullptr);
|
||||
protected slots:
|
||||
void clearSelected();
|
||||
void clearAll();
|
||||
};
|
||||
|
||||
#endif // MARKEDFILES_H
|
||||
@@ -1,171 +1,279 @@
|
||||
#include "rawimage.h"
|
||||
#include <QDebug>
|
||||
#include <cstring>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
RawImage::ImgType CV2Type(int cvtype)
|
||||
int THUMB_SIZE = 128;
|
||||
int THUMB_SIZE_BORDER = 138;
|
||||
int THUMB_SIZE_BORDER_Y = 158;
|
||||
double SATURATION = 0.95;
|
||||
bool QUALITY_RESIZE = true;
|
||||
|
||||
#ifdef __SSE2__
|
||||
template<typename T, int ch>
|
||||
void fromPlanarSSE(const void *in, void *out, size_t count);
|
||||
#endif
|
||||
|
||||
size_t RawImage::typeSize(RawImage::DataType type)
|
||||
{
|
||||
switch (cvtype)
|
||||
switch(type)
|
||||
{
|
||||
case CV_8U:
|
||||
return RawImage::UINT8;
|
||||
case CV_16U:
|
||||
return RawImage::UINT16;
|
||||
case CV_32F:
|
||||
return RawImage::FLOAT32;
|
||||
case CV_8UC3:
|
||||
return RawImage::UINT8C3;
|
||||
case CV_8UC4:
|
||||
return RawImage::UINT8C4;
|
||||
case CV_16UC3:
|
||||
return RawImage::UINT16C3;
|
||||
case CV_32FC3:
|
||||
return RawImage::FLOAT32C3;
|
||||
default:
|
||||
return RawImage::UNKNOWN;
|
||||
case RawImage::UINT8:
|
||||
return 1;
|
||||
case RawImage::UINT16:
|
||||
return 2;
|
||||
case RawImage::UINT32:
|
||||
case RawImage::FLOAT32:
|
||||
return 4;
|
||||
case RawImage::FLOAT64:
|
||||
return 8;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int Type2CV(RawImage::ImgType type)
|
||||
void RawImage::allocate(uint32_t w, uint32_t h, uint32_t ch, DataType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
return CV_8U;
|
||||
case RawImage::UINT16:
|
||||
return CV_16U;
|
||||
case RawImage::FLOAT32:
|
||||
return CV_32F;
|
||||
case RawImage::UINT8C3:
|
||||
return CV_8UC3;
|
||||
case RawImage::UINT8C4:
|
||||
return CV_8UC4;
|
||||
case RawImage::UINT16C3:
|
||||
return CV_16UC3;
|
||||
case RawImage::FLOAT32C3:
|
||||
return CV_32FC3;
|
||||
case RawImage::UNKNOWN:
|
||||
return CV_8S;
|
||||
default:
|
||||
return CV_8U;
|
||||
}
|
||||
m_width = w;
|
||||
m_height = h;
|
||||
m_channels = ch;
|
||||
m_ch = ch == 3 ? 4 : ch;
|
||||
m_origType = m_type = type;
|
||||
m_pixels = std::make_unique<PixelType[]>(m_width * m_height * m_ch * typeSize(type));
|
||||
}
|
||||
|
||||
RawImage::RawImage()
|
||||
{
|
||||
m_stats = false;
|
||||
}
|
||||
|
||||
RawImage::RawImage(int w, int h, ImgType type)
|
||||
RawImage::RawImage(uint32_t w, uint32_t h, uint32_t ch, DataType type)
|
||||
{
|
||||
m_img.create(h, w, Type2CV(type));
|
||||
m_stats = false;
|
||||
}
|
||||
|
||||
RawImage::RawImage(cv::Mat &img)
|
||||
{
|
||||
m_img = img;
|
||||
m_stats = false;
|
||||
allocate(w, h, ch, type);
|
||||
}
|
||||
|
||||
RawImage::RawImage(const RawImage &d)
|
||||
{
|
||||
d.m_img.copyTo(m_img);
|
||||
m_mean = d.m_mean;
|
||||
m_stdDev = d.m_stdDev;
|
||||
m_median = d.m_median;
|
||||
m_min = d.m_min;
|
||||
m_max = d.m_max;
|
||||
m_mad = d.m_mad;
|
||||
allocate(d.m_width, d.m_height, d.m_channels, d.m_type);
|
||||
std::memcpy(m_pixels.get(), d.m_pixels.get(), m_width * m_height * m_ch * typeSize(m_type));
|
||||
m_stats = d.m_stats;
|
||||
}
|
||||
|
||||
RawImage::RawImage(RawImage &&d)
|
||||
{
|
||||
m_pixels = std::move(d.m_pixels);
|
||||
m_original = std::move(d.m_original);
|
||||
m_width = d.m_width;
|
||||
m_height = d.m_height;
|
||||
m_channels = d.m_channels;
|
||||
m_ch = d.m_ch;
|
||||
m_type = d.m_type;
|
||||
m_origType = d.m_origType;
|
||||
m_stats = d.m_stats;
|
||||
m_thumbAspect = d.m_thumbAspect;
|
||||
}
|
||||
|
||||
RawImage::RawImage(const QImage &img)
|
||||
{
|
||||
if(img.format() == QImage::Format_RGB32)
|
||||
qDebug() << img;
|
||||
if(img.format() == QImage::Format_RGBX8888)
|
||||
{
|
||||
m_img.create(img.height(), img.width(), CV_8UC4);
|
||||
allocate(img.width(), img.height(), 3, UINT8);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*4);
|
||||
std::memcpy(data(i), img.scanLine(i), img.width()*4);
|
||||
}
|
||||
else if(img.format() == QImage::Format_RGBA8888)
|
||||
{
|
||||
allocate(img.width(), img.height(), 4, UINT8);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(data(i), img.scanLine(i), img.width()*4);
|
||||
}
|
||||
else if(img.format() == QImage::Format_RGBX64)
|
||||
{
|
||||
allocate(img.width(), img.height(), 3, UINT16);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(data(i), img.scanLine(i), img.width()*8);
|
||||
}
|
||||
else if(img.format() == QImage::Format_RGBA64)
|
||||
{
|
||||
allocate(img.width(), img.height(), 4, UINT16);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(data(i), img.scanLine(i), img.width()*8);
|
||||
}
|
||||
else if(img.format() == QImage::Format_Grayscale8)
|
||||
{
|
||||
allocate(img.width(), img.height(), 1, UINT8);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(data(i), img.scanLine(i), img.width());
|
||||
}
|
||||
else if(img.format() == QImage::Format_Grayscale16)
|
||||
{
|
||||
allocate(img.width(), img.height(), 1, UINT16);
|
||||
for(int i=0; i<img.height(); i++)
|
||||
std::memcpy(data(i), img.scanLine(i), img.width()*2);
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage tmp = img.convertToFormat(QImage::Format_RGB888);
|
||||
m_img.create(img.height(), img.width(), CV_8UC3);
|
||||
|
||||
QImage tmp = img.convertToFormat(QImage::Format_RGBA8888);
|
||||
allocate(img.width(), img.height(), 4, UINT8);
|
||||
for(int i=0; i<tmp.height(); i++)
|
||||
std::memcpy(m_img.ptr(i), tmp.scanLine(i), tmp.width()*3);
|
||||
std::memcpy(data(i), tmp.scanLine(i), tmp.width()*4);
|
||||
}
|
||||
m_stats = false;
|
||||
m_stats.m_stats = false;
|
||||
}
|
||||
|
||||
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad)
|
||||
const RawImage::Stats& RawImage::imageStats() const
|
||||
{
|
||||
if(!m_stats)calcStats();
|
||||
if(mean)*mean = m_mean;
|
||||
if(stdDev)*stdDev = m_stdDev;
|
||||
if(median)*median = m_median;
|
||||
if(min)*min = m_min;
|
||||
if(max)*max = m_max;
|
||||
if(mad)*mad = m_mad;
|
||||
return m_stats;
|
||||
}
|
||||
|
||||
return true;
|
||||
template<typename T, typename U, int ch>
|
||||
void calcStats(const T *data, size_t n, size_t w, RawImage::Stats &stats)
|
||||
{
|
||||
U sum[4] = {0};
|
||||
U sumSq[4] = {0};
|
||||
T min[4] = {std::numeric_limits<T>::max(), std::numeric_limits<T>::max(), std::numeric_limits<T>::max(), std::numeric_limits<T>::max()};
|
||||
T max[4] = {std::numeric_limits<T>::min(), std::numeric_limits<T>::min(), std::numeric_limits<T>::min(), std::numeric_limits<T>::min()};
|
||||
uint32_t histSize = 65536;
|
||||
if constexpr(std::is_same<T, uint8_t>::value)histSize = 256;
|
||||
std::vector<uint32_t> histogram[4];
|
||||
histogram[0].resize(histSize); histogram[1].resize(histSize); histogram[2].resize(histSize); histogram[3].resize(histSize);
|
||||
|
||||
T sat = SATURATION * std::numeric_limits<T>::max();
|
||||
if constexpr(!std::numeric_limits<T>::is_integer)sat = SATURATION;
|
||||
uint32_t saturated[4] = {0};
|
||||
|
||||
auto statsFunc = [&](T d, int x)
|
||||
{
|
||||
sum[x] += d;
|
||||
sumSq[x] += (U)d * d;
|
||||
min[x] = std::min(min[x], d);
|
||||
max[x] = std::max(max[x], d);
|
||||
uint16_t idx;
|
||||
if constexpr(std::is_same<T, uint32_t>::value)idx = d >> 16;
|
||||
if constexpr(std::is_same<T, uint8_t>::value || std::is_same<T, uint16_t>::value)idx = d;
|
||||
if constexpr(!std::numeric_limits<T>::is_integer)idx = std::clamp((T)d * histSize, (T)0.0, (T)65535.0);
|
||||
histogram[x][idx]++;
|
||||
if(d > sat)saturated[x]++;
|
||||
};
|
||||
|
||||
auto findMedian = [histSize](std::vector<uint32_t> &histogram, size_t n) -> size_t
|
||||
{
|
||||
size_t histSum = 0;
|
||||
for(size_t o=0; o < histSize; o++)
|
||||
{
|
||||
histSum += histogram[o];
|
||||
if(histSum >= n/2)
|
||||
return o;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
size_t na[4] = {n, n, n, n};
|
||||
if constexpr(ch == 1)
|
||||
{
|
||||
na[1] /= 4;
|
||||
na[2] /= 2;
|
||||
na[3] /= 4;
|
||||
}
|
||||
for(size_t i = 0; i < n; i++)
|
||||
{
|
||||
statsFunc(data[i*ch], 0);
|
||||
if constexpr(ch >= 3)
|
||||
{
|
||||
statsFunc(data[i*ch + 1], 1);
|
||||
statsFunc(data[i*ch + 2], 2);
|
||||
}
|
||||
}
|
||||
if constexpr(ch == 1)
|
||||
{
|
||||
size_t h = (n / w) & (SIZE_MAX-1);
|
||||
w &= (SIZE_MAX-1);
|
||||
for(size_t y=0; y<h; y+=2)
|
||||
{
|
||||
for(size_t x=0; x<w; x+=2)
|
||||
{
|
||||
statsFunc(data[y*w+x], 1);
|
||||
statsFunc(data[y*w+x+1], 2);
|
||||
statsFunc(data[(y+1)*w+x], 2);
|
||||
statsFunc(data[(y+1)*w+x+1], 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = 0; i < 4; i++)
|
||||
{
|
||||
stats.m_min[i] = min[i];
|
||||
stats.m_max[i] = max[i];
|
||||
stats.m_mean[i] = (double)sum[i] / na[i];
|
||||
stats.m_saturated[i] = saturated[i];
|
||||
double sum2 = (double)sum[i] * sum[i];
|
||||
stats.m_stdDev[i] = std::sqrt((sumSq[i] - sum2 / na[i]) / (na[i] - 1));
|
||||
|
||||
uint32_t median = findMedian(histogram[i], na[i]);
|
||||
stats.m_median[i] = median;
|
||||
std::vector<uint32_t> madHist(histSize, 0);
|
||||
madHist[0] = histogram[i][median];
|
||||
for(size_t o = 1; o < histSize; o++)
|
||||
{
|
||||
if(median + o < histSize)madHist[o] += histogram[i][median + o];
|
||||
if(o <= median)madHist[o] += histogram[i][median - o];
|
||||
}
|
||||
stats.m_mad[i] = findMedian(madHist, na[i]);
|
||||
if constexpr(!std::numeric_limits<T>::is_integer)
|
||||
{
|
||||
stats.m_median[i] /= 65535.0;
|
||||
stats.m_mad[i] /= 65535.0;
|
||||
}
|
||||
}
|
||||
|
||||
stats.m_histogram.resize(histSize, 0);
|
||||
for(size_t i = 0; i < histSize; i++)
|
||||
for(size_t o = 0; o < ch; o++)
|
||||
stats.m_histogram[i] += histogram[o][i];
|
||||
}
|
||||
|
||||
void RawImage::calcStats()
|
||||
{
|
||||
if(m_stats)return;
|
||||
m_stats = true;
|
||||
if(m_stats.m_stats)return;
|
||||
m_stats.m_stats = true;
|
||||
|
||||
cv::Scalar meanS, stdDevS;
|
||||
|
||||
cv::meanStdDev(m_img, meanS, stdDevS);
|
||||
cv::minMaxIdx(m_img, &m_min, &m_max);
|
||||
|
||||
cv::Mat img;
|
||||
if(m_img.channels() == 1)img = m_img;
|
||||
else if (m_img.channels() == 3)cv::cvtColor(m_img, img, cv::COLOR_BGR2GRAY);
|
||||
else if (m_img.channels() == 4)cv::cvtColor(m_img, img, cv::COLOR_BGRA2GRAY);
|
||||
|
||||
int histSize = 256;
|
||||
if(img.type() == CV_16U || img.type() == CV_32F)histSize = 65536;
|
||||
float range[] = {0, (float)histSize};
|
||||
if(img.type() == CV_32F)range[1] = 1.0f;
|
||||
const float *ranges[] = {range};
|
||||
cv::Mat hist;
|
||||
cv::calcHist(&img, 1, nullptr, cv::Mat(), hist, 1, &histSize, ranges);
|
||||
|
||||
m_mean = meanS[0];
|
||||
m_stdDev = stdDevS[0];
|
||||
size_t halfImageSize = size()/2;
|
||||
size_t medianSum = 0;
|
||||
for(int i=0; i < histSize; i++)
|
||||
switch(m_origType)
|
||||
{
|
||||
medianSum += hist.at<float>(0, i);
|
||||
if(medianSum >= halfImageSize)
|
||||
{
|
||||
m_median = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cv::Mat absDev;
|
||||
img.convertTo(absDev, CV_32F, 1, -m_median);
|
||||
absDev = cv::abs(absDev);
|
||||
cv::Mat madHist;
|
||||
medianSum = 0;
|
||||
cv::calcHist(&absDev, 1, nullptr, cv::Mat(), madHist, 1, &histSize, ranges);
|
||||
for(int i=0; i < histSize; i++)
|
||||
{
|
||||
medianSum += madHist.at<float>(0, i);
|
||||
if(medianSum >= halfImageSize)
|
||||
{
|
||||
m_mad = i;
|
||||
break;
|
||||
}
|
||||
case UINT8:
|
||||
if(channels()==1)
|
||||
::calcStats<uint8_t, uint64_t, 1>(static_cast<const uint8_t*>(origData()), size(), width(), m_stats);
|
||||
else
|
||||
::calcStats<uint8_t, uint64_t, 4>(static_cast<const uint8_t*>(origData()), size(), width(), m_stats);
|
||||
break;
|
||||
case UINT16:
|
||||
if(channels()==1)
|
||||
::calcStats<uint16_t, uint64_t, 1>(static_cast<const uint16_t*>(origData()), size(), width(), m_stats);
|
||||
else
|
||||
::calcStats<uint16_t, uint64_t, 4>(static_cast<const uint16_t*>(origData()), size(), width(), m_stats);
|
||||
break;
|
||||
case UINT32:
|
||||
if(channels()==1)
|
||||
::calcStats<uint32_t, double, 1>(static_cast<const uint32_t*>(origData()), size(), width(), m_stats);
|
||||
else
|
||||
::calcStats<uint32_t, double, 4>(static_cast<const uint32_t*>(origData()), size(), width(), m_stats);
|
||||
break;
|
||||
case FLOAT32:
|
||||
if(channels()==1)
|
||||
::calcStats<float, double, 1>(static_cast<const float*>(origData()), size(), width(), m_stats);
|
||||
else
|
||||
::calcStats<float, double, 4>(static_cast<const float*>(origData()), size(), width(), m_stats);
|
||||
break;
|
||||
case FLOAT64:
|
||||
if(channels()==1)
|
||||
::calcStats<double, double, 1>(static_cast<const double*>(origData()), size(), width(), m_stats);
|
||||
else
|
||||
::calcStats<double, double, 4>(static_cast<const double*>(origData()), size(), width(), m_stats);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RawImage::rect(int &x, int &y, int w, int h, std::vector<double> &r) const
|
||||
{
|
||||
r.resize(w*h);
|
||||
/*r.resize(w*h);
|
||||
x -= w/2;
|
||||
y -= h/2;
|
||||
if(x<0)x = 0;
|
||||
@@ -175,19 +283,22 @@ void RawImage::rect(int &x, int &y, int w, int h, std::vector<double> &r) const
|
||||
cv::Mat roiImg(m_img, cv::Rect(x, y, w, h));
|
||||
cv::Mat doubleMat;
|
||||
roiImg.convertTo(doubleMat, CV_64F);
|
||||
r = std::vector<double>(doubleMat.begin<double>(), doubleMat.end<double>());
|
||||
r = std::vector<double>(doubleMat.begin<double>(), doubleMat.end<double>());*/
|
||||
}
|
||||
|
||||
int RawImage::findPeaks(double background, double distance, std::vector<Peak> &peaks) const
|
||||
{
|
||||
std::vector<std::vector<cv::Point>> contours;
|
||||
/*std::vector<std::vector<cv::Point>> contours;
|
||||
|
||||
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(distance, distance));
|
||||
|
||||
cv::Mat mask, dilate, locMax, result;
|
||||
cv::dilate(m_img, dilate, kernel);
|
||||
cv::compare(m_img, dilate, locMax, cv::CMP_GE);
|
||||
cv::compare(m_img, cv::Scalar(background), mask, cv::CMP_GT);
|
||||
cv::Mat img, mask, dilate, locMax, result;
|
||||
if(m_img.channels() == 1)img = m_img;
|
||||
else cv::cvtColor(m_img, img, cv::COLOR_RGB2GRAY);
|
||||
|
||||
cv::dilate(img, dilate, kernel);
|
||||
cv::compare(img, dilate, locMax, cv::CMP_GE);
|
||||
cv::compare(img, cv::Scalar(background), mask, cv::CMP_GT);
|
||||
cv::bitwise_and(locMax, mask, result);
|
||||
|
||||
cv::findContours(result, contours, cv::noArray(), cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
|
||||
@@ -197,29 +308,22 @@ int RawImage::findPeaks(double background, double distance, std::vector<Peak> &p
|
||||
peaks.push_back(Peak(1, contour[0].x, contour[0].y));
|
||||
}
|
||||
|
||||
return peaks.size();
|
||||
}
|
||||
|
||||
RawImage* RawImage::medianFilter() const
|
||||
{
|
||||
RawImage *ret = new RawImage();
|
||||
cv::medianBlur(m_img, ret->m_img, 3);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void RawImage::quarter()
|
||||
{
|
||||
|
||||
return peaks.size();*/
|
||||
}
|
||||
|
||||
uint32_t RawImage::width() const
|
||||
{
|
||||
return m_img.cols;
|
||||
return m_width;
|
||||
}
|
||||
|
||||
uint32_t RawImage::height() const
|
||||
{
|
||||
return m_img.rows;
|
||||
return m_height;
|
||||
}
|
||||
|
||||
uint32_t RawImage::channels() const
|
||||
{
|
||||
return m_channels;
|
||||
}
|
||||
|
||||
uint32_t RawImage::size() const
|
||||
@@ -227,33 +331,462 @@ uint32_t RawImage::size() const
|
||||
return width()*height();
|
||||
}
|
||||
|
||||
RawImage::ImgType RawImage::type() const
|
||||
RawImage::DataType RawImage::type() const
|
||||
{
|
||||
return CV2Type(m_img.type());
|
||||
return m_type;
|
||||
}
|
||||
|
||||
uint32_t RawImage::norm() const
|
||||
{
|
||||
switch(m_img.type())
|
||||
switch(m_type)
|
||||
{
|
||||
case CV_8U:
|
||||
case CV_8UC3:
|
||||
case CV_8UC4:
|
||||
case UINT8:
|
||||
return UINT8_MAX;
|
||||
case CV_16U:
|
||||
case CV_16UC3:
|
||||
case UINT16:
|
||||
return UINT16_MAX;
|
||||
case UINT32:
|
||||
return UINT32_MAX;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t RawImage::widthBytes() const
|
||||
{
|
||||
return m_ch * m_width;
|
||||
}
|
||||
|
||||
void* RawImage::data()
|
||||
{
|
||||
return m_img.ptr();
|
||||
return m_pixels.get();
|
||||
}
|
||||
|
||||
const void *RawImage::data() const
|
||||
{
|
||||
return m_img.ptr();
|
||||
return m_pixels.get();
|
||||
}
|
||||
|
||||
void *RawImage::data(uint32_t row, uint32_t col)
|
||||
{
|
||||
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
|
||||
}
|
||||
|
||||
const void *RawImage::data(uint32_t row, uint32_t col) const
|
||||
{
|
||||
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
|
||||
}
|
||||
|
||||
const void *RawImage::origData() const
|
||||
{
|
||||
if(m_original)
|
||||
return m_original.get();
|
||||
else
|
||||
return m_pixels.get();
|
||||
}
|
||||
|
||||
const void *RawImage::origData(uint32_t row, uint32_t col) const
|
||||
{
|
||||
if(m_original)
|
||||
return m_original.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_origType);
|
||||
else
|
||||
return m_pixels.get() + (m_width * row * m_ch + col * m_ch) * typeSize(m_type);
|
||||
}
|
||||
|
||||
bool RawImage::planar() const
|
||||
{
|
||||
return m_planar;
|
||||
}
|
||||
|
||||
void RawImage::convertToThumbnail()
|
||||
{
|
||||
if(!valid())
|
||||
return;
|
||||
|
||||
if(m_thumbAspect == 0.0f)
|
||||
m_thumbAspect = (float)width() / height();
|
||||
std::unique_ptr<PixelType[]> outptr = std::make_unique<PixelType[]>(THUMB_SIZE * THUMB_SIZE * 4 * sizeof(uint16_t));
|
||||
uint16_t *out = reinterpret_cast<uint16_t*>(outptr.get());
|
||||
|
||||
auto loop = [&](uint16_t *out, auto *in, auto scale)
|
||||
{
|
||||
for(int i=0; i<THUMB_SIZE; i++)
|
||||
{
|
||||
for(int o=0; o<THUMB_SIZE; o++)
|
||||
{
|
||||
int idx = (i*THUMB_SIZE + o)*4;
|
||||
int idx2 = ((i * m_height / THUMB_SIZE * m_width) + (o * m_width / THUMB_SIZE)) * m_ch;
|
||||
|
||||
if(m_channels == 1)
|
||||
{
|
||||
out[idx] = out[idx + 1] = out[idx + 2] = in[idx2] * scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
out[idx] = in[idx2] * scale;;
|
||||
out[idx + 1] = in[idx2 + 1] * scale;;
|
||||
out[idx + 2] = in[idx2 + 2] * scale;;
|
||||
}
|
||||
out[idx + 3] = UINT16_MAX;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch(m_type)
|
||||
{
|
||||
case UINT8:
|
||||
loop(out, reinterpret_cast<uint8_t*>(m_pixels.get()), 256);
|
||||
break;
|
||||
case UINT16:
|
||||
loop(out, reinterpret_cast<uint16_t*>(m_pixels.get()), 1);
|
||||
break;
|
||||
case UINT32:
|
||||
loop(out, reinterpret_cast<uint32_t*>(m_pixels.get()), UINT16_MAX/(float)UINT32_MAX);
|
||||
break;
|
||||
case FLOAT32:
|
||||
loop(out, reinterpret_cast<float*>(m_pixels.get()), 65535.0);
|
||||
break;
|
||||
default:
|
||||
qWarning() << "FLOAT64 should not happend";
|
||||
return;
|
||||
}
|
||||
|
||||
m_pixels = std::move(outptr);
|
||||
m_width = THUMB_SIZE;
|
||||
m_height = THUMB_SIZE;
|
||||
m_ch = 4;
|
||||
m_channels = 3;
|
||||
m_type = UINT16;
|
||||
}
|
||||
|
||||
void RawImage::convertToGLFormat()
|
||||
{
|
||||
size_t s = size() * m_ch;
|
||||
if(m_type == UINT32)
|
||||
{
|
||||
m_original = std::move(m_pixels);
|
||||
allocate(m_width, m_height, m_channels, FLOAT32);
|
||||
m_origType = UINT32;
|
||||
float *dst = reinterpret_cast<float*>(m_pixels.get());
|
||||
uint32_t *src = reinterpret_cast<uint32_t*>(m_original.get());
|
||||
|
||||
for(size_t i = 0; i < s; i++)
|
||||
dst[i] = src[i] / (float)UINT32_MAX;
|
||||
}
|
||||
else if(m_type == FLOAT64)
|
||||
{
|
||||
m_original = std::move(m_pixels);
|
||||
allocate(m_width, m_height, m_channels, FLOAT32);
|
||||
m_origType = FLOAT64;
|
||||
float *dst = reinterpret_cast<float*>(m_pixels.get());
|
||||
double *src = reinterpret_cast<double*>(m_original.get());
|
||||
|
||||
for(size_t i = 0; i < s; i++)
|
||||
dst[i] = src[i];
|
||||
}
|
||||
}
|
||||
|
||||
float RawImage::thumbAspect() const
|
||||
{
|
||||
return m_thumbAspect;
|
||||
}
|
||||
|
||||
bool RawImage::pixel(int x, int y, double &r, double &g, double &b) const
|
||||
{
|
||||
if(x < 0 || y < 0 || x >= (int)width() || y >= (int)height())return false;
|
||||
|
||||
switch(m_origType)
|
||||
{
|
||||
case UINT8:
|
||||
{
|
||||
const uint8_t *v = static_cast<const uint8_t*>(origData(y, x));
|
||||
if(m_channels == 1)
|
||||
{
|
||||
r = g = b = *v;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = v[0];
|
||||
g = v[1];
|
||||
b = v[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UINT16:
|
||||
{
|
||||
const uint16_t *v = static_cast<const uint16_t*>(origData(y, x));
|
||||
if(m_channels == 1)
|
||||
{
|
||||
r = g = b = *v;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = v[0];
|
||||
g = v[1];
|
||||
b = v[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UINT32:
|
||||
{
|
||||
const uint32_t *v = static_cast<const uint32_t*>(origData(y, x));
|
||||
if(m_channels == 1)
|
||||
{
|
||||
r = g = b = *v;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = v[0];
|
||||
g = v[1];
|
||||
b = v[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FLOAT32:
|
||||
{
|
||||
const float *v = static_cast<const float*>(origData(y, x));
|
||||
if(m_channels == 1)
|
||||
{
|
||||
r = g = b = *v;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = v[0];
|
||||
g = v[1];
|
||||
b = v[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FLOAT64:
|
||||
{
|
||||
const double *v = static_cast<const double*>(origData(y, x));
|
||||
if(m_channels == 1)
|
||||
{
|
||||
r = g = b = *v;
|
||||
}
|
||||
else
|
||||
{
|
||||
r = v[0];
|
||||
g = v[1];
|
||||
b = v[2];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void boxResample(uint32_t w, uint32_t h, uint32_t ch, uint32_t oldw, uint32_t oldh, const uint8_t *in_, uint8_t *out_)
|
||||
{
|
||||
if(oldw == 0 || oldh == 0)return;
|
||||
|
||||
const T *in = reinterpret_cast<const T*>(in_);
|
||||
T *out = reinterpret_cast<T*>(out_);
|
||||
float max = 255.0f;
|
||||
if constexpr(std::is_same<T, uint16_t>::value)
|
||||
max = UINT16_MAX;
|
||||
|
||||
float sx = (float)w / oldw;
|
||||
float sy = (float)h / oldh;
|
||||
for(uint32_t y = 0; y < h; y++)//iterate over destination Y
|
||||
{
|
||||
for(uint32_t x = 0; x < w; x++)//iterate over destination X
|
||||
{
|
||||
float p[4] = {0.0f};
|
||||
uint32_t xx = x * oldw / w;//calculate source rect
|
||||
uint32_t yy = y * oldh / h;
|
||||
uint32_t xe = std::min((x + 1) * oldw / w, oldw - 1);
|
||||
uint32_t ye = std::min((y + 1) * oldh / h, oldh - 1);
|
||||
for(uint32_t o = yy; o <= ye; o++)//iterate over source Y
|
||||
{
|
||||
float cy = o * sy - y;
|
||||
if(cy < 0.0f)cy = sy + cy;
|
||||
else if(sy + cy > 1.0f)cy = 1.0f - cy;
|
||||
else cy = sy;
|
||||
if(yy==ye)cy = 1.0f;
|
||||
for(uint32_t i = xx; i <= xe; i++)//iterate over source X
|
||||
{
|
||||
float cx = i * sx - x;
|
||||
if(cx < 0.0f)cx = sx + cx;
|
||||
else if(sx + cx > 1.0f)cx = 1.0f - cx;
|
||||
else cx = sx;
|
||||
if(xx==xe)cx = 1.0f;
|
||||
for(uint32_t z = 0; z < ch; z++)
|
||||
p[z] += in[(o * oldw + i) * ch + z] * cy * cx;
|
||||
}
|
||||
}
|
||||
for(uint32_t z = 0; z < ch; z++)
|
||||
if constexpr(std::is_floating_point<T>::value)
|
||||
out[(y * w + x) * ch + z] = p[z];
|
||||
else
|
||||
out[(y * w + x) * ch + z] = std::clamp(std::round(p[z]), 0.0f, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RawImage::resize(uint32_t w, uint32_t h)
|
||||
{
|
||||
if(!valid())return;
|
||||
|
||||
std::unique_ptr<PixelType[]> old_pixels = std::move(m_pixels);
|
||||
uint32_t oldw = m_width;
|
||||
uint32_t oldh = m_height;
|
||||
m_thumbAspect = (float)m_width / m_height;
|
||||
|
||||
allocate(w, h, m_channels, m_type);
|
||||
|
||||
switch(m_type)
|
||||
{
|
||||
case RawImage::UINT8:
|
||||
boxResample<uint8_t>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
|
||||
break;
|
||||
case RawImage::UINT16:
|
||||
boxResample<uint16_t>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
|
||||
break;
|
||||
case RawImage::FLOAT32:
|
||||
boxResample<float>(w, h, m_ch, oldw, oldh, old_pixels.get(), m_pixels.get());
|
||||
break;
|
||||
default:
|
||||
qWarning() << "Resizing format not supported";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<float, float> RawImage::unitScale() const
|
||||
{
|
||||
float min = *std::min_element(m_stats.m_min, m_stats.m_min + 4);
|
||||
float max = *std::max_element(m_stats.m_max, m_stats.m_max + 4);
|
||||
|
||||
if(m_origType == UINT32)
|
||||
{
|
||||
min /= (float)UINT32_MAX;
|
||||
max /= (float)UINT32_MAX;
|
||||
}
|
||||
|
||||
if(min < 0.0f || max > 1.0f)
|
||||
return {1.0f / (max - min), min / (max - min)};
|
||||
else
|
||||
return {1.0f, 0.0f};
|
||||
}
|
||||
|
||||
void RawImage::flip()
|
||||
{
|
||||
std::unique_ptr<PixelType[]> tmp = std::move(m_pixels);
|
||||
allocate(m_width, m_height, m_channels, m_type);
|
||||
uint32_t rowSize = m_width * m_ch * typeSize(m_type);
|
||||
for(uint32_t i=0; i<m_height; i++)
|
||||
std::memcpy(m_pixels.get() + rowSize * (m_height - i - 1), tmp.get() + rowSize * i, rowSize);
|
||||
}
|
||||
|
||||
std::shared_ptr<RawImage> RawImage::fromPlanar(const RawImage &img)
|
||||
{
|
||||
return RawImage::fromPlanar(img.data(), img.width(), img.height(), img.channels(), img.type());
|
||||
}
|
||||
|
||||
std::shared_ptr<RawImage> RawImage::fromPlanar(const void *pixels, uint32_t w, uint32_t h, uint32_t ch, RawImage::DataType type)
|
||||
{
|
||||
std::shared_ptr<RawImage> image = std::make_shared<RawImage>(w, h, ch, type);
|
||||
size_t size = w * h;
|
||||
size_t ch2 = ch == 1 ? 1 : 4;
|
||||
auto convert = [&](auto *in, auto *out, auto alpha)
|
||||
{
|
||||
for(size_t i=0; i<size; i++)
|
||||
for(size_t o=0; o<ch; o++)
|
||||
out[i*ch2 + o] = in[o*size + i];
|
||||
|
||||
if(ch != ch2)
|
||||
for(size_t i=0; i<size; i++)
|
||||
out[i*ch2 + 3] = alpha;
|
||||
};
|
||||
|
||||
switch(type)
|
||||
{
|
||||
case UINT8:
|
||||
#ifdef __SSE2__
|
||||
if(ch==3)
|
||||
fromPlanarSSE<uint8_t, 3>(pixels, image->data(), size);
|
||||
else
|
||||
fromPlanarSSE<uint8_t, 4>(pixels, image->data(), size);
|
||||
#else
|
||||
convert(static_cast<const uint8_t*>(pixels), static_cast<uint8_t*>(image->data()), UINT8_MAX);
|
||||
#endif
|
||||
break;
|
||||
case UINT16:
|
||||
#ifdef __SSE2__
|
||||
if(ch==3)
|
||||
fromPlanarSSE<uint16_t, 3>(pixels, static_cast<uint16_t*>(image->data()), size);
|
||||
else
|
||||
fromPlanarSSE<uint16_t, 4>(pixels, static_cast<uint16_t*>(image->data()), size);
|
||||
#else
|
||||
convert(static_cast<const uint16_t*>(pixels), static_cast<uint16_t*>(image->data()), UINT16_MAX);
|
||||
#endif
|
||||
break;
|
||||
case UINT32:
|
||||
#ifdef __SSE2__
|
||||
if(ch==3)
|
||||
fromPlanarSSE<uint32_t, 3>(pixels, image->data(), size);
|
||||
else
|
||||
fromPlanarSSE<uint32_t, 4>(pixels, image->data(), size);
|
||||
#else
|
||||
convert(static_cast<const uint32_t*>(pixels), static_cast<uint32_t*>(image->data()), UINT32_MAX);
|
||||
#endif
|
||||
break;
|
||||
case FLOAT32:
|
||||
#ifdef __SSE2__
|
||||
if(ch==3)
|
||||
fromPlanarSSE<float, 3>(pixels, image->data(), size);
|
||||
else
|
||||
fromPlanarSSE<float, 4>(pixels, image->data(), size);
|
||||
#else
|
||||
convert(static_cast<const float*>(pixels), static_cast<float*>(image->data()), 1);
|
||||
#endif
|
||||
break;
|
||||
case FLOAT64:
|
||||
convert(static_cast<const double*>(pixels), static_cast<double*>(image->data()), 1);
|
||||
break;
|
||||
}
|
||||
image->m_planar = false;
|
||||
return image;
|
||||
}
|
||||
|
||||
std::vector<RawImage> RawImage::split() const
|
||||
{
|
||||
std::vector<RawImage> planes;
|
||||
planes.resize(m_channels);
|
||||
for(size_t i=0; i<m_channels; i++)
|
||||
planes[i].allocate(m_width, m_height, 1, m_type);
|
||||
|
||||
size_t s = size();
|
||||
auto extract = [&](auto *in, auto *out, size_t off)
|
||||
{
|
||||
for(size_t i=0; i < s; i++)
|
||||
out[i] = in[i*m_ch + off];
|
||||
};
|
||||
|
||||
for(uint32_t i=0; i<m_channels; i++)
|
||||
{
|
||||
switch(m_type)
|
||||
{
|
||||
case UINT8:
|
||||
extract(static_cast<const uint8_t*>(data()), static_cast<uint8_t*>(planes[i].data()), i);
|
||||
break;
|
||||
case UINT16:
|
||||
extract(static_cast<const uint16_t*>(data()), static_cast<uint16_t*>(planes[i].data()), i);
|
||||
break;
|
||||
case UINT32:
|
||||
case FLOAT32:
|
||||
extract(static_cast<const uint32_t*>(data()), static_cast<uint32_t*>(planes[i].data()), i);
|
||||
break;
|
||||
case FLOAT64:
|
||||
extract(static_cast<const double*>(data()), static_cast<double*>(planes[i].data()), i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return planes;
|
||||
}
|
||||
|
||||
bool RawImage::valid() const
|
||||
{
|
||||
return m_width > 0 && m_height > 0;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include <memory.h>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <QImage>
|
||||
|
||||
extern int THUMB_SIZE;
|
||||
extern int THUMB_SIZE_BORDER;
|
||||
extern int THUMB_SIZE_BORDER_Y;
|
||||
extern bool QUALITY_RESIZE;
|
||||
|
||||
class Peak
|
||||
{
|
||||
uint32_t m_v;
|
||||
@@ -33,45 +38,82 @@ public:
|
||||
|
||||
class RawImage
|
||||
{
|
||||
protected:
|
||||
cv::Mat m_img;
|
||||
bool m_stats;
|
||||
double m_mean;
|
||||
double m_stdDev;
|
||||
double m_median;
|
||||
double m_min;
|
||||
double m_max;
|
||||
double m_mad;
|
||||
using PixelType = uint8_t;
|
||||
public:
|
||||
enum ImgType
|
||||
enum DataType
|
||||
{
|
||||
UINT8,
|
||||
UINT16,
|
||||
UINT32,
|
||||
FLOAT32,
|
||||
UINT8C3,
|
||||
UINT8C4,
|
||||
UINT16C3,
|
||||
FLOAT32C3,
|
||||
UNKNOWN,
|
||||
FLOAT64,
|
||||
};
|
||||
struct Stats
|
||||
{
|
||||
bool m_stats = false;
|
||||
double m_mean[4] = {0.0};
|
||||
double m_stdDev[4] = {0.0};
|
||||
double m_median[4] = {0.0};
|
||||
double m_min[4] = {0.0};
|
||||
double m_max[4] = {0.0};
|
||||
double m_mad[4] = {0.0};
|
||||
uint32_t m_saturated[4] = {0};
|
||||
std::vector<uint32_t> m_histogram;
|
||||
};
|
||||
protected:
|
||||
std::unique_ptr<PixelType[]> m_pixels;
|
||||
std::unique_ptr<PixelType[]> m_original;
|
||||
uint32_t m_width = 0;
|
||||
uint32_t m_height = 0;
|
||||
uint32_t m_channels = 0;
|
||||
uint32_t m_ch = 0;
|
||||
DataType m_type = UINT8;
|
||||
DataType m_origType = UINT8;
|
||||
float m_thumbAspect = 0.0;
|
||||
Stats m_stats;
|
||||
bool m_planar = false;
|
||||
void allocate(uint32_t w, uint32_t h, uint32_t ch, DataType type);
|
||||
public:
|
||||
RawImage();
|
||||
RawImage(int w, int h, ImgType type);
|
||||
RawImage(cv::Mat &img);
|
||||
RawImage(uint32_t w, uint32_t h, uint32_t ch, DataType type);
|
||||
RawImage(const RawImage &d);
|
||||
RawImage(RawImage &&d);
|
||||
RawImage(const QImage &img);
|
||||
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad);
|
||||
const RawImage::Stats& imageStats() const;
|
||||
void calcStats();
|
||||
void rect(int &x, int &y, int w, int h, std::vector<double> &r) const;
|
||||
int findPeaks(double background, double distance, std::vector<Peak> &peaks) const;
|
||||
RawImage* medianFilter() const;
|
||||
void quarter();
|
||||
uint32_t width() const;
|
||||
uint32_t height() const;
|
||||
uint32_t channels() const;
|
||||
uint32_t size() const;
|
||||
ImgType type() const;
|
||||
DataType type() const;
|
||||
uint32_t norm() const;
|
||||
uint32_t widthBytes() const;
|
||||
void* data();
|
||||
const void* data() const;
|
||||
void* data(uint32_t row, uint32_t col = 0);
|
||||
const void* data(uint32_t row, uint32_t col = 0) const;
|
||||
const void *origData() const;
|
||||
const void *origData(uint32_t row, uint32_t col = 0) const;
|
||||
bool planar() const;
|
||||
void setPlanar();
|
||||
void convertToThumbnail();
|
||||
void convertToGLFormat();
|
||||
float thumbAspect() const;
|
||||
bool pixel(int x, int y, double &r, double &g, double &b) const;
|
||||
void resize(uint32_t w, uint32_t h);
|
||||
std::pair<float, float> unitScale() const;
|
||||
void flip();
|
||||
|
||||
static std::shared_ptr<RawImage> fromPlanar(const RawImage &img);
|
||||
static std::shared_ptr<RawImage> fromPlanar(const void *pixels, uint32_t w, uint32_t h, uint32_t ch, DataType type);
|
||||
static size_t typeSize(DataType type);
|
||||
std::vector<RawImage> split() const;
|
||||
bool valid() const;
|
||||
};
|
||||
|
||||
//Q_DECLARE_SMART_POINTER_METATYPE(std::shared_ptr);
|
||||
//Q_DECLARE_METATYPE(std::shared_ptr<RawImage>);
|
||||
|
||||
#endif // RAWIMAGE_H
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "rawimage.h"
|
||||
|
||||
#ifdef __SSE2__
|
||||
#include <x86intrin.h>
|
||||
|
||||
template<typename T, int ch>
|
||||
void fromPlanarSSE(const void *in, void *out, size_t count)
|
||||
{
|
||||
const __m128i *_in[4] = {(const __m128i*) static_cast<const T*>(in),
|
||||
(const __m128i*)(static_cast<const T*>(in) + count),
|
||||
(const __m128i*)(static_cast<const T*>(in) + count*2),
|
||||
(const __m128i*)(static_cast<const T*>(in) + count*3)};
|
||||
__m128i *_out = (__m128i*)out;
|
||||
size_t s2 = count;
|
||||
if constexpr(sizeof(T) == 1)
|
||||
{
|
||||
count /= 16;
|
||||
__m128i a = _mm_set1_epi8(-1);
|
||||
for(size_t i = 0; i < count; i++)
|
||||
{
|
||||
__m128i r = _mm_loadu_si128(_in[0] + i);
|
||||
__m128i g = _mm_loadu_si128(_in[1] + i);
|
||||
__m128i b = _mm_loadu_si128(_in[2] + i);
|
||||
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
|
||||
|
||||
__m128i d1 = _mm_unpacklo_epi8(r, b);
|
||||
__m128i d2 = _mm_unpacklo_epi8(g, a);
|
||||
_mm_storeu_si128(_out + i*4, _mm_unpacklo_epi8(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 1, _mm_unpackhi_epi8(d1, d2));
|
||||
d1 = _mm_unpackhi_epi8(r, b);
|
||||
d2 = _mm_unpackhi_epi8(g, a);
|
||||
_mm_storeu_si128(_out + i*4 + 2, _mm_unpacklo_epi8(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 3, _mm_unpackhi_epi8(d1, d2));
|
||||
}
|
||||
count *= 16;
|
||||
}
|
||||
if constexpr(sizeof(T) == 2)
|
||||
{
|
||||
count /= 8;
|
||||
__m128i a = _mm_set1_epi16(-1);
|
||||
for(size_t i = 0; i < count; i++)
|
||||
{
|
||||
__m128i r = _mm_loadu_si128(_in[0] + i);
|
||||
__m128i g = _mm_loadu_si128(_in[1] + i);
|
||||
__m128i b = _mm_loadu_si128(_in[2] + i);
|
||||
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
|
||||
|
||||
__m128i d1 = _mm_unpacklo_epi16(r, b);
|
||||
__m128i d2 = _mm_unpacklo_epi16(g, a);
|
||||
_mm_storeu_si128(_out + i*4, _mm_unpacklo_epi16(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 1, _mm_unpackhi_epi16(d1, d2));
|
||||
d1 = _mm_unpackhi_epi16(r, b);
|
||||
d2 = _mm_unpackhi_epi16(g, a);
|
||||
_mm_storeu_si128(_out + i*4 + 2, _mm_unpacklo_epi16(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 3, _mm_unpackhi_epi16(d1, d2));
|
||||
}
|
||||
count *= 8;
|
||||
}
|
||||
if constexpr(sizeof(T) == 4)
|
||||
{
|
||||
count /= 4;
|
||||
__m128i a = _mm_set1_epi32(-1);
|
||||
if constexpr(!std::numeric_limits<T>::is_integer)a = _mm_castps_si128(_mm_set1_ps(1.0));
|
||||
for(size_t i = 0; i < count; i++)
|
||||
{
|
||||
__m128i r = _mm_loadu_si128(_in[0] + i);
|
||||
__m128i g = _mm_loadu_si128(_in[1] + i);
|
||||
__m128i b = _mm_loadu_si128(_in[2] + i);
|
||||
if constexpr(ch==4)a = _mm_loadu_si128(_in[3]);
|
||||
|
||||
__m128i d1 = _mm_unpacklo_epi32(r, b);
|
||||
__m128i d2 = _mm_unpacklo_epi32(g, a);
|
||||
_mm_storeu_si128(_out + i*4, _mm_unpacklo_epi32(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 1, _mm_unpackhi_epi32(d1, d2));
|
||||
d1 = _mm_unpackhi_epi32(r, b);
|
||||
d2 = _mm_unpackhi_epi32(g, a);
|
||||
_mm_storeu_si128(_out + i*4 + 2, _mm_unpacklo_epi32(d1, d2));
|
||||
_mm_storeu_si128(_out + i*4 + 3, _mm_unpackhi_epi32(d1, d2));
|
||||
}
|
||||
count *= 4;
|
||||
}
|
||||
for(size_t i = count; i < s2; i++)
|
||||
{
|
||||
switch(sizeof(T))
|
||||
{
|
||||
case 1:
|
||||
for(uint32_t o=0; o<ch; o++)static_cast<uint8_t*>(out)[i + o] = static_cast<const uint8_t*>(in)[i + o + s2];
|
||||
if(ch==3)static_cast<uint8_t*>(out)[i*4 + 3] = 0xff;
|
||||
break;
|
||||
case 2:
|
||||
for(uint32_t o=0; o<ch; o++)static_cast<uint16_t*>(out)[i + o] = static_cast<const uint16_t*>(in)[i + o + s2];
|
||||
if(ch==3)static_cast<uint16_t*>(out)[i*4 + 3] = 0xffff;
|
||||
break;
|
||||
case 4:
|
||||
for(uint32_t o=0; o<ch; o++)static_cast<uint32_t*>(out)[i + o] = static_cast<const uint32_t*>(in)[i + o + s2];
|
||||
if(ch==3)
|
||||
{
|
||||
if(!std::numeric_limits<T>::is_integer)static_cast<float*>(out)[i*4 + 3] = 1.0;
|
||||
else static_cast<uint32_t*>(out)[i*4 + 3] = 0xffffffff;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template void fromPlanarSSE<uint8_t, 3>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<uint8_t, 4>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<uint16_t, 3>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<uint16_t, 4>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<uint32_t, 3>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<uint32_t, 4>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<float, 3>(const void *in, void *out, size_t count);
|
||||
template void fromPlanarSSE<float, 4>(const void *in, void *out, size_t count);
|
||||
|
||||
#endif
|
||||
@@ -1,12 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/shaders">
|
||||
<file>image.frag</file>
|
||||
<file>image.vert</file>
|
||||
</qresource>
|
||||
<qresource prefix="/">
|
||||
<file>icon.png</file>
|
||||
<file>invert.png</file>
|
||||
<file>nuke.png</file>
|
||||
<file>bayer.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 947 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 454 B |
@@ -0,0 +1,29 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>invert.png</file>
|
||||
<file>nuke.png</file>
|
||||
<file>bayer.png</file>
|
||||
<file>space.nouspiro.tenmon.png</file>
|
||||
<file>nuke_a.png</file>
|
||||
<file>../about/tenmon</file>
|
||||
<file>../translations/tenmon_en.qm</file>
|
||||
<file>../translations/tenmon_sk.qm</file>
|
||||
<file>../about/filter.png</file>
|
||||
<file>../about/stretch-panel.png</file>
|
||||
<file>../translations/tenmon_fr.qm</file>
|
||||
<file>falsecolor.png</file>
|
||||
<file>link.png</file>
|
||||
<file>bggr.png</file>
|
||||
<file>grbg.png</file>
|
||||
<file>gbrg.png</file>
|
||||
</qresource>
|
||||
<qresource lang="en" prefix="/">
|
||||
<file alias="help">../about/help_en</file>
|
||||
</qresource>
|
||||
<qresource lang="sk" prefix="/">
|
||||
<file alias="help">../about/help_sk</file>
|
||||
</qresource>
|
||||
<qresource lang="fr" prefix="/">
|
||||
<file alias="help">../about/help_fr</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866668"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="space.nouspiro.tenmon.svg"
|
||||
inkscape:export-filename="/home/nou/c++/tenmon/space.nouspiro.tenmon_128.png"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
width="128px"
|
||||
inkscape:zoom="2.6547419"
|
||||
inkscape:cx="54.430903"
|
||||
inkscape:cy="78.162024"
|
||||
inkscape:window-width="1862"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="58"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Vrstva 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#000000;stroke:#ffffff;stroke-width:0;stroke-linejoin:round"
|
||||
id="rect1196"
|
||||
width="33.866665"
|
||||
height="33.866665"
|
||||
x="5e-07"
|
||||
y="5e-07" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:17.276px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.359917"
|
||||
x="-0.41414261"
|
||||
y="23.331123"
|
||||
id="text8592"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan8590"
|
||||
style="fill:#ffffff;stroke-width:0.359917"
|
||||
x="-0.41414261"
|
||||
y="23.331123">天文</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,628 @@
|
||||
#include "scriptengine.h"
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
#include <QInputDialog>
|
||||
#include "loadrunable.h"
|
||||
#include "rawimage.h"
|
||||
#include "loadrunable.h"
|
||||
#include "batchprocessing.h"
|
||||
#include <fitsio2.h>
|
||||
#include "libXISF/libxisf.h"
|
||||
|
||||
namespace Script
|
||||
{
|
||||
|
||||
ScriptEngine::ScriptEngine(BatchProcessing *parent)
|
||||
: _jsEngine(new QJSEngine(this))
|
||||
, _database(new Database(this))
|
||||
, _parent(parent)
|
||||
, _pool(new QThreadPool(this))
|
||||
{
|
||||
QJSValue core = _jsEngine->newQObject(this);
|
||||
_jsEngine->globalObject().setProperty("core", core);
|
||||
QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject);
|
||||
_jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject);
|
||||
_database->init(QLatin1String("scriptengine"));
|
||||
_semaphore.release(_pool->maxThreadCount());
|
||||
}
|
||||
|
||||
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
|
||||
{
|
||||
_scriptPath = scriptPath;
|
||||
_paths = paths;
|
||||
_outputDir = outputDir + "/";
|
||||
}
|
||||
|
||||
void ScriptEngine::reportError(const QString &message)
|
||||
{
|
||||
_jsEngine->throwError(message);
|
||||
}
|
||||
|
||||
const QString &ScriptEngine::outputDir() const
|
||||
{
|
||||
return _outputDir;
|
||||
}
|
||||
|
||||
void ScriptEngine::interrupt()
|
||||
{
|
||||
_jsEngine->setInterrupted(true);
|
||||
}
|
||||
|
||||
void ScriptEngine::logError(const QString &message)
|
||||
{
|
||||
emit newMessage(message, true);
|
||||
}
|
||||
|
||||
void ScriptEngine::log(const QString &message)
|
||||
{
|
||||
emit newMessage(message, false);
|
||||
}
|
||||
|
||||
void ScriptEngine::mark(File *file)
|
||||
{
|
||||
_database->mark(file->absoluteFilePath());
|
||||
}
|
||||
|
||||
void ScriptEngine::unmark(File *file)
|
||||
{
|
||||
_database->unmark(file->absoluteFilePath());
|
||||
}
|
||||
|
||||
bool ScriptEngine::isMarked(const File *file) const
|
||||
{
|
||||
return _database->isMarked(file->absoluteFilePath());
|
||||
}
|
||||
|
||||
void ScriptEngine::setMaxThread(int maxthread)
|
||||
{
|
||||
int newval = std::max(std::min(QThread::idealThreadCount(), maxthread), 1);
|
||||
int oldval = _pool->maxThreadCount();
|
||||
if(newval > oldval)
|
||||
_semaphore.release(newval - oldval);
|
||||
else if(newval < oldval)
|
||||
_semaphore.acquire(oldval - newval);
|
||||
_pool->setMaxThreadCount(newval);
|
||||
}
|
||||
|
||||
void ScriptEngine::sync()
|
||||
{
|
||||
_pool->waitForDone();
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::getString(const QString &label, const QString &text) const
|
||||
{
|
||||
QJSValue ret;
|
||||
QMetaObject::invokeMethod(_parent, "getString", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(QString, text));
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::getInt(const QString &label, int value)
|
||||
{
|
||||
QJSValue ret;
|
||||
QMetaObject::invokeMethod(_parent, "getInt", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(int, value));
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::getFloat(const QString &label, double value, int decimals) const
|
||||
{
|
||||
QJSValue ret;
|
||||
QMetaObject::invokeMethod(_parent, "getFloat", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(double, value), Q_ARG(int, decimals));
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, int current) const
|
||||
{
|
||||
QJSValue ret;
|
||||
QMetaObject::invokeMethod(_parent, "getItem", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QStringList, items), Q_ARG(QString, label), Q_ARG(int, current));
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async)
|
||||
{
|
||||
QString path;
|
||||
QDir dir(_outputDir);
|
||||
QFileInfo info(outpath);
|
||||
QString suffix = info.suffix();
|
||||
if(info.isAbsolute())
|
||||
path = info.absolutePath();
|
||||
else
|
||||
path = dir.absoluteFilePath(outpath);
|
||||
|
||||
QString f = format.toLower();
|
||||
if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg")
|
||||
{
|
||||
logError("Output format must be one of xisf fits jpg png bmp");
|
||||
return false;
|
||||
}
|
||||
|
||||
info.setFile(path);
|
||||
outpath = info.absolutePath() + "/" + info.completeBaseName() + "." + f;
|
||||
if(async)
|
||||
{
|
||||
_semaphore.acquire();
|
||||
_pool->start(new ConvertRunable(file->absoluteFilePath(), outpath, f, params, &_semaphore));
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvertRunable crun(file->absoluteFilePath(), outpath, f, params, nullptr);
|
||||
crun.run();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::newObject()
|
||||
{
|
||||
return _jsEngine->newObject();
|
||||
}
|
||||
|
||||
QJSValue ScriptEngine::newArray(uint size)
|
||||
{
|
||||
return _jsEngine->newArray(size);
|
||||
}
|
||||
|
||||
void ScriptEngine::run()
|
||||
{
|
||||
QJSValue jsPaths = _jsEngine->newArray(_paths.size());
|
||||
for(qsizetype i=0; i<_paths.size(); i++)
|
||||
jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i].first, _paths[i].second, this)));
|
||||
|
||||
_jsEngine->globalObject().setProperty("files", jsPaths);
|
||||
|
||||
QFile scriptFile(_scriptPath);
|
||||
if(!scriptFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
emit newMessage("Failed to open " + _scriptPath, true);
|
||||
emit finished();
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream stream(&scriptFile);
|
||||
QString contents = stream.readAll();
|
||||
scriptFile.close();
|
||||
QJSValue result = _jsEngine->evaluate(contents, _scriptPath);
|
||||
qDebug() << result.isError() << result.toString();
|
||||
_pool->waitForDone();
|
||||
if(result.isError())
|
||||
{
|
||||
QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
|
||||
error += "\n" + result.property("stack").toString();
|
||||
emit newMessage(error, true);
|
||||
}
|
||||
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void File::loadFitsKeywords()
|
||||
{
|
||||
if(!_fitsKeywordsLoaded)
|
||||
{
|
||||
_fitsKeywordsLoaded = true;
|
||||
ImageInfoData info;
|
||||
if(suffix().toLower() == "xisf")
|
||||
{
|
||||
readXISFHeader(_path, info);
|
||||
}
|
||||
else if(suffix().toLower() == "fits" || suffix().toLower() == "fit")
|
||||
{
|
||||
readFITSHeader(_path, info);
|
||||
}
|
||||
else return;
|
||||
|
||||
for(auto &record : info.fitsHeader)
|
||||
{
|
||||
_fitsKeywords.append(record.key);
|
||||
_fitsRecords.insert(record.key, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool File::mkpath(const QString &path) const
|
||||
{
|
||||
QFileInfo info(path);
|
||||
if(!info.isRelative())
|
||||
{
|
||||
_engine->logError("Destination path is not relative");
|
||||
return false;
|
||||
}
|
||||
QDir dir(_engine->outputDir());
|
||||
if(dir.mkpath(info.path()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_engine->logError("Failed to create dir " + info.path());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File::File(const QString &path, Script::ScriptEngine *engine) : File(path, QString(), engine)
|
||||
{
|
||||
}
|
||||
|
||||
File::File(const QString &path, const QString &root, ScriptEngine *engine) :
|
||||
_engine(engine),
|
||||
_path(path),
|
||||
_root(root),
|
||||
_info(path)
|
||||
{
|
||||
}
|
||||
|
||||
QString File::fileName() const
|
||||
{
|
||||
return _info.fileName();
|
||||
}
|
||||
|
||||
QString File::absoluteFilePath() const
|
||||
{
|
||||
return _info.absoluteFilePath();
|
||||
}
|
||||
|
||||
QString File::absolutePath() const
|
||||
{
|
||||
return _info.absolutePath();
|
||||
}
|
||||
|
||||
QString File::relativeFilePath() const
|
||||
{
|
||||
QDir dir(_root);
|
||||
return dir.relativeFilePath(_info.absoluteFilePath());
|
||||
}
|
||||
|
||||
QString File::relativePath() const
|
||||
{
|
||||
QDir dir(_root);
|
||||
return dir.relativeFilePath(_info.absolutePath());
|
||||
}
|
||||
|
||||
QString File::baseName() const
|
||||
{
|
||||
return _info.baseName();
|
||||
}
|
||||
|
||||
QString File::completeBaseName() const
|
||||
{
|
||||
return _info.completeBaseName();
|
||||
}
|
||||
|
||||
QString File::suffix() const
|
||||
{
|
||||
return _info.suffix();
|
||||
}
|
||||
|
||||
qint64 File::size() const
|
||||
{
|
||||
return _info.size();
|
||||
}
|
||||
|
||||
QStringList File::fitsKeywords()
|
||||
{
|
||||
loadFitsKeywords();
|
||||
return _fitsKeywords;
|
||||
}
|
||||
|
||||
QString File::fitsValue(const QString &key)
|
||||
{
|
||||
loadFitsKeywords();
|
||||
if(_fitsRecords.contains(key))
|
||||
return _fitsRecords[key].value.toString();
|
||||
else
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJSValue File::fitsValues(const QString &key)
|
||||
{
|
||||
loadFitsKeywords();
|
||||
if(_fitsRecords.contains(key))
|
||||
{
|
||||
QList<FITSRecord> values = _fitsRecords.values(key);
|
||||
QJSValue array = _engine->newArray(values.size());
|
||||
for(qsizetype i=0; i<values.size(); i++)
|
||||
array.setProperty(i, values[i].value.toString());
|
||||
return array;
|
||||
}
|
||||
else
|
||||
return QString();
|
||||
}
|
||||
|
||||
QJSValue File::fitsRecords()
|
||||
{
|
||||
loadFitsKeywords();
|
||||
|
||||
QJSValue array = _engine->newArray(_fitsRecords.size());
|
||||
uint i = 0;
|
||||
for(auto &record : _fitsRecords)
|
||||
{
|
||||
QJSValue item = _engine->newObject();
|
||||
item.setProperty("key", QString::fromUtf8(record.key));
|
||||
item.setProperty("value", record.value.toString());
|
||||
item.setProperty("comment", QString::fromUtf8(record.comment));
|
||||
item.setProperty("xisf", record.xisf);
|
||||
array.setProperty(i++, item);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
bool File::modifyFITSRecords(const FITSRecordModify *modify)
|
||||
{
|
||||
_fitsKeywordsLoaded = false;
|
||||
_fitsKeywords.clear();
|
||||
|
||||
if(QRegularExpression("fits?", QRegularExpression::CaseInsensitiveOption).match(suffix()).hasMatch())
|
||||
{
|
||||
fitsfile *file;
|
||||
int status = 0;
|
||||
fits_open_diskfile(&file, _path.toLocal8Bit().data(), READWRITE, &status);
|
||||
int num = 0;
|
||||
fits_get_num_hdus(file, &num, &status);
|
||||
if(status)
|
||||
{
|
||||
_engine->newMessage("Failed to open FITS file", true);
|
||||
return false;
|
||||
}
|
||||
int imgtype;
|
||||
int naxis;
|
||||
long naxes[3] = {0};
|
||||
int type = -1;
|
||||
for(int i=1; i <= num; i++)
|
||||
{
|
||||
fits_movabs_hdu(file, i, IMAGE_HDU, &status);
|
||||
fits_get_hdu_type(file, &type, &status);
|
||||
fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
|
||||
if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0)
|
||||
break;
|
||||
if(i == num)return false;
|
||||
}
|
||||
|
||||
for(auto &remove : modify->_remove)
|
||||
{
|
||||
int status = 0;//we ignore errors from here
|
||||
fits_delete_key(file, remove.toLatin1().data(), &status);
|
||||
}
|
||||
for(auto &record : modify->_update)
|
||||
{
|
||||
switch(record.value.typeId())
|
||||
{
|
||||
case QMetaType::Bool:
|
||||
{
|
||||
int val = record.value.toBool();
|
||||
fits_update_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Int:
|
||||
case QMetaType::UInt:
|
||||
{
|
||||
long long val = record.value.toLongLong();
|
||||
fits_update_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QString:
|
||||
{
|
||||
QByteArray val = record.value.toString().toLatin1();
|
||||
fits_update_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Float:
|
||||
case QMetaType::Double:
|
||||
{
|
||||
double val = record.value.toDouble();
|
||||
fits_update_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_engine->newMessage("Unknown type for KEY " + record.key, true);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
if(status)
|
||||
{
|
||||
char error[100];
|
||||
fits_get_errstatus(status, error);
|
||||
_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto &record : modify->_add)
|
||||
{
|
||||
switch(record.value.typeId())
|
||||
{
|
||||
case QMetaType::Bool:
|
||||
{
|
||||
int val = record.value.toBool();
|
||||
fits_write_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Int:
|
||||
case QMetaType::UInt:
|
||||
{
|
||||
long long val = record.value.toLongLong();
|
||||
fits_write_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QString:
|
||||
{
|
||||
QByteArray val = record.value.toString().toLatin1();
|
||||
fits_write_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Float:
|
||||
case QMetaType::Double:
|
||||
{
|
||||
double val = record.value.toDouble();
|
||||
fits_write_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_engine->newMessage("Unknown type for KEY " + record.key, true);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
if(status)
|
||||
{
|
||||
char error[100];
|
||||
fits_get_errstatus(status, error);
|
||||
_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
fits_close_file(file, &status);
|
||||
|
||||
return status == 0;
|
||||
}
|
||||
else if(suffix() == "xisf")
|
||||
{
|
||||
try
|
||||
{
|
||||
LibXISF::XISFModify modifyXISF;
|
||||
modifyXISF.open(_path.toLocal8Bit().data());
|
||||
QFileInfo in(_path);
|
||||
QFileInfo out(_path + "~");
|
||||
|
||||
for(auto &remove : modify->_remove)
|
||||
modifyXISF.removeFITSKeyword(0, remove.toStdString());
|
||||
|
||||
for(auto &record : modify->_update)
|
||||
modifyXISF.updateFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()}, true);
|
||||
|
||||
for(auto &record : modify->_add)
|
||||
modifyXISF.addFITSKeyword(0, {record.key.toStdString(), record.value.toString().toStdString(), record.value.toString().toStdString()});
|
||||
|
||||
modifyXISF.save(out.absoluteFilePath().toLocal8Bit().toStdString());
|
||||
modifyXISF.close();
|
||||
std::filesystem::rename(out.filesystemAbsoluteFilePath(), in.filesystemAbsoluteFilePath());
|
||||
}
|
||||
catch(LibXISF::Error &err)
|
||||
{
|
||||
_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool File::isMarked() const
|
||||
{
|
||||
return _engine->isMarked(this);
|
||||
}
|
||||
|
||||
File* File::copy(const QString &newpath) const
|
||||
{
|
||||
if(mkpath(newpath))
|
||||
{
|
||||
if(QFile::copy(_path, _engine->outputDir() + newpath))
|
||||
return new File(_engine->outputDir() + newpath, _engine);
|
||||
_engine->logError("Failed copy to " + newpath);
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool File::move(const QString &newpath)
|
||||
{
|
||||
if(mkpath(newpath))
|
||||
{
|
||||
if(QFile::rename(_path, _engine->outputDir() + newpath))
|
||||
{
|
||||
_path = _engine->outputDir() + newpath;
|
||||
return true;
|
||||
}
|
||||
_engine->logError("Failed move to " + newpath);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
File* File::convert(const QString &outpath, const QString &format, const QVariantMap ¶ms)
|
||||
{
|
||||
QString path = outpath;
|
||||
if(_engine->convert(this, path, format, params, false))
|
||||
return new File(path, _engine);
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
File* File::convertAsync(const QString &outpath, const QString &format, const QVariantMap ¶ms)
|
||||
{
|
||||
QString path = outpath;
|
||||
if(_engine->convert(this, path, format, params, true))
|
||||
return new File(path, _engine);
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QJSValue File::stats()
|
||||
{
|
||||
if(_stats.isUndefined())
|
||||
{
|
||||
ImageInfoData info;
|
||||
std::shared_ptr<RawImage> rawImage;
|
||||
loadImage(_path, info, rawImage);
|
||||
rawImage->calcStats();
|
||||
RawImage::Stats stats = rawImage->imageStats();
|
||||
_stats = _engine->newObject();
|
||||
_stats.setProperty("mean", stats.m_mean[0]);
|
||||
_stats.setProperty("stddev", stats.m_stdDev[0]);
|
||||
_stats.setProperty("median", stats.m_median[0]);
|
||||
_stats.setProperty("min", stats.m_min[0]);
|
||||
_stats.setProperty("max", stats.m_max[0]);
|
||||
_stats.setProperty("mad", stats.m_mean[0]);
|
||||
}
|
||||
return _stats;
|
||||
}
|
||||
|
||||
ScriptEngineThread::ScriptEngineThread(BatchProcessing *parent) : QObject(parent)
|
||||
{
|
||||
_thread = new QThread();
|
||||
_thread->setObjectName("ScriptEngine");
|
||||
_engine = new ScriptEngine(parent);
|
||||
_engine->moveToThread(_thread);
|
||||
connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit);
|
||||
connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage);
|
||||
connect(_thread, &QThread::started, _engine, &ScriptEngine::run);
|
||||
connect(_thread, &QThread::finished, _engine, &ScriptEngine::deleteLater);
|
||||
connect(_engine, &ScriptEngine::destroyed, [this](){ _engine = nullptr; });
|
||||
connect(_thread, &QThread::finished, _thread, &QThread::deleteLater);
|
||||
connect(_thread, &QThread::finished, this, &ScriptEngineThread::finished);
|
||||
}
|
||||
|
||||
ScriptEngineThread::~ScriptEngineThread()
|
||||
{
|
||||
if(_engine)_engine->interrupt();
|
||||
}
|
||||
|
||||
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
|
||||
{
|
||||
_engine->setParams(scriptPath, paths, outputDir);
|
||||
}
|
||||
|
||||
void ScriptEngineThread::start()
|
||||
{
|
||||
_thread->start();
|
||||
}
|
||||
|
||||
void ScriptEngineThread::interrupt()
|
||||
{
|
||||
if(_engine)_engine->interrupt();
|
||||
}
|
||||
|
||||
void FITSRecordModify::removeKeyword(const QString &key)
|
||||
{
|
||||
if(!_remove.contains(key))
|
||||
_remove.append(key);
|
||||
}
|
||||
|
||||
void FITSRecordModify::updateKeyword(const QString &key, const QVariant &value, const QString &comment)
|
||||
{
|
||||
_update.append({key.toLatin1(), value, comment.toLatin1()});
|
||||
}
|
||||
|
||||
void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, const QString &comment)
|
||||
{
|
||||
_update.append({key.toLatin1(), value, comment.toLatin1()});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#ifndef SCRIPTENGINE_H
|
||||
#define SCRIPTENGINE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJSEngine>
|
||||
#include <QFileInfo>
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QSemaphore>
|
||||
#include "database.h"
|
||||
#include "imageinfo.h"
|
||||
|
||||
class BatchProcessing;
|
||||
|
||||
namespace Script
|
||||
{
|
||||
|
||||
class File;
|
||||
|
||||
class ScriptEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QJSEngine *_jsEngine;
|
||||
Database *_database;
|
||||
BatchProcessing *_parent;
|
||||
QThreadPool *_pool;
|
||||
QSemaphore _semaphore;
|
||||
QString _scriptPath;
|
||||
QString _outputDir;
|
||||
QList<QPair<QString, QString>> _paths;
|
||||
public:
|
||||
explicit ScriptEngine(BatchProcessing *parent = nullptr);
|
||||
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
|
||||
void reportError(const QString &message);
|
||||
const QString& outputDir() const;
|
||||
void interrupt();
|
||||
void logError(const QString &message);
|
||||
Q_INVOKABLE void log(const QString &message);
|
||||
Q_INVOKABLE void mark(File *file);
|
||||
Q_INVOKABLE void unmark(File *file);
|
||||
Q_INVOKABLE bool isMarked(const File *file) const;
|
||||
Q_INVOKABLE void setMaxThread(int maxthread);
|
||||
Q_INVOKABLE void sync();
|
||||
Q_INVOKABLE QJSValue getString(const QString &label = QString(), const QString &text = QString()) const;
|
||||
Q_INVOKABLE QJSValue getInt(const QString &label = QString(), int value = 0);
|
||||
Q_INVOKABLE QJSValue getFloat(const QString &label = QString(), double value = 0, int decimals = 3) const;
|
||||
Q_INVOKABLE QJSValue getItem(const QStringList &items, const QString &label = "", int current = 0) const;
|
||||
bool convert(File *file, QString &outpath, const QString &format, const QVariantMap ¶ms, bool async);
|
||||
QJSValue newObject();
|
||||
QJSValue newArray(uint size);
|
||||
public slots:
|
||||
void run();
|
||||
signals:
|
||||
void newMessage(const QString &message, bool error);
|
||||
void finished();
|
||||
};
|
||||
|
||||
class ScriptEngineThread : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QThread *_thread;
|
||||
ScriptEngine *_engine;
|
||||
public:
|
||||
ScriptEngineThread(BatchProcessing *parent = nullptr);
|
||||
~ScriptEngineThread();
|
||||
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
|
||||
void start();
|
||||
void interrupt();
|
||||
signals:
|
||||
void newMessage(const QString &message, bool error);
|
||||
void finished();
|
||||
};
|
||||
|
||||
class FITSRecordModify;
|
||||
|
||||
class File : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
ScriptEngine *_engine;
|
||||
QString _path;
|
||||
QString _root;
|
||||
QFileInfo _info;
|
||||
bool _fitsKeywordsLoaded = false;
|
||||
QStringList _fitsKeywords;
|
||||
QMultiHash<QString, FITSRecord> _fitsRecords;
|
||||
void loadFitsKeywords();
|
||||
bool mkpath(const QString &path) const;
|
||||
QJSValue _stats;
|
||||
public:
|
||||
explicit File(const QString &path, ScriptEngine *engine);
|
||||
explicit File(const QString &path, const QString &root, ScriptEngine *engine);
|
||||
Q_INVOKABLE QString fileName() const;
|
||||
Q_INVOKABLE QString absoluteFilePath() const;
|
||||
Q_INVOKABLE QString absolutePath() const;
|
||||
Q_INVOKABLE QString relativeFilePath() const;
|
||||
Q_INVOKABLE QString relativePath() const;
|
||||
Q_INVOKABLE QString baseName() const;
|
||||
Q_INVOKABLE QString completeBaseName() const;
|
||||
Q_INVOKABLE QString suffix() const;
|
||||
Q_INVOKABLE qint64 size() const;
|
||||
Q_INVOKABLE QStringList fitsKeywords();
|
||||
Q_INVOKABLE QString fitsValue(const QString &key);
|
||||
Q_INVOKABLE QJSValue fitsValues(const QString &key);
|
||||
Q_INVOKABLE QJSValue fitsRecords();
|
||||
Q_INVOKABLE bool modifyFITSRecords(const FITSRecordModify *modify);
|
||||
Q_INVOKABLE bool isMarked() const;
|
||||
Q_INVOKABLE File* copy(const QString &newpath) const;
|
||||
Q_INVOKABLE bool move(const QString &newpath);
|
||||
Q_INVOKABLE File* convert(const QString &outpath, const QString &format, const QVariantMap ¶ms = QVariantMap());
|
||||
Q_INVOKABLE File* convertAsync(const QString &outpath, const QString &format, const QVariantMap ¶ms = QVariantMap());
|
||||
Q_INVOKABLE QJSValue stats();
|
||||
};
|
||||
|
||||
class FITSRecordModify : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QStringList _remove;
|
||||
QVector<FITSRecord> _update;
|
||||
QVector<FITSRecord> _add;
|
||||
|
||||
friend class File;
|
||||
public:
|
||||
Q_INVOKABLE FITSRecordModify(){};
|
||||
Q_INVOKABLE void removeKeyword(const QString &key);
|
||||
Q_INVOKABLE void updateKeyword(const QString &key, const QVariant &value, const QString &comment = QString());
|
||||
Q_INVOKABLE void addKeyword(const QString &key, const QVariant &value, const QString &comment = QString());
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SCRIPTENGINE_H
|
||||
@@ -0,0 +1,18 @@
|
||||
core.log("This script convert any FITS file into XISF with ZSTD compression");
|
||||
|
||||
if(files.length == 0)
|
||||
{
|
||||
core.log("No input files");
|
||||
throw "";
|
||||
}
|
||||
|
||||
let compression = {"compressionType": "zstd+sh"};
|
||||
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit")
|
||||
{
|
||||
core.log("Converting " + file.fileName());
|
||||
file.convertAsync(file.relativeFilePath(), "XISF", compression);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// how to get input from user
|
||||
let d = core.getFloat("Getting float value");
|
||||
let i = core.getInt("Getting integer value");
|
||||
let s = core.getString("Getting string value");
|
||||
|
||||
// print user input
|
||||
core.log("Your input " + d + " " + i + " " + s);
|
||||
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||
{
|
||||
let keywords = file.fitsKeywords();
|
||||
let item = core.getItem(keywords, "Select keyword");
|
||||
core.log("You selected keyword " + item); core.log(file.fitsKeywords());
|
||||
|
||||
core.log("fileName() " + file.fileName());
|
||||
core.log("absoluteFilePath() " + file.absoluteFilePath());
|
||||
core.log("absolutePath() " + file.absolutePath());
|
||||
core.log("relativeFilePath() " + file.relativeFilePath());
|
||||
core.log("relativePath() " + file.relativePath());
|
||||
core.log("baseName() " + file.baseName());
|
||||
core.log("completeBase() " + file.completeBaseName());
|
||||
core.log("suffix() " + file.suffix());
|
||||
core.log("size() " + file.size());
|
||||
let stats = file.stats();
|
||||
core.log("Image statistics");
|
||||
core.log("\tMinimum: " + stats.min);
|
||||
core.log("\tMaximum: " + stats.max);
|
||||
core.log("\tMedian:" + stats.median);
|
||||
core.log("\tStandard deviation:" + stats.stddev);
|
||||
core.log("\tMedian Absolute Deviation:" + stats.mad);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||
{
|
||||
let stats = file.stats();
|
||||
core.log("File: \"" + file.fileName() + "\" - median: " + stats.median);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
function checkFITS(key)
|
||||
{
|
||||
const noEditableKey = ["SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"];
|
||||
return noEditableKey.indexOf(key) < 0;
|
||||
}
|
||||
|
||||
if(files.length == 0)
|
||||
{
|
||||
core.log("No input files");
|
||||
throw "";
|
||||
}
|
||||
|
||||
let action = core.getItem(["UPDATE", "ADD", "REMOVE"], "Do you want update, add or remove record?");
|
||||
|
||||
let modify = new FITSRecordModify();
|
||||
|
||||
if(action == "UPDATE")
|
||||
{
|
||||
let keywords = files[0].fitsKeywords().filter(checkFITS);
|
||||
let keyword = core.getItem(keywords, "Select keyword to update");
|
||||
let value = files[0].fitsValue(keyword);
|
||||
if(isNaN(value))
|
||||
value = core.getString("Enter new value", value);
|
||||
else
|
||||
value = core.getFloat("Enter new value", value);
|
||||
modify.updateKeyword(keyword, value);
|
||||
}
|
||||
else if(action == "ADD")
|
||||
{
|
||||
let keyword = core.getString("Enter keyword to add");
|
||||
let value = core.getString("Enter new value");
|
||||
keyword = keyword.toUpperCase();
|
||||
modify.addKeyword(keyword, value);
|
||||
}
|
||||
else if(action == "REMOVE")
|
||||
{
|
||||
let keywords = files[0].fitsKeywords().filter(checkFITS);
|
||||
let keyword = core.getItem(keywords, "Select keyword to remove");
|
||||
modify.removeKeyword(keyword);
|
||||
}
|
||||
|
||||
for(file of files)
|
||||
{
|
||||
if(file.suffix() == "fits" || file.suffix() == "fit" || file.suffix() == "xisf")
|
||||
{
|
||||
core.log("Modifing " + file.fileName());
|
||||
file.modifyFITSRecords(modify);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<RCC>
|
||||
<qresource prefix="/scripts">
|
||||
<file>example script</file>
|
||||
<file>convert to XISF</file>
|
||||
<file>median</file>
|
||||
<file>modify FITS header</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "settingsdialog.h"
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include "rawimage.h"
|
||||
|
||||
extern int DEFAULT_WIDTH;
|
||||
extern double SATURATION;
|
||||
extern int FILTERING;
|
||||
|
||||
class EvenNumber : public QSpinBox
|
||||
{
|
||||
public:
|
||||
explicit EvenNumber(QWidget *parent) : QSpinBox(parent){}
|
||||
protected:
|
||||
QValidator::State validate(QString &text, int &) const
|
||||
{
|
||||
bool ok;
|
||||
int val = text.toInt(&ok);
|
||||
if(ok && (val & 1) == 0)return QValidator::Acceptable;
|
||||
if(ok)return QValidator::Intermediate;
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
void fixup(QString &input) const
|
||||
{
|
||||
bool ok;
|
||||
int val = input.toInt(&ok);
|
||||
val -= val & 1;
|
||||
input = QString::number(val);
|
||||
}
|
||||
};
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
QFormLayout *layout = new QFormLayout(this);
|
||||
setWindowTitle(tr("Settings"));
|
||||
|
||||
QSettings settings;
|
||||
|
||||
m_preloadImages = new QSpinBox(this);
|
||||
m_preloadImages->setRange(0, 32);
|
||||
m_preloadImages->setValue(settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt());
|
||||
m_preloadImages->setToolTip(tr("How many images are preloaded before and after current image."));
|
||||
|
||||
m_thumSize = new EvenNumber(this);
|
||||
m_thumSize->setRange(64, 512);
|
||||
m_thumSize->setSingleStep(2);
|
||||
m_thumSize->setValue(settings.value("settings/thumnailsize", THUMB_SIZE).toInt());
|
||||
m_thumSize->setToolTip(tr("Thumbnail size in pixels"));
|
||||
|
||||
m_saturation = new QDoubleSpinBox(this);
|
||||
m_saturation->setMinimum(0);
|
||||
m_saturation->setMaximum(100);
|
||||
m_saturation->setSuffix(" %");
|
||||
m_saturation->setValue(settings.value("settings/saturation", SATURATION * 100.0).toDouble());
|
||||
m_saturation->setToolTip(tr("Set threshold value that is considered saturated when showing statistics.\nFor RAW files you may set 22%"));
|
||||
|
||||
m_slideShowTime = new QDoubleSpinBox(this);
|
||||
m_slideShowTime->setMinimum(0.01);
|
||||
m_slideShowTime->setMaximum(10);
|
||||
m_slideShowTime->setSuffix(" s");
|
||||
m_slideShowTime->setValue(settings.value("settings/slideshowtime", 1.0).toDouble());
|
||||
m_slideShowTime->setSingleStep(0.1);
|
||||
|
||||
m_useNativeDialog = new QCheckBox(tr("Don't use native file dialog"), this);
|
||||
m_useNativeDialog->setChecked(QApplication::testAttribute(Qt::AA_DontUseNativeDialogs));
|
||||
|
||||
m_filtering = new QComboBox(this);
|
||||
m_filtering->addItems({tr("Nearest"), tr("Linear"), tr("Cubic")});
|
||||
m_filtering->setCurrentIndex(FILTERING);
|
||||
|
||||
m_qualityThumbnail = new QCheckBox(tr("Smooth thumbnails"), this);
|
||||
m_qualityThumbnail->setChecked(QUALITY_RESIZE);
|
||||
m_qualityThumbnail->setToolTip(tr("Use box filter when downsampling thumbnails instead of nearest. Slightly slower."));
|
||||
|
||||
layout->addRow(tr("Image preload count"), m_preloadImages);
|
||||
layout->addRow(tr("Thumbnails size"), m_thumSize);
|
||||
layout->addRow(tr("Saturation"), m_saturation);
|
||||
layout->addRow(tr("Slideshow interval"), m_slideShowTime);
|
||||
layout->addRow(tr("Image interpolation"), m_filtering);
|
||||
layout->addRow(m_qualityThumbnail);
|
||||
layout->addRow(m_useNativeDialog);
|
||||
//layout->addRow(new QLabel(tr("Changes in settings will take effect after program restart.")));
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
|
||||
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(this, &QDialog::accepted, this, &SettingsDialog::saveSettings);
|
||||
|
||||
layout->addRow(buttonBox);
|
||||
}
|
||||
|
||||
void SettingsDialog::loadSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
DEFAULT_WIDTH = settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt();
|
||||
SATURATION = settings.value("settings/saturation", 95.0).toDouble() / 100.0;
|
||||
FILTERING = settings.value("settings/filtering", FILTERING).toInt();
|
||||
QUALITY_RESIZE = settings.value("settings/qualitythumbnail", QUALITY_RESIZE).toBool();
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, settings.value("settings/dontusenativedialogs", false).toBool());
|
||||
}
|
||||
|
||||
bool SettingsDialog::loadThumbsizes()
|
||||
{
|
||||
QSettings settings;
|
||||
int OLD_THUMB_SIZE = THUMB_SIZE;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
return OLD_THUMB_SIZE != THUMB_SIZE;
|
||||
}
|
||||
|
||||
void SettingsDialog::saveSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("settings/thumbnailsize", m_thumSize->value());
|
||||
settings.setValue("settings/preloadimagecount", m_preloadImages->value());
|
||||
settings.setValue("settings/dontusenativedialogs", m_useNativeDialog->isChecked());
|
||||
settings.setValue("settings/saturation", m_saturation->value());
|
||||
settings.setValue("settings/slideshowtime", m_slideShowTime->value());
|
||||
settings.setValue("settings/qualitythumbnail", m_qualityThumbnail->isChecked());
|
||||
QUALITY_RESIZE = m_qualityThumbnail->isChecked();
|
||||
FILTERING = m_filtering->currentIndex();
|
||||
settings.setValue("settings/filtering", FILTERING);
|
||||
SATURATION = m_saturation->value() / 100.0;
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, m_useNativeDialog->isChecked());
|
||||
if(DEFAULT_WIDTH != m_preloadImages->value())
|
||||
emit preloadChanged(m_preloadImages->value());
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef SETTINGSDIALOG_H
|
||||
#define SETTINGSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(QWidget *parent = nullptr);
|
||||
static void loadSettings();
|
||||
static bool loadThumbsizes();
|
||||
signals:
|
||||
void preloadChanged(int witdth);
|
||||
private:
|
||||
void saveSettings();
|
||||
|
||||
QSpinBox *m_preloadImages;
|
||||
QSpinBox *m_thumSize;
|
||||
QDoubleSpinBox *m_slideShowTime;
|
||||
QDoubleSpinBox *m_saturation;
|
||||
QCheckBox *m_useNativeDialog;
|
||||
QCheckBox *m_qualityThumbnail;
|
||||
QComboBox *m_filtering;
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
@@ -0,0 +1,42 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
uniform ivec2 firstRed;
|
||||
in vec2 qt_TexCoord0;
|
||||
in vec2 center;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
#define f(x, y) texelFetch(qt_Texture0, icenter + ivec2(x, y), 0).r
|
||||
|
||||
void main(void)
|
||||
{
|
||||
ivec2 texSize = textureSize(qt_Texture0, 0);
|
||||
ivec2 icenter = ivec2(center);
|
||||
ivec2 alternate = (icenter + firstRed) % 2;
|
||||
|
||||
// cross, checker, theta, phi
|
||||
const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0;
|
||||
const vec4 kB = vec4( 2.0, 0.0, 0.0, 4.0) / 8.0;
|
||||
const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0;
|
||||
const vec4 kD = vec4( 0.0, 2.0, -1.0, -1.0) / 8.0;
|
||||
const vec4 kE = vec4(-1.0, -1.5, -1.0, 0.5) / 8.0;
|
||||
const vec4 kF = vec4( 2.0, 0.0, 4.0, 0.0) / 8.0;
|
||||
|
||||
float A = f(0,2) + f(0,-2);
|
||||
float B = f(0,1) + f(0,-1);
|
||||
float C = f(0,0);
|
||||
float D = f(1,1) + f(-1,1) + f(1,-1) + f(-1,-1);
|
||||
float E = f(2,0) + f(-2,0);
|
||||
float F = f(1,0) + f(-1,0);
|
||||
|
||||
vec4 P = kA*A + kB*B + kC*C + kD*D + kE*E + kF*F;
|
||||
|
||||
color.rgb = alternate.y == 0 ?
|
||||
(alternate.x == 0 ? vec3(C, P.xy) : // even row even col
|
||||
vec3(P.z, C, P.w)) : // even row odd col
|
||||
(alternate.x == 0 ? vec3(P.w, C, P.z) : // odd row even col
|
||||
vec3(P.yx, C)); // odd row odd col
|
||||
|
||||
color.a = 1.0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
out vec2 qt_TexCoord0;
|
||||
out vec2 center;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 texSize = vec2(textureSize(qt_Texture0, 0));
|
||||
gl_Position = vec4(qt_Vertex, 0.0, 1.0);
|
||||
qt_TexCoord0 = qt_MultiTexCoord0;
|
||||
qt_TexCoord0.y = 1.0 - qt_TexCoord0.y;
|
||||
center = qt_TexCoord0 * texSize.xy;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
uniform vec3 mtf_param[3];
|
||||
uniform vec2 unit_scale;
|
||||
uniform bool bw;
|
||||
uniform bool invert;
|
||||
uniform bool srgb;
|
||||
uniform bool false_color;
|
||||
uniform int filtering;
|
||||
in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec3 Linear2sRGB(vec3 color)
|
||||
{
|
||||
return mix(12.92 * color.rgb,
|
||||
1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,
|
||||
greaterThan(color, vec3(0.0031308)));
|
||||
}
|
||||
|
||||
vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
|
||||
{
|
||||
x = (x - low) / (high - low);
|
||||
x = clamp(x, vec4(0.0), vec4(1.0));
|
||||
return ((mid - 1) * x) / ((2 * mid - 1) * x - mid);
|
||||
}
|
||||
|
||||
vec3 falsecolor(float color)
|
||||
{
|
||||
const vec3 pallete[] = vec3[](
|
||||
vec3(1.0, 0.0, 1.0), //magneta
|
||||
vec3(0.0, 0.0, 1.0), //blue
|
||||
vec3(0.0, 1.0, 1.0), //cyan
|
||||
vec3(0.0, 1.0, 0.0), //green
|
||||
vec3(1.0, 1.0, 0.0), //yellow
|
||||
vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0));//red
|
||||
color *= 5.0;
|
||||
int i = int(color);
|
||||
float f = fract(color);
|
||||
return mix(pallete[i], pallete[i+1], f);// * (f * 0.5 + 0.5);
|
||||
}
|
||||
|
||||
vec3 checker()
|
||||
{
|
||||
vec2 pattern = fract(gl_FragCoord.xy * 0.0625) - 0.5;
|
||||
return vec3(step(pattern.x * pattern.y, 0.0) * 0.25 + 0.25);
|
||||
}
|
||||
|
||||
vec4 cubic(float v)
|
||||
{
|
||||
vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
|
||||
vec4 s = n * n * n;
|
||||
float x = s.x;
|
||||
float y = s.y - 4.0 * s.x;
|
||||
float z = s.z - 4.0 * s.y + 6.0 * s.x;
|
||||
float w = 6.0 - x - y - z;
|
||||
return vec4(x, y, z, w) * (1.0/6.0);
|
||||
}
|
||||
|
||||
vec4 textureBicubic(sampler2D sampler, vec2 texCoords)
|
||||
{
|
||||
vec2 texSize = textureSize(sampler, 0);
|
||||
vec2 invTexSize = 1.0 / texSize;
|
||||
|
||||
texCoords = texCoords * texSize - 0.5;
|
||||
|
||||
vec2 fxy = fract(texCoords);
|
||||
texCoords -= fxy;
|
||||
|
||||
vec4 xcubic = cubic(fxy.x);
|
||||
vec4 ycubic = cubic(fxy.y);
|
||||
|
||||
vec4 c = texCoords.xxyy + vec2 (-0.5, +1.5).xyxy;
|
||||
|
||||
vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
|
||||
vec4 offset = c + vec4 (xcubic.yw, ycubic.yw) / s;
|
||||
|
||||
offset *= invTexSize.xxyy;
|
||||
|
||||
vec4 sample0 = texture(sampler, offset.xz);
|
||||
vec4 sample1 = texture(sampler, offset.yz);
|
||||
vec4 sample2 = texture(sampler, offset.xw);
|
||||
vec4 sample3 = texture(sampler, offset.yw);
|
||||
|
||||
float sx = s.x / (s.x + s.y);
|
||||
float sy = s.z / (s.z + s.w);
|
||||
|
||||
return mix(mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy);
|
||||
}
|
||||
|
||||
vec4 textureCatmul(sampler2D sampler, vec2 texCoords)
|
||||
{
|
||||
ivec2 texSize = textureSize(sampler, 0);
|
||||
|
||||
texCoords = texCoords * vec2(texSize) - 0.5;
|
||||
|
||||
ivec2 texel = ivec2(floor(texCoords));
|
||||
vec2 fra = fract(texCoords);
|
||||
texSize -= 1;
|
||||
|
||||
const mat4 CatMul = mat4(0, 1, 0, 0, -0.5, 0, 0.5, 0, 1, -2.5, 2, -0.5, -0.5, 1.5, -1.5, 0.5);
|
||||
vec4 xx = CatMul * vec4(1.0, fra.x, fra.x*fra.x, fra.x*fra.x*fra.x);
|
||||
vec4 yy = CatMul * vec4(1.0, fra.y, fra.y*fra.y, fra.y*fra.y*fra.y);
|
||||
|
||||
vec4 a00 = texelFetch(sampler, clamp(texel + ivec2(-1, -1), ivec2(0, 0), texSize), 0) * xx.x;
|
||||
vec4 a01 = texelFetch(sampler, clamp(texel + ivec2( 0, -1), ivec2(0, 0), texSize), 0) * xx.y;
|
||||
vec4 a02 = texelFetch(sampler, clamp(texel + ivec2( 1, -1), ivec2(0, 0), texSize), 0) * xx.z;
|
||||
vec4 a03 = texelFetch(sampler, clamp(texel + ivec2( 2, -1), ivec2(0, 0), texSize), 0) * xx.w;
|
||||
vec4 a10 = texelFetch(sampler, clamp(texel + ivec2(-1, 0), ivec2(0, 0), texSize), 0) * xx.x;
|
||||
vec4 a11 = texelFetch(sampler, clamp(texel + ivec2( 0, 0), ivec2(0, 0), texSize), 0) * xx.y;
|
||||
vec4 a12 = texelFetch(sampler, clamp(texel + ivec2( 1, 0), ivec2(0, 0), texSize), 0) * xx.z;
|
||||
vec4 a13 = texelFetch(sampler, clamp(texel + ivec2( 2, 0), ivec2(0, 0), texSize), 0) * xx.w;
|
||||
vec4 a20 = texelFetch(sampler, clamp(texel + ivec2(-1, 1), ivec2(0, 0), texSize), 0) * xx.x;
|
||||
vec4 a21 = texelFetch(sampler, clamp(texel + ivec2( 0, 1), ivec2(0, 0), texSize), 0) * xx.y;
|
||||
vec4 a22 = texelFetch(sampler, clamp(texel + ivec2( 1, 1), ivec2(0, 0), texSize), 0) * xx.z;
|
||||
vec4 a23 = texelFetch(sampler, clamp(texel + ivec2( 2, 1), ivec2(0, 0), texSize), 0) * xx.w;
|
||||
vec4 a30 = texelFetch(sampler, clamp(texel + ivec2(-1, 2), ivec2(0, 0), texSize), 0) * xx.x;
|
||||
vec4 a31 = texelFetch(sampler, clamp(texel + ivec2( 0, 2), ivec2(0, 0), texSize), 0) * xx.y;
|
||||
vec4 a32 = texelFetch(sampler, clamp(texel + ivec2( 1, 2), ivec2(0, 0), texSize), 0) * xx.z;
|
||||
vec4 a33 = texelFetch(sampler, clamp(texel + ivec2( 2, 2), ivec2(0, 0), texSize), 0) * xx.w;
|
||||
|
||||
vec4 c = vec4(0.0);
|
||||
c += (a00 + a01 + a02 + a03) * yy.x;
|
||||
c += (a10 + a11 + a12 + a13) * yy.y;
|
||||
c += (a20 + a21 + a22 + a23) * yy.z;
|
||||
c += (a30 + a31 + a32 + a33) * yy.w;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
switch(filtering)
|
||||
{
|
||||
case 0://nearest
|
||||
color = texelFetch(qt_Texture0, ivec2(qt_TexCoord0 * textureSize(qt_Texture0, 0)), 0);
|
||||
break;
|
||||
default:
|
||||
case 1://bilinear
|
||||
color = texture(qt_Texture0, qt_TexCoord0);
|
||||
break;
|
||||
case 2://catmul bicubic
|
||||
color = textureCatmul(qt_Texture0, qt_TexCoord0);
|
||||
break;
|
||||
}
|
||||
|
||||
color.rgb = color.rgb * unit_scale.x + unit_scale.y;
|
||||
if(bw)color = color.rrra;
|
||||
color = MTF(color, vec4(mtf_param[0], 0.0), vec4(mtf_param[1], 0.5), vec4(mtf_param[2], 1.0));
|
||||
if(false_color)color.rgb = falsecolor(color.r);
|
||||
|
||||
if(invert)color.rgb = vec3(1.0) - color.rgb;
|
||||
|
||||
color.rgb = mix(checker(), color.rgb, color.a);
|
||||
|
||||
if(srgb)color.rgb = Linear2sRGB(color.rgb);
|
||||
|
||||
if(any(lessThan(qt_TexCoord0, vec2(0.0))) || any(greaterThan(qt_TexCoord0, vec2(1.0))))
|
||||
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
color.a = 1.0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#version 130
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_Vertex;
|
||||
@@ -0,0 +1,10 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>debayer.frag</file>
|
||||
<file>debayer.vert</file>
|
||||
<file>image.frag</file>
|
||||
<file>image.vert</file>
|
||||
<file>thumb.frag</file>
|
||||
<file>thumb.vert</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,22 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2DArray qt_Texture0;
|
||||
uniform vec3 mtf_param[3];
|
||||
uniform bool invert;
|
||||
in vec3 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
vec4 MTF(vec4 x, vec4 low, vec4 mid, vec4 high)
|
||||
{
|
||||
x = (x - low) / (high - low);
|
||||
x = clamp(x, vec4(0.0), vec4(1.0));
|
||||
return ((mid - 1) * x) / ((2 * mid - 1) * x - mid);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
color = texture(qt_Texture0, qt_TexCoord0);
|
||||
color = MTF(color, vec4(mtf_param[0], 0.0), vec4(mtf_param[1], 0.5), vec4(mtf_param[2], 1.0));
|
||||
if(invert)color = vec4(1.0) - color;
|
||||
color.a = 1.0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 330
|
||||
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
in ivec3 imageSize_num;
|
||||
out vec3 qt_TexCoord0;
|
||||
uniform ivec3 viewport_row;
|
||||
uniform mat4 mvp;
|
||||
uniform vec2 offset;
|
||||
uniform ivec3 thumb_size;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 pos = qt_Vertex * 0.5;
|
||||
pos.y *= -1.0;
|
||||
pos = pos * imageSize_num.xy + thumb_size.x;
|
||||
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * thumb_size.yz;
|
||||
|
||||
gl_Position = mvp * vec4(pos - offset + off, 0.0, 1.0);
|
||||
qt_TexCoord0 = vec3(qt_MultiTexCoord0, imageSize_num.z + 0.1);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Exec=tenmon %U
|
||||
Icon=space.nouspiro.tenmon
|
||||
Comment=FITS Image viewer
|
||||
Name=Tenmon
|
||||
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;Science;Astronomy
|
||||
MimeType=image/fits;image/x-xisf;
|
||||
Terminal=false
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>space.nouspiro.tenmon</id>
|
||||
<launchable type="desktop-id">space.nouspiro.tenmon.desktop</launchable>
|
||||
<name>Tenmon</name>
|
||||
<summary>FITS/XISF image viewer, converter, index and search</summary>
|
||||
<developer id="nouspiro.space">
|
||||
<name>Dušan Poizl</name>
|
||||
</developer>
|
||||
<developer_name>Dušan Poizl</developer_name>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<description>
|
||||
<p>It is intended primarily for viewing astro photos and images. It supports the following formats:</p>
|
||||
<ul>
|
||||
<li>FITS 8, 16, 32 bit integer and 32, 64 bit float</li>
|
||||
<li>XISF 8, 16, 32 bit integer and 32, 64 bit float</li>
|
||||
<li>RAW CR2/CR3, DNG, NEF</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
|
||||
</ul>
|
||||
<p>Features of application:</p>
|
||||
<ul>
|
||||
<li>Using same stretch function as PixInsight</li>
|
||||
<li>OpenGL accelerated drawing</li>
|
||||
<li>Index and search FITS XISF header data</li>
|
||||
<li>Quick mark images and then copy/move marked files</li>
|
||||
<li>Convert FITS <-> XISF</li>
|
||||
<li>Convert FITS/XISF -> JPEG/PNG</li>
|
||||
<li>Image statistics mean, media, min, max</li>
|
||||
<li>Support for WCS</li>
|
||||
<li>Thumbnails</li>
|
||||
<li>Convert CFA images to colour - debayer</li>
|
||||
<li>Color space aware</li>
|
||||
<li>Histogram</li>
|
||||
</ul>
|
||||
</description>
|
||||
<categories>
|
||||
<category>Graphics</category>
|
||||
<category>Viewer</category>
|
||||
<category>Science</category>
|
||||
<category>Astronomy</category>
|
||||
</categories>
|
||||
<keywords>
|
||||
<keyword>astronomy</keyword>
|
||||
</keywords>
|
||||
<url type="homepage">https://nouspiro.space/?page_id=206</url>
|
||||
<url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Main window with image</caption>
|
||||
<image>https://nouspiro.space/wp-content/uploads/2022/04/tenmon-1024x579.png</image>
|
||||
</screenshot>
|
||||
<screenshot type="default">
|
||||
<caption>Thumnail view</caption>
|
||||
<image>https://nouspiro.space/wp-content/uploads/2022/12/tenmon2-1024x645.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="20240616" date="2024-06-16">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Batch processing with JavaScript</li>
|
||||
<li>Opening directory recursively</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20240201" date="2024-02-01">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Smooth thumbnails</li>
|
||||
<li>Respect ROWORDER</li>
|
||||
<li>Bugfixes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20240108" date="2024-01-08">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Update to Qt6</li>
|
||||
<li>Add support for CR3 RAW files</li>
|
||||
<li>Slideshow</li>
|
||||
<li>Improved rapid image view</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20231116" date="2023-11-16">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Histogram</li>
|
||||
<li>False colors</li>
|
||||
<li>Strech each RGB channel individually</li>
|
||||
<li>Better white balancing</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20230419" date="2023-04-19">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add file sorting</li>
|
||||
<li>Improved zoom and scrolling.</li>
|
||||
<li>Shift modify if zoom on mouse position or center</li>
|
||||
<li>Fix issue with XISF from NINA</li>
|
||||
<li>Fix crash in flatpak version</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20230212" date="2023-02-12">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Replace PCL with LibXISF</li>
|
||||
<li>Fix issue with OpenGL</li>
|
||||
<li>Fix loading SATURATION settings</li>
|
||||
<li>Fix issue with whitebalance coefficients</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20221228" date="2022-12-28">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add gray world white balance</li>
|
||||
<li>Export database to CSV</li>
|
||||
<li>Fine tune of STF when Shift key is pressed</li>
|
||||
<li>Add saturation statistic</li>
|
||||
<li>Various fixes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20221215" date="2022-12-15">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add debayer support</li>
|
||||
<li>Move to trash action</li>
|
||||
<li>Change copy and move shortcuts to F5 and F6</li>
|
||||
<li>Change mark and unmark key shortcuts to F7 and F8</li>
|
||||
<li>Fix not refreshing file tree</li>
|
||||
<li>Add option to not use native file dialog</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20221209" date="2022-12-09"/>
|
||||
<release version="20221126" date="2022-11-26"/>
|
||||
<release version="20221121" date="2022-11-11"/>
|
||||
<release version="20221023" date="2022-10-23"/>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -237,6 +237,8 @@ StarFit::StarFit(int size)
|
||||
m_fdf_an.fvv = nullptr;
|
||||
m_fdf_an.n = size*size;
|
||||
m_fdf_an.p = 6;//number of model parameters amplitude, x, y, sigma_x, sigma_y, angle
|
||||
|
||||
gsl_set_error_handler_off();
|
||||
}
|
||||
|
||||
StarFit::~StarFit()
|
||||
@@ -272,14 +274,10 @@ Star StarFit::fitStar(const std::vector<double> &data, bool angle)
|
||||
|
||||
gsl_multifit_nlinear_workspace *workspace = gsl_multifit_nlinear_alloc(gsl_multifit_nlinear_trust, &m_fdf_params, fdf->n, fdf->p);
|
||||
|
||||
gsl_multifit_nlinear_init(start, fdf, workspace);
|
||||
gsl_vector *f = gsl_multifit_nlinear_residual(workspace);
|
||||
int ret = gsl_multifit_nlinear_init(start, fdf, workspace);
|
||||
if(ret)return star;
|
||||
|
||||
double cost, cost0;
|
||||
gsl_blas_ddot(f, f, &cost0);
|
||||
int ret = gsl_multifit_nlinear_driver(MAX_ITER, TOL, TOL, TOL, nullptr, nullptr, &info, workspace);
|
||||
|
||||
gsl_blas_ddot(f, f, &cost);
|
||||
ret = gsl_multifit_nlinear_driver(MAX_ITER, TOL, TOL, TOL, nullptr, nullptr, &info, workspace);
|
||||
|
||||
if(ret==0)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "statusbar.h"
|
||||
#include <QFontMetrics>
|
||||
|
||||
StatusBar::StatusBar(QWidget *parent) : QStatusBar(parent)
|
||||
{
|
||||
m_value = new QLabel(this);
|
||||
m_pixelCoords = new QLabel(this);
|
||||
m_celestianCoords = new QLabel(this);
|
||||
|
||||
m_value->setMinimumWidth(m_value->fontMetrics().horizontalAdvance("R:65536 G:65536 B:65536 "));
|
||||
m_pixelCoords->setMinimumWidth(m_pixelCoords->fontMetrics().horizontalAdvance("X:65536 Y:65536 "));
|
||||
m_celestianCoords->setMinimumWidth(m_celestianCoords->fontMetrics().horizontalAdvance("RA: 00h00m00s DEC: 00° 00' 00\" "));
|
||||
addPermanentWidget(m_value);
|
||||
addPermanentWidget(m_pixelCoords);
|
||||
addPermanentWidget(m_celestianCoords);
|
||||
}
|
||||
|
||||
void StatusBar::newStatus(const QString &value, const QString &pixelCoords, const QString &celestialCoords)
|
||||
{
|
||||
m_value->setText(value);
|
||||
m_pixelCoords->setText(pixelCoords);
|
||||
m_celestianCoords->setText(celestialCoords);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef STATUSBAR_H
|
||||
#define STATUSBAR_H
|
||||
|
||||
#include <QStatusBar>
|
||||
#include <QLabel>
|
||||
|
||||
class StatusBar : public QStatusBar
|
||||
{
|
||||
Q_OBJECT
|
||||
QLabel *m_value;
|
||||
QLabel *m_pixelCoords;
|
||||
QLabel *m_celestianCoords;
|
||||
public:
|
||||
explicit StatusBar(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
void newStatus(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
|
||||
};
|
||||
|
||||
#endif // STATUSBAR_H
|
||||
@@ -10,15 +10,35 @@ static float clamp(float x)
|
||||
return std::min(std::max(x, 0.0f), 1.0f);
|
||||
}
|
||||
|
||||
STFSlider::STFSlider(QWidget *parent) : QWidget(parent)
|
||||
STFSlider::STFSlider(const QColor &color, QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setMinimumHeight(15);
|
||||
setMaximumHeight(15);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setMinimumWidth(100);
|
||||
setMouseTracking(true);
|
||||
|
||||
if(color == Qt::white)
|
||||
{
|
||||
setMaximumHeight(16);
|
||||
setMinimumHeight(16);
|
||||
}
|
||||
else
|
||||
{
|
||||
setMaximumHeight(10);
|
||||
setMinimumHeight(10);
|
||||
}
|
||||
m_blackPoint = 0;
|
||||
m_midPoint = 0.5;
|
||||
m_whitePoint = 1;
|
||||
m_grabbed = -1;
|
||||
m_fineTune = false;
|
||||
m_color = color;
|
||||
if(color == Qt::blue || color == Qt::red)
|
||||
m_threshold = 1.1f;
|
||||
else if(color == Qt::green)
|
||||
m_threshold = 0.8f;
|
||||
else
|
||||
m_threshold = 0.4f;
|
||||
setToolTip(tr("Press Shift for fine tuning"));
|
||||
}
|
||||
|
||||
float STFSlider::blackPoint() const
|
||||
@@ -56,7 +76,7 @@ void STFSlider::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
qreal p = i/32.0f;
|
||||
qreal c = std::pow(p, 1.0/2.2)*255;
|
||||
gradient.setColorAt(p, QColor(c, c, c));
|
||||
gradient.setColorAt(p, QColor(m_color.redF()*c, m_color.greenF()*c, m_color.blueF()*c));
|
||||
}
|
||||
|
||||
QPainterPath tick(QPointF(0, 0));
|
||||
@@ -71,7 +91,7 @@ void STFSlider::paintEvent(QPaintEvent *event)
|
||||
|
||||
auto drawTick = [&](qreal p)
|
||||
{
|
||||
painter.setPen(p < 0.4 ? Qt::white : Qt::black);
|
||||
painter.setPen(p < m_threshold ? Qt::white : Qt::black);
|
||||
painter.resetTransform();
|
||||
painter.translate(w*p, 0);
|
||||
painter.drawPath(tick);
|
||||
@@ -85,29 +105,44 @@ void STFSlider::paintEvent(QPaintEvent *event)
|
||||
|
||||
void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if(std::abs(m_blackPoint*width() - event->x()) < 5 ||
|
||||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - event->x()) < 5 ||
|
||||
std::abs(m_whitePoint*width() - event->x()) < 5)
|
||||
const qreal x = event->position().x();
|
||||
if(std::abs(m_blackPoint*width() - x) < 5 ||
|
||||
std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5 ||
|
||||
std::abs(m_whitePoint*width() - x) < 5)
|
||||
setCursor(Qt::SplitHCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
qreal xw = x/width();
|
||||
if(event->modifiers() & Qt::ShiftModifier && !m_fineTune)
|
||||
{
|
||||
m_fineTune = true;
|
||||
m_fineTuneX = xw;
|
||||
}
|
||||
if(!(event->modifiers() & Qt::ShiftModifier) && m_fineTune)
|
||||
m_fineTune = false;
|
||||
|
||||
if(m_fineTune)
|
||||
{
|
||||
xw = m_fineTuneX + (xw - m_fineTuneX) * 0.2;
|
||||
}
|
||||
|
||||
switch(m_grabbed)
|
||||
{
|
||||
case 0:
|
||||
m_blackPoint = clamp((qreal)event->x()/width());
|
||||
m_blackPoint = clamp(xw);
|
||||
m_whitePoint = std::max(m_whitePoint, m_blackPoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_blackPoint), this);
|
||||
QToolTip::showText(event->globalPosition().toPoint(), QString::number(m_blackPoint), this);
|
||||
break;
|
||||
case 1:
|
||||
m_midPoint = ((qreal)event->x()/width() - m_blackPoint) / (m_whitePoint - m_blackPoint);
|
||||
m_midPoint = (xw - m_blackPoint) / (m_whitePoint - m_blackPoint);
|
||||
m_midPoint = clamp(m_midPoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_midPoint), this);
|
||||
QToolTip::showText(event->globalPosition().toPoint(), QString::number(m_midPoint), this);
|
||||
break;
|
||||
case 2:
|
||||
m_whitePoint = clamp((qreal)event->x()/width());
|
||||
m_whitePoint = clamp(xw);
|
||||
m_blackPoint = std::min(m_blackPoint, m_whitePoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_whitePoint), this);
|
||||
QToolTip::showText(event->globalPosition().toPoint(), QString::number(m_whitePoint), this);
|
||||
break;
|
||||
}
|
||||
if(m_grabbed >= 0)
|
||||
@@ -119,11 +154,18 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void STFSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - event->x()) < 5)
|
||||
const qreal x = event->position().x();
|
||||
if(event->modifiers() & Qt::ShiftModifier)
|
||||
{
|
||||
m_fineTune = true;
|
||||
m_fineTuneX = x/width();
|
||||
}
|
||||
|
||||
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - x) < 5)
|
||||
m_grabbed = 1;
|
||||
else if(std::abs(m_blackPoint*width() - event->x()) < 5)
|
||||
else if(std::abs(m_blackPoint*width() - x) < 5)
|
||||
m_grabbed = 0;
|
||||
else if(std::abs(m_whitePoint*width() - event->x()) < 5)
|
||||
else if(std::abs(m_whitePoint*width() - x) < 5)
|
||||
m_grabbed = 2;
|
||||
else
|
||||
m_grabbed = -1;
|
||||
@@ -132,5 +174,6 @@ void STFSlider::mousePressEvent(QMouseEvent *event)
|
||||
void STFSlider::mouseReleaseEvent(QMouseEvent *)
|
||||
{
|
||||
m_grabbed = -1;
|
||||
m_fineTune = false;
|
||||
emit paramChanged(m_blackPoint, midPoint(), m_whitePoint);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ class STFSlider : public QWidget
|
||||
float m_midPoint;
|
||||
float m_whitePoint;
|
||||
int m_grabbed;
|
||||
bool m_fineTune;
|
||||
float m_fineTuneX;
|
||||
QColor m_color;
|
||||
float m_threshold;
|
||||
public:
|
||||
explicit STFSlider(QWidget *parent = nullptr);
|
||||
explicit STFSlider(const QColor &color = Qt::white, QWidget *parent = nullptr);
|
||||
float blackPoint() const;
|
||||
float midPoint() const;
|
||||
float whitePoint() const;
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#include "stretchpanel.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
#include <QToolButton>
|
||||
#include "imageringlist.h"
|
||||
|
||||
const float BLACK_POINT_SIGMA = -2.8f;
|
||||
const float MAD_TO_SIGMA = 1.4826f;
|
||||
const float TARGET_BACKGROUND = 0.25f;
|
||||
|
||||
float MTF(float x, float m)
|
||||
{
|
||||
if(x < 0)return 0;
|
||||
if(x > 1)return 1;
|
||||
return ((m - 1) * x) / ((2 * m - 1) * x - m);
|
||||
}
|
||||
|
||||
StretchPanel::StretchPanel(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
QHBoxLayout *layout = new QHBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
m_stfSlider = new STFSlider(this);
|
||||
layout->addWidget(m_stfSlider);
|
||||
connect(m_stfSlider, SIGNAL(paramChanged(float, float, float)), this, SIGNAL(paramChanged(float,float,float)));
|
||||
|
||||
QToolButton *autoStretchButton = new QToolButton(this);
|
||||
autoStretchButton->setIcon(QIcon(":/nuke.png"));
|
||||
autoStretchButton->setToolTip(tr("Auto Stretch F12"));
|
||||
autoStretchButton->setShortcut(Qt::Key_F12);
|
||||
connect(autoStretchButton, SIGNAL(pressed()), this, SIGNAL(autoStretch()));
|
||||
|
||||
QToolButton *resetButton = new QToolButton(this);
|
||||
resetButton->setIcon(style()->standardIcon(QStyle::SP_DialogResetButton));
|
||||
resetButton->setToolTip(tr("Reset Screen Transfer Function F11"));
|
||||
resetButton->setShortcut(Qt::Key_F11);
|
||||
connect(resetButton, &QToolButton::pressed, this, &StretchPanel::resetMTF);
|
||||
|
||||
QToolButton *invertButton = new QToolButton(this);
|
||||
invertButton->setIcon(QIcon(":/invert.png"));
|
||||
invertButton->setCheckable(true);
|
||||
connect(invertButton, SIGNAL(toggled(bool)), this, SIGNAL(invert(bool)));
|
||||
|
||||
QToolButton *superPixelButton = new QToolButton(this);
|
||||
superPixelButton->setIcon(QIcon(":/bayer.png"));
|
||||
superPixelButton->setCheckable(true);
|
||||
superPixelButton->setToolTip(tr("Superpixel CFA draw 2x2 pixel as one"));
|
||||
connect(superPixelButton, SIGNAL(toggled(bool)), this, SIGNAL(superPixel(bool)));
|
||||
|
||||
layout->addWidget(autoStretchButton);
|
||||
layout->addWidget(resetButton);
|
||||
layout->addWidget(invertButton);
|
||||
layout->addWidget(superPixelButton);
|
||||
}
|
||||
|
||||
void StretchPanel::stretchImage(Image *img)
|
||||
{
|
||||
if(img)
|
||||
{
|
||||
if(img->rawImage())
|
||||
{
|
||||
double median, mad;
|
||||
img->rawImage()->imageStats(nullptr, nullptr, &median, nullptr, nullptr, &mad);
|
||||
median /= img->rawImage()->norm();
|
||||
mad /= img->rawImage()->norm();
|
||||
float bp = median + mad * BLACK_POINT_SIGMA * MAD_TO_SIGMA;
|
||||
float mid = MTF(median - bp, TARGET_BACKGROUND);
|
||||
m_stfSlider->setMTFParams(bp, mid, 1.0f);
|
||||
emit paramChanged(m_stfSlider->blackPoint(), m_stfSlider->midPoint(), 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StretchPanel::resetMTF()
|
||||
{
|
||||
m_stfSlider->setMTFParams(0, 0.5, 1);
|
||||
emit paramChanged(0, 0.5, 1);
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#ifndef STRETCHPANEL_H
|
||||
#define STRETCHPANEL_H
|
||||
|
||||
#include <QWidget>
|
||||
#include "stfslider.h"
|
||||
|
||||
class Image;
|
||||
|
||||
class StretchPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
STFSlider *m_stfSlider;
|
||||
public:
|
||||
explicit StretchPanel(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
void stretchImage(Image *img);
|
||||
void resetMTF();
|
||||
signals:
|
||||
void paramChanged(float low, float mid, float high);
|
||||
void autoStretch();
|
||||
void invert(bool enable);
|
||||
void superPixel(bool enable);
|
||||
};
|
||||
|
||||
#endif // STRETCHPANEL_H
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "stretchtoolbar.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
#include <QToolButton>
|
||||
#include "imageringlist.h"
|
||||
|
||||
const float BLACK_POINT_SIGMA = -2.8f;
|
||||
const float MAD_TO_SIGMA = 1.4826f;
|
||||
const float TARGET_BACKGROUND = 0.25f;
|
||||
|
||||
float MTF(float x, float m)
|
||||
{
|
||||
if(x < 0)return 0;
|
||||
if(x > 1)return 1;
|
||||
return ((m - 1) * x) / ((2 * m - 1) * x - m);
|
||||
}
|
||||
|
||||
StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar"), parent)
|
||||
{
|
||||
setObjectName("stretchtoolbar");
|
||||
QWidget *lum = new QWidget(this);
|
||||
QVBoxLayout *vbox1 = new QVBoxLayout(lum);
|
||||
m_stfSlider = new STFSlider(Qt::white, this);
|
||||
vbox1->addWidget(m_stfSlider);
|
||||
|
||||
m_stfSliderR = new STFSlider(Qt::red, this);
|
||||
m_stfSliderG = new STFSlider(Qt::green, this);
|
||||
m_stfSliderB = new STFSlider(Qt::blue, this);
|
||||
QWidget *rgb = new QWidget(this);
|
||||
QVBoxLayout *vbox2 = new QVBoxLayout(rgb);
|
||||
vbox2->setSpacing(0);
|
||||
vbox2->addWidget(m_stfSliderR);
|
||||
vbox2->addWidget(m_stfSliderG);
|
||||
vbox2->addWidget(m_stfSliderB);
|
||||
|
||||
m_stack = new QStackedWidget(this);
|
||||
m_stack->addWidget(lum);
|
||||
m_stack->addWidget(rgb);
|
||||
m_stack->setCurrentIndex(0);
|
||||
addWidget(m_stack);
|
||||
|
||||
connect(m_stfSlider, &STFSlider::paramChanged, [this](float blackPoint, float midPoint, float whitePoint){
|
||||
m_mtfParam.blackPoint[0] = m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = blackPoint;
|
||||
m_mtfParam.midPoint[0] = m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = midPoint;
|
||||
m_mtfParam.whitePoint[0] = m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = whitePoint;
|
||||
emit paramChanged(m_mtfParam);
|
||||
});
|
||||
connect(m_stfSliderR, &STFSlider::paramChanged, [this](float blackPoint, float midPoint, float whitePoint){
|
||||
m_mtfParam.blackPoint[0] = blackPoint;
|
||||
m_mtfParam.midPoint[0] = midPoint;
|
||||
m_mtfParam.whitePoint[0] = whitePoint;
|
||||
emit paramChanged(m_mtfParam);
|
||||
});
|
||||
connect(m_stfSliderG, &STFSlider::paramChanged, [this](float blackPoint, float midPoint, float whitePoint){
|
||||
m_mtfParam.blackPoint[1] = blackPoint;
|
||||
m_mtfParam.midPoint[1] = midPoint;
|
||||
m_mtfParam.whitePoint[1] = whitePoint;
|
||||
emit paramChanged(m_mtfParam);
|
||||
});
|
||||
connect(m_stfSliderB, &STFSlider::paramChanged, [this](float blackPoint, float midPoint, float whitePoint){
|
||||
m_mtfParam.blackPoint[2] = blackPoint;
|
||||
m_mtfParam.midPoint[2] = midPoint;
|
||||
m_mtfParam.whitePoint[2] = whitePoint;
|
||||
emit paramChanged(m_mtfParam);
|
||||
});
|
||||
|
||||
QAction *rgbStretch = addAction(QIcon(":/link.png"), tr("Linked channels"));
|
||||
rgbStretch->setCheckable(true);
|
||||
rgbStretch->setChecked(true);
|
||||
connect(rgbStretch, &QAction::toggled, this, &StretchToolbar::unlinkStretch);
|
||||
|
||||
QAction *autoStretchButton = addAction(QIcon(":/nuke.png"), tr("Auto Stretch F12"));
|
||||
autoStretchButton->setShortcut(Qt::Key_F12);
|
||||
connect(autoStretchButton, &QAction::triggered, this, &StretchToolbar::autoStretch);
|
||||
|
||||
QAction *resetButton = addAction(style()->standardIcon(QStyle::SP_DialogResetButton), tr("Reset Screen Transfer Function F11"));
|
||||
resetButton->setShortcut(Qt::Key_F11);
|
||||
connect(resetButton, &QAction::triggered, this, &StretchToolbar::resetMTF);
|
||||
|
||||
QAction *invertButton = addAction(QIcon(":/invert.png"), tr("Invert colors"));
|
||||
invertButton->setCheckable(true);
|
||||
connect(invertButton, &QAction::toggled, this, &StretchToolbar::invert);
|
||||
|
||||
QAction *falseColorButton = addAction(QIcon(":/falsecolor.png"), tr("False colors"));
|
||||
falseColorButton->setCheckable(true);
|
||||
connect(falseColorButton, &QAction::toggled, this, &StretchToolbar::falseColor);
|
||||
|
||||
m_debayer = addAction(QIcon(":/bayer.png"), tr("Debayer CFA"));
|
||||
m_debayer->setCheckable(true);
|
||||
connect(m_debayer, &QAction::toggled, this, &StretchToolbar::superPixel);
|
||||
|
||||
m_autoStretchOnLoad = addAction(QIcon(":/nuke_a.png"), tr("Apply auto stretch on load"));
|
||||
m_autoStretchOnLoad->setCheckable(true);
|
||||
}
|
||||
|
||||
void StretchToolbar::stretchImage(Image *img)
|
||||
{
|
||||
if(img && img->rawImage())
|
||||
{
|
||||
const RawImage::Stats &stats = img->rawImage()->imageStats();
|
||||
int i = 0;
|
||||
int ch = 1;
|
||||
int o = 0;
|
||||
if(m_stack->currentIndex() == 1 && img->rawImage()->channels() == 1 && m_debayer->isChecked())
|
||||
{
|
||||
i = 1;
|
||||
ch = 4;
|
||||
o = 1;
|
||||
}
|
||||
if(img->rawImage()->channels() >= 3)
|
||||
ch = 3;
|
||||
|
||||
float bp2 = 0;
|
||||
float mid2 = 0;
|
||||
float max2 = 0;
|
||||
for(; i < ch; i++)
|
||||
{
|
||||
double median, mad, max;
|
||||
median = stats.m_median[i];
|
||||
mad = stats.m_mad[i];
|
||||
max = stats.m_max[i];
|
||||
median /= img->rawImage()->norm();
|
||||
bool a = median > 0.5 ? true : false;
|
||||
mad /= img->rawImage()->norm();
|
||||
max = 1.0f;// /= img->rawImage()->norm();
|
||||
float bp = a || mad == 0.0f ? 0.0f : std::clamp(median + mad * BLACK_POINT_SIGMA * MAD_TO_SIGMA, 0.0, 1.0);
|
||||
if(a && mad != 0.0f)
|
||||
max = std::clamp(median - mad * BLACK_POINT_SIGMA * MAD_TO_SIGMA, 0.0, 1.0);
|
||||
float mid = !a ? MTF(median - bp, TARGET_BACKGROUND) : MTF(TARGET_BACKGROUND, max - median);
|
||||
m_mtfParam.blackPoint[i-o] = bp;
|
||||
m_mtfParam.midPoint[i-o] = mid;// / max;
|
||||
m_mtfParam.whitePoint[i-o] = max;
|
||||
bp2 += bp;
|
||||
mid2 += mid;
|
||||
max2 = max > max2 ? max : max2;
|
||||
}
|
||||
if(ch == 1)
|
||||
{
|
||||
m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = m_mtfParam.blackPoint[0];
|
||||
m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = m_mtfParam.midPoint[0];
|
||||
m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = m_mtfParam.whitePoint[0];
|
||||
}
|
||||
if(m_stack->currentIndex() == 0)
|
||||
{
|
||||
m_mtfParam.blackPoint[0] = m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = bp2 / ch;
|
||||
m_mtfParam.midPoint[0] = m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = mid2 / ch;
|
||||
m_mtfParam.whitePoint[0] = m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = max2;
|
||||
m_stfSlider->setMTFParams(m_mtfParam.blackPoint[0], m_mtfParam.midPoint[0], m_mtfParam.whitePoint[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_stfSliderR->setMTFParams(m_mtfParam.blackPoint[0], m_mtfParam.midPoint[0], m_mtfParam.whitePoint[0]);
|
||||
m_stfSliderG->setMTFParams(m_mtfParam.blackPoint[1], m_mtfParam.midPoint[1], m_mtfParam.whitePoint[1]);
|
||||
m_stfSliderB->setMTFParams(m_mtfParam.blackPoint[2], m_mtfParam.midPoint[2], m_mtfParam.whitePoint[2]);
|
||||
}
|
||||
emit paramChanged(m_mtfParam);
|
||||
}
|
||||
}
|
||||
|
||||
void StretchToolbar::resetMTF()
|
||||
{
|
||||
MTFParam params;
|
||||
m_mtfParam = params;
|
||||
if(m_stack->currentIndex() == 0)
|
||||
{
|
||||
m_stfSlider->setMTFParams(0, 0.5, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_stfSliderR->setMTFParams(0, 0.5, 1);
|
||||
m_stfSliderG->setMTFParams(0, 0.5, 1);
|
||||
m_stfSliderB->setMTFParams(0, 0.5, 1);
|
||||
}
|
||||
emit paramChanged(params);
|
||||
}
|
||||
|
||||
void StretchToolbar::imageLoaded(Image *img)
|
||||
{
|
||||
if(m_autoStretchOnLoad->isChecked())
|
||||
stretchImage(img);
|
||||
}
|
||||
|
||||
void StretchToolbar::unlinkStretch(bool enable)
|
||||
{
|
||||
if(!enable)
|
||||
{
|
||||
m_stack->setCurrentIndex(1);
|
||||
m_mtfParam.blackPoint[0] = m_stfSliderR->blackPoint(); m_mtfParam.midPoint[0] = m_stfSliderR->midPoint(); m_mtfParam.whitePoint[0] = m_stfSliderR->whitePoint();
|
||||
m_mtfParam.blackPoint[1] = m_stfSliderG->blackPoint(); m_mtfParam.midPoint[1] = m_stfSliderG->midPoint(); m_mtfParam.whitePoint[1] = m_stfSliderG->whitePoint();
|
||||
m_mtfParam.blackPoint[2] = m_stfSliderB->blackPoint(); m_mtfParam.midPoint[2] = m_stfSliderB->midPoint(); m_mtfParam.whitePoint[2] = m_stfSliderB->whitePoint();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_stack->setCurrentIndex(0);
|
||||
m_mtfParam.blackPoint[0] = m_mtfParam.blackPoint[1] = m_mtfParam.blackPoint[2] = m_stfSlider->blackPoint();
|
||||
m_mtfParam.midPoint[0] = m_mtfParam.midPoint[1] = m_mtfParam.midPoint[2] = m_stfSlider->midPoint();
|
||||
m_mtfParam.whitePoint[0] = m_mtfParam.whitePoint[1] = m_mtfParam.whitePoint[2] = m_mtfParam.whitePoint[2] = m_stfSlider->whitePoint();
|
||||
}
|
||||
emit paramChanged(m_mtfParam);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef STRETCHTOOLBAR_H
|
||||
#define STRETCHTOOLBAR_H
|
||||
|
||||
#include <QToolBar>
|
||||
#include <QStackedWidget>
|
||||
#include "stfslider.h"
|
||||
|
||||
class Image;
|
||||
|
||||
struct MTFParam
|
||||
{
|
||||
float blackPoint[3] = {0.0f, 0.0f, 0.0f};
|
||||
float midPoint[3] = {0.5f, 0.5f, 0.5f};
|
||||
float whitePoint[3] = {1.0f, 1.0f, 1.0f};
|
||||
};
|
||||
|
||||
class StretchToolbar : public QToolBar
|
||||
{
|
||||
Q_OBJECT
|
||||
STFSlider *m_stfSlider;
|
||||
STFSlider *m_stfSliderR;
|
||||
STFSlider *m_stfSliderG;
|
||||
STFSlider *m_stfSliderB;
|
||||
QAction *m_autoStretchOnLoad;
|
||||
QAction *m_debayer;
|
||||
QStackedWidget *m_stack;
|
||||
MTFParam m_mtfParam;
|
||||
public:
|
||||
explicit StretchToolbar(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
void stretchImage(Image *img);
|
||||
void resetMTF();
|
||||
void imageLoaded(Image *img);
|
||||
void unlinkStretch(bool enable);
|
||||
signals:
|
||||
void paramChanged(const MTFParam ¶ms);
|
||||
void autoStretch();
|
||||
void invert(bool enable);
|
||||
void superPixel(bool enable);
|
||||
void falseColor(bool enable);
|
||||
};
|
||||
|
||||
#endif // STRETCHTOOLBAR_H
|
||||
|
After Width: | Height: | Size: 882 B |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 686 B |