Compare commits

...

115 Commits

Author SHA1 Message Date
nou 1a220bc3ed Update metainfo 2022-12-28 14:59:17 +01:00
nou f67539a3a1 Update translations 2022-12-28 14:51:45 +01:00
nou 468bcb5abb Add saturation statistic 2022-12-28 14:43:47 +01:00
nou fd49ba9a44 CSV export 2022-12-28 13:24:05 +01:00
nou 22e3b06fdd Fix potentional race conditions 2022-12-28 11:33:26 +01:00
nou 01febbf421 Fix invalid query when no column is selected 2022-12-28 11:17:36 +01:00
nou 7690496cf5 Load XISF::ReadImageProperties 2022-12-26 18:07:16 +01:00
nou 743a5f50c4 Disable manual mipmap generation on ATI cards 2022-12-26 10:19:45 +01:00
nou eac534352f Tidy up some debug outputs 2022-12-26 10:19:20 +01:00
nou 57bdc74ef6 Get rid of QRegExp 2022-12-26 10:13:24 +01:00
nou fd1fd7ff08 Get rid of fullscreen mode 2022-12-25 19:34:09 +01:00
nou c1aca3ca65 Add fine tune for STF slider 2022-12-22 12:45:47 +01:00
nou 5cc8fdd83d Simple grey world white balance 2022-12-20 22:27:15 +01:00
nou 66e13529be Allow change of preload without restart 2022-12-18 10:18:01 +01:00
nou e1bed8e1cb Allow change of thumbnail size without restart 2022-12-18 00:17:28 +01:00
nou 151f521688 Set image width and height for XISF 2022-12-17 09:48:35 +01:00
nou 926647e1a7 Don't do unecessary color profile conversion 2022-12-17 09:47:38 +01:00
nou 71fc1f2bd1 Release 20221215 2022-12-15 20:54:39 +01:00
nou f1ff04382b Add option to not use native file dialogs
Thanks to Patrick Chevalley for updated french tranlation
2022-12-15 20:36:19 +01:00
nou ad91adf1d9 Implement workaround for flatpak QFile::moveToTrash 2022-12-15 17:26:16 +01:00
nou b7369c2501 Remove unused variables in debayer shader 2022-12-15 00:25:00 +01:00
nou 79ed6b2059 Fix error in slovak translation 2022-12-15 00:10:52 +01:00
nou 380974a088 Allow saving debayered image 2022-12-15 00:03:29 +01:00
nou b1ad56ca1f Fix debayer shader 2022-12-15 00:03:14 +01:00
nou 58abef5a72 Update metainfo 2022-12-14 22:00:07 +01:00
nou 5427ff57cb Change superpixel tooltip 2022-12-14 21:58:24 +01:00
nou 6a2fa3f656 Update translation 2022-12-14 21:51:19 +01:00
nou c62ec7db8c Add move to trash action 2022-12-14 21:51:05 +01:00
nou d5eb0fdce5 Change key shortcuts 2022-12-14 21:50:30 +01:00
nou 6d25919e1f Add image formats to help 2022-12-14 21:50:05 +01:00
nou 26d1af6077 Add debayer 2022-12-14 21:18:25 +01:00
nou 44d8a8b856 Fix invert button 2022-12-14 17:42:13 +01:00
nou dab6c1f79d Add All files filter to open dialog 2022-12-14 17:16:32 +01:00
nou ce6a4cc40c Refresh dir when show hidden files is toggled 2022-12-13 21:31:23 +01:00
nou 0368c1f1dc Update metainfo xml 2022-12-10 07:48:17 +01:00
nou a1e98d818b Fix copy on btrfs 2022-12-09 19:58:15 +01:00
nou f3f194bcef Update translation and metainfo 2022-12-09 19:33:26 +01:00
nou efd36f96c3 Use native dialogs 2022-12-09 18:38:07 +01:00
nou 37923b37b3 Add error message for copy/move 2022-12-09 18:36:43 +01:00
nou 900453577e Fix includes 2022-11-28 17:36:37 +01:00
nou 34d466c3e0 Show checker pattern with transparent files 2022-11-28 17:32:23 +01:00
nou 9e98127084 Do gamma conversion manualy
Requesting sRGB capable framebuffer is unreliable
2022-11-28 17:31:50 +01:00
nou ba6062b925 Enale loading all image types that Qt can load 2022-11-27 21:10:43 +01:00
nou 6411b7cd15 Release 20221126 2022-11-26 12:03:36 +01:00
nou 223f7cd0ea Refractor save dialog 2022-11-26 11:02:29 +01:00
nou f8f9ee08b3 Add QFileDialog::DontUseNativeDialog 2022-11-22 11:04:04 +01:00
nou af5aed7ef8 Install metainfo generally 2022-11-21 19:09:19 +01:00
nou 8f5249b142 Add metainfo file 2022-11-21 17:44:48 +01:00
nou a7dc942c62 Add scalable icon 2022-11-21 15:49:09 +01:00
nou 1a1399434b Change domain name 2022-11-21 13:18:46 +01:00
nou be567841bf Workaround in AMD OpenGL driver bug
AMD OpenGL driver on Windows doesn't generate mipmaps for sRGB textures
correctly
2022-10-26 23:32:22 +02:00
nou 62d2671112 Update french translation 2022-10-23 12:11:40 +02:00
nou 1f8923512e Update translations 2022-10-23 10:18:21 +02:00
nou 455c3b2d64 Make it combilable with Qt 5.13 and older 2022-10-23 09:54:23 +02:00
nou 4fe546f0e5 Add support for ICC color profiles 2022-10-18 21:19:05 +02:00
nou 95c6fc5343 Enable sRGB to gamma correct scaling 2022-10-16 11:05:15 +02:00
nou 2bc54ea0cc Fix loading RAW on MacOS
LibRaw object is too big for stack so needs to be on heap
2022-10-15 09:56:14 +02:00
nou be6e472081 Settings dialog 2022-10-10 10:30:25 +02:00
nou 9746f8f653 Add option to show hidden files 2022-08-29 18:37:51 +02:00
nou b51a305c63 Remove unused method 2022-08-29 18:29:00 +02:00
nou 39775b5e98 Scale float images to 0,1 range on load 2022-07-22 11:36:10 +02:00
nou 93b56e2966 Workaround for QTBUG-87332 2022-07-03 13:40:54 +02:00
nou 2e41464ff4 Update build instructions 2022-06-27 10:26:27 +02:00
nou b6ae7d4cdb Update french help, fix few typo. Thanks to Patrick Chevalley 2022-06-24 21:14:08 +02:00
nou 1bd48e8fb4 Refres database table when indexing is done 2022-06-24 18:30:28 +02:00
nou 1d65eda490 Search with CRVAL# 2022-06-24 18:06:26 +02:00
nou 97346df596 Update help 2022-06-24 18:05:33 +02:00
nou a157b274a2 Save to database CRVALi 2022-06-23 16:57:00 +02:00
nou 6466702819 Set correct type to vertex attribute 2022-06-22 23:43:56 +02:00
nou b4ea65b42a Upload sizes to OpenGL only once per draw
Should fix MacOS issue
2022-06-22 23:24:29 +02:00
nou 1682de4e1b Update translations 2022-06-22 23:01:35 +02:00
nou 00872b31df Add icon for MacOS 2022-06-21 09:12:26 +02:00
nou 2884787916 Changes to build on MacOS 2022-06-20 23:52:09 +02:00
nou 86ea9fc137 Add MacOS PCL libs 2022-06-20 23:36:31 +02:00
nou 67199a033d Better status bar 2022-06-17 13:24:52 +02:00
nou 0f182900c2 Fix build on older gcc 2022-06-17 09:54:50 +02:00
nou 19ed5ae1a4 Better handling of FITS records 2022-06-17 00:31:27 +02:00
nou 46215c7a7d Workaround for incorect handling of PV1_2 2022-06-16 23:46:54 +02:00
nou c346487504 Add parsing WCS info from XISF 2022-06-16 23:44:28 +02:00
nou 5b6fead6f1 Fix issue with numeric values in XISF FITS header 2022-06-15 08:55:44 +02:00
nou 3e94aa0eda Add search by RA and DEC point 2022-06-14 22:46:40 +02:00
nou 08e70cdb52 Calculate bounds on indexing 2022-06-14 21:44:32 +02:00
nou 04e587b51c Add WCSData::calculateBounds 2022-06-13 23:28:39 +02:00
nou 42dd55244a Add wcslib as lib name 2022-06-13 18:50:57 +02:00
nou 6b9ea5e4b9 Add support for WCS 2022-06-13 18:02:58 +02:00
nou 701a425cc7 Add support for 16 bit PNG images 2022-06-11 22:39:06 +02:00
nou 9cd2ae14b3 Add status bar with color value 2022-06-11 17:19:03 +02:00
nou f7e4e1874f Proper filter setting 2022-06-11 14:10:07 +02:00
nou e6749fc487 Move shaders to subdirectory 2022-06-11 12:27:15 +02:00
nou dc6aa6baa8 Fix bug with wayland backend 2022-06-09 23:30:23 +02:00
nou c8a70d22f8 Show marked files in file list bold 2022-06-04 15:59:51 +02:00
nou dbb533176c Remove unecessary call 2022-06-04 08:10:47 +02:00
nou 032f5b0577 Improve file selection in file system widget 2022-06-03 10:51:51 +02:00
nou eb417010c3 Set max value in autostretch 2022-06-02 15:46:06 +02:00
nou 5b44d2ac69 Add support for NEF, DNG 2022-06-02 15:41:43 +02:00
nou d1df789691 French help and updated translations 2022-05-22 18:49:21 +02:00
nou ab7d04b625 Workaround for unsupported QSqlQuery::size() 2022-05-21 14:53:54 +02:00
nou a4cfc65d4b Wrap reindex into transaction 2022-05-20 22:57:36 +02:00
nou b4746be190 Update help about marking images 2022-05-20 22:41:00 +02:00
nou 9ceb7556f9 Add marking and unmark from thumbnails view 2022-05-20 22:27:53 +02:00
nou 41b29f0701 Show marked files in database view 2022-05-20 12:10:06 +02:00
nou 67ae2d4b62 Mark unmark files from database view 2022-05-20 11:07:18 +02:00
nou b6b6863331 Add override keyword 2022-05-20 10:34:23 +02:00
nou 571fa57af2 Update translations 2022-05-19 09:58:01 +02:00
nou abb3d631bf Double click in file tree open file 2022-05-19 09:51:47 +02:00
nou b65911943e Fix in context menu 2022-05-19 09:47:42 +02:00
nou 9d9f8db499 Fix crash 2022-05-18 17:43:19 +02:00
nou 3060b17c0c Translations update 2022-05-18 17:39:59 +02:00
nou fcf336d63a Second call to QTranslator::load() seem to clear translation 2022-05-18 17:34:15 +02:00
nou 54ef8e990c Selecting thumbnails 2022-05-18 17:33:20 +02:00
nou 94466a6b9b Draw file name under thumbnail 2022-05-09 22:56:06 +02:00
nou b84d8127ad Add copy, move and index action to File tree 2022-05-09 18:14:10 +02:00
nou c971a919ec Add Filetree dock 2022-05-09 15:58:23 +02:00
nou 8c248b7cfc Add French tranlation, credit Patrick Chevalley 2022-05-08 00:18:42 +02:00
nou 43b510a78c Try load translation from application dir 2022-05-08 00:16:11 +02:00
61 changed files with 3354 additions and 403 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+39 -13
View File
@@ -19,11 +19,13 @@ 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 REQUIRED)
find_library(WCS_LIB wcs wcslib PATHS REQUIRED)
set(TENMON_SRC
about.cpp
database.cpp
databaseview.cpp
delete.cpp
filesystemwidget.cpp
imageinfo.cpp
imageringlist.cpp
@@ -34,53 +36,77 @@ set(TENMON_SRC
mainwindow.cpp
markedfiles.cpp
rawimage.cpp
settingsdialog.cpp
starfit.cpp
statusbar.cpp
stfslider.cpp
stretchtoolbar.cpp
)
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
set(COLOR_MANAGMENT OFF)
endif(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
if(COLOR_MANAGMENT)
add_compile_definitions("COLOR_MANAGMENT")
endif(COLOR_MANAGMENT)
qt5_add_resources(TENMON_SRC resources.qrc)
if(WIN32)
list(APPEND TENMON_SRC icon.rc)
add_compile_definitions("__PCL_WINDOWS")
set(tenmon_ICON "")
elseif(APPLE)
add_compile_definitions("__PCL_MACOS")
set(tenmon_ICON ${CMAKE_CURRENT_SOURCE_DIR}/tenmon.icns)
set_source_files_properties(${tenmon_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
else()
add_compile_definitions("__PCL_LINUX")
set(tenmon_ICON "")
find_package(PkgConfig REQUIRED)
pkg_search_module(GIO REQUIRED gio-2.0)
endif()
add_executable(tenmon WIN32 ${TENMON_SRC})
add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
find_path(FITS_INCLUDE fitsio2.h PATH_SUFFIXES cfitsio REQUIRED)
target_include_directories(tenmon PRIVATE ${OpenCV_INCLUDE_DIRS} ${FITS_INCLUDE} 3rdparty/include ${CMAKE_BINARY_DIR})
if(WIN32)
target_link_directories(tenmon PRIVATE 3rdparty/lib/Windows)
elseif(APPLE)
target_link_directories(tenmon PRIVATE 3rdparty/lib/MacOS)
else()
target_link_directories(tenmon PRIVATE 3rdparty/lib/Linux)
target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS})
endif()
target_link_libraries(tenmon Qt5::Widgets Qt5::Sql ${OpenCV_LIBS} ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB})
target_link_libraries(tenmon PCL lcms lz4 RFC6234 zlib)
target_link_libraries(tenmon Qt5::Widgets Qt5::Sql ${OpenCV_LIBS} ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB})
if(APPLE)
target_link_libraries(tenmon PCL-pxi lcms-pxi lz4-pxi RFC6234-pxi zlib-pxi "-framework CoreFoundation")
else()
target_link_libraries(tenmon PCL lcms lz4 RFC6234 zlib ${GIO_LDFLAGS})
endif(APPLE)
if(LIBRAW_STATIC)
add_compile_definitions("LIBRAW_NODLL")
target_link_libraries(tenmon jasper)
endif()
install(TARGETS tenmon)
if(UNIX)
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()
if(DEFINED ENV{FLATPAK_DEST})
install(FILES org.nou.tenmon.desktop DESTINATION "$ENV{FLATPAK_DEST}/share/applications")
install(FILES org.nou.tenmon.png DESTINATION "$ENV{FLATPAK_DEST}/share/icons/hicolor/32x32/apps")
else()
install(FILES org.nou.tenmon.desktop DESTINATION "/usr/share/applications")
install(FILES org.nou.tenmon.png DESTINATION "/usr/share/icons/hicolor/32x32/apps")
endif()
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
install(FILES space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
install(FILES space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
endif()
endif(UNIX)
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)
+16 -3
View File
@@ -2,10 +2,23 @@ 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 libgsl-dev cmake
sudo apt install qtbase5-dev libraw-dev libexif-dev libcfitsio-dev libgsl-dev wcslib-dev libopencv-dev cmake
on OpenSUSE
sudo zypper install opencv-devel gsl-devel exif-devel libraw-devel wcslib-devel libqt5-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 qt5 libraw cfitsio libexif libgsl wcslib opencv
You may need to set CMAKE_PREFIX_PATH for Qt5 and OpenCV so CMake can find them.
Then to build run standard cmake
cmake -B build -S .
make
./tenmon
cmake --build build
./build/tenmon
+26 -1
View File
@@ -13,7 +13,8 @@ img { margin: 5px; }
<ul>
<li>FITS 8, 16 bit integer and 32 bit float</li>
<li>XISF 8, 16 bit integer and 32 bit float</li>
<li>JPEG and PNG images</li>
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
<li>CR2, NEF, DNG raw images</li>
</ul>
</p>
@@ -23,6 +24,8 @@ The <i>File system</i> panel shows other images in the same directory as the loa
<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
@@ -65,6 +68,20 @@ Following the slider are 5 buttons for automatic stretching:
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>
@@ -78,6 +95,14 @@ 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>
+106
View File
@@ -0,0 +1,106 @@
<!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, 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>
<p><small>PS: Le Kanji de icône (tenmon) signifie astronomie en japonais</small></p>
</body>
</html>
+6 -1
View File
@@ -12,7 +12,8 @@ p { padding:0px; margin:5px 5px 10px 5px; }
<ul>
<li>FITS 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
<li>XISF 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
<li>JPEG a PNG obrázky</li>
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM a SVG obrázky</li>
<li>CR2, NEF, DNG raw obrázky</li>
</ul>
</p>
@@ -59,6 +60,10 @@ Posledné tlačidlo zapína a vypína nastavovanie optimálnych hodnôt úrovní
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á
+1 -1
View File
@@ -1,5 +1,5 @@
<table><tr>
<td style="padding-right:10px"><img src=":/org.nou.tenmon.png"></td>
<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>
+96 -20
View File
@@ -22,16 +22,32 @@ bool Database::init()
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)");
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)");
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)
@@ -45,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);
@@ -55,7 +73,6 @@ bool Database::init()
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
return true;
}
qDebug() << error.text();
}
}
@@ -76,6 +93,20 @@ bool Database::unmark(const QString &filename)
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);
@@ -94,7 +125,6 @@ QStringList Database::getMarkedFiles()
files << markedFiles.value("file").toString();
}
qDebug() << files.size();
return files;
}
@@ -115,6 +145,15 @@ bool Database::checkError(QSqlQuery &query)
}
}
int Database::checkVersion()
{
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, int count = 0)
@@ -134,17 +173,25 @@ void Database::indexDir(const QDir &dir, QProgressDialog *progress)
QSqlDatabase database = QSqlDatabase::database();
database.transaction();
if(indexDir2(dir, progress))
{
database.commit();
emit databaseChanged();
}
else
{
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");
progress->setMaximum(files.size());
int i = 0;
while(files.next())
{
@@ -155,10 +202,16 @@ void Database::reindex(QProgressDialog *progress)
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()
@@ -172,7 +225,6 @@ QStringList Database::getFitsKeywords()
return keywords;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
{
QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
@@ -219,16 +271,40 @@ bool Database::indexFile(const QFileInfo &file)
qlonglong last_id = -1;
if(ok)
{
m_insertFile.bindValue(0, filePath);
m_insertFile.bindValue(1, mtime);
if(!m_insertFile.exec())
if(info.wcs)
{
qDebug() << m_insertFile.lastError();
return false;
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())
{
qDebug() << "Database error" << m_insertFileWcs.lastError();
return false;
}
last_id = m_insertFileWcs.lastInsertId().toLongLong();
}
last_id = m_insertFile.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();
}
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);
@@ -241,10 +317,10 @@ bool Database::indexFile(const QFileInfo &file)
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;
qDebug() << "Indexed" << filePath << last_id;
return true;
}
+6
View File
@@ -15,6 +15,7 @@ class Database : public QObject
QSqlQuery m_isMarkedQuery;
QSqlQuery m_insertFile;
QSqlQuery m_insertFileWcs;
QSqlQuery m_insertFitsHeader;
QSqlQuery m_checkFile;
QSqlQuery m_headerKeywords;
@@ -26,6 +27,8 @@ public:
bool init();
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();
@@ -37,6 +40,9 @@ protected:
bool indexDir2(const QDir &dir, QProgressDialog *progress);
bool indexFile(const QFileInfo &file);
bool checkError(QSqlQuery &query);
int checkVersion();
signals:
void databaseChanged();
};
#endif // DATABASE_H
+195 -13
View File
@@ -6,10 +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);
@@ -47,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)
@@ -71,31 +97,79 @@ void FITSFileModel::setColumns(const QStringList &columns)
prepareQuery();
}
void FITSFileModel::setFilter(const QStringList &key, const QStringList &value)
void FITSFileModel::setFilter(const QStringList &key, const QStringList &value, const QStringList &limit)
{
if(value.isEmpty())
{
m_key.clear();
m_value.clear();
m_limit.clear();
}
else
{
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;
}
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 cols;
QString join;
QString where;
QString sql = "SELECT f.file,";
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 = QString(" WHERE f.file LIKE '%1'").arg(m_value[i]);
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]);
}
@@ -110,7 +184,7 @@ void FITSFileModel::prepareQuery()
sql += cols;
sql += " FROM fits_files AS f";
sql += join;
sql += where;
if(!where.isEmpty())sql += " WHERE " + where.join("AND");
sql += " GROUP BY f.id" + m_sort;
setQuery(sql);
setHeaderData(0, Qt::Horizontal, tr("File name"));
@@ -121,7 +195,32 @@ void FITSFileModel::prepareQuery()
}
std::cout << sql.toStdString() << std::endl;
if(lastError().type() != QSqlError::NoError)
qDebug() << lastError();
qDebug() << "Database error" << lastError();
m_markedFiles = m_database->getMarkedFiles().toSet();
}
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)
@@ -130,14 +229,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);
@@ -151,21 +251,67 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
hlayout->addWidget(selectColumnsButton);
connect(selectColumnsButton, &QPushButton::pressed, this, &DataBaseView::selectColumns);
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);
});
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);
m_filterKeyword[i]->addItem("file");
m_filterKeyword[i]->addItems(m_database->getFitsKeywords());
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(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()
@@ -203,6 +349,7 @@ void DataBaseView::applyFilter()
{
QStringList keys;
QStringList values;
QStringList limits;
for(int i=0; i<3; i++)
{
QString key = m_filterKeyword[i]->currentText();
@@ -210,8 +357,43 @@ void DataBaseView::applyFilter()
{
keys.append(key);
values.append(m_search[i]->text());
limits.append(m_limit[i]->text());
}
}
m_model->setFilter(keys, values);
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;
}
+26 -6
View File
@@ -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();
};
@@ -27,31 +27,51 @@ class FITSFileModel : public QSqlQueryModel
QString m_sort;
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 QStringList &key, const QStringList &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[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);
};
+30
View 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
+116 -10
View File
@@ -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,126 @@ FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) :
setLayout(layout);
connect(m_listView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(fileClicked(QModelIndex)));
}
void FilesystemWidget::setDir(const QString &dir)
{
//m_model->setRootPath(dir);
//m_treeView->setRootIndex(m_model->index(m_model->rootPath()));
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilesystemWidget::fileClicked);
}
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());
}
Filetree::Filetree(QWidget *parent) : QTreeView(parent)
{
QSettings settings;
m_rootDir = settings.value("filetree/rootDir", QDir::homePath()).toString();
m_fileSystemModel = new QFileSystemModel(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);
}
+19 -2
View File
@@ -4,6 +4,7 @@
#include <QWidget>
#include <QFileSystemModel>
#include <QListView>
#include <QTreeView>
class FilesystemWidget : public QWidget
{
@@ -12,12 +13,28 @@ class FilesystemWidget : public QWidget
QAbstractItemModel *m_model;
public:
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
void setDir(const QString &dir);
private slots:
void selectFile(int row);
void fileClicked(const QModelIndex &index);
void fileClicked(const QModelIndex &index, const QModelIndex &);
signals:
void fileSelected(int row);
};
class Filetree : public QTreeView
{
Q_OBJECT
QFileSystemModel *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
-27
View File
@@ -1,27 +0,0 @@
#version 330
uniform sampler2D qt_Texture0;
uniform vec3 mtf_param;
uniform bool bw;
uniform bool invert;
in vec2 qt_TexCoord0;
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 = texture(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);
}
+262
View File
@@ -1,6 +1,11 @@
#include "imageinfo.h"
#include <QSettings>
#include <QTime>
#include <QHeaderView>
#include <wcslib/wcshdr.h>
#include <wcslib/wcsfix.h>
#include "pcl/FITSHeaderKeyword.h"
#include "pcl/Property.h"
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
@@ -9,6 +14,76 @@ 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 pcl::FITSHeaderKeyword &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 pcl::Property &property)
{
key = property.Identifier().c_str();
const pcl::Variant &pclval = property.Value();
switch(pclval.Type())
{
case pcl::VariantType::Bool:
value = pclval.ToBool();
break;
case pcl::VariantType::Int8:
case pcl::VariantType::Int16:
case pcl::VariantType::Int32:
case pcl::VariantType::Int64:
value = pclval.ToInt64();
break;
case pcl::VariantType::UInt8:
case pcl::VariantType::UInt16:
case pcl::VariantType::UInt32:
case pcl::VariantType::UInt64:
value = pclval.ToUInt64();
break;
case pcl::VariantType::Float:
case pcl::VariantType::Double:
value = pclval.ToDouble();
break;
default:
value = pclval.ToIsoString().c_str();
break;
}
}
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);
@@ -47,3 +122,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::modf(dec, &deg) * 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');
}
+43 -1
View File
@@ -2,6 +2,11 @@
#define IMAGEINFO_H
#include <QTreeWidget>
#include <wcslib/wcs.h>
#include <cmath>
#include <memory>
namespace pcl { class FITSHeaderKeyword; class Property; }
struct FITSRecord
{
@@ -9,12 +14,49 @@ struct FITSRecord
QVariant value;
QByteArray comment;
bool editable() const;
FITSRecord(){}
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
FITSRecord(const pcl::FITSHeaderKeyword &record);
FITSRecord(const pcl::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 +74,7 @@ class ImageInfo : public QTreeWidget
Q_OBJECT
public:
explicit ImageInfo(QWidget *parent);
~ImageInfo();
~ImageInfo() override;
public slots:
void setInfo(const ImageInfoData &info);
};
+105 -30
View File
@@ -3,10 +3,11 @@
#include <QDir>
#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, int number, ImageRingList *ringList) :
m_loading(false),
@@ -50,9 +51,9 @@ 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
@@ -75,6 +76,11 @@ int Image::number() const
return m_number;
}
void Image::clearThumbnail()
{
m_thumbnail.reset();
}
void Image::imageLoaded(void *rawImage, ImageInfoData info)
{
m_loading = false;
@@ -97,11 +103,14 @@ void Image::thumbnailLoadFinish(void *rawImage)
emit thumbnailLoaded(this);
}
ImageRingList::ImageRingList(QObject *parent) : QAbstractItemModel(parent)
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);
}
@@ -120,10 +129,7 @@ bool ImageRingList::setDir(const QString path, const QString &currentFile)
if(dir.exists())
{
QStringList nameFilter;
nameFilter << "*.jpg" << "*.jpeg" << "*.png" << "*.cr2" << "*.fit" << "*.fits" << "*.xisf";
QStringList list = dir.entryList(nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name | QDir::IgnoreCase);
QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name | QDir::IgnoreCase);
QStringList absolutePaths;
foreach(const QString &file, list)
{
@@ -210,30 +216,38 @@ 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);
}
}
}
}
@@ -250,6 +264,7 @@ void ImageRingList::loadThumbnails()
void ImageRingList::stopLoading()
{
m_thumbPool->clear();
m_thumbPool->waitForDone();
}
int ImageRingList::imageCount() const
@@ -257,6 +272,29 @@ 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
{
return createIndex(row, column, m_images.at(row).get());
@@ -289,6 +327,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();
}
@@ -303,6 +348,35 @@ 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::setFiles(const QStringList files, const QString &currentFile)
{
QThreadPool::globalInstance()->clear();
@@ -325,6 +399,7 @@ void ImageRingList::setFiles(const QStringList files, const QString &currentFile
index = 0;
endResetModel();
m_currImage = m_images.end();
loadFile(index);
}
+14 -4
View File
@@ -19,7 +19,7 @@ class Image : public QObject
bool m_released;
bool m_current;
int m_number;
std::unique_ptr<RawImage> m_rawImage;
std::shared_ptr<RawImage> m_rawImage;
std::unique_ptr<RawImage> m_thumbnail;
QString m_name;
ImageInfoData m_info;
@@ -30,11 +30,12 @@ public:
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);
@@ -45,6 +46,8 @@ protected slots:
typedef std::shared_ptr<Image> ImagePtr;
class Database;
class ImageRingList : public QAbstractItemModel
{
Q_OBJECT
@@ -57,9 +60,11 @@ class ImageRingList : public QAbstractItemModel
bool m_liveMode;
AnalyzeLevel m_analyzeLevel;
QThreadPool *m_thumbPool;
Database *m_database;
QStringList m_nameFilter;
public:
explicit ImageRingList(QObject *parent = 0);
~ImageRingList();
explicit ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent = 0);
~ImageRingList() override;
bool setDir(const QString path, const QString &currentFile = QString());
void setFile(const QString &file);
ImagePtr currentImage();
@@ -74,6 +79,9 @@ public:
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;
@@ -81,6 +89,8 @@ 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);
protected:
void setFiles(const QStringList files, const QString &currentFile = QString());
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
+6 -6
View File
@@ -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
+333 -63
View File
@@ -9,6 +9,9 @@
#include <QMimeData>
#include <QMessageBox>
#include <QCoreApplication>
#include <QPainter>
#include <QFileInfo>
#include <cmath>
struct RawImageType
{
@@ -22,12 +25,20 @@ 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},
#ifdef COLOR_MANAGMENT
{QOpenGLTexture::RGB, QOpenGLTexture::SRGB8, QOpenGLTexture::UInt8, false},
{QOpenGLTexture::RGBA,QOpenGLTexture::SRGB8_Alpha8, QOpenGLTexture::UInt8, false},
#else
{QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
{QOpenGLTexture::BGRA,QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
{QOpenGLTexture::RGBA,QOpenGLTexture::RGBA8_UNorm, QOpenGLTexture::UInt8, false},
#endif
{QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
{QOpenGLTexture::RGBA, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
{QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
};
static bool MANUAL_MIPMAP_GEN = false;
void setScrollRange(QScrollBar *scrollBar, int newRange)
{
int page = scrollBar->pageStep();
@@ -44,7 +55,8 @@ void setScrollRange(QScrollBar *scrollBar, int newRange)
scrollBar->setValue(relPos*newRange - page/2);
}
ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
ImageWidget::ImageWidget(Database *database, QWidget *parent) : QOpenGLWidget(parent)
, m_database(database)
{
setFocusPolicy(Qt::ClickFocus);
m_range = UINT16_MAX;
@@ -58,10 +70,12 @@ ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
m_imgWidth = m_imgHeight = -1;
m_superpixel = m_invert = false;
m_showThumbnails = false;
m_selecting = false;
m_thumbnailCount = 0;
m_updateTimer = new QTimer(this);
m_updateTimer->setInterval(500);
m_updateTimer->setSingleShot(true);
m_sizesDirty = false;
connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(update()));
setAcceptDrops(true);
QTimer::singleShot(1000, [this](){
@@ -71,6 +85,8 @@ ImageWidget::ImageWidget(QWidget *parent) : QOpenGLWidget(parent)
QCoreApplication::exit(-1);
}
});
setMouseTracking(true);
}
ImageWidget::~ImageWidget()
@@ -78,16 +94,24 @@ ImageWidget::~ImageWidget()
makeCurrent();
}
void ImageWidget::setImage(const RawImage *image)
void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
{
if(image == nullptr)return;
makeCurrent();
m_rawImage = image;
m_imgWidth = image->width();
m_imgHeight = image->height();
m_currentImg = index;
if(!m_image)return;
const RawImageType &rawImageType = rawImageTypes[image->type()];
m_srgb = rawImageType.textureFormat == QOpenGLTexture::SRGB8 || rawImageType.textureFormat == QOpenGLTexture::SRGB8_Alpha8;
m_bwImg = rawImageType.bw;
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; }());
@@ -95,27 +119,53 @@ 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->generateMipMaps();
m_bwImg = rawImageType.bw;
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, (const void*)image->data(), m_transferOptions.get());
auto sRGB_linear = [](cv::Point3f &pixel, const int *pos)
{
pixel.x = pixel.x <= 0.04045f ? pixel.x / 12.92f : std::pow((pixel.x + 0.055) / 1.055f, 2.4f);
pixel.y = pixel.y <= 0.04045f ? pixel.y / 12.92f : std::pow((pixel.y + 0.055) / 1.055f, 2.4f);
pixel.z = pixel.z <= 0.04045f ? pixel.z / 12.92f : std::pow((pixel.z + 0.055) / 1.055f, 2.4f);
};
auto linear_sRGB = [](cv::Point3f &pixel, const int *pos)
{
pixel.x = pixel.x <= 0.0031308f ? pixel.x * 12.92f : 1.055f * std::pow(pixel.x , 1/2.4f) - 0.055f;
pixel.y = pixel.y <= 0.0031308f ? pixel.y * 12.92f : 1.055f * std::pow(pixel.y , 1/2.4f) - 0.055f;
pixel.z = pixel.z <= 0.0031308f ? pixel.z * 12.92f : 1.055f * std::pow(pixel.z , 1/2.4f) - 0.055f;
};
//AMD OpenGL driver on Windows doesn't generate mipmaps for sRGB textures correctly
if(m_srgb && MANUAL_MIPMAP_GEN)
{
cv::Mat img = image->mat();
img.convertTo(img, CV_32FC3, 1/255.0);
img.forEach<cv::Point3f>(sRGB_linear);
cv::Size size(img.cols, img.rows);
for(int i=1; i<m_image->mipLevels(); i++)
{
cv::Mat mip;
size /= 2;
cv::resize(img, mip, size);
mip.copyTo(img);
mip.forEach<cv::Point3f>(linear_sRGB);
mip.convertTo(mip, CV_8UC3, 255);
m_image->setData(i, rawImageType.pixelFormat, rawImageType.dataType, (const void*)mip.ptr(), m_transferOptions.get());
}
}
else m_image->generateMipMaps();
if(m_debayerTex)
{
f->glDeleteTextures(1, &m_debayerTex);
m_debayerTex = 0;
}
update();
}
void ImageWidget::setImage(const QPixmap &pixmap)
void ImageWidget::setWCS(std::shared_ptr<WCSData> wcs)
{
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;
update();
m_wcs = wcs;
}
void ImageWidget::setScale(float scale)
@@ -130,20 +180,24 @@ void ImageWidget::blockRepaint(bool block)
if(!block)update();
}
void ImageWidget::allocateThumbnails(int count)
void ImageWidget::allocateThumbnails(const QStringList &paths)
{
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(count);
m_thumbnailTexture->setLayers(paths.size());
m_thumbnailTexture->allocateStorage();
m_bufferSizes->bind();
float *tmp = new float[count*3];
memset(tmp, 0, count * sizeof(float)*3);
m_bufferSizes->allocate(tmp, count * sizeof(float)*3);
delete [] tmp;
m_thumbnailCount = count;
}
void ImageWidget::setMTFParams(float low, float mid, float high)
@@ -164,7 +218,6 @@ void ImageWidget::setOffset(int dx, int dy)
void ImageWidget::superPixel(bool enable)
{
m_superpixel = enable;
m_image->setLevelOfDetailRange(enable ? 1 : 0, m_image->mipMaxLevel());
update();
}
@@ -188,7 +241,10 @@ 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();
@@ -201,8 +257,8 @@ void ImageWidget::thumbnailLoaded(const Image *image)
m_thumbnailTexture->setData(0, image->number(), QOpenGLTexture::RGB, 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_bufferSizes->bind();
m_bufferSizes->write(image->number() * sizeof(sizes), sizes, sizeof(sizes));
m_sizesDirty = true;
m_thumnails[image->number()].size = QSize(sizes[0], sizes[1]);
if(!m_updateTimer->isActive())m_updateTimer->start();
}
@@ -222,33 +278,88 @@ void ImageWidget::paintGL()
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;
QBrush highlight = style()->standardPalette().highlight();
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->setUniformValue("mtf_param", m_low, m_mid, m_high);
m_thumbnailProgram->setUniformValue("invert", m_invert);
m_thumbnailProgram->setUniformValue("offset", 0, m_dy);
f3->glVertexAttribDivisor(m_thumbnailProgram->attributeLocation("imageSize_num"), 1);
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;
for(int i=0; i < m_thumbnailCount; 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
{
debayer();
m_vao->bind();
m_image->bind(0);
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", 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("bw", m_bwImg && !m_superpixel);
m_program->setUniformValue("invert", m_invert);
if(m_superpixel)m_program->setUniformValue("whiteBalance", m_whiteBalance[0], m_whiteBalance[1], m_whiteBalance[2]);
else m_program->setUniformValue("whiteBalance", 1.0f, 1.0f, 1.0f);
#ifdef COLOR_MANAGMENT
m_program->setUniformValue("srgb", m_srgb);
#endif
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
@@ -288,6 +399,8 @@ void ImageWidget::initializeGL()
qDebug() << (char*)f->glGetString(GL_RENDERER);
qDebug() << (char*)f->glGetString(GL_VERSION);
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
qDebug() << context()->format();
// each vertex is x,y 2D position and s,t texture coordinates
@@ -319,12 +432,27 @@ 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, ":/shaders/debayer.vert");
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/debayer.frag");
if(f3)f3->glBindFragDataLocation(m_debayerProgram->programId(), 0, "color");
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, ":/shaders/thumb.vert");
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/thumb.frag");
if(f3)f3->glBindFragDataLocation(m_program->programId(), 0, "color");
if(f3)f3->glBindFragDataLocation(m_thumbnailProgram->programId(), 0, "color");
m_thumbnailProgram->bind();
m_thumbnailProgram->enableAttributeArray("qt_Vertex");
m_thumbnailProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
@@ -340,10 +468,10 @@ void ImageWidget::initializeGL()
m_bufferSizes->setUsagePattern(QOpenGLBuffer::StaticDraw);
m_bufferSizes->create();
m_bufferSizes->bind();
m_bufferSizes->allocate(12);
m_thumbnailProgram->enableAttributeArray("imageSize_num");
m_thumbnailProgram->setAttributeBuffer("imageSize_num", GL_FLOAT, 0, 3);
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));
@@ -359,11 +487,13 @@ void ImageWidget::initializeGL()
m_thumbnailTexture->setLayers(1);
m_thumbnailTexture->allocateStorage();
m_thumbnailTexture->bind(1);
m_thumbnailTexture->setMinificationFilter(QOpenGLTexture::Linear);
m_thumbnailTexture->setMinificationFilter(QOpenGLTexture::Linear);
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);
}
void ImageWidget::dragEnterEvent(QDragEnterEvent *event)
@@ -389,12 +519,157 @@ void ImageWidget::dropEvent(QDropEvent *event)
event->ignore();
}
ImageScrollAreaGL::ImageScrollAreaGL(QWidget *parent) : QWidget(parent)
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
event->ignore();
}
void ImageWidget::mouseMoveEvent(QMouseEvent *event)
{
if(m_selecting)
{
thumbSelect(event);
}
else
event->ignore();
if(!m_showThumbnails && m_rawImage)
{
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;
QVector2D offset(dx, dy);
QVector2D pos = QVector2D(event->pos());
QVector2D pix = (pos + offset) / m_scale;
QVector3D rgb;
SkyPoint sky;
if(m_wcs)
{
m_wcs->pixelToWorld(QPointF(pix.x(), pix.y()), sky);
}
if(m_rawImage->pixel(pix.x(), pix.y(), rgb))
{
if(m_bwImg)
emit status(tr("L:%1").arg(rgb.x()), 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(rgb.x()).arg(rgb.y()).arg(rgb.z()), 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
event->ignore();
}
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();
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);
int size = std::max(m_imgWidth, m_imgHeight);
int level = 0;
while(size >>= 1)level++;
int w,h;
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &w);
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &h);
uint16_t pixel[w*h*4];
f3->glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_SHORT, pixel);
float maxRGB = std::max(std::max(pixel[0], pixel[1]), pixel[2]);
m_whiteBalance[0] = maxRGB / pixel[0];
m_whiteBalance[1] = maxRGB / pixel[1];
m_whiteBalance[2] = maxRGB / pixel[2];
}
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);
@@ -416,22 +691,17 @@ 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);
m_imgWidth = image->rawImage()->width();
m_imgHeight = image->rawImage()->height();
if(m_bestFit)bestFit();
updateScrollbars();
}
}
ImageWidget *ImageScrollAreaGL::imageWidget()
@@ -444,8 +714,8 @@ void ImageScrollAreaGL::setThumbnails(int count)
m_thumbCount = count;
if(m_thumbCount)
{
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER);
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
}
else
{
@@ -461,8 +731,8 @@ void ImageScrollAreaGL::updateScrollbars(bool zoom)
{
m_horizontalScrollBar->hide();
m_verticalScrollBar->show();
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER);
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
}
else
{
@@ -484,8 +754,8 @@ void ImageScrollAreaGL::resizeEvent(QResizeEvent *event)
QWidget::resizeEvent(event);
if(m_thumbCount)
{
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER);
m_verticalScrollBar->setRange(0, m_thumbCount / (m_imageWidget->width() / THUMB_SIZE_BORDER) * THUMB_SIZE_BORDER_Y);
m_verticalScrollBar->setPageStep(THUMB_SIZE_BORDER_Y);
}
else
{
+42 -24
View File
@@ -13,15 +13,16 @@
#include <QTimer>
#include "rawimage.h"
#include "imageringlist.h"
#include "database.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
{
@@ -31,6 +32,7 @@ class ImageWidget : public QOpenGLWidget
QTimer *m_updateTimer;
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;
@@ -38,28 +40,39 @@ class ImageWidget : public QOpenGLWidget
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;
int m_currentImg;
float m_low;
float m_mid;
float m_high;
float m_range;
float m_dx, m_dy;
float m_scale;
float m_whiteBalance[3];
bool m_blockRepaint;
bool m_bwImg;
bool m_invert;
bool m_superpixel;
bool m_showThumbnails;
bool m_selecting;
bool m_sizesDirty;
bool m_srgb;
int m_thumbnailCount;
QVector<ImageThumb> m_thumnails;
Database *m_database;
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 setWCS(std::shared_ptr<WCSData> wcs);
void setScale(float scale);
void blockRepaint(bool block);
void allocateThumbnails(int count);
void allocateThumbnails(const QStringList &paths);
public slots:
void setMTFParams(float low, float mid, float high);
void setOffset(int dx, int dy);
@@ -69,13 +82,19 @@ public slots:
void thumbnailLoaded(const Image *image);
void showThumbnail(bool enable);
protected:
void paintGL();
void resizeGL(int w, int h);
void initializeGL();
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
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 thumbSelect(QMouseEvent *event);
void debayer();
signals:
void fileDropped(const QString &path);
void status(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
};
class ImageScrollAreaGL : public QWidget
@@ -90,18 +109,17 @@ class ImageScrollAreaGL : public QWidget
bool m_bestFit;
int m_thumbCount;
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();
void setThumbnails(int count);
protected:
void updateScrollbars(bool zoom = false);
void resizeEvent(QResizeEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
void resizeEvent(QResizeEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void zoom(float delta);
public slots:
void zoomIn();
+3 -2
View File
@@ -1,4 +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 org.nou.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 32 org.nou.tenmon.png org.nou.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
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 space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
+96 -29
View File
@@ -12,6 +12,12 @@
#include <pcl/XISF.h>
#include "rawimage.h"
#include "starfit.h"
#include "wcslib/wcshdr.h"
#ifdef COLOR_MANAGMENT
#include <QColorSpace>
static pcl::ICCProfile sRgbIccProfile((void*)QColorSpace(QColorSpace::SRgb).iccProfile().data());
#endif
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
m_file(file),
@@ -82,17 +88,17 @@ bool loadRAW(const QString path, ImageInfoData &info, RawImage **image)
if(!image)
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())
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;
if(image)
{
libraw_rawdata_t rawdata = raw.imgdata.rawdata;
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
size_t size = rawdata.sizes.width*rawdata.sizes.height;
std::vector<uint16_t> out;
@@ -114,14 +120,14 @@ bool loadRAW(const QString path, ImageInfoData &info, RawImage **image)
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("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)));
@@ -131,6 +137,9 @@ bool loadRAW(const 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];
@@ -138,6 +147,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++)
{
@@ -156,16 +166,29 @@ 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;
}
@@ -248,7 +271,8 @@ bool loadFITS(const QString path, ImageInfoData &info, RawImage **image)
}
}
noload:
loadFITSHeader(file, info);
if(file)
loadFITSHeader(file, info);
fits_close_file(file, &status);
if(status)
@@ -256,17 +280,32 @@ bool loadFITS(const 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;
}
#include "pcl/ICCProfileTransformation.h"
template<typename T, typename PCLtype, int CVtype>
bool loadPCLImage(pcl::XISFReader &xisf, RawImage **image)
{
PCLtype pclImage;
xisf.ReadImage(pclImage);
pclImage.Status().DisableInitialization();
#ifdef COLOR_MANAGMENT
pcl::ICCProfile iccProfile = xisf.ReadICCProfile();
if(iccProfile.IsProfile() && iccProfile != sRgbIccProfile)
{
pcl::ICCProfileTransformation iccTran;
iccTran.DisableParallelProcessing();
iccTran.Add(iccProfile);
iccTran.Add(sRgbIccProfile);
iccTran >> pclImage;
}
#endif
int numChannels = pclImage.NumberOfChannels();
cv::Mat cvImg[numChannels];
@@ -296,6 +335,7 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
{
try
{
pcl::XISF::EnsurePTLUTInitialized();
pcl::String pclPath = path.utf16();
pcl::XISFReader xisf;
xisf.Open(pclPath);
@@ -307,8 +347,18 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
auto fitskeywords = xisf.ReadFITSKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append({fits.name.c_str(), fits.value.c_str(), fits.comment.c_str()});
info.fitsHeader.append(fits);
}
auto imageproperties = xisf.ReadImageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
}
info.wcs = std::make_shared<WCSData>(xisf.ImageInfo().width, xisf.ImageInfo().height, info.fitsHeader);
info.info.append({QObject::tr("Width"), QString::number(xisf.ImageInfo().width)});
info.info.append({QObject::tr("Height"), QString::number(xisf.ImageInfo().height)});
if(!info.wcs->valid())info.wcs.reset();
if(floatType && bps == 32)
return loadPCLImage<float, pcl::FImage, CV_32F>(xisf, image);
@@ -321,13 +371,12 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
case 16:
return loadPCLImage<uint16_t, pcl::UInt16Image, CV_16U>(xisf, image);
}
}
}
catch (pcl::Error err)
{
info.info.append(QPair<QString, QString>("Error", err.FormatInfo().ToUTF8().c_str()));
qDebug() << err.FormatInfo().ToUTF8().c_str();
qDebug() << "Failed to load XISF" << err.FormatInfo().ToUTF8().c_str();
}
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
return false;
@@ -346,24 +395,31 @@ void LoadRunable::run()
RawImage *rawImage = nullptr;
bool raw = false;
if(m_file.endsWith(".CR2", Qt::CaseInsensitive))
timer.start();
if(m_file.endsWith(".CR2", Qt::CaseInsensitive) || m_file.endsWith(".NEF", Qt::CaseInsensitive) || m_file.endsWith(".DNG", Qt::CaseInsensitive))
{
timer.start();
loadRAW(m_file, info, &rawImage);
raw = true;
qDebug() << "LoadRaw" << timer.elapsed();
qDebug() << "LoadRAW" << timer.elapsed();
}
else if(m_file.endsWith(".FIT", Qt::CaseInsensitive) || m_file.endsWith(".FITS", Qt::CaseInsensitive))
{
loadFITS(m_file, info, &rawImage);
qDebug() << "LoadFITS" << timer.elapsed();
}
else if(m_file.endsWith(".XISF", Qt::CaseInsensitive))
{
loadXISF(m_file, info, &rawImage);
qDebug() << "LoadXISF" << timer.elapsed();
}
else
{
QImage img(m_file);
#ifdef COLOR_MANAGMENT
if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb)
img.convertToColorSpace(QColorSpace::SRgb);
#endif
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())});
@@ -380,8 +436,9 @@ void LoadRunable::run()
{
double mean, median, min, max, mad;
double stdDev;
uint32_t saturated;
timer.start();
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad);
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad, &saturated);
qDebug() << "image stats" << timer.restart();
info.info.append({QObject::tr("Mean"), QString::number(mean)});
info.info.append({QObject::tr("Standart deviation"), QString::number(stdDev)});
@@ -389,6 +446,7 @@ void LoadRunable::run()
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)});
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * saturated / rawImage->size()) + "%"});
if(m_analyzeLevel >= Peaks)
{
@@ -406,7 +464,7 @@ void LoadRunable::run()
// 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())});
//info.info.append({QObject::tr("Peaks draw"), QString::number(peaks.size())});
if(m_analyzeLevel>= Stars)
{
@@ -482,8 +540,16 @@ bool readXISFHeader(const QString &path, ImageInfoData &info)
auto fitskeywords = xisf.ReadFITSKeywords();
for(auto fits : fitskeywords)
{
info.fitsHeader.append({fits.name.c_str(), fits.value.c_str(), fits.comment.c_str()});
info.fitsHeader.append(fits);
}
auto imageproperties = xisf.ReadImageProperties();
for(auto prop : imageproperties)
{
info.fitsHeader.append(prop);
}
info.wcs = std::make_shared<WCSData>(xisf.ImageInfo().width, xisf.ImageInfo().height, info.fitsHeader);
if(!info.wcs->valid())info.wcs.reset();
}
catch (pcl::Error err)
{
@@ -493,9 +559,10 @@ bool readXISFHeader(const QString &path, ImageInfoData &info)
return true;
}
ConvertRunable::ConvertRunable(const QString &in, const QString &out) :
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format) :
m_infile(in),
m_outfile(out)
m_outfile(out),
m_format(format)
{
}
@@ -600,13 +667,13 @@ void ConvertRunable::run()
if(rawimage)
{
if(m_outfile.endsWith(".XISF", Qt::CaseInsensitive))
if(m_format == "XISF")
{
pcl::XISFOptions options;
pcl::FITSKeywordArray fitskeywords;
for(auto &record : imageinfo.fitsHeader)
{
pcl::FITSHeaderKeyword key(pcl::IsoString(record.key.data()), pcl::IsoString(record.value.toString().toLatin1().data()), pcl::IsoString(record.comment.data()));
pcl::FITSHeaderKeyword key(pcl::IsoString(record.key.data()), pcl::IsoString(record.valueToByteArray().data()), pcl::IsoString(record.comment.data()));
fitskeywords.Append(key);
}
pcl::XISFWriter xisf;
@@ -627,7 +694,7 @@ void ConvertRunable::run()
}
if(m_outfile.endsWith(".FITS", Qt::CaseInsensitive) || m_outfile.endsWith(".FIT", Qt::CaseInsensitive))
if(m_format == "FITS")
{
int status = 0;
fitsfile *fw;
+4 -3
View File
@@ -18,16 +18,17 @@ class LoadRunable : public QRunnable
bool m_thumbnail;
public:
LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail = false);
void run();
void run() override;
};
class ConvertRunable : public QRunnable
{
QString m_infile;
QString m_outfile;
QString m_format;
public:
ConvertRunable(const QString &in, const QString &out);
void run();
ConvertRunable(const QString &in, const QString &out, const QString &format);
void run() override;
};
#endif // LOADRUNABLE_H
+9 -1
View File
@@ -2,9 +2,14 @@
#include <QApplication>
#include <QSurfaceFormat>
#include <QTranslator>
#include <stdlib.h>
int main(int argc, char *argv[])
{
#ifdef __linux__
setenv("LC_NUMERIC", "C", 1);
#endif
QSurfaceFormat format;
format.setMajorVersion(3);
format.setMinorVersion(3);
@@ -15,11 +20,14 @@ int main(int argc, char *argv[])
QApplication a(argc, argv);
a.setOrganizationName("nou");
a.setApplicationName("Tenmon");
a.setWindowIcon(QIcon(":/org.nou.tenmon.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();
+188 -54
View File
@@ -13,11 +13,16 @@
#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"
#ifdef __linux__
#include <sys/ioctl.h>
@@ -25,6 +30,8 @@
#include <sys/socket.h>
#endif
bool moveToTrash(const QString &path);
int MainWindow::socketPair[2] = {0, 0};
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
@@ -32,32 +39,61 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
qRegisterMetaType<ImageInfoData>("ImageInfoData");
qRegisterMetaType<RawImage*>("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 *.nef *.dng)");
_openFilter.append(tr(";;All files (*)"));
nameFilter.append({"fit", "fits", "xisf", "cr2", "nef", "dng"});
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);
setStatusBar(new QStatusBar(this));
m_imageGL = new ImageScrollAreaGL(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, SIGNAL(paramChanged(float,float,float)), m_imageGL->imageWidget(), SLOT(setMTFParams(float,float,float)));
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);
m_ringList = new ImageRingList(this);
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)));
m_database = new Database(this);
if(!m_database->init())
QMessageBox::critical(this, tr("Can't open DB"), tr("Can't open SQLITE database"));
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)));
@@ -75,6 +111,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
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);
setWindowTitle(tr("Tenmon"));
connect(m_ringList, SIGNAL(pixmapLoaded(Image*)), this, SLOT(pixmapLoaded(Image*)));
@@ -89,11 +131,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence::Open);
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence::Save);
fileMenu->addSeparator();
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()));
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()));
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()), Qt::Key_F5);
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()), Qt::Key_F6);
fileMenu->addAction(tr("Move marked files to trash"), this, &MainWindow::deleteMarked, QKeySequence::Delete);
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->addSeparator();
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
liveModeAction->setCheckable(true);
@@ -101,14 +145,18 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
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("100%"), m_imageGL, SLOT(oneToOne()));
viewMenu->addAction(tr("Fullscreen"), this, SLOT(toggleFullScreen()), Qt::CTRL + Qt::Key_F11);
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), [this](bool checked){
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageCount());
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
m_imageGL->imageWidget()->showThumbnail(checked);
m_imageGL->setThumbnails(checked ? m_ringList->imageCount() : 0);
if(checked)m_ringList->loadThumbnails();
@@ -118,7 +166,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
menuBar()->addMenu(viewMenu);
QMenu *selectMenu = new QMenu(tr("Select"), this);
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F5);
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F7);
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
selectMenu->addSeparator();
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
@@ -141,7 +189,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);
@@ -156,6 +204,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
dockMenu->addAction(m_stretchPanel->toggleViewAction());
dockMenu->addAction(filesystemDock->toggleViewAction());
dockMenu->addAction(databaseViewDock->toggleViewAction());
dockMenu->addAction(filetreeDock->toggleViewAction());
menuBar()->addMenu(dockMenu);
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
@@ -173,7 +222,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
_lastDir = standardLocations.first();
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
m_filesystem->setDir(_lastDir);
QStringList args = QCoreApplication::arguments();
args.removeFirst();
@@ -191,6 +239,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()
@@ -203,9 +261,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:
@@ -258,7 +318,15 @@ void MainWindow::closeEvent(QCloseEvent *event)
void MainWindow::copyOrMove(bool copy)
{
QString dest = QFileDialog::getExistingDirectory(this, tr("Select destination"), _lastDir);
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())
{
@@ -267,8 +335,9 @@ void MainWindow::copyOrMove(bool copy)
QProgressDialog progress(copy ? tr("Copying") : tr("Moving"), tr("Cancel"), 0, files.size(), this);
progress.setWindowModality(Qt::WindowModal);
progress.show();
foreach(const QString &file, files)
for(const QString &file : files)
{
bool result = false;
QFileInfo info(file);
QFile srcFile(file);
QFile dstFile(dir.absoluteFilePath(info.fileName()));
@@ -277,7 +346,7 @@ void MainWindow::copyOrMove(bool copy)
continue;
if(progress.wasCanceled())
break;
return;
#ifdef __linux__
if(copy)
{
@@ -287,24 +356,33 @@ void MainWindow::copyOrMove(bool copy)
{
dstFile.remove();
dstFile.close();
qDebug() << dstFile.fileName();
srcFile.copy(dstFile.fileName());
result = srcFile.copy(dstFile.fileName());
}
else result = true;
}
else
{
srcFile.rename(dstFile.fileName());
result = srcFile.rename(dstFile.fileName());
}
#else
if(copy)
srcFile.copy(dstFile.fileName());
result = srcFile.copy(dstFile.fileName());
else
srcFile.rename(dstFile.fileName());
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();
}
m_database->clearMarkedFiles();
}
void MainWindow::socketNotify()
@@ -318,16 +396,18 @@ 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 *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)"));
QString file = QFileDialog::getOpenFileName(this,
tr("Open file"),
_lastDir,
_openFilter);
loadFile(file);
}
@@ -344,7 +424,6 @@ void MainWindow::loadFile(const QString &path)
_lastDir = info.canonicalPath();
QSettings settings;
settings.setValue("mainwindow/lastdir", _lastDir);
m_filesystem->setDir(_lastDir);
}
}
@@ -355,7 +434,12 @@ void MainWindow::loadFile(int row)
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);
@@ -374,35 +458,45 @@ void MainWindow::reindex()
void MainWindow::saveAs()
{
QString selectedFilter;
QString file = QFileDialog::getSaveFileName(this, tr("Save as"), _lastDir, tr("JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)"), &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())
{
QFileInfo info(file);
if(info.suffix().isEmpty())
{
if(selectedFilter.contains("jpg"))file += ".jpg";
if(selectedFilter.contains("png"))file += ".png";
if(selectedFilter.contains("fits"))file += ".fits";
if(selectedFilter.contains("xisf"))file += ".xisf";
}
QString format = filterToFormat(file, selectedFilter);
if(file.endsWith(".fits") || file.endsWith(".xisf"))
if(format == "FITS" || format == "XISF")
{
convert(file);
convert(file, format);
}
else
{
QImage img = m_imageGL->imageWidget()->renderToImage();
if(!img.isNull())
img.save(file);
img.save(file, filterToFormat(file, selectedFilter));
}
}
}
void MainWindow::convert(const QString &outfile)
void MainWindow::convert(const QString &outfile, const QString &format)
{
QString file = m_ringList->currentImage()->name();
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile));
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format));
}
void MainWindow::markImage()
@@ -412,7 +506,10 @@ void MainWindow::markImage()
{
QString file = ptr->name();
if(!file.isEmpty())
{
m_database->mark(file);
m_ringList->updateMark();
}
updateWindowTitle();
}
@@ -425,7 +522,10 @@ void MainWindow::unmarkImage()
{
QString file = ptr->name();
if(!file.isEmpty())
{
m_database->unmark(file);
m_ringList->updateMark();
}
updateWindowTitle();
}
@@ -455,18 +555,32 @@ void MainWindow::moveMarked()
copyOrMove(false);
}
void MainWindow::toggleFullScreen()
void MainWindow::deleteMarked()
{
if(isFullScreen())
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)
@@ -491,10 +605,30 @@ void MainWindow::starFinder(bool findStars)
void MainWindow::showMarkFilesDialog()
{
MarkedFiles markedFiles;
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();
+13 -6
View File
@@ -22,21 +22,25 @@ class MainWindow : public QMainWindow
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();
@@ -45,21 +49,24 @@ protected slots:
void loadFile(const QString &path);
void loadFile(int row);
void indexDir();
void indexDir(const QString &dir);
void reindex();
void saveAs();
void convert(const QString &outfile);
void convert(const QString &outfile, const QString &format);
void markImage();
void unmarkImage();
void markAndNext();
void unmarkAndNext();
void copyMarked();
void moveMarked();
void toggleFullScreen();
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
+111 -2
View File
@@ -1,5 +1,10 @@
#include "rawimage.h"
int THUMB_SIZE = 128;
int THUMB_SIZE_BORDER = 138;
int THUMB_SIZE_BORDER_Y = 158;
double SATURATION = 0.95;
RawImage::ImgType CV2Type(int cvtype)
{
switch (cvtype)
@@ -16,6 +21,8 @@ RawImage::ImgType CV2Type(int cvtype)
return RawImage::UINT8C4;
case CV_16UC3:
return RawImage::UINT16C3;
case CV_16UC4:
return RawImage::UINT16C4;
case CV_32FC3:
return RawImage::FLOAT32C3;
default:
@@ -39,6 +46,8 @@ int Type2CV(RawImage::ImgType type)
return CV_8UC4;
case RawImage::UINT16C3:
return CV_16UC3;
case RawImage::UINT16C4:
return CV_16UC4;
case RawImage::FLOAT32C3:
return CV_32FC3;
case RawImage::UNKNOWN:
@@ -63,6 +72,7 @@ RawImage::RawImage(cv::Mat &img)
{
m_img = img;
m_stats = false;
scaleToUnit();
}
RawImage::RawImage(const RawImage &d)
@@ -75,6 +85,7 @@ RawImage::RawImage(const RawImage &d)
m_max = d.m_max;
m_mad = d.m_mad;
m_stats = d.m_stats;
m_saturated = d.m_saturated;
}
RawImage::RawImage(const QImage &img)
@@ -84,6 +95,27 @@ RawImage::RawImage(const QImage &img)
m_img.create(img.height(), img.width(), CV_8UC4);
for(int i=0; i<img.height(); i++)
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*4);
cv::cvtColor(m_img, m_img, cv::COLOR_BGRA2RGB);
}
else if(img.format() == QImage::Format_ARGB32)
{
m_img.create(img.height(), img.width(), CV_8UC4);
for(int i=0; i<img.height(); i++)
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*4);
cv::cvtColor(m_img, m_img, cv::COLOR_BGRA2RGBA);
}
else if(img.format() == QImage::Format_RGBX64)
{
m_img.create(img.height(), img.width(), CV_16UC4);
for(int i=0; i<img.height(); i++)
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*8);
cv::cvtColor(m_img, m_img, cv::COLOR_RGBA2RGB);
}
else if(img.format() == QImage::Format_RGBA64)
{
m_img.create(img.height(), img.width(), CV_16UC4);
for(int i=0; i<img.height(); i++)
std::memcpy(m_img.ptr(i), img.scanLine(i), img.width()*8);
}
else
{
@@ -96,7 +128,7 @@ RawImage::RawImage(const QImage &img)
m_stats = false;
}
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad)
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad, uint32_t *saturated)
{
if(!m_stats)calcStats();
if(mean)*mean = m_mean;
@@ -105,6 +137,7 @@ bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *
if(min)*min = m_min;
if(max)*max = m_max;
if(mad)*mad = m_mad;
if(saturated)*saturated = m_saturated;
return true;
}
@@ -146,6 +179,12 @@ void RawImage::calcStats()
}
}
if(img.type() == CV_32F)m_median /= histSize;
int threshold = SATURATION * histSize;
m_saturated = 0;
for(int i = histSize-1; i >= threshold; i--)
m_saturated += hist.at<float>(0, i);
cv::Mat absDev;
img.convertTo(absDev, CV_32F, 1, -m_median);
absDev = cv::abs(absDev);
@@ -284,7 +323,7 @@ void RawImage::convertToThumbnail()
if(m_img.channels() == 1)
cv::cvtColor(m_img, m_img, cv::COLOR_GRAY2RGB);
if(m_img.channels() == 4)
cv::cvtColor(m_img, m_img, cv::COLOR_BGRA2RGB);
cv::cvtColor(m_img, m_img, cv::COLOR_RGBA2RGB);
cv::Size dsize(THUMB_SIZE, THUMB_SIZE);
cv::resize(m_img, m_img, dsize, 0, 0, cv::INTER_NEAREST);
}
@@ -298,3 +337,73 @@ const cv::Mat& RawImage::mat() const
{
return m_img;
}
bool RawImage::pixel(int x, int y, QVector3D &rgb) const
{
if(x < 0 || y < 0 || x >= (int)width() || y >= (int)height())return false;
switch(m_img.type())
{
case CV_8U:
{
uint8_t v = m_img.at<uint8_t>(y, x);
rgb = QVector3D(v, v, v);
break;
}
case CV_16U:
{
uint16_t v = m_img.at<uint16_t>(y, x);
rgb = QVector3D(v, v, v);
break;
}
case CV_32F:
{
float v = m_img.at<float>(y, x);
rgb = QVector3D(v, v, v);
break;
}
case CV_8UC3:
{
cv::Vec3b v = m_img.at<cv::Vec3b>(y, x);
rgb = QVector3D(v[0], v[1], v[2]);
break;
}
case CV_8UC4:
{
cv::Vec4b v = m_img.at<cv::Vec4b>(y, x);
rgb = QVector3D(v[0], v[1], v[2]);
break;
}
case CV_16UC3:
{
cv::Vec3w v = m_img.at<cv::Vec3w>(y, x);
rgb = QVector3D(v[0], v[1], v[2]);
break;
}
case CV_32FC3:
{
cv::Vec3f v = m_img.at<cv::Vec3f>(y, x);
rgb = QVector3D(v[0], v[1], v[2]);
break;
}
default:
rgb = QVector3D(0, 0, 0);
break;
}
return true;
}
void RawImage::scaleToUnit()
{
if(CV_MAT_DEPTH(m_img.type()) == CV_32F)
{
double min, max;
cv::minMaxIdx(m_img, &min, &max);
if(min < 0 || max > 1)
{
float scale = 1.0 / (max - min);
float zero = min * scale;
m_img = m_img * scale - zero;
}
}
}
+9 -3
View File
@@ -8,9 +8,11 @@
#include <memory.h>
#include <opencv2/imgproc.hpp>
#include <QImage>
#include <QVector3D>
const int THUMB_SIZE = 128;
const int THUMB_SIZE_BORDER = 138;
extern int THUMB_SIZE;
extern int THUMB_SIZE_BORDER;
extern int THUMB_SIZE_BORDER_Y;
class Peak
{
@@ -46,6 +48,7 @@ protected:
double m_max;
double m_mad;
float m_thumbAspect;
uint32_t m_saturated;
public:
enum ImgType
{
@@ -55,6 +58,7 @@ public:
UINT8C3,
UINT8C4,
UINT16C3,
UINT16C4,
FLOAT32C3,
UNKNOWN,
};
@@ -63,7 +67,7 @@ public:
RawImage(cv::Mat &img);
RawImage(const RawImage &d);
RawImage(const QImage &img);
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad);
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad, uint32_t *saturated);
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;
@@ -80,6 +84,8 @@ public:
void convertToThumbnail();
float thumbAspect() const;
const cv::Mat& mat() const;
bool pixel(int x, int y, QVector3D &rgb) const;
void scaleToUnit();
};
#endif // RAWIMAGE_H
+11 -9
View File
@@ -1,15 +1,9 @@
<RCC>
<qresource prefix="/shaders">
<file>image.frag</file>
<file>image.vert</file>
<file>thumb.frag</file>
<file>thumb.vert</file>
</qresource>
<qresource prefix="/">
<file>invert.png</file>
<file>nuke.png</file>
<file>bayer.png</file>
<file>org.nou.tenmon.png</file>
<file>space.nouspiro.tenmon.png</file>
<file>nuke_a.png</file>
<file>about/tenmon</file>
<file>about/pcl</file>
@@ -17,6 +11,13 @@
<file>translations/tenmon_sk.qm</file>
<file>about/filter.png</file>
<file>about/stretch-panel.png</file>
<file>translations/tenmon_fr.qm</file>
<file>shaders/image.frag</file>
<file>shaders/image.vert</file>
<file>shaders/thumb.frag</file>
<file>shaders/thumb.vert</file>
<file>shaders/debayer.frag</file>
<file>shaders/debayer.vert</file>
</qresource>
<qresource lang="en" prefix="/">
<file alias="help">about/help_en</file>
@@ -24,6 +25,7 @@
<qresource lang="sk" prefix="/">
<file alias="help">about/help_sk</file>
</qresource>
<qresource prefix="/help"/>
<qresource prefix="/helpimg"/>
<qresource lang="fr" prefix="/">
<file alias="help">about/help_fr</file>
</qresource>
</RCC>
+109
View File
@@ -0,0 +1,109 @@
#include "settingsdialog.h"
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include <QSettings>
#include <QApplication>
#include "rawimage.h"
extern int DEFAULT_WIDTH;
extern double SATURATION;
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, 8);
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_useNativeDialog = new QCheckBox(tr("Don't use native file dialog"), this);
m_useNativeDialog->setChecked(QApplication::testAttribute(Qt::AA_DontUseNativeDialogs));
layout->addRow(tr("Image preload count"), m_preloadImages);
layout->addRow(tr("Thumbnails size"), m_thumSize);
layout->addRow(tr("Saturation"), m_saturation);
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();
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;
SATURATION = settings.value("settings/saturation", 95.0).toDouble() / 100.0;
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());
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());
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QDialog>
#include <QSpinBox>
#include <QCheckBox>
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_saturation;
QCheckBox *m_useNativeDialog;
};
#endif // SETTINGSDIALOG_H
+41
View File
@@ -0,0 +1,41 @@
#version 330
uniform sampler2D qt_Texture0;
in vec2 qt_TexCoord0;
in vec2 center;
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 % 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;
}
+16
View File
@@ -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;
}
+50
View File
@@ -0,0 +1,50 @@
#version 330
uniform sampler2D qt_Texture0;
uniform vec3 mtf_param;
uniform bool bw;
uniform bool invert;
uniform bool srgb;
uniform vec3 whiteBalance;
in vec2 qt_TexCoord0;
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, 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);
}
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);
}
void main(void)
{
color = texture(qt_Texture0, qt_TexCoord0);
if(bw)color = color.rrra;
color = MTF(color, mtf_param);
if(invert)color.rgb = vec3(1.0) - color.rgb;
color.rgb = mix(checker(), color.rgb, color.a);
if(srgb)color.rgb = Linear2sRGB(color.rgb);
color.rgb *= whiteBalance;
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;
}
View File
+1
View File
@@ -18,4 +18,5 @@ void main(void)
color = texture(qt_Texture0, qt_TexCoord0);
color = MTF(color, mtf_param);
if(invert)color = vec4(1.0) - color;
color.a = 1.0;
}
+3 -2
View File
@@ -7,13 +7,14 @@ 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 + 69;
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * 138;
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);
@@ -1,9 +1,9 @@
[Desktop Entry]
Type=Application
Exec=tenmon %U
Icon=org.nou.tenmon
Icon=space.nouspiro.tenmon
Comment=FITS Image viewer
Name=Tenmon
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;Science;Astronomy
MimeType=image/fits;image/x-xisf;
Terminal=false
+79
View File
@@ -0,0 +1,79 @@
<?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>
<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 bit integer and 32 bit float</li>
<li>XISF 8, 16 bit integer and 32 bit float</li>
<li>RAW CR2, 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 &lt;-&gt; XISF</li>
<li>Convert FITS/XISF -&gt; 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>
</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">
<image>https://nouspiro.space/wp-content/uploads/2022/04/tenmon-1024x579.png</image>
<image>https://nouspiro.space/wp-content/uploads/2022/12/tenmon2-1024x645.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>
<releases>
<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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+65
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+23
View File
@@ -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);
}
+19
View File
@@ -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
+26 -3
View File
@@ -21,6 +21,8 @@ STFSlider::STFSlider(QWidget *parent) : QWidget(parent)
m_midPoint = 0.5;
m_whitePoint = 1;
m_grabbed = -1;
m_fineTune = false;
setToolTip(tr("Press Shift for fine tuning"));
}
float STFSlider::blackPoint() const
@@ -94,20 +96,34 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
else
unsetCursor();
qreal x = (qreal)event->x()/width();
if(event->modifiers() & Qt::ShiftModifier && !m_fineTune)
{
m_fineTune = true;
m_fineTuneX = x;
}
if(!(event->modifiers() & Qt::ShiftModifier) && m_fineTune)
m_fineTune = false;
if(m_fineTune)
{
x = m_fineTuneX + (x - m_fineTuneX) * 0.2;
}
switch(m_grabbed)
{
case 0:
m_blackPoint = clamp((qreal)event->x()/width());
m_blackPoint = clamp(x);
m_whitePoint = std::max(m_whitePoint, m_blackPoint);
QToolTip::showText(event->globalPos(), QString::number(m_blackPoint), this);
break;
case 1:
m_midPoint = ((qreal)event->x()/width() - m_blackPoint) / (m_whitePoint - m_blackPoint);
m_midPoint = (x - m_blackPoint) / (m_whitePoint - m_blackPoint);
m_midPoint = clamp(m_midPoint);
QToolTip::showText(event->globalPos(), QString::number(m_midPoint), this);
break;
case 2:
m_whitePoint = clamp((qreal)event->x()/width());
m_whitePoint = clamp(x);
m_blackPoint = std::min(m_blackPoint, m_whitePoint);
QToolTip::showText(event->globalPos(), QString::number(m_whitePoint), this);
break;
@@ -121,6 +137,12 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
void STFSlider::mousePressEvent(QMouseEvent *event)
{
if(event->modifiers() & Qt::ShiftModifier)
{
m_fineTune = true;
m_fineTuneX = (qreal)event->x()/width();
}
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - event->x()) < 5)
m_grabbed = 1;
else if(std::abs(m_blackPoint*width() - event->x()) < 5)
@@ -134,5 +156,6 @@ void STFSlider::mousePressEvent(QMouseEvent *event)
void STFSlider::mouseReleaseEvent(QMouseEvent *)
{
m_grabbed = -1;
m_fineTune = false;
emit paramChanged(m_blackPoint, midPoint(), m_whitePoint);
}
+2
View File
@@ -11,6 +11,8 @@ class STFSlider : public QWidget
float m_midPoint;
float m_whitePoint;
int m_grabbed;
bool m_fineTune;
float m_fineTuneX;
public:
explicit STFSlider(QWidget *parent = nullptr);
float blackPoint() const;
+7 -5
View File
@@ -34,7 +34,7 @@ StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar")
invertButton->setCheckable(true);
connect(invertButton, SIGNAL(toggled(bool)), this, SIGNAL(invert(bool)));
QAction *superPixelButton = addAction(QIcon(":/bayer.png"), tr("Superpixel CFA draw 2x2 pixel as one"));
QAction *superPixelButton = addAction(QIcon(":/bayer.png"), tr("Debayer CFA"));
superPixelButton->setCheckable(true);
connect(superPixelButton, SIGNAL(toggled(bool)), this, SIGNAL(superPixel(bool)));
@@ -48,14 +48,16 @@ void StretchToolbar::stretchImage(Image *img)
{
if(img->rawImage())
{
double median, mad;
img->rawImage()->imageStats(nullptr, nullptr, &median, nullptr, nullptr, &mad);
double median, mad, max;
img->rawImage()->imageStats(nullptr, nullptr, &median, nullptr, &max, &mad, nullptr);
median /= img->rawImage()->norm();
mad /= img->rawImage()->norm();
max /= img->rawImage()->norm();
if(max>1.0f)max = 1.0f;
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);
m_stfSlider->setMTFParams(bp, mid, max);
emit paramChanged(m_stfSlider->blackPoint(), m_stfSlider->midPoint(), max);
}
}
}
-9
View File
@@ -1,9 +0,0 @@
[Desktop Entry]
Type=Application
Exec=Tenmon %U
Icon=org.nou.tenmon
Comment=FITS Image viewer
Name=Tenmon
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;
MimeType=image/fits;image/x-xisf;
Terminal=false
BIN
View File
Binary file not shown.
Binary file not shown.
+196 -18
View File
@@ -23,6 +23,17 @@
<translation>Filter</translation>
</message>
</context>
<context>
<name>DatabaseTableView</name>
<message>
<source>Mark</source>
<translation>Mark</translation>
</message>
<message>
<source>Unmark</source>
<translation>Unmark</translation>
</message>
</context>
<context>
<name>FITSFileModel</name>
<message>
@@ -30,6 +41,41 @@
<translation>File name</translation>
</message>
</context>
<context>
<name>Filetree</name>
<message>
<source>Open</source>
<translation>Open</translation>
</message>
<message>
<source>Copy marked files</source>
<translation>Copy marked files</translation>
</message>
<message>
<source>Move marked files</source>
<translation>Move marked files</translation>
</message>
<message>
<source>Index directory</source>
<translation>Index directory</translation>
</message>
<message>
<source>Set as root</source>
<translation>Set as root directory</translation>
</message>
<message>
<source>Reset root</source>
<translation>Reset root directory</translation>
</message>
<message>
<source>Go up</source>
<translation>Go up</translation>
</message>
<message>
<source>Show hidden files</source>
<translation>Show hidden files</translation>
</message>
</context>
<context>
<name>HelpDialog</name>
<message>
@@ -77,6 +123,18 @@
<source>Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed.</source>
<translation>Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed.</translation>
</message>
<message>
<source>L:%1</source>
<translation>L:%1</translation>
</message>
<message>
<source>X:%3 Y:%4</source>
<translation>X:%3 Y:%4</translation>
</message>
<message>
<source>R:%1 G:%2 B:%3</source>
<translation>R:%1 G:%2 B:%3</translation>
</message>
</context>
<context>
<name>MainWindow</name>
@@ -96,10 +154,6 @@
<source>Filesystem</source>
<translation>File system</translation>
</message>
<message>
<source>FITS files database</source>
<translation type="vanished">FITS files database</translation>
</message>
<message>
<source>Tenmon</source>
<translation>Tenmon</translation>
@@ -150,7 +204,7 @@
</message>
<message>
<source>Fullscreen</source>
<translation>Fullscreen</translation>
<translation type="vanished">Fullscreen</translation>
</message>
<message>
<source>Select</source>
@@ -236,18 +290,10 @@
<source>Moving</source>
<translation>Moving</translation>
</message>
<message>
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</source>
<translation>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</translation>
</message>
<message>
<source>Indexing FITS files</source>
<translation>Indexing FITS files</translation>
</message>
<message>
<source>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</source>
<translation>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</translation>
</message>
<message>
<source>Reindex files</source>
<translation>Reindex files</translation>
@@ -256,6 +302,86 @@
<source>FITS/XISF files database</source>
<translation>FITS/XISF files database</translation>
</message>
<message>
<source>File tree</source>
<translation>File tree</translation>
</message>
<message>
<source>Star finder</source>
<translation>Star finder</translation>
</message>
<message>
<source>Edit</source>
<translation>Edit</translation>
</message>
<message>
<source>FITS header editor</source>
<translation type="vanished">FITS header editor</translation>
</message>
<message>
<source>Settings</source>
<translation>Settings</translation>
</message>
<message>
<source>Images (</source>
<translation>Images (</translation>
</message>
<message>
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
<translation>FITS image (*.fits *.fit);;XISF image (*.xisf);;</translation>
</message>
<message>
<source>Failed to copy</source>
<translation>Failed to copy</translation>
</message>
<message>
<source>Failed to move</source>
<translation>Failed to move</translation>
</message>
<message>
<source>Failed to move from %1 to %2</source>
<translation>Failed to move from %1 to %2</translation>
</message>
<message>
<source>Failed to copy from %1 to %2</source>
<translation>Failed to copy from %1 to %2</translation>
</message>
<message>
<source>;;All files (*)</source>
<translation>;;All files (*)</translation>
</message>
<message>
<source>Move files to trash?</source>
<translation>Move files to trash?</translation>
</message>
<message>
<source>Do you want to move %1 files to trash?</source>
<translation>Do you want to move %1 files to trash?</translation>
</message>
<message>
<source>Failed to move file to trash</source>
<translation>Failed to move file to trash</translation>
</message>
<message>
<source>Failed to move file to trash %1</source>
<translation>Failed to move file to trash %1</translation>
</message>
<message>
<source>Move marked files to trash</source>
<translation>Move marked files to trash</translation>
</message>
<message>
<source>Moving marked files to trash</source>
<translation>Moving marked files to trash</translation>
</message>
<message>
<source>Export database to CSV</source>
<translation>Export database to CSV file</translation>
</message>
<message>
<source>CSV file (*.csv)</source>
<translation>CSV files (*.csv)</translation>
</message>
</context>
<context>
<name>MarkedFiles</name>
@@ -332,7 +458,7 @@
</message>
<message>
<source>Peaks draw</source>
<translation>Peaks draw</translation>
<translation type="vanished">Peaks draw</translation>
</message>
<message>
<source>FWHM X</source>
@@ -346,6 +472,17 @@
<source>Unsupported sample format</source>
<translation>Unsupported sample format</translation>
</message>
<message>
<source>Saturated</source>
<translation>Saturated</translation>
</message>
</context>
<context>
<name>STFSlider</name>
<message>
<source>Press Shift for fine tuning</source>
<translation>Press Shift for fine tuning</translation>
</message>
</context>
<context>
<name>SelectColumnsDialog</name>
@@ -354,6 +491,47 @@
<translation>Select columns</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<source>Settings</source>
<translation>Settings</translation>
</message>
<message>
<source>How many images are preloaded before and after current image.</source>
<translation>How many images are preloaded before and after current image.</translation>
</message>
<message>
<source>Thumbnail size in pixels</source>
<translation>Thumbnail size in pixels</translation>
</message>
<message>
<source>Image preload count</source>
<translation>Image preload count</translation>
</message>
<message>
<source>Thumbnails size</source>
<translation>Thumbnails size</translation>
</message>
<message>
<source>Changes in settings will take effect after program restart.</source>
<translation>Changes in settings will take effect after program restart.</translation>
</message>
<message>
<source>Don&apos;t use native file dialog</source>
<translation>Don&apos;t use native file dialog</translation>
</message>
<message>
<source>Set threshold value that is considered saturated when showing statistics.
For RAW files you may set 22%</source>
<translation>Set threshold value that is considered saturated when showing statistics.
For RAW files you may set 22%</translation>
</message>
<message>
<source>Saturation</source>
<translation>Saturated</translation>
</message>
</context>
<context>
<name>StretchToolbar</name>
<message>
@@ -372,13 +550,13 @@
<source>Invert colors</source>
<translation>Invert colors</translation>
</message>
<message>
<source>Superpixel CFA draw 2x2 pixel as one</source>
<translation>Superpixel CFA draw 2x2 pixel as one</translation>
</message>
<message>
<source>Apply auto stretch on load</source>
<translation>Apply auto stretch on load</translation>
</message>
<message>
<source>Debayer CFA</source>
<translation>Debayer CFA</translation>
</message>
</context>
</TS>
Binary file not shown.
+562
View File
@@ -0,0 +1,562 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr_FR" sourcelanguage="en">
<context>
<name>About</name>
<message>
<source>About Tenmon</source>
<translation>A propos de Tenmon</translation>
</message>
</context>
<context>
<name>DataBaseView</name>
<message>
<source>Select columns</source>
<translation>Choix des colonnes</translation>
</message>
<message>
<source>Text to search, you can % as wildcard</source>
<translation>Texte à chercher, utilisez % comme caractère générique</translation>
</message>
<message>
<source>Filter</source>
<translation>Filtre</translation>
</message>
</context>
<context>
<name>DatabaseTableView</name>
<message>
<source>Mark</source>
<translation>Marquer</translation>
</message>
<message>
<source>Unmark</source>
<translation>Décocher</translation>
</message>
</context>
<context>
<name>FITSFileModel</name>
<message>
<source>File name</source>
<translation>Nom de fichier</translation>
</message>
</context>
<context>
<name>Filetree</name>
<message>
<source>Open</source>
<translation>Ouvrir</translation>
</message>
<message>
<source>Copy marked files</source>
<translation>Copier les fichiers marqués</translation>
</message>
<message>
<source>Move marked files</source>
<translation>Déplacer les fichiers marqués</translation>
</message>
<message>
<source>Index directory</source>
<translation>Indexer le répertoire</translation>
</message>
<message>
<source>Set as root</source>
<translation>Définir comme répertoire racine</translation>
</message>
<message>
<source>Reset root</source>
<translation>Réinitialiser la racine</translation>
</message>
<message>
<source>Go up</source>
<translation>Monter</translation>
</message>
<message>
<source>Show hidden files</source>
<translation>Afficher les fichiers cachés</translation>
</message>
</context>
<context>
<name>HelpDialog</name>
<message>
<source>Help</source>
<translation>Aide</translation>
</message>
</context>
<context>
<name>ImageInfo</name>
<message>
<source>Property</source>
<translation>Propriété</translation>
</message>
<message>
<source>Value</source>
<translation>Valeur</translation>
</message>
<message>
<source>Comment</source>
<translation>Commentaire</translation>
</message>
<message>
<source>FITS Header</source>
<translation>En-tête FITS</translation>
</message>
<message>
<source>Image info</source>
<translation>Informations sur l&apos;image</translation>
</message>
</context>
<context>
<name>ImageRingList</name>
<message>
<source>Name</source>
<translation>Nom</translation>
</message>
</context>
<context>
<name>ImageWidget</name>
<message>
<source>OpenGL error</source>
<translation>Erreur OpenGL</translation>
</message>
<message>
<source>Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed.</source>
<translation>Impossible d&apos;initialiser le contexte OpenGL 3.3. Assurez-vous que le pilote GPU approprié est installé.</translation>
</message>
<message>
<source>L:%1</source>
<translation>L:%1</translation>
</message>
<message>
<source>X:%3 Y:%4</source>
<translation>X:%3 Y:%4</translation>
</message>
<message>
<source>R:%1 G:%2 B:%3</source>
<translation>R:%1 G:%2 B:%3</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<source>Image info</source>
<translation>Information sur l&apos;image</translation>
</message>
<message>
<source>Can&apos;t open DB</source>
<translation>Ne peut ouvrir la base de donnée</translation>
</message>
<message>
<source>Can&apos;t open SQLITE database</source>
<translation>Ne peut ouvrir la base de donnée SQLITE</translation>
</message>
<message>
<source>Filesystem</source>
<translation>Système de fichier</translation>
</message>
<message>
<source>Tenmon</source>
<translation>Tenmon</translation>
</message>
<message>
<source>File</source>
<translation>Fichier</translation>
</message>
<message>
<source>Open</source>
<translation>Ouvrir</translation>
</message>
<message>
<source>Copy marked files</source>
<translation>Copier les fichiers marqués</translation>
</message>
<message>
<source>Save as</source>
<translation>Enregistrer sous</translation>
</message>
<message>
<source>Live mode</source>
<translation>Mode temps réel</translation>
</message>
<message>
<source>Exit</source>
<translation>Sortir</translation>
</message>
<message>
<source>View</source>
<translation>Voir</translation>
</message>
<message>
<source>Zoom In</source>
<translation>Zoom avant</translation>
</message>
<message>
<source>Zoom Out</source>
<translation>Zoom arrière</translation>
</message>
<message>
<source>Best Fit</source>
<translation>Meilleur ajustement</translation>
</message>
<message>
<source>100%</source>
<translation>100%</translation>
</message>
<message>
<source>Fullscreen</source>
<translation type="vanished">Plein écran</translation>
</message>
<message>
<source>Select</source>
<translation>Sélectionner</translation>
</message>
<message>
<source>Mark</source>
<translation>Marquer</translation>
</message>
<message>
<source>Unmark</source>
<translation>Décocher</translation>
</message>
<message>
<source>Mark and next</source>
<translation>Marquer et suivant</translation>
</message>
<message>
<source>Unmark and next</source>
<translation>Décocher et suivant</translation>
</message>
<message>
<source>Analyze</source>
<translation>Analyse</translation>
</message>
<message>
<source>Image statistics</source>
<translation>Statistiques de l&apos;image</translation>
</message>
<message>
<source>Peak finder</source>
<translation>Détecteur de pic</translation>
</message>
<message>
<source>Docks</source>
<translation>Fenêtres encrables</translation>
</message>
<message>
<source>Open file</source>
<translation>Ouvrir le ficher</translation>
</message>
<message>
<source>Select destination</source>
<translation>Choisir la destination</translation>
</message>
<message>
<source>Copying</source>
<translation>Copier</translation>
</message>
<message>
<source>Cancel</source>
<translation>Abandon</translation>
</message>
<message>
<source>Move marked files</source>
<translation>Déplacer les fichiers marqués</translation>
</message>
<message>
<source>Index directory</source>
<translation>Indexer le répertoire</translation>
</message>
<message>
<source>Thumbnails</source>
<translation>Vignettes</translation>
</message>
<message>
<source>Show marked</source>
<translation>Afficher marqué</translation>
</message>
<message>
<source>Help</source>
<translation>Aide</translation>
</message>
<message>
<source>About Tenmon</source>
<translation>A propos de Tenmon</translation>
</message>
<message>
<source>About Qt</source>
<translation>A propos de Qt</translation>
</message>
<message>
<source>Moving</source>
<translation>Déplacement</translation>
</message>
<message>
<source>Indexing FITS files</source>
<translation>Indexation des fichiers FITS</translation>
</message>
<message>
<source>Reindex files</source>
<translation>-indexer les fichiers</translation>
</message>
<message>
<source>FITS/XISF files database</source>
<translation>Base de donnée FITS/XISF</translation>
</message>
<message>
<source>File tree</source>
<translation>Arborescence de fichiers</translation>
</message>
<message>
<source>Star finder</source>
<translation>Détecteur d&apos;étoiles</translation>
</message>
<message>
<source>Edit</source>
<translation>Éditer</translation>
</message>
<message>
<source>FITS header editor</source>
<translation type="vanished">Éditeur d&apos;en-tête FITS</translation>
</message>
<message>
<source>Settings</source>
<translation>Réglages</translation>
</message>
<message>
<source>Images (</source>
<translation>Images (</translation>
</message>
<message>
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
<translation>FITS image (*.fits *.fit);;XISF image (*.xisf);;</translation>
</message>
<message>
<source>Failed to copy</source>
<translation>Échec de la copie</translation>
</message>
<message>
<source>Failed to move</source>
<translation>Échec du déplacement</translation>
</message>
<message>
<source>Failed to move from %1 to %2</source>
<translation>Échec du déplacement de %1 vers %2</translation>
</message>
<message>
<source>Failed to copy from %1 to %2</source>
<translation>Échec de la copie de %1 vers %2</translation>
</message>
<message>
<source>;;All files (*)</source>
<translation>;;Tout les fichiers (*)</translation>
</message>
<message>
<source>Move files to trash?</source>
<translation>Déplacer les fichiers dans la corbeille?</translation>
</message>
<message>
<source>Do you want to move %1 files to trash?</source>
<translation>Voulez-vous déplacer le fichier %1 dans la corbeille?</translation>
</message>
<message>
<source>Failed to move file to trash</source>
<translation>Echec du déplacement dans la corbeille</translation>
</message>
<message>
<source>Failed to move file to trash %1</source>
<translation>Echec du déplacement de %1 dans la corbeille</translation>
</message>
<message>
<source>Move marked files to trash</source>
<translation>Déplacer les fichiers marqués dans la corbeille</translation>
</message>
<message>
<source>Moving marked files to trash</source>
<translation>Déplacement des fichiers marqués dans la corbeille</translation>
</message>
<message>
<source>Export database to CSV</source>
<translation>Exporter la base de données vers un fichier CSV</translation>
</message>
<message>
<source>CSV file (*.csv)</source>
<translation>Fichiers CSV (*.csv)</translation>
</message>
</context>
<context>
<name>MarkedFiles</name>
<message>
<source>Marked files</source>
<translation>Fichiers marqués</translation>
</message>
<message>
<source>Filename</source>
<translation>Nom de fichier</translation>
</message>
<message>
<source>Clear selected</source>
<translation>Effacer la sélection</translation>
</message>
<message>
<source>Clear all</source>
<translation>Effacer tout</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<source>ISO</source>
<translation>ISO</translation>
</message>
<message>
<source>Shutter speed</source>
<translation>Vitesse d&apos;obturation</translation>
</message>
<message>
<source>Width</source>
<translation>Largeur</translation>
</message>
<message>
<source>Height</source>
<translation>Hauteur</translation>
</message>
<message>
<source>Error</source>
<translation>Erreur</translation>
</message>
<message>
<source>Filename</source>
<translation>Nom de fichier</translation>
</message>
<message>
<source>Mean</source>
<translation>Moyenne</translation>
</message>
<message>
<source>Standart deviation</source>
<translation>Écart-type</translation>
</message>
<message>
<source>Median</source>
<translation>Médiane</translation>
</message>
<message>
<source>Minimum</source>
<translation>Minimum</translation>
</message>
<message>
<source>Maximum</source>
<translation>Maximum</translation>
</message>
<message>
<source>MAD</source>
<translation>MAD</translation>
</message>
<message>
<source>Peaks</source>
<translation>Pics</translation>
</message>
<message>
<source>Peaks draw</source>
<translation type="vanished">Dessin des pic</translation>
</message>
<message>
<source>FWHM X</source>
<translation>FWHM X</translation>
</message>
<message>
<source>FWHM Y</source>
<translation>FWHM Y</translation>
</message>
<message>
<source>Unsupported sample format</source>
<translation>Format non pris en charge</translation>
</message>
<message>
<source>Saturated</source>
<translation>Saturé</translation>
</message>
</context>
<context>
<name>STFSlider</name>
<message>
<source>Press Shift for fine tuning</source>
<translation>Appuyez sur Shift pour un réglage fin</translation>
</message>
</context>
<context>
<name>SelectColumnsDialog</name>
<message>
<source>Select columns</source>
<translation>Choix des colonnes</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<source>Settings</source>
<translation>Réglages</translation>
</message>
<message>
<source>How many images are preloaded before and after current image.</source>
<translation>Combien d&apos;images sont préchargées avant et après l&apos;image courante.</translation>
</message>
<message>
<source>Thumbnail size in pixels</source>
<translation>Taille des vignettes en pixels</translation>
</message>
<message>
<source>Image preload count</source>
<translation>Nombre d&apos;images préchargées</translation>
</message>
<message>
<source>Thumbnails size</source>
<translation>Taille des vignette</translation>
</message>
<message>
<source>Changes in settings will take effect after program restart.</source>
<translation>Les changements de paramètres prendront effet après le redémarrage du programme.</translation>
</message>
<message>
<source>Don&apos;t use native file dialog</source>
<translation>N&apos;utilisez pas la boîte de dialogue de fichier natif</translation>
</message>
<message>
<source>Set threshold value that is considered saturated when showing statistics.
For RAW files you may set 22%</source>
<translation>Définissez la valeur seuil qui est considérée comme saturée lors de l&apos;affichage des statistiques.
Pour les fichiers RAW, vous pouvez définir 22&#xa0;%</translation>
</message>
<message>
<source>Saturation</source>
<translation>Saturé</translation>
</message>
</context>
<context>
<name>StretchToolbar</name>
<message>
<source>Stretch toolbar</source>
<translation>Réglage de la luminosité</translation>
</message>
<message>
<source>Auto Stretch F12</source>
<translation>Luminosité automatique F12</translation>
</message>
<message>
<source>Reset Screen Transfer Function F11</source>
<translation>Réinitialiser la fonction de transfert d&apos;écran F11</translation>
</message>
<message>
<source>Invert colors</source>
<translation>Inverser les couleurs</translation>
</message>
<message>
<source>Apply auto stretch on load</source>
<translation>Appliquer la luminosité automatiquement au chargement</translation>
</message>
<message>
<source>Debayer CFA</source>
<translation>Débayeriser CFA</translation>
</message>
</context>
</TS>
Binary file not shown.
+196 -30
View File
@@ -24,6 +24,17 @@
<translation>Filter</translation>
</message>
</context>
<context>
<name>DatabaseTableView</name>
<message>
<source>Mark</source>
<translation>Označiť</translation>
</message>
<message>
<source>Unmark</source>
<translation>Odznačiť</translation>
</message>
</context>
<context>
<name>FITSFileModel</name>
<message>
@@ -31,6 +42,41 @@
<translation>Meno súboru</translation>
</message>
</context>
<context>
<name>Filetree</name>
<message>
<source>Open</source>
<translation>Otvoriť</translation>
</message>
<message>
<source>Copy marked files</source>
<translation>Skopírovať označené súbory</translation>
</message>
<message>
<source>Move marked files</source>
<translation>Presunúť označené súbory</translation>
</message>
<message>
<source>Index directory</source>
<translation>Indexovať adresár</translation>
</message>
<message>
<source>Set as root</source>
<translation>Nastav koreňový adresár</translation>
</message>
<message>
<source>Reset root</source>
<translation>Resetuj koreňový adresár</translation>
</message>
<message>
<source>Go up</source>
<translation>O úroveň vyššie</translation>
</message>
<message>
<source>Show hidden files</source>
<translation>Zobraz skryté súbory</translation>
</message>
</context>
<context>
<name>HelpDialog</name>
<message>
@@ -78,6 +124,18 @@
<source>Could not initialize OpenGL 3.3 context. Ensure that proper GPU driver is installed.</source>
<translation>Nepodarilo sa incializovať OpenGL 3.3 context. Ubezpečte sa že nainštalované ovládače pre GPU.</translation>
</message>
<message>
<source>L:%1</source>
<translation>L:%1</translation>
</message>
<message>
<source>X:%3 Y:%4</source>
<translation>X:%3 Y:%4</translation>
</message>
<message>
<source>R:%1 G:%2 B:%3</source>
<translation>R:%1 G:%2 B:%3</translation>
</message>
</context>
<context>
<name>MainWindow</name>
@@ -97,14 +155,6 @@
<source>Filesystem</source>
<translation>Zoznam súborov</translation>
</message>
<message>
<source>FITS Editor</source>
<translation type="vanished">FITS editor</translation>
</message>
<message>
<source>FITS files database</source>
<translation type="vanished">Databáza FITS súborov</translation>
</message>
<message>
<source>Tenmon</source>
<translation>Tenmon</translation>
@@ -151,7 +201,7 @@
</message>
<message>
<source>Fullscreen</source>
<translation>Celá obrazovka</translation>
<translation type="vanished">Celá obrazovka</translation>
</message>
<message>
<source>Select</source>
@@ -189,14 +239,6 @@
<source>Open file</source>
<translation>Otvoriť súbor</translation>
</message>
<message>
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)</source>
<translation type="vanished">Obrázky (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)</translation>
</message>
<message>
<source>Images (*.jpg *.png *.JPG *.PNG)</source>
<translation type="vanished">Obrázky (*.jpg *.png *.JPG *.PNG)</translation>
</message>
<message>
<source>Select destination</source>
<translation>Vybrať cieľ</translation>
@@ -249,18 +291,10 @@
<source>Moving</source>
<translation>Presúvanie</translation>
</message>
<message>
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</source>
<translation>Obrázky (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</translation>
</message>
<message>
<source>Indexing FITS files</source>
<translation>Indexovanie FITS/XISF súborov</translation>
</message>
<message>
<source>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</source>
<translation>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</translation>
</message>
<message>
<source>Reindex files</source>
<translation>Reindexuj súbory</translation>
@@ -269,6 +303,86 @@
<source>FITS/XISF files database</source>
<translation>Databáza FITS/XISF súborov</translation>
</message>
<message>
<source>File tree</source>
<translation>Strom súborov</translation>
</message>
<message>
<source>Star finder</source>
<translation>Vyhľadávač hviezd</translation>
</message>
<message>
<source>Edit</source>
<translation>Upraviť</translation>
</message>
<message>
<source>FITS header editor</source>
<translation type="vanished">Editor FITS hlavičky</translation>
</message>
<message>
<source>Settings</source>
<translation>Nastavenia</translation>
</message>
<message>
<source>Images (</source>
<translation>Obrázky (</translation>
</message>
<message>
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
<translation>Obrázok FITS (*.fits *.fit);;Obrázok XISF (*.xisf);;</translation>
</message>
<message>
<source>Failed to copy</source>
<translation>Zlyhalo kopírovanie</translation>
</message>
<message>
<source>Failed to move</source>
<translation>Zlyhalo presúvanie</translation>
</message>
<message>
<source>Failed to move from %1 to %2</source>
<translation>Zlyhalo presúvanie z %1 do %2</translation>
</message>
<message>
<source>Failed to copy from %1 to %2</source>
<translation>Zlyhalo kopírovanie z %1 do %2</translation>
</message>
<message>
<source>;;All files (*)</source>
<translation>;;Všetky súbory (*)</translation>
</message>
<message>
<source>Move files to trash?</source>
<translation>Presunúť súbory do koša?</translation>
</message>
<message>
<source>Do you want to move %1 files to trash?</source>
<translation>Presunúť %1 súborov do koša?</translation>
</message>
<message>
<source>Failed to move file to trash</source>
<translation>Zlyhalo presunutie súbora do koša</translation>
</message>
<message>
<source>Failed to move file to trash %1</source>
<translation>Zlyhalo presunutie súbora do koša %1</translation>
</message>
<message>
<source>Move marked files to trash</source>
<translation>Presunúť označené súbory do koša</translation>
</message>
<message>
<source>Moving marked files to trash</source>
<translation>Presúvanie do koša</translation>
</message>
<message>
<source>Export database to CSV</source>
<translation>Exportovať databázu do CSV súboru</translation>
</message>
<message>
<source>CSV file (*.csv)</source>
<translation>Súbory CSV (*.csv)</translation>
</message>
</context>
<context>
<name>MarkedFiles</name>
@@ -349,7 +463,7 @@
</message>
<message>
<source>Peaks draw</source>
<translation>Vykreslené vrcholky</translation>
<translation type="vanished">Vykreslené vrcholky</translation>
</message>
<message>
<source>FWHM X</source>
@@ -359,6 +473,17 @@
<source>FWHM Y</source>
<translation>FWHM Y</translation>
</message>
<message>
<source>Saturated</source>
<translation>Saturované</translation>
</message>
</context>
<context>
<name>STFSlider</name>
<message>
<source>Press Shift for fine tuning</source>
<translation>Stlačte Shift pre jemné ladenie</translation>
</message>
</context>
<context>
<name>SelectColumnsDialog</name>
@@ -367,6 +492,47 @@
<translation>Výber stĺpcov</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<source>Settings</source>
<translation>Nastavenia</translation>
</message>
<message>
<source>How many images are preloaded before and after current image.</source>
<translation>Koľko obrázkov sa prednačíta pred a za aktuálnym obrázkom.</translation>
</message>
<message>
<source>Thumbnail size in pixels</source>
<translation>Veľkosť náhľadu v pixeloch</translation>
</message>
<message>
<source>Image preload count</source>
<translation>Počet prednačítaných obrázkov</translation>
</message>
<message>
<source>Thumbnails size</source>
<translation>Veľkosť náhľadu</translation>
</message>
<message>
<source>Changes in settings will take effect after program restart.</source>
<translation>Zmeny v nastaveniach sa prejavia po reštarte programu.</translation>
</message>
<message>
<source>Don&apos;t use native file dialog</source>
<translation>Nepoužívať natívny súborový dialóg</translation>
</message>
<message>
<source>Set threshold value that is considered saturated when showing statistics.
For RAW files you may set 22%</source>
<translation>Nastavuje prahovú hodnotu ktorá sa považuje za saturovanú.
Pre RAW súbory možno treba nastaviť 22%</translation>
</message>
<message>
<source>Saturation</source>
<translation>Saturované</translation>
</message>
</context>
<context>
<name>StretchToolbar</name>
<message>
@@ -385,13 +551,13 @@
<source>Invert colors</source>
<translation>Invertuj farby</translation>
</message>
<message>
<source>Superpixel CFA draw 2x2 pixel as one</source>
<translation>Super pixel CFA kreslenie 2x2 pixelov ako jeden</translation>
</message>
<message>
<source>Apply auto stretch on load</source>
<translation>Aplikuj automatické natiahnutie pri načítaní</translation>
</message>
<message>
<source>Debayer CFA</source>
<translation>Preveď CFA na farbu</translation>
</message>
</context>
</TS>