├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── ImageTools.iss ├── ImageTools.py ├── Readme.html ├── Readme.md ├── buildexe.bat ├── components ├── BasicImage.py ├── __init__.py ├── check_update.py ├── customwidget.py ├── help_doc.py ├── histview.py ├── logconfig.py ├── property.py ├── resource.qrc ├── resource │ ├── add.png │ ├── compare.png │ ├── config.png │ ├── delete.png │ ├── down.png │ ├── main.ico │ ├── main.png │ ├── open.png │ ├── pause.png │ ├── restart .png │ ├── right.png │ ├── save_icon.png │ ├── start.png │ ├── stats.png │ ├── stop.png │ ├── subtract.png │ ├── up.png │ ├── 快进.png │ └── 快退.png ├── status_code_enum.py ├── ui │ ├── check_update_win.py │ ├── check_update_win.ui │ ├── help_window.py │ ├── help_window.ui │ ├── histgramview.py │ ├── histgramview.ui │ ├── mainwindow.py │ └── mainwindow.ui └── window.py ├── requirements.txt ├── resource_rc.py ├── subapps ├── FocusDepthTool.iss ├── FocusDepthTool.py ├── PQtools2Code.iss ├── PQtools2Code.py ├── ShakeTestTool.iss ├── ShakeTestTool.py ├── VideoCompare.iss ├── VideoCompare.py └── __init__.py ├── test ├── __init__.py ├── components │ ├── __init__.py │ ├── test_basic_image.py │ └── test_check_update.py └── resource │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.png │ ├── 6月壁纸.jpg │ └── 星空背景 绘画 夜空 4k壁纸_彼岸图网.jpg ├── tools ├── __init__.py ├── depth_of_focus │ ├── LenParameters.py │ ├── Readme.md │ ├── __init__.py │ ├── depth_of_focus.py │ ├── field_depth_window.py │ └── field_depth_window.ui ├── imageeditor │ ├── Readme.md │ ├── __init__.py │ ├── imageeditor.py │ ├── imageeffect.py │ └── ui │ │ ├── imageeditor_window.py │ │ ├── imageeditor_window.ui │ │ ├── watermarkview.py │ │ └── watermarkview.ui ├── pqtools_to_code │ ├── pqtools2code_window.py │ ├── pqtools2code_window.ui │ └── pqtools_to_code.py ├── rawimageeditor │ ├── RawImageEditor.py │ ├── RawImageInfo.py │ ├── RawImageParams.py │ ├── Readme.md │ ├── __init__.py │ ├── debayer.py │ ├── isp.py │ ├── ispfunction.py │ ├── isppipeline.py │ ├── ui │ │ ├── __init__.py │ │ ├── rawimageeditor_window.py │ │ └── rawimageeditor_window.ui │ └── utility.py ├── shake_test │ ├── Readme.md │ ├── rtspconfigview.py │ ├── rtspconfigview.ui │ ├── shake_test.py │ ├── shake_test_window.py │ └── shake_test_window.ui ├── video_compare │ ├── Readme.md │ ├── rtspconfigview.py │ ├── rtspconfigview.ui │ ├── shake_test_window.py │ ├── shake_test_window.ui │ ├── video_pre_settings.py │ ├── video_pre_settings.ui │ ├── videocompare.py │ └── videoview.py └── yuv_viewer │ ├── Readme.md │ ├── __init__.py │ ├── ui │ ├── yuvconfig.py │ ├── yuvconfig.ui │ ├── yuvviewer_window.py │ └── yuvviewer_window.ui │ ├── yuv_config.py │ └── yuv_viewer.py └── version.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.spec 4 | __pycache__/ 5 | *.pyc 6 | settings.json 7 | *.mp4 8 | *.jpg 9 | config/ 10 | *.exe 11 | *.raw -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: 当前文件", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/ImageTools.py", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:\\Users\\qinxing\\AppData\\Local\\Programs\\Python\\Python39\\python.exe", 3 | // 默认对Python文件进行静态检查 4 | "python.linting.enabled": true, 5 | 6 | // 默认在Python文件保存时进行静态检查 7 | "python.linting.lintOnSave": true, 8 | 9 | // 默认使用pylint对Python文件进行静态检查 10 | "python.linting.pylintEnabled": true, 11 | 12 | // 静态检查时是否使用pylint的最小规则集(minimal set of rules) 13 | "python.linting.pylintUseMinimalCheckers": true, 14 | 15 | "python.linting.pylintArgs": [ 16 | "--extension-pkg-whitelist=PySide2,cv2" 17 | ], 18 | "python.testing.pytestArgs": [ 19 | "." 20 | ], 21 | "python.testing.unittestEnabled": false, 22 | "python.testing.nosetestsEnabled": false, 23 | "python.testing.pytestEnabled": true 24 | } -------------------------------------------------------------------------------- /ImageTools.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "ImageTools" 5 | #define MyAppVersion "1.4" 6 | #define MyAppPublisher "liqinxing, Inc." 7 | #define MyAppURL "https://www.qinxing.xyz/" 8 | #define MyAppExeName "ImageTools.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{12F34F85-9D0D-4F1B-9492-E8875EE23A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName=C:\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=.\Output 26 | OutputBaseFilename=ImageTools 27 | SetupIconFile=.\components\resource\main.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | 38 | [Files] 39 | Source: ".\dist\ImageTools\ImageTools.exe"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: ".\dist\ImageTools\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 45 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | [installDelete] 51 | Type: filesandordirs; Name:"{app}\config\*"; 52 | -------------------------------------------------------------------------------- /ImageTools.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QApplication 2 | from PySide2.QtCore import Qt 3 | import sys 4 | from components.window import MainWindow 5 | from components.customwidget import info_win 6 | from tools.depth_of_focus.depth_of_focus import FieldDepthWindow 7 | from tools.shake_test.shake_test import ShakeTestTool 8 | from tools.imageeditor.imageeditor import ImageEditor 9 | from components.help_doc import HelpDoc 10 | from tools.rawimageeditor.RawImageEditor import RawImageEditor 11 | from tools.video_compare.videocompare import VideoCompare 12 | from tools.pqtools_to_code.pqtools_to_code import PQtoolsToCode 13 | from tools.yuv_viewer.yuv_viewer import YUVViewer 14 | from components.check_update import CheckUpdate, simple_check_is_need_update 15 | import components.logconfig as log 16 | from logging import info 17 | from components.property import get_persist, IS_NEED_AUTO_UPDATE 18 | 19 | 20 | class ImageTools(MainWindow): 21 | def __init__(self): 22 | super().__init__() 23 | self.subwindow_function = { 24 | "FieldDepthWindow": [self.ui.field_depth_tool, FieldDepthWindow], 25 | "ShakeTestTool": [self.ui.shake_tool, ShakeTestTool], 26 | "ImageEditor": [self.ui.imageeditor, ImageEditor], 27 | "RawImageEditor": [self.ui.rawimageeditor, RawImageEditor], 28 | "VideoCompare": [self.ui.video_compare, VideoCompare], 29 | "HelpDoc": [self.ui.userguide, HelpDoc], 30 | "PQtoolsToCode": [self.ui.pqtools2code, PQtoolsToCode], 31 | "YUVViewer": [self.ui.yuv_viewer, YUVViewer], 32 | } 33 | self.subwindows_ui = self.ui.mdiArea 34 | self.subwindows_ui.setStyleSheet("QTabBar::tab { height: 30px;}") 35 | self.ui.clearcache.triggered.connect(self.clear_cache) 36 | self.ui.checkupdate.triggered.connect(self.add_checkupdate_window) 37 | for (key, value) in self.subwindow_function.items(): 38 | # 注意,这个lambda表达式必须要先赋值才能使用,否则connect的永远是最后的一个类 39 | value[0].triggered.connect( 40 | lambda win_name=key, win_object=value[1]: self.add_sub_window(win_name, win_object)) 41 | 42 | info('ImageTools 工具初始化成功') 43 | 44 | def add_sub_window(self, name, win_object): 45 | sub_window = win_object(parent=self) 46 | self.subwindows_ui.addSubWindow(sub_window) 47 | self.sub_windows.append(sub_window) 48 | sub_window.show() 49 | info('打开{}工具 success'.format(name)) 50 | 51 | def check_version(self): 52 | is_need_auto_update = get_persist(IS_NEED_AUTO_UPDATE, False) 53 | info('是否需要自动更新 {}'.format(is_need_auto_update)) 54 | if is_need_auto_update is True: 55 | if(simple_check_is_need_update() == True): 56 | info('软件需要更新') 57 | self.add_checkupdate_window() 58 | else: 59 | info('软件不需要更新') 60 | 61 | def load_saved_windows(self): 62 | for name in self.sub_windows_list: 63 | self.add_sub_window(name, self.subwindow_function[name][1]) 64 | 65 | def add_checkupdate_window(self): 66 | sub_win = CheckUpdate(parent=self) 67 | sub_win.show() 68 | 69 | def clear_cache(self): 70 | self.need_clear_cache = True 71 | info_win('缓存删除成功!\r\n请重启软件', self) 72 | 73 | 74 | if __name__ == "__main__": 75 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 76 | apps = QApplication([]) 77 | apps.setStyle('Fusion') 78 | log.clean_old_log() 79 | log.init_log() 80 | appswindow = ImageTools() 81 | appswindow.show() 82 | appswindow.load_saved_windows() 83 | appswindow.check_version() 84 | sys.exit(apps.exec_()) 85 | -------------------------------------------------------------------------------- /buildexe.bat: -------------------------------------------------------------------------------- 1 | DEL /q .\build\* 2 | DEL /q .\dist\* 3 | RD /S /Q .\build 4 | RD /S /Q .\dist 5 | pip install pyinstaller 6 | @REM pyinstaller -w ./ImageTools.py --noconfirm --add-data "./Readme.html;./" 7 | start cmd /c "pyinstaller -w ./ImageTools.py --noconfirm && copy .\Readme.html .\dist\ImageTools\ && copy .\version.md .\dist\ImageTools\ && compil32 /cc ./ImageTools.iss" 8 | @REM start cmd /c "pyinstaller -w --noconfirm ./subapps/PQtools2Code.py && compil32 /cc ./subapps/PQtools2Code.iss" 9 | @REM start cmd /c "pyinstaller -w --noconfirm ./subapps/ShakeTestTool.py && compil32 /cc ./subapps/ShakeTestTool.iss" 10 | @REM start cmd /c "pyinstaller -w --noconfirm ./subapps/FocusDepthTool.py && compil32 /cc ./subapps/FocusDepthTool.iss" 11 | @REM start cmd /c "pyinstaller -w --noconfirm ./subapps/VideoCompare.py && compil32 /cc ./subapps/VideoCompare.iss" -------------------------------------------------------------------------------- /components/BasicImage.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from PySide2.QtGui import QPixmap, QImage 4 | from os import listdir, remove 5 | from os.path import isfile, join, getmtime, dirname, basename, isdir, splitext 6 | from natsort import natsorted 7 | from components.status_code_enum import * 8 | 9 | YUV_FORMAT_MAP = { 10 | 'NV21': cv2.COLOR_YUV2BGR_NV21, 11 | 'NV12': cv2.COLOR_YUV2BGR_NV12, 12 | 'YCrCb': cv2.COLOR_YCrCb2BGR, 13 | 'YUV420': cv2.COLOR_YUV2BGR_I420, 14 | 'YUV422': cv2.COLOR_YUV2BGR_Y422, 15 | 'UYVY': cv2.COLOR_YUV2BGR_UYVY, 16 | 'YUYV': cv2.COLOR_YUV2BGR_YUYV, 17 | 'YVYU': cv2.COLOR_YUV2BGR_YVYU, 18 | } 19 | 20 | 21 | class ImageBasic: 22 | img = None 23 | imgpath = '' # 图片路径 24 | height = 0 25 | width = 0 26 | depth = 0 # 通道数 27 | yuv_format = '' 28 | 29 | def __update_attr(self): 30 | if (self.img is not None): 31 | self.height = self.img.shape[0] 32 | self.width = self.img.shape[1] 33 | self.depth = self.img.shape[2] 34 | 35 | def get_dir(self): 36 | if self.imgpath == '': 37 | return '.' 38 | return dirname(self.imgpath) 39 | 40 | def remove_image(self): 41 | self.img = None 42 | if isfile(self.imgpath) is False: 43 | return 44 | remove(self.imgpath) 45 | 46 | def load_yuv_config(self, width, height, yuv_format): 47 | self.width = width 48 | self.height = height 49 | self.yuv_format = yuv_format 50 | 51 | def load_file(self, filename): 52 | if isfile(filename) is False: 53 | raise FileNotFoundErr 54 | if splitext(filename)[-1] in ['.jpg', '.png', '.bmp']: 55 | self.__load_imagefile(filename) 56 | elif splitext(filename)[-1] in ['.yuv']: 57 | self.__load_yuvfile(filename) 58 | else: 59 | raise ImageFormatNotSupportErr 60 | 61 | def __load_imagefile(self, filename): 62 | # 防止有中文,因此不使用imread 63 | self.img = cv2.imdecode(np.fromfile(filename, dtype=np.uint8), 1) 64 | if self.img is None: 65 | raise ImageReadErr 66 | self.imgpath = filename 67 | self.__update_attr() 68 | 69 | def __load_yuvfile(self, filename): 70 | yuv_format = YUV_FORMAT_MAP.get(self.yuv_format) 71 | if yuv_format is None: 72 | raise ImageFormatNotSupportErr 73 | 74 | yuvdata = np.fromfile(filename, dtype=np.uint8) 75 | if yuvdata is None: 76 | raise ImageNoneErr 77 | if self.yuv_format in ['NV21', 'NV12', 'YUV420']: 78 | if len(yuvdata) != self.height * self.width * 3 / 2: 79 | raise ImageFormatErr 80 | yuvdata = yuvdata.reshape(self.height*3//2, self.width) 81 | elif self.yuv_format in ['YCrCb', 'YUV422', 'UYVY', 'YUYV', 'YVYU']: 82 | if len(yuvdata) != self.height * self.width * 2: 83 | raise ImageFormatErr 84 | yuvdata = yuvdata.reshape(self.height*2, self.width) 85 | else: 86 | raise ImageFormatNotSupportErr 87 | 88 | try: 89 | self.img = cv2.cvtColor(yuvdata, yuv_format) 90 | except Exception: 91 | raise ImageFormatErr 92 | self.imgpath = filename 93 | self.__update_attr() 94 | 95 | # display 96 | def display_in_scene(self, scene): 97 | """ 98 | return: true or error string 99 | """ 100 | scene.clear() 101 | if self.img is not None: 102 | # numpy转qimage的标准流程 103 | if len(self.img.shape) == 2: 104 | bytes_per_line = self.img.shape[1] 105 | qimg = QImage( 106 | self.img, self.img.shape[1], self.img.shape[0], bytes_per_line, QImage.Format_Grayscale8) 107 | elif self.img.shape[2] == 3: 108 | bytes_per_line = 3 * self.img.shape[1] 109 | qimg = QImage( 110 | self.img, self.img.shape[1], self.img.shape[0], bytes_per_line, QImage.Format_BGR888) 111 | elif self.img.shape[2] == 4: 112 | bytes_per_line = 4 * self.img.shape[1] 113 | qimg = QImage( 114 | self.img, self.img.shape[1], self.img.shape[0], bytes_per_line, QImage.Format_RGBA8888) 115 | else: 116 | raise ImageFormatNotSupportErr 117 | scene.addPixmap(QPixmap.fromImage(qimg)) 118 | return 119 | raise ImageNoneErr 120 | 121 | # proc 122 | def save_image(self, filename): 123 | if self.img is None: 124 | raise ImageNoneErr 125 | if isdir(dirname(filename)) is False: 126 | raise FilePathNotValidErr 127 | # 解决中文路径的问题, 不使用imwrite 128 | cv2.imencode('.jpg', self.img)[1].tofile(filename) 129 | 130 | def get_img_point(self, x, y): 131 | """ 132 | 获取图像中一个点的RGB值,注意颜色顺序是BGR 133 | """ 134 | if(x > 0 and x < self.width and y > 0 and y < self.height): 135 | return self.img[y, x] 136 | else: 137 | return None 138 | 139 | def rotate90(self): 140 | """ 141 | 顺时针旋转90度 142 | """ 143 | if self.img is None: 144 | raise ImageNoneErr 145 | self.img = cv2.rotate(self.img, cv2.ROTATE_90_CLOCKWISE) 146 | self.__update_attr() 147 | 148 | def find_next_time_photo(self, nextIndex): 149 | """ 150 | 获取下一个或者上一个图片(按照时间顺序排列) 151 | """ 152 | next_photo_name = '' 153 | index = 0 154 | path = dirname(self.imgpath) 155 | img_name = basename(self.imgpath) 156 | filelist = [f for f in listdir(path) if isfile( 157 | join(path, f)) and f.split('.')[-1] in ["jpg", "png", "bmp"]] 158 | filelist = sorted( 159 | filelist, key=lambda x: getmtime(join(path, x))) 160 | files_nums = len(filelist) 161 | if img_name in filelist: 162 | index = filelist.index(img_name) + nextIndex 163 | if(index > len(filelist) - 1): 164 | index = 0 165 | elif(index < 0): 166 | index = len(filelist) - 1 167 | next_photo_name = join(path, filelist[index]) 168 | return (next_photo_name, index, files_nums) 169 | 170 | def find_next_nat_photo(self, nextIndex): 171 | """ 172 | 获取下一个或者上一个图片(按照自然顺序排列) 173 | """ 174 | next_photo_name = '' 175 | index = 0 176 | path = dirname(self.imgpath) 177 | img_name = basename(self.imgpath) 178 | filelist = [f for f in listdir(path) if isfile( 179 | join(path, f)) and f.split('.')[-1] in ["jpg", "png", "bmp"]] 180 | natsorted(filelist) 181 | files_nums = len(filelist) 182 | if img_name in filelist: 183 | index = filelist.index(img_name) + nextIndex 184 | if(index > len(filelist) - 1): 185 | index = 0 186 | elif(index < 0): 187 | index = len(filelist) - 1 188 | next_photo_name = join(path, filelist[index]) 189 | return (next_photo_name, index, files_nums) 190 | -------------------------------------------------------------------------------- /components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/__init__.py -------------------------------------------------------------------------------- /components/check_update.py: -------------------------------------------------------------------------------- 1 | from os import startfile, mkdir 2 | from os.path import exists, abspath, dirname 3 | from sys import exit 4 | from PySide2.QtWidgets import QDialog 5 | from components.ui.check_update_win import Ui_CheckUpdate 6 | import requests 7 | import time 8 | from logging import error, info 9 | from PySide2.QtCore import Signal, QThread 10 | from components.customwidget import critical_win 11 | from components.property import set_persist, get_persist, IS_NEED_AUTO_UPDATE 12 | 13 | VERSION_NUMS_LEN = 3 14 | VERSION_FILE = 'https://imagetools.qinxing.xyz/version.md' 15 | 16 | CURRENT_VERSION_FILE = 'version.md' 17 | REMOTE_INSTALL_PACK = 'https://imagetools.qinxing.xyz/ImageTools.exe' 18 | DOWNLOAD_INSTALL_PACK = '.\\latest_installer\\installer.exe' 19 | 20 | 21 | def check_is_latest(current, new) -> bool: 22 | """ 23 | func: 检查是否为最新版本 24 | """ 25 | current_version = current.split('.') 26 | new_version = new.split('.') 27 | for i in range(VERSION_NUMS_LEN): 28 | if(int(current_version[i]) < int(new_version[i])): 29 | return False 30 | if(int(current_version[i]) > int(new_version[i])): 31 | return True 32 | return True 33 | 34 | 35 | def get_latest_version_log(): 36 | """ 37 | func: 获取最新的版本日志文件 38 | 把日志转换成UTF-8格式 39 | """ 40 | try: 41 | res = requests.get(VERSION_FILE, timeout=2) 42 | if res.status_code != 200: 43 | return '' 44 | res.encoding = 'utf-8' 45 | res.close() 46 | return res.text 47 | except Exception as e: 48 | error(e) 49 | return '' 50 | 51 | 52 | def get_current_version_log(): 53 | """ 54 | func: 获取当前版本日志 55 | """ 56 | with open(CURRENT_VERSION_FILE, 'r', encoding='utf-8') as f: 57 | return f.read() 58 | 59 | 60 | def get_version(version_log): 61 | """ 62 | func: 获取日志文件的最新版本号 63 | 日志文件格式必须为 **x.x.x** 64 | """ 65 | start_index = version_log.find('**') 66 | end_index = version_log.find('**', start_index + 1) 67 | return version_log[start_index + 2:end_index] 68 | 69 | 70 | def download_file(srcUrl): 71 | """ 72 | func: 下载一个大文件 73 | """ 74 | localFile = srcUrl.split('/')[-1] 75 | info('开始下载 %s' % (srcUrl)) 76 | startTime = time.time() 77 | with requests.get(srcUrl, stream=True) as r: 78 | contentLength = int(r.headers['content-length']) 79 | line = 'content-length: %dB/ %.2fKB/ %.2fMB' 80 | line = line % (contentLength, contentLength / 81 | 1024, contentLength/1024/1024) 82 | info(line) 83 | downSize = 0 84 | with open(localFile, 'wb') as f: 85 | for chunk in r.iter_content(8192): 86 | if chunk: 87 | f.write(chunk) 88 | downSize += len(chunk) 89 | # line = '%d KB/s - %.2f MB, 共 %.2f MB' 90 | # line = line % (downSize/1024/(time.time()-startTime), 91 | # downSize/1024/1024, contentLength/1024/1024) 92 | # info(line) 93 | if downSize >= contentLength: 94 | break 95 | timeCost = time.time() - startTime 96 | info('下载成功, 共耗时: %.2f s, 平均速度: %.2f KB/s' % 97 | (timeCost, downSize/1024/timeCost)) 98 | return True 99 | 100 | 101 | def simple_check_is_need_update() -> bool: 102 | """ 103 | func: 简单的返回是否需要更新 104 | """ 105 | current_version = get_version(get_current_version_log()) 106 | version_log = get_latest_version_log() 107 | if(version_log == ''): 108 | return False 109 | latest_version = get_version(version_log) 110 | if check_is_latest(current_version, latest_version) is True: 111 | return False 112 | return True 113 | 114 | 115 | def check_update(): 116 | """ 117 | 检查更新 118 | """ 119 | current_log = get_current_version_log() 120 | current_version = get_version(current_log) 121 | 122 | version_log = get_latest_version_log() 123 | if(version_log == ''): 124 | return 'connect error', current_version, current_log 125 | 126 | latest_version = get_version(version_log) 127 | if check_is_latest(current_version, latest_version) is True: 128 | return 'already latest', current_version, current_log 129 | else: 130 | return latest_version, current_version, version_log 131 | 132 | 133 | class CheckUpdate(QDialog): 134 | 135 | def __init__(self, parent): 136 | """ 137 | func: 初始化直方图统计信息UI,把父类的指针变成选中方框模式 138 | """ 139 | super().__init__(parent) 140 | self.ui = Ui_CheckUpdate() 141 | self.ui.setupUi(self) 142 | self.autoupdate = get_persist(IS_NEED_AUTO_UPDATE, False) 143 | self.ui.autoupdate.setChecked(self.autoupdate) 144 | self.need_update = 0 145 | self.ui.autoupdate.stateChanged.connect(self.set_autoupdate_param) 146 | self.ui.ok.clicked.connect(self.start_update) 147 | self.ui.cancel.clicked.connect(lambda: self.close()) 148 | self.update_proc = UpdateProc( 149 | REMOTE_INSTALL_PACK, self) 150 | self.update_proc.doneCB.connect(self.update_proc_done) 151 | self.update_proc.processRateCB.connect( 152 | lambda rate: self.ui.progress.setValue(rate)) 153 | 154 | latest_version, current_version, latest_log = check_update() 155 | 156 | # 异常处理 157 | if(latest_version == 'connect error'): 158 | self.ui.version_num.setText( 159 | '!!!网络连接出错!!! 当前版本为 ' + current_version) 160 | self.need_update = -1 161 | elif(latest_version == 'already latest'): 162 | self.ui.version_num.setText('软件已经是最新啦!当前版本为 ' + current_version) 163 | self.need_update = 0 164 | else: 165 | self.ui.version_num.setText( 166 | '有版本更新啦!当前版本为 ' + current_version + '; 最新版本为 ' + latest_version) 167 | self.need_update = 1 168 | 169 | self.ui.textBrowser.setMarkdown(latest_log) 170 | 171 | def set_autoupdate_param(self, p): 172 | self.autoupdate = (p != 0) 173 | set_persist(IS_NEED_AUTO_UPDATE, self.autoupdate) 174 | info('设置自动更新功能 {}'.format(self.autoupdate)) 175 | 176 | def start_update(self): 177 | if self.need_update == 1: 178 | if(not exists(dirname(DOWNLOAD_INSTALL_PACK))): 179 | mkdir(dirname(DOWNLOAD_INSTALL_PACK)) 180 | self.update_proc.start() 181 | elif self.need_update == 0: 182 | critical_win('软件已经是最新啦') 183 | elif self.need_update == -1: 184 | critical_win('网络连接出错') 185 | 186 | def update_proc_done(self, ret): 187 | if(ret == 0): 188 | info('下载成功') 189 | # Popen('start ' + REMOTE_INSTALL_PACK.split('/') 190 | # [-1], shell=True, stdin=PIPE, stdout=PIPE) 191 | # os.system('start ChromeSetup.exe') 192 | if exists(DOWNLOAD_INSTALL_PACK) is True: 193 | startfile(DOWNLOAD_INSTALL_PACK) 194 | info('正在启用最新升级包') 195 | exit() 196 | critical_win('下载的升级包不存在') 197 | else: 198 | info('下载失败') 199 | 200 | 201 | class UpdateProc(QThread): 202 | doneCB = Signal(int) 203 | processRateCB = Signal(int) 204 | 205 | def __init__(self, url, parent=None) -> None: 206 | super(UpdateProc, self).__init__(parent=parent) 207 | self.url = url 208 | 209 | def run(self): 210 | self.processRateCB.emit(0) 211 | info('开始下载更新...') 212 | with requests.get(self.url, stream=True, timeout=5) as r: 213 | contentLength = int(r.headers['content-length']) 214 | downSize = 0 215 | with open(DOWNLOAD_INSTALL_PACK, 'wb') as f: 216 | for chunk in r.iter_content(8192): 217 | if chunk: 218 | f.write(chunk) 219 | downSize += len(chunk) 220 | self.processRateCB.emit(downSize/contentLength * 100) 221 | info('下载进度{}%'.format(downSize/contentLength * 100)) 222 | if downSize >= contentLength: 223 | self.doneCB.emit(0) 224 | return 225 | self.doneCB.emit(-1) 226 | return 227 | -------------------------------------------------------------------------------- /components/help_doc.py: -------------------------------------------------------------------------------- 1 | from components.ui.help_window import Ui_HelpWindow 2 | from components.window import SubWindow 3 | 4 | 5 | class HelpDoc(SubWindow): 6 | 7 | def __init__(self, name='HelpDoc', parent=None): 8 | super().__init__(name, parent, Ui_HelpWindow()) 9 | self.ui.textBrowser.setSource("Readme.html") -------------------------------------------------------------------------------- /components/histview.py: -------------------------------------------------------------------------------- 1 | from components.ui.histgramview import Ui_HistgramView 2 | from PySide2.QtWidgets import QGraphicsView, QDialog 3 | from components.customwidget import MatplotlibWidget 4 | import numpy as np 5 | import cv2 6 | 7 | 8 | class HistView(QDialog): 9 | def __init__(self, parent): 10 | """ 11 | func: 初始化直方图统计信息UI,把父类的指针变成选中方框模式 12 | """ 13 | super().__init__(parent) 14 | self.parent = parent 15 | self.parent.setDragMode(QGraphicsView.RubberBandDrag) 16 | self.ui = Ui_HistgramView() 17 | self.ui.setupUi(self) 18 | self.ui.r_enable.stateChanged.connect( 19 | self.on_r_hist_enable) 20 | self.ui.g_enable.stateChanged.connect( 21 | self.on_g_hist_enable) 22 | self.ui.b_enable.stateChanged.connect( 23 | self.on_b_hist_enable) 24 | self.ui.y_enable.stateChanged.connect( 25 | self.on_y_hist_enable) 26 | self.histview = MatplotlibWidget( 27 | self.ui.gridLayout_10) 28 | self.x_axis = np.linspace(0, 255, num=256) 29 | self.r_hist_visible = 2 30 | self.g_hist_visible = 2 31 | self.b_hist_visible = 2 32 | self.y_hist_visible = 2 33 | self.enable = True 34 | 35 | def update_rect_data(self, img, rect): 36 | """ 37 | func: 更新方框内的图像统计信息 38 | """ 39 | (rect, image) = self.update_rect(img, rect) 40 | self.calcHist(image) 41 | self.hist_show() 42 | self.stats_show(self.calcStatics(image, rect)) 43 | 44 | def closeEvent(self, event): 45 | """ 46 | func: 关闭窗口的时候,把鼠标还原 47 | """ 48 | self.parent.setDragMode(QGraphicsView.ScrollHandDrag) 49 | self.enable = False 50 | return super().closeEvent(event) 51 | 52 | def update_rect(self, img, rect): 53 | [x1, y1, x2, y2] = rect 54 | i1 = max(x1, 0) 55 | i2 = min(x2, img.shape[1]) 56 | j1 = max(y1, 0) 57 | j2 = min(img.shape[0], y2) 58 | if (i2 > i1 and j2 > j1): 59 | img = img[j1:j2, i1:i2][:, :, :3] 60 | rect = [j1, i1, j2, i2] 61 | return (rect, img) 62 | 63 | def calcStatics(self, img, rect): 64 | [j1, i1, j2, i2] = rect 65 | (average_rgb, stddv_rgb) = cv2.meanStdDev(img) 66 | # TODO 信噪比的公式有些问题,当标准差为0的时候,信噪比该是无穷大 67 | snr_rgb = 20 * np.log10(average_rgb/stddv_rgb) 68 | img = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) 69 | (average_yuv, stddv_yuv) = cv2.meanStdDev(img) 70 | snr_yuv = 20 * np.log10(average_yuv/stddv_yuv) 71 | rgb_ratio = [0.0, 0.0] 72 | awb_gain = [0.0, 0.0, 0.0] 73 | rgb_ratio[0] = average_rgb[2]/average_rgb[1] 74 | rgb_ratio[1] = average_rgb[0]/average_rgb[1] 75 | awb_gain[0] = 1/rgb_ratio[0] 76 | awb_gain[1] = 1 77 | awb_gain[2] = 1/rgb_ratio[1] 78 | enable_rect = [i1, j1, i2-i1, j2-j1] 79 | return (average_rgb, snr_rgb, average_yuv, snr_yuv, rgb_ratio, awb_gain, enable_rect) 80 | 81 | def calcHist(self, img): 82 | chans = cv2.split(img) 83 | self.b_hist = (cv2.calcHist([chans[0]], [0], None, [ 84 | 256], [0, 256])) 85 | self.g_hist = (cv2.calcHist([chans[1]], [0], None, [ 86 | 256], [0, 256])) 87 | self.r_hist = (cv2.calcHist([chans[2]], [0], None, [ 88 | 256], [0, 256])) 89 | # 转为灰度图,然后算亮度直方图 90 | img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 91 | self.y_hist = (cv2.calcHist([img], [0], None, [ 92 | 256], [0, 256])) 93 | self.r_hist.reshape(1, 256) 94 | self.g_hist.reshape(1, 256) 95 | self.b_hist.reshape(1, 256) 96 | self.y_hist.reshape(1, 256) 97 | 98 | def on_r_hist_enable(self, type): 99 | self.r_hist_visible = type 100 | self.hist_show() 101 | 102 | def on_g_hist_enable(self, type): 103 | self.g_hist_visible = type 104 | self.hist_show() 105 | 106 | def on_b_hist_enable(self, type): 107 | self.b_hist_visible = type 108 | self.hist_show() 109 | 110 | def on_y_hist_enable(self, type): 111 | self.y_hist_visible = type 112 | self.hist_show() 113 | 114 | def hist_show(self): 115 | self.histview.clean() 116 | self.histview.label("亮度", "数量") 117 | if (self.r_hist_visible == 2): 118 | self.histview.input_r_hist(self.x_axis, self.r_hist) 119 | if (self.g_hist_visible == 2): 120 | self.histview.input_g_hist(self.x_axis, self.g_hist) 121 | if (self.b_hist_visible == 2): 122 | self.histview.input_b_hist(self.x_axis, self.b_hist) 123 | if (self.y_hist_visible == 2): 124 | self.histview.input_y_hist(self.x_axis, self.y_hist) 125 | self.histview.draw() 126 | 127 | def stats_show(self, value): 128 | (average_rgb, snr_rgb, average_yuv, snr_yuv, 129 | rgb_ratio, awb_gain, enable_rect) = value 130 | self.ui.average_r.setValue(average_rgb[2]) 131 | self.ui.average_g.setValue(average_rgb[1]) 132 | self.ui.average_b.setValue(average_rgb[0]) 133 | self.ui.average_y.setValue(average_yuv[0]) 134 | self.ui.average_cr.setValue(average_yuv[1]) 135 | self.ui.average_cb.setValue(average_yuv[2]) 136 | self.ui.rg_ratio.setValue(rgb_ratio[0]) 137 | self.ui.bg_ratio.setValue(rgb_ratio[1]) 138 | self.ui.r_gain.setValue(awb_gain[0]) 139 | self.ui.g_gain.setValue(awb_gain[1]) 140 | self.ui.b_gain.setValue(awb_gain[2]) 141 | self.ui.section_x.setValue(enable_rect[0]) 142 | self.ui.section_y.setValue(enable_rect[1]) 143 | self.ui.section_height.setValue(enable_rect[2]) 144 | self.ui.section_width.setValue(enable_rect[3]) 145 | self.ui.snr_r.setValue(snr_rgb[2]) 146 | self.ui.snr_g.setValue(snr_rgb[1]) 147 | self.ui.snr_b.setValue(snr_rgb[0]) 148 | self.ui.snr_y.setValue(snr_yuv[0]) 149 | self.ui.snr_cr.setValue(snr_yuv[1]) 150 | self.ui.snr_cb.setValue(snr_yuv[2]) 151 | -------------------------------------------------------------------------------- /components/logconfig.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import logging 4 | 5 | # 设置日志存放路径 6 | LOGFILE_PATH = '.\\log\\' 7 | if not os.path.exists(LOGFILE_PATH): 8 | os.mkdir(LOGFILE_PATH) 9 | 10 | # 获取今天的日期 格式2019-08-01 11 | TODAY_DATE = str(datetime.date.today()) 12 | 13 | 14 | def init_log(): 15 | """ 16 | func: 初始化日志,只需要在主函数中初始化一次 17 | """ 18 | root_logger = logging.getLogger() 19 | root_logger.setLevel(logging.INFO) 20 | handler = logging.FileHandler( 21 | LOGFILE_PATH + TODAY_DATE + '.log', 'a', 'utf-8') 22 | formatter = logging.Formatter( 23 | '%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s') 24 | handler.setFormatter(formatter) 25 | root_logger.addHandler(handler) 26 | logging.info('logging module init is ok') 27 | 28 | 29 | def clean_old_log(): 30 | """ 31 | 删除除今天以外的日志 32 | """ 33 | # 遍历目录下的所有日志文件 i是文件名 34 | for i in os.listdir(LOGFILE_PATH): 35 | if i != TODAY_DATE + '.log': 36 | os.remove(LOGFILE_PATH + i) 37 | -------------------------------------------------------------------------------- /components/property.py: -------------------------------------------------------------------------------- 1 | from os.path import exists 2 | import pickle 3 | 4 | CACHE_PERSIST_PATH = './config/property.tmp' 5 | 6 | # 参数表 7 | IS_NEED_AUTO_UPDATE = 0 8 | 9 | 10 | def load_persist(): 11 | ret = {} 12 | if exists(CACHE_PERSIST_PATH): 13 | with open(CACHE_PERSIST_PATH, "rb") as fp: 14 | ret = pickle.load(fp) 15 | return ret 16 | 17 | 18 | def dump_persist(value): 19 | with open(CACHE_PERSIST_PATH, "wb") as fp: 20 | pickle.dump(value, fp) 21 | 22 | 23 | def get_persist(key, default_value): 24 | p = load_persist() 25 | if key in p: 26 | return p[key] 27 | else: 28 | return default_value 29 | 30 | 31 | def set_persist(key, value): 32 | p = load_persist() 33 | p[key] = value 34 | dump_persist(p) 35 | -------------------------------------------------------------------------------- /components/resource.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resource/add.png 4 | resource/subtract.png 5 | resource/快进.png 6 | resource/快退.png 7 | resource/restart .png 8 | resource/save_icon.png 9 | resource/compare.png 10 | resource/start.png 11 | resource/pause.png 12 | resource/stop.png 13 | resource/main.png 14 | resource/open.png 15 | resource/stats.png 16 | resource/down.png 17 | resource/up.png 18 | resource/delete.png 19 | resource/right.png 20 | resource/config.png 21 | 22 | 23 | -------------------------------------------------------------------------------- /components/resource/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/add.png -------------------------------------------------------------------------------- /components/resource/compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/compare.png -------------------------------------------------------------------------------- /components/resource/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/config.png -------------------------------------------------------------------------------- /components/resource/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/delete.png -------------------------------------------------------------------------------- /components/resource/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/down.png -------------------------------------------------------------------------------- /components/resource/main.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/main.ico -------------------------------------------------------------------------------- /components/resource/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/main.png -------------------------------------------------------------------------------- /components/resource/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/open.png -------------------------------------------------------------------------------- /components/resource/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/pause.png -------------------------------------------------------------------------------- /components/resource/restart .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/restart .png -------------------------------------------------------------------------------- /components/resource/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/right.png -------------------------------------------------------------------------------- /components/resource/save_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/save_icon.png -------------------------------------------------------------------------------- /components/resource/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/start.png -------------------------------------------------------------------------------- /components/resource/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/stats.png -------------------------------------------------------------------------------- /components/resource/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/stop.png -------------------------------------------------------------------------------- /components/resource/subtract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/subtract.png -------------------------------------------------------------------------------- /components/resource/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/up.png -------------------------------------------------------------------------------- /components/resource/快进.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/快进.png -------------------------------------------------------------------------------- /components/resource/快退.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/components/resource/快退.png -------------------------------------------------------------------------------- /components/status_code_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from logging import error 3 | from traceback import format_exc 4 | from components.customwidget import critical_win 5 | 6 | 7 | class StatusCode(Enum): 8 | """状态码枚举类""" 9 | OK = '成功' 10 | ERROR = '错误' 11 | FILE_NOT_FOUND = '文件不存在' 12 | FILE_PATH_NOT_VALID = '文件路径不合法' 13 | IMAGE_FORMAT_ERR = '图像格式错误' 14 | IMAGE_FORMAT_NOT_SUPPORT = '图像格式不支持' 15 | IMAGE_READ_ERR = '图像读取失败' 16 | IMAGE_IS_NONE = '图片为空' 17 | 18 | 19 | class ImageToolError(Exception): 20 | def show(self): 21 | error(format_exc()) 22 | critical_win(str(self)) 23 | 24 | 25 | class FileNotFoundErr(ImageToolError): 26 | def __init__(self): 27 | super().__init__('文件不存在') 28 | 29 | 30 | class FilePathNotValidErr(ImageToolError): 31 | def __init__(self): 32 | super().__init__('文件路径不合法') 33 | 34 | 35 | class ImageFormatErr(ImageToolError): 36 | def __init__(self): 37 | super().__init__('图像格式错误') 38 | 39 | 40 | class ImageFormatNotSupportErr(ImageToolError): 41 | def __init__(self): 42 | super().__init__('图像格式不支持') 43 | 44 | 45 | class ImageReadErr(ImageToolError): 46 | def __init__(self): 47 | super().__init__('图像读取失败') 48 | 49 | 50 | class ImageNoneErr(ImageToolError): 51 | def __init__(self): 52 | super().__init__('图片为空') 53 | -------------------------------------------------------------------------------- /components/ui/check_update_win.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'check_update_win.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | 16 | class Ui_CheckUpdate(object): 17 | def setupUi(self, CheckUpdate): 18 | if not CheckUpdate.objectName(): 19 | CheckUpdate.setObjectName(u"CheckUpdate") 20 | CheckUpdate.resize(395, 294) 21 | self.gridLayout = QGridLayout(CheckUpdate) 22 | self.gridLayout.setObjectName(u"gridLayout") 23 | self.verticalLayout = QVBoxLayout() 24 | self.verticalLayout.setObjectName(u"verticalLayout") 25 | self.version_num = QLabel(CheckUpdate) 26 | self.version_num.setObjectName(u"version_num") 27 | 28 | self.verticalLayout.addWidget(self.version_num) 29 | 30 | self.textBrowser = QTextBrowser(CheckUpdate) 31 | self.textBrowser.setObjectName(u"textBrowser") 32 | self.textBrowser.setOpenExternalLinks(True) 33 | 34 | self.verticalLayout.addWidget(self.textBrowser) 35 | 36 | self.horizontalLayout = QHBoxLayout() 37 | self.horizontalLayout.setObjectName(u"horizontalLayout") 38 | self.autoupdate = QCheckBox(CheckUpdate) 39 | self.autoupdate.setObjectName(u"autoupdate") 40 | 41 | self.horizontalLayout.addWidget(self.autoupdate) 42 | 43 | self.progress = QProgressBar(CheckUpdate) 44 | self.progress.setObjectName(u"progress") 45 | self.progress.setValue(0) 46 | 47 | self.horizontalLayout.addWidget(self.progress) 48 | 49 | self.ok = QPushButton(CheckUpdate) 50 | self.ok.setObjectName(u"ok") 51 | self.ok.setLayoutDirection(Qt.RightToLeft) 52 | 53 | self.horizontalLayout.addWidget(self.ok) 54 | 55 | self.cancel = QPushButton(CheckUpdate) 56 | self.cancel.setObjectName(u"cancel") 57 | self.cancel.setLayoutDirection(Qt.RightToLeft) 58 | 59 | self.horizontalLayout.addWidget(self.cancel) 60 | 61 | 62 | self.verticalLayout.addLayout(self.horizontalLayout) 63 | 64 | 65 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 66 | 67 | 68 | self.retranslateUi(CheckUpdate) 69 | 70 | QMetaObject.connectSlotsByName(CheckUpdate) 71 | # setupUi 72 | 73 | def retranslateUi(self, CheckUpdate): 74 | CheckUpdate.setWindowTitle(QCoreApplication.translate("CheckUpdate", u"\u68c0\u67e5\u66f4\u65b0", None)) 75 | self.version_num.setText("") 76 | self.autoupdate.setText(QCoreApplication.translate("CheckUpdate", u"\u81ea\u52a8\u66f4\u65b0", None)) 77 | self.ok.setText(QCoreApplication.translate("CheckUpdate", u"\u7acb\u523b\u66f4\u65b0", None)) 78 | self.cancel.setText(QCoreApplication.translate("CheckUpdate", u"\u4ee5\u540e\u518d\u8bf4", None)) 79 | # retranslateUi 80 | 81 | -------------------------------------------------------------------------------- /components/ui/check_update_win.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CheckUpdate 4 | 5 | 6 | 7 | 0 8 | 0 9 | 395 10 | 294 11 | 12 | 13 | 14 | 检查更新 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 自动更新 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | 52 | Qt::RightToLeft 53 | 54 | 55 | 立刻更新 56 | 57 | 58 | 59 | 60 | 61 | 62 | Qt::RightToLeft 63 | 64 | 65 | 以后再说 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /components/ui/help_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'help_window.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | 16 | class Ui_HelpWindow(object): 17 | def setupUi(self, HelpWindow): 18 | if not HelpWindow.objectName(): 19 | HelpWindow.setObjectName(u"HelpWindow") 20 | HelpWindow.resize(800, 600) 21 | self.centralwidget = QWidget(HelpWindow) 22 | self.centralwidget.setObjectName(u"centralwidget") 23 | self.gridLayout = QGridLayout(self.centralwidget) 24 | self.gridLayout.setObjectName(u"gridLayout") 25 | self.textBrowser = QTextBrowser(self.centralwidget) 26 | self.textBrowser.setObjectName(u"textBrowser") 27 | font = QFont() 28 | font.setFamily(u"\u5fae\u8f6f\u96c5\u9ed1") 29 | font.setPointSize(15) 30 | self.textBrowser.setFont(font) 31 | self.textBrowser.setAcceptRichText(False) 32 | self.textBrowser.setOpenExternalLinks(True) 33 | self.textBrowser.setOpenLinks(True) 34 | 35 | self.gridLayout.addWidget(self.textBrowser, 0, 0, 1, 1) 36 | 37 | HelpWindow.setCentralWidget(self.centralwidget) 38 | self.menubar = QMenuBar(HelpWindow) 39 | self.menubar.setObjectName(u"menubar") 40 | self.menubar.setGeometry(QRect(0, 0, 800, 22)) 41 | HelpWindow.setMenuBar(self.menubar) 42 | self.statusbar = QStatusBar(HelpWindow) 43 | self.statusbar.setObjectName(u"statusbar") 44 | HelpWindow.setStatusBar(self.statusbar) 45 | 46 | self.retranslateUi(HelpWindow) 47 | 48 | QMetaObject.connectSlotsByName(HelpWindow) 49 | # setupUi 50 | 51 | def retranslateUi(self, HelpWindow): 52 | HelpWindow.setWindowTitle(QCoreApplication.translate("HelpWindow", u"\u5e2e\u52a9\u624b\u518c", None)) 53 | # retranslateUi 54 | 55 | -------------------------------------------------------------------------------- /components/ui/help_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | HelpWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | 帮助手册 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 微软雅黑 23 | 15 24 | 25 | 26 | 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 0 44 | 800 45 | 22 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /components/ui/mainwindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'mainwindow.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | 16 | class Ui_MainWindow(object): 17 | def setupUi(self, MainWindow): 18 | if not MainWindow.objectName(): 19 | MainWindow.setObjectName(u"MainWindow") 20 | MainWindow.setWindowModality(Qt.NonModal) 21 | MainWindow.resize(1140, 857) 22 | icon = QIcon() 23 | icon.addFile(u":/tool_icon/resource/main.png", QSize(), QIcon.Normal, QIcon.Off) 24 | MainWindow.setWindowIcon(icon) 25 | self.shake_tool = QAction(MainWindow) 26 | self.shake_tool.setObjectName(u"shake_tool") 27 | self.imageeditor = QAction(MainWindow) 28 | self.imageeditor.setObjectName(u"imageeditor") 29 | self.about = QAction(MainWindow) 30 | self.about.setObjectName(u"about") 31 | self.rawimageeditor = QAction(MainWindow) 32 | self.rawimageeditor.setObjectName(u"rawimageeditor") 33 | self.video_compare = QAction(MainWindow) 34 | self.video_compare.setObjectName(u"video_compare") 35 | self.pqtools2code = QAction(MainWindow) 36 | self.pqtools2code.setObjectName(u"pqtools2code") 37 | self.field_depth_tool = QAction(MainWindow) 38 | self.field_depth_tool.setObjectName(u"field_depth_tool") 39 | self.af_calc_tool = QAction(MainWindow) 40 | self.af_calc_tool.setObjectName(u"af_calc_tool") 41 | self.userguide = QAction(MainWindow) 42 | self.userguide.setObjectName(u"userguide") 43 | self.clearcache = QAction(MainWindow) 44 | self.clearcache.setObjectName(u"clearcache") 45 | self.checkupdate = QAction(MainWindow) 46 | self.checkupdate.setObjectName(u"checkupdate") 47 | self.yuv_viewer = QAction(MainWindow) 48 | self.yuv_viewer.setObjectName(u"yuv_viewer") 49 | self.centralwidget = QWidget(MainWindow) 50 | self.centralwidget.setObjectName(u"centralwidget") 51 | self.gridLayout_2 = QGridLayout(self.centralwidget) 52 | self.gridLayout_2.setObjectName(u"gridLayout_2") 53 | self.mdiArea = QMdiArea(self.centralwidget) 54 | self.mdiArea.setObjectName(u"mdiArea") 55 | self.mdiArea.setAcceptDrops(True) 56 | self.mdiArea.setLineWidth(0) 57 | self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 58 | self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 59 | self.mdiArea.setViewMode(QMdiArea.TabbedView) 60 | self.mdiArea.setTabsClosable(True) 61 | self.mdiArea.setTabsMovable(True) 62 | self.mdiArea.setTabShape(QTabWidget.Rounded) 63 | 64 | self.gridLayout_2.addWidget(self.mdiArea, 0, 0, 1, 1) 65 | 66 | MainWindow.setCentralWidget(self.centralwidget) 67 | self.menubar = QMenuBar(MainWindow) 68 | self.menubar.setObjectName(u"menubar") 69 | self.menubar.setGeometry(QRect(0, 0, 1140, 22)) 70 | self.menu_2 = QMenu(self.menubar) 71 | self.menu_2.setObjectName(u"menu_2") 72 | self.menuISP = QMenu(self.menubar) 73 | self.menuISP.setObjectName(u"menuISP") 74 | self.menu = QMenu(self.menubar) 75 | self.menu.setObjectName(u"menu") 76 | MainWindow.setMenuBar(self.menubar) 77 | 78 | self.menubar.addAction(self.menu_2.menuAction()) 79 | self.menubar.addAction(self.menuISP.menuAction()) 80 | self.menubar.addAction(self.menu.menuAction()) 81 | self.menu_2.addAction(self.shake_tool) 82 | self.menu_2.addAction(self.imageeditor) 83 | self.menu_2.addAction(self.rawimageeditor) 84 | self.menu_2.addAction(self.video_compare) 85 | self.menu_2.addAction(self.yuv_viewer) 86 | self.menuISP.addAction(self.pqtools2code) 87 | self.menuISP.addAction(self.field_depth_tool) 88 | self.menu.addAction(self.userguide) 89 | self.menu.addAction(self.clearcache) 90 | self.menu.addAction(self.checkupdate) 91 | 92 | self.retranslateUi(MainWindow) 93 | 94 | QMetaObject.connectSlotsByName(MainWindow) 95 | # setupUi 96 | 97 | def retranslateUi(self, MainWindow): 98 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"ImageTools", None)) 99 | self.shake_tool.setText(QCoreApplication.translate("MainWindow", u"\u6296\u52a8\u6d4b\u8bd5\u5de5\u5177", None)) 100 | self.imageeditor.setText(QCoreApplication.translate("MainWindow", u"\u56fe\u7247\u5206\u6790\u5de5\u5177", None)) 101 | self.about.setText(QCoreApplication.translate("MainWindow", u"\u5173\u4e8e", None)) 102 | self.rawimageeditor.setText(QCoreApplication.translate("MainWindow", u"raw\u56fe\u5206\u6790\u5de5\u5177", None)) 103 | self.video_compare.setText(QCoreApplication.translate("MainWindow", u"\u89c6\u9891\u5bf9\u6bd4\u5de5\u5177", None)) 104 | self.pqtools2code.setText(QCoreApplication.translate("MainWindow", u"PQtools\u8f6c\u4ee3\u7801", None)) 105 | self.field_depth_tool.setText(QCoreApplication.translate("MainWindow", u"\u955c\u5934\u8ba1\u7b97\u5668", None)) 106 | self.af_calc_tool.setText(QCoreApplication.translate("MainWindow", u"\u955c\u5934\u66f2\u7ebf\u8ba1\u7b97\u5de5\u5177", None)) 107 | self.userguide.setText(QCoreApplication.translate("MainWindow", u"\u7528\u6237\u624b\u518c", None)) 108 | self.clearcache.setText(QCoreApplication.translate("MainWindow", u"\u6e05\u7406\u7f13\u5b58", None)) 109 | self.checkupdate.setText(QCoreApplication.translate("MainWindow", u"\u68c0\u67e5\u66f4\u65b0", None)) 110 | self.yuv_viewer.setText(QCoreApplication.translate("MainWindow", u"YUV\u67e5\u770b\u5de5\u5177", None)) 111 | self.menu_2.setTitle(QCoreApplication.translate("MainWindow", u"\u5de5\u5177", None)) 112 | self.menuISP.setTitle(QCoreApplication.translate("MainWindow", u"ISP", None)) 113 | self.menu.setTitle(QCoreApplication.translate("MainWindow", u"\u5173\u4e8e", None)) 114 | # retranslateUi 115 | 116 | -------------------------------------------------------------------------------- /components/ui/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | Qt::NonModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 1140 13 | 857 14 | 15 | 16 | 17 | ImageTools 18 | 19 | 20 | 21 | :/tool_icon/resource/main.png:/tool_icon/resource/main.png 22 | 23 | 24 | 25 | 26 | 27 | 28 | true 29 | 30 | 31 | 0 32 | 33 | 34 | Qt::ScrollBarAsNeeded 35 | 36 | 37 | Qt::ScrollBarAsNeeded 38 | 39 | 40 | QMdiArea::TabbedView 41 | 42 | 43 | true 44 | 45 | 46 | true 47 | 48 | 49 | QTabWidget::Rounded 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 0 59 | 0 60 | 1140 61 | 22 62 | 63 | 64 | 65 | 66 | 工具 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ISP 77 | 78 | 79 | 80 | 81 | 82 | 83 | 关于 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 抖动测试工具 96 | 97 | 98 | 99 | 100 | 图片分析工具 101 | 102 | 103 | 104 | 105 | 关于 106 | 107 | 108 | 109 | 110 | raw图分析工具 111 | 112 | 113 | 114 | 115 | 视频对比工具 116 | 117 | 118 | 119 | 120 | PQtools转代码 121 | 122 | 123 | 124 | 125 | 镜头计算器 126 | 127 | 128 | 129 | 130 | 镜头曲线计算工具 131 | 132 | 133 | 134 | 135 | 用户手册 136 | 137 | 138 | 139 | 140 | 清理缓存 141 | 142 | 143 | 144 | 145 | 检查更新 146 | 147 | 148 | 149 | 150 | YUV查看工具 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /components/window.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QMessageBox, QMainWindow, QProgressBar, QLabel 2 | from components.ui.mainwindow import Ui_MainWindow 3 | import pickle 4 | import os 5 | 6 | CACHE_FILEPATH = './config' 7 | 8 | 9 | class MainWindow(QMainWindow): 10 | """对QMainWindow类重写,实现一些功能""" 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.sub_windows = list() 15 | self.filename = './config/ImageToolsSubWindows.tmp' 16 | self.sub_windows_list = list() 17 | self.ui = Ui_MainWindow() 18 | self.ui.setupUi(self) 19 | self.need_clear_cache = False 20 | if os.path.exists(self.filename): 21 | with open(self.filename, "rb") as fp: 22 | self.sub_windows_list = pickle.load(fp) 23 | 24 | def closeEvent(self, event): 25 | """ 26 | 重写closeEvent方法,实现dialog窗体关闭时执行一些代码 27 | :param event: close()触发的事件 28 | :return: None 29 | """ 30 | reply = QMessageBox.question(self, 31 | 'ImageTools', 32 | "是否要退出程序?", 33 | QMessageBox.Yes | QMessageBox.No, 34 | QMessageBox.No) 35 | if reply == QMessageBox.Yes: 36 | if self.need_clear_cache == False: 37 | if not os.path.exists(CACHE_FILEPATH): 38 | os.mkdir(CACHE_FILEPATH) 39 | sub_windows_list = list() 40 | while len(self.sub_windows) > 0: 41 | if (self.sub_windows[0].name is not None): 42 | sub_windows_list.append(self.sub_windows[0].name) 43 | self.sub_windows[0].close() 44 | with open(self.filename, "wb") as fp: 45 | pickle.dump(sub_windows_list, fp) 46 | else: 47 | # 清楚缓存 48 | if os.path.exists(CACHE_FILEPATH): 49 | for files in os.listdir(CACHE_FILEPATH): 50 | filepath = os.path.join(CACHE_FILEPATH, files) 51 | if os.path.isfile(filepath): 52 | os.remove(filepath) 53 | event.accept() 54 | else: 55 | event.ignore() 56 | 57 | 58 | class SubWindow(QMainWindow): 59 | """对QMainWindow类重写,实现一些功能""" 60 | 61 | def __init__(self, name, parent, ui_view, need_processBar=False): 62 | super().__init__(parent) 63 | self.parent = parent 64 | self.name = name 65 | self.filename = "./config/" + name + ".tmp" 66 | self.__saved_params = None 67 | self.ui = ui_view 68 | self.ui.setupUi(self) 69 | # add 进度条和详细信息显示 需要在ui里面加入statusBar 70 | if(need_processBar == True): 71 | self.progress_bar = QProgressBar() 72 | self.info_bar = QLabel() 73 | self.time_bar = QLabel() 74 | self.ui.statusBar.addPermanentWidget(self.info_bar, stretch=8) 75 | self.ui.statusBar.addPermanentWidget(self.time_bar, stretch=1) 76 | self.ui.statusBar.addPermanentWidget(self.progress_bar, stretch=2) 77 | self.progress_bar.setRange(0, 100) # 设置进度条的范围 78 | self.progress_bar.setValue(0) 79 | 80 | def load_params(self, init_value): 81 | """ 82 | 加载存储的类,返回的参数可以直接进行修改,会保存到本地,下一次打开会自动加载 83 | """ 84 | if os.path.exists(self.filename): 85 | with open(self.filename, "rb") as fp: 86 | self.__saved_params = pickle.load(fp) 87 | if (self.__saved_params is None): 88 | self.__saved_params = init_value 89 | return self.__saved_params 90 | 91 | def closeEvent(self, event): 92 | """ 93 | 重写closeEvent方法,实现dialog窗体关闭时执行一些代码 94 | :param event: close()触发的事件 95 | :return: None 96 | """ 97 | if not os.path.exists(CACHE_FILEPATH): 98 | os.mkdir(CACHE_FILEPATH) 99 | self.name = None 100 | with open(self.filename, "wb") as fp: 101 | pickle.dump(self.__saved_params, fp) 102 | try: 103 | self.parent.sub_windows.remove(self) 104 | except Exception: 105 | print('{}工具不支持记忆存储'.format(self.name)) 106 | event.accept() 107 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyside2 2 | matplotlib 3 | numpy 4 | opencv-python 5 | scipy 6 | numba 7 | PyWavelets 8 | logging 9 | requests 10 | natsort -------------------------------------------------------------------------------- /subapps/FocusDepthTool.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "FocusDepthTool" 5 | #define MyAppVersion "1.3" 6 | #define MyAppPublisher "liqinxing, Inc." 7 | #define MyAppURL "https://www.qinxing.xyz/" 8 | #define MyAppExeName "FocusDepthTool.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{12F34F85-9D0D-4F1B-9492-E8875EE23A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName=C:/ImageTools\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=..\Output 26 | OutputBaseFilename={#MyAppName} 27 | SetupIconFile=..\ui\resource\main.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | 38 | [Files] 39 | Source: "..\dist\{#MyAppName}\{#MyAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: "..\dist\{#MyAppName}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 45 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | -------------------------------------------------------------------------------- /subapps/FocusDepthTool.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("..") 3 | from tools.depth_of_focus.depth_of_focus import FieldDepthWindow 4 | from PySide2.QtWidgets import QApplication 5 | from PySide2.QtCore import Qt 6 | 7 | if __name__ == "__main__": 8 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 9 | apps = QApplication([]) 10 | apps.setStyle('Fusion') 11 | appswindow = FieldDepthWindow() 12 | appswindow.show() 13 | sys.exit(apps.exec_()) 14 | -------------------------------------------------------------------------------- /subapps/PQtools2Code.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "PQtools2Code" 5 | #define MyAppVersion "1.3" 6 | #define MyAppPublisher "liqinxing, Inc." 7 | #define MyAppURL "https://www.qinxing.xyz/" 8 | #define MyAppExeName "PQtools2Code.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{12F34F85-9D0D-4F1B-9492-E8875EE23A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName=C:/ImageTools\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=..\Output 26 | OutputBaseFilename={#MyAppName} 27 | SetupIconFile=..\ui\resource\main.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | 38 | [Files] 39 | Source: "..\dist\{#MyAppName}\{#MyAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: "..\dist\{#MyAppName}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 45 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | -------------------------------------------------------------------------------- /subapps/PQtools2Code.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("..") 3 | from tools.pqtools_to_code.pqtools_to_code import PQtoolsToCode 4 | from PySide2.QtWidgets import QApplication 5 | from PySide2.QtCore import Qt 6 | 7 | if __name__ == "__main__": 8 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 9 | apps = QApplication([]) 10 | apps.setStyle('Fusion') 11 | appswindow = PQtoolsToCode() 12 | appswindow.show() 13 | sys.exit(apps.exec_()) 14 | -------------------------------------------------------------------------------- /subapps/ShakeTestTool.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "ShakeTestTool" 5 | #define MyAppVersion "1.3" 6 | #define MyAppPublisher "liqinxing, Inc." 7 | #define MyAppURL "https://www.qinxing.xyz/" 8 | #define MyAppExeName "ShakeTestTool.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{12F34F85-9D0D-4F1B-9492-E8875EE23A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName=C:/ImageTools\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=..\Output 26 | OutputBaseFilename={#MyAppName} 27 | SetupIconFile=..\ui\resource\main.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | 38 | [Files] 39 | Source: "..\dist\{#MyAppName}\{#MyAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: "..\dist\{#MyAppName}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 45 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | -------------------------------------------------------------------------------- /subapps/ShakeTestTool.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("..") 3 | from tools.shake_test.shake_test import ShakeTestTool 4 | from PySide2.QtWidgets import QApplication 5 | from PySide2.QtCore import Qt 6 | 7 | if __name__ == "__main__": 8 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 9 | apps = QApplication([]) 10 | apps.setStyle('Fusion') 11 | appswindow = ShakeTestTool() 12 | appswindow.show() 13 | sys.exit(apps.exec_()) 14 | -------------------------------------------------------------------------------- /subapps/VideoCompare.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "VideoCompare" 5 | #define MyAppVersion "1.3" 6 | #define MyAppPublisher "liqinxing, Inc." 7 | #define MyAppURL "https://www.qinxing.xyz/" 8 | #define MyAppExeName "VideoCompare.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{12F34F85-9D0D-4F1B-9492-E8875EE23A31} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName=C:/ImageTools\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputDir=..\Output 26 | OutputBaseFilename={#MyAppName} 27 | SetupIconFile=..\ui\resource\main.ico 28 | Compression=lzma 29 | SolidCompression=yes 30 | WizardStyle=modern 31 | 32 | [Languages] 33 | Name: "english"; MessagesFile: "compiler:Default.isl" 34 | 35 | [Tasks] 36 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 37 | 38 | [Files] 39 | Source: "..\dist\{#MyAppName}\{#MyAppName}.exe"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: "..\dist\{#MyAppName}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 41 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 42 | 43 | [Icons] 44 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 45 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 46 | 47 | [Run] 48 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | 50 | -------------------------------------------------------------------------------- /subapps/VideoCompare.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("..") 3 | from tools.video_compare.videocompare import VideoCompare 4 | from PySide2.QtWidgets import QApplication 5 | from PySide2.QtCore import Qt 6 | 7 | if __name__ == "__main__": 8 | QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 9 | apps = QApplication([]) 10 | apps.setStyle('Fusion') 11 | appswindow = VideoCompare() 12 | appswindow.show() 13 | sys.exit(apps.exec_()) 14 | -------------------------------------------------------------------------------- /subapps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/subapps/__init__.py -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/__init__.py -------------------------------------------------------------------------------- /test/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/components/__init__.py -------------------------------------------------------------------------------- /test/components/test_basic_image.py: -------------------------------------------------------------------------------- 1 | from os.path import samefile, isfile 2 | from components.BasicImage import ImageBasic 3 | from components.status_code_enum import StatusCode 4 | from components.status_code_enum import * 5 | 6 | 7 | def test_status_code(): 8 | ret = StatusCode.OK 9 | if ret is StatusCode.OK: 10 | assert True 11 | 12 | ret = StatusCode.FILE_NOT_FOUND 13 | if ret is StatusCode.OK: 14 | assert '文件不存在' == StatusCode.FILE_NOT_FOUND.value 15 | assert StatusCode.FILE_NOT_FOUND.name == 'FILE_NOT_FOUND' 16 | 17 | 18 | class TestBasicImage: 19 | def test_load_imagefile(self): 20 | img = ImageBasic() 21 | try: 22 | img.load_file("test/resource/5.jpg") 23 | assert False 24 | except FileNotFoundErr: 25 | assert True 26 | except Exception: 27 | assert False 28 | 29 | try: 30 | img.load_file("test/resource/1.jpg") 31 | assert True 32 | except Exception: 33 | assert False 34 | 35 | def test_save_and_remove_file(self): 36 | img = ImageBasic() 37 | try: 38 | img.save_image("test/resource/1-1.jpg") 39 | assert False 40 | except ImageNoneErr: 41 | assert True 42 | except Exception: 43 | assert False 44 | 45 | try: 46 | img.load_file("test/resource/1.jpg") 47 | img.save_image("test/resource/1-1.jpg") 48 | assert isfile("test/resource/1-1.jpg") 49 | img.remove_image() 50 | assert isfile("test/resource/1.jpg") is False 51 | assert img.img is None 52 | assert True 53 | except Exception: 54 | assert False 55 | 56 | try: 57 | img.load_file("test/resource/1-1.jpg") 58 | img.save_image("test/resource/1.jpg") 59 | img.save_image("test/resource/1.jpg") 60 | assert isfile("test/resource/1.jpg") 61 | img.remove_image() 62 | assert isfile("test/resource/1.jpg") 63 | assert img.img is None 64 | assert True 65 | except Exception: 66 | assert False 67 | 68 | def test_next_photo(self): 69 | img = ImageBasic() 70 | try: 71 | img.load_file("test/resource/2.jpg") 72 | next_photo_name, index, files_nums = img.find_next_time_photo(1) 73 | assert samefile(next_photo_name, "test/resource/3.png") 74 | assert index == 3 75 | assert files_nums == 5 76 | 77 | next_photo_name, index, files_nums = img.find_next_time_photo(-1) 78 | assert samefile(next_photo_name, 79 | "test/resource/星空背景 绘画 夜空 4k壁纸_彼岸图网.jpg") 80 | assert index == 1 81 | assert files_nums == 5 82 | 83 | img.load_file("test/resource/3.png") 84 | next_photo_name, index, files_nums = img.find_next_nat_photo(1) 85 | assert samefile(next_photo_name, "test/resource/6月壁纸.jpg") 86 | assert index == 3 87 | assert files_nums == 5 88 | 89 | next_photo_name, index, files_nums = img.find_next_nat_photo(-1) 90 | assert samefile(next_photo_name, "test/resource/2.jpg") 91 | assert index == 1 92 | assert files_nums == 5 93 | assert True 94 | except Exception: 95 | assert False 96 | 97 | def test_get_img_point(self): 98 | img = ImageBasic() 99 | try: 100 | img.load_file("test/resource/2.jpg") 101 | point = img.get_img_point(214, 190) 102 | assert (point == [218, 211, 255]).all() 103 | 104 | img.load_file("test/resource/3.png") 105 | point = img.get_img_point(217, 197) 106 | assert (point == [241, 192, 190]).all() 107 | assert True 108 | except Exception: 109 | assert False 110 | -------------------------------------------------------------------------------- /test/components/test_check_update.py: -------------------------------------------------------------------------------- 1 | from components.check_update import get_version, get_latest_version_log, get_current_version_log, check_is_latest 2 | 3 | 4 | def test_check_is_latest(): 5 | assert check_is_latest('1.5.0', '1.4.0') is True 6 | assert check_is_latest('1.5.0', '1.6.0') is False 7 | assert check_is_latest('1.5.0', '1.12.0') is False 8 | assert check_is_latest('1.5.0', '12.4.0') is False 9 | assert check_is_latest('1.5.0', '0.5.1') is True 10 | 11 | 12 | TEST_VERSION_LOG = """ 13 | **1.5.0** 14 | 15 | 做了一些修改 16 | 17 | **1.4.0** 18 | 19 | 做了另外一些修改 20 | """ 21 | 22 | 23 | def test_get_version(): 24 | assert get_version(TEST_VERSION_LOG) == '1.5.0' 25 | assert get_version(TEST_VERSION_LOG[10:]) == '1.4.0' 26 | 27 | 28 | def test_get_latest_version_log(): 29 | assert get_version(get_latest_version_log()) == get_version( 30 | get_current_version_log()) 31 | 32 | 33 | TEST_DOWNLOAD_LINK = 'https://imagetools.qinxing.xyz/ChromeSetup.exe' 34 | TEST_DOWNLOAD_FILE = './test.exe' 35 | 36 | 37 | def test_download(): 38 | # assert download_file(TEST_DOWNLOAD_LINK) == True 下载文件OK 39 | assert True 40 | -------------------------------------------------------------------------------- /test/resource/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/resource/1.jpg -------------------------------------------------------------------------------- /test/resource/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/resource/2.jpg -------------------------------------------------------------------------------- /test/resource/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/resource/3.png -------------------------------------------------------------------------------- /test/resource/6月壁纸.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/resource/6月壁纸.jpg -------------------------------------------------------------------------------- /test/resource/星空背景 绘画 夜空 4k壁纸_彼岸图网.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/test/resource/星空背景 绘画 夜空 4k壁纸_彼岸图网.jpg -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/__init__.py -------------------------------------------------------------------------------- /tools/depth_of_focus/LenParameters.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | 5 | class LenParameters(object): 6 | def __init__(self): 7 | ''' 8 | @focus_length: 实际焦距(单位mm) 9 | @aperture: 光圈(F) 10 | @focus_distance: 对焦距离(单位mm) 11 | @effective_focus_length: 有效焦距(单位mm) 12 | @confusion_circle_diam: 弥散圈直径(单位mm) 13 | @cmos_size: sensor尺寸(单位mm) 14 | ''' 15 | self.focus_length = 50 16 | self.aperture = 1.4 17 | self.focus_distance = 2000 18 | self.focus_distance_range = [0, 0] 19 | self.aperture_range = [0, 0] 20 | self.focus_range = [0, 0] 21 | self.confusion_circle_diam = 0.04 22 | self.cmos_size = 3.0 23 | 24 | def show(self): 25 | ''' 26 | 调试打印 27 | ''' 28 | # print('=========================================') 29 | # print('实际焦距: ' + str(self.focus_length) + ' mm') 30 | # print('光圈: F/' + str(self.aperture)) 31 | # print('对焦距离: ' + str(self.focus_distance) + ' mm') 32 | # print('弥散圈直径: ' + str(self.confusion_circle_diam) + ' mm') 33 | # print('=========================================') 34 | return 35 | 36 | def calc_front_field_depth(self): 37 | """ 38 | 计算前景深,有两种方法:本文用的https://wenku.baidu.com/view/2191302baf45b307e9719706.html的方法,更加准确一点 39 | """ 40 | # a = self.aperture*self.confusion_circle_diam * \ 41 | # self.focus_distance*self.focus_distance 42 | # b = self.focus_length*self.focus_length + self.aperture * \ 43 | # self.confusion_circle_diam*self.focus_distance 44 | a = self.focus_length*self.focus_length*self.focus_distance 45 | b = self.focus_length*self.focus_length + \ 46 | (self.focus_distance - self.focus_length) * \ 47 | self.aperture*self.confusion_circle_diam 48 | return (a/b) 49 | 50 | def calc_back_field_depth(self): 51 | """ 52 | 计算后景深 53 | """ 54 | # a = self.aperture*self.confusion_circle_diam * \ 55 | # self.focus_distance*self.focus_distance 56 | # b = self.focus_length*self.focus_length - self.aperture * \ 57 | # self.confusion_circle_diam*self.focus_distance 58 | a = self.focus_length*self.focus_length*self.focus_distance 59 | b = self.focus_length*self.focus_length - \ 60 | (self.focus_distance - self.focus_length) * \ 61 | self.aperture*self.confusion_circle_diam 62 | ret = a/b 63 | # 防止后景深计算为负数 64 | if(ret <= 0): 65 | ret = float('inf') 66 | return ret 67 | 68 | def calc_confusion_circle_diam(self): 69 | ''' 70 | 通过CMOS的尺寸计算弥散圈直径,默认是CMOS对角线尺寸除1000 71 | ''' 72 | self.confusion_circle_diam = self.cmos_size/1000 73 | return self.confusion_circle_diam 74 | 75 | def calc_image_distance(self, step=10, unit=1000): 76 | ''' 77 | 计算不同物距范围内的像距 78 | ''' 79 | y = list() 80 | x = range(self.focus_distance_range[0], 81 | self.focus_distance_range[1], step*10) 82 | for self.focus_distance in x: 83 | y.append((self.focus_distance*self.focus_length) / 84 | (self.focus_distance-self.focus_length)) 85 | return (x, y) 86 | 87 | def calc_field_depth(self): 88 | ''' 89 | 计算总景深,后景深减去前景深 90 | ''' 91 | return (self.calc_back_field_depth() - self.calc_front_field_depth()) 92 | 93 | def calc_fov(self): 94 | ''' 95 | 计算对角线视场角 96 | ''' 97 | image_distance = (self.focus_distance*self.focus_length) / \ 98 | (self.focus_distance-self.focus_length) 99 | alpha = math.atan((self.cmos_size/2)/image_distance) 100 | return (2*alpha*180/math.pi) 101 | 102 | def calc_equivalent_focus_length(self): 103 | ''' 104 | 计算等效焦距 105 | ''' 106 | return (43.27/self.cmos_size*self.focus_length) 107 | 108 | def calc_hyperfocal_distance(self): 109 | ''' 110 | 计算超焦距,刚好后景深是无穷远时的对焦距离 111 | ''' 112 | return (self.focus_length*self.focus_length/self.aperture/self.confusion_circle_diam+self.focus_length) 113 | 114 | def calc_depth_map_from_distance(self, step_num=1000, unit=1000): 115 | ''' 116 | 计算不同物距范围内的景深 117 | ''' 118 | y1 = list() 119 | y2 = list() 120 | x = np.linspace(self.focus_distance_range[0], 121 | self.focus_distance_range[1], step_num) 122 | for self.focus_distance in x: 123 | y1.append(self.calc_front_field_depth()) 124 | y2.append(self.calc_back_field_depth()) 125 | y1 = np.array(y1)/unit 126 | y2 = np.array(y2)/unit 127 | x = np.array(x)/unit 128 | return (x, y1, y2) 129 | 130 | def calc_depth_map_from_focus(self, step_num=1000, unit=1000): 131 | ''' 132 | 计算不同焦距范围内的景深 133 | ''' 134 | y1 = list() 135 | y2 = list() 136 | x = np.linspace(self.focus_range[0], 137 | self.focus_range[1], step_num) 138 | for self.focus_length in x: 139 | y1.append(self.calc_front_field_depth()) 140 | y2.append(self.calc_back_field_depth()) 141 | y1 = np.array(y1)/unit 142 | y2 = np.array(y2)/unit 143 | x = np.array(x) 144 | return (x, y1, y2) 145 | 146 | def calc_depth_map_from_apeture(self, step_num=1000, unit=1000): 147 | ''' 148 | 计算不同光圈范围内的景深 149 | ''' 150 | y1 = list() 151 | y2 = list() 152 | x = np.linspace(self.aperture_range[0], 153 | self.aperture_range[1], step_num) 154 | for self.aperture in x: 155 | y1.append(self.calc_front_field_depth()) 156 | value = self.calc_back_field_depth() 157 | # 防止后景深计算为负数 158 | if(value <= 0): 159 | value = float('inf') 160 | y2.append(value) 161 | y1 = np.array(y1)/unit 162 | y2 = np.array(y2)/unit 163 | x = np.array(x) 164 | return (x, y1, y2) 165 | 166 | 167 | class SettingParamters(object): 168 | def __init__(self): 169 | # input setting 170 | self.input_focus_length = False 171 | self.input_apeture = False 172 | self.input_distance = False 173 | # output setting 174 | self.output_field_depth = False 175 | self.output_image_distance = False 176 | self.output_params = False 177 | 178 | 179 | cmos_size_dist = { 180 | "1/6": 3.0, 181 | "1/4": 4.0, 182 | "1/3.6": 5.0, 183 | "1/3.2": 5.678, 184 | "1/3": 6.0, 185 | "1/2.8": 6.46, 186 | "1/2.7": 6.592, 187 | "1/2.5": 7.182, 188 | "1/2": 8.000, 189 | "1/1.8": 8.933, 190 | "1/1.7": 9.500, 191 | "1/1.6": 10.07, 192 | "2/3": 11.00, 193 | "1": 16.0, 194 | "4/3": 22.5, 195 | "1.8": 25.878, 196 | "35mm film": 43.267 197 | } 198 | -------------------------------------------------------------------------------- /tools/depth_of_focus/Readme.md: -------------------------------------------------------------------------------- 1 | # 镜头计算器 2 | ## 整体界面 3 | 4 | ![image-20200903152506219](https://image.qinxing.xyz/20200903152510.png) 5 | 6 | ![image-20200903154550650](https://image.qinxing.xyz/20200903154554.png) 7 | 8 | ## 主要功能 9 | 10 | ### 景深计算 11 | 12 | 1. 图像**输出**选择景深 13 | 14 | 2. 可以选择的**变量**有三种,焦距、光圈和物距,物距就是镜头的对焦距离,这个决定了左侧图像X轴的意义 15 | 16 | 3. **基础设置**:设置镜头的焦距,光圈,物体对焦距离和sensor尺寸,这四个参数必须准确 17 | 18 | 4. 高级设置—**sensor尺寸**:由于sensor尺寸用英寸表示,并不准确,这里提供sensor尺寸的微调。 19 | 20 | 5. 高级设置—**弥散圈直径**:景深就是图像清晰的范围,而弥散圈就是衡量图像清晰的标准。滑块往左拖,弥散圈越小,那么清晰度的标准就越高,景深对应也会越小。 21 | 22 | 6. **高级设置**:焦距、光圈、对焦范围,是用来调节图像X轴的范围 23 | 24 | 7. 最后点击**确认**就会输出想要的图像 25 | 26 | 8. 图像下方有对图像微调的**按钮** 27 | 28 | 从左到右的功能分别是还原图像,回退上一步,前进一步,移动图像,图像局部放大,调整图像坐标轴的范围,调整图像坐标轴的名称,另存图像 29 | 30 | ### 参数输出 31 | 32 | 1. 图像输出选择参数 33 | 2. 其余步骤参考景深计算一致 34 | 3. 等效焦距:sensor对角线的长度,等效成35mm照相机画幅对角线长度(42.27mm)时,其镜头的实际焦距所对应的35mm照相机镜头的焦距。 35 | 4. 超焦距距离:对焦距离越近,前景深越近。而对焦在远处的某一点,使得景深的另一极端恰为”无限远“,此时对焦距离就称作超焦距距离。相机对焦在超焦距距离,可以让景深最大 36 | 5. 前景深距离:当前配置下,能看清的最近距离 37 | 6. 后景深距离:当前配置下,能看清的最远距离 38 | 7. 总景深:当前配置下,清晰的范围 39 | 40 | ## 计算原理 41 | 42 | 本文用的方法来自此[论文](https://wenku.baidu.com/view/2191302baf45b307e9719706.html) 43 | 44 | -------------------------------------------------------------------------------- /tools/depth_of_focus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/depth_of_focus/__init__.py -------------------------------------------------------------------------------- /tools/depth_of_focus/depth_of_focus.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from tools.depth_of_focus.field_depth_window import Ui_FieldDepthWindow 3 | from tools.depth_of_focus.LenParameters import LenParameters, SettingParamters, cmos_size_dist 4 | from components.customwidget import MatplotlibLayout, ParamsTable 5 | from components.window import SubWindow 6 | 7 | 8 | class FieldDepthWindow(SubWindow): 9 | def __init__(self, name='FieldDepthWindow', parent=None): 10 | super().__init__(name, parent, Ui_FieldDepthWindow()) 11 | [self.params, self.setting] = self.load_params( 12 | [LenParameters(), SettingParamters()]) 13 | self.init_params_range() 14 | 15 | def show(self): 16 | super().show() 17 | self.ui.pushButton.clicked.connect(self.finished_plot_cb) 18 | self.ui.sensor_size.editingFinished.connect(self.coms_size_changed_cb) 19 | self.ui.confusion_circle_diam_slide.sliderMoved.connect( 20 | self.confusion_circle_diam_changed_cb) 21 | self.ui.sensor_size_list.currentTextChanged.connect( 22 | self.coms_size_list_changed_cb) 23 | 24 | self.set_ui_params() 25 | 26 | self.plot_fig = MatplotlibLayout(self.ui.plotview) 27 | self.tableWidget = ParamsTable(self.ui.plotview) 28 | self.finished_plot_cb() 29 | 30 | def init_params_range(self): 31 | self.params.aperture_range[0] = self.params.aperture/2 32 | self.params.aperture_range[1] = self.params.aperture*2 33 | self.params.focus_distance_range[0] = self.params.focus_distance/2 34 | self.params.focus_distance_range[1] = self.params.focus_distance*2 35 | self.params.focus_range[0] = self.params.focus_length/2 36 | self.params.focus_range[1] = self.params.focus_length*2 37 | 38 | def plot_figure(self): 39 | self.plot_fig.clean() 40 | if(self.setting.output_field_depth == True): 41 | if(self.setting.input_distance == True): 42 | (x, y1, y2) = self.params.calc_depth_map_from_distance() 43 | self.plot_fig.label("对焦距离(m)", "景深范围(m)") 44 | elif(self.setting.input_focus_length == True): 45 | (x, y1, y2) = self.params.calc_depth_map_from_focus() 46 | self.plot_fig.label("焦距(mm)", "景深范围(m)") 47 | elif(self.setting.input_apeture == True): 48 | (x, y1, y2) = self.params.calc_depth_map_from_apeture() 49 | self.plot_fig.label("光圈值(F)", "景深范围(m)") 50 | self.plot_fig.input_2line(x, y1, y2) 51 | self.plot_fig.draw() 52 | 53 | def plot_params(self): 54 | self.tableWidget.clean() 55 | self.tableWidget.append("视场角", str(self.params.calc_fov()), '度') 56 | self.tableWidget.append("等效焦距", str( 57 | self.params.calc_equivalent_focus_length()), 'mm') 58 | self.tableWidget.append("超焦距距离", str( 59 | self.params.calc_hyperfocal_distance()/1000), 'm') 60 | self.tableWidget.append("前景深", str( 61 | self.params.calc_front_field_depth()/1000), 'm') 62 | self.tableWidget.append("后景深", str( 63 | self.params.calc_back_field_depth()/1000), 'm') 64 | self.tableWidget.append("总景深", str( 65 | self.params.calc_field_depth()/1000), 'm') 66 | self.tableWidget.show() 67 | 68 | def calc_len_params(self): 69 | # print('视场角:'+str(self.params.calc_fov())+'度') 70 | # print('等效焦距:'+str(self.params.calc_equivalent_focus_length())+'mm') 71 | # print('超焦距距离:'+str(self.params.calc_hyperfocal_distance()/1000) + 'm') 72 | # print("前景深:"+str(self.params.calc_front_field_depth()/1000) + 'm') 73 | # print("后景深:"+str(self.params.calc_back_field_depth()/1000) + 'm') 74 | # print("总景深:" + str(self.params.calc_field_depth()/1000)+'m') 75 | return 76 | 77 | # get params 78 | def get_ui_params(self): 79 | # basic setting 80 | self.params.focus_length = float(self.ui.focus_length.value()) 81 | self.params.confusion_circle_diam = float( 82 | self.ui.confusion_circle_diam.value()) 83 | self.params.aperture = float(self.ui.aperture.value()) 84 | self.params.focus_distance = float(self.ui.focus_distance.value())*1000 85 | self.params.cmos_size = float(self.ui.sensor_size.value()) 86 | # input setting 87 | self.setting.input_focus_length = self.ui.input_focus_length.isChecked() 88 | self.setting.input_apeture = self.ui.input_apeture.isChecked() 89 | self.setting.input_distance = self.ui.input_distance.isChecked() 90 | # output setting 91 | self.setting.output_field_depth = self.ui.output_field_depth.isChecked() 92 | self.setting.output_image_distance = self.ui.output_image_distance.isChecked() 93 | self.setting.output_params = self.ui.output_params.isChecked() 94 | # advanced setting 95 | self.params.aperture_range[0] = float( 96 | self.ui.apeture_min_range.value()) 97 | self.params.aperture_range[1] = float( 98 | self.ui.apeture_max_range.value()) 99 | self.params.focus_distance_range[0] = float( 100 | self.ui.distance_min_range.value())*1000 101 | self.params.focus_distance_range[1] = float( 102 | self.ui.distance_max_range.value())*1000 103 | self.params.focus_range[0] = float(self.ui.focus_min_range.value()) 104 | self.params.focus_range[1] = float(self.ui.focus_max_range.value()) 105 | 106 | def set_ui_params(self): 107 | # basic setting 108 | self.ui.focus_length.setValue(self.params.focus_length) 109 | self.ui.confusion_circle_diam.setValue( 110 | self.params.confusion_circle_diam) 111 | self.ui.aperture.setValue(self.params.aperture) 112 | self.ui.focus_distance.setValue(self.params.focus_distance / 1000) 113 | self.ui.sensor_size.setValue(self.params.cmos_size) 114 | 115 | # input setting 116 | self.ui.input_focus_length.setChecked(self.setting.input_focus_length) 117 | self.ui.input_apeture.setChecked(self.setting.input_apeture) 118 | self.ui.input_distance.setChecked(self.setting.input_distance) 119 | # output setting 120 | self.ui.output_field_depth.setChecked(self.setting.output_field_depth) 121 | self.ui.output_image_distance.setChecked( 122 | self.setting.output_image_distance) 123 | self.ui.output_params.setChecked(self.setting.output_params) 124 | 125 | # advanced setting 126 | self.ui.apeture_min_range.setValue(self.params.aperture_range[0]) 127 | self.ui.apeture_max_range.setValue(self.params.aperture_range[1]) 128 | self.ui.distance_min_range.setValue( 129 | self.params.focus_distance_range[0]/1000) 130 | self.ui.distance_max_range.setValue( 131 | self.params.focus_distance_range[1]/1000) 132 | self.ui.focus_min_range.setValue(self.params.focus_range[0]) 133 | self.ui.focus_max_range.setValue(self.params.focus_range[1]) 134 | 135 | # CALLBACKS 136 | def finished_plot_cb(self): 137 | self.get_ui_params() 138 | if self.setting.output_field_depth == True: 139 | self.tableWidget.clean() 140 | self.plot_figure() 141 | elif self.setting.output_params == True: 142 | self.plot_fig.clean() 143 | self.plot_params() 144 | self.calc_len_params() 145 | 146 | def coms_size_list_changed_cb(self): 147 | self.params.cmos_size = cmos_size_dist[self.ui.sensor_size_list.currentText( 148 | )] 149 | self.ui.sensor_size.setValue(self.params.cmos_size) 150 | self.confusion_circle_diam_changed_cb() 151 | 152 | def coms_size_changed_cb(self): 153 | self.params.cmos_size = self.ui.sensor_size.value() 154 | self.confusion_circle_diam_changed_cb() 155 | 156 | def confusion_circle_diam_changed_cb(self): 157 | value = self.ui.confusion_circle_diam_slide.value() 158 | self.params.confusion_circle_diam = self.params.cmos_size / \ 159 | 1000*(0.5+value/100) 160 | self.ui.confusion_circle_diam.setValue( 161 | self.params.confusion_circle_diam) 162 | -------------------------------------------------------------------------------- /tools/imageeditor/Readme.md: -------------------------------------------------------------------------------- 1 | # 图片查看工具 2 | 这是一款能够对图像进行分析以及简单的处理工具 3 | ## 界面 4 | 1. 右侧有四个按钮,分为为:打开图像,保存图像,图像处理前后对比,图像统计分析 5 | 2. 菜单栏有各种图像处理的模块,目前仅仅做了滤波的相关处理 6 | ## 使用方法 7 | 1. 可以通过点击打开图像的按钮或者拖动图片到图像显示框里进行显示 8 | 2. 通过菜单栏可以对图像进行简单的处理 9 | 3. 通过保存按钮可以保存当前的图像(可是是图像处理前后的) 10 | 4. 通过对比按钮可以对比图像处理前后的变化 11 | 5. 通过点击图像统计分析按钮可以获取到整幅图像的统计信息,包括直方图,RGB均值与信噪比,转成YUV之后的均值和信噪比,窗口大小,以及RGB增益 12 | 6. 统计信息窗口不关闭的情况下,框选图像中的任意一块区域,都可以显示这块区域的统计信息 13 | 7. 统计信息窗口中的直方图,显示了RGB和Y通道的直方图,可以通过复选框选择是否显示该通道的直方图 -------------------------------------------------------------------------------- /tools/imageeditor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/imageeditor/__init__.py -------------------------------------------------------------------------------- /tools/imageeditor/imageeffect.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from components.customwidget import critical_win 4 | 5 | 6 | class WaterMarkType(): 7 | """ 8 | 水印的类型 9 | """ 10 | NoWaterMark = -1 11 | TransparentWaterMark = 0 12 | SpaceWaterMark = 1 13 | FrequencyWaterMark = 2 14 | 15 | 16 | class WaterMarkParams(): 17 | """ 18 | 水印的参数 19 | transparent:透明度 范围:0-100 20 | watermark_type:水印的类型 范围:WaterMarkType 21 | threshold:水印二值化的阈值 范围:0-255 22 | size:水印的缩放大小 范围:0-1000 23 | """ 24 | transparent = 0 25 | watermark_type = WaterMarkType.NoWaterMark 26 | threshold = 50 27 | size = 100 28 | filename = "" 29 | 30 | 31 | class ImageEffect(object): 32 | nowImage = None 33 | img_index = 0 34 | 35 | def __init__(self): 36 | self.is_load_image = False 37 | 38 | def load_image(self, filename): 39 | # 防止有中文 40 | # self.srcImage = cv2.imread(filename) 41 | self.srcImage = cv2.imdecode(np.fromfile(filename, dtype=np.uint8), 1) 42 | 43 | # 根据不同的颜色通道,进行不同的颜色转换 44 | # if(self.depth == 3): 45 | # self.srcImage = cv2.cvtColor(self.srcImage, cv2.COLOR_BGR2RGB) 46 | # elif(self.depth == 4): 47 | # self.srcImage = cv2.cvtColor(self.srcImage, cv2.COLOR_BGR2BGRA) 48 | # else: 49 | # self.srcImage == None 50 | self.depth = 0 51 | if (self.srcImage is not None): 52 | self.height = self.srcImage.shape[0] 53 | self.width = self.srcImage.shape[1] 54 | self.depth = self.srcImage.shape[2] 55 | self.dstImage = self.srcImage.copy() 56 | self.nowImage = self.srcImage 57 | self.is_load_image = True 58 | 59 | def save_image(self, img, filename): 60 | # cv2.imwrite(filename, self.nowImage) 61 | # 解决中文路径的问题 62 | cv2.imencode('.jpg', self.nowImage)[1].tofile(filename) 63 | 64 | def imageconvert(self, index): 65 | """ 66 | func: 切换需要操作的img, 0 为原图,1为目标图 67 | """ 68 | if (index == 0): 69 | self.nowImage = self.srcImage 70 | self.img_index = 0 71 | elif (index == 1): 72 | self.nowImage = self.dstImage 73 | self.img_index = 1 74 | 75 | def get_img_index(self): 76 | return self.img_index 77 | 78 | def get_img_point(self, x, y): 79 | """ 80 | 获取图像中一个点的RGB值,注意颜色顺序是BGR 81 | """ 82 | if(x > 0 and x < self.width and y > 0 and y < self.height): 83 | return self.nowImage[y, x] 84 | else: 85 | return None 86 | 87 | def get_src_image(self): 88 | return self.srcImage 89 | 90 | def get_dst_image(self): 91 | return self.dstImage 92 | 93 | def blur(self, type): 94 | if (type == BlurType.BoxBlur): 95 | self.dstImage = cv2.boxFilter(self.srcImage, -1, BlurType.BlurSize) 96 | elif (type == BlurType.GaussianBlur): 97 | self.dstImage = cv2.GaussianBlur( 98 | self.srcImage, BlurType.BlurSize, 0, 0) 99 | elif (type == BlurType.MediaBlur): 100 | self.dstImage = cv2.medianBlur(self.srcImage, BlurType.medianSize) 101 | elif (type == BlurType.BilateralBlur): 102 | self.dstImage = cv2.bilateralFilter(self.srcImage, BlurType.BilateralSize, 103 | BlurType.BilateralSize * 2, BlurType.BilateralSize / 2) 104 | 105 | def set_watermark_img(self, filename): 106 | self.watermark_img = cv2.imdecode( 107 | np.fromfile(filename, dtype=np.uint8), -1) 108 | if (self.watermark_img is not None): 109 | self.watermark_img_height = self.watermark_img.shape[0] 110 | self.watermark_img_width = self.watermark_img.shape[1] 111 | else: 112 | critical_win('水印图片无法打开') 113 | 114 | def set_watermark_show(self, params: WaterMarkParams): 115 | """ 116 | func: 设置水印的显示 117 | 先进行水印的缩放,如果是空域水印,就进行转换成灰度图二值化,如果是半透明水印,就进行透明化处理,最后和原图进行融合 118 | """ 119 | if(params.watermark_type != WaterMarkType.NoWaterMark): 120 | watermark_img_tmp = self.watermark_img.copy() 121 | self.dstImage = self.srcImage.copy() 122 | if(params.size <= 1000 and params.size > 0): 123 | watermark_img_tmp = cv2.resize(watermark_img_tmp, (int( 124 | self.watermark_img_width*params.size/100), int(self.watermark_img_height*params.size/100)), interpolation=cv2.INTER_AREA) 125 | if(params.watermark_type == WaterMarkType.SpaceWaterMark and params.threshold <= 255 and params.threshold >= 0): 126 | watermark_img_tmp = cv2.cvtColor( 127 | watermark_img_tmp, cv2.COLOR_BGR2GRAY) 128 | watermark_img_tmp = cv2.threshold( 129 | watermark_img_tmp, params.threshold, 255, cv2.THRESH_BINARY)[1] 130 | watermark_img_tmp = cv2.cvtColor( 131 | watermark_img_tmp, cv2.COLOR_GRAY2BGR) 132 | width = min(watermark_img_tmp.shape[1], self.width) 133 | height = min(watermark_img_tmp.shape[0], self.height) 134 | w_start = int((self.width - width)/2) 135 | h_start = int((self.height - height)/2) 136 | clip_img = self.dstImage[h_start:height + 137 | h_start, w_start:width+w_start] 138 | if(params.watermark_type == WaterMarkType.TransparentWaterMark and params.transparent <= 100 and params.transparent >= 0): 139 | self.dstImage[h_start:height+h_start, w_start:width+w_start] = cv2.addWeighted( 140 | watermark_img_tmp[:height, :width], 1-params.transparent/100, clip_img, params.transparent/100, 0) 141 | else: 142 | self.dstImage[h_start:height+h_start, w_start:width + 143 | w_start] = watermark_img_tmp[:height, :width] 144 | 145 | def generate_watermark(self, params: WaterMarkParams): 146 | if(params.watermark_type == WaterMarkType.SpaceWaterMark): 147 | watermark_img_tmp = self.watermark_img.copy() 148 | self.dstImage = self.srcImage.copy() 149 | watermark_img_tmp = cv2.cvtColor( 150 | watermark_img_tmp, cv2.COLOR_BGR2GRAY) 151 | watermark_img_tmp = cv2.threshold( 152 | watermark_img_tmp, params.threshold, 1, cv2.THRESH_BINARY)[1] 153 | width = min(watermark_img_tmp.shape[1], self.width) 154 | height = min(watermark_img_tmp.shape[0], self.height) 155 | w_start = int((self.width - width)/2) 156 | h_start = int((self.height - height)/2) 157 | self.dstImage[h_start:height+h_start, 158 | w_start:width+w_start, 0] &= 254 159 | self.dstImage[h_start:height+h_start, w_start:width + 160 | w_start, 0] |= watermark_img_tmp[:height, :width] 161 | 162 | def analysis_space_watermark(self): 163 | tmp = cv2.threshold( 164 | self.nowImage[:, :, 0], 0, 255, cv2.THRESH_BINARY)[1] 165 | self.dstImage = cv2.cvtColor(tmp, cv2.COLOR_GRAY2BGR) 166 | 167 | 168 | class BlurType(): 169 | BoxBlur = 0 170 | GaussianBlur = 1 171 | MediaBlur = 2 172 | BilateralBlur = 3 173 | BlurSize = (5, 5) 174 | medianSize = 5 175 | BilateralSize = 25 176 | -------------------------------------------------------------------------------- /tools/imageeditor/ui/imageeditor_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ImageEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | 图片处理工具 15 | 16 | 17 | 18 | :/tool_icon/resource/main.png:/tool_icon/resource/main.png 19 | 20 | 21 | 22 | 23 | 24 | -1 25 | 26 | 27 | true 28 | 29 | 30 | QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging|QMainWindow::VerticalTabs 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 800 54 | 22 55 | 56 | 57 | 58 | 59 | 图像处理 60 | 61 | 62 | 63 | 滤波 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 工具 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | true 84 | 85 | 86 | 87 | 88 | 89 | 0 90 | 0 91 | 92 | 93 | 94 | toolBar_2 95 | 96 | 97 | Qt::AllToolBarAreas 98 | 99 | 100 | Qt::Vertical 101 | 102 | 103 | RightToolBarArea 104 | 105 | 106 | false 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 直方图 119 | 120 | 121 | 122 | 123 | true 124 | 125 | 126 | 统计信息 127 | 128 | 129 | 130 | 131 | 方框滤波 132 | 133 | 134 | 135 | 136 | 高斯滤波 137 | 138 | 139 | 140 | 141 | 142 | :/tool_icon/resource/save_icon.png:/tool_icon/resource/save_icon.png 143 | 144 | 145 | save 146 | 147 | 148 | Ctrl+S 149 | 150 | 151 | 152 | 153 | 中值滤波 154 | 155 | 156 | Qt::WindowShortcut 157 | 158 | 159 | 160 | 161 | 双边滤波 162 | 163 | 164 | 165 | 166 | 167 | :/tool_icon/resource/compare.png:/tool_icon/resource/compare.png 168 | 169 | 170 | compare 171 | 172 | 173 | compare 174 | 175 | 176 | 177 | 178 | 179 | :/tool_icon/resource/open.png:/tool_icon/resource/open.png 180 | 181 | 182 | open 183 | 184 | 185 | open 186 | 187 | 188 | 189 | 190 | 191 | :/tool_icon/resource/stats.png:/tool_icon/resource/stats.png 192 | 193 | 194 | stats 195 | 196 | 197 | 统计信息 198 | 199 | 200 | 201 | 202 | 制作水印 203 | 204 | 205 | 206 | 207 | 解析水印 208 | 209 | 210 | 211 | 212 | 水印制作 213 | 214 | 215 | 216 | 217 | 水印制作 218 | 219 | 220 | 221 | 222 | 223 | :/tool_icon/resource/down.png:/tool_icon/resource/down.png 224 | 225 | 226 | 下一个图片 227 | 228 | 229 | Down 230 | 231 | 232 | 233 | 234 | 235 | :/tool_icon/resource/up.png:/tool_icon/resource/up.png 236 | 237 | 238 | 上一张图片 239 | 240 | 241 | Up 242 | 243 | 244 | 245 | 246 | 247 | :/tool_icon/resource/delete.png:/tool_icon/resource/delete.png 248 | 249 | 250 | 删除 251 | 252 | 253 | 删除 254 | 255 | 256 | Del 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /tools/imageeditor/ui/watermarkview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'watermarkview.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | 16 | class Ui_WaterMarkView(object): 17 | def setupUi(self, WaterMarkView): 18 | if not WaterMarkView.objectName(): 19 | WaterMarkView.setObjectName(u"WaterMarkView") 20 | WaterMarkView.resize(252, 327) 21 | sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 22 | sizePolicy.setHorizontalStretch(0) 23 | sizePolicy.setVerticalStretch(0) 24 | sizePolicy.setHeightForWidth(WaterMarkView.sizePolicy().hasHeightForWidth()) 25 | WaterMarkView.setSizePolicy(sizePolicy) 26 | WaterMarkView.setSizeGripEnabled(True) 27 | self.gridLayout = QGridLayout(WaterMarkView) 28 | self.gridLayout.setObjectName(u"gridLayout") 29 | self.groupBox = QGroupBox(WaterMarkView) 30 | self.groupBox.setObjectName(u"groupBox") 31 | self.groupBox.setMaximumSize(QSize(16777215, 16777215)) 32 | self.gridLayout_9 = QGridLayout(self.groupBox) 33 | self.gridLayout_9.setObjectName(u"gridLayout_9") 34 | self.analysis = QPushButton(self.groupBox) 35 | self.analysis.setObjectName(u"analysis") 36 | 37 | self.gridLayout_9.addWidget(self.analysis, 2, 1, 1, 1) 38 | 39 | self.generate = QPushButton(self.groupBox) 40 | self.generate.setObjectName(u"generate") 41 | 42 | self.gridLayout_9.addWidget(self.generate, 1, 1, 1, 1) 43 | 44 | self.gridLayout_8 = QGridLayout() 45 | self.gridLayout_8.setObjectName(u"gridLayout_8") 46 | self.label = QLabel(self.groupBox) 47 | self.label.setObjectName(u"label") 48 | 49 | self.gridLayout_8.addWidget(self.label, 1, 0, 1, 1) 50 | 51 | self.change_transparent = QSlider(self.groupBox) 52 | self.change_transparent.setObjectName(u"change_transparent") 53 | self.change_transparent.setMaximum(100) 54 | self.change_transparent.setOrientation(Qt.Horizontal) 55 | 56 | self.gridLayout_8.addWidget(self.change_transparent, 4, 1, 1, 1) 57 | 58 | self.label_2 = QLabel(self.groupBox) 59 | self.label_2.setObjectName(u"label_2") 60 | 61 | self.gridLayout_8.addWidget(self.label_2, 4, 0, 1, 1) 62 | 63 | self.label_19 = QLabel(self.groupBox) 64 | self.label_19.setObjectName(u"label_19") 65 | 66 | self.gridLayout_8.addWidget(self.label_19, 3, 0, 1, 1) 67 | 68 | self.change_watermark_size = QSlider(self.groupBox) 69 | self.change_watermark_size.setObjectName(u"change_watermark_size") 70 | self.change_watermark_size.setMinimum(1) 71 | self.change_watermark_size.setMaximum(500) 72 | self.change_watermark_size.setValue(100) 73 | self.change_watermark_size.setOrientation(Qt.Horizontal) 74 | 75 | self.gridLayout_8.addWidget(self.change_watermark_size, 3, 1, 1, 1) 76 | 77 | self.change_watermark_th = QSlider(self.groupBox) 78 | self.change_watermark_th.setObjectName(u"change_watermark_th") 79 | self.change_watermark_th.setMaximum(255) 80 | self.change_watermark_th.setValue(127) 81 | self.change_watermark_th.setOrientation(Qt.Horizontal) 82 | 83 | self.gridLayout_8.addWidget(self.change_watermark_th, 2, 1, 1, 1) 84 | 85 | self.change_watermark_type = QComboBox(self.groupBox) 86 | self.change_watermark_type.addItem("") 87 | self.change_watermark_type.addItem("") 88 | self.change_watermark_type.addItem("") 89 | self.change_watermark_type.setObjectName(u"change_watermark_type") 90 | 91 | self.gridLayout_8.addWidget(self.change_watermark_type, 1, 1, 1, 1) 92 | 93 | self.label_18 = QLabel(self.groupBox) 94 | self.label_18.setObjectName(u"label_18") 95 | 96 | self.gridLayout_8.addWidget(self.label_18, 2, 0, 1, 1) 97 | 98 | self.open_watermark = QPushButton(self.groupBox) 99 | self.open_watermark.setObjectName(u"open_watermark") 100 | 101 | self.gridLayout_8.addWidget(self.open_watermark, 0, 0, 1, 1) 102 | 103 | self.watermark_path = QLineEdit(self.groupBox) 104 | self.watermark_path.setObjectName(u"watermark_path") 105 | 106 | self.gridLayout_8.addWidget(self.watermark_path, 0, 1, 1, 1) 107 | 108 | 109 | self.gridLayout_9.addLayout(self.gridLayout_8, 0, 1, 1, 1) 110 | 111 | 112 | self.gridLayout.addWidget(self.groupBox, 0, 1, 1, 1) 113 | 114 | 115 | self.retranslateUi(WaterMarkView) 116 | 117 | QMetaObject.connectSlotsByName(WaterMarkView) 118 | # setupUi 119 | 120 | def retranslateUi(self, WaterMarkView): 121 | WaterMarkView.setWindowTitle(QCoreApplication.translate("WaterMarkView", u"\u6c34\u5370\u5236\u4f5c\u5de5\u5177", None)) 122 | self.groupBox.setTitle(QCoreApplication.translate("WaterMarkView", u"\u5236\u4f5c\u6c34\u5370", None)) 123 | self.analysis.setText(QCoreApplication.translate("WaterMarkView", u"\u89e3\u6790\u7a7a\u57df\u9690\u5f62\u6c34\u5370", None)) 124 | self.generate.setText(QCoreApplication.translate("WaterMarkView", u"\u5236\u4f5c\u5e26\u6c34\u5370\u7684\u56fe\u7247", None)) 125 | self.label.setText(QCoreApplication.translate("WaterMarkView", u"\u6c34\u5370\u7c7b\u578b", None)) 126 | self.label_2.setText(QCoreApplication.translate("WaterMarkView", u"\u900f\u660e\u7a0b\u5ea6", None)) 127 | self.label_19.setText(QCoreApplication.translate("WaterMarkView", u"\u8c03\u6574\u6c34\u5370\u5927\u5c0f", None)) 128 | self.change_watermark_type.setItemText(0, QCoreApplication.translate("WaterMarkView", u"\u534a\u900f\u660e\u6c34\u5370", None)) 129 | self.change_watermark_type.setItemText(1, QCoreApplication.translate("WaterMarkView", u"\u7a7a\u57df\u9690\u5f62\u6c34\u5370", None)) 130 | self.change_watermark_type.setItemText(2, QCoreApplication.translate("WaterMarkView", u"\u9891\u57df\u9690\u5f62\u6c34\u5370", None)) 131 | 132 | self.label_18.setText(QCoreApplication.translate("WaterMarkView", u"\u4e8c\u503c\u5316", None)) 133 | self.open_watermark.setText(QCoreApplication.translate("WaterMarkView", u"\u5bfc\u5165\u6c34\u5370", None)) 134 | # retranslateUi 135 | 136 | -------------------------------------------------------------------------------- /tools/imageeditor/ui/watermarkview.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | WaterMarkView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 252 10 | 327 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 水印制作工具 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 16777215 31 | 16777215 32 | 33 | 34 | 35 | 制作水印 36 | 37 | 38 | 39 | 40 | 41 | 解析空域隐形水印 42 | 43 | 44 | 45 | 46 | 47 | 48 | 制作带水印的图片 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 水印类型 58 | 59 | 60 | 61 | 62 | 63 | 64 | 100 65 | 66 | 67 | Qt::Horizontal 68 | 69 | 70 | 71 | 72 | 73 | 74 | 透明程度 75 | 76 | 77 | 78 | 79 | 80 | 81 | 调整水印大小 82 | 83 | 84 | 85 | 86 | 87 | 88 | 1 89 | 90 | 91 | 500 92 | 93 | 94 | 100 95 | 96 | 97 | Qt::Horizontal 98 | 99 | 100 | 101 | 102 | 103 | 104 | 255 105 | 106 | 107 | 127 108 | 109 | 110 | Qt::Horizontal 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 半透明水印 119 | 120 | 121 | 122 | 123 | 空域隐形水印 124 | 125 | 126 | 127 | 128 | 频域隐形水印 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 二值化 137 | 138 | 139 | 140 | 141 | 142 | 143 | 导入水印 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /tools/rawimageeditor/Readme.md: -------------------------------------------------------------------------------- 1 | # RAW图片编辑工具 2 | 这是一款能够对RAW图进行解析和ISP处理的工具。可以方便的进行后期算法的验证(热更新、直观的图像显示和分析)。退出后会自动保存上次打开的窗口,以及窗口里的参数。同时将界面与处理线程分离,让界面更加流畅。 3 | 4 | ### 界面 5 | 6 | ![预览效果图](https://image.qinxing.xyz/20210506214838.png) 7 | 8 | 1. 左侧是图像预览窗口 9 | 2. 右上角有两个窗口,左边的是RAW图的设置,右边的是ISP处理流程 10 | 3. 右下角是对ISP处理的一些配置 11 | 12 | ### 使用方法 13 | 14 | 1. **导入raw图**:先进行RAW图的设置,然后可以点击“打开图片”或者拖拽的方式打开图片,此时图片预览窗口显示的是RAW图,可以用鼠标进行放大缩小和移动,窗口的左下角会显示每个点的值,以及缩放比例 15 | 2. **ISP pipeline设置**:可以通过`勾选`的方式去启用部分ISP流程,通过`拖拽`的方式去调整ISP的顺序,然后点击确定按钮,可以进行ISP的处理,右下角的进度条可以显示ISP处理的进度。 16 | 3. **ISP 参数设置**:右下角的ISP参数设置窗口,修改参数后,也需要点击确定按钮,运行ISP。第二步和第三步ISP的处理,会对比之前的ISP流程,自动搜索最少的处理流程。右下角有处理的进度条以及耗时显示。 17 | 4. **过程中的图片查看**:`双击`ISP处理流程中的模块,可以看到经过这个模块的处理,图片变成了什么效果。此功能可以方便的看到每个ISP模块的效果。 18 | 5. **图片分析**:图片查看时点击`图片分析`按钮,会立刻显示全局的直方图统计,图片大小,平均值等信息。此时鼠标变成了选框模式,只要选中图片中的某一区域,就会显示这个区域的直方图信息,信噪比,平均值,RGB比值等信息,直方图可以挑选YRGB中的任意通道进行显示。 19 | 6. **算法调试**:如果需要进行算法的调试,修改相关ISP算法代码之后,点击`算法热更新`,可以重新加载ISP相关算法(在isp.py中),不需要重新启动程序。如果报错,弹出的窗口会显示错误信息。 20 | 21 | ### 参数设置 22 | 23 | 1. 图片格式:RAW图的宽,高,bit位数,格式和pattern,目前仅支持海思的raw图格式 24 | 2. ISP处理流程:ISP处理分为三个区域,绿色的是raw域的处理,黄色的是RGB域的处理,蓝色的是yuv域的处理。可以通过勾选的方式去启用部分ISP流程,通过拖拽的方式去调整ISP的顺序,但是调整不要超过自己的区域,如raw域的处理不能放在yuv域进行处理。 25 | 3. 黑电平:每个通道的黑电平 26 | 4. 坏点检测:调整检测的区域 27 | 5. rolloff:暗影矫正 28 | 导入拍摄均匀光照的raw图,格式要一样 29 | 6. demosaic:去马赛克 30 | 有三种模式:双线性插值、malver、menon实现 31 | 7. awb: 白平衡 32 | 填入RGB的增益,也可以点击`从raw图选取`的按钮,然后用鼠标在图中选中一块灰色的区域即可,注意用来选取白平衡的图,需要是在黑电平处理过后的raw图 33 | 8. ccm: 3x3色彩校正矩阵 34 | 9. gamma:gamma值 35 | 10. LTM:局部对比度增强 36 | 1. 暗区提升:提升暗区亮度 37 | 2. 亮度抑制:抑制亮度亮度 38 | 11. CSC:色度空间转换RGB->YUV 39 | 1. 是否限制YUV输出范围:TV标准中亮度的范围是16-235,PC标准是0-255,如果选中,就是采用TV标准,对比度会低一些 40 | 2. 色域标准:BT709,BT2020, BT601三种可选 41 | 3. 亮度、对比度、色调、饱和度调整 42 | 12. yuv denoise: 在三种频率上分别进行小波降噪 43 | 1. 降噪强度:双边滤波的强度,值越大,降噪强度越大 44 | 2. 噪声阈值:值越大,选取的噪声范围越大 45 | 3. 降噪权重:值越大,降噪越强,值为0的时候,就不进行降噪 46 | 4. 色度降噪强度:值越大,颜色降噪越强 47 | 13. yuv sharpen: 基于方向检测的锐化 48 | 1. 3x3中值滤波强度:在锐化强进行一次中值滤波,值越大,滤波越强,值为0,就是没有进行中值滤波 49 | 2. 锐化强度:值越大,锐化强度越大 50 | 3. 锐化钳位阈值:控制锐化上限,避免出现白边,值越小,锐化上限越低 51 | 4. 降噪阈值:小于这个范围的细节进行降噪,大于这个范围的细节进行锐化,值越大,降噪的范围越多 52 | 53 | ### 目前进展 54 | 55 | 目前实现了黑电平,坏点矫正,暗影矫正,去马赛克,白平衡,色彩校正,gamma,局部对比度增强,色度空间转换,对比度亮度调整,小波降噪WNR,锐化等算法 56 | 57 | ## 软件架构 58 | 59 | 60 | 61 | 62 | 63 | ``` mermaid 64 | classDiagram 65 | RawImageEditor --|> SubWindow 66 | RawImageEditor *--> RawImageParams 67 | RawImageEditor *--> ISPpipeline 68 | ISPpipeline *--> RawImageInfo 69 | ISPpipeline o--> RawImageParams 70 | ISPpipeline *--> ISPProc 71 | ISPProc ..> ispfunction 72 | RawImageParams o--> RawImageParams : instance 73 | 74 | SubWindow: imagetools子窗口 75 | SubWindow: 实现参数缓存功能 76 | ISPpipeline: ISP处理流程 77 | RawImageEditor: 主窗口 78 | RawImageParams: ISP相关参数设置 79 | RawImageInfo: 图片属性 80 | RawImageInfo: 图片显示等操作方法 81 | RawImageInfo: 包括RAW/RGB/YUV 82 | ISPProc: ISP后台处理线程 83 | ISPProc: 自动搜索最短过程 84 | ispfunction: isp算法实现 85 | ``` 86 | 87 | 主窗口继承的是Subwindow,实现了参数缓存的功能,里面创建了参数类RawImageParams和ISP处理流程类ISPpipeline。 88 | 89 | 参数类RawImageParams主要是界面上ISP相关参数的设置,组合了RAW图的格式,黑电平,去马赛克等过程的参数配置。此参数配置会缓存到本地,以便下次使用。 90 | 91 | ISP处理流程类ISPpipeline主要是进行isp算法的处理。他会使用界面配置的参数RawImageParams,调用ISP后台处理线程进行处理。期间产生的图像,用RawImageInfo来存储。ISP后台处理线程依赖ispfunction中的算法实现,因此如果想要拓展算法,只需要修改isp.py中的实现即可。 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /tools/rawimageeditor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/rawimageeditor/__init__.py -------------------------------------------------------------------------------- /tools/rawimageeditor/ispfunction.py: -------------------------------------------------------------------------------- 1 | import tools.rawimageeditor.isp as isp 2 | import tools.rawimageeditor.debayer as debayer 3 | 4 | # pipeline名称全部小写 5 | pipeline_dict = { 6 | "original raw": isp.get_src_raw_data, 7 | "black level": isp.black_level_correction, 8 | "digital gain": isp.apply_digital_gain, 9 | "blc": isp.black_level_correction, 10 | "rolloff": isp.rolloff_correction, 11 | "bad pixel correction": isp.bad_pixel_correction, 12 | "bayer denoise": None, 13 | "demosaic": debayer.demosaic, 14 | "awb": isp.channel_gain_white_balance, 15 | "ccm": isp.color_correction, 16 | "gamma": isp.gamma_correction, 17 | "ltm": isp.ltm_correction, 18 | "csc": isp.color_space_conversion, 19 | "yuv denoise": isp.wavelet_denoise, 20 | "yuv sharpen": isp.sharpen 21 | } -------------------------------------------------------------------------------- /tools/rawimageeditor/isppipeline.py: -------------------------------------------------------------------------------- 1 | from tools.rawimageeditor.RawImageParams import RawImageParams 2 | from tools.rawimageeditor.RawImageInfo import RawImageInfo 3 | import tools.rawimageeditor.ispfunction as ispfunc 4 | from imp import reload 5 | import time 6 | from PySide2.QtCore import Signal, QThread 7 | from PySide2.QtWidgets import QMessageBox 8 | from threading import Lock 9 | 10 | class IspPipeline(): 11 | def __init__(self, parmas, process_bar=None): 12 | self.old_pipeline = [] 13 | self.pipeline = [] 14 | self.params = parmas 15 | # img_list存储了pipeline中途所有的图像 16 | # img_list长度比pipeline长1 17 | self.img_list = [RawImageInfo()] 18 | self.process_bar = process_bar 19 | self.imglist_mutex = Lock() 20 | self.ispProcthread = ISPProc(self.params, self.img_list, self.imglist_mutex) 21 | 22 | def reload_isp(self): 23 | """ 24 | func: 热更新 重载ISP算法模块 25 | """ 26 | reload(ispfunc.debayer) 27 | reload(ispfunc.isp) 28 | reload(ispfunc) 29 | self.params.need_flush = True 30 | if (self.process_bar is not None): 31 | self.process_bar.setValue(0) 32 | 33 | def set_pipeline(self, pipeline): 34 | self.old_pipeline = self.pipeline 35 | self.pipeline = pipeline 36 | 37 | def pipeline_clear(self): 38 | self.old_pipeline = self.pipeline 39 | self.pipeline = [] 40 | 41 | def pipeline_reset(self): 42 | """ 43 | func: 重新开始一个pipeline,把以前的图像清除 44 | """ 45 | if(len(self.img_list) > 1): 46 | self.imglist_mutex.acquire() 47 | self.img_list = [RawImageInfo()] 48 | self.imglist_mutex.release() 49 | self.old_pipeline = [] 50 | self.pipeline = [] 51 | return True 52 | return False 53 | 54 | def add_pipeline_node(self, node): 55 | """ 56 | func: 为pipeline添加一个节点 57 | 输入是pipeline_dict的字符串 58 | """ 59 | if(node.lower() in ispfunc.pipeline_dict): 60 | self.pipeline.append(node.lower()) 61 | 62 | def get_pipeline_node_index(self, node): 63 | """ 64 | func: 返回该node在pipeline的index, 如果不存在,就返回-1 65 | """ 66 | if(node.lower() in ispfunc.pipeline_dict and node.lower() in self.pipeline): 67 | return self.pipeline.index(node.lower()) 68 | else: 69 | return -1 70 | 71 | def compare_pipeline(self): 72 | """ 73 | func: 对比新老pipeline的区别 74 | 如果不同的话,会返回一个index,表示从第index个值开始不一样的,注意这个index可能不存在于老的pipeline中 75 | 如果相同的话,会返回0 76 | """ 77 | for i, node in enumerate(self.pipeline): 78 | if(i > len(self.old_pipeline) - 1 or node != self.old_pipeline[i]): 79 | return i 80 | return -1 81 | 82 | def check_pipeline(self): 83 | """ 84 | func: 检查pipeline,如果有不同的,修改img_list 85 | ret: 如果pipeline不需要修改,就返回None,如果需要修改,就返回需要修改的pipeline 86 | """ 87 | # 如果参数有修改,优先返回需要修改的pipeline 88 | if(self.params.need_flush == True): 89 | if(len(self.params.need_flush_isp) > 0): 90 | index = -1 91 | self.params.need_flush = False 92 | for node in self.params.need_flush_isp: 93 | index = self.get_pipeline_node_index(node) 94 | if(index != -1): 95 | self.remove_img_node_tail(index) 96 | # 需要把新老pipeline进行对比 97 | index_ret = self.compare_pipeline() 98 | if(index_ret != -1): 99 | min_index = min(index_ret, index) 100 | self.remove_img_node_tail(min_index) 101 | return self.pipeline[min_index:] 102 | else: 103 | return self.pipeline[index:] 104 | else: 105 | return None 106 | else: 107 | self.remove_img_node_tail(0) 108 | self.params.need_flush = False 109 | return self.pipeline 110 | else: 111 | index = self.compare_pipeline() 112 | if(index != -1): 113 | self.remove_img_node_tail(index) 114 | return self.pipeline[index:] 115 | return None 116 | 117 | def run_pipeline(self): 118 | """ 119 | func: 运行pipeline,process_bar是用于显示进度的process bar, callback是运行完的回调函数 120 | """ 121 | pipeline = self.check_pipeline() 122 | print(pipeline) 123 | self.ispProcthread.set_pipeline(pipeline) 124 | self.ispProcthread.start() 125 | 126 | def remove_img_node_tail(self, index): 127 | """ 128 | func: 去除>=index之后的node,由于image的长度比pipeline多1,因此需要将index+1 129 | """ 130 | index += 1 131 | self.imglist_mutex.acquire() 132 | while index < len(self.img_list): 133 | self.img_list.pop() 134 | self.imglist_mutex.release() 135 | 136 | def get_image(self, index): 137 | """ 138 | func: 获取pipeline中的一幅图像 139 | 如果输入-1,则返回最后一幅图像 140 | """ 141 | ret_img = None 142 | self.imglist_mutex.acquire() 143 | if (index < len(self.img_list) and index >= 0): 144 | ret_img = self.img_list[index] 145 | elif (index < 0 and len(self.pipeline)+1 + index < len(self.img_list)): 146 | ret_img = self.img_list[len(self.pipeline)+1 + index] 147 | self.imglist_mutex.release() 148 | if(ret_img is not None): 149 | return ret_img 150 | else: 151 | return RawImageInfo() 152 | 153 | class ISPProc(QThread): 154 | doneCB = Signal() # 自定义信号,其中 object 为信号承载数据的类型 155 | processRateCB = Signal(int) 156 | costTimeCB = Signal(str) 157 | errorCB = Signal(str) 158 | 159 | def __init__(self, params, img_list, mutex:Lock, parent=None): 160 | super(ISPProc, self).__init__(parent) 161 | self.params = params 162 | self.img_list = img_list 163 | self.pipeline = None 164 | self.mutex = mutex 165 | 166 | def run_node(self, node, data): 167 | # 这里进行检查之后,后续就不需要检查了 168 | if(data is not None and self.params is not None): 169 | return ispfunc.pipeline_dict[node](data, self.params) 170 | elif(self.params is None): 171 | self.params.set_error_str("输入的参数为空") 172 | return None 173 | 174 | def set_pipeline(self, pipeline): 175 | self.pipeline = pipeline 176 | 177 | def run(self): 178 | self.processRateCB.emit(0) 179 | if (self.pipeline is not None): 180 | length = len(self.pipeline) 181 | i = 1 182 | params = self.params 183 | start_time = time.time() 184 | for node in self.pipeline: 185 | data = self.img_list[-1] 186 | try: 187 | ret_img = self.run_node(node, data) 188 | except Exception as e: 189 | self.errorCB.emit("ISP算法[{}]运行错误:{}\r\n{}".format(node, params.get_error_str(),e)) 190 | return 191 | 192 | if(ret_img is not None): 193 | self.mutex.acquire() 194 | self.img_list.append(ret_img) 195 | self.mutex.release() 196 | else: 197 | self.errorCB.emit(params.get_error_str()) 198 | break 199 | self.processRateCB.emit(i / length * 100) 200 | i += 1 201 | stop_time = time.time() 202 | self.costTimeCB.emit('总耗时:{:.3f}s'.format(stop_time-start_time)) 203 | self.doneCB.emit() 204 | else: 205 | self.processRateCB.emit(100) -------------------------------------------------------------------------------- /tools/rawimageeditor/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/rawimageeditor/ui/__init__.py -------------------------------------------------------------------------------- /tools/shake_test/Readme.md: -------------------------------------------------------------------------------- 1 | # 防抖检测工具 2 | ## 使用方法 3 | 1. 拍摄视频,视频的要求:尽量用棋盘格等边缘比较明显的物体,图像中心尽量有明显的特征点 4 | 2. 导入振动台上拍摄的视频,可以通过打开文件或者拖动文件到视频预览窗口进行导入 5 | 3. 点击右下角的开始,会开始对图像进行处理,并会更新实时的结果。点击停止,会停止图像的处理 6 | 4. 视频预览会显示每个特征点的运动轨迹,中心的特征点会着重标注出来。确保每个点的轨迹都没有问题。在最终结果那一栏会得出这段时间内的最终结果 7 | 8 | ## 高级操作 9 | 1. ROI上下边界:由于有些视频存在OSD,会干扰到特征点的选取,可以调整上下边界控制特征点的选取范围 10 | 2. 跳过帧数:因为有的时候前几帧有干扰的物体,或者效果不佳,可以跳过前几帧,再进行特征点选取。 11 | 3. 特征点的数量:如果画面中心没有特征点,那么可以调整此滑块。如果特征点过多,也可以调整此滑块 12 | 4. 特征点的运动幅度:如果运动幅度过大,出现跟踪错误的话,就增大特征点的运动幅度 13 | 5. 计算间隔(帧):每多少帧会输出一个实时的结果,确保每次的实时结果都比较正常,再去看最终结果 14 | 6. 计算方向:跟最后结果挂钩,只显示某个方向上的结果 15 | 7. 去除静止的特征点:可以排除roi等静止的点,优先选用ROI的方式 16 | 17 | ## 显示情况 18 | 1. 视频预览中会把所有的特征点着重标出来,中心参与计算的特征点更加明显 19 | 20 | 21 | ## 评价标准 22 | 23 | 我觉得应该有两个,一个是图片中心的位移,一个是图像的形变程度 24 | 如果图像不变形,每个特征点相对于图片中心都是固定的, 25 | 每个特征点的位移应该是和图片中心一致的 26 | 计算方法:第一帧算出每个特征点相对于图片中心的距离,后续每帧的特征点 27 | 都与第一帧的距离做一个差,所有差的平方和就作为图片的形变程度 28 | 29 | 每个点的运动角度 30 | 31 | 形变程度和每个点的运动方向和运动速度有关,而两帧之间的距离太小,计算容易出现误差,直接拿最左边和最右边的值做一个差。 32 | 每个点的运动可以看做一个向量,每个点的运动向量和中心点的运动向量差就是图像的变形程度 33 | 34 | ## 原理 35 | 先在第一张图像上找到比较好的特征点 36 | 后续的帧对这些特征点进行跟踪 37 | 38 | ## 遇到的问题 39 | 有些特征点跟踪的时候会产生漂移,甚至跟丢了的情况 40 | 1. 利用cornerSubPix计算出亚像素点,更加精准 41 | 2. 还可能是光流法搜索的区域不够大,需要增大winSize -------------------------------------------------------------------------------- /tools/shake_test/rtspconfigview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'rtspconfigview.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, 12 | QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) 13 | from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, 14 | QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, 15 | QPixmap, QRadialGradient) 16 | from PySide2.QtWidgets import * 17 | 18 | 19 | class Ui_RtspConfigView(object): 20 | def setupUi(self, RtspConfigView): 21 | if not RtspConfigView.objectName(): 22 | RtspConfigView.setObjectName(u"RtspConfigView") 23 | RtspConfigView.resize(174, 186) 24 | self.gridLayout = QGridLayout(RtspConfigView) 25 | self.gridLayout.setObjectName(u"gridLayout") 26 | self.buttonBox = QDialogButtonBox(RtspConfigView) 27 | self.buttonBox.setObjectName(u"buttonBox") 28 | self.buttonBox.setOrientation(Qt.Horizontal) 29 | self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) 30 | 31 | self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) 32 | 33 | self.gridLayout_2 = QGridLayout() 34 | self.gridLayout_2.setObjectName(u"gridLayout_2") 35 | self.username = QLineEdit(RtspConfigView) 36 | self.username.setObjectName(u"username") 37 | 38 | self.gridLayout_2.addWidget(self.username, 0, 1, 1, 1) 39 | 40 | self.label_2 = QLabel(RtspConfigView) 41 | self.label_2.setObjectName(u"label_2") 42 | 43 | self.gridLayout_2.addWidget(self.label_2, 1, 0, 1, 1) 44 | 45 | self.label_3 = QLabel(RtspConfigView) 46 | self.label_3.setObjectName(u"label_3") 47 | 48 | self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1) 49 | 50 | self.port = QLineEdit(RtspConfigView) 51 | self.port.setObjectName(u"port") 52 | 53 | self.gridLayout_2.addWidget(self.port, 3, 1, 1, 1) 54 | 55 | self.ip = QLineEdit(RtspConfigView) 56 | self.ip.setObjectName(u"ip") 57 | 58 | self.gridLayout_2.addWidget(self.ip, 2, 1, 1, 1) 59 | 60 | self.label_4 = QLabel(RtspConfigView) 61 | self.label_4.setObjectName(u"label_4") 62 | 63 | self.gridLayout_2.addWidget(self.label_4, 3, 0, 1, 1) 64 | 65 | self.password = QLineEdit(RtspConfigView) 66 | self.password.setObjectName(u"password") 67 | 68 | self.gridLayout_2.addWidget(self.password, 1, 1, 1, 1) 69 | 70 | self.label = QLabel(RtspConfigView) 71 | self.label.setObjectName(u"label") 72 | 73 | self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) 74 | 75 | self.isphoto = QCheckBox(RtspConfigView) 76 | self.isphoto.setObjectName(u"isphoto") 77 | self.isphoto.setChecked(True) 78 | 79 | self.gridLayout_2.addWidget(self.isphoto, 4, 1, 1, 1) 80 | 81 | 82 | self.gridLayout.addLayout(self.gridLayout_2, 0, 0, 1, 1) 83 | 84 | 85 | self.retranslateUi(RtspConfigView) 86 | self.buttonBox.accepted.connect(RtspConfigView.accept) 87 | self.buttonBox.rejected.connect(RtspConfigView.reject) 88 | 89 | QMetaObject.connectSlotsByName(RtspConfigView) 90 | # setupUi 91 | 92 | def retranslateUi(self, RtspConfigView): 93 | RtspConfigView.setWindowTitle(QCoreApplication.translate("RtspConfigView", u"\u6253\u5f00\u8bbe\u5907(RTSP)", None)) 94 | self.username.setText(QCoreApplication.translate("RtspConfigView", u"admin", None)) 95 | self.label_2.setText(QCoreApplication.translate("RtspConfigView", u"\u5bc6\u7801", None)) 96 | self.label_3.setText(QCoreApplication.translate("RtspConfigView", u"IP", None)) 97 | self.port.setText(QCoreApplication.translate("RtspConfigView", u"1554", None)) 98 | self.ip.setText(QCoreApplication.translate("RtspConfigView", u"127.0.0.1", None)) 99 | self.label_4.setText(QCoreApplication.translate("RtspConfigView", u"\u7aef\u53e3", None)) 100 | self.password.setText(QCoreApplication.translate("RtspConfigView", u"admin123", None)) 101 | self.label.setText(QCoreApplication.translate("RtspConfigView", u"\u7528\u6237\u540d", None)) 102 | self.isphoto.setText(QCoreApplication.translate("RtspConfigView", u"\u79fb\u52a8\u8bbe\u5907", None)) 103 | # retranslateUi 104 | 105 | -------------------------------------------------------------------------------- /tools/shake_test/rtspconfigview.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RtspConfigView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 174 10 | 186 11 | 12 | 13 | 14 | 打开设备(RTSP) 15 | 16 | 17 | 18 | 19 | 20 | Qt::Horizontal 21 | 22 | 23 | QDialogButtonBox::Ok 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | admin 33 | 34 | 35 | 36 | 37 | 38 | 39 | 密码 40 | 41 | 42 | 43 | 44 | 45 | 46 | IP 47 | 48 | 49 | 50 | 51 | 52 | 53 | 1554 54 | 55 | 56 | 57 | 58 | 59 | 60 | 127.0.0.1 61 | 62 | 63 | 64 | 65 | 66 | 67 | 端口 68 | 69 | 70 | 71 | 72 | 73 | 74 | admin123 75 | 76 | 77 | 78 | 79 | 80 | 81 | 用户名 82 | 83 | 84 | 85 | 86 | 87 | 88 | 移动设备 89 | 90 | 91 | true 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | buttonBox 103 | accepted() 104 | RtspConfigView 105 | accept() 106 | 107 | 108 | 248 109 | 254 110 | 111 | 112 | 157 113 | 274 114 | 115 | 116 | 117 | 118 | buttonBox 119 | rejected() 120 | RtspConfigView 121 | reject() 122 | 123 | 124 | 316 125 | 260 126 | 127 | 128 | 286 129 | 274 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tools/video_compare/Readme.md: -------------------------------------------------------------------------------- 1 | # 视频对比工具 2 | ## 使用方法 3 | 1. 拍摄视频,视频的要求:尽量用棋盘格等边缘比较明显的物体,图像中心尽量有明显的特征点 4 | 2. 导入振动台上拍摄的视频,可以通过打开文件或者拖动文件到视频预览窗口进行导入 5 | 3. 点击右下角的开始,会开始对图像进行处理,并会更新实时的结果。点击停止,会停止图像的处理 6 | 4. 视频预览会显示每个特征点的运动轨迹,中心的特征点会着重标注出来。确保每个点的轨迹都没有问题。在最终结果那一栏会得出这段时间内的最终结果 7 | 8 | ## 高级操作 9 | 1. ROI上下边界:由于有些视频存在OSD,会干扰到特征点的选取,可以调整上下边界控制特征点的选取范围 10 | 2. 跳过帧数:因为有的时候前几帧有干扰的物体,或者效果不佳,可以跳过前几帧,再进行特征点选取。 11 | 3. 特征点的数量:如果画面中心没有特征点,那么可以调整此滑块。如果特征点过多,也可以调整此滑块 12 | 4. 特征点的运动幅度:如果运动幅度过大,出现跟踪错误的话,就增大特征点的运动幅度 13 | 5. 计算间隔(帧):每多少帧会输出一个实时的结果,确保每次的实时结果都比较正常,再去看最终结果 14 | 6. 计算方向:跟最后结果挂钩,只显示某个方向上的结果 15 | 7. 去除静止的特征点:可以排除roi等静止的点,优先选用ROI的方式 16 | 17 | ## 显示情况 18 | 1. 视频预览中会把所有的特征点着重标出来,中心参与计算的特征点更加明显 19 | 20 | 21 | ## 评价标准 22 | 23 | 我觉得应该有两个,一个是图片中心的位移,一个是图像的形变程度 24 | 如果图像不变形,每个特征点相对于图片中心都是固定的, 25 | 每个特征点的位移应该是和图片中心一致的 26 | 计算方法:第一帧算出每个特征点相对于图片中心的距离,后续每帧的特征点 27 | 都与第一帧的距离做一个差,所有差的平方和就作为图片的形变程度 28 | 29 | 每个点的运动角度 30 | 31 | 形变程度和每个点的运动方向和运动速度有关,而两帧之间的距离太小,计算容易出现误差,直接拿最左边和最右边的值做一个差。 32 | 每个点的运动可以看做一个向量,每个点的运动向量和中心点的运动向量差就是图像的变形程度 33 | 34 | ## 原理 35 | 先在第一张图像上找到比较好的特征点 36 | 后续的帧对这些特征点进行跟踪 37 | 38 | ## 遇到的问题 39 | 有些特征点跟踪的时候会产生漂移,甚至跟丢了的情况 40 | 1. 利用cornerSubPix计算出亚像素点,更加精准 41 | 2. 还可能是光流法搜索的区域不够大,需要增大winSize -------------------------------------------------------------------------------- /tools/video_compare/rtspconfigview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'rtspconfigview.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, 12 | QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) 13 | from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, 14 | QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, 15 | QPixmap, QRadialGradient) 16 | from PySide2.QtWidgets import * 17 | 18 | 19 | class Ui_RtspConfigView(object): 20 | def setupUi(self, RtspConfigView): 21 | if not RtspConfigView.objectName(): 22 | RtspConfigView.setObjectName(u"RtspConfigView") 23 | RtspConfigView.resize(174, 186) 24 | self.gridLayout = QGridLayout(RtspConfigView) 25 | self.gridLayout.setObjectName(u"gridLayout") 26 | self.buttonBox = QDialogButtonBox(RtspConfigView) 27 | self.buttonBox.setObjectName(u"buttonBox") 28 | self.buttonBox.setOrientation(Qt.Horizontal) 29 | self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) 30 | 31 | self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1) 32 | 33 | self.gridLayout_2 = QGridLayout() 34 | self.gridLayout_2.setObjectName(u"gridLayout_2") 35 | self.username = QLineEdit(RtspConfigView) 36 | self.username.setObjectName(u"username") 37 | 38 | self.gridLayout_2.addWidget(self.username, 0, 1, 1, 1) 39 | 40 | self.label_2 = QLabel(RtspConfigView) 41 | self.label_2.setObjectName(u"label_2") 42 | 43 | self.gridLayout_2.addWidget(self.label_2, 1, 0, 1, 1) 44 | 45 | self.label_3 = QLabel(RtspConfigView) 46 | self.label_3.setObjectName(u"label_3") 47 | 48 | self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1) 49 | 50 | self.port = QLineEdit(RtspConfigView) 51 | self.port.setObjectName(u"port") 52 | 53 | self.gridLayout_2.addWidget(self.port, 3, 1, 1, 1) 54 | 55 | self.ip = QLineEdit(RtspConfigView) 56 | self.ip.setObjectName(u"ip") 57 | 58 | self.gridLayout_2.addWidget(self.ip, 2, 1, 1, 1) 59 | 60 | self.label_4 = QLabel(RtspConfigView) 61 | self.label_4.setObjectName(u"label_4") 62 | 63 | self.gridLayout_2.addWidget(self.label_4, 3, 0, 1, 1) 64 | 65 | self.password = QLineEdit(RtspConfigView) 66 | self.password.setObjectName(u"password") 67 | 68 | self.gridLayout_2.addWidget(self.password, 1, 1, 1, 1) 69 | 70 | self.label = QLabel(RtspConfigView) 71 | self.label.setObjectName(u"label") 72 | 73 | self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) 74 | 75 | self.isphoto = QCheckBox(RtspConfigView) 76 | self.isphoto.setObjectName(u"isphoto") 77 | self.isphoto.setChecked(True) 78 | 79 | self.gridLayout_2.addWidget(self.isphoto, 4, 1, 1, 1) 80 | 81 | 82 | self.gridLayout.addLayout(self.gridLayout_2, 0, 0, 1, 1) 83 | 84 | 85 | self.retranslateUi(RtspConfigView) 86 | self.buttonBox.accepted.connect(RtspConfigView.accept) 87 | self.buttonBox.rejected.connect(RtspConfigView.reject) 88 | 89 | QMetaObject.connectSlotsByName(RtspConfigView) 90 | # setupUi 91 | 92 | def retranslateUi(self, RtspConfigView): 93 | RtspConfigView.setWindowTitle(QCoreApplication.translate("RtspConfigView", u"\u6253\u5f00\u8bbe\u5907(RTSP)", None)) 94 | self.username.setText(QCoreApplication.translate("RtspConfigView", u"admin", None)) 95 | self.label_2.setText(QCoreApplication.translate("RtspConfigView", u"\u5bc6\u7801", None)) 96 | self.label_3.setText(QCoreApplication.translate("RtspConfigView", u"IP", None)) 97 | self.port.setText(QCoreApplication.translate("RtspConfigView", u"1554", None)) 98 | self.ip.setText(QCoreApplication.translate("RtspConfigView", u"127.0.0.1", None)) 99 | self.label_4.setText(QCoreApplication.translate("RtspConfigView", u"\u7aef\u53e3", None)) 100 | self.password.setText(QCoreApplication.translate("RtspConfigView", u"admin123", None)) 101 | self.label.setText(QCoreApplication.translate("RtspConfigView", u"\u7528\u6237\u540d", None)) 102 | self.isphoto.setText(QCoreApplication.translate("RtspConfigView", u"\u79fb\u52a8\u8bbe\u5907", None)) 103 | # retranslateUi 104 | 105 | -------------------------------------------------------------------------------- /tools/video_compare/rtspconfigview.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RtspConfigView 4 | 5 | 6 | 7 | 0 8 | 0 9 | 174 10 | 186 11 | 12 | 13 | 14 | 打开设备(RTSP) 15 | 16 | 17 | 18 | 19 | 20 | Qt::Horizontal 21 | 22 | 23 | QDialogButtonBox::Ok 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | admin 33 | 34 | 35 | 36 | 37 | 38 | 39 | 密码 40 | 41 | 42 | 43 | 44 | 45 | 46 | IP 47 | 48 | 49 | 50 | 51 | 52 | 53 | 1554 54 | 55 | 56 | 57 | 58 | 59 | 60 | 127.0.0.1 61 | 62 | 63 | 64 | 65 | 66 | 67 | 端口 68 | 69 | 70 | 71 | 72 | 73 | 74 | admin123 75 | 76 | 77 | 78 | 79 | 80 | 81 | 用户名 82 | 83 | 84 | 85 | 86 | 87 | 88 | 移动设备 89 | 90 | 91 | true 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | buttonBox 103 | accepted() 104 | RtspConfigView 105 | accept() 106 | 107 | 108 | 248 109 | 254 110 | 111 | 112 | 157 113 | 274 114 | 115 | 116 | 117 | 118 | buttonBox 119 | rejected() 120 | RtspConfigView 121 | reject() 122 | 123 | 124 | 316 125 | 260 126 | 127 | 128 | 286 129 | 274 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tools/video_compare/shake_test_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'shake_test_window.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, 12 | QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) 13 | from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, 14 | QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, 15 | QPixmap, QRadialGradient) 16 | from PySide2.QtWidgets import * 17 | 18 | import resource_rc 19 | 20 | class Ui_ShakeTestWindow(object): 21 | def setupUi(self, ShakeTestWindow): 22 | if not ShakeTestWindow.objectName(): 23 | ShakeTestWindow.setObjectName(u"ShakeTestWindow") 24 | ShakeTestWindow.resize(1034, 653) 25 | self.actionstart = QAction(ShakeTestWindow) 26 | self.actionstart.setObjectName(u"actionstart") 27 | icon = QIcon() 28 | icon.addFile(u":/tool_icon/resource/start.png", QSize(), QIcon.Normal, QIcon.Off) 29 | self.actionstart.setIcon(icon) 30 | self.actionpause = QAction(ShakeTestWindow) 31 | self.actionpause.setObjectName(u"actionpause") 32 | icon1 = QIcon() 33 | icon1.addFile(u":/tool_icon/resource/pause.png", QSize(), QIcon.Normal, QIcon.Off) 34 | self.actionpause.setIcon(icon1) 35 | self.actionrestart = QAction(ShakeTestWindow) 36 | self.actionrestart.setObjectName(u"actionrestart") 37 | icon2 = QIcon() 38 | icon2.addFile(u":/tool_icon/resource/restart .png", QSize(), QIcon.Normal, QIcon.Off) 39 | self.actionrestart.setIcon(icon2) 40 | self.actionspeedup = QAction(ShakeTestWindow) 41 | self.actionspeedup.setObjectName(u"actionspeedup") 42 | icon3 = QIcon() 43 | icon3.addFile(u":/tool_icon/resource/\u5feb\u8fdb.png", QSize(), QIcon.Normal, QIcon.Off) 44 | self.actionspeedup.setIcon(icon3) 45 | self.actionspeeddown = QAction(ShakeTestWindow) 46 | self.actionspeeddown.setObjectName(u"actionspeeddown") 47 | icon4 = QIcon() 48 | icon4.addFile(u":/tool_icon/resource/\u5feb\u9000.png", QSize(), QIcon.Normal, QIcon.Off) 49 | self.actionspeeddown.setIcon(icon4) 50 | self.actionadd = QAction(ShakeTestWindow) 51 | self.actionadd.setObjectName(u"actionadd") 52 | icon5 = QIcon() 53 | icon5.addFile(u":/tool_icon/resource/add.png", QSize(), QIcon.Normal, QIcon.Off) 54 | self.actionadd.setIcon(icon5) 55 | self.actionsubtract = QAction(ShakeTestWindow) 56 | self.actionsubtract.setObjectName(u"actionsubtract") 57 | icon6 = QIcon() 58 | icon6.addFile(u":/tool_icon/resource/subtract.png", QSize(), QIcon.Normal, QIcon.Off) 59 | self.actionsubtract.setIcon(icon6) 60 | self.centralwidget = QWidget(ShakeTestWindow) 61 | self.centralwidget.setObjectName(u"centralwidget") 62 | self.gridLayout = QGridLayout(self.centralwidget) 63 | self.gridLayout.setObjectName(u"gridLayout") 64 | self.horizontalLayout = QHBoxLayout() 65 | self.horizontalLayout.setObjectName(u"horizontalLayout") 66 | 67 | self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1) 68 | 69 | ShakeTestWindow.setCentralWidget(self.centralwidget) 70 | self.menubar = QMenuBar(ShakeTestWindow) 71 | self.menubar.setObjectName(u"menubar") 72 | self.menubar.setGeometry(QRect(0, 0, 1034, 22)) 73 | ShakeTestWindow.setMenuBar(self.menubar) 74 | self.statusbar = QStatusBar(ShakeTestWindow) 75 | self.statusbar.setObjectName(u"statusbar") 76 | ShakeTestWindow.setStatusBar(self.statusbar) 77 | self.toolBar = QToolBar(ShakeTestWindow) 78 | self.toolBar.setObjectName(u"toolBar") 79 | ShakeTestWindow.addToolBar(Qt.RightToolBarArea, self.toolBar) 80 | 81 | self.toolBar.addAction(self.actionstart) 82 | self.toolBar.addAction(self.actionpause) 83 | self.toolBar.addAction(self.actionrestart) 84 | self.toolBar.addSeparator() 85 | self.toolBar.addAction(self.actionadd) 86 | self.toolBar.addAction(self.actionsubtract) 87 | self.toolBar.addSeparator() 88 | self.toolBar.addAction(self.actionspeedup) 89 | self.toolBar.addAction(self.actionspeeddown) 90 | 91 | self.retranslateUi(ShakeTestWindow) 92 | 93 | QMetaObject.connectSlotsByName(ShakeTestWindow) 94 | # setupUi 95 | 96 | def retranslateUi(self, ShakeTestWindow): 97 | ShakeTestWindow.setWindowTitle(QCoreApplication.translate("ShakeTestWindow", u"\u89c6\u9891\u5bf9\u6bd4\u5de5\u5177", None)) 98 | self.actionstart.setText(QCoreApplication.translate("ShakeTestWindow", u"start", None)) 99 | #if QT_CONFIG(tooltip) 100 | self.actionstart.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u5f00\u59cb", None)) 101 | #endif // QT_CONFIG(tooltip) 102 | self.actionpause.setText(QCoreApplication.translate("ShakeTestWindow", u"pause", None)) 103 | #if QT_CONFIG(tooltip) 104 | self.actionpause.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u6682\u505c", None)) 105 | #endif // QT_CONFIG(tooltip) 106 | self.actionrestart.setText(QCoreApplication.translate("ShakeTestWindow", u"restart", None)) 107 | #if QT_CONFIG(tooltip) 108 | self.actionrestart.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u91cd\u65b0\u5f00\u59cb", None)) 109 | #endif // QT_CONFIG(tooltip) 110 | self.actionspeedup.setText(QCoreApplication.translate("ShakeTestWindow", u"speedup", None)) 111 | #if QT_CONFIG(tooltip) 112 | self.actionspeedup.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u52a0\u901f", None)) 113 | #endif // QT_CONFIG(tooltip) 114 | self.actionspeeddown.setText(QCoreApplication.translate("ShakeTestWindow", u"speeddown", None)) 115 | #if QT_CONFIG(tooltip) 116 | self.actionspeeddown.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u51cf\u901f", None)) 117 | #endif // QT_CONFIG(tooltip) 118 | self.actionadd.setText(QCoreApplication.translate("ShakeTestWindow", u"add", None)) 119 | #if QT_CONFIG(tooltip) 120 | self.actionadd.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u589e\u52a0\u89c6\u9891", None)) 121 | #endif // QT_CONFIG(tooltip) 122 | self.actionsubtract.setText(QCoreApplication.translate("ShakeTestWindow", u"subtract", None)) 123 | #if QT_CONFIG(tooltip) 124 | self.actionsubtract.setToolTip(QCoreApplication.translate("ShakeTestWindow", u"\u51cf\u5c11\u89c6\u9891", None)) 125 | #endif // QT_CONFIG(tooltip) 126 | self.toolBar.setWindowTitle(QCoreApplication.translate("ShakeTestWindow", u"toolBar", None)) 127 | # retranslateUi 128 | 129 | -------------------------------------------------------------------------------- /tools/video_compare/shake_test_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ShakeTestWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1034 10 | 653 11 | 12 | 13 | 14 | 视频对比工具 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 0 28 | 1034 29 | 22 30 | 31 | 32 | 33 | 34 | 35 | 36 | toolBar 37 | 38 | 39 | RightToolBarArea 40 | 41 | 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | :/tool_icon/resource/start.png:/tool_icon/resource/start.png 58 | 59 | 60 | start 61 | 62 | 63 | 开始 64 | 65 | 66 | 67 | 68 | 69 | :/tool_icon/resource/pause.png:/tool_icon/resource/pause.png 70 | 71 | 72 | pause 73 | 74 | 75 | 暂停 76 | 77 | 78 | 79 | 80 | 81 | :/tool_icon/resource/restart .png:/tool_icon/resource/restart .png 82 | 83 | 84 | restart 85 | 86 | 87 | 重新开始 88 | 89 | 90 | 91 | 92 | 93 | :/tool_icon/resource/快进.png:/tool_icon/resource/快进.png 94 | 95 | 96 | speedup 97 | 98 | 99 | 加速 100 | 101 | 102 | 103 | 104 | 105 | :/tool_icon/resource/快退.png:/tool_icon/resource/快退.png 106 | 107 | 108 | speeddown 109 | 110 | 111 | 减速 112 | 113 | 114 | 115 | 116 | 117 | :/tool_icon/resource/add.png:/tool_icon/resource/add.png 118 | 119 | 120 | add 121 | 122 | 123 | 增加视频 124 | 125 | 126 | 127 | 128 | 129 | :/tool_icon/resource/subtract.png:/tool_icon/resource/subtract.png 130 | 131 | 132 | subtract 133 | 134 | 135 | 减少视频 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /tools/video_compare/video_pre_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'video_pre_settings.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, 12 | QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) 13 | from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, 14 | QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, 15 | QPixmap, QRadialGradient) 16 | from PySide2.QtWidgets import * 17 | 18 | 19 | class Ui_video_pre_settings(object): 20 | def setupUi(self, video_pre_settings): 21 | if not video_pre_settings.objectName(): 22 | video_pre_settings.setObjectName(u"video_pre_settings") 23 | video_pre_settings.resize(310, 220) 24 | video_pre_settings.setMaximumSize(QSize(16777215, 16777215)) 25 | self.gridLayout_2 = QGridLayout(video_pre_settings) 26 | self.gridLayout_2.setObjectName(u"gridLayout_2") 27 | self.verticalLayout = QVBoxLayout() 28 | self.verticalLayout.setObjectName(u"verticalLayout") 29 | self.groupBox = QGroupBox(video_pre_settings) 30 | self.groupBox.setObjectName(u"groupBox") 31 | self.gridLayout_3 = QGridLayout(self.groupBox) 32 | self.gridLayout_3.setObjectName(u"gridLayout_3") 33 | self.videoview = QHBoxLayout() 34 | self.videoview.setObjectName(u"videoview") 35 | 36 | self.gridLayout_3.addLayout(self.videoview, 0, 0, 1, 1) 37 | 38 | 39 | self.verticalLayout.addWidget(self.groupBox) 40 | 41 | self.groupBox_2 = QGroupBox(video_pre_settings) 42 | self.groupBox_2.setObjectName(u"groupBox_2") 43 | self.groupBox_2.setMaximumSize(QSize(16777215, 113)) 44 | self.gridLayout_4 = QGridLayout(self.groupBox_2) 45 | self.gridLayout_4.setObjectName(u"gridLayout_4") 46 | self.verticalLayout_2 = QVBoxLayout() 47 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 48 | self.gridLayout = QGridLayout() 49 | self.gridLayout.setObjectName(u"gridLayout") 50 | self.label = QLabel(self.groupBox_2) 51 | self.label.setObjectName(u"label") 52 | self.label.setMaximumSize(QSize(16777215, 16777215)) 53 | 54 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 55 | 56 | self.path = QLineEdit(self.groupBox_2) 57 | self.path.setObjectName(u"path") 58 | self.path.setMaximumSize(QSize(16777215, 16777215)) 59 | 60 | self.gridLayout.addWidget(self.path, 0, 1, 1, 1) 61 | 62 | self.label_2 = QLabel(self.groupBox_2) 63 | self.label_2.setObjectName(u"label_2") 64 | self.label_2.setMaximumSize(QSize(16777215, 16777215)) 65 | 66 | self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) 67 | 68 | self.skipframe = QSpinBox(self.groupBox_2) 69 | self.skipframe.setObjectName(u"skipframe") 70 | self.skipframe.setMaximumSize(QSize(16777215, 16777215)) 71 | self.skipframe.setMaximum(10000) 72 | self.skipframe.setSingleStep(10) 73 | 74 | self.gridLayout.addWidget(self.skipframe, 1, 1, 1, 1) 75 | 76 | 77 | self.verticalLayout_2.addLayout(self.gridLayout) 78 | 79 | self.horizontalLayout = QHBoxLayout() 80 | self.horizontalLayout.setObjectName(u"horizontalLayout") 81 | self.open_rtsp = QPushButton(self.groupBox_2) 82 | self.open_rtsp.setObjectName(u"open_rtsp") 83 | self.open_rtsp.setMaximumSize(QSize(16777215, 16777215)) 84 | 85 | self.horizontalLayout.addWidget(self.open_rtsp) 86 | 87 | self.openvideo = QPushButton(self.groupBox_2) 88 | self.openvideo.setObjectName(u"openvideo") 89 | self.openvideo.setMaximumSize(QSize(16777215, 16777215)) 90 | 91 | self.horizontalLayout.addWidget(self.openvideo) 92 | 93 | 94 | self.verticalLayout_2.addLayout(self.horizontalLayout) 95 | 96 | 97 | self.gridLayout_4.addLayout(self.verticalLayout_2, 0, 0, 1, 1) 98 | 99 | 100 | self.verticalLayout.addWidget(self.groupBox_2) 101 | 102 | 103 | self.gridLayout_2.addLayout(self.verticalLayout, 0, 0, 1, 1) 104 | 105 | 106 | self.retranslateUi(video_pre_settings) 107 | 108 | QMetaObject.connectSlotsByName(video_pre_settings) 109 | # setupUi 110 | 111 | def retranslateUi(self, video_pre_settings): 112 | video_pre_settings.setWindowTitle(QCoreApplication.translate("video_pre_settings", u"Form", None)) 113 | self.groupBox.setTitle(QCoreApplication.translate("video_pre_settings", u"\u89c6\u9891\u9884\u89c8", None)) 114 | self.groupBox_2.setTitle("") 115 | self.label.setText(QCoreApplication.translate("video_pre_settings", u"\u89c6\u9891\u8def\u5f84", None)) 116 | self.label_2.setText(QCoreApplication.translate("video_pre_settings", u"\u8df3\u8fc7\u8fc7\u5e27\u6570", None)) 117 | self.open_rtsp.setText(QCoreApplication.translate("video_pre_settings", u"\u6253\u5f00\u8bbe\u5907", None)) 118 | self.openvideo.setText(QCoreApplication.translate("video_pre_settings", u"\u6253\u5f00\u6587\u4ef6", None)) 119 | # retranslateUi 120 | 121 | -------------------------------------------------------------------------------- /tools/video_compare/video_pre_settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | video_pre_settings 4 | 5 | 6 | 7 | 0 8 | 0 9 | 310 10 | 220 11 | 12 | 13 | 14 | 15 | 16777215 16 | 16777215 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 视频预览 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 16777215 42 | 113 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 16777215 58 | 16777215 59 | 60 | 61 | 62 | 视频路径 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 16777215 71 | 16777215 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 16777215 81 | 16777215 82 | 83 | 84 | 85 | 跳过过帧数 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 16777215 94 | 16777215 95 | 96 | 97 | 98 | 10000 99 | 100 | 101 | 10 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 16777215 114 | 16777215 115 | 116 | 117 | 118 | 打开设备 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 16777215 127 | 16777215 128 | 129 | 130 | 131 | 打开文件 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /tools/video_compare/videocompare.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | from PySide2.QtWidgets import QFileDialog, QMessageBox, QDialog, QWidget 3 | from PySide2.QtGui import QImage, QPixmap 4 | from PySide2.QtCore import QTimer 5 | from tools.video_compare.shake_test_window import Ui_ShakeTestWindow 6 | from tools.video_compare.rtspconfigview import Ui_RtspConfigView 7 | from components.customwidget import VideoView 8 | from components.window import SubWindow 9 | from tools.video_compare.videoview import VideoCompareView 10 | import numpy as np 11 | import math 12 | import os 13 | 14 | 15 | class VideoCompare(SubWindow): 16 | def __init__(self, name='VideoCompare', parent=None): 17 | super().__init__(name, parent, Ui_ShakeTestWindow()) 18 | self.videoview = [VideoCompareView(self.ui.horizontalLayout)] 19 | self.videoview.append(VideoCompareView(self.ui.horizontalLayout)) 20 | #self.videoview[0].setupCbFunc(self.open_video, self.open_rtsp, self.open_video_path) 21 | self.ui.actionstart.triggered.connect(self.start_video) 22 | self.ui.actionpause.triggered.connect(self.pause_video) 23 | self.ui.actionrestart.triggered.connect(self.restart_video) 24 | self.ui.actionadd.triggered.connect(self.add_video) 25 | self.ui.actionsubtract.triggered.connect(self.remove_video) 26 | self.ui.actionspeedup.triggered.connect(self.speed_up_video) 27 | self.ui.actionspeeddown.triggered.connect(self.speed_down_video) 28 | self.speed = 1.0 29 | self.ui.statusbar.showMessage("播放速度 {}".format(self.speed)) 30 | 31 | def speed_down_video(self): 32 | if(self.speed < 3): 33 | self.speed += 0.1 34 | self.ui.statusbar.showMessage("播放速度 {:.2f}".format(1/self.speed)) 35 | for video in self.videoview: 36 | video.set_speed(self.speed) 37 | else: 38 | return 39 | 40 | def speed_up_video(self): 41 | if(self.speed > 0.3): 42 | self.speed -= 0.1 43 | self.ui.statusbar.showMessage("播放速度 {:.2f}".format(1/self.speed)) 44 | for video in self.videoview: 45 | video.set_speed(self.speed) 46 | else: 47 | return 48 | 49 | def add_video(self): 50 | self.videoview.append(VideoCompareView(self.ui.horizontalLayout)) 51 | 52 | def remove_video(self): 53 | remove_widget = self.videoview.pop() 54 | # 必须要加这一句才能彻底移除! 55 | remove_widget.widget.setParent(None) 56 | self.ui.horizontalLayout.removeWidget(remove_widget.widget) 57 | 58 | def start_video(self): 59 | for video in self.videoview: 60 | video.start_video() 61 | 62 | def pause_video(self): 63 | for video in self.videoview: 64 | video.stop_video() 65 | 66 | def restart_video(self): 67 | for video in self.videoview: 68 | video.restart_video() 69 | 70 | def set_ui_enable(self, value): 71 | self.ui.set_roi_up.setEnabled(value) 72 | self.ui.set_roi_down.setEnabled(value) 73 | self.ui.direction_select.setEnabled(value) 74 | self.ui.skipframes.setEnabled(value) 75 | self.ui.remove_move_point_enable.setEnabled(value) 76 | self.ui.calc_inter_frams.setEnabled(value) 77 | 78 | 79 | class RtspConfigView(QDialog): 80 | def __init__(self, parent): 81 | super().__init__() 82 | self.parent = parent 83 | 84 | def closeEvent(self, event): 85 | return super().closeEvent(event) 86 | -------------------------------------------------------------------------------- /tools/video_compare/videoview.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QDialog, QWidget, QGraphicsScene, QFileDialog, QMessageBox 2 | from PySide2.QtGui import QImage, QPixmap 3 | from PySide2.QtCore import QTimer 4 | from components.customwidget import ImageView 5 | from tools.video_compare.video_pre_settings import Ui_video_pre_settings 6 | from tools.video_compare.rtspconfigview import Ui_RtspConfigView 7 | import os 8 | import cv2 9 | 10 | 11 | class VideoCompareView(object): 12 | def __init__(self, parent=None): 13 | self.widget = QWidget() 14 | self.setting_widget = Ui_video_pre_settings() 15 | self.setting_widget.setupUi(self.widget) 16 | self.scene = QGraphicsScene() 17 | self.imageview = ImageView(self.scene, self.widget) 18 | self.setting_widget.videoview.addWidget(self.imageview) 19 | parent.addWidget(self.widget) 20 | # init params 21 | self.video_valid = False 22 | self.process_speed = 33 23 | self.skip_frames = 0 24 | self.processing_video = False 25 | self.video_timer = QTimer() 26 | self.video_timer.timeout.connect(self.open_frame) 27 | # init func 28 | self.setting_widget.openvideo.clicked.connect(self.open_video) 29 | self.setting_widget.open_rtsp.clicked.connect(self.open_rtsp) 30 | self.setting_widget.skipframe.valueChanged.connect(self.set_skip_frame) 31 | self.imageview.sigDragEvent.connect(self.open_video_path) 32 | 33 | def set_skip_frame(self, value): 34 | # self.skip_frames = self.setting_widget.skipframe.value() 35 | self.skip_frames = value 36 | self.vertify_video() 37 | 38 | def open_video(self): 39 | videopath = QFileDialog.getOpenFileName( 40 | None, '打开文件', './', 'video files(*.mp4)') 41 | if(videopath[0] != ''): 42 | self.open_video_path(videopath[0]) 43 | 44 | def open_video_path(self, string): 45 | self.setting_widget.path.setText(string) 46 | self.vertify_video() 47 | # self.set_ui_enable(True) 48 | 49 | def open_rtsp(self): 50 | self.rtsp_config_window = QDialog() 51 | self.rtsp_config_ui = Ui_RtspConfigView() 52 | self.rtsp_config_ui.setupUi(self.rtsp_config_window) 53 | self.rtsp_config_window.show() 54 | self.rtsp_config_ui.buttonBox.clicked.connect(self.rtsp_config) 55 | 56 | def rtsp_config(self): 57 | username = self.rtsp_config_ui.username.text() 58 | password = self.rtsp_config_ui.password.text() 59 | ip = self.rtsp_config_ui.ip.text() 60 | port = self.rtsp_config_ui.port.text() 61 | # 移动设备需要通过adb映射端口 62 | if(self.rtsp_config_ui.isphoto.isChecked() == True): 63 | command = "forward tcp:" + port + ' ' + "tcp:" + port 64 | os.system("adb " + command) 65 | os.system("kdb " + command) 66 | rtsp_path = "rtsp://"+username+":"+password+"@"+ip+":"+port 67 | self.open_video_path(rtsp_path) 68 | 69 | def vertify_video(self): 70 | # 输出参数初始化 71 | # self.frame_count = 0 72 | self.vidcap = cv2.VideoCapture(self.setting_widget.path.text()) 73 | # 片头调过多少帧 74 | self.vidcap.set(cv2.CAP_PROP_POS_FRAMES, self.skip_frames) 75 | success, frame = self.vidcap.read() 76 | if success: 77 | self.display(frame) 78 | self.video_valid = True 79 | else: 80 | self.critical_window_show('视频打不开') 81 | self.video_valid = False 82 | return 83 | 84 | def display(self, img): 85 | self.scene.clear() 86 | self.scene.addPixmap( 87 | QPixmap(QImage(img, img.shape[1], img.shape[0], QImage.Format_BGR888))) 88 | 89 | def open_frame(self): 90 | success, frame = self.vidcap.read() 91 | if success: 92 | # self.frame_count += 1 93 | self.display(frame) 94 | else: 95 | self.video_timer.stop() 96 | self.processing_video = False 97 | self.video_valid = False 98 | 99 | def set_speed(self, value): 100 | speed = value * 33 101 | if (self.processing_video == True and int(speed) != self.process_speed): 102 | self.process_speed = int(speed) 103 | self.stop_video() 104 | self.start_video() 105 | 106 | def start_video(self): 107 | if (self.video_valid == True): 108 | self.processing_video = True 109 | # 增加定时器,每100ms进行一帧的处理 110 | self.video_timer.start(self.process_speed) 111 | 112 | def stop_video(self): 113 | self.processing_video = False 114 | self.video_timer.stop() 115 | 116 | def restart_video(self): 117 | self.stop_video() 118 | self.vertify_video() 119 | self.start_video() 120 | 121 | def critical_window_show(self, str): 122 | reply = QMessageBox.critical( 123 | self.widget, '警告', str, 124 | QMessageBox.Yes, QMessageBox.Yes) 125 | return 126 | -------------------------------------------------------------------------------- /tools/yuv_viewer/Readme.md: -------------------------------------------------------------------------------- 1 | # YUV 图片查看工具 2 | 3 | 这是一款能够对 YUV 图像进行分析以及简单的处理工具 4 | 5 | ## 背景 6 | 7 | 1. 经常需要抓取 yuv,又没有比较好的工具,能够打开比较多的格式,并且保存成 jpg 8 | 2. 多张 yuv 之间的对比也比较麻烦 9 | 10 | ## 将要实现的功能点 11 | 12 | [ ] 能够支持常见的 yuv 格式 13 | [ ] 打开或者拖动的时候,会提示选取 yuv 格式,并保存最近一次格式 14 | [ ] 能够保存成 jpg 格式 15 | [ ] 能够通过鼠标对图片放大和缩小 16 | [ ] 能够通过鼠标对像素点的信息进行显示 17 | [ ] 能够通过直方图等统计分析对选中区域进行分析 18 | [ ] 能够支持多张图片对比,按+号,可以添加图片窗口 19 | [ ] 多张图片拖动与放大应该保持一致 20 | [ ] 能够支持批量 yuv 图片转换成 jpg 的功能 21 | [ ] 能够按上下键去查看上下的 yuv 22 | [ ] 能够支持多帧 yuv 的解析,通过数字或者滑动条去选择相应帧的图片显示 23 | [ ] 支持多张图片的偏移调整 24 | -------------------------------------------------------------------------------- /tools/yuv_viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoqinxing/ImageTools/7ddf141c10fb55c0be4babb1004776e112a9f330/tools/yuv_viewer/__init__.py -------------------------------------------------------------------------------- /tools/yuv_viewer/ui/yuvconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'yuvconfig.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | 16 | class Ui_YUVConfig(object): 17 | def setupUi(self, YUVConfig): 18 | if not YUVConfig.objectName(): 19 | YUVConfig.setObjectName(u"YUVConfig") 20 | YUVConfig.resize(212, 175) 21 | self.verticalLayout = QVBoxLayout(YUVConfig) 22 | self.verticalLayout.setObjectName(u"verticalLayout") 23 | self.groupBox = QGroupBox(YUVConfig) 24 | self.groupBox.setObjectName(u"groupBox") 25 | self.verticalLayout_2 = QVBoxLayout(self.groupBox) 26 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 27 | self.formLayout = QFormLayout() 28 | self.formLayout.setObjectName(u"formLayout") 29 | self.label_2 = QLabel(self.groupBox) 30 | self.label_2.setObjectName(u"label_2") 31 | 32 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.label_2) 33 | 34 | self.label_3 = QLabel(self.groupBox) 35 | self.label_3.setObjectName(u"label_3") 36 | 37 | self.formLayout.setWidget(2, QFormLayout.LabelRole, self.label_3) 38 | 39 | self.yuvformat = QComboBox(self.groupBox) 40 | self.yuvformat.addItem("") 41 | self.yuvformat.addItem("") 42 | self.yuvformat.addItem("") 43 | self.yuvformat.addItem("") 44 | self.yuvformat.addItem("") 45 | self.yuvformat.addItem("") 46 | self.yuvformat.addItem("") 47 | self.yuvformat.addItem("") 48 | self.yuvformat.setObjectName(u"yuvformat") 49 | 50 | self.formLayout.setWidget(2, QFormLayout.FieldRole, self.yuvformat) 51 | 52 | self.width = QSpinBox(self.groupBox) 53 | self.width.setObjectName(u"width") 54 | self.width.setMaximum(8192) 55 | self.width.setSingleStep(10) 56 | self.width.setValue(3840) 57 | 58 | self.formLayout.setWidget(0, QFormLayout.FieldRole, self.width) 59 | 60 | self.label = QLabel(self.groupBox) 61 | self.label.setObjectName(u"label") 62 | 63 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.label) 64 | 65 | self.height = QSpinBox(self.groupBox) 66 | self.height.setObjectName(u"height") 67 | self.height.setMaximum(8192) 68 | self.height.setSingleStep(10) 69 | self.height.setValue(2160) 70 | 71 | self.formLayout.setWidget(1, QFormLayout.FieldRole, self.height) 72 | 73 | 74 | self.verticalLayout_2.addLayout(self.formLayout) 75 | 76 | 77 | self.verticalLayout.addWidget(self.groupBox) 78 | 79 | self.saveimg_in_rotate = QCheckBox(YUVConfig) 80 | self.saveimg_in_rotate.setObjectName(u"saveimg_in_rotate") 81 | self.saveimg_in_rotate.setChecked(True) 82 | 83 | self.verticalLayout.addWidget(self.saveimg_in_rotate) 84 | 85 | self.buttonBox = QDialogButtonBox(YUVConfig) 86 | self.buttonBox.setObjectName(u"buttonBox") 87 | self.buttonBox.setOrientation(Qt.Horizontal) 88 | self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) 89 | 90 | self.verticalLayout.addWidget(self.buttonBox) 91 | 92 | 93 | self.retranslateUi(YUVConfig) 94 | self.buttonBox.accepted.connect(YUVConfig.accept) 95 | self.buttonBox.rejected.connect(YUVConfig.reject) 96 | 97 | QMetaObject.connectSlotsByName(YUVConfig) 98 | # setupUi 99 | 100 | def retranslateUi(self, YUVConfig): 101 | YUVConfig.setWindowTitle(QCoreApplication.translate("YUVConfig", u"\u56fe\u7247\u67e5\u770b\u5de5\u5177\u914d\u7f6e", None)) 102 | self.groupBox.setTitle(QCoreApplication.translate("YUVConfig", u"YUV\u914d\u7f6e", None)) 103 | self.label_2.setText(QCoreApplication.translate("YUVConfig", u"\u5bbd", None)) 104 | self.label_3.setText(QCoreApplication.translate("YUVConfig", u"\u683c\u5f0f", None)) 105 | self.yuvformat.setItemText(0, QCoreApplication.translate("YUVConfig", u"NV21", None)) 106 | self.yuvformat.setItemText(1, QCoreApplication.translate("YUVConfig", u"NV12", None)) 107 | self.yuvformat.setItemText(2, QCoreApplication.translate("YUVConfig", u"YCrCb", None)) 108 | self.yuvformat.setItemText(3, QCoreApplication.translate("YUVConfig", u"YUV420", None)) 109 | self.yuvformat.setItemText(4, QCoreApplication.translate("YUVConfig", u"YUV422", None)) 110 | self.yuvformat.setItemText(5, QCoreApplication.translate("YUVConfig", u"UYVY", None)) 111 | self.yuvformat.setItemText(6, QCoreApplication.translate("YUVConfig", u"YUYV", None)) 112 | self.yuvformat.setItemText(7, QCoreApplication.translate("YUVConfig", u"YVYU", None)) 113 | 114 | self.label.setText(QCoreApplication.translate("YUVConfig", u"\u9ad8", None)) 115 | self.saveimg_in_rotate.setText(QCoreApplication.translate("YUVConfig", u"\u65cb\u8f6c\u65f6\u4fdd\u5b58\u56fe\u7247", None)) 116 | # retranslateUi 117 | 118 | -------------------------------------------------------------------------------- /tools/yuv_viewer/ui/yuvconfig.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | YUVConfig 4 | 5 | 6 | 7 | 0 8 | 0 9 | 212 10 | 175 11 | 12 | 13 | 14 | 图片查看工具配置 15 | 16 | 17 | 18 | 19 | 20 | YUV配置 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 格式 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | NV21 44 | 45 | 46 | 47 | 48 | NV12 49 | 50 | 51 | 52 | 53 | YCrCb 54 | 55 | 56 | 57 | 58 | YUV420 59 | 60 | 61 | 62 | 63 | YUV422 64 | 65 | 66 | 67 | 68 | UYVY 69 | 70 | 71 | 72 | 73 | YUYV 74 | 75 | 76 | 77 | 78 | YVYU 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 8192 87 | 88 | 89 | 10 90 | 91 | 92 | 3840 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 8192 107 | 108 | 109 | 10 110 | 111 | 112 | 2160 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 旋转时保存图片 125 | 126 | 127 | true 128 | 129 | 130 | 131 | 132 | 133 | 134 | Qt::Horizontal 135 | 136 | 137 | QDialogButtonBox::Ok 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | buttonBox 147 | accepted() 148 | YUVConfig 149 | accept() 150 | 151 | 152 | 248 153 | 254 154 | 155 | 156 | 157 157 | 274 158 | 159 | 160 | 161 | 162 | buttonBox 163 | rejected() 164 | YUVConfig 165 | reject() 166 | 167 | 168 | 316 169 | 260 170 | 171 | 172 | 286 173 | 274 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /tools/yuv_viewer/ui/yuvviewer_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'yuvviewer_window.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 5.15.2 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide2.QtCore import * 12 | from PySide2.QtGui import * 13 | from PySide2.QtWidgets import * 14 | 15 | import resource_rc 16 | 17 | class Ui_YUVEditor(object): 18 | def setupUi(self, YUVEditor): 19 | if not YUVEditor.objectName(): 20 | YUVEditor.setObjectName(u"YUVEditor") 21 | YUVEditor.resize(800, 600) 22 | icon = QIcon() 23 | icon.addFile(u":/tool_icon/resource/main.png", QSize(), QIcon.Normal, QIcon.Off) 24 | YUVEditor.setWindowIcon(icon) 25 | YUVEditor.setToolTipDuration(-1) 26 | YUVEditor.setAnimated(True) 27 | YUVEditor.setDockOptions(QMainWindow.AllowTabbedDocks|QMainWindow.AnimatedDocks|QMainWindow.GroupedDragging|QMainWindow.VerticalTabs) 28 | self.saveimage = QAction(YUVEditor) 29 | self.saveimage.setObjectName(u"saveimage") 30 | icon1 = QIcon() 31 | icon1.addFile(u":/tool_icon/resource/save_icon.png", QSize(), QIcon.Normal, QIcon.Off) 32 | self.saveimage.setIcon(icon1) 33 | self.openimage = QAction(YUVEditor) 34 | self.openimage.setObjectName(u"openimage") 35 | icon2 = QIcon() 36 | icon2.addFile(u":/tool_icon/resource/open.png", QSize(), QIcon.Normal, QIcon.Off) 37 | self.openimage.setIcon(icon2) 38 | self.actionstats = QAction(YUVEditor) 39 | self.actionstats.setObjectName(u"actionstats") 40 | icon3 = QIcon() 41 | icon3.addFile(u":/tool_icon/resource/stats.png", QSize(), QIcon.Normal, QIcon.Off) 42 | self.actionstats.setIcon(icon3) 43 | self.nextphoto = QAction(YUVEditor) 44 | self.nextphoto.setObjectName(u"nextphoto") 45 | icon4 = QIcon() 46 | icon4.addFile(u":/tool_icon/resource/down.png", QSize(), QIcon.Normal, QIcon.Off) 47 | self.nextphoto.setIcon(icon4) 48 | self.prephoto = QAction(YUVEditor) 49 | self.prephoto.setObjectName(u"prephoto") 50 | icon5 = QIcon() 51 | icon5.addFile(u":/tool_icon/resource/up.png", QSize(), QIcon.Normal, QIcon.Off) 52 | self.prephoto.setIcon(icon5) 53 | self.deletephoto = QAction(YUVEditor) 54 | self.deletephoto.setObjectName(u"deletephoto") 55 | icon6 = QIcon() 56 | icon6.addFile(u":/tool_icon/resource/delete.png", QSize(), QIcon.Normal, QIcon.Off) 57 | self.deletephoto.setIcon(icon6) 58 | self.add_compare = QAction(YUVEditor) 59 | self.add_compare.setObjectName(u"add_compare") 60 | icon7 = QIcon() 61 | icon7.addFile(u":/tool_icon/resource/add.png", QSize(), QIcon.Normal, QIcon.Off) 62 | self.add_compare.setIcon(icon7) 63 | self.rotateright = QAction(YUVEditor) 64 | self.rotateright.setObjectName(u"rotateright") 65 | icon8 = QIcon() 66 | icon8.addFile(u":/tool_icon/resource/right.png", QSize(), QIcon.Normal, QIcon.Off) 67 | self.rotateright.setIcon(icon8) 68 | self.yuvconfig = QAction(YUVEditor) 69 | self.yuvconfig.setObjectName(u"yuvconfig") 70 | icon9 = QIcon() 71 | icon9.addFile(u":/tool_icon/resource/config.png", QSize(), QIcon.Normal, QIcon.Off) 72 | self.yuvconfig.setIcon(icon9) 73 | self.centralwidget = QWidget(YUVEditor) 74 | self.centralwidget.setObjectName(u"centralwidget") 75 | self.gridLayout_2 = QGridLayout(self.centralwidget) 76 | self.gridLayout_2.setObjectName(u"gridLayout_2") 77 | self.photo_title = QGroupBox(self.centralwidget) 78 | self.photo_title.setObjectName(u"photo_title") 79 | self.gridLayout_3 = QGridLayout(self.photo_title) 80 | self.gridLayout_3.setObjectName(u"gridLayout_3") 81 | self.gridLayout = QGridLayout() 82 | self.gridLayout.setObjectName(u"gridLayout") 83 | 84 | self.gridLayout_3.addLayout(self.gridLayout, 0, 0, 1, 1) 85 | 86 | 87 | self.gridLayout_2.addWidget(self.photo_title, 0, 0, 1, 1) 88 | 89 | YUVEditor.setCentralWidget(self.centralwidget) 90 | self.statusBar = QStatusBar(YUVEditor) 91 | self.statusBar.setObjectName(u"statusBar") 92 | self.statusBar.setMouseTracking(True) 93 | YUVEditor.setStatusBar(self.statusBar) 94 | self.toolBar = QToolBar(YUVEditor) 95 | self.toolBar.setObjectName(u"toolBar") 96 | sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) 97 | sizePolicy.setHorizontalStretch(0) 98 | sizePolicy.setVerticalStretch(0) 99 | sizePolicy.setHeightForWidth(self.toolBar.sizePolicy().hasHeightForWidth()) 100 | self.toolBar.setSizePolicy(sizePolicy) 101 | self.toolBar.setAllowedAreas(Qt.AllToolBarAreas) 102 | self.toolBar.setOrientation(Qt.Vertical) 103 | YUVEditor.addToolBar(Qt.RightToolBarArea, self.toolBar) 104 | 105 | self.toolBar.addAction(self.openimage) 106 | self.toolBar.addAction(self.saveimage) 107 | self.toolBar.addAction(self.yuvconfig) 108 | self.toolBar.addAction(self.deletephoto) 109 | self.toolBar.addAction(self.actionstats) 110 | self.toolBar.addAction(self.prephoto) 111 | self.toolBar.addAction(self.nextphoto) 112 | self.toolBar.addAction(self.rotateright) 113 | self.toolBar.addAction(self.add_compare) 114 | 115 | self.retranslateUi(YUVEditor) 116 | 117 | QMetaObject.connectSlotsByName(YUVEditor) 118 | # setupUi 119 | 120 | def retranslateUi(self, YUVEditor): 121 | YUVEditor.setWindowTitle(QCoreApplication.translate("YUVEditor", u"YUV\u56fe\u7247\u67e5\u770b\u5de5\u5177", None)) 122 | #if QT_CONFIG(tooltip) 123 | YUVEditor.setToolTip("") 124 | #endif // QT_CONFIG(tooltip) 125 | self.saveimage.setText(QCoreApplication.translate("YUVEditor", u"save", None)) 126 | #if QT_CONFIG(shortcut) 127 | self.saveimage.setShortcut(QCoreApplication.translate("YUVEditor", u"Ctrl+S", None)) 128 | #endif // QT_CONFIG(shortcut) 129 | self.openimage.setText(QCoreApplication.translate("YUVEditor", u"open", None)) 130 | #if QT_CONFIG(tooltip) 131 | self.openimage.setToolTip(QCoreApplication.translate("YUVEditor", u"open", None)) 132 | #endif // QT_CONFIG(tooltip) 133 | self.actionstats.setText(QCoreApplication.translate("YUVEditor", u"\u7edf\u8ba1\u4fe1\u606f", None)) 134 | #if QT_CONFIG(tooltip) 135 | self.actionstats.setToolTip(QCoreApplication.translate("YUVEditor", u"\u7edf\u8ba1\u4fe1\u606f", None)) 136 | #endif // QT_CONFIG(tooltip) 137 | self.nextphoto.setText(QCoreApplication.translate("YUVEditor", u"\u4e0b\u4e00\u4e2a\u56fe\u7247", None)) 138 | #if QT_CONFIG(shortcut) 139 | self.nextphoto.setShortcut(QCoreApplication.translate("YUVEditor", u"Down", None)) 140 | #endif // QT_CONFIG(shortcut) 141 | self.prephoto.setText(QCoreApplication.translate("YUVEditor", u"\u4e0a\u4e00\u5f20\u56fe\u7247", None)) 142 | #if QT_CONFIG(shortcut) 143 | self.prephoto.setShortcut(QCoreApplication.translate("YUVEditor", u"Up", None)) 144 | #endif // QT_CONFIG(shortcut) 145 | self.deletephoto.setText(QCoreApplication.translate("YUVEditor", u"\u5220\u9664", None)) 146 | #if QT_CONFIG(tooltip) 147 | self.deletephoto.setToolTip(QCoreApplication.translate("YUVEditor", u"\u5220\u9664", None)) 148 | #endif // QT_CONFIG(tooltip) 149 | #if QT_CONFIG(shortcut) 150 | self.deletephoto.setShortcut(QCoreApplication.translate("YUVEditor", u"Del", None)) 151 | #endif // QT_CONFIG(shortcut) 152 | self.add_compare.setText(QCoreApplication.translate("YUVEditor", u"\u6dfb\u52a0\u5bf9\u6bd4\u56fe\u7247", None)) 153 | self.rotateright.setText(QCoreApplication.translate("YUVEditor", u"\u987a\u65f6\u9488\u65cb\u8f6c", None)) 154 | self.yuvconfig.setText(QCoreApplication.translate("YUVEditor", u"YUV\u914d\u7f6e", None)) 155 | self.photo_title.setTitle("") 156 | self.toolBar.setWindowTitle(QCoreApplication.translate("YUVEditor", u"toolBar_2", None)) 157 | # retranslateUi 158 | 159 | -------------------------------------------------------------------------------- /tools/yuv_viewer/ui/yuvviewer_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | YUVEditor 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | YUV图片查看工具 15 | 16 | 17 | 18 | :/tool_icon/resource/main.png:/tool_icon/resource/main.png 19 | 20 | 21 | 22 | 23 | 24 | -1 25 | 26 | 27 | true 28 | 29 | 30 | QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging|QMainWindow::VerticalTabs 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | 54 | 55 | 56 | 0 57 | 0 58 | 59 | 60 | 61 | toolBar_2 62 | 63 | 64 | Qt::AllToolBarAreas 65 | 66 | 67 | Qt::Vertical 68 | 69 | 70 | RightToolBarArea 71 | 72 | 73 | false 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | :/tool_icon/resource/save_icon.png:/tool_icon/resource/save_icon.png 89 | 90 | 91 | save 92 | 93 | 94 | Ctrl+S 95 | 96 | 97 | 98 | 99 | 100 | :/tool_icon/resource/open.png:/tool_icon/resource/open.png 101 | 102 | 103 | open 104 | 105 | 106 | open 107 | 108 | 109 | 110 | 111 | 112 | :/tool_icon/resource/stats.png:/tool_icon/resource/stats.png 113 | 114 | 115 | 统计信息 116 | 117 | 118 | 统计信息 119 | 120 | 121 | 122 | 123 | 124 | :/tool_icon/resource/down.png:/tool_icon/resource/down.png 125 | 126 | 127 | 下一个图片 128 | 129 | 130 | Down 131 | 132 | 133 | 134 | 135 | 136 | :/tool_icon/resource/up.png:/tool_icon/resource/up.png 137 | 138 | 139 | 上一张图片 140 | 141 | 142 | Up 143 | 144 | 145 | 146 | 147 | 148 | :/tool_icon/resource/delete.png:/tool_icon/resource/delete.png 149 | 150 | 151 | 删除 152 | 153 | 154 | 删除 155 | 156 | 157 | Del 158 | 159 | 160 | 161 | 162 | 163 | :/tool_icon/resource/add.png:/tool_icon/resource/add.png 164 | 165 | 166 | 添加对比图片 167 | 168 | 169 | 170 | 171 | 172 | :/tool_icon/resource/right.png:/tool_icon/resource/right.png 173 | 174 | 175 | 顺时针旋转 176 | 177 | 178 | 179 | 180 | 181 | :/tool_icon/resource/config.png:/tool_icon/resource/config.png 182 | 183 | 184 | YUV配置 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /tools/yuv_viewer/yuv_config.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QDialog 2 | from PySide2.QtGui import Qt 3 | from PySide2.QtCore import Signal 4 | from .ui.yuvconfig import Ui_YUVConfig 5 | 6 | 7 | class YUVConfig(QDialog): 8 | height = 2160 9 | width = 3840 10 | yuv_format = 'NV21' 11 | need_saveimg_in_rotate = True 12 | configUpdateEvent = Signal() 13 | 14 | def __init__(self, parent=None) -> None: 15 | super().__init__(parent) 16 | self.ui = Ui_YUVConfig() 17 | self.ui.setupUi(self) 18 | self.ui.buttonBox.clicked.connect(self.get) 19 | 20 | def set(self): 21 | self.ui.width.setValue(self.width) 22 | self.ui.height.setValue(self.height) 23 | index = self.ui.yuvformat.findText(self.yuv_format) 24 | self.ui.yuvformat.setCurrentIndex(index) 25 | self.ui.saveimg_in_rotate.setCheckState( 26 | Qt.Checked if self.need_saveimg_in_rotate else Qt.Unchecked) 27 | 28 | def get(self): 29 | self.height = self.ui.height.value() 30 | self.width = self.ui.width.value() 31 | self.yuv_format = self.ui.yuvformat.currentText() 32 | self.need_saveimg_in_rotate = ( 33 | self.ui.saveimg_in_rotate.checkState() == Qt.Checked) 34 | self.configUpdateEvent.emit() 35 | -------------------------------------------------------------------------------- /tools/yuv_viewer/yuv_viewer.py: -------------------------------------------------------------------------------- 1 | from PySide2.QtWidgets import QGraphicsScene, QFileDialog 2 | from components.customwidget import ImageView 3 | from components.status_code_enum import ImageToolError 4 | from components.window import SubWindow 5 | from components.histview import HistView 6 | from .ui.yuvviewer_window import Ui_YUVEditor 7 | from components.BasicImage import ImageBasic 8 | from .yuv_config import YUVConfig 9 | 10 | 11 | class YUVViewer(SubWindow): 12 | def __init__(self, name='YUVViewer', parent=None): 13 | super().__init__(name, parent, Ui_YUVEditor()) 14 | self.x = 0 15 | self.y = 0 16 | self.img = ImageBasic() 17 | self.hist_window = None 18 | self.img_index_str = '' 19 | self.config = YUVConfig() 20 | self.config.configUpdateEvent.connect( 21 | lambda: self.__init_img(self.img.imgpath)) # 配置界面参数更新 22 | 23 | self.scene = QGraphicsScene() 24 | self.imageview = ImageView(self.scene, parent) 25 | # 由于graphicsView被自定义了,需要重新定义一下UI,gridlayout还需要重新加一下widget 26 | self.ui.gridLayout.addWidget(self.imageview, 0, 1, 3, 1) 27 | self.imageview.sigDragEvent.connect(self.__init_img) 28 | self.imageview.sigMouseMovePoint.connect(self.show_point_rgb) 29 | self.imageview.sigWheelEvent.connect(self.__show_point_stats) 30 | self.ui.openimage.triggered.connect(self.on_open_img) 31 | self.ui.saveimage.triggered.connect(self.save_now_image) 32 | self.ui.actionstats.triggered.connect(self.on_calc_stats) 33 | self.ui.nextphoto.triggered.connect(self.switch_next_photo) 34 | self.ui.prephoto.triggered.connect(self.switch_pre_photo) 35 | self.imageview.rubberBandChanged.connect(self.update_stats_range) 36 | self.ui.deletephoto.triggered.connect(self.delete_photo) 37 | self.ui.rotateright.triggered.connect(self.rotate_photo) 38 | self.ui.yuvconfig.triggered.connect(self.config.show) # 配置UI显示 39 | 40 | def delete_photo(self): 41 | self.img.remove_image() 42 | next_photo, index, files_nums = self.img.find_next_time_photo(1) 43 | self.img_index_str = "({}/{})".format(index, files_nums - 1) 44 | self.__init_img(next_photo, self.img_index_str) 45 | 46 | def switch_next_photo(self): 47 | next_photo, index, files_nums = self.img.find_next_time_photo(1) 48 | self.img_index_str = "({}/{})".format(index + 1, files_nums) 49 | self.__init_img(next_photo, self.img_index_str) 50 | 51 | def switch_pre_photo(self): 52 | pre_photo, index, files_nums = self.img.find_next_time_photo(-1) 53 | self.img_index_str = "({}/{})".format(index + 1, files_nums) 54 | self.__init_img(pre_photo, self.img_index_str) 55 | 56 | def rotate_photo(self): 57 | try: 58 | self.img.rotate90() 59 | self.__display_img(self.img_index_str) 60 | if self.config.need_saveimg_in_rotate is True: 61 | self.img.save_image(self.img.imgpath) 62 | except ImageToolError as e: 63 | e.show() 64 | 65 | def on_open_img(self): 66 | imagepath = QFileDialog.getOpenFileName( 67 | None, '打开图片', self.img.get_dir(), "Images (*.jpg *.png *.bmp *.yuv)") 68 | if imagepath[0] != '': 69 | self.__init_img(imagepath[0]) 70 | 71 | def save_now_image(self): 72 | try: 73 | imagepath = QFileDialog.getSaveFileName( 74 | None, '保存图片', self.img.get_dir(), "Images (*.jpg)") 75 | if imagepath[0] != '': 76 | self.img.save_image(imagepath[0]) 77 | except ImageToolError as e: 78 | e.show() 79 | 80 | def __init_img(self, filename, indexstr=''): 81 | try: 82 | self.img.load_yuv_config( 83 | self.config.width, self.config.height, self.config.yuv_format) 84 | self.img.load_file(filename) 85 | self.__display_img(indexstr) 86 | except ImageToolError as e: 87 | e.show() 88 | 89 | def __display_img(self, indexstr=''): 90 | try: 91 | self.img.display_in_scene(self.scene) 92 | self.ui.photo_title.setTitle(indexstr + self.img.imgpath) 93 | if self.hist_window is not None and self.hist_window.enable is True: 94 | self.hist_window.update_rect_data(self.img.img, self.rect) 95 | except ImageToolError as e: 96 | e.show() 97 | 98 | def __show_point_stats(self): 99 | if self.img.img is not None: 100 | rgb = self.img.get_img_point(self.x, self.y) 101 | if (rgb is not None): 102 | scale_ratio = int(self.imageview.scale_ratio * 100) 103 | self.ui.statusBar.showMessage( 104 | "x:{},y:{} : R:{} G:{} B:{} 缩放比例:{}%".format(self.x, self.y, rgb[2], rgb[1], rgb[0], scale_ratio)) 105 | 106 | def update_stats_range(self, _, fromScenePoint, toScenePoint): 107 | if(toScenePoint.x() == 0 and toScenePoint.y() == 0 108 | and self.rect[2] > self.rect[0] and self.rect[3] > self.rect[1]): 109 | if self.hist_window is not None: 110 | self.hist_window.update_rect_data(self.img.img, self.rect) 111 | else: 112 | self.rect = [int(fromScenePoint.x()), int(fromScenePoint.y()), int( 113 | toScenePoint.x()), int(toScenePoint.y())] 114 | return 115 | 116 | def show_point_rgb(self, point): 117 | """func: 鼠标移动的回调""" 118 | self.x = int(point.x()) 119 | self.y = int(point.y()) 120 | self.__show_point_stats() 121 | 122 | def on_calc_stats(self): 123 | """打开统计信息的窗口""" 124 | if self.img.img is not None: 125 | self.hist_window = HistView(self.imageview) 126 | self.rect = [0, 0, self.img.img.shape[1], self.img.img.shape[0]] 127 | self.hist_window.update_rect_data(self.img.img, self.rect) 128 | self.hist_window.show() 129 | -------------------------------------------------------------------------------- /version.md: -------------------------------------------------------------------------------- 1 | **1.5.1** 2 | 3 | - 修复部分缺陷 4 | 5 | **1.5.0** 6 | 7 | - 增加在线升级的功能 8 | - 优化 RAW 图处理工具,增加 ISP 算法,优化 ISP pipeline,增强统计信息显示模块,优化多线程处理,优化算法运行报错提醒 9 | - 优化图片查看工具,增加切换图片,显示图片序号等功能 10 | 11 | **1.4.0** 12 | 13 | - 增加 RAW 图处理工具 14 | 15 | 可以查看和处理 RAW 图,已经实现了黑电平,坏点矫正,暗影矫正,去马赛克,白平衡,色彩校正,gamma,局部对比度增强,色度空间转换,对比度亮度调整,小波降噪 WNR,锐化等 ISP 算法,可以方便的进行后期算法的验证(热更新、直观的图像显示和分析) 16 | 17 | **1.3.1** 18 | 19 | - 解决不能创建备份文件的权限问题 20 | - 将比较成熟且常用的一些小工具单独发布 21 | 22 | - PQtools 转代码 23 | - 防抖测试工具 24 | - 视频对比工具 25 | - 景深计算工具 26 | 27 | 其中 Imagetools 包含了所有的工具以及说明文档,体积会大一点 28 | 29 | **1.3.0** 30 | 31 | - 优化了软件的使用细节 32 | - 增加了 pqtools 转头文件的工具 33 | 34 | **1.2.0** 35 | 36 | - 初始版本 37 | --------------------------------------------------------------------------------