From cd7bca4de0c044ba60cf4cce31174eef8698eae7 Mon Sep 17 00:00:00 2001 From: tseiffert <37099995+tseiffert@users.noreply.github.com> Date: Sun, 27 Aug 2023 17:53:24 +0200 Subject: [PATCH] Add LUT and processing with OpenCV Add LUT and some smal preprocessing with OpenCV --- .gitignore | 1 + VERSION | 2 +- handyview/actions.py | 6 +++ handyview/canvas.py | 75 +++++++++++++++++++++++--- handyview/canvas_crop.py | 15 ++++-- handyview/db.py | 28 ++++++++++ handyview/handyviewer.py | 103 +++++++++++++++++++++++++++++++----- handyview/utils.py | 33 ++++++------ how_to_build.md | 2 +- icons/LUT.png | Bin 0 -> 19686 bytes pyinstaller_install_win.cmd | 1 + requirements.txt | 9 ++-- 12 files changed, 230 insertions(+), 45 deletions(-) create mode 100644 icons/LUT.png create mode 100644 pyinstaller_install_win.cmd 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 0000000000000000000000000000000000000000..2265740097cb6796b605b7d8bff211862d526631 GIT binary patch literal 19686 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�eY#&^y(NhSE+GC~>9L0tkEic7=tkUNTkZREh#(8e?%O zAjBAK%VCd^`t_f0#-7>=#SI{?At5#~X2(Ot+SZP2z_>gKY%Vj8aitYLBMlvy#})hh zbGyJ2iA3;&t})xz!rOyd1aNE;5h$!hzWHz!zOt)IdJ=C@At9i6CeHU=Qy&%v*G#~O zK-pZjV3yt~XQ1RWEN6SK;LPZpEgZZ-T1z!_PA#lzOf3giSXekjCLy3~uM6PZpN13w zaBGpQAt9jnuKr!$)w1UuXAd3>>HjRoCU&r#iyGJyQ1T8InmbC)12emA!2n=_LuwHh zHsN#1;9u^lukb7V**>4FA&Ew(k<>~;<1_pFWE*2LNB}V?c5UZcv=bWwtrj@Q{9H8s zJidANsg(mm|Bd+|0Ym@`F$bSREad_eQS15jpKq34mbm1Qu=(UxBGi!!9DEH;19fZC z(7XYzA`#4ew>Z80nyaU~4|4+?;cusEq~!1<@}KLdaj3cH%wO+m>7hzuB^QIHvu z#R6neAR)_Q78C+SHYQ;$!a_mj=b{ESc3*jBhsqoT8u+Lez||!q8Mb+AfZZ{KU-WAe^zJ@C+1)cGBhp8IP>OUO=4%1Jtf!C zHPtON2RUYE6#|0@6j60eR-&csCLKwOA#e~_Y~f%{VrLW3IRlsRG4#(F6o}+pCp<;zC31IsVZWejsvA?< z19X%cWBzkDVz1LzrK3JDL&p1`l9mS7>^Z_6rFq*#o?R&uTr-EkjUN}H%f zT1uNtwwTf$ptoO>moqx!YToTn1nTo zX%lK5kS1ST*iQQ53t{m_>~*a7f0|l@)O!D?vDy0}-kH3HU1b|gpC@gGjE3F>MeT;7 zc02!pGxi|!jBTgdI$th8meeE`qoii~-yHotgDh#C7=*Wo%qC6@(icym-lv&BF|C>y ziwHNUj0UJm@1onpbvCI6etAzdeM6PgtIP5l5_V_Sug3e&-TF_X_tsZjWf3Rs#tsyx zfUub)6EH$1M6hsx*tMNZ)moZf!7NokUG{dvSA}2cPb8Aqx%P1V7q?$lHUwHT0+K0% zEG8g>aSfLf+o2mhj*Q~UB$Bq{W;y)ih9KU2uu)lVK$g`cyj47@nf~`CUZhsZ)*`t8 z0swEX?vI)s27GPWiSeZTk=Ye~3I@N#Mn1r_q0PMNzPn18d z`W3wdAd^L8fi;^NjBA!T8Fiz_1P1}e&L%=Y03jewm%&eN2;$VkA6F{;N`ES~a(`rM z#eVfrAg~6rv$IH+@2=lh^G1EcUH?kq;8!y2CYsX~jh#hUYXHC&UrJz#9%>ZcCZhcl z0*Yy+!@{A29j|&?OEnC9rr{u2y#Rowt=u1(T9J0cS2Q{euh)Y_A_>xh2oQEFn_N~i zJSfr^PXdZ)wPTx2B*nI!)=~}Ms;|VJ+6n~#mOFdoH6&!ItZ5#2y&lZW%pABl^YacC z7FiY%0kc?u*g@iYm16@-i)Tw-!jX57@cSx$HQt{ImTXKSYBe%gG6h)PCbr@AHiRQ@ zArZks+R$%rJx|Zi@OfmZOp=XFq$ZgM<{K3|(NjD5Cf8h&X{)}a^e%jB!y#;27dTi^;yDQb(+L%+RPwffSoZ5s3rg=>pe*dm1_+PxS@o@kE002ovPDHLkV1n2d1=aun literal 0 HcmV?d00001 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