├── CONTRIBUTING.md ├── LICENSE ├── PixelView ├── __init__.py ├── __main__.py ├── cli.py ├── config │ ├── __init__.py │ └── configManager.py ├── gui │ ├── __init__.py │ ├── centralWidgets │ │ ├── __init__.py │ │ ├── compare.py │ │ └── view.py │ ├── loadingIndicators │ │ ├── __init__.py │ │ ├── abstractLoadingIndicator.py │ │ ├── blink.py │ │ ├── common.py │ │ ├── fourCorners.py │ │ └── line.py │ └── mainWindow.py ├── imageContainers │ ├── __init__.py │ ├── abstractImage.py │ ├── common.py │ ├── rgb888Image.py │ └── rgba8888Image.py ├── topLevel.py └── utils │ ├── __init__.py │ ├── cli.py │ ├── image.py │ ├── other.py │ └── threading.py ├── README.md └── setup.py /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to PixelView 2 | 3 | Want to hack on the PixelView Project? Awesome! 4 | We only require you to sign your work, the below section describes this! 5 | 6 | ## Sign your work 7 | 8 | The sign-off is a simple line at the end of the explanation for the patch. Your 9 | signature certifies that you wrote the patch or otherwise have the right to pass 10 | it on as an open-source patch. The rules are pretty simple: if you can certify 11 | the below (from [developercertificate.org](http://developercertificate.org)): 12 | 13 | ``` 14 | Developer Certificate of Origin 15 | Version 1.1 16 | 17 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 18 | 1 Letterman Drive 19 | Suite D4700 20 | San Francisco, CA, 94129 21 | 22 | Everyone is permitted to copy and distribute verbatim copies of this 23 | license document, but changing it is not allowed. 24 | 25 | Developer's Certificate of Origin 1.1 26 | 27 | By making a contribution to this project, I certify that: 28 | 29 | (a) The contribution was created in whole or in part by me and I 30 | have the right to submit it under the open source license 31 | indicated in the file; or 32 | 33 | (b) The contribution is based upon previous work that, to the best 34 | of my knowledge, is covered under an appropriate open source 35 | license and I have the right under that license to submit that 36 | work with modifications, whether created in whole or in part 37 | by me, under the same open source license (unless I am 38 | permitted to submit under a different license), as indicated 39 | in the file; or 40 | 41 | (c) The contribution was provided directly to me by some other 42 | person who certified (a), (b) or (c) and I have not modified 43 | it. 44 | 45 | (d) I understand and agree that this project and the contribution 46 | are public and that a record of the contribution (including all 47 | personal information I submit with it, including my sign-off) is 48 | maintained indefinitely and may be redistributed consistent with 49 | this project or the open source license(s) involved. 50 | ``` 51 | 52 | Then you just add a line to every git commit message: 53 | 54 | Signed-off-by: Joe Smith 55 | 56 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 57 | 58 | If you set your `user.name` and `user.email` git configs, you can sign your 59 | commit automatically with `git commit -s`. 60 | 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 41 | 42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | -------------------------------------------------------------------------------- /PixelView/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from PixelView.cli import run 17 | run() 18 | -------------------------------------------------------------------------------- /PixelView/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import argparse 17 | from PixelView import topLevel 18 | from PixelView.utils.cli import paramList, handleCli 19 | from PixelView.imageContainers.common import Geometry 20 | from PixelView.config.configManager import ConfigManager 21 | 22 | 23 | def subCommands(subparsers): 24 | command = 'version' 25 | subparser = subparsers.add_parser(command) 26 | subparser.set_defaults(command=command) 27 | 28 | command = 'info' 29 | subparser = subparsers.add_parser(command) 30 | subparser.set_defaults(command=command) 31 | subparser.add_argument('filePath', help='Image File Path') 32 | 33 | command = 'printVal' 34 | subparser = subparsers.add_parser(command) 35 | subparser.set_defaults(command=command) 36 | subparser.add_argument('filePath', help='Image File Path') 37 | subparser.add_argument('x', help='X coordinate', type=int) 38 | subparser.add_argument('y', help='Y coordinate', type=int) 39 | 40 | command = 'genCanvas' 41 | subparser = subparsers.add_parser(command) 42 | subparser.set_defaults(command=command) 43 | subparser.add_argument('outFilePath', help='Output File Path') 44 | subparser.add_argument('red', help='Red component [0-255]', type=int) 45 | subparser.add_argument('green', help='Green component [0-255]', type=int) 46 | subparser.add_argument('blue', help='Blue component [0-255]', type=int) 47 | subparser.add_argument('--alpha', help='Alpha component [0-255]', type=int) 48 | subparser.add_argument('--width', help='Surface Area Width', type=int, default='320') 49 | subparser.add_argument('--height', help='Surface Area Height', type=int, default='240') 50 | 51 | command = 'genConfig' 52 | subparser = subparsers.add_parser(command) 53 | subparser.set_defaults(command=command) 54 | subparser.add_argument('dirPath', help='Path to directory that will contain the config files\n' 55 | 'If it does not exists, it is created') 56 | command = 'setConfigStart' 57 | subparser = subparsers.add_parser(command) 58 | subparser.set_defaults(command=command) 59 | subparser.add_argument('filePath', help='Path to the configMenu File') 60 | 61 | command = 'clearConfigStart' 62 | subparser = subparsers.add_parser(command) 63 | subparser.set_defaults(command=command) 64 | 65 | command = 'view' 66 | subparser = subparsers.add_parser(command, formatter_class=argparse.RawTextHelpFormatter) 67 | subparser.set_defaults(command=command) 68 | subparser.set_defaults(fListVarNameList=['filePathList']) 69 | subparser.add_argument('filePathList', 70 | help='This argument can be either:\n' 71 | '- The file path for the image\n' 72 | '- A commaseparated list of file paths for the images\n' 73 | '- A path of a file that contains file paths for the images (with --fList flag)', 74 | type=paramList) 75 | subparser.add_argument('--fList', action='store_true', 76 | help='If present, any path provided is treated as a file that contains file paths to images') 77 | 78 | command = 'compare' 79 | subparser = subparsers.add_parser(command, formatter_class=argparse.RawTextHelpFormatter) 80 | subparser.set_defaults(command=command) 81 | subparser.set_defaults(fListVarNameList=['filePathList1', 'filePathList2']) 82 | subparser.add_argument('filePathList1', 83 | help='This argument can be either:\n' 84 | '- The file path for the image\n' 85 | '- A commaseparated list of file paths for the images\n' 86 | '- A path of a file that contains file paths for the images (with --fList flag)', 87 | type=paramList) 88 | subparser.add_argument('filePathList2', 89 | help='Same as filePathList1, but for the second image (or second set of images)', 90 | type=paramList) 91 | subparser.add_argument('--fList', action='store_true', 92 | help='If present, any path provided is treated as a file that contains file paths to images') 93 | subparser.add_argument('--geometry1', help='The area within the image to compare, of the form: x++', type=Geometry) 94 | subparser.add_argument('--geometry2', help='The area within the image to compare, of the form: x++', type=Geometry) 95 | 96 | 97 | def run(): 98 | parser = argparse.ArgumentParser(prog='PixelView', formatter_class=argparse.RawTextHelpFormatter) 99 | subparsers = parser.add_subparsers(dest='command') 100 | subparsers.required = True 101 | parser.add_argument('-v', '--verbose', help='Verbose', action='store_true') 102 | parser.add_argument('--cf', help='Config File Path', dest='configFilePath') 103 | parser.add_argument('--cn', help='Config Name', dest='configName') 104 | parser.add_argument('--useInternalDefaults', help='Config Name', dest='isUseInternalDefaults', action='store_true') 105 | subCommands(subparsers) 106 | args = parser.parse_args() 107 | handleCli(args, topLevel, ConfigManager) 108 | -------------------------------------------------------------------------------- /PixelView/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/config/configManager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import os 17 | import pUtils 18 | from importlib import import_module 19 | from PixelView.utils.cli import pprint, COLOR 20 | from PixelView.gui.loadingIndicators.common import SHAPE, DIRECTION 21 | 22 | 23 | class ConfigManager: 24 | """ 25 | The ConfigManager takes care of all settings/configurations throughout the 26 | life of the application. 27 | When the app requires to know the current value of a given setting/config, 28 | it just asks the ConfigManager through the 'get' functions on the bottom 29 | part of this file. 30 | Any and all logic to figure out or pre-process any settings/config values 31 | is centralized and encapsulated here. 32 | """ 33 | 34 | def __init__(self, configFilePath, configName, isUseInternalDefaults, verbose, **kwargs): 35 | self.configStartPath = os.path.join(os.path.dirname(os.path.abspath(os.path.realpath(__file__))), 'nvidiaPixelViewConfigStart.cfg') 36 | self.isUseInternaldefaults = isUseInternalDefaults 37 | self.verbose = verbose 38 | self.configData = {} 39 | self.configFilePath = None 40 | if self.isUseInternaldefaults is True: return 41 | 42 | self.determineConfigFilePath(configFilePath, configName) 43 | self.load() 44 | 45 | def determineConfigFilePath(self, configFilePath, configName): 46 | """ 47 | ConfigManager determines what config file to use in the 48 | following way: 49 | 50 | 1) If configFilePath is not None uses that and returns. 51 | 52 | 2) If configStart.txt exists: 53 | 2.1) If configName is not None then: 54 | - ConfigManager loads 'configStart.txt' from the same directory as this file. 55 | 'configStart.txt' points to a config menu file 56 | - ConfigManager loads the config menu file 57 | The config menu file is a dictionary of 'configName: path' pairs 58 | - ConfigManager uses configName with the dictionary and gets the path 59 | 60 | 2.2) if configName is None, is then the same as in #2.1 except that 61 | defaultConfigName (from the config menu file) is used as configName. 62 | 63 | 3) Otherwise no configFile would be load 64 | """ 65 | 66 | if configFilePath is not None: 67 | self.configFilePath = configFilePath 68 | if not os.path.isabs(self.configFilePath): 69 | self.configFilePath = os.path.join(os.path.dirname(self.configStartPath), self.configFilePath) 70 | return 71 | 72 | try: 73 | self.configMenuPath = pUtils.quickFileRead(self.configStartPath, 'txt')[0] 74 | except Exception: 75 | if self.verbose: 76 | pprint('---------------------------------') 77 | pprint('Info: ', color=COLOR.TEAL, endLine=False); pprint('Unable to load:') 78 | pprint(' %s' % self.configStartPath, color=COLOR.TEAL) 79 | pprint('Internal defaults will be used') 80 | pprint('---------------------------------') 81 | return 82 | 83 | if not os.path.isabs(self.configMenuPath): 84 | self.configMenuPath = os.path.join(os.path.dirname(self.configStartPath), self.configMenuPath) 85 | 86 | try: 87 | configMenuData = pUtils.quickFileRead(self.configMenuPath, 'json') 88 | except Exception: 89 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('Unable to load json file:') 90 | pprint(' ' + self.configMenuPath, color=COLOR.TEAL) 91 | exit(1) 92 | 93 | if configName is None: 94 | try: 95 | defaultConfigName = configMenuData['defaultConfigName'] 96 | except Exception: 97 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('On file:') 98 | pprint(' ' + self.configMenuPath, color=COLOR.TEAL) 99 | pprint('Key: "', endLine=False); pprint('defaultConfigName', endLine=False, color=COLOR.TEAL); pprint('" not found') 100 | exit(1) 101 | configName = defaultConfigName 102 | 103 | try: 104 | self.configFilePathFromMenu = configMenuData['configFilePathDict'][configName] 105 | except Exception: 106 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('On file:') 107 | pprint(' ' + self.configMenuPath, color=COLOR.TEAL) 108 | pprint('Key sequence: "', endLine=False); pprint('configFilePathDict ' + configName, endLine=False, color=COLOR.TEAL); pprint('" not found') 109 | exit(1) 110 | 111 | self.configFilePath = self.configFilePathFromMenu 112 | if not os.path.isabs(self.configFilePath): 113 | self.configFilePath = os.path.join(os.path.dirname(self.configMenuPath), self.configFilePath) 114 | 115 | def load(self): 116 | if self.configFilePath is None: return 1 117 | try: 118 | self.configData = pUtils.quickFileRead(self.configFilePath, 'json') 119 | except Exception: 120 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('Unable to load json file:') 121 | pprint(' ' + self.configFilePath, color=COLOR.TEAL) 122 | exit(1) 123 | return 0 124 | 125 | def dump(self, filePath): 126 | data = {'configData': self.configData, 127 | 'metadata': {'configStartPath': self.configStartPath, 128 | 'configMenuPath': self.configMenuPath, 129 | 'configFilePathFromMenu': self.configFilePathFromMenu, 130 | 'configFilePath': self.configFilePath}} 131 | pUtils.quickFileWrite(filePath, data, 'json') 132 | 133 | def saveFullConfig(self, filePath): 134 | if os.path.exists(filePath): return 1 135 | 136 | defaultSettingFuncList = [ 137 | 'getPropDx', 138 | 'getPropHollowColor', 139 | 'getLoadingIndicatorDirection', 140 | 'getDumpFileName', 141 | 'getPropFillColor', 142 | 'getLoadingIndicatorClass', 143 | 'getPropShape', 144 | 'getNullColor', 145 | 'getDeltaImageColorDict', 146 | 'getPropDy', 147 | 'getMarkerColor', 148 | 'getLoadingIndicatorRefreshRate', 149 | ] 150 | 151 | for funcName in defaultSettingFuncList: 152 | func = getattr(self, funcName) 153 | func() 154 | pUtils.quickFileWrite(filePath, self.configData, 'json') 155 | return 0 156 | 157 | def genMenuConfigFile(self, filePath, configName, configPath): 158 | if os.path.exists(filePath): return 1 159 | 160 | configMenuData = {'defaultConfigName': configName, 161 | 'configFilePathDict': {configName: configPath}} 162 | pUtils.quickFileWrite(filePath, configMenuData, 'json') 163 | return 0 164 | 165 | def setConfigStart(self, filePath): 166 | if not os.path.exists(filePath): 167 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('File:') 168 | pprint(' ' + filePath, color=COLOR.TEAL) 169 | pprint('Does not exists.') 170 | return 1 171 | 172 | if os.path.exists(self.configStartPath): 173 | pprint('Warning: ', color=COLOR.RED, endLine=False); pprint('File:') 174 | pprint(' ' + self.configStartPath, color=COLOR.TEAL) 175 | pprint('Will be overwritten.') 176 | promptString = 'Proceed (y/n)? ' 177 | if input(promptString) != 'y': 178 | pprint('Aborted action') 179 | return 2 180 | 181 | pUtils.quickFileWrite(self.configStartPath, os.path.abspath(os.path.realpath(filePath))) 182 | pprint('DONE', color=COLOR.TEAL) 183 | return 0 184 | 185 | def clearConfigStart(self): 186 | if not os.path.exists(self.configStartPath): 187 | pprint('Info: ', color=COLOR.TEAL, endLine=False); pprint('Already clear') 188 | return 1 189 | 190 | pprint('Warning: ', color=COLOR.RED, endLine=False); pprint('File:') 191 | pprint(' ' + self.configStartPath, color=COLOR.TEAL) 192 | pprint('Will be deleted.') 193 | promptString = 'Proceed (y/n)? ' 194 | if input(promptString) != 'y': 195 | pprint('Aborted action') 196 | return 2 197 | 198 | try: 199 | os.remove(self.configStartPath) 200 | except Exception: 201 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('Unable to clear configStart') 202 | return 3 203 | 204 | pprint('DONE', color=COLOR.TEAL) 205 | return 0 206 | 207 | def getter(self, key, default): 208 | self.configData[key] = self.configData.get(key, default) 209 | return self.configData[key] 210 | 211 | # Get functions 212 | def getDeltaImageColorDict(self): 213 | return self.getter('deltaImageColor', 214 | {'0': [0x00, 0x00, 0x00], 215 | '1': [0x00, 0xFF, 0x00], 216 | '2': [0x00, 0x00, 0xFF], 217 | 'default': [0xFF, 0xFF, 0xFF]}) 218 | 219 | def getDeltaImageColor(self, deltaValue): 220 | t = self.getDeltaImageColorDict() 221 | return t.get(str(deltaValue), t['default']) 222 | 223 | def getDumpFileName(self): 224 | return self.getter('dumpFileName', 'dump.json') 225 | 226 | def getMarkerColor(self): 227 | return self.getter('markerColor', [0xFF, 0x00, 0x00]) 228 | 229 | def getNullColor(self): 230 | return self.getter('nullColor', [0xFF, 0x00, 0xFF]) 231 | 232 | def getLoadingIndicatorClass(self): 233 | moduleName, objName = self.getter('loadingIndicator', ['PixelView.gui.loadingIndicators.line', 'Line']) 234 | module = import_module(moduleName, 'PixelView.gui.loadingIndicators') 235 | obj = getattr(module, objName) 236 | return obj 237 | 238 | def getLoadingIndicatorRefreshRate(self): 239 | return self.getter('refreshRate', 100) 240 | 241 | def getLoadingIndicatorDirection(self): 242 | default = 3 243 | try: 244 | t = DIRECTION(self.getter('loadingIndicatorDirection', default)) 245 | except Exception: 246 | t = DIRECTION(default) 247 | return t 248 | 249 | def getPropDx(self): 250 | return self.getter('propDx', 15) 251 | 252 | def getPropDy(self): 253 | return self.getter('propDy', 15) 254 | 255 | def getPropFillColor(self): 256 | return self.getter('propFillColor', [0, 0, 255, 255]) 257 | 258 | def getPropHollowColor(self): 259 | return self.getter('propHollowColor', [150, 150, 150, 150]) 260 | 261 | def getPropShape(self): 262 | default = 1 263 | try: 264 | t = SHAPE(self.getter('propShape', default)) 265 | except Exception: 266 | t = SHAPE(default) 267 | return t 268 | -------------------------------------------------------------------------------- /PixelView/gui/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/gui/centralWidgets/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/gui/centralWidgets/compare.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from copy import deepcopy 17 | from PySide2.QtCore import Qt 18 | from PySide2.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QFrame, QMessageBox 19 | from PixelView.utils.other import truncateString 20 | from PixelView.utils.threading import OneShotThread 21 | from PixelView.utils.image import loadImage, widgetDisplayImage, getAlphaImage 22 | from PixelView.imageContainers.common import COMPARE_TYPE 23 | from PixelView.imageContainers.rgb888Image import Rgb888Image 24 | 25 | 26 | class Compare(QWidget): 27 | def __init__(self, configManager, geometry1=None, geometry2=None, parent=None, **kwargs): 28 | super(Compare, self).__init__(parent) 29 | 30 | self.cm = configManager 31 | self.geometry1 = geometry1 32 | self.geometry2 = geometry2 33 | 34 | self.initVars() 35 | self.initLayout() 36 | self.initLoadingIndicator() 37 | 38 | def initVars(self): 39 | self.pixelIndex1 = 0 40 | self.pixelIndex2 = 0 41 | self.pixelDiffIndex = -1 42 | 43 | def initPixelInfoLayout(self): 44 | layout = QGridLayout() 45 | layout.setColumnStretch(3, 9) 46 | layout.setVerticalSpacing(0) 47 | layout.setHorizontalSpacing(20) 48 | 49 | self.pixelXY1Label = QLabel() 50 | self.pixelXY2Label = QLabel() 51 | self.pixelIndex1Label = QLabel() 52 | self.pixelIndex2Label = QLabel() 53 | self.pixelColor1Label = QLabel() 54 | self.pixelColor2Label = QLabel() 55 | 56 | layout.addWidget(self.pixelXY1Label, 0, 0, alignment=Qt.AlignLeft | Qt.AlignTop) 57 | layout.addWidget(self.pixelXY2Label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignTop) 58 | layout.addWidget(self.pixelIndex1Label, 0, 1, alignment=Qt.AlignLeft | Qt.AlignTop) 59 | layout.addWidget(self.pixelIndex2Label, 1, 1, alignment=Qt.AlignLeft | Qt.AlignTop) 60 | layout.addWidget(self.pixelColor1Label, 0, 2, alignment=Qt.AlignLeft | Qt.AlignTop) 61 | layout.addWidget(self.pixelColor2Label, 1, 2, alignment=Qt.AlignLeft | Qt.AlignTop) 62 | 63 | return layout 64 | 65 | def initDifferencesInfoLayout(self): 66 | layout = QGridLayout() 67 | self.differentPixelsTotalLabel = QLabel() 68 | layout.addWidget(self.differentPixelsTotalLabel, 0, 0, alignment=Qt.AlignRight | Qt.AlignBottom) 69 | return layout 70 | 71 | def initInfoLayout(self): 72 | layout = QVBoxLayout() 73 | 74 | headerLayout = QHBoxLayout() 75 | self.counterLabel = QLabel() 76 | headerLayout.addWidget(self.counterLabel, alignment=Qt.AlignHCenter | Qt.AlignTop) 77 | 78 | bodyLayout = QHBoxLayout() 79 | bodyLayout.addLayout(self.initPixelInfoLayout()) 80 | bodyLayout.addLayout(self.initDifferencesInfoLayout()) 81 | 82 | layout.addLayout(headerLayout) 83 | layout.addLayout(bodyLayout) 84 | return layout 85 | 86 | def initImagesLayout(self): 87 | def initSubLayout(imageLabel, imagePathLabel): 88 | subLayout = QGridLayout() 89 | subLayout.setColumnStretch(1, 9) 90 | subLayout.addWidget(imageLabel, 0, 0) 91 | subLayout.addWidget(imagePathLabel, 1, 0, 1, 2, alignment=Qt.AlignRight | Qt.AlignTop) 92 | return subLayout 93 | 94 | layout = QGridLayout() 95 | self.imageLabelList = [] 96 | for i in range(6): 97 | tLabel = QLabel() 98 | tLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) 99 | self.imageLabelList.append(tLabel) 100 | 101 | self.imagePath1Label = QLabel() 102 | self.imagePath2Label = QLabel() 103 | self.differentPixelsRgbLabel = QLabel() 104 | self.differentPixelsAlphaLabel = QLabel() 105 | 106 | subLayout = initSubLayout(self.imageLabelList[0], self.imagePath1Label) 107 | layout.addLayout(subLayout, 0, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) 108 | 109 | subLayout = initSubLayout(self.imageLabelList[1], self.imagePath2Label) 110 | layout.addLayout(subLayout, 0, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) 111 | 112 | subLayout = initSubLayout(self.imageLabelList[2], self.differentPixelsRgbLabel) 113 | layout.addLayout(subLayout, 0, 2, alignment=Qt.AlignHCenter | Qt.AlignTop) 114 | 115 | layout.addWidget(self.imageLabelList[3], 1, 0, alignment=Qt.AlignHCenter | Qt.AlignTop) 116 | 117 | layout.addWidget(self.imageLabelList[4], 1, 1, alignment=Qt.AlignHCenter | Qt.AlignTop) 118 | 119 | subLayout = initSubLayout(self.imageLabelList[5], self.differentPixelsAlphaLabel) 120 | layout.addLayout(subLayout, 1, 2, alignment=Qt.AlignHCenter | Qt.AlignTop) 121 | 122 | return layout 123 | 124 | def initLayout(self): 125 | layout = QVBoxLayout() 126 | layout.addLayout(self.initInfoLayout()) 127 | layout.addLayout(self.initImagesLayout()) 128 | self.setLayout(layout) 129 | 130 | def initLoadingIndicator(self): 131 | self.loadingIndicator = self.cm.getLoadingIndicatorClass()(parent=self, configManager=self.cm) 132 | 133 | def pixelIndexToXY(self, pixelIndex, bytesPerPixel, width): 134 | t = pixelIndex / bytesPerPixel 135 | y = int(t / width) 136 | x = int(t % width) 137 | return (x, y) 138 | 139 | def updateMarker(self): 140 | t = deepcopy(self.diffData['deltaImageRgbData']) 141 | img3 = Rgb888Image(*t) 142 | 143 | ### Calculate the pixel index for the subImage ### 144 | x, y = self.pixelIndexToXY(self.pixelIndex1, self.img1.bytesPerPixel, self.img1.width) 145 | x0 = x - self.geometry1.x 146 | y0 = y - self.geometry1.y 147 | indexTmp = (self.geometry1.width * y0 + x0) * 3 148 | ################################################## 149 | 150 | img3.data[indexTmp: indexTmp + 3] = self.cm.getMarkerColor() 151 | widgetDisplayImage(self.imageLabelList[2], img3) 152 | 153 | def updateInfo(self): 154 | def genHexFormatString(bytesPerPixel): 155 | return '[%02X' + (':%02X' * (bytesPerPixel - 1)) + ']' 156 | 157 | self.counterLabel.setText('%i of %i' % (self.index + 1, self.totalImageSets)) 158 | 159 | ### Pixel Info ### 160 | if self.pixelDiffIndex == -1: 161 | pixelXY1String = 'PixelXY1: ' + ' ' 162 | pixelXY2String = 'PixelXY2: ' + ' ' 163 | pixelIndex1String = 'PixelIndex1: ' + ' ' 164 | pixelIndex2String = 'PixelIndex2: ' + ' ' 165 | pixelColor1String = 'PixelColor1: ' + ' ' 166 | pixelColor2String = 'PixelColor1: ' + ' ' 167 | else: 168 | pixel1Data = self.img1.data[self.pixelIndex1:self.pixelIndex1 + self.img1.bytesPerPixel] 169 | pixel2Data = self.img2.data[self.pixelIndex2:self.pixelIndex2 + self.img2.bytesPerPixel] 170 | 171 | pixelXY1String = 'PixelXY1: ' + '%i,%i' % self.pixelIndexToXY(self.pixelIndex1, self.img1.bytesPerPixel, self.img1.width) 172 | pixelXY2String = 'PixelXY2: ' + '%i,%i' % self.pixelIndexToXY(self.pixelIndex2, self.img2.bytesPerPixel, self.img2.width) 173 | pixelIndex1String = 'PixelIndex1: ' + str(self.pixelIndex1) + ' (in bytes)' 174 | pixelIndex2String = 'PixelIndex2: ' + str(self.pixelIndex2) + ' (in bytes)' 175 | pixelColor1String = 'PixelColor1: ' + genHexFormatString(self.img1.bytesPerPixel) % tuple(pixel1Data) 176 | pixelColor2String = 'PixelColor2: ' + genHexFormatString(self.img2.bytesPerPixel) % tuple(pixel2Data) 177 | 178 | self.pixelXY1Label.setText(pixelXY1String) 179 | self.pixelXY2Label.setText(pixelXY2String) 180 | self.pixelIndex1Label.setText(pixelIndex1String) 181 | self.pixelIndex2Label.setText(pixelIndex2String) 182 | self.pixelColor1Label.setText(pixelColor1String) 183 | self.pixelColor2Label.setText(pixelColor2String) 184 | ################## 185 | 186 | ### Different Pixels Counts ### 187 | differentPixelsTotal = self.diffData.get('pixelDiffCount') 188 | differentPixelsRgb = self.diffData.get('diffPixelRgbList') 189 | differentPixelsAlpha = self.diffData.get('diffPixelAlphaList') 190 | differentPixelsTotalString = str(differentPixelsTotal) if differentPixelsTotal is not None else 'UNAVAILABLE' 191 | differentPixelsRgbString = str(len(differentPixelsRgb)) if differentPixelsRgb is not None else 'UNAVAILABLE' 192 | differentPixelsAlphaString = str(len(differentPixelsAlpha)) if differentPixelsAlpha is not None else 'UNAVAILABLE' 193 | self.differentPixelsTotalLabel.setText('Different Pixels Total: ' + differentPixelsTotalString) 194 | self.differentPixelsRgbLabel.setText( 'Different Pixels (RGB): ' + differentPixelsRgbString) 195 | self.differentPixelsAlphaLabel.setText('Different Pixels (Alpha): ' + differentPixelsAlphaString) 196 | ############################### 197 | 198 | ### Image Paths ### 199 | self.imagePath1Label.setText(truncateString(self.imagePath1, self.img1.width)) 200 | self.imagePath2Label.setText(truncateString(self.imagePath2, self.img2.width)) 201 | ################### 202 | 203 | def nextDiffPixel(self): 204 | t = self.diffData.get('diffPixelRgbList') 205 | if t is None or len(t) == 0: return 206 | 207 | self.pixelDiffIndex += 1 208 | self.pixelDiffIndex = min(self.pixelDiffIndex, len(self.diffData['diffPixelRgbList']) - 1) 209 | 210 | self.pixelIndex1 = self.diffData['diffPixelRgbList'][self.pixelDiffIndex][0] 211 | self.pixelIndex2 = self.diffData['diffPixelRgbList'][self.pixelDiffIndex][1] 212 | 213 | self.updateInfo() 214 | self.updateMarker() 215 | 216 | def prevDiffPixel(self): 217 | t = self.diffData.get('diffPixelRgbList') 218 | if t is None or len(t) == 0: return 219 | 220 | self.pixelDiffIndex -= 1 221 | self.pixelDiffIndex = max(self.pixelDiffIndex, 0) 222 | 223 | self.pixelIndex1 = self.diffData['diffPixelRgbList'][self.pixelDiffIndex][0] 224 | self.pixelIndex2 = self.diffData['diffPixelRgbList'][self.pixelDiffIndex][1] 225 | 226 | self.updateInfo() 227 | self.updateMarker() 228 | 229 | def loading(self): 230 | nullImageData = None 231 | 232 | def genNullImageData(refImg): 233 | if nullImageData: return nullImageData 234 | return [bytearray(self.cm.getNullColor() * refImg.width * refImg.height), refImg.width, refImg.height] 235 | 236 | img1 = loadImage(self.imagePath1, self.cm.getNullColor()) 237 | img2 = loadImage(self.imagePath2, self.cm.getNullColor()) 238 | 239 | nullImageData1 = genNullImageData(img1) 240 | nullImageData2 = genNullImageData(img2) 241 | img4 = getAlphaImage(img1) 242 | img5 = getAlphaImage(img2) 243 | if img4 is None: img4 = nullImageData1 244 | if img5 is None: img5 = nullImageData2 245 | 246 | if img1.srcFileFormat == 'nullImage' or img2.srcFileFormat == 'nullImage': 247 | data = {} 248 | img3 = None 249 | img6 = None 250 | else: 251 | data = img1.getDiff(img2, compareType=COMPARE_TYPE.FULL, 252 | returnFailPixelList=True, colorDict=self.cm.getDeltaImageColorDict(), 253 | geometry1=self.geometry1, geometry2=self.geometry2) 254 | 255 | self.geometry1 = data.get('geometry1', self.geometry1) 256 | self.geometry2 = data.get('geometry2', self.geometry2) 257 | 258 | img3 = Rgb888Image(*data.get('deltaImageRgbData', nullImageData1)) 259 | img6 = Rgb888Image(*data.get('deltaImageAlphaData', nullImageData1)) 260 | 261 | returnData = dict(img1=img1, 262 | img2=img2, 263 | img3=img3, 264 | img4=img4, 265 | img5=img5, 266 | img6=img6, 267 | diffData=data) 268 | return returnData 269 | 270 | def draw(self, imagePath1, imagePath2, index, totalImageSets, **kwargs): 271 | self.imagePath1 = imagePath1 272 | self.imagePath2 = imagePath2 273 | self.index = index 274 | self.totalImageSets = totalImageSets 275 | 276 | self.loadingThread = OneShotThread(oneShotFunc=self.loading) 277 | self.loadingThread.start() 278 | 279 | self.loadingIndicator.start(isDoneFunc=lambda: not self.loadingThread.isAlive(), 280 | postFunc=self.drawPart2) 281 | 282 | def drawPart2(self): 283 | data = self.loadingThread.returnData 284 | 285 | self.initVars() 286 | self.img1 = data.get('img1') 287 | self.img2 = data.get('img2') 288 | self.diffData = data.get('diffData') 289 | 290 | img3 = data.get('img3') 291 | img4 = data.get('img4') 292 | img5 = data.get('img5') 293 | img6 = data.get('img6') 294 | 295 | self.updateInfo() 296 | widgetDisplayImage(self.imageLabelList[0], self.img1) 297 | widgetDisplayImage(self.imageLabelList[1], self.img2) 298 | widgetDisplayImage(self.imageLabelList[3], img4) 299 | widgetDisplayImage(self.imageLabelList[4], img5) 300 | 301 | widgetList = [ 302 | self.imageLabelList[2], 303 | self.imageLabelList[5], 304 | self.differentPixelsTotalLabel, 305 | self.differentPixelsRgbLabel, 306 | self.differentPixelsAlphaLabel, 307 | ] 308 | 309 | if img3 is None or img6 is None: 310 | for widget in widgetList: 311 | func = getattr(widget, 'hide') 312 | func() 313 | else: 314 | widgetDisplayImage(self.imageLabelList[2], img3) 315 | widgetDisplayImage(self.imageLabelList[5], img6) 316 | 317 | for widget in widgetList: 318 | func = getattr(widget, 'show') 319 | func() 320 | 321 | if self.img1.srcFileFormat == 'nullImage' or self.img2.srcFileFormat == 'nullImage': 322 | msgBox = QMessageBox(self) 323 | msgBox.setText('Unable to load the current image pair') 324 | msgBox.setInformativeText('Go to the next image pair to continue reviewing the images') 325 | msgBox.setIcon(QMessageBox.Warning) 326 | msgBox.setStandardButtons(QMessageBox.Ok) 327 | msgBox.exec_() 328 | -------------------------------------------------------------------------------- /PixelView/gui/centralWidgets/view.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PySide2.QtCore import Qt 16 | from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QFrame 17 | from PixelView.utils.other import truncateString 18 | from PixelView.utils.threading import OneShotThread 19 | from PixelView.utils.image import loadImage, widgetDisplayImage 20 | 21 | 22 | class View(QWidget): 23 | def __init__(self, configManager, parent=None, **kwargs): 24 | super(View, self).__init__(parent) 25 | self.cm = configManager 26 | self.initLayout() 27 | self.initLoadingIndicator() 28 | 29 | def initInfoLayout(self): 30 | layout = QVBoxLayout() 31 | self.counterLabel = QLabel() 32 | layout.addWidget(self.counterLabel, alignment=Qt.AlignHCenter | Qt.AlignTop) 33 | return layout 34 | 35 | def initLayout(self): 36 | layout = QVBoxLayout() 37 | layout.addLayout(self.initInfoLayout()) 38 | 39 | self.imageLabel = QLabel() 40 | self.imageLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) 41 | self.imagePathLabel = QLabel() 42 | imageLayout = QVBoxLayout() 43 | imageLayout.addWidget(self.imageLabel) 44 | imageLayout.addWidget(self.imagePathLabel, alignment=Qt.AlignRight | Qt.AlignTop) 45 | 46 | layout.addLayout(imageLayout) 47 | self.setLayout(layout) 48 | 49 | def initLoadingIndicator(self): 50 | self.loadingIndicator = self.cm.getLoadingIndicatorClass()(parent=self, configManager=self.cm) 51 | 52 | def updateInfo(self): 53 | self.counterLabel.setText('%i of %i' % (self.index + 1, self.totalImageSets)) 54 | self.imagePathLabel.setText(truncateString(self.imagePath, self.img.width)) 55 | 56 | def loading(self): 57 | img = loadImage(self.imagePath, self.cm.getNullColor()) 58 | returnData = dict(img=img) 59 | return returnData 60 | 61 | def draw(self, imagePath1, index, totalImageSets, **kwargs): 62 | self.imagePath = imagePath1 63 | self.index = index 64 | self.totalImageSets = totalImageSets 65 | 66 | self.loadingThread = OneShotThread(oneShotFunc=self.loading) 67 | self.loadingThread.start() 68 | 69 | self.loadingIndicator.start(isDoneFunc=lambda: not self.loadingThread.isAlive(), 70 | postFunc=self.drawPart2) 71 | 72 | def drawPart2(self): 73 | data = self.loadingThread.returnData 74 | self.img = data.get('img') 75 | 76 | self.updateInfo() 77 | widgetDisplayImage(self.imageLabel, data.get('img')) 78 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/abstractLoadingIndicator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from PySide2.QtWidgets import QWidget 17 | from PySide2.QtGui import QPainter, QBrush, QColor 18 | from PySide2.QtCore import QRect, QPoint, QSize, Qt 19 | from PixelView.utils.cli import pprint 20 | from PixelView.gui.loadingIndicators.common import SHAPE, DIRECTION 21 | 22 | 23 | class Prop: 24 | def __init__(self): 25 | self.dx = 0 26 | self.dy = 0 27 | self.space = 0 28 | self.fillBrush = None 29 | self.hollowBrush = None 30 | self.numSockets = 0 31 | self.shape = SHAPE.RECT 32 | self.drawFuncDict = { 33 | SHAPE.RECT: self.drawRect, 34 | SHAPE.ELLIPSE: self.drawEllipse, 35 | } 36 | 37 | def draw(self, painter, center, isFill): 38 | self.drawFuncDict.get(self.shape, self.drawDefault)(painter, center, isFill) 39 | 40 | def drawEllipse(self, painter, center, isFill): 41 | painter.setBrush(self.fillBrush if isFill else self.hollowBrush) 42 | painter.drawEllipse(center, self.dx, self.dy) 43 | 44 | def drawRect(self, painter, center, isFill): 45 | painter.fillRect(center.x() - self.dx, center.y() - self.dy, self.dx * 2, self.dy * 2, self.fillBrush if isFill else self.hollowBrush) 46 | 47 | def drawDefault(self, painter, center, isFill): 48 | self.drawRect(self, painter, center, isFill) 49 | 50 | 51 | class AbstractLoadingIndicator(QWidget): 52 | def __init__(self, parent=None, **kwargs): 53 | super(AbstractLoadingIndicator, self).__init__(parent) 54 | self.palette().setColor(self.palette().Background, Qt.transparent) 55 | self.initVars(parent, **kwargs) 56 | 57 | def initVars(self, parent, configManager, **kwargs): 58 | self.cm = configManager 59 | self.parent = parent 60 | 61 | self.counter = 0 62 | self.refreshRate = self.cm.getLoadingIndicatorRefreshRate() 63 | 64 | self.prop = Prop() 65 | self.prop.dx = self.cm.getPropDx() 66 | self.prop.dy = self.cm.getPropDy() 67 | self.prop.fillBrush = QBrush(QColor(*self.cm.getPropFillColor())) 68 | self.prop.hollowBrush = QBrush(QColor(*self.cm.getPropHollowColor())) 69 | self.prop.shape = self.cm.getPropShape() 70 | 71 | self.posList = [] 72 | self.spacing = QPoint(0, 0) 73 | self.direction = self.cm.getLoadingIndicatorDirection() 74 | self.tmpIncr = 1 75 | 76 | self.postFunc = None 77 | 78 | def paintEvent(self, event): 79 | painter = QPainter() 80 | painter.begin(self) 81 | 82 | center = QPoint(self.width() / 2, self.height() / 2) 83 | 84 | # Dim out the background 85 | painter.fillRect(self.rect(), QBrush(QColor(255, 255, 255, 160))) 86 | 87 | ### Add a black rectangle where the loading icon would be displayed ### 88 | startPos = QPoint(min([i.x() for i in self.posList]), 89 | min([i.y() for i in self.posList])) 90 | endPos = QPoint(max([i.x() for i in self.posList]), 91 | max([i.y() for i in self.posList])) 92 | delta = QPoint(endPos.x() - startPos.x(), 93 | endPos.y() - startPos.y()) 94 | 95 | marginX = 20 96 | marginY = 20 97 | origin = QPoint(center.x() + startPos.x() * self.spacing.x() - self.prop.dx - marginX, 98 | center.y() + startPos.y() * self.spacing.y() - self.prop.dy - marginY) 99 | size = QSize(delta.x() * self.spacing.x() + 2 * self.prop.dx + 2 * marginX, 100 | delta.y() * self.spacing.y() + 2 * self.prop.dy + 2 * marginY) 101 | 102 | painter.fillRect(QRect(origin, size), QColor(0, 0, 0, 255)) 103 | ####################################################################### 104 | 105 | for i in range(len(self.posList)): 106 | pos = self.posList[i] 107 | self.prop.draw(painter, 108 | QPoint(center.x() + pos.x() * self.spacing.x(), 109 | center.y() + pos.y() * self.spacing.y()), 110 | True if i == self.counter else False) 111 | 112 | painter.end() 113 | self.counterNext() 114 | 115 | def counterForward(self): 116 | self.counter += 1 117 | self.counter %= len(self.posList) 118 | return self.counter 119 | 120 | def counterBack(self): 121 | self.counter -= 1 122 | if self.counter < 0: 123 | self.counter = len(self.posList) - 1 124 | return self.counter 125 | 126 | def counterBackAndForth(self): 127 | self.counter += self.tmpIncr 128 | if self.counter < 0: 129 | self.counter = 1 130 | self.tmpIncr = 1 131 | elif self.counter >= len(self.posList): 132 | self.counter = len(self.posList) - 2 133 | self.tmpIncr = -1 134 | return self.counter 135 | 136 | def counterNext(self): 137 | if self.direction is DIRECTION.FORWARD: return self.counterForward() 138 | if self.direction is DIRECTION.BACKWARD: return self.counterBack() 139 | if self.direction is DIRECTION.BACK_AND_FORTH: return self.counterBackAndForth() 140 | 141 | # This should never be reached, but just for safety 142 | pprint('WARNING: direction "%s" not known' % str(self.direction)) 143 | return self.counterForward 144 | 145 | def isDoneFunc(self): 146 | return False 147 | 148 | def timerEvent(self, event): 149 | if self.isDoneFunc(): 150 | self.hide() 151 | self.postFunc() 152 | return 153 | self.update() 154 | 155 | def start(self, isDoneFunc, postFunc): 156 | self.isDoneFunc = isDoneFunc 157 | self.postFunc = postFunc 158 | self.show() 159 | 160 | def showEvent(self, event): 161 | self.resize(self.parent.width(), self.parent.height()) 162 | self.timer = self.startTimer(self.refreshRate) 163 | 164 | def hideEvent(self, event): 165 | self.killTimer(self.timer) 166 | self.counter = 0 167 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/blink.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from PySide2.QtCore import QPoint, QRect 17 | from PySide2.QtGui import QPainter, QBrush, QColor 18 | from .abstractLoadingIndicator import AbstractLoadingIndicator 19 | 20 | 21 | class Blink(AbstractLoadingIndicator): 22 | def __init__(self, parent=None, **kwargs): 23 | super().__init__(parent, **kwargs) 24 | 25 | def paintEvent(self, event): 26 | painter = QPainter() 27 | painter.begin(self) 28 | 29 | center = QPoint(self.width() / 2, self.height() / 2) 30 | painter.fillRect(self.rect(), QBrush(QColor(255, 255, 255, 160))) 31 | 32 | margin = 20 33 | painter.fillRect(QRect(center.x() - self.prop.dx - margin, 34 | center.y() - self.prop.dy - margin, 35 | (self.prop.dx + margin) * 2, 36 | (self.prop.dy + margin) * 2), 37 | QBrush(QColor(0, 0, 0, 255))) 38 | 39 | self.prop.draw(painter, 40 | QPoint(center.x(), 41 | center.y()), 42 | True if self.counter == 0 else False) 43 | 44 | painter.end() 45 | 46 | self.counter += 1 47 | self.counter %= 2 48 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import enum 17 | 18 | 19 | @enum.unique 20 | class SHAPE(enum.Enum): 21 | RECT = 1 22 | ELLIPSE = 2 23 | 24 | 25 | @enum.unique 26 | class DIRECTION(enum.Enum): 27 | FORWARD = 1 28 | BACKWARD = 2 29 | BACK_AND_FORTH = 3 30 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/fourCorners.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from PySide2.QtCore import QPoint 17 | from .abstractLoadingIndicator import AbstractLoadingIndicator 18 | 19 | 20 | class FourCorners(AbstractLoadingIndicator): 21 | def __init__(self, parent=None, **kwargs): 22 | super().__init__(parent, **kwargs) 23 | 24 | def initVars(self, parent, **kwargs): 25 | super().initVars(parent, **kwargs) 26 | 27 | self.posList = [QPoint(-1, -1), 28 | QPoint( 1, -1), 29 | QPoint( 1, 1), 30 | QPoint(-1, 1)] 31 | 32 | self.spacing = QPoint(2 * self.prop.dx + 5, 2 * self.prop.dy + 5) 33 | -------------------------------------------------------------------------------- /PixelView/gui/loadingIndicators/line.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from PySide2.QtCore import QPoint 17 | from .abstractLoadingIndicator import AbstractLoadingIndicator 18 | 19 | 20 | class Line(AbstractLoadingIndicator): 21 | def __init__(self, parent=None, **kwargs): 22 | super().__init__(parent, **kwargs) 23 | 24 | def initVars(self, parent, **kwargs): 25 | super().initVars(parent, **kwargs) 26 | 27 | self.posList = [ 28 | QPoint(-2, 0), 29 | QPoint(-1, 0), 30 | QPoint( 0, 0), 31 | QPoint( 1, 0), 32 | QPoint( 2, 0), 33 | ] 34 | 35 | self.spacing = QPoint(2 * self.prop.dx + 10, 2 * self.prop.dy + 10) 36 | -------------------------------------------------------------------------------- /PixelView/gui/mainWindow.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import os 17 | import enum 18 | import pUtils 19 | from PySide2.QtWidgets import QApplication, QMainWindow, QMenuBar, QAction, QMessageBox 20 | from PixelView.gui.centralWidgets.view import View 21 | from PixelView.gui.centralWidgets.compare import Compare 22 | 23 | 24 | @enum.unique 25 | class MAIN_WINDOW_MODE(enum.Enum): 26 | VIEW = 1 27 | COMPARE = 2 28 | 29 | 30 | class MainWindow(QMainWindow): 31 | def __init__(self, parent=None, **kwargs): 32 | super(MainWindow, self).__init__(parent) 33 | 34 | self.initVars(**kwargs) 35 | 36 | self.resize(200, 200) 37 | self.move(0, 0) 38 | self.setWindowTitle('PixelView') 39 | self.initMenuBar() 40 | self.selectCentralWidget() 41 | 42 | def initVars(self, mode, configManager, **kwargs): 43 | self.cm = configManager 44 | self.imagePathList1 = [] 45 | self.imagePathList2 = [] 46 | 47 | self.droppedList1 = [] 48 | self.droppedList2 = [] 49 | 50 | self.index = 0 51 | self.pixelDiffIndex = None 52 | self.pixelIndex1 = 0 53 | self.pixelIndex2 = 0 54 | 55 | self.mode = mode 56 | self.centralWidgetDict = { 57 | MAIN_WINDOW_MODE.VIEW: View(configManager=configManager, **kwargs), 58 | MAIN_WINDOW_MODE.COMPARE: Compare(configManager=configManager, **kwargs), 59 | } 60 | 61 | def initMenuBar(self, mode=None): 62 | if mode: 63 | self.mode = mode 64 | self.setMenuBar(self.createMenuBar()) 65 | 66 | def selectCentralWidget(self, mode=None): 67 | if mode: 68 | self.mode = mode 69 | self.setCentralWidget(self.centralWidgetDict[self.mode]) 70 | 71 | def dropImage(self): 72 | if len(self.imagePathList1) == 1: return 73 | self.droppedList1.append(self.imagePathList1.pop(self.index)) 74 | if self.imagePathList2: 75 | self.droppedList2.append(self.imagePathList2.pop(self.index)) 76 | if self.index >= len(self.imagePathList1): 77 | self.index = len(self.imagePathList1) - 1 78 | self.draw() 79 | 80 | def writeLists(self): 81 | def f(s): 82 | return os.path.abspath(os.path.realpath(s)) 83 | 84 | d = dict(imagePathList1=[f(i) for i in self.imagePathList1], 85 | imagePathList2=[f(i) for i in self.imagePathList2], 86 | droppedPathList1=[f(i) for i in self.droppedList1], 87 | droppedPathList2=[f(i) for i in self.droppedList2],) 88 | 89 | filePath = os.path.abspath(self.cm.getDumpFileName()) 90 | try: 91 | if not os.path.exists(filePath): 92 | pUtils.quickFileWrite(filePath, d, 'json') 93 | else: 94 | msgBox = QMessageBox(self) 95 | msgBox.setText('File:\n %s\nalready exists' % filePath) 96 | msgBox.setInformativeText('Do you want to overwrite it?') 97 | msgBox.setIcon(QMessageBox.Warning) 98 | msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) 99 | msgBox.setDefaultButton(QMessageBox.Cancel) 100 | ret = msgBox.exec_() 101 | if ret == QMessageBox.Yes: 102 | pUtils.quickFileWrite(filePath, d, 'json') 103 | except Exception: 104 | msgBox = QMessageBox(self) 105 | msgBox.setText('Unable to write file:\n %s' % filePath) 106 | msgBox.setInformativeText('Please make sure you have right access and space left') 107 | msgBox.setIcon(QMessageBox.Critical) 108 | msgBox.setStandardButtons(QMessageBox.Ok) 109 | msgBox.exec_() 110 | 111 | def prevImage(self): 112 | if self.index <= 0: return 113 | self.index = self.index - 1 114 | self.draw() 115 | 116 | def nextImage(self): 117 | if self.index >= len(self.imagePathList1) - 1: return 118 | self.index = self.index + 1 119 | self.draw() 120 | 121 | def createMenuBar(self): 122 | menuBar = QMenuBar(parent=None) 123 | 124 | # File Menu 125 | fileMenu = menuBar.addMenu('&File') 126 | 127 | dumpListsAction = QAction('Dump Lists', fileMenu) 128 | dumpListsAction.setShortcut('Ctrl+S') 129 | dumpListsAction.triggered.connect(self.writeLists) 130 | fileMenu.addAction(dumpListsAction) 131 | 132 | fileMenu.addSeparator() 133 | 134 | exitAction = QAction('E&xit', fileMenu) 135 | exitAction.setShortcut('Ctrl+Q') 136 | exitAction.triggered.connect(QApplication.closeAllWindows) 137 | fileMenu.addAction(exitAction) 138 | 139 | # View Menu 140 | viewMenu = menuBar.addMenu('V&iew') 141 | 142 | t = ' pair' if self.mode == MAIN_WINDOW_MODE.COMPARE else '' 143 | 144 | nextImageAction = QAction('next image' + t, viewMenu) 145 | nextImageAction.setShortcut('Ctrl+]') 146 | nextImageAction.triggered.connect(self.nextImage) 147 | viewMenu.addAction(nextImageAction) 148 | 149 | prevImageAction = QAction('prev image' + t, viewMenu) 150 | prevImageAction.setShortcut('Ctrl+[') 151 | prevImageAction.triggered.connect(self.prevImage) 152 | viewMenu.addAction(prevImageAction) 153 | 154 | viewMenu.addSeparator() 155 | 156 | dropImageAction = QAction('Drop image' + t, viewMenu) 157 | dropImageAction.setShortcut('Ctrl+D') 158 | dropImageAction.triggered.connect(self.dropImage) 159 | viewMenu.addAction(dropImageAction) 160 | 161 | if self.mode == MAIN_WINDOW_MODE.COMPARE: 162 | viewMenu.addSeparator() 163 | 164 | nextDiffPixelAction = QAction('next diff pixel', viewMenu) 165 | nextDiffPixelAction.setShortcut('Ctrl+.') 166 | nextDiffPixelAction.triggered.connect(self.centralWidgetDict[MAIN_WINDOW_MODE.COMPARE].nextDiffPixel) 167 | viewMenu.addAction(nextDiffPixelAction) 168 | 169 | prevDiffPixelAction = QAction('prev diff pixel', viewMenu) 170 | prevDiffPixelAction.setShortcut('Ctrl+,') 171 | prevDiffPixelAction.triggered.connect(self.centralWidgetDict[MAIN_WINDOW_MODE.COMPARE].prevDiffPixel) 172 | viewMenu.addAction(prevDiffPixelAction) 173 | return menuBar 174 | 175 | def draw(self): 176 | imagePath1 = self.imagePathList1[self.index] 177 | imagePath2 = '' 178 | if self.imagePathList2: imagePath2 = self.imagePathList2[self.index] 179 | self.centralWidget().draw(imagePath1=imagePath1, 180 | imagePath2=imagePath2, 181 | index=self.index, 182 | totalImageSets=len(self.imagePathList1)) 183 | 184 | 185 | def launch(configManager, imagePathList1, imagePathList2=[], mode=MAIN_WINDOW_MODE.VIEW, **kwargs): 186 | 187 | app = QApplication([]) 188 | 189 | pv = MainWindow(mode=mode, configManager=configManager, **kwargs) 190 | pv.imagePathList1 = imagePathList1 191 | pv.imagePathList2 = imagePathList2 192 | pv.draw() 193 | pv.show() 194 | 195 | app.exec_() 196 | -------------------------------------------------------------------------------- /PixelView/imageContainers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/imageContainers/abstractImage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import os 17 | from copy import deepcopy 18 | from .common import Geometry, COMPARE_TYPE 19 | 20 | 21 | class AbstractImage(object): 22 | def __init__(self, data, width, height): 23 | self.data = bytearray(data) 24 | 25 | self.width = width 26 | self.height = height 27 | self.mode = None 28 | self.srcFilePath = None 29 | self.srcFileFormat = None 30 | 31 | def getImageInfo(self): 32 | t = deepcopy(self.__dict__) 33 | t.pop('data') 34 | return t 35 | 36 | def getImageName(self): 37 | if self.filePath: return os.path.basename(self.filePath) 38 | return None 39 | 40 | def validateGeometry(self, geometry): 41 | return (geometry.width + geometry.x <= self.width and 42 | geometry.height + geometry.y <= self.height) 43 | 44 | def getDiff(self, other, geometry1=None, geometry2=None, stopOnDiff=False, compareType=COMPARE_TYPE.FULL, returnFailPixelList=False, colorDict=None): 45 | """ 46 | Compares two images: self vs other 47 | 48 | Args: 49 | other: image object to compare (against self) 50 | geometry1: The rectangular area within the 'self' image to compare 51 | geometry2: The rectangular area within the 'other' image to compare 52 | stopOnDiff: If True, return as soon as the first pixel difference is detected. 53 | If False, continue comparing the image until the end 54 | compareType: What type of comparison to perform for details see the 55 | COMPARE_TYPE enum above 56 | returnFailPixelList: If true, return a list with information for every pixel 57 | that was found different in the comparison. 58 | If false, don't return nor collect this data. 59 | colorDict: A dictionary with the colors to use for the deltaImages. 60 | 61 | Returns: 62 | A dictionary that always has the item 'isDiff', and additional data depending 63 | the case. 64 | If the areas of the images within its respective geometries are equal, then 65 | isDiff=False if not isdiff=True. 66 | Also if the geometries are invalid (e.g. the area specified by the geometry is 67 | beyond the image) or the geometries does not have the same area, then 68 | isDiff=True. 69 | 70 | The following items are included in the return dictionary if the image 71 | comparison reaches the end: 72 | 73 | 'deltaImageRgbData': Delta image for the RGB channels 74 | 'deltaImageAlphaData': Delta image for the alpha channel 75 | 'img1AlphaData': Image for the alpha channel of the 'self' image 76 | 'img2AlphaData': Image for the alpha channel of the 'other' image 77 | 78 | 'pixelDiffCount': How may pixels were different 79 | 'absDiffCount': The sum of the differences per channel of every pixel comapred 80 | 'maxChannelDelta': The max difference found in a channel 81 | 'diffPixelRgbList': The list of pixels that were different for the RGB channels 82 | 'diffPixelAlphaList': The list of pixels that were different for the alpha channel 83 | """ 84 | if geometry1 is None: 85 | geometry1 = Geometry(0, 0, self.width, self.height) 86 | else: 87 | geometry1 = Geometry(geometry1) 88 | 89 | if geometry2 is None: 90 | geometry2 = Geometry(0, 0, other.width, other.height) 91 | else: 92 | geometry2 = Geometry(geometry2) 93 | 94 | if not geometry1.isAreaEqual(geometry2): 95 | return {'isDiff': True, 'debugData': {'msg': 'Geometry mismatch', 'geometry1': str(geometry1), 'geometry2': str(geometry2)}} 96 | 97 | if (not self.validateGeometry(geometry1) or not other.validateGeometry(geometry2)): 98 | return {'isDiff': True, 'debugData': {'msg': 'Invalid geometry', 99 | 'geometry1': str(geometry1), 'geometry2': str(geometry2), 100 | 'width1': str(self.width), 'width2': str(other.width), 101 | 'height1': str(self.height), 'height2': str(other.height)}} 102 | 103 | if (((compareType is COMPARE_TYPE.ALPHA_HI1 or compareType is COMPARE_TYPE.ALPHA_LO1) and self.bytesPerPixel != 4) or 104 | ((compareType is COMPARE_TYPE.ALPHA_HI2 or compareType is COMPARE_TYPE.ALPHA_LO2) and other.bytesPerPixel != 4)): 105 | return {'isDiff': True, 'debugData': {'msg': 'Invalid image format and comparison type combo', 106 | 'compareType': str(compareType), 107 | 'bytesPerPixel1': self.bytesPerPixel, 'bytesPerPixel2': other.bytesPerPixel}} 108 | 109 | flagCompareAlpha = False 110 | if compareType is COMPARE_TYPE.FULL: 111 | if (self.bytesPerPixel == 4) != (other.bytesPerPixel == 4): return {'isDiff': True} 112 | if (self.bytesPerPixel == 4) and (other.bytesPerPixel == 4): flagCompareAlpha = True 113 | 114 | deltaImageRgb = bytearray(b'\x00\x00\x00' * geometry1.width * geometry1.height) 115 | deltaImageAlpha = bytearray(b'\x00\x00\x00' * geometry1.width * geometry1.height) 116 | img1Alpha = bytearray(b'\x00\x00\x00' * geometry1.width * geometry1.height) 117 | img2Alpha = bytearray(b'\x00\x00\x00' * geometry1.width * geometry1.height) 118 | 119 | maxChannelDelta = 0 120 | pixelDiffCount = 0 # Total pixels that differ 121 | absDiffCount = 0 # The sum of the absolute valuies of all bytes differences for all pixels 122 | absDiffCountPixel = 0 # The abs diff count of a single pixel 123 | diffPixelRgbList = [] 124 | diffPixelAlphaList = [] 125 | for j in range(0, geometry1.height): 126 | for i in range(0, geometry1.width): 127 | outputPixelIndex = (j * geometry1.width + i) * 3 128 | inputPixelIndex1 = ((j + geometry1.y) * self.width + i + geometry1.x) * self.bytesPerPixel 129 | inputPixelIndex2 = ((j + geometry2.y) * other.width + i + geometry2.x) * other.bytesPerPixel 130 | 131 | if compareType is COMPARE_TYPE.ALPHA_HI1 and self.data[inputPixelIndex1 + 3] != 0xFF: continue 132 | if compareType is COMPARE_TYPE.ALPHA_LO1 and self.data[inputPixelIndex1 + 3] == 0x00: continue 133 | if compareType is COMPARE_TYPE.ALPHA_HI2 and other.data[inputPixelIndex2 + 3] != 0xFF: continue 134 | if compareType is COMPARE_TYPE.ALPHA_LO2 and other.data[inputPixelIndex2 + 3] == 0x00: continue 135 | 136 | absDiffCountPixelRed = abs(self.data[inputPixelIndex1 + 0] - other.data[inputPixelIndex2 + 0]) 137 | absDiffCountPixelGreen = abs(self.data[inputPixelIndex1 + 1] - other.data[inputPixelIndex2 + 1]) 138 | absDiffCountPixelBlue = abs(self.data[inputPixelIndex1 + 2] - other.data[inputPixelIndex2 + 2]) 139 | 140 | absDiffCountPixel = absDiffCountPixelRed + absDiffCountPixelGreen + absDiffCountPixelBlue 141 | 142 | absDiffCountPixelAlpha = 0 143 | if flagCompareAlpha: 144 | absDiffCountPixelAlpha = abs(self.data[inputPixelIndex1 + 3] - other.data[inputPixelIndex2 + 3]) 145 | 146 | img1Alpha[outputPixelIndex + 0] = self.data[inputPixelIndex1 + 3] 147 | img1Alpha[outputPixelIndex + 1] = self.data[inputPixelIndex1 + 3] 148 | img1Alpha[outputPixelIndex + 2] = self.data[inputPixelIndex1 + 3] 149 | img2Alpha[outputPixelIndex + 0] = other.data[inputPixelIndex2 + 3] 150 | img2Alpha[outputPixelIndex + 1] = other.data[inputPixelIndex2 + 3] 151 | img2Alpha[outputPixelIndex + 2] = other.data[inputPixelIndex2 + 3] 152 | 153 | maxChannelDelta = max(maxChannelDelta, absDiffCountPixelRed, absDiffCountPixelGreen, absDiffCountPixelBlue, absDiffCountPixelAlpha) 154 | absDiffCountPixel = absDiffCountPixelRed + absDiffCountPixelGreen + absDiffCountPixelBlue + absDiffCountPixelAlpha 155 | absDiffCount += absDiffCountPixel 156 | 157 | pixelDiffCountTmp = 0 158 | if (absDiffCountPixelRed > 0 or 159 | absDiffCountPixelGreen > 0 or 160 | absDiffCountPixelBlue > 0): 161 | 162 | pixelDiffCountTmp = 1 163 | t = max(absDiffCountPixelRed, absDiffCountPixelGreen, absDiffCountPixelBlue) 164 | 165 | color = [0xFF, 0xFF, 0xFF] 166 | if colorDict: 167 | color = colorDict.get(str(t), colorDict.get('default', color)) 168 | deltaImageRgb[outputPixelIndex: outputPixelIndex + 3] = color 169 | 170 | if returnFailPixelList: 171 | diffPixelEntry = [] 172 | diffPixelEntry.append(inputPixelIndex1) 173 | diffPixelEntry.append(inputPixelIndex2) 174 | diffPixelRgbList.append(diffPixelEntry) 175 | 176 | if absDiffCountPixelAlpha > 0: 177 | pixelDiffCountTmp = 1 178 | 179 | t = absDiffCountPixelAlpha 180 | color = [0xFF, 0xFF, 0xFF] 181 | if colorDict: 182 | color = colorDict.get(str(t), colorDict.get('default', color)) 183 | deltaImageAlpha[outputPixelIndex: outputPixelIndex + 3] = color 184 | 185 | if returnFailPixelList: 186 | diffPixelEntry = [] 187 | diffPixelEntry.append(inputPixelIndex1) 188 | diffPixelEntry.append(inputPixelIndex2) 189 | diffPixelAlphaList.append(diffPixelEntry) 190 | 191 | pixelDiffCount += pixelDiffCountTmp 192 | if stopOnDiff and pixelDiffCount > 0: return {'isDiff': True} 193 | 194 | returnDict = {'isDiff': pixelDiffCount != 0, 195 | 'deltaImageRgbData': (deltaImageRgb, geometry1.width, geometry1.height), 196 | 'maxChannelDelta': maxChannelDelta, 197 | 'pixelDiffCount': pixelDiffCount, 198 | 'absDiffCount': absDiffCount, 199 | 'diffPixelRgbList': diffPixelRgbList, 200 | 'geometry1': geometry1, 201 | 'geometry2': geometry2} 202 | 203 | if flagCompareAlpha: 204 | alphaDict = {'deltaImageAlphaData': (deltaImageAlpha, geometry1.width, geometry1.height), 205 | 'img1AlphaData': (img1Alpha, geometry1.width, geometry1.height), 206 | 'img2AlphaData': (img2Alpha, geometry1.width, geometry1.height), 207 | 'diffPixelAlphaList': diffPixelAlphaList} 208 | 209 | returnDict.update(alphaDict) 210 | 211 | return returnDict 212 | -------------------------------------------------------------------------------- /PixelView/imageContainers/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import re 17 | import enum 18 | 19 | 20 | @enum.unique 21 | class COMPARE_TYPE(enum.Enum): 22 | 23 | # Alpha component is ignored if present 24 | ALPHALESS = 1 25 | 26 | # The alpha component from the first image is used as a bitmask 27 | # If the alpha is 255 the corresponding pixel is compared normally 28 | # Otherwise the corresponding pixel is excluded from the comparison 29 | ALPHA_HI1 = 2 30 | 31 | # The alpha component from the second image is used as a bitmask 32 | # If the alpha is 255 the corresponding pixel is compared normally 33 | # Otherwise the corresponding pixel is excluded from the comparison 34 | ALPHA_HI2 = 3 35 | 36 | # The alpha component from the first image is used as a bitmask 37 | # If the alpha is 0 the corresponding pixel is excluded from the comparison 38 | # Otherwise the corresponding pixel is compared normally 39 | ALPHA_LO1 = 4 40 | 41 | # The alpha component from the second image is used as a bitmask 42 | # If the alpha is 0 the corresponding pixel is excluded from the comparison 43 | # Otherwise the corresponding pixel is compared normally 44 | ALPHA_LO2 = 5 45 | 46 | # Compares color channels as well as the alpha channel 47 | # A difference of value for a pixel in the alpha channel is enough to deem 48 | # the result of the comparison as 2 different images 49 | # images without alpha can be compared like this as well 50 | # (For such case being equivalent of "ALPHALESS" 51 | # Alpha in one image and no alpha on the other is considered a difference 52 | FULL = 6 53 | 54 | 55 | class Geometry: 56 | def __init__(self, data=0, y=0, width=0, height=0): 57 | if isinstance(data, Geometry): 58 | self.x = data.x 59 | self.y = data.y 60 | self.width = data.width 61 | self.height = data.height 62 | return 63 | 64 | if isinstance(data, int): 65 | self.x = data 66 | self.y = y 67 | self.width = width 68 | self.height = height 69 | return 70 | 71 | if isinstance(data, str): 72 | # Sample string to match: 320x240+0+0 73 | t = re.match(r'([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+)', data) 74 | if t: 75 | self.x = int(t.group(3)) 76 | self.y = int(t.group(4)) 77 | self.width = int(t.group(1)) 78 | self.height = int(t.group(2)) 79 | return 80 | 81 | def __str__(self): 82 | return ','.join([str(item) for item in [self.x, self.y, self.width, self.height]]) 83 | 84 | def __eq__(self, other): 85 | if other is None: return False 86 | return (self.__dict__ == other.__dict__) 87 | 88 | def isAreaEqual(self, other): 89 | return (self.width == other.width and 90 | self.height == other.height) 91 | -------------------------------------------------------------------------------- /PixelView/imageContainers/rgb888Image.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import re 17 | import pUtils 18 | from PIL import Image 19 | from PixelView.imageContainers.abstractImage import AbstractImage 20 | 21 | 22 | class Rgb888Image(AbstractImage): 23 | 24 | def __init__(self, data=bytearray(), width=0, height=0): 25 | super().__init__(data, width, height) 26 | self.bytesPerPixel = 3 27 | self.mode = 'RGB' 28 | 29 | def save(self, filePath): 30 | header = str.encode('rgb888 ' + str(self.width) + ' ' + str(self.height) + chr(0x0A)) 31 | pUtils.quickFileWrite(filePath, header + self.data, 'wb') 32 | 33 | def savePNG(self, filePath): 34 | img = Image.frombytes('RGB', (self.width, self.height), self.data) 35 | img.save(filePath, "PNG") 36 | 37 | def load(self, filePath, data=None): 38 | if data is None: 39 | data = pUtils.quickFileRead(filePath, 'rb') 40 | 41 | index = data.find(b'\x0A') 42 | header = data[:index] 43 | body = data[index + 1:] 44 | 45 | # Sample string to match: rgb888 320 240 46 | t = re.match(b'rgb888 ([\x30-\x39]+) ([\x30-\x39]+)', header) 47 | if t is None: 48 | raise Exception('Invalid header for a rgb888 file type') 49 | self.width = int(t.group(1)) 50 | self.height = int(t.group(2)) 51 | self.data = bytearray(body) 52 | self.srcFilePath = filePath 53 | self.srcFileFormat = 'RGB888' 54 | -------------------------------------------------------------------------------- /PixelView/imageContainers/rgba8888Image.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import re 17 | import pUtils 18 | from PIL import Image 19 | from PixelView.imageContainers.abstractImage import AbstractImage 20 | 21 | 22 | class Rgba8888Image(AbstractImage): 23 | 24 | def __init__(self, data=bytearray(), width=0, height=0): 25 | super().__init__(data, width, height) 26 | self.bytesPerPixel = 4 27 | self.mode = 'RGBA' 28 | 29 | def save(self, filePath): 30 | header = str.encode('rgba8888 ' + str(self.width) + ' ' + str(self.height) + chr(0x0A)) 31 | pUtils.quickFileWrite(filePath, header + self.data, 'wb') 32 | 33 | def savePNG(self, filePath): 34 | img = Image.frombytes('RGBA', (self.width, self.height), self.data) 35 | img.save(filePath, 'PNG') 36 | 37 | def load(self, filePath, data=None): 38 | if data is None: 39 | data = pUtils.quickFileRead(filePath, 'rb') 40 | 41 | index = data.find(b'\x0A') 42 | header = data[:index] 43 | body = data[index + 1:] 44 | 45 | # Sample string to match: rgba8888 320 240 46 | t = re.match(b'rgba8888 ([\x30-\x39]+) ([\x30-\x39]+)', header) 47 | if t is None: 48 | raise Exception('Invalid header for a rgba8888 file type') 49 | self.width = int(t.group(1)) 50 | self.height = int(t.group(2)) 51 | self.data = bytearray(body) 52 | self.srcFilePath = filePath 53 | self.srcFileFormat = 'RGBA8888' 54 | -------------------------------------------------------------------------------- /PixelView/topLevel.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import os 17 | import pUtils 18 | from PixelView.utils.image import loadImage 19 | from PixelView.utils.cli import pprint, COLOR 20 | from PixelView.gui.mainWindow import launch, MAIN_WINDOW_MODE 21 | from PixelView.imageContainers.rgb888Image import Rgb888Image 22 | from PixelView.imageContainers.rgba8888Image import Rgba8888Image 23 | 24 | VERSION = '.01' 25 | 26 | 27 | def version(**kwargs): 28 | pprint('Version: ', color=COLOR.TEAL, endLine=False); pprint(VERSION) 29 | 30 | 31 | def info(filePath, **kwargs): 32 | try: 33 | img = loadImage(filePath) 34 | except IOError as e: 35 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('[I/O] ({0}): {1}'.format(e.errno, e.strerror)) 36 | exit(1) 37 | except Exception: 38 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('Unsupported image format') 39 | exit(1) 40 | 41 | pprint('-----------------------------------') 42 | pprint('srcFileName: ' + os.path.basename(img.srcFilePath)) 43 | pprint('mode: ' + img.mode) 44 | pprint('size: ' + str(img.width) + 'x' + str(img.height)) 45 | pprint('srcFileFormat: ' + img.srcFileFormat) 46 | pprint('-----------------------------------') 47 | 48 | 49 | def printVal(filePath, x, y, **kwargs): 50 | try: 51 | img = loadImage(filePath) 52 | except IOError as e: 53 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('[I/O] ({0}): {1}'.format(e.errno, e.strerror)) 54 | exit(1) 55 | except Exception: 56 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('Unsupported image format') 57 | exit(1) 58 | 59 | start = (img.width * y + x) * img.bytesPerPixel 60 | pprint(pUtils.formatHex(img.data[start:start + img.bytesPerPixel])) 61 | 62 | 63 | def genCanvas(outFilePath, red, green, blue, width, height, alpha, **kwargs): 64 | 65 | if os.path.exists(outFilePath): 66 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('File:') 67 | pprint(' %s' % outFilePath, color=COLOR.TEAL) 68 | pprint('Already exists') 69 | exit(1) 70 | 71 | if alpha is None: 72 | img = Rgb888Image(bytearray([red, green, blue] * width * height), width, height) 73 | else: 74 | img = Rgba8888Image(bytearray([red, green, blue, alpha] * width * height), width, height) 75 | 76 | img.save(outFilePath) 77 | pprint('DONE', color=COLOR.TEAL) 78 | 79 | 80 | def genConfig(dirPath, configManager, **kwargs): 81 | pUtils.createDirectory(os.path.abspath(dirPath)) 82 | 83 | filePath = os.path.join(dirPath, 'config1.json') 84 | if configManager.saveFullConfig(filePath) != 0: 85 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('File: %s already exists' % filePath) 86 | exit(1) 87 | 88 | filePath = os.path.join(dirPath, 'configMenu.json') 89 | if configManager.genMenuConfigFile(filePath, configName='config1', configPath='config1.json'): 90 | pprint('Error: ', color=COLOR.RED, endLine=False); pprint('File: %s already exists' % filePath) 91 | exit(1) 92 | 93 | pprint('DONE', color=COLOR.TEAL) 94 | 95 | 96 | def setConfigStart(filePath, configManager, **kwargs): 97 | configManager.setConfigStart(filePath) 98 | 99 | 100 | def clearConfigStart(configManager, **kwargs): 101 | configManager.clearConfigStart() 102 | 103 | 104 | def view(filePathList, configManager, **kwargs): 105 | launch(configManager, filePathList, mode=MAIN_WINDOW_MODE.VIEW, **kwargs) 106 | 107 | 108 | def compare(filePathList1, filePathList2, fList, configManager, **kwargs): 109 | launch(configManager, filePathList1, filePathList2, mode=MAIN_WINDOW_MODE.COMPARE, **kwargs) 110 | -------------------------------------------------------------------------------- /PixelView/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /PixelView/utils/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import sys 17 | import enum 18 | import pUtils 19 | import platform 20 | 21 | 22 | def paramList(arg): 23 | return arg.split(',') 24 | 25 | 26 | def handleCli(args, topLevel, ConfigManager=None): 27 | kwargs = vars(args) 28 | 29 | preprocessFileList(kwargs) 30 | 31 | command = kwargs.pop('command') 32 | func = getattr(topLevel, command) 33 | 34 | if kwargs['verbose']: 35 | pprint(kwargs, color=COLOR.TEAL) 36 | pprint('---------------------------') 37 | 38 | if ConfigManager: 39 | kwargs['configManager'] = ConfigManager(**kwargs) 40 | 41 | func(**kwargs) 42 | 43 | 44 | def preprocessFileList(kwargs): 45 | fList = kwargs.get('fList') 46 | 47 | if not fList: return 48 | fListVarNameList = kwargs['fListVarNameList'] 49 | for fListVarName in fListVarNameList: 50 | fListVar = kwargs.get(fListVarName) 51 | if fListVar is None: continue 52 | t = [] 53 | for item in fListVar: 54 | t += pUtils.quickFileRead(item, 'txt') 55 | kwargs[fListVarName] = t 56 | 57 | 58 | class COLOR(enum.Enum): 59 | RED = 91 60 | GREEN = 32 61 | BLUE = 34 62 | TEAL = 96 63 | 64 | 65 | def colorString(s, color): 66 | if platform.system() == 'Linux': 67 | return '\033[%im%s\033[00m' % (color.value, s) 68 | return s 69 | 70 | 71 | def pprint(s, color=None, endLine=True): 72 | s = str(s) 73 | 74 | if endLine: 75 | s += '\n' 76 | 77 | if PPRINT_LOG_FILE: 78 | pUtils.quickFileWrite(PPRINT_LOG_FILE, s, 'at') 79 | 80 | if color: 81 | s = colorString(s, color) 82 | 83 | sys.stdout.write(s) 84 | sys.stdout.flush() 85 | 86 | 87 | PPRINT_LOG_FILE = None 88 | -------------------------------------------------------------------------------- /PixelView/utils/image.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pUtils 16 | from PIL import Image 17 | from io import BytesIO 18 | from PySide2.QtGui import QImage, QPixmap 19 | from PixelView.imageContainers.rgb888Image import Rgb888Image 20 | from PixelView.imageContainers.rgba8888Image import Rgba8888Image 21 | 22 | 23 | def loadImage(filePath, nullColor=None): 24 | 25 | def genPlaceHolder(): 26 | # If we couldn't load an image we generate a placeholder 27 | # This is necessary since we may be dealing with a list of images 28 | # and we can't just end the application when one 'unloadable' image 29 | # is found. 30 | # By generating a place holder instead, we allow the user to keep 31 | # reviewing the images on the list as well as indicating something 32 | # went wrong. 33 | 34 | width = 100 35 | height = 100 36 | t = Rgba8888Image(bytearray(nullColor + [0]) * width * height, width, height) 37 | t.srcFileFormat = 'nullImage' 38 | return t 39 | 40 | try: 41 | data = pUtils.quickFileRead(filePath, 'rb') 42 | except Exception: 43 | if nullColor: return genPlaceHolder() 44 | raise 45 | 46 | try: 47 | img = Rgba8888Image() 48 | img.load(filePath, data=data) 49 | return img 50 | except Exception: pass 51 | 52 | try: 53 | img = Rgb888Image() 54 | img.load(filePath, data=data) 55 | return img 56 | except Exception: pass 57 | 58 | try: 59 | img = Image.open(BytesIO(data)) 60 | except Exception: 61 | raise Exception('Unable to identify image format') 62 | 63 | try: 64 | if img.format != 'PNG': raise Exception('Unsupported image format ' + img.format) 65 | 66 | width, height = img.size 67 | data = bytearray(img.tobytes()) 68 | if img.mode == 'RGBA': 69 | t = Rgba8888Image(data, width, height) 70 | elif img.mode == 'RGB': 71 | t = Rgb888Image(data, width, height) 72 | else: 73 | raise Exception('Unknown Image mode') 74 | t.srcFilePath = filePath 75 | t.srcFileFormat = 'PNG' 76 | return t 77 | except Exception: 78 | if nullColor: return genPlaceHolder() 79 | raise 80 | 81 | 82 | def dropAlpha(img): 83 | if isinstance(img, Rgb888Image): 84 | return img 85 | 86 | if isinstance(img, Rgba8888Image): 87 | data = img.data 88 | red = data[0::4] 89 | green = data[1::4] 90 | blue = data[2::4] 91 | 92 | newData = bytearray([0, 0, 0] * int(len(data) / 4)) 93 | newData[0::3] = red 94 | newData[1::3] = green 95 | newData[2::3] = blue 96 | return Rgb888Image(newData, img.width, img.height) 97 | 98 | raise Exception('Invalid input parameter type') 99 | 100 | 101 | def getAlphaImage(img): 102 | if isinstance(img, Rgb888Image): 103 | return None 104 | 105 | if isinstance(img, Rgba8888Image): 106 | data = img.data 107 | alpha = data[3::4] 108 | 109 | newData = bytearray([alpha[i // 3] for i in range(len(alpha) * 3)]) 110 | return Rgb888Image(newData, img.width, img.height) 111 | 112 | raise Exception('Invalid input parameter type') 113 | 114 | 115 | def widgetDisplayImage(widget, img): 116 | imgFormat = QImage.Format_RGB888 117 | 118 | img = dropAlpha(img) 119 | displayImage = QImage(img.data, 120 | img.width, img.height, 121 | imgFormat) 122 | 123 | displayImagePix = QPixmap.fromImage((displayImage)) 124 | widget.setPixmap(displayImagePix) 125 | -------------------------------------------------------------------------------- /PixelView/utils/other.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def truncateString(s, pixelWidth): 17 | retString = s[int(-1 * pixelWidth / 8):] 18 | if len(retString) != len(s): 19 | retString = '###' + retString 20 | return retString 21 | -------------------------------------------------------------------------------- /PixelView/utils/threading.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import threading 17 | 18 | 19 | class OneShotThread(threading.Thread): 20 | def __init__(self, oneShotFunc, inputData=None): 21 | super().__init__() 22 | self.inputData = inputData 23 | self.oneShotFunc = oneShotFunc 24 | self.sleepInterval = 100 25 | 26 | def run(self): 27 | if self.inputData is None: 28 | self.returnData = self.oneShotFunc() 29 | else: 30 | self.returnData = self.oneShotFunc(**self.inputData) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PixelView 2 | A compact and extensible image viewer 3 | * Facilitates the comparison and review of sets of multiple images 4 | * Is easily extendable to perform custom pixel level analysis 5 | * It can be cleanly wrapped by other programs to create more complex and specific automation (it is both, a python app and a python module) 6 | * Can run in Linux and Windows 7 | 8 | 9 | ## Requirements 10 | * Python3 (https://www.python.org) 11 | * pip (https://pypi.org/project/pip) 12 | * Pillow (https://pypi.org/project/Pillow) 13 | * PySide2 (https://pypi.org/project/PySide2) 14 | * pUtils (https://github.com/GawpAzrag/pUtils) 15 | 16 | 17 | ## Installation 18 | * Install the requirements listed above 19 | * Clone PixelView and install it 20 | ``` 21 | git clone https://github.com/NVIDIA/PixelView.git 22 | cd PixelView 23 | pip install . 24 | ``` 25 | > **Note:** In Windows you need to have the 'Scripts' directory within your python installation on your path if you want to invoke it just by 'PixelView'. Alternatively you can invoke it by 'python3 -m PixelView' as well. 26 | 27 | 28 | ## Usage 29 | PixelView supports PNG (24bit rgb and 32bit rgba) as well as its own format, which is simply a flat rgb(a) 8bits per channel with a single line ascii header at the top. 30 | The examples below use rgba format so that the earlier commands generate some single color images and the commands later on, reads them. This is so that it is possible to copy and paste each command in order as they are and see them work (except for the couple that have a syntax place holder, easily identifiable by the "< >" symbols), to provide a smooth "tour" of the application. 31 | 32 | ### version 33 | To display PixelView's version 34 | ``` 35 | PixelView version 36 | ``` 37 | 38 | ### genCanvas 39 | To generate a single color image file in rgb(a) format (the file will contain a one line ascii header at the top) 40 | ``` 41 | PixelView genCanvas red320.rgba 255 0 0 --alpha 0 42 | PixelView genCanvas blue320.rgba 0 0 255 --alpha 0 43 | ``` 44 | 45 | ### info 46 | To display basic information of an image 47 | ``` 48 | PixelView info red320.rgba 49 | ``` 50 | 51 | ### printVal 52 | To print the value of a specific pixel 53 | ``` 54 | PixelView printVal red320.rgba 0 0 55 | ``` 56 | 57 | ### view 58 | To view an image 59 | ``` 60 | PixelView view red320.rgba 61 | ``` 62 | 63 | To view a list of images (displayed one at a time) 64 | ``` 65 | PixelView view red320.rgba,blue320.rgba 66 | ``` 67 | Same as above, but loading the list from a file (one path per line) 68 | ``` 69 | PixelView view --fList 70 | ``` 71 | 72 | ### compare 73 | To compare two images 74 | ``` 75 | PixelView compare red320.rgba blue320.rgba 76 | ``` 77 | 78 | To compare two lists of images (displayed one pair at a time) 79 | ``` 80 | PixelView compare red320.rgba,blue320.rgba blue320.rgba,red320.rgba 81 | ``` 82 | Same as above but providing the list files (one path per line) 83 | ``` 84 | PixelView compare --fList 85 | ``` 86 | 87 | To compare subsections of the images 88 | ``` 89 | PixelView compare red320.rgba,blue320.rgba blue320.rgba,red320.rgba --geometry1=200x100+0+0 --geometry2=200x100+20+10 90 | ``` 91 | 92 | ### Customization and configuration 93 | To generate a set of starting configuration files and tell PixelView to use them 94 | ``` 95 | PixelView --useInternalDefaults genConfig myConfigDir 96 | PixelView setConfigStart myConfigDir/configMenu.json 97 | ``` 98 | Edit myConfigDir/config1.json to customize PixelView 99 | 100 | ### help 101 | For a full list of commands 102 | ``` 103 | PixelView -h 104 | ``` 105 | 106 | 107 | ## Issues and Contributing 108 | [Checkout the Contributing document!](CONTRIBUTING.md) 109 | * Please let us know by [filing a new issue](http://github.com/NVIDIA/PixelView/issues/new) 110 | * You can contribute by opening a [pull request](https://help.github.com/articles/using-pull-requests) 111 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PixelView.topLevel import VERSION 16 | from setuptools import setup, find_packages 17 | setup( 18 | name='PixelView', 19 | version=VERSION, 20 | packages=find_packages(), 21 | package_data={'': ['*.json', '*.txt']}, 22 | entry_points={'console_scripts': ['PixelView = PixelView.cli:run']}, 23 | ) 24 | --------------------------------------------------------------------------------