diff --git a/.gitignore b/.gitignore index 5485831..8c4ebe9 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ venv.bak/ # mypy .mypy_cache/ +/.idea diff --git a/VERSION b/VERSION index 6d7de6e..781dcb0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.2 +1.1.3 diff --git a/handyview/actions.py b/handyview/actions.py index bda496f..e3bb56e 100644 --- a/handyview/actions.py +++ b/handyview/actions.py @@ -43,6 +43,11 @@ def history(parent): return new_action(parent, 'History', icon_name='history.png', slot=parent.open_history) +def LUT(parent): + """Show open LUT.""" + return new_action(parent, 'LUT', icon_name='LUT.png', slot=parent.open_LUT) + + # --------------------------------------- # refresh and index # --------------------------------------- @@ -138,6 +143,7 @@ def set_fingerprint(parent): # --------------------------------------- # auto zoom # --------------------------------------- + def auto_zoom(parent): return new_action(parent, 'Auto Zoom', icon_name='auto_zoom.png', slot=parent.auto_zoom) diff --git a/handyview/canvas.py b/handyview/canvas.py index 3a7ffad..6a5b642 100644 --- a/handyview/canvas.py +++ b/handyview/canvas.py @@ -1,11 +1,14 @@ import os + +import numpy as np from PyQt5 import QtCore -from PyQt5.QtGui import QColor, QImage, QPainter, QPen, QPixmap +from PyQt5.QtGui import QColor, QImage, QPainter, QPen, QPixmap, qRgb from PyQt5.QtWidgets import QApplication, QGridLayout, QSplitter, QWidget from handyview.view_scene import HVScene, HVView from handyview.widgets import ColorLabel, HVLable, show_msg - +# For LUT +import cv2 class Canvas(QWidget): """Main canvas""" @@ -192,7 +195,7 @@ def add_cmp_folder(self, cmp_path): show_str = 'Number for each folder:\n\t' + '\n\t'.join(map(str, img_len_list)) self.comparison_label.setText(show_str) if is_same_len is False: - msg = f'Comparison folders have differnet number of images.\n{show_str}' + msg = f'Comparison folders have different number of images.\n{show_str}' show_msg('Warning', 'Warning!', msg) # refresh self.show_image() @@ -202,7 +205,7 @@ def update_path_list(self): show_str = 'Comparison:\n # for each folder:\n\t' + '\n\t'.join(map(str, img_len_list)) self.comparison_label.setText(show_str) if is_same_len is False: - msg = f'Comparison folders have differnet number of images.\n{show_str}' + msg = f'Comparison folders have different number of images.\n{show_str}' show_msg('Warning', 'Warning!', msg) def compare_folders(self, step): @@ -239,12 +242,62 @@ def show_image(self, init=False): md5, phash = self.db.get_fingerprint(fidx=fidx) md5_0, phash_0 = self.db.get_fingerprint(fidx=self.db.fidx) - qimg = QImage(img_path) + # get LUT and processing + cv2_lut, cv2_lut_name = self.db.get_LUT() + processing = self.db.get_processing() + + # None use directly Qt + if cv2_lut == 0 and processing == "None": + # no LUT --> use QImage function + qimg = QImage(img_path) # using Qt to load the image + else: + # Using OpenCV for image processing w/o LUT + if cv2_lut == 0 and processing != "None": + # No LUT but processing (on RGB) + cvimg = cv2.imread(img_path, flags=cv2.IMREAD_COLOR) + + # Apply the different processing params + if processing == "Shift 2 Bit": + cvimgproc = cvimg * 4 + elif processing == "Histogram equalization": + cvimgyuv = cv2.cvtColor(cvimg, cv2.COLOR_BGR2YUV) + # equalize the histogram of the Y channel + cvimgyuv[:, :, 0] = cv2.equalizeHist(cvimgyuv[:, :, 0]) + + # convert the YUV image back to RGB format + cvimgproc = cv2.cvtColor(cvimgyuv, cv2.COLOR_YUV2BGR) + else: + cvimgproc = cvimg + + # Using OpenCV for image processing w LUT + if cv2_lut != 0: + # using OpenCV to load the image in gray and apply the LUT + cvimg = cv2.imread(img_path, flags=cv2.IMREAD_GRAYSCALE) + + # Apply the different processing params + if processing == "Shift 2 Bit": + cvimgproc = cvimg * 4 + elif processing == "Histogram equalization": + cvimgproc = cv2.equalizeHist(cvimg) + else: + cvimgproc = cvimg + + # Apply LUT + if cv2_lut != 0: + # Apply LUT + lutImg = cv2.applyColorMap(cvimgproc, cv2_lut) + else: + lutImg = cvimgproc + + # finally set image for Qt + qimg = QImage(lutImg.data, lutImg.shape[1], lutImg.shape[0], lutImg.strides[0], QImage.Format_RGB888) + self.img_path = img_path if idx == 0: # for HVView, HVScene show_mouse_color. # only work on the first qimg (main canvas mode) self.qimg = qimg + # show image path in the statusbar self.parent.set_statusbar(f'{img_path}') @@ -275,6 +328,13 @@ def get_parent_dir(path, levels=1): f'[{shown_idx:d} / {self.db.get_path_len():d}] {tail}', head, f'{height:d} x {width:d}, {file_size}', f'{color_type}' ] + + # Add processing and LUT + if processing != "None": + shown_text.append(" Processing:" + processing) + if cv2_lut != 0: + shown_text.append(" LUT:" + cv2_lut_name) + # show fingerprint if self.show_fingerprint: if idx > 0: @@ -287,10 +347,11 @@ def get_parent_dir(path, levels=1): shown_text.append(f'phash: {phash}') if qview.hasFocus(): - color = 'red' - else: color = 'green' + else: + color = 'red' qview.set_shown_text(shown_text, color) + # qview.viewport().update() qpixmap = QPixmap.fromImage(qimg) diff --git a/handyview/canvas_crop.py b/handyview/canvas_crop.py index d51b225..c994bb1 100644 --- a/handyview/canvas_crop.py +++ b/handyview/canvas_crop.py @@ -143,7 +143,6 @@ def init_widgets_layout(self): button_open_rect.clicked.connect(self.open_rect_folder) button_open_history = QPushButton('Open History Info', self) button_open_history.clicked.connect(self.open_history_file) - button_delete_patch = QPushButton('Delete Patch Folder', self) button_delete_patch.clicked.connect(self.delete_patch_folder) button_delete_rect = QPushButton('Delete Rect Folder', self) @@ -162,9 +161,9 @@ def init_widgets_layout(self): action_grid.addWidget(button_open_patch, 4, 0, 1, 1) action_grid.addWidget(button_open_rect, 5, 0, 1, 1) action_grid.addWidget(button_open_history, 6, 0, 1, 1) - action_grid.addWidget(HLine(), 7, 0, 1, 1) - action_grid.addWidget(button_delete_patch, 8, 0, 1, 1) - action_grid.addWidget(button_delete_rect, 9, 0, 1, 1) + action_grid.addWidget(HLine(), 8, 0, 1, 1) + action_grid.addWidget(button_delete_patch, 9, 0, 1, 1) + action_grid.addWidget(button_delete_rect, 10, 0, 1, 1) config_box = QGroupBox('Config') config_box.setLayout(config_grid) @@ -304,5 +303,13 @@ def open_history_file(self): except Exception as error: show_msg(icon='Critical', title='Title', text=f'Open error: {error}', timeout=None) + def open_LUT(selfself): + try: + # TODO + show_msg(icon='Info', title='LUT', text='LUT', timeout=None) + + except Exception as error: + show_msg(icon='Critical', title='Title', text=f'Open error: {error}', timeout=None) + def keyPressEvent(self, event): pass diff --git a/handyview/db.py b/handyview/db.py index c2e108c..ba1e099 100644 --- a/handyview/db.py +++ b/handyview/db.py @@ -2,6 +2,7 @@ import imagehash import os from PIL import Image, ImageFile +import cv2 from handyview.utils import FORMATS, ROOT_DIR, get_img_list, scandir, sizeof_fmt from handyview.widgets import show_msg @@ -10,6 +11,11 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True Image.MAX_IMAGE_PIXELS = None +LUT_Names = ["None", "Jet", "Cool", "Hot", "HSV"] +LUT_Values = [0, cv2.COLORMAP_JET, cv2.COLORMAP_COOL, cv2.COLORMAP_HOT, cv2.COLORMAP_HSV] + +processing_names = ["None", "Shift 2 Bit", "Histogram equalization"] + class HVDB(): """HandyView database. @@ -45,6 +51,10 @@ def __init__(self, init_path): self.get_init_path_list() + # LUT + self.use_LUT = "None" + self.useprocessing = "None" + def get_init_path_list(self): """get path list when first launch (double click or from cmd)""" # if init_path is a folder, try to get the first image @@ -158,6 +168,24 @@ def get_folder(self, folder=None, fidx=None): folder = self.folder_list[fidx] return folder + def get_LUT(self): + if self.use_LUT is None: + self.use_LUT = "None" + cv_lut_name = LUT_Names.index(self.use_LUT) + cv_lut_id = LUT_Values[cv_lut_name] + return cv_lut_id, self.use_LUT + + def get_processing(self): + if self.useprocessing is None: + self.useprocessing = "None" + return self.useprocessing + + def get_all_LUTs(self): + return LUT_Names + + def get_all_processings(self): + return processing_names + def get_path(self, fidx=None, pidx=None): if fidx is None: fidx = self._fidx diff --git a/handyview/handyviewer.py b/handyview/handyviewer.py index 1a5372c..e697827 100644 --- a/handyview/handyviewer.py +++ b/handyview/handyviewer.py @@ -1,9 +1,11 @@ import os +import locale import sys from PyQt5 import QtCore from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import (QApplication, QDockWidget, QFileDialog, QGridLayout, QInputDialog, QLabel, QLineEdit, - QMainWindow, QTabWidget, QToolBar, QVBoxLayout, QWidget) + QMainWindow, QTabWidget, QToolBar, QVBoxLayout, QWidget, + QDialog, QDialogButtonBox, QComboBox) import handyview.actions as actions from handyview.canvas import Canvas @@ -46,9 +48,20 @@ def __init__(self, parent, hvdb): self.canvas = Canvas(self, hvdb) self.canvas_crop = CanvasCrop(self, hvdb) self.canvas_video = CanvasVideo(self) - self.tabs.addTab(self.canvas, 'View 图像') - self.tabs.addTab(self.canvas_crop, 'Crop 裁剪') - self.tabs.addTab(self.canvas_video, 'Video 视频') + + # find the local language setting + windll = ctypes.windll.kernel32 + lng = locale.windows_locale[windll.GetUserDefaultUILanguage()] + if lng == "cn_CN": + tabText = [" 图像", " 裁剪", " 视频"] + self.lng = "CN" + else: + tabText = ["", "", ""] + self.lng = "EN" + + self.tabs.addTab(self.canvas, 'View' + tabText[0]) + self.tabs.addTab(self.canvas_crop, 'Crop' + tabText[1]) + self.tabs.addTab(self.canvas_video, 'Video' + tabText[2]) self.tabs.setTabIcon(0, QIcon(os.path.join(ROOT_DIR, 'icons/image.png'))) self.tabs.setTabIcon(1, QIcon(os.path.join(ROOT_DIR, 'icons/crop.png'))) self.tabs.setTabIcon(2, QIcon(os.path.join(ROOT_DIR, 'icons/video.png'))) @@ -110,10 +123,19 @@ def init_menubar(self): # create menubar menubar = self.menuBar() - # File - file_menu = menubar.addMenu('&File(文件)') + # find the local language setting + windll = ctypes.windll.kernel32 + lng = locale.windows_locale[windll.GetUserDefaultUILanguage()] + if lng == "cn_CN": + tabText = ["(比较)", "(布局)", "(选项卡)", "(查看)", "(帮助)", "(文件)"] + else: + tabText = ["", "", "", "", "", "", ""] + + # File Menu + file_menu = menubar.addMenu('&File' + tabText[5]) file_menu.addAction(actions.open(self)) file_menu.addAction(actions.history(self)) + file_menu.addAction(actions.LUT(self)) file_menu.addSeparator() file_menu.addAction(actions.refresh(self)) file_menu.addAction(actions.goto_index(self)) @@ -128,29 +150,29 @@ def init_menubar(self): # draw_menu = menubar.addMenu('&Draw(画图)') # noqa: F841 # Compare - compare_menu = menubar.addMenu('&Compare(比较)') + compare_menu = menubar.addMenu('&Compare' + tabText[0]) compare_menu.addAction(actions.compare(self)) compare_menu.addAction(actions.clear_compare(self)) compare_menu.addAction(actions.set_fingerprint(self)) # Layouts - layout_menu = menubar.addMenu('&Layout(布局)') + layout_menu = menubar.addMenu('&Layout' + tabText[1]) layout_menu.addAction(actions.switch_main_canvas(self)) layout_menu.addAction(actions.switch_compare_canvas(self)) - layout_menu.addAction(actions.switch_preview_canvas(self)) + # layout_menu.addAction(actions.switch_preview_canvas(self)) # Tabs - layout_menu = menubar.addMenu('&Tabs(选项卡)') + layout_menu = menubar.addMenu('&Tabs' + tabText[2]) layout_menu.addAction(actions.select_basic_tab(self)) layout_menu.addAction(actions.select_crop_tab(self)) layout_menu.addAction(actions.select_video_tab(self)) # View - layout_menu = menubar.addMenu('&View(查看)') + layout_menu = menubar.addMenu('&View' + tabText[3]) layout_menu.addAction(actions.auto_zoom_dialog(self)) # Help - help_menu = menubar.addMenu('&Help(帮助)') + help_menu = menubar.addMenu('&Help' + tabText[4]) help_menu.addAction(actions.show_instruction_msg(self)) def init_toolbar(self): @@ -160,6 +182,7 @@ def init_toolbar(self): # open and history self.toolbar.addAction(actions.open(self)) self.toolbar.addAction(actions.history(self)) + self.toolbar.addAction(actions.LUT(self)) self.toolbar.addSeparator() # refresh and index self.toolbar.addAction(actions.refresh(self)) @@ -173,12 +196,16 @@ def init_toolbar(self): self.toolbar.addAction(actions.compare(self)) self.toolbar.addAction(actions.clear_compare(self)) + # exit + # self.toolbar.addSeparator() + # self.toolbar.addAction(actions.exit_UI(self)) + # canvas layout self.toolbar.addSeparator() self.toolbar.addSeparator() self.toolbar.addAction(actions.switch_main_canvas(self)) self.toolbar.addAction(actions.switch_compare_canvas(self)) - self.toolbar.addAction(actions.switch_preview_canvas(self)) + # self.toolbar.addAction(actions.switch_preview_canvas(self)) # others self.toolbar.addSeparator() @@ -286,6 +313,19 @@ def open_history(self): self.center_canvas.canvas_crop.update_db(self.hvdb) self.empty = False + def open_LUT(self): + # open LUT and processing dialog + use_LUT, useprocessing, ok = LUTDialog.getLUTProcessing() + + if ok: + # set processing parameters + self.hvdb.use_LUT = use_LUT + self.hvdb.useprocessing = useprocessing + # Update UI + self.center_canvas.canvas.show_image(init=True) + self.center_canvas.canvas_crop.update_db(self.hvdb) + self.empty = False + # --------------------------------------- # slots: refresh and index # --------------------------------------- @@ -415,7 +455,8 @@ def switch_compare_canvas(self): self.canvas_type = 'compare' def switch_preview_canvas(self): - show_msg('Information', '^_^', text=('Has not implemented yet.\nContributions are welcome!\n尚未实现, 欢迎贡献!')) + show_msg('Information', '^_^', + text=('Has not implemented yet.\nContributions are welcome!\n尚未实现, 欢迎贡献!')) # --------------------------------------- # slots: canvas tabs @@ -463,6 +504,38 @@ def auto_zoom_dialog(self): self.center_canvas.canvas.show_image(init=False) +class LUTDialog(QDialog): + def __init__(self, parent=None): + super(LUTDialog, self).__init__(parent) + + self.LUTs = HVDB.get_all_LUTs(self) + self.processings = HVDB.get_all_processings(self) + + layout = QVBoxLayout(self) + self.comboboxLUT = QComboBox() + self.comboboxLUT.addItems(self.LUTs) + layout.addWidget(self.comboboxLUT) + + self.comboboxProcessing = QComboBox() + self.comboboxProcessing.addItems(self.processings) + layout.addWidget(self.comboboxProcessing) + + # OK and Cancel buttons + self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + layout.addWidget(self.buttons) + self.buttons.accepted.connect(self.accept) + self.buttons.rejected.connect(self.reject) + + # static method to create the dialog and return values + @staticmethod + def getLUTProcessing(parent=None): + dialog = LUTDialog(parent) + result = dialog.exec_() + useLUT = dialog.comboboxLUT.currentText() + useprocessing = dialog.comboboxProcessing.currentText() + return useLUT, useprocessing, result == QDialog.Accepted + + def create_new_window(init_path=None): screen = app.primaryScreen() size = screen.size() @@ -478,9 +551,11 @@ def create_new_window(init_path=None): if __name__ == '__main__': import platform + if platform.system() == 'Windows': # set the icon in the task bar import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('HandyView') print('Welcome to HandyView.') diff --git a/handyview/utils.py b/handyview/utils.py index 43e614d..b933261 100644 --- a/handyview/utils.py +++ b/handyview/utils.py @@ -52,22 +52,25 @@ def scandir(dir_path, suffix=None, recursive=False, full_path=False): root = dir_path def _scandir(dir_path, suffix, recursive): - for entry in os.scandir(dir_path): - if not entry.name.startswith('.') and entry.is_file(): - if full_path: - return_path = entry.path + try: + for entry in os.scandir(dir_path): + if not entry.name.startswith('.') and entry.is_file(): + if full_path: + return_path = entry.path + else: + return_path = os.path.relpath(entry.path, root) + + if suffix is None: + yield return_path + elif return_path.endswith(suffix): + yield return_path else: - return_path = os.path.relpath(entry.path, root) - - if suffix is None: - yield return_path - elif return_path.endswith(suffix): - yield return_path - else: - if recursive: - yield from _scandir(entry.path, suffix=suffix, recursive=recursive) - else: - continue + if recursive: + yield from _scandir(entry.path, suffix=suffix, recursive=recursive) + else: + continue + except Exception: + return_path = os.path.relpath(entry.path, root) return _scandir(dir_path, suffix=suffix, recursive=recursive) diff --git a/how_to_build.md b/how_to_build.md index d3fad78..9e8f5e7 100644 --- a/how_to_build.md +++ b/how_to_build.md @@ -21,7 +21,7 @@ Using Anaconda for building will produce app with a very large file size, so we ```bash pip3 install virtualenv virtualenv venv - source venv/bin/activate + source venv/bin/activat pip install -r requirements.txt ``` 3. Use pyinstaller to build diff --git a/icons/LUT.png b/icons/LUT.png new file mode 100644 index 0000000..2265740 Binary files /dev/null and b/icons/LUT.png differ diff --git a/pyinstaller_install_win.cmd b/pyinstaller_install_win.cmd new file mode 100644 index 0000000..8b94c44 --- /dev/null +++ b/pyinstaller_install_win.cmd @@ -0,0 +1 @@ +pyinstaller -F handyview/handyviewer.py -i icon.ico --add-data="handyview;handyview" --add-data="icons;icons" --add-data="icon.png;." --add-data="icon.ico;." --add-data="VERSION;." --windowed --noconfirm --hidden-import="PIL.Image" --hidden-import="PIL.ImageDraw" --hidden-import="PyQt5.QtechoMultimedia" --hidden-import="PyQt5.QtMultimediaWidgets" --hidden-import="imagehash" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 57f72b9..054ff2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ -Pillow -imagehash -pyqt5 +Pillow~=8.4.0 +imagehash~=4.3.1 +pyqt5~=5.15.6 +setuptools~=59.6.0 +numpy~=1.19.5 +opencv-python==4.1.2.30 \ No newline at end of file