├── .gitignore ├── LICENSE ├── PaddleOCRModel ├── 00.png ├── 11.png ├── PaddleOCRModel.py ├── README.md ├── modelv3 │ ├── det_model.onnx │ └── rec_model.onnx ├── opencv_ocr.ipynb └── ppocr_keys_v1.txt ├── README.md ├── fake_useragent_0.1.11.json ├── image ├── 0180a5748681abe7254ce6734aa64de.png ├── 58e820362dd287f6668e011e20a1020.png ├── 60430e4e61d28d0e79da9d58e46037f.png ├── jp.png ├── jp0.jpg ├── jp00.png ├── jp1.jpg ├── jp2.png ├── 固定截屏.gif └── 截屏文字识别.gif ├── j_temp └── ocrtemp.png ├── jamWidgets.py ├── jam_transtalater.py ├── jampublic.py ├── jamresourse.py ├── jamroll_screenshot.py ├── jamscreenshot.py ├── jamspeak.py ├── old_version ├── jamresourse.py └── jamscreenshot.py └── requirement.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PaddleOCRModel/00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/PaddleOCRModel/00.png -------------------------------------------------------------------------------- /PaddleOCRModel/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/PaddleOCRModel/11.png -------------------------------------------------------------------------------- /PaddleOCRModel/PaddleOCRModel.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import sys 3 | import threading 4 | import time 5 | import cv2 6 | import os 7 | import math 8 | import copy 9 | import onnxruntime 10 | import numpy as np 11 | import pyclipper 12 | from shapely.geometry import Polygon 13 | model_shape_w = 48 14 | module_dir = os.path.dirname(os.path.abspath(__file__)) 15 | # print("model path",module_dir) 16 | # %% 17 | class NormalizeImage(object): 18 | """ normalize image such as substract mean, divide std 19 | """ 20 | 21 | def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs): 22 | if isinstance(scale, str): 23 | scale = eval(scale) 24 | self.scale = np.float32(scale if scale is not None else 1.0 / 255.0) 25 | mean = mean if mean is not None else [0.485, 0.456, 0.406] 26 | std = std if std is not None else [0.229, 0.224, 0.225] 27 | 28 | shape = (3, 1, 1) if order == 'chw' else (1, 1, 3) 29 | self.mean = np.array(mean).reshape(shape).astype('float32') 30 | self.std = np.array(std).reshape(shape).astype('float32') 31 | 32 | def __call__(self, data): 33 | img = data['image'] 34 | from PIL import Image 35 | if isinstance(img, Image.Image): 36 | img = np.array(img) 37 | 38 | assert isinstance(img, 39 | np.ndarray), "invalid input 'img' in NormalizeImage" 40 | data['image'] = ( 41 | img.astype('float32') * self.scale - self.mean) / self.std 42 | return data 43 | 44 | 45 | class ToCHWImage(object): 46 | """ convert hwc image to chw image 47 | """ 48 | 49 | def __init__(self, **kwargs): 50 | pass 51 | 52 | def __call__(self, data): 53 | img = data['image'] 54 | from PIL import Image 55 | if isinstance(img, Image.Image): 56 | img = np.array(img) 57 | data['image'] = img.transpose((2, 0, 1)) 58 | return data 59 | 60 | 61 | class KeepKeys(object): 62 | def __init__(self, keep_keys, **kwargs): 63 | self.keep_keys = keep_keys 64 | 65 | def __call__(self, data): 66 | data_list = [] 67 | for key in self.keep_keys: 68 | data_list.append(data[key]) 69 | return data_list 70 | 71 | class DetResizeForTest(object): 72 | def __init__(self, **kwargs): 73 | super(DetResizeForTest, self).__init__() 74 | self.resize_type = 0 75 | self.keep_ratio = False 76 | if 'image_shape' in kwargs: 77 | self.image_shape = kwargs['image_shape'] 78 | self.resize_type = 1 79 | if 'keep_ratio' in kwargs: 80 | self.keep_ratio = kwargs['keep_ratio'] 81 | elif 'limit_side_len' in kwargs: 82 | self.limit_side_len = kwargs['limit_side_len'] 83 | self.limit_type = kwargs.get('limit_type', 'min') 84 | elif 'resize_long' in kwargs: 85 | self.resize_type = 2 86 | self.resize_long = kwargs.get('resize_long', 640) 87 | else: 88 | self.limit_side_len = 736 89 | self.limit_type = 'min' 90 | 91 | def __call__(self, data): 92 | img = data['image'] 93 | src_h, src_w, _ = img.shape 94 | #print(self.resize_type) 95 | if sum([src_h, src_w]) < 64: 96 | img = self.image_padding(img) 97 | if self.resize_type == 0: 98 | # img, shape = self.resize_image_type0(img) 99 | img, [ratio_h, ratio_w] = self.resize_image_type0(img) 100 | elif self.resize_type == 2: 101 | img, [ratio_h, ratio_w] = self.resize_image_type2(img) 102 | else: 103 | # img, shape = self.resize_image_type1(img) 104 | img, [ratio_h, ratio_w] = self.resize_image_type1(img) 105 | data['image'] = img 106 | data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w]) 107 | return data 108 | 109 | def image_padding(self, im, value=0): 110 | h, w, c = im.shape 111 | im_pad = np.zeros((max(model_shape_w, h), max(model_shape_w, w), c), np.uint8) + value 112 | im_pad[:h, :w, :] = im 113 | return im_pad 114 | 115 | def image_padding_640(self, im, value=0): 116 | h, w, c = im.shape 117 | im_pad = np.zeros((max(640, h), max(640, w), c), np.uint8) + value 118 | im_pad[:h, :w, :] = im 119 | return im_pad 120 | 121 | def resize_image_type1(self, img): 122 | resize_h, resize_w = self.image_shape 123 | ori_h, ori_w = img.shape[:2] # (h, w, c) 124 | if self.keep_ratio is True: 125 | resize_w = ori_w * resize_h / ori_h 126 | N = math.ceil(resize_w / model_shape_w) 127 | resize_w = N * model_shape_w 128 | ratio_h = float(resize_h) / ori_h 129 | ratio_w = float(resize_w) / ori_w 130 | 131 | img = cv2.resize(img, (int(resize_w), int(resize_h))) 132 | # return img, np.array([ori_h, ori_w]) 133 | return img, [ratio_h, ratio_w] 134 | 135 | def resize_image_type0(self, img): 136 | """ 137 | resize image to a size multiple of 48 which is required by the network 138 | args: 139 | img(array): array with shape [h, w, c] 140 | return(tuple): 141 | img, (ratio_h, ratio_w) 142 | """ 143 | limit_side_len = self.limit_side_len 144 | h, w, c = img.shape 145 | 146 | # limit the max side 147 | if self.limit_type == 'max': 148 | if max(h, w) > limit_side_len: 149 | if h > w: 150 | ratio = float(limit_side_len) / h 151 | else: 152 | ratio = float(limit_side_len) / w 153 | else: 154 | ratio = 1. 155 | elif self.limit_type == 'min': 156 | if min(h, w) < limit_side_len: 157 | if h < w: 158 | ratio = float(limit_side_len) / h 159 | else: 160 | ratio = float(limit_side_len) / w 161 | else: 162 | ratio = 1. 163 | elif self.limit_type == 'resize_long': 164 | ratio = float(limit_side_len) / max(h, w) 165 | else: 166 | raise Exception('not support limit type, image ') 167 | resize_h = int(h * ratio) 168 | resize_w = int(w * ratio) 169 | 170 | resize_h = max(int(round(resize_h / model_shape_w) * model_shape_w), model_shape_w) 171 | resize_w = max(int(round(resize_w / model_shape_w) * model_shape_w), model_shape_w) 172 | 173 | try: 174 | if int(resize_w) <= 0 or int(resize_h) <= 0: 175 | return None, (None, None) 176 | img = cv2.resize(img, (int(resize_w), int(resize_h))) 177 | except: 178 | print(img.shape, resize_w, resize_h) 179 | sys.exit(0) 180 | ratio_h = resize_h / float(h) 181 | ratio_w = resize_w / float(w) 182 | return img, [ratio_h, ratio_w] 183 | 184 | def resize_image_type2(self, img): 185 | h, w, _ = img.shape 186 | 187 | resize_w = w 188 | resize_h = h 189 | 190 | if resize_h > resize_w: 191 | ratio = float(self.resize_long) / resize_h 192 | else: 193 | ratio = float(self.resize_long) / resize_w 194 | 195 | resize_h = int(resize_h * ratio) 196 | resize_w = int(resize_w * ratio) 197 | img = cv2.resize(img, (resize_w, resize_h)) 198 | #这里加个0填充,适配固定shape模型 199 | img = self.image_padding_640(img) 200 | return img, [ratio, ratio] 201 | 202 | # %% 203 | ### 检测结果后处理过程(得到检测框) 204 | class DBPostProcess(object): 205 | """ 206 | The post process for Differentiable Binarization (DB). 207 | """ 208 | 209 | def __init__(self, 210 | thresh=0.3, 211 | box_thresh=0.7, 212 | max_candidates=1000, 213 | unclip_ratio=2.0, 214 | use_dilation=False, 215 | **kwargs): 216 | self.thresh = thresh 217 | self.box_thresh = box_thresh 218 | self.max_candidates = max_candidates 219 | self.unclip_ratio = unclip_ratio 220 | self.min_size = 3 221 | self.dilation_kernel = np.array([[1, 1], [1, 1]]) if use_dilation else None 222 | 223 | def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height,ratio): 224 | ''' 225 | _bitmap: single map with shape (1, H, W), 226 | whose values are binarized as {0, 1} 227 | ''' 228 | bitmap = _bitmap 229 | #height, width = bitmap.shape 230 | height, width = dest_height*ratio, dest_width*ratio, 231 | 232 | outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST, 233 | cv2.CHAIN_APPROX_SIMPLE) 234 | if len(outs) == 3: 235 | img, contours, _ = outs[0], outs[1], outs[2] 236 | elif len(outs) == 2: 237 | contours, _ = outs[0], outs[1] 238 | 239 | num_contours = min(len(contours), self.max_candidates) 240 | 241 | boxes = [] 242 | scores = [] 243 | for index in range(num_contours): 244 | contour = contours[index] 245 | points, sside = self.get_mini_boxes(contour) 246 | if sside < self.min_size: 247 | continue 248 | points = np.array(points) 249 | score = self.box_score_fast(pred, points.reshape(-1, 2)) 250 | if self.box_thresh > score: 251 | continue 252 | 253 | box = self.unclip(points).reshape(-1, 1, 2) 254 | box, sside = self.get_mini_boxes(box) 255 | if sside < self.min_size + 2: 256 | continue 257 | box = np.array(box) 258 | 259 | box[:, 0] = np.clip( # 640 * 661 260 | np.round(box[:, 0] / width * dest_width), 0, dest_width) 261 | box[:, 1] = np.clip( 262 | np.round(box[:, 1] / height * dest_height), 0, dest_height) 263 | boxes.append(box.astype(np.int16)) 264 | scores.append(score) 265 | return np.array(boxes, dtype=np.int16), scores 266 | 267 | def unclip(self, box): 268 | unclip_ratio = self.unclip_ratio 269 | poly = Polygon(box) 270 | distance = poly.area * unclip_ratio / poly.length 271 | offset = pyclipper.PyclipperOffset() 272 | offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) 273 | return np.array(offset.Execute(distance)) 274 | 275 | def get_mini_boxes(self, contour): 276 | bounding_box = cv2.minAreaRect(contour) 277 | points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) 278 | 279 | index_1, index_2, index_3, index_4 = 0, 1, 2, 3 280 | if points[1][1] > points[0][1]: 281 | index_1 = 0 282 | index_4 = 1 283 | else: 284 | index_1 = 1 285 | index_4 = 0 286 | if points[3][1] > points[2][1]: 287 | index_2 = 2 288 | index_3 = 3 289 | else: 290 | index_2 = 3 291 | index_3 = 2 292 | 293 | box = [ 294 | points[index_1], points[index_2], points[index_3], points[index_4] 295 | ] 296 | return box, min(bounding_box[1]) 297 | 298 | def box_score_fast(self, bitmap, _box): 299 | h, w = bitmap.shape[:2] 300 | box = _box.copy() 301 | xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int32), 0, w - 1) 302 | xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int32), 0, w - 1) 303 | ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int32), 0, h - 1) 304 | ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int32), 0, h - 1) 305 | 306 | mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) 307 | box[:, 0] = box[:, 0] - xmin 308 | box[:, 1] = box[:, 1] - ymin 309 | cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1) 310 | return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] 311 | 312 | def __call__(self, outs_dict, shape_list): 313 | pred = outs_dict 314 | pred = pred[:, 0, :, :] 315 | segmentation = pred > self.thresh 316 | boxes_batch = [] 317 | for batch_index in range(pred.shape[0]): 318 | src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] 319 | if self.dilation_kernel is not None: 320 | mask = cv2.dilate( 321 | np.array(segmentation[batch_index]).astype(np.uint8), 322 | self.dilation_kernel) 323 | else: 324 | mask = segmentation[batch_index] 325 | boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, 326 | src_w, src_h,ratio_w) 327 | boxes_batch.append({'points': boxes}) 328 | return boxes_batch 329 | 330 | 331 | # %% 332 | ## 根据推理结果解码识别结果 333 | class process_pred(object): 334 | def __init__(self, character_dict_path=None, character_type='ch', use_space_char=False): 335 | self.character_str = '' 336 | with open(character_dict_path, 'rb') as fin: 337 | lines = fin.readlines() 338 | for line in lines: 339 | line = line.decode('utf-8').strip('\n').strip('\r\n') 340 | self.character_str += line 341 | if use_space_char: 342 | self.character_str += ' ' 343 | dict_character = list(self.character_str) 344 | 345 | dict_character = self.add_special_char(dict_character) 346 | self.dict = {char: i for i, char in enumerate(dict_character)} 347 | self.character = dict_character 348 | 349 | def add_special_char(self, dict_character): 350 | dict_character = ['blank'] + dict_character 351 | return dict_character 352 | 353 | def decode(self, text_index, text_prob=None, is_remove_duplicate=False): 354 | result_list = [] 355 | ignored_tokens = [0] 356 | batch_size = len(text_index) 357 | for batch_idx in range(batch_size): 358 | char_list = [] 359 | conf_list = [] 360 | for idx in range(len(text_index[batch_idx])): 361 | if text_index[batch_idx][idx] in ignored_tokens: 362 | continue 363 | if is_remove_duplicate and idx > 0 and text_index[batch_idx][idx - 1] == text_index[batch_idx][idx]: 364 | continue 365 | char_list.append(self.character[int(text_index[batch_idx][idx])]) 366 | if text_prob is not None: 367 | conf_list.append(text_prob[batch_idx][idx]) 368 | else: 369 | conf_list.append(1) 370 | text = ''.join(char_list) 371 | result_list.append((text, np.mean(conf_list))) 372 | return result_list 373 | 374 | def __call__(self, preds, label=None): 375 | if not isinstance(preds, np.ndarray): 376 | preds = np.array(preds) 377 | preds_idx = preds.argmax(axis=2) 378 | preds_prob = preds.max(axis=2) 379 | text = self.decode(preds_idx, preds_prob, is_remove_duplicate=True) 380 | if label is None: 381 | return text 382 | label = self.decode(label) 383 | return text, label 384 | 385 | 386 | # %% 387 | class det_rec_functions(object): 388 | def __init__(self, image,use_dnn = False,version=3): 389 | global model_shape_w 390 | self.img = image.copy() 391 | self.det_file = os.path.join(module_dir,'modelv{}/det_model.onnx'.format(version)) 392 | self.small_rec_file = os.path.join(module_dir,'modelv{}/rec_model.onnx'.format(version)) 393 | model_shape_w = 48 if version == 3 else 32 # 适配v2和v3 394 | self.model_shape = [3,model_shape_w,1000] 395 | self.use_dnn = use_dnn 396 | if self.use_dnn == False: 397 | self.onet_det_session = onnxruntime.InferenceSession(self.det_file) 398 | self.onet_rec_session = onnxruntime.InferenceSession(self.small_rec_file) 399 | else: 400 | self.onet_det_session = cv2.dnn.readNetFromONNX(self.det_file) 401 | self.onet_rec_session = cv2.dnn.readNetFromONNX(self.small_rec_file) 402 | self.infer_before_process_op, self.det_re_process_op = self.get_process() 403 | self.postprocess_op = process_pred(os.path.join(module_dir,'ppocr_keys_v1.txt'), 'ch', True) 404 | 405 | ## 图片预处理过程 406 | def transform(self, data, ops=None): 407 | """ transform """ 408 | if ops is None: 409 | ops = [] 410 | for op in ops: 411 | data = op(data) 412 | if data is None: 413 | return None 414 | return data 415 | 416 | def create_operators(self, op_param_list, global_config=None): 417 | """ 418 | create operators based on the config 419 | 420 | Args: 421 | params(list): a dict list, used to create some operators 422 | """ 423 | assert isinstance(op_param_list, list), ('operator config should be a list') 424 | ops = [] 425 | for operator in op_param_list: 426 | assert isinstance(operator, 427 | dict) and len(operator) == 1, "yaml format error" 428 | op_name = list(operator)[0] 429 | param = {} if operator[op_name] is None else operator[op_name] 430 | if global_config is not None: 431 | param.update(global_config) 432 | op = eval(op_name)(**param) 433 | ops.append(op) 434 | return ops 435 | 436 | ### 检测框的后处理 437 | def order_points_clockwise(self, pts): 438 | """ 439 | reference from: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py 440 | # sort the points based on their x-coordinates 441 | """ 442 | xSorted = pts[np.argsort(pts[:, 0]), :] 443 | 444 | # grab the left-most and right-most points from the sorted 445 | # x-roodinate points 446 | leftMost = xSorted[:2, :] 447 | rightMost = xSorted[2:, :] 448 | 449 | # now, sort the left-most coordinates according to their 450 | # y-coordinates so we can grab the top-left and bottom-left 451 | # points, respectively 452 | leftMost = leftMost[np.argsort(leftMost[:, 1]), :] 453 | (tl, bl) = leftMost 454 | 455 | rightMost = rightMost[np.argsort(rightMost[:, 1]), :] 456 | (tr, br) = rightMost 457 | 458 | rect = np.array([tl, tr, br, bl], dtype="float32") 459 | return rect 460 | 461 | def clip_det_res(self, points, img_height, img_width): 462 | for pno in range(points.shape[0]): 463 | points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1)) 464 | points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1)) 465 | return points 466 | 467 | #shape_part_list = [661 969 7.74583964e-01 6.60474716e-01] 468 | def filter_tag_det_res(self, dt_boxes, shape_part_list): 469 | img_height, img_width = shape_part_list[0],shape_part_list[1] 470 | dt_boxes_new = [] 471 | for box in dt_boxes: 472 | box = self.order_points_clockwise(box) 473 | box = self.clip_det_res(box, img_height, img_width) 474 | rect_width = int(np.linalg.norm(box[0] - box[1])) 475 | rect_height = int(np.linalg.norm(box[0] - box[3])) 476 | if rect_width <= 3 or rect_height <= 3: 477 | continue 478 | dt_boxes_new.append(box) 479 | dt_boxes = np.array(dt_boxes_new) 480 | return dt_boxes 481 | 482 | ### 定义图片前处理过程,和检测结果后处理过程 483 | def get_process(self): 484 | det_db_thresh = 0.3 485 | det_db_box_thresh = 0.3 486 | max_candidates = 2000 487 | unclip_ratio = 1.6 488 | use_dilation = True 489 | # DetResizeForTest 定义检测模型前处理规则 490 | pre_process_list = [{ 491 | 'DetResizeForTest': { 492 | # 'limit_side_len': 2500, 493 | # 'limit_type': 'max', 494 | 'resize_long': 640 495 | # 'image_shape':[640,640], 496 | # 'keep_ratio':True, 497 | } 498 | }, { 499 | 'NormalizeImage': { 500 | 'std': [0.229, 0.224, 0.225], 501 | 'mean': [0.485, 0.456, 0.406], 502 | 'scale': '1./255.', 503 | 'order': 'hwc' 504 | } 505 | }, { 506 | 'ToCHWImage': None 507 | }, { 508 | 'KeepKeys': { 509 | 'keep_keys': ['image', 'shape'] 510 | } 511 | }] 512 | 513 | infer_before_process_op = self.create_operators(pre_process_list) 514 | det_re_process_op = DBPostProcess(det_db_thresh, det_db_box_thresh, max_candidates, unclip_ratio, use_dilation) 515 | return infer_before_process_op, det_re_process_op 516 | 517 | def sorted_boxes(self, dt_boxes): 518 | """ 519 | Sort text boxes in order from top to bottom, left to right 520 | args: 521 | dt_boxes(array):detected text boxes with shape [4, 2] 522 | return: 523 | sorted boxes(array) with shape [4, 2] 524 | """ 525 | num_boxes = dt_boxes.shape[0] 526 | sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0])) 527 | _boxes = list(sorted_boxes) 528 | 529 | for i in range(num_boxes - 1): 530 | if abs(_boxes[i + 1][0][1] - _boxes[i][0][1]) < 10 and \ 531 | (_boxes[i + 1][0][0] < _boxes[i][0][0]): 532 | tmp = _boxes[i] 533 | _boxes[i] = _boxes[i + 1] 534 | _boxes[i + 1] = tmp 535 | return _boxes 536 | 537 | ### 图像输入预处理 538 | def resize_norm_img(self, img): 539 | imgC, imgH, imgW = [int(v) for v in self.model_shape] 540 | assert imgC == img.shape[2] 541 | h, w = img.shape[:2] 542 | ratio = w / float(h) 543 | if math.ceil(imgH * ratio) > imgW: 544 | resized_w = imgW 545 | else: 546 | resized_w = int(math.ceil(imgH * ratio)) 547 | resized_image = cv2.resize(img, (resized_w, imgH)) 548 | resized_image = resized_image.astype('float32') 549 | resized_image = resized_image.transpose((2, 0, 1)) / 255 550 | resized_image -= 0.5 551 | resized_image /= 0.5 552 | padding_im = np.zeros((imgC, imgH, imgW), dtype=np.float32) 553 | padding_im[:, :, 0:resized_w] = resized_image 554 | return padding_im 555 | def draw_boxes(self,boxes,image,display = False): 556 | for points in boxes: 557 | # 将四个点转换成轮廓格式 558 | contours = [points.astype(int)] 559 | # 绘制边框 560 | cv2.drawContours(image, contours, -1, (0, 255, 0), 1) 561 | # cv2.putText(image,str(contours),(points[0],points[1])) 562 | # cv2.imwrite("testocr.png",image) 563 | # 显示图像 564 | if display: 565 | # cv2.namedWindow("Bounding Boxes", cv2.WINDOW_NORMAL) 566 | cv2.resizeWindow("Bounding Boxes", 1280, 720) 567 | cv2.imshow("Bounding Boxes", image) 568 | cv2.waitKey(0) 569 | cv2.destroyAllWindows() 570 | return image 571 | ## 推理检测图片中的部分 572 | def get_boxes(self): 573 | img_ori = self.img 574 | img_part = img_ori.copy() 575 | data_part = {'image': img_part} 576 | data_part = self.transform(data_part, self.infer_before_process_op) 577 | img_part, shape_part_list = data_part 578 | img_part = np.expand_dims(img_part, axis=0) 579 | shape_part_list = np.expand_dims(shape_part_list, axis=0) 580 | if self.use_dnn == True: 581 | self.onet_det_session.setInput(img_part) 582 | outs_part = self.onet_det_session.forward() 583 | else: 584 | inputs_part = {self.onet_det_session.get_inputs()[0].name: img_part} 585 | outs_part = self.onet_det_session.run(None, inputs_part) 586 | outs_part = outs_part[0] 587 | print(outs_part.shape) 588 | post_res_part = self.det_re_process_op(outs_part, shape_part_list) 589 | dt_boxes_part = post_res_part[0]['points'] 590 | dt_boxes_part = self.filter_tag_det_res(dt_boxes_part,shape_part_list[0]) 591 | dt_boxes_part = self.sorted_boxes(dt_boxes_part) 592 | 593 | return dt_boxes_part,img_part 594 | 595 | ### 根据bounding box得到单元格图片 596 | def get_rotate_crop_image(self, img, points): 597 | img_crop_width = int( 598 | max( 599 | np.linalg.norm(points[0] - points[1]), 600 | np.linalg.norm(points[2] - points[3]))) 601 | img_crop_height = int( 602 | max( 603 | np.linalg.norm(points[0] - points[3]), 604 | np.linalg.norm(points[1] - points[2]))) 605 | pts_std = np.float32([[0, 0], [img_crop_width, 0], 606 | [img_crop_width, img_crop_height], 607 | [0, img_crop_height]]) 608 | M = cv2.getPerspectiveTransform(points, pts_std) 609 | dst_img = cv2.warpPerspective( 610 | img, 611 | M, (img_crop_width, img_crop_height), 612 | borderMode=cv2.BORDER_REPLICATE, 613 | flags=cv2.INTER_CUBIC) 614 | dst_img_height, dst_img_width = dst_img.shape[0:2] 615 | if dst_img_height * 1.0 / dst_img_width >= 1.5: 616 | dst_img = np.rot90(dst_img) 617 | return dst_img 618 | 619 | ### 单张图片推理 620 | def get_img_res(self, onnx_model, img, process_op): 621 | img = self.resize_norm_img(img) 622 | img = img[np.newaxis, :] 623 | if self.use_dnn: 624 | onnx_model.setInput(img) # 设置模型输入 625 | outs = onnx_model.forward() # 推理出结果 626 | else: 627 | inputs = {onnx_model.get_inputs()[0].name: img} 628 | outs = onnx_model.run(None, inputs) 629 | outs = outs[0] 630 | return process_op(outs) 631 | 632 | def process_n_pic(self,piclist): 633 | results = [] 634 | results_info = [] 635 | for pic in piclist: 636 | res = self.get_img_res(self.onet_rec_session, pic, self.postprocess_op) 637 | results.append(res[0]) 638 | results_info.append(res) 639 | def get_match_text_boxes(self,dt_boxes,results,threshold = 0.5): 640 | match_text_boxes = [] 641 | for pos, textres in zip(dt_boxes,results): 642 | if textres[1]>threshold: 643 | text = textres[0] 644 | match_text_boxes.append({'text': text, 'box': pos}) 645 | return match_text_boxes 646 | 647 | def get_format_text(self,match_text_boxes): 648 | if len(match_text_boxes) == 0: 649 | return "no result" 650 | text_blocks = [] 651 | total_h = 0 652 | for text_box in match_text_boxes: 653 | pos = text_box["box"] 654 | text = text_box["text"] 655 | # 提取最小的x和y 656 | min_x = np.min(pos[:, 0]) 657 | min_y = np.min(pos[:, 1]) 658 | 659 | # 提取近似的宽度和高度 660 | width = np.max(pos[:, 0]) - min_x 661 | height = np.max(pos[:, 1]) - min_y 662 | total_h += height 663 | print((min_x, min_y, width, height)) 664 | text_blocks.append({'text': text, 'box': (min_x, min_y, width, height)}) 665 | 666 | av_h = int(total_h/len(text_blocks)) 667 | def sort_blocks(blocks): 668 | # 定义一个排序函数 669 | def sort_func(block): 670 | # 获取box中的x和y 671 | x, y, w, h = block['box'] 672 | # 返回一个元组,第一个元素是x,第二个元素是y,如果两个块的x相差小于h//2,则比较y 673 | return ( y//av_h,x) 674 | 675 | # 使用sorted函数进行排序,key参数传入排序函数 676 | return sorted(blocks, key=sort_func) 677 | # 按照文字块的顶部位置从上到下排序 678 | text_blocks = sort_blocks(text_blocks) 679 | 680 | # 拼接所有文字块的识别结果 681 | result = '' 682 | last_bottom = 0 683 | last_right = 0 684 | first_line_x = 0 685 | adding_line_first_W = False 686 | for i,block in enumerate(text_blocks): 687 | text = block['text'] 688 | box = block['box'] 689 | x, y, w, h = box 690 | top = y 691 | bottom = y + h 692 | if i == 0 : 693 | result += text 694 | last_bottom = bottom 695 | last_right = x + w 696 | first_line_x = x 697 | continue 698 | print(block,top , bottom,last_bottom - h // 2) 699 | # 如果当前文字块的顶部位置比上一个文字块的底部位置高,则需要换行 700 | new_line = False 701 | if top > last_bottom - h // 2: 702 | result += '\n' 703 | last_right = 0 704 | new_line=True 705 | print("add new line") 706 | if new_line: 707 | if x - first_line_x > h // 2: 708 | result += ' ' * (int(x-last_right)//av_h) 709 | # 如果当前文字块的左侧位置比上一个文字块的右侧位置大,则需要增加空格 710 | elif x > last_right: 711 | result += ' ' * (int(x - last_right)//av_h) 712 | print("add space") 713 | 714 | # 添加当前文字块的识别结果 715 | result += text 716 | 717 | # 更新上一个文字块的底部位置和右侧位置 718 | last_bottom = bottom 719 | last_right = x + w 720 | 721 | return result 722 | def recognition_img(self, dt_boxes): 723 | stime = time.time() 724 | img_ori = self.img #原图大小 725 | img = img_ori.copy() 726 | img_list = [] 727 | for box in dt_boxes[0]: 728 | tmp_box = copy.deepcopy(box) 729 | img_crop = self.get_rotate_crop_image(img, tmp_box) 730 | img_list.append(img_crop) 731 | ptime = time.time() 732 | print("rec preprocess time",ptime-stime) 733 | ## 识别小图片 734 | results = [] 735 | results_info = [] 736 | for pic in img_list: 737 | res = self.get_img_res(self.onet_rec_session, pic, self.postprocess_op) 738 | results.append(res[0]) 739 | results_info.append(res) 740 | # threads = [] 741 | # tmp_img = [] 742 | # for i, pic in enumerate(img_list): 743 | # tmp_img.append(pic) 744 | # if len(tmp_img)==116 or i == len(img_list)-1: 745 | # print("process len",len(tmp_img)) 746 | # thread = threading.Thread(target=self.process_n_pic,args=(copy.deepcopy(tmp_img),)) 747 | # thread.start() 748 | # threads.append(thread) 749 | # tmp_img = [] 750 | # print("waiting..") 751 | # # 等待所有线程执行完毕 752 | # for thread in threads: 753 | # thread.join() 754 | 755 | print("rec end time",time.time()-ptime) 756 | return results, results_info 757 | 758 | 759 | # %% 760 | if __name__=='__main__': 761 | # 读取图片 762 | image = cv2.imread('./00.png') 763 | # 文本检测 764 | # 模型固化为640*640 需要修改对应前处理,box的后处理。 765 | # ocr_sys = det_rec_functions(image,use_dnn = True) 766 | # # 得到检测框 767 | # dt_boxes = ocr_sys.get_boxes() 768 | # # 识别 results: 单纯的识别结果,results_info: 识别结果+置信度 原图 769 | # # 识别模型固定尺寸只能100长度,需要处理可以根据自己场景导出模型 1000 770 | # # onnx可以支持动态,不受限 771 | # results, results_info = ocr_sys.recognition_img(dt_boxes) 772 | # print(f'opencv dnn :{str(results)}') 773 | # print('------------------------------') 774 | 775 | ocr_sys = det_rec_functions(image,use_dnn = False,version=3)# 支持v2和v3版本的 776 | stime = time.time() 777 | # 得到检测框 778 | dt_boxes = ocr_sys.get_boxes() 779 | ocr_sys.draw_boxes(dt_boxes[0],image,display=True) 780 | dettime = time.time() 781 | print(len(dt_boxes[0]),dt_boxes[0][:3]) 782 | # 识别 results: 单纯的识别结果,results_info: 识别结果+置信度 原图 783 | # 识别模型固定尺寸只能100长度,需要处理可以根据自己场景导出模型 1000 784 | # onnx可以支持动态,不受限 785 | results, results_info = ocr_sys.recognition_img(dt_boxes) 786 | print(f'results :{str(results)}',len(results)) 787 | print(f'results_info :{str(results_info)}') 788 | print(ocr_sys.get_format_text(ocr_sys.get_match_text_boxes(dt_boxes[0],results))) 789 | print(time.time()-dettime,dettime - stime) 790 | 791 | 792 | 793 | -------------------------------------------------------------------------------- /PaddleOCRModel/README.md: -------------------------------------------------------------------------------- 1 | # PaddleOCR-OpenCV-DNN 2 | 尝试 opencv dnn 推理 paddleocr ;大部分代码来自PaddleOCR项目,修改前后处理适配DNN推理。 3 | 4 | 5 | ## 环境: 6 | 7 | - onnx 1.11.0 8 | - onnxruntime 1.10.0 9 | - opencv 4.5.5.62 10 | - paddle2onnx 1.0.1 11 | - paddlpaddle 2.3.2 12 | 13 | ## 转换模型 14 | - 使用paddle2onnx 转换模型: 15 | 见aistudio项目地址: https://aistudio.baidu.com/aistudio/projectdetail/5672175?contributionType=1 16 | 17 | - onnx模型simplifier:https://convertmodel.com/ 18 | 19 | - 推理 20 | 21 | ## 不足: 22 | 23 | - 由于固化模型shape,使得部分特殊场景下效果不良,如长条形图片,降低了通用性;可根据自己对应的场景去固化模型尺寸(通用场景下建议导出dynamic shape模型,使用onnxruntime推理)。 24 | 25 | - 相同图片下(前后处理一致),速度方面DNN比onnxruntime慢(大约2~3倍)。 26 | - 精度未与原始paddle模型对比;简单测试效果尚可。 27 | - 测试PaddleOCR v3版本,det模型dnn支持,rec模型dnn推理。 28 | 29 | ## 引用: 30 | - https://github.com/PaddlePaddle/PaddleOCR 31 | - https://github.com/PaddlePaddle/Paddle2ONNX 32 | - https://github.com/daquexian/onnx-simplifier 33 | - https://blog.csdn.net/favorxin/article/details/115270800 34 | -------------------------------------------------------------------------------- /PaddleOCRModel/modelv3/det_model.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/PaddleOCRModel/modelv3/det_model.onnx -------------------------------------------------------------------------------- /PaddleOCRModel/modelv3/rec_model.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/PaddleOCRModel/modelv3/rec_model.onnx -------------------------------------------------------------------------------- /PaddleOCRModel/opencv_ocr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 6, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import sys\n", 10 | "import time\n", 11 | "import cv2\n", 12 | "import math\n", 13 | "import copy\n", 14 | "import onnxruntime\n", 15 | "import numpy as np\n", 16 | "import pyclipper\n", 17 | "from shapely.geometry import Polygon" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 7, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "class NormalizeImage(object):\n", 27 | " \"\"\" normalize image such as substract mean, divide std\n", 28 | " \"\"\"\n", 29 | "\n", 30 | " def __init__(self, scale=None, mean=None, std=None, order='chw', **kwargs):\n", 31 | " if isinstance(scale, str):\n", 32 | " scale = eval(scale)\n", 33 | " self.scale = np.float32(scale if scale is not None else 1.0 / 255.0)\n", 34 | " mean = mean if mean is not None else [0.485, 0.456, 0.406]\n", 35 | " std = std if std is not None else [0.229, 0.224, 0.225]\n", 36 | "\n", 37 | " shape = (3, 1, 1) if order == 'chw' else (1, 1, 3)\n", 38 | " self.mean = np.array(mean).reshape(shape).astype('float32')\n", 39 | " self.std = np.array(std).reshape(shape).astype('float32')\n", 40 | "\n", 41 | " def __call__(self, data):\n", 42 | " img = data['image']\n", 43 | " from PIL import Image\n", 44 | " if isinstance(img, Image.Image):\n", 45 | " img = np.array(img)\n", 46 | "\n", 47 | " assert isinstance(img,\n", 48 | " np.ndarray), \"invalid input 'img' in NormalizeImage\"\n", 49 | " data['image'] = (\n", 50 | " img.astype('float32') * self.scale - self.mean) / self.std\n", 51 | " return data\n", 52 | "\n", 53 | "\n", 54 | "class ToCHWImage(object):\n", 55 | " \"\"\" convert hwc image to chw image\n", 56 | " \"\"\"\n", 57 | "\n", 58 | " def __init__(self, **kwargs):\n", 59 | " pass\n", 60 | "\n", 61 | " def __call__(self, data):\n", 62 | " img = data['image']\n", 63 | " from PIL import Image\n", 64 | " if isinstance(img, Image.Image):\n", 65 | " img = np.array(img)\n", 66 | " data['image'] = img.transpose((2, 0, 1))\n", 67 | " return data\n", 68 | "\n", 69 | "\n", 70 | "class KeepKeys(object):\n", 71 | " def __init__(self, keep_keys, **kwargs):\n", 72 | " self.keep_keys = keep_keys\n", 73 | "\n", 74 | " def __call__(self, data):\n", 75 | " data_list = []\n", 76 | " for key in self.keep_keys:\n", 77 | " data_list.append(data[key])\n", 78 | " return data_list\n", 79 | "\n", 80 | "class DetResizeForTest(object):\n", 81 | " def __init__(self, **kwargs):\n", 82 | " super(DetResizeForTest, self).__init__()\n", 83 | " self.resize_type = 0\n", 84 | " self.keep_ratio = False\n", 85 | " if 'image_shape' in kwargs:\n", 86 | " self.image_shape = kwargs['image_shape']\n", 87 | " self.resize_type = 1\n", 88 | " if 'keep_ratio' in kwargs:\n", 89 | " self.keep_ratio = kwargs['keep_ratio']\n", 90 | " elif 'limit_side_len' in kwargs:\n", 91 | " self.limit_side_len = kwargs['limit_side_len']\n", 92 | " self.limit_type = kwargs.get('limit_type', 'min')\n", 93 | " elif 'resize_long' in kwargs:\n", 94 | " self.resize_type = 2\n", 95 | " self.resize_long = kwargs.get('resize_long', 640)\n", 96 | " else:\n", 97 | " self.limit_side_len = 736\n", 98 | " self.limit_type = 'min'\n", 99 | "\n", 100 | " def __call__(self, data):\n", 101 | " img = data['image']\n", 102 | " src_h, src_w, _ = img.shape\n", 103 | " #print(self.resize_type)\n", 104 | " if sum([src_h, src_w]) < 64:\n", 105 | " img = self.image_padding(img)\n", 106 | " if self.resize_type == 0:\n", 107 | " # img, shape = self.resize_image_type0(img)\n", 108 | " img, [ratio_h, ratio_w] = self.resize_image_type0(img)\n", 109 | " elif self.resize_type == 2:\n", 110 | " img, [ratio_h, ratio_w] = self.resize_image_type2(img)\n", 111 | " else:\n", 112 | " # img, shape = self.resize_image_type1(img)\n", 113 | " img, [ratio_h, ratio_w] = self.resize_image_type1(img)\n", 114 | " data['image'] = img\n", 115 | " data['shape'] = np.array([src_h, src_w, ratio_h, ratio_w])\n", 116 | " return data\n", 117 | "\n", 118 | " def image_padding(self, im, value=0):\n", 119 | " h, w, c = im.shape\n", 120 | " im_pad = np.zeros((max(32, h), max(32, w), c), np.uint8) + value\n", 121 | " im_pad[:h, :w, :] = im\n", 122 | " return im_pad\n", 123 | "\n", 124 | " def image_padding_640(self, im, value=0):\n", 125 | " h, w, c = im.shape\n", 126 | " im_pad = np.zeros((max(640, h), max(640, w), c), np.uint8) + value\n", 127 | " im_pad[:h, :w, :] = im\n", 128 | " return im_pad\n", 129 | "\n", 130 | " def resize_image_type1(self, img):\n", 131 | " resize_h, resize_w = self.image_shape\n", 132 | " ori_h, ori_w = img.shape[:2] # (h, w, c)\n", 133 | " if self.keep_ratio is True:\n", 134 | " resize_w = ori_w * resize_h / ori_h\n", 135 | " N = math.ceil(resize_w / 32)\n", 136 | " resize_w = N * 32\n", 137 | " ratio_h = float(resize_h) / ori_h\n", 138 | " ratio_w = float(resize_w) / ori_w\n", 139 | " \n", 140 | " img = cv2.resize(img, (int(resize_w), int(resize_h)))\n", 141 | " # return img, np.array([ori_h, ori_w])\n", 142 | " return img, [ratio_h, ratio_w]\n", 143 | "\n", 144 | " def resize_image_type0(self, img):\n", 145 | " \"\"\"\n", 146 | " resize image to a size multiple of 32 which is required by the network\n", 147 | " args:\n", 148 | " img(array): array with shape [h, w, c]\n", 149 | " return(tuple):\n", 150 | " img, (ratio_h, ratio_w)\n", 151 | " \"\"\"\n", 152 | " limit_side_len = self.limit_side_len\n", 153 | " h, w, c = img.shape\n", 154 | "\n", 155 | " # limit the max side\n", 156 | " if self.limit_type == 'max':\n", 157 | " if max(h, w) > limit_side_len:\n", 158 | " if h > w:\n", 159 | " ratio = float(limit_side_len) / h\n", 160 | " else:\n", 161 | " ratio = float(limit_side_len) / w\n", 162 | " else:\n", 163 | " ratio = 1.\n", 164 | " elif self.limit_type == 'min':\n", 165 | " if min(h, w) < limit_side_len:\n", 166 | " if h < w:\n", 167 | " ratio = float(limit_side_len) / h\n", 168 | " else:\n", 169 | " ratio = float(limit_side_len) / w\n", 170 | " else:\n", 171 | " ratio = 1.\n", 172 | " elif self.limit_type == 'resize_long':\n", 173 | " ratio = float(limit_side_len) / max(h, w)\n", 174 | " else:\n", 175 | " raise Exception('not support limit type, image ')\n", 176 | " resize_h = int(h * ratio)\n", 177 | " resize_w = int(w * ratio)\n", 178 | "\n", 179 | " resize_h = max(int(round(resize_h / 32) * 32), 32)\n", 180 | " resize_w = max(int(round(resize_w / 32) * 32), 32)\n", 181 | "\n", 182 | " try:\n", 183 | " if int(resize_w) <= 0 or int(resize_h) <= 0:\n", 184 | " return None, (None, None)\n", 185 | " img = cv2.resize(img, (int(resize_w), int(resize_h)))\n", 186 | " except:\n", 187 | " print(img.shape, resize_w, resize_h)\n", 188 | " sys.exit(0)\n", 189 | " ratio_h = resize_h / float(h)\n", 190 | " ratio_w = resize_w / float(w)\n", 191 | " return img, [ratio_h, ratio_w]\n", 192 | "\n", 193 | " def resize_image_type2(self, img):\n", 194 | " h, w, _ = img.shape\n", 195 | "\n", 196 | " resize_w = w\n", 197 | " resize_h = h\n", 198 | "\n", 199 | " if resize_h > resize_w:\n", 200 | " ratio = float(self.resize_long) / resize_h\n", 201 | " else:\n", 202 | " ratio = float(self.resize_long) / resize_w\n", 203 | "\n", 204 | " resize_h = int(resize_h * ratio)\n", 205 | " resize_w = int(resize_w * ratio)\n", 206 | " img = cv2.resize(img, (resize_w, resize_h))\n", 207 | " #这里加个0填充,适配固定shape模型\n", 208 | " img = self.image_padding_640(img)\n", 209 | " return img, [ratio, ratio]" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 8, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "### 检测结果后处理过程(得到检测框)\n", 219 | "class DBPostProcess(object):\n", 220 | " \"\"\"\n", 221 | " The post process for Differentiable Binarization (DB).\n", 222 | " \"\"\"\n", 223 | "\n", 224 | " def __init__(self,\n", 225 | " thresh=0.3,\n", 226 | " box_thresh=0.7,\n", 227 | " max_candidates=1000,\n", 228 | " unclip_ratio=2.0,\n", 229 | " use_dilation=False,\n", 230 | " **kwargs):\n", 231 | " self.thresh = thresh\n", 232 | " self.box_thresh = box_thresh\n", 233 | " self.max_candidates = max_candidates\n", 234 | " self.unclip_ratio = unclip_ratio\n", 235 | " self.min_size = 3\n", 236 | " self.dilation_kernel = np.array([[1, 1], [1, 1]]) if use_dilation else None\n", 237 | "\n", 238 | " def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height,ratio):\n", 239 | " '''\n", 240 | " _bitmap: single map with shape (1, H, W),\n", 241 | " whose values are binarized as {0, 1}\n", 242 | " '''\n", 243 | " bitmap = _bitmap\n", 244 | " #height, width = bitmap.shape \n", 245 | " height, width = dest_height*ratio, dest_width*ratio,\n", 246 | " \n", 247 | " outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST,\n", 248 | " cv2.CHAIN_APPROX_SIMPLE)\n", 249 | " if len(outs) == 3:\n", 250 | " img, contours, _ = outs[0], outs[1], outs[2]\n", 251 | " elif len(outs) == 2:\n", 252 | " contours, _ = outs[0], outs[1]\n", 253 | "\n", 254 | " num_contours = min(len(contours), self.max_candidates)\n", 255 | "\n", 256 | " boxes = []\n", 257 | " scores = []\n", 258 | " for index in range(num_contours):\n", 259 | " contour = contours[index]\n", 260 | " points, sside = self.get_mini_boxes(contour)\n", 261 | " if sside < self.min_size:\n", 262 | " continue\n", 263 | " points = np.array(points)\n", 264 | " score = self.box_score_fast(pred, points.reshape(-1, 2))\n", 265 | " if self.box_thresh > score:\n", 266 | " continue\n", 267 | "\n", 268 | " box = self.unclip(points).reshape(-1, 1, 2)\n", 269 | " box, sside = self.get_mini_boxes(box)\n", 270 | " if sside < self.min_size + 2:\n", 271 | " continue\n", 272 | " box = np.array(box)\n", 273 | "\n", 274 | " box[:, 0] = np.clip( # 640 * 661\n", 275 | " np.round(box[:, 0] / width * dest_width), 0, dest_width)\n", 276 | " box[:, 1] = np.clip(\n", 277 | " np.round(box[:, 1] / height * dest_height), 0, dest_height)\n", 278 | " boxes.append(box.astype(np.int16))\n", 279 | " scores.append(score)\n", 280 | " return np.array(boxes, dtype=np.int16), scores\n", 281 | "\n", 282 | " def unclip(self, box):\n", 283 | " unclip_ratio = self.unclip_ratio\n", 284 | " poly = Polygon(box)\n", 285 | " distance = poly.area * unclip_ratio / poly.length\n", 286 | " offset = pyclipper.PyclipperOffset()\n", 287 | " offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)\n", 288 | " return np.array(offset.Execute(distance))\n", 289 | "\n", 290 | " def get_mini_boxes(self, contour):\n", 291 | " bounding_box = cv2.minAreaRect(contour)\n", 292 | " points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0])\n", 293 | "\n", 294 | " index_1, index_2, index_3, index_4 = 0, 1, 2, 3\n", 295 | " if points[1][1] > points[0][1]:\n", 296 | " index_1 = 0\n", 297 | " index_4 = 1\n", 298 | " else:\n", 299 | " index_1 = 1\n", 300 | " index_4 = 0\n", 301 | " if points[3][1] > points[2][1]:\n", 302 | " index_2 = 2\n", 303 | " index_3 = 3\n", 304 | " else:\n", 305 | " index_2 = 3\n", 306 | " index_3 = 2\n", 307 | "\n", 308 | " box = [\n", 309 | " points[index_1], points[index_2], points[index_3], points[index_4]\n", 310 | " ]\n", 311 | " return box, min(bounding_box[1])\n", 312 | "\n", 313 | " def box_score_fast(self, bitmap, _box):\n", 314 | " h, w = bitmap.shape[:2]\n", 315 | " box = _box.copy()\n", 316 | " xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1)\n", 317 | " xmax = np.clip(np.ceil(box[:, 0].max()).astype(np.int), 0, w - 1)\n", 318 | " ymin = np.clip(np.floor(box[:, 1].min()).astype(np.int), 0, h - 1)\n", 319 | " ymax = np.clip(np.ceil(box[:, 1].max()).astype(np.int), 0, h - 1)\n", 320 | "\n", 321 | " mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8)\n", 322 | " box[:, 0] = box[:, 0] - xmin\n", 323 | " box[:, 1] = box[:, 1] - ymin\n", 324 | " cv2.fillPoly(mask, box.reshape(1, -1, 2).astype(np.int32), 1)\n", 325 | " return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0]\n", 326 | "\n", 327 | " def __call__(self, outs_dict, shape_list):\n", 328 | " pred = outs_dict\n", 329 | " pred = pred[:, 0, :, :]\n", 330 | " segmentation = pred > self.thresh\n", 331 | " boxes_batch = []\n", 332 | " for batch_index in range(pred.shape[0]):\n", 333 | " src_h, src_w, ratio_h, ratio_w = shape_list[batch_index]\n", 334 | " if self.dilation_kernel is not None:\n", 335 | " mask = cv2.dilate(\n", 336 | " np.array(segmentation[batch_index]).astype(np.uint8),\n", 337 | " self.dilation_kernel)\n", 338 | " else:\n", 339 | " mask = segmentation[batch_index]\n", 340 | " boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask,\n", 341 | " src_w, src_h,ratio_w)\n", 342 | " boxes_batch.append({'points': boxes})\n", 343 | " return boxes_batch\n" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 9, 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "## 根据推理结果解码识别结果\n", 353 | "class process_pred(object):\n", 354 | " def __init__(self, character_dict_path=None, character_type='ch', use_space_char=False):\n", 355 | " self.character_str = ''\n", 356 | " with open(character_dict_path, 'rb') as fin:\n", 357 | " lines = fin.readlines()\n", 358 | " for line in lines:\n", 359 | " line = line.decode('utf-8').strip('\\n').strip('\\r\\n')\n", 360 | " self.character_str += line\n", 361 | " if use_space_char:\n", 362 | " self.character_str += ' '\n", 363 | " dict_character = list(self.character_str)\n", 364 | "\n", 365 | " dict_character = self.add_special_char(dict_character)\n", 366 | " self.dict = {char: i for i, char in enumerate(dict_character)}\n", 367 | " self.character = dict_character\n", 368 | "\n", 369 | " def add_special_char(self, dict_character):\n", 370 | " dict_character = ['blank'] + dict_character\n", 371 | " return dict_character\n", 372 | "\n", 373 | " def decode(self, text_index, text_prob=None, is_remove_duplicate=False):\n", 374 | " result_list = []\n", 375 | " ignored_tokens = [0]\n", 376 | " batch_size = len(text_index)\n", 377 | " for batch_idx in range(batch_size):\n", 378 | " char_list = []\n", 379 | " conf_list = []\n", 380 | " for idx in range(len(text_index[batch_idx])):\n", 381 | " if text_index[batch_idx][idx] in ignored_tokens:\n", 382 | " continue\n", 383 | " if is_remove_duplicate and idx > 0 and text_index[batch_idx][idx - 1] == text_index[batch_idx][idx]:\n", 384 | " continue\n", 385 | " char_list.append(self.character[int(text_index[batch_idx][idx])])\n", 386 | " if text_prob is not None:\n", 387 | " conf_list.append(text_prob[batch_idx][idx])\n", 388 | " else:\n", 389 | " conf_list.append(1)\n", 390 | " text = ''.join(char_list)\n", 391 | " result_list.append((text, np.mean(conf_list)))\n", 392 | " return result_list\n", 393 | "\n", 394 | " def __call__(self, preds, label=None):\n", 395 | " if not isinstance(preds, np.ndarray):\n", 396 | " preds = np.array(preds)\n", 397 | " preds_idx = preds.argmax(axis=2)\n", 398 | " preds_prob = preds.max(axis=2)\n", 399 | " text = self.decode(preds_idx, preds_prob, is_remove_duplicate=True)\n", 400 | " if label is None:\n", 401 | " return text\n", 402 | " label = self.decode(label)\n", 403 | " return text, label\n" 404 | ] 405 | }, 406 | { 407 | "cell_type": "code", 408 | "execution_count": 10, 409 | "metadata": {}, 410 | "outputs": [], 411 | "source": [ 412 | "class det_rec_functions(object):\n", 413 | " def __init__(self, image,use_dnn = False):\n", 414 | " self.img = image.copy()\n", 415 | " self.det_file = './det_model.onnx'\n", 416 | " self.small_rec_file = './rec_model.onnx'\n", 417 | " self.model_shape = [3,32,1000]\n", 418 | " self.use_dnn = use_dnn\n", 419 | " if self.use_dnn == False:\n", 420 | " self.onet_det_session = onnxruntime.InferenceSession(self.det_file)\n", 421 | " self.onet_rec_session = onnxruntime.InferenceSession(self.small_rec_file)\n", 422 | " else:\n", 423 | " self.onet_det_session = cv2.dnn.readNetFromONNX(self.det_file)\n", 424 | " self.onet_rec_session = cv2.dnn.readNetFromONNX(self.small_rec_file)\n", 425 | " self.infer_before_process_op, self.det_re_process_op = self.get_process()\n", 426 | " self.postprocess_op = process_pred('./ppocr_keys_v1.txt', 'ch', True)\n", 427 | "\n", 428 | " ## 图片预处理过程\n", 429 | " def transform(self, data, ops=None):\n", 430 | " \"\"\" transform \"\"\"\n", 431 | " if ops is None:\n", 432 | " ops = []\n", 433 | " for op in ops:\n", 434 | " data = op(data)\n", 435 | " if data is None:\n", 436 | " return None\n", 437 | " return data\n", 438 | "\n", 439 | " def create_operators(self, op_param_list, global_config=None):\n", 440 | " \"\"\"\n", 441 | " create operators based on the config\n", 442 | "\n", 443 | " Args:\n", 444 | " params(list): a dict list, used to create some operators\n", 445 | " \"\"\"\n", 446 | " assert isinstance(op_param_list, list), ('operator config should be a list')\n", 447 | " ops = []\n", 448 | " for operator in op_param_list:\n", 449 | " assert isinstance(operator,\n", 450 | " dict) and len(operator) == 1, \"yaml format error\"\n", 451 | " op_name = list(operator)[0]\n", 452 | " param = {} if operator[op_name] is None else operator[op_name]\n", 453 | " if global_config is not None:\n", 454 | " param.update(global_config)\n", 455 | " op = eval(op_name)(**param)\n", 456 | " ops.append(op)\n", 457 | " return ops\n", 458 | "\n", 459 | " ### 检测框的后处理\n", 460 | " def order_points_clockwise(self, pts):\n", 461 | " \"\"\"\n", 462 | " reference from: https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py\n", 463 | " # sort the points based on their x-coordinates\n", 464 | " \"\"\"\n", 465 | " xSorted = pts[np.argsort(pts[:, 0]), :]\n", 466 | "\n", 467 | " # grab the left-most and right-most points from the sorted\n", 468 | " # x-roodinate points\n", 469 | " leftMost = xSorted[:2, :]\n", 470 | " rightMost = xSorted[2:, :]\n", 471 | "\n", 472 | " # now, sort the left-most coordinates according to their\n", 473 | " # y-coordinates so we can grab the top-left and bottom-left\n", 474 | " # points, respectively\n", 475 | " leftMost = leftMost[np.argsort(leftMost[:, 1]), :]\n", 476 | " (tl, bl) = leftMost\n", 477 | "\n", 478 | " rightMost = rightMost[np.argsort(rightMost[:, 1]), :]\n", 479 | " (tr, br) = rightMost\n", 480 | "\n", 481 | " rect = np.array([tl, tr, br, bl], dtype=\"float32\")\n", 482 | " return rect\n", 483 | "\n", 484 | " def clip_det_res(self, points, img_height, img_width):\n", 485 | " for pno in range(points.shape[0]):\n", 486 | " points[pno, 0] = int(min(max(points[pno, 0], 0), img_width - 1))\n", 487 | " points[pno, 1] = int(min(max(points[pno, 1], 0), img_height - 1))\n", 488 | " return points\n", 489 | " \n", 490 | " #shape_part_list = [661 969 7.74583964e-01 6.60474716e-01]\n", 491 | " def filter_tag_det_res(self, dt_boxes, shape_part_list):\n", 492 | " img_height, img_width = shape_part_list[0],shape_part_list[1]\n", 493 | " dt_boxes_new = []\n", 494 | " for box in dt_boxes:\n", 495 | " box = self.order_points_clockwise(box)\n", 496 | " box = self.clip_det_res(box, img_height, img_width) \n", 497 | " rect_width = int(np.linalg.norm(box[0] - box[1]))\n", 498 | " rect_height = int(np.linalg.norm(box[0] - box[3]))\n", 499 | " if rect_width <= 3 or rect_height <= 3:\n", 500 | " continue\n", 501 | " dt_boxes_new.append(box)\n", 502 | " dt_boxes = np.array(dt_boxes_new)\n", 503 | " return dt_boxes\n", 504 | "\n", 505 | " ### 定义图片前处理过程,和检测结果后处理过程\n", 506 | " def get_process(self):\n", 507 | " det_db_thresh = 0.3\n", 508 | " det_db_box_thresh = 0.3\n", 509 | " max_candidates = 2000\n", 510 | " unclip_ratio = 1.6\n", 511 | " use_dilation = True\n", 512 | " # DetResizeForTest 定义检测模型前处理规则\n", 513 | " pre_process_list = [{\n", 514 | " 'DetResizeForTest': {\n", 515 | " # 'limit_side_len': 2500,\n", 516 | " # 'limit_type': 'max',\n", 517 | " 'resize_long': 640\n", 518 | " # 'image_shape':[640,640],\n", 519 | " # 'keep_ratio':True,\n", 520 | " }\n", 521 | " }, {\n", 522 | " 'NormalizeImage': {\n", 523 | " 'std': [0.229, 0.224, 0.225],\n", 524 | " 'mean': [0.485, 0.456, 0.406],\n", 525 | " 'scale': '1./255.',\n", 526 | " 'order': 'hwc'\n", 527 | " }\n", 528 | " }, {\n", 529 | " 'ToCHWImage': None\n", 530 | " }, {\n", 531 | " 'KeepKeys': {\n", 532 | " 'keep_keys': ['image', 'shape']\n", 533 | " }\n", 534 | " }]\n", 535 | "\n", 536 | " infer_before_process_op = self.create_operators(pre_process_list)\n", 537 | " det_re_process_op = DBPostProcess(det_db_thresh, det_db_box_thresh, max_candidates, unclip_ratio, use_dilation)\n", 538 | " return infer_before_process_op, det_re_process_op\n", 539 | "\n", 540 | " def sorted_boxes(self, dt_boxes):\n", 541 | " \"\"\"\n", 542 | " Sort text boxes in order from top to bottom, left to right\n", 543 | " args:\n", 544 | " dt_boxes(array):detected text boxes with shape [4, 2]\n", 545 | " return:\n", 546 | " sorted boxes(array) with shape [4, 2]\n", 547 | " \"\"\"\n", 548 | " num_boxes = dt_boxes.shape[0]\n", 549 | " sorted_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0]))\n", 550 | " _boxes = list(sorted_boxes)\n", 551 | "\n", 552 | " for i in range(num_boxes - 1):\n", 553 | " if abs(_boxes[i + 1][0][1] - _boxes[i][0][1]) < 10 and \\\n", 554 | " (_boxes[i + 1][0][0] < _boxes[i][0][0]):\n", 555 | " tmp = _boxes[i]\n", 556 | " _boxes[i] = _boxes[i + 1]\n", 557 | " _boxes[i + 1] = tmp\n", 558 | " return _boxes\n", 559 | "\n", 560 | " ### 图像输入预处理\n", 561 | " def resize_norm_img(self, img):\n", 562 | " imgC, imgH, imgW = [int(v) for v in self.model_shape]\n", 563 | " assert imgC == img.shape[2]\n", 564 | " h, w = img.shape[:2]\n", 565 | " ratio = w / float(h)\n", 566 | " if math.ceil(imgH * ratio) > imgW:\n", 567 | " resized_w = imgW\n", 568 | " else:\n", 569 | " resized_w = int(math.ceil(imgH * ratio))\n", 570 | " resized_image = cv2.resize(img, (resized_w, imgH))\n", 571 | " resized_image = resized_image.astype('float32')\n", 572 | " resized_image = resized_image.transpose((2, 0, 1)) / 255\n", 573 | " resized_image -= 0.5\n", 574 | " resized_image /= 0.5\n", 575 | " padding_im = np.zeros((imgC, imgH, imgW), dtype=np.float32)\n", 576 | " padding_im[:, :, 0:resized_w] = resized_image\n", 577 | " return padding_im\n", 578 | "\n", 579 | " ## 推理检测图片中的部分\n", 580 | " def get_boxes(self):\n", 581 | " img_ori = self.img\n", 582 | " img_part = img_ori.copy()\n", 583 | " data_part = {'image': img_part}\n", 584 | " data_part = self.transform(data_part, self.infer_before_process_op)\n", 585 | " img_part, shape_part_list = data_part\n", 586 | " img_part = np.expand_dims(img_part, axis=0)\n", 587 | " shape_part_list = np.expand_dims(shape_part_list, axis=0)\n", 588 | " if self.use_dnn == True:\n", 589 | " self.onet_det_session.setInput(img_part) \n", 590 | " outs_part = self.onet_det_session.forward()\n", 591 | " else:\n", 592 | " inputs_part = {self.onet_det_session.get_inputs()[0].name: img_part}\n", 593 | " outs_part = self.onet_det_session.run(None, inputs_part)\n", 594 | " outs_part = outs_part[0]\n", 595 | " #print(outs_part.shape)\n", 596 | " post_res_part = self.det_re_process_op(outs_part, shape_part_list)\n", 597 | " dt_boxes_part = post_res_part[0]['points']\n", 598 | " dt_boxes_part = self.filter_tag_det_res(dt_boxes_part,shape_part_list[0])\n", 599 | " dt_boxes_part = self.sorted_boxes(dt_boxes_part)\n", 600 | "\n", 601 | " return dt_boxes_part,img_part\n", 602 | "\n", 603 | " ### 根据bounding box得到单元格图片\n", 604 | " def get_rotate_crop_image(self, img, points):\n", 605 | " img_crop_width = int(\n", 606 | " max(\n", 607 | " np.linalg.norm(points[0] - points[1]),\n", 608 | " np.linalg.norm(points[2] - points[3])))\n", 609 | " img_crop_height = int(\n", 610 | " max(\n", 611 | " np.linalg.norm(points[0] - points[3]),\n", 612 | " np.linalg.norm(points[1] - points[2])))\n", 613 | " pts_std = np.float32([[0, 0], [img_crop_width, 0],\n", 614 | " [img_crop_width, img_crop_height],\n", 615 | " [0, img_crop_height]])\n", 616 | " M = cv2.getPerspectiveTransform(points, pts_std)\n", 617 | " dst_img = cv2.warpPerspective(\n", 618 | " img,\n", 619 | " M, (img_crop_width, img_crop_height),\n", 620 | " borderMode=cv2.BORDER_REPLICATE,\n", 621 | " flags=cv2.INTER_CUBIC)\n", 622 | " dst_img_height, dst_img_width = dst_img.shape[0:2]\n", 623 | " if dst_img_height * 1.0 / dst_img_width >= 1.5:\n", 624 | " dst_img = np.rot90(dst_img)\n", 625 | " return dst_img\n", 626 | "\n", 627 | " ### 单张图片推理\n", 628 | " def get_img_res(self, onnx_model, img, process_op):\n", 629 | " img = self.resize_norm_img(img)\n", 630 | " img = img[np.newaxis, :]\n", 631 | " if self.use_dnn:\n", 632 | " onnx_model.setInput(img) # 设置模型输入\n", 633 | " outs = onnx_model.forward() # 推理出结果\n", 634 | " else:\n", 635 | " inputs = {onnx_model.get_inputs()[0].name: img}\n", 636 | " outs = onnx_model.run(None, inputs)\n", 637 | " outs = outs[0]\n", 638 | " return process_op(outs)\n", 639 | "\n", 640 | " def recognition_img(self, dt_boxes):\n", 641 | " img_ori = self.img #原图大小\n", 642 | " img = img_ori.copy()\n", 643 | " img_list = []\n", 644 | " for box in dt_boxes[0]:\n", 645 | " tmp_box = copy.deepcopy(box)\n", 646 | " img_crop = self.get_rotate_crop_image(img, tmp_box)\n", 647 | " img_list.append(img_crop)\n", 648 | "\n", 649 | " ## 识别小图片\n", 650 | " results = []\n", 651 | " results_info = []\n", 652 | " for pic in img_list:\n", 653 | " res = self.get_img_res(self.onet_rec_session, pic, self.postprocess_op)\n", 654 | " results.append(res[0])\n", 655 | " results_info.append(res)\n", 656 | " return results, results_info\n" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": 11, 662 | "metadata": {}, 663 | "outputs": [ 664 | { 665 | "ename": "AttributeError", 666 | "evalue": "module 'cv2.dnn' has no attribute 'readNetFromONNX'", 667 | "output_type": "error", 668 | "traceback": [ 669 | "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", 670 | "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", 671 | "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_1964\\2969005901.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;31m# 文本检测\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;31m# 模型固化为640*640 需要修改对应前处理,box的后处理。\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[0mocr_sys\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdet_rec_functions\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mimage\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0muse_dnn\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;32mTrue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[1;31m# 得到检测框\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[0mdt_boxes\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mocr_sys\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_boxes\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 672 | "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_1964\\804329560.py\u001b[0m in \u001b[0;36m__init__\u001b[1;34m(self, image, use_dnn)\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0monet_rec_session\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0monnxruntime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mInferenceSession\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msmall_rec_file\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 11\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 12\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0monet_det_session\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdnn\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mreadNetFromONNX\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdet_file\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 13\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0monet_rec_session\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mcv2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdnn\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mreadNetFromONNX\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msmall_rec_file\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 14\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0minfer_before_process_op\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdet_re_process_op\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_process\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", 673 | "\u001b[1;31mAttributeError\u001b[0m: module 'cv2.dnn' has no attribute 'readNetFromONNX'" 674 | ] 675 | } 676 | ], 677 | "source": [ 678 | "if __name__=='__main__':\n", 679 | " # 读取图片\n", 680 | " image = cv2.imread('./00.png')\n", 681 | " # 文本检测\n", 682 | " # 模型固化为640*640 需要修改对应前处理,box的后处理。\n", 683 | " ocr_sys = det_rec_functions(image,use_dnn = True)\n", 684 | " # 得到检测框\n", 685 | " dt_boxes = ocr_sys.get_boxes()\n", 686 | " # 识别 results: 单纯的识别结果,results_info: 识别结果+置信度 原图\n", 687 | " # 识别模型固定尺寸只能100长度,需要处理可以根据自己场景导出模型 1000\n", 688 | " # onnx可以支持动态,不受限\n", 689 | " results, results_info = ocr_sys.recognition_img(dt_boxes)\n", 690 | " print(f'opencv dnn :{str(results)}')\n", 691 | " print('------------------------------')\n", 692 | " ocr_sys = det_rec_functions(image,use_dnn = False)\n", 693 | " # 得到检测框\n", 694 | " dt_boxes = ocr_sys.get_boxes()\n", 695 | " # 识别 results: 单纯的识别结果,results_info: 识别结果+置信度 原图\n", 696 | " # 识别模型固定尺寸只能100长度,需要处理可以根据自己场景导出模型 1000\n", 697 | " # onnx可以支持动态,不受限\n", 698 | " results, results_info = ocr_sys.recognition_img(dt_boxes)\n", 699 | " print(f'onnxruntime :{str(results)}')\n" 700 | ] 701 | } 702 | ], 703 | "metadata": { 704 | "kernelspec": { 705 | "display_name": "Python 3.6.13 ('paddle')", 706 | "language": "python", 707 | "name": "python3" 708 | }, 709 | "language_info": { 710 | "codemirror_mode": { 711 | "name": "ipython", 712 | "version": 3 713 | }, 714 | "file_extension": ".py", 715 | "mimetype": "text/x-python", 716 | "name": "python", 717 | "nbconvert_exporter": "python", 718 | "pygments_lexer": "ipython3", 719 | "version": "3.7.8" 720 | }, 721 | "orig_nbformat": 4, 722 | "vscode": { 723 | "interpreter": { 724 | "hash": "8400e034ae209d81f9114f403eb7f9856a4c9161d3b220bef5747c4b976a2b28" 725 | } 726 | } 727 | }, 728 | "nbformat": 4, 729 | "nbformat_minor": 2 730 | } 731 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jamscreenshot 2 | 一个用python和pyqt5实现的类似微信QQ截屏的工具源码,提取自本人自制工具集[Jamtools](https://github.com/fandesfyf/JamTools)里面的截屏部分功能整合,代码完全原创,分享出来大家一起学习鸭! 转载请标一下出处谢谢! 3 | > 注意这个仓库中的代码可能会落后于[Jamtools](https://github.com/fandesfyf/JamTools)仓库, 一般都是先更新到Jamtools之后再提取截屏功能的更新 4 | 5 | - 如果你还需要滚动截屏功能, (本仓库中也已经集成了滚动截屏功能了)也可以看看[这个](https://github.com/fandesfyf/roll_screenshot) 6 | - 录屏功能在[Jamtools](https://github.com/fandesfyf/JamTools)提供,有时间再整理出来 7 | 8 |
9 | 10 |   11 |   12 | 13 | 访客
14 | 15 | 16 | ### 酱截屏(全局快捷键Alt+z) 17 | - [Jamtools](https://github.com/fandesfyf/JamTools)中的截屏功能页面,包含隐藏窗口、自动保存文件、复制文件还是图像数据、滚动截屏等参数的配置,可以直接从release下载安装程序 18 | 19 | ![image](image/jp.png) 20 | 21 | - 支持截屏时选区录屏、文字识别(离线)、翻译等 22 | ![image](image/截屏文字识别.gif) 23 | 24 | - 截屏时有各种画笔橡皮擦工具、透视裁剪、油漆桶、多边形截图等工具 25 | 26 | ![image](image/jp0.jpg) 27 | ![image](image/jp1.jpg) 28 | 29 | - 支持将截屏固定到屏幕上,固定的截屏可以快速放大缩小(鼠标滚轮)、设置透明度、边框、置顶、文字识别等方便的操作 30 | ![image](image/固定截屏.gif) 31 | 32 | ## 更新 33 | ----20230407更新------ 34 | 35 | 增加了离线文字识别功能 36 | 37 | ----20210206更新------ 38 | 39 | 新增了透视裁剪工具(类似于PS里的用法)、多边形截图工具、取色器工具、油漆桶工具、背景还原画笔(配合背景橡皮擦使用)、支持回退10步操作历史记录、新增一键还原按钮、新增智能选框的开关。画笔等增加透明度支持,在画笔/标记时可以通过按住ctrl键+滚轮快速调整画笔透明度,新增常用颜色到取色按钮(鼠标划过即可显示)、固定截屏在屏幕上时可以通过按住ctrl+滚轮快速调节截屏的透明度 40 | 41 | # 效果图 42 | 加了一个简陋的主界面 43 | 44 | ![image](image/60430e4e61d28d0e79da9d58e46037f.png) 45 | 46 | 截图效果: 47 | ![image](image/jp00.png) 48 | 49 | ![image](image/jp2.png) 50 | 51 | ![image](image/58e820362dd287f6668e011e20a1020.png) 52 | 53 | ![image](image/0180a5748681abe7254ce6734aa64de.png) 54 | 可以看到,几乎实现了微信截图的所有功能,还有一些微信截图没有的功能,像材质图片画笔、背景橡皮擦、所有颜色自选、画笔大小/放大镜倍数可通过滑轮调节等; 55 | 代码总长2000+行,直接运行即可! 56 | 57 | 58 | -----------------2020.4.9更新-------------- 59 | 60 | 更新: 61 | 支持把多个图片固定在屏幕上 62 | 63 | 支持窗口控件识别(基于opencv的轮廓识别功能),需要opencv库! 64 | 65 | 直接pip install opencv-python即可(滚动截屏需要安装contrib版本的opencv,版本小于opencv-contrib-python==3.4.2.17) 66 | 67 | 68 | # 模块安装 69 | 主要使用的是PyQt5模块 70 | 直接 pip install PyQt5 即可 71 | 还需要PIL 72 | 直接pip install Pillow 即可 73 | 74 | 附带的jamresourse.py文件是图片资源文件(鼠标样式等) 75 | 76 | # 提交环境为python3.7 pyqt5==5.13.2 win10 一切正常! 77 | 其他环境自行测试 78 | 79 | # 说一说大概的思路吧 80 | 截屏流程: 81 | 82 | 先分析用户动作:用户点击截屏按钮(或按下快捷键)时截屏软件开始响应(通过一个按钮事件或者pyqtsignal,其实都是signal,来调用起截屏函数screen_shot),迅速截下当前屏幕的全屏内容(通过pyqt的grabWindow函数),同时显示截屏界面。 83 | 84 | 85 | 对截屏界面有几点说明: 86 | 87 | 1.截屏界面就是一个全屏窗口而已,该窗口是一个label类型的控件(因此可以直接将其当做背景层)有置顶、无边框、鼠标追踪等属性 88 | 89 | 2.截屏界面由背景层(Slabel本身)、绘图层(PaintLayer类)和遮罩层(MaskLayer类)依次堆叠而成,每一层都是一个Qlabel,绘图层和遮罩成以Slabel作为parent,并调用self.parent.xxx直接获得Slabel的属性。背景层用于显示之前截屏时的那个全屏内容,因为之前那个截屏是全屏幕截的,当前窗口又是全屏窗口,所以显示背景中的内容的位置就是之前在屏幕中实际的位置,而且这个时间很短,看起来的结果就像是用户直接操作在屏幕上一样;第二层是绘图层,有透明背景属性,用于用户进行涂鸦等操作(画笔中除了背景相关的画笔几乎都作用于这一层);最上面一层是遮罩层,该层主要用于显示截屏的阴影部分和方框(只是显示而已,背后的逻辑还是在它的parent即Slabel中) 90 | 91 | 区域截屏过程:在进入截屏界面后用户可以点击屏幕(该动作由mousePressEvent捕获),然后拖动(由mouseMoveEvent捕获),然后松开(由mouseReleaseEvent捕获),同时弹出确定按钮(botton_box)即可在界面上显示出选区界面。 92 | 93 | 关于选框参数,所有参数均在Slabel主类中设置,self.x0,x1,y0,y1是选区的对角坐标,在用户点击下鼠标左键时,记录下当前的位置,然后动鼠标时记录下鼠标位置,当松开鼠标时记录下松开的位置.注意每次点击/移动/松开鼠标都会调用update函数使得所有层(包括遮罩层)的界面更新(即自动调用了paintEvent函数) 94 | 95 | -------------------------------------------------------------------------------- /fake_useragent_0.1.11.json: -------------------------------------------------------------------------------- 1 | {"browsers": {"chrome": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", "Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36", "Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36", "Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36", "Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", "Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17"], "internetexplorer": ["Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko", "Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0; InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64)", "Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0)", "Mozilla/4.0 (Compatible; MSIE 8.0; Windows NT 5.2; Trident/6.0)", "Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)", "Mozilla/1.22 (compatible; MSIE 10.0; Windows 3.1)", "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))", "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; yie8)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET CLR 1.1.4322; .NET4.0C; Tablet PC 2.0)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; FunWebProducts)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/13.0.782.215)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.1; SV1; .NET CLR 2.8.52393; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57)", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.8.36217; WOW64; en-US)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 2.7.58687; SLCC2; Media Center PC 5.0; Zune 3.4; Tablet PC 3.6; InfoPath.3)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; SLCC1; .NET CLR 1.1.4322)", "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30)", "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2)", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0)"], "firefox": ["Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0", "Mozilla/5.0 (X11; Linux i586; rv:63.0) Gecko/20100101 Firefox/63.0", "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:63.0) Gecko/20100101 Firefox/63.0", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.10; rv:62.0) Gecko/20100101 Firefox/62.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:10.0) Gecko/20100101 Firefox/62.0", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; ko; rv:1.9.1b2) Gecko/20081201 Firefox/60.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/58.0.1", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/58.0", "Mozilla/5.0 (Windows NT 6.3; WOW64; rv:52.59.12) Gecko/20160044 Firefox/52.59.12", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20060814 Firefox/51.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20120121 Firefox/46.0", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.66.18) Gecko/20177177 Firefox/45.66.18", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", "Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0", "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:28.0) Gecko/20100101 Firefox/31.0", "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0", "Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0", "Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0", "Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0", "Mozilla/5.0 (Windows NT 6.2; rv:20.0) Gecko/20121202 Firefox/26.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0", "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0", "Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0", "Mozilla/5.0 (Microsoft Windows NT 6.2.9200.0); rv:22.0) Gecko/20130405 Firefox/22.0", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1", "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20130514 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0"], "safari": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A", "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; da-dk) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1", "Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ko-KR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr-FR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.1; cs-CZ) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; sv-se) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ko-kr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; it-it) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-fr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; es-es) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-us) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-gb) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", "Mozilla/5.0 (Windows; U; Windows NT 6.1; sv-SE) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; hu-HU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us) AppleWebKit/534.16+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-ch) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; de-de) AppleWebKit/534.15+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; ar) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Android 2.2; Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-HK) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 6.0; tr-TR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 6.0; nb-NO) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; zh-cn) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5"], "opera": ["Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16", "Opera/9.80 (Macintosh; Intel Mac OS X 10.14.1) Presto/2.12.388 Version/12.16", "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14", "Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14", "Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02", "Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00", "Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00", "Opera/12.0(Windows NT 5.2;U;en)Presto/22.9.168 Version/12.00", "Opera/12.0(Windows NT 5.1;U;en)Presto/22.9.168 Version/12.00", "Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0", "Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62", "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.10.229 Version/11.62", "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52", "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52", "Opera/9.80 (Windows NT 5.1; U; en) Presto/2.9.168 Version/11.51", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51", "Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50", "Opera/9.80 (X11; Linux i686; U; hu) Presto/2.9.168 Version/11.50", "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11", "Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.8.131 Version/11.11", "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/5.0 Opera 11.11", "Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10", "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.8.99 Version/11.10", "Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10", "Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1", "Opera/9.80 (X11; Linux x86_64; U; Ubuntu/10.10 (maverick); pl) Presto/2.7.62 Version/11.01", "Opera/9.80 (X11; Linux i686; U; ja) Presto/2.7.62 Version/11.01", "Opera/9.80 (X11; Linux i686; U; fr) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; sv) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; en-US) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.1; U;) Presto/2.7.62 Version/11.01", "Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.7.62 Version/11.01", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101213 Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01", "Mozilla/5.0 (Windows NT 6.1; U; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", "Mozilla/5.0 (Windows NT 6.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; de) Opera 11.01", "Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00", "Opera/9.80 (X11; Linux i686; U; it) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.37 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; ko) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1; U; en-GB) Presto/2.7.62 Version/11.00", "Opera/9.80 (Windows NT 6.1 x64; U; en) Presto/2.7.62 Version/11.00"]}, "randomize": {"0": "chrome", "1": "chrome", "2": "chrome", "3": "chrome", "4": "chrome", "5": "chrome", "6": "chrome", "7": "chrome", "8": "chrome", "9": "chrome", "10": "chrome", "11": "chrome", "12": "chrome", "13": "chrome", "14": "chrome", "15": "chrome", "16": "chrome", "17": "chrome", "18": "chrome", "19": "chrome", "20": "chrome", "21": "chrome", "22": "chrome", "23": "chrome", "24": "chrome", "25": "chrome", "26": "chrome", "27": "chrome", "28": "chrome", "29": "chrome", "30": "chrome", "31": "chrome", "32": "chrome", "33": "chrome", "34": "chrome", "35": "chrome", "36": "chrome", "37": "chrome", "38": "chrome", "39": "chrome", "40": "chrome", "41": "chrome", "42": "chrome", "43": "chrome", "44": "chrome", "45": "chrome", "46": "chrome", "47": "chrome", "48": "chrome", "49": "chrome", "50": "chrome", "51": "chrome", "52": "chrome", "53": "chrome", "54": "chrome", "55": "chrome", "56": "chrome", "57": "chrome", "58": "chrome", "59": "chrome", "60": "chrome", "61": "chrome", "62": "chrome", "63": "chrome", "64": "chrome", "65": "chrome", "66": "chrome", "67": "chrome", "68": "chrome", "69": "chrome", "70": "chrome", "71": "chrome", "72": "chrome", "73": "chrome", "74": "chrome", "75": "chrome", "76": "chrome", "77": "chrome", "78": "chrome", "79": "chrome", "80": "chrome", "81": "chrome", "82": "chrome", "83": "chrome", "84": "chrome", "85": "chrome", "86": "chrome", "87": "chrome", "88": "chrome", "89": "chrome", "90": "chrome", "91": "chrome", "92": "chrome", "93": "chrome", "94": "chrome", "95": "chrome", "96": "chrome", "97": "chrome", "98": "chrome", "99": "chrome", "100": "chrome", "101": "chrome", "102": "chrome", "103": "chrome", "104": "chrome", "105": "chrome", "106": "chrome", "107": "chrome", "108": "chrome", "109": "chrome", "110": "chrome", "111": "chrome", "112": "chrome", "113": "chrome", "114": "chrome", "115": "chrome", "116": "chrome", "117": "chrome", "118": "chrome", "119": "chrome", "120": "chrome", "121": "chrome", "122": "chrome", "123": "chrome", "124": "chrome", "125": "chrome", "126": "chrome", "127": "chrome", "128": "chrome", "129": "chrome", "130": "chrome", "131": "chrome", "132": "chrome", "133": "chrome", "134": "chrome", "135": "chrome", "136": "chrome", "137": "chrome", "138": "chrome", "139": "chrome", "140": "chrome", "141": "chrome", "142": "chrome", "143": "chrome", "144": "chrome", "145": "chrome", "146": "chrome", "147": "chrome", "148": "chrome", "149": "chrome", "150": "chrome", "151": "chrome", "152": "chrome", "153": "chrome", "154": "chrome", "155": "chrome", "156": "chrome", "157": "chrome", "158": "chrome", "159": "chrome", "160": "chrome", "161": "chrome", "162": "chrome", "163": "chrome", "164": "chrome", "165": "chrome", "166": "chrome", "167": "chrome", "168": "chrome", "169": "chrome", "170": "chrome", "171": "chrome", "172": "chrome", "173": "chrome", "174": "chrome", "175": "chrome", "176": "chrome", "177": "chrome", "178": "chrome", "179": "chrome", "180": "chrome", "181": "chrome", "182": "chrome", "183": "chrome", "184": "chrome", "185": "chrome", "186": "chrome", "187": "chrome", "188": "chrome", "189": "chrome", "190": "chrome", "191": "chrome", "192": "chrome", "193": "chrome", "194": "chrome", "195": "chrome", "196": "chrome", "197": "chrome", "198": "chrome", "199": "chrome", "200": "chrome", "201": "chrome", "202": "chrome", "203": "chrome", "204": "chrome", "205": "chrome", "206": "chrome", "207": "chrome", "208": "chrome", "209": "chrome", "210": "chrome", "211": "chrome", "212": "chrome", "213": "chrome", "214": "chrome", "215": "chrome", "216": "chrome", "217": "chrome", "218": "chrome", "219": "chrome", "220": "chrome", "221": "chrome", "222": "chrome", "223": "chrome", "224": "chrome", "225": "chrome", "226": "chrome", "227": "chrome", "228": "chrome", "229": "chrome", "230": "chrome", "231": "chrome", "232": "chrome", "233": "chrome", "234": "chrome", "235": "chrome", "236": "chrome", "237": "chrome", "238": "chrome", "239": "chrome", "240": "chrome", "241": "chrome", "242": "chrome", "243": "chrome", "244": "chrome", "245": "chrome", "246": "chrome", "247": "chrome", "248": "chrome", "249": "chrome", "250": "chrome", "251": "chrome", "252": "chrome", "253": "chrome", "254": "chrome", "255": "chrome", "256": "chrome", "257": "chrome", "258": "chrome", "259": "chrome", "260": "chrome", "261": "chrome", "262": "chrome", "263": "chrome", "264": "chrome", "265": "chrome", "266": "chrome", "267": "chrome", "268": "chrome", "269": "chrome", "270": "chrome", "271": "chrome", "272": "chrome", "273": "chrome", "274": "chrome", "275": "chrome", "276": "chrome", "277": "chrome", "278": "chrome", "279": "chrome", "280": "chrome", "281": "chrome", "282": "chrome", "283": "chrome", "284": "chrome", "285": "chrome", "286": "chrome", "287": "chrome", "288": "chrome", "289": "chrome", "290": "chrome", "291": "chrome", "292": "chrome", "293": "chrome", "294": "chrome", "295": "chrome", "296": "chrome", "297": "chrome", "298": "chrome", "299": "chrome", "300": "chrome", "301": "chrome", "302": "chrome", "303": "chrome", "304": "chrome", "305": "chrome", "306": "chrome", "307": "chrome", "308": "chrome", "309": "chrome", "310": "chrome", "311": "chrome", "312": "chrome", "313": "chrome", "314": "chrome", "315": "chrome", "316": "chrome", "317": "chrome", "318": "chrome", "319": "chrome", "320": "chrome", "321": "chrome", "322": "chrome", "323": "chrome", "324": "chrome", "325": "chrome", "326": "chrome", "327": "chrome", "328": "chrome", "329": "chrome", "330": "chrome", "331": "chrome", "332": "chrome", "333": "chrome", "334": "chrome", "335": "chrome", "336": "chrome", "337": "chrome", "338": "chrome", "339": "chrome", "340": "chrome", "341": "chrome", "342": "chrome", "343": "chrome", "344": "chrome", "345": "chrome", "346": "chrome", "347": "chrome", "348": "chrome", "349": "chrome", "350": "chrome", "351": "chrome", "352": "chrome", "353": "chrome", "354": "chrome", "355": "chrome", "356": "chrome", "357": "chrome", "358": "chrome", "359": "chrome", "360": "chrome", "361": "chrome", "362": "chrome", "363": "chrome", "364": "chrome", "365": "chrome", "366": "chrome", "367": "chrome", "368": "chrome", "369": "chrome", "370": "chrome", "371": "chrome", "372": "chrome", "373": "chrome", "374": "chrome", "375": "chrome", "376": "chrome", "377": "chrome", "378": "chrome", "379": "chrome", "380": "chrome", "381": "chrome", "382": "chrome", "383": "chrome", "384": "chrome", "385": "chrome", "386": "chrome", "387": "chrome", "388": "chrome", "389": "chrome", "390": "chrome", "391": "chrome", "392": "chrome", "393": "chrome", "394": "chrome", "395": "chrome", "396": "chrome", "397": "chrome", "398": "chrome", "399": "chrome", "400": "chrome", "401": "chrome", "402": "chrome", "403": "chrome", "404": "chrome", "405": "chrome", "406": "chrome", "407": "chrome", "408": "chrome", "409": "chrome", "410": "chrome", "411": "chrome", "412": "chrome", "413": "chrome", "414": "chrome", "415": "chrome", "416": "chrome", "417": "chrome", "418": "chrome", "419": "chrome", "420": "chrome", "421": "chrome", "422": "chrome", "423": "chrome", "424": "chrome", "425": "chrome", "426": "chrome", "427": "chrome", "428": "chrome", "429": "chrome", "430": "chrome", "431": "chrome", "432": "chrome", "433": "chrome", "434": "chrome", "435": "chrome", "436": "chrome", "437": "chrome", "438": "chrome", "439": "chrome", "440": "chrome", "441": "chrome", "442": "chrome", "443": "chrome", "444": "chrome", "445": "chrome", "446": "chrome", "447": "chrome", "448": "chrome", "449": "chrome", "450": "chrome", "451": "chrome", "452": "chrome", "453": "chrome", "454": "chrome", "455": "chrome", "456": "chrome", "457": "chrome", "458": "chrome", "459": "chrome", "460": "chrome", "461": "chrome", "462": "chrome", "463": "chrome", "464": "chrome", "465": "chrome", "466": "chrome", "467": "chrome", "468": "chrome", "469": "chrome", "470": "chrome", "471": "chrome", "472": "chrome", "473": "chrome", "474": "chrome", "475": "chrome", "476": "chrome", "477": "chrome", "478": "chrome", "479": "chrome", "480": "chrome", "481": "chrome", "482": "chrome", "483": "chrome", "484": "chrome", "485": "chrome", "486": "chrome", "487": "chrome", "488": "chrome", "489": "chrome", "490": "chrome", "491": "chrome", "492": "chrome", "493": "chrome", "494": "chrome", "495": "chrome", "496": "chrome", "497": "chrome", "498": "chrome", "499": "chrome", "500": "chrome", "501": "chrome", "502": "chrome", "503": "chrome", "504": "chrome", "505": "chrome", "506": "chrome", "507": "chrome", "508": "chrome", "509": "chrome", "510": "chrome", "511": "chrome", "512": "chrome", "513": "chrome", "514": "chrome", "515": "chrome", "516": "chrome", "517": "chrome", "518": "chrome", "519": "chrome", "520": "chrome", "521": "chrome", "522": "chrome", "523": "chrome", "524": "chrome", "525": "chrome", "526": "chrome", "527": "chrome", "528": "chrome", "529": "chrome", "530": "chrome", "531": "chrome", "532": "chrome", "533": "chrome", "534": "chrome", "535": "chrome", "536": "chrome", "537": "chrome", "538": "chrome", "539": "chrome", "540": "chrome", "541": "chrome", "542": "chrome", "543": "chrome", "544": "chrome", "545": "chrome", "546": "chrome", "547": "chrome", "548": "chrome", "549": "chrome", "550": "chrome", "551": "chrome", "552": "chrome", "553": "chrome", "554": "chrome", "555": "chrome", "556": "chrome", "557": "chrome", "558": "chrome", "559": "chrome", "560": "chrome", "561": "chrome", "562": "chrome", "563": "chrome", "564": "chrome", "565": "chrome", "566": "chrome", "567": "chrome", "568": "chrome", "569": "chrome", "570": "chrome", "571": "chrome", "572": "chrome", "573": "chrome", "574": "chrome", "575": "chrome", "576": "chrome", "577": "chrome", "578": "chrome", "579": "chrome", "580": "chrome", "581": "chrome", "582": "chrome", "583": "chrome", "584": "chrome", "585": "chrome", "586": "chrome", "587": "chrome", "588": "chrome", "589": "chrome", "590": "chrome", "591": "chrome", "592": "chrome", "593": "chrome", "594": "chrome", "595": "chrome", "596": "chrome", "597": "chrome", "598": "chrome", "599": "chrome", "600": "chrome", "601": "chrome", "602": "chrome", "603": "chrome", "604": "chrome", "605": "chrome", "606": "chrome", "607": "chrome", "608": "chrome", "609": "chrome", "610": "chrome", "611": "chrome", "612": "chrome", "613": "chrome", "614": "chrome", "615": "chrome", "616": "chrome", "617": "chrome", "618": "chrome", "619": "chrome", "620": "chrome", "621": "chrome", "622": "chrome", "623": "chrome", "624": "chrome", "625": "chrome", "626": "chrome", "627": "chrome", "628": "chrome", "629": "chrome", "630": "chrome", "631": "chrome", "632": "chrome", "633": "chrome", "634": "chrome", "635": "chrome", "636": "chrome", "637": "chrome", "638": "chrome", "639": "chrome", "640": "chrome", "641": "chrome", "642": "chrome", "643": "chrome", "644": "chrome", "645": "chrome", "646": "chrome", "647": "chrome", "648": "chrome", "649": "chrome", "650": "chrome", "651": "chrome", "652": "chrome", "653": "chrome", "654": "chrome", "655": "chrome", "656": "chrome", "657": "chrome", "658": "chrome", "659": "chrome", "660": "chrome", "661": "chrome", "662": "chrome", "663": "chrome", "664": "chrome", "665": "chrome", "666": "chrome", "667": "chrome", "668": "chrome", "669": "chrome", "670": "chrome", "671": "chrome", "672": "chrome", "673": "chrome", "674": "chrome", "675": "chrome", "676": "chrome", "677": "chrome", "678": "chrome", "679": "chrome", "680": "chrome", "681": "chrome", "682": "chrome", "683": "chrome", "684": "chrome", "685": "chrome", "686": "chrome", "687": "chrome", "688": "chrome", "689": "chrome", "690": "chrome", "691": "chrome", "692": "chrome", "693": "chrome", "694": "chrome", "695": "chrome", "696": "chrome", "697": "chrome", "698": "chrome", "699": "chrome", "700": "chrome", "701": "chrome", "702": "chrome", "703": "chrome", "704": "chrome", "705": "chrome", "706": "chrome", "707": "chrome", "708": "chrome", "709": "chrome", "710": "chrome", "711": "chrome", "712": "chrome", "713": "chrome", "714": "chrome", "715": "chrome", "716": "chrome", "717": "chrome", "718": "chrome", "719": "chrome", "720": "chrome", "721": "chrome", "722": "chrome", "723": "chrome", "724": "chrome", "725": "chrome", "726": "chrome", "727": "chrome", "728": "chrome", "729": "chrome", "730": "chrome", "731": "chrome", "732": "chrome", "733": "chrome", "734": "chrome", "735": "chrome", "736": "chrome", "737": "chrome", "738": "chrome", "739": "chrome", "740": "chrome", "741": "chrome", "742": "chrome", "743": "chrome", "744": "chrome", "745": "chrome", "746": "chrome", "747": "chrome", "748": "chrome", "749": "chrome", "750": "chrome", "751": "chrome", "752": "chrome", "753": "chrome", "754": "chrome", "755": "chrome", "756": "chrome", "757": "chrome", "758": "chrome", "759": "chrome", "760": "chrome", "761": "chrome", "762": "chrome", "763": "chrome", "764": "chrome", "765": "chrome", "766": "chrome", "767": "chrome", "768": "chrome", "769": "chrome", "770": "chrome", "771": "chrome", "772": "chrome", "773": "chrome", "774": "chrome", "775": "chrome", "776": "chrome", "777": "chrome", "778": "chrome", "779": "chrome", "780": "chrome", "781": "chrome", "782": "chrome", "783": "chrome", "784": "chrome", "785": "chrome", "786": "chrome", "787": "chrome", "788": "chrome", "789": "chrome", "790": "chrome", "791": "chrome", "792": "chrome", "793": "chrome", "794": "chrome", "795": "chrome", "796": "chrome", "797": "chrome", "798": "chrome", "799": "chrome", "800": "chrome", "801": "chrome", "802": "chrome", "803": "internetexplorer", "804": "internetexplorer", "805": "internetexplorer", "806": "internetexplorer", "807": "internetexplorer", "808": "internetexplorer", "809": "internetexplorer", "810": "internetexplorer", "811": "internetexplorer", "812": "internetexplorer", "813": "internetexplorer", "814": "internetexplorer", "815": "internetexplorer", "816": "internetexplorer", "817": "internetexplorer", "818": "internetexplorer", "819": "internetexplorer", "820": "internetexplorer", "821": "internetexplorer", "822": "internetexplorer", "823": "internetexplorer", "824": "internetexplorer", "825": "internetexplorer", "826": "internetexplorer", "827": "internetexplorer", "828": "internetexplorer", "829": "internetexplorer", "830": "internetexplorer", "831": "internetexplorer", "832": "internetexplorer", "833": "internetexplorer", "834": "internetexplorer", "835": "internetexplorer", "836": "internetexplorer", "837": "internetexplorer", "838": "internetexplorer", "839": "firefox", "840": "firefox", "841": "firefox", "842": "firefox", "843": "firefox", "844": "firefox", "845": "firefox", "846": "firefox", "847": "firefox", "848": "firefox", "849": "firefox", "850": "firefox", "851": "firefox", "852": "firefox", "853": "firefox", "854": "firefox", "855": "firefox", "856": "firefox", "857": "firefox", "858": "firefox", "859": "firefox", "860": "firefox", "861": "firefox", "862": "firefox", "863": "firefox", "864": "firefox", "865": "firefox", "866": "firefox", "867": "firefox", "868": "firefox", "869": "firefox", "870": "firefox", "871": "firefox", "872": "firefox", "873": "firefox", "874": "firefox", "875": "firefox", "876": "firefox", "877": "firefox", "878": "firefox", "879": "firefox", "880": "firefox", "881": "firefox", "882": "firefox", "883": "firefox", "884": "firefox", "885": "firefox", "886": "firefox", "887": "firefox", "888": "firefox", "889": "firefox", "890": "firefox", "891": "firefox", "892": "firefox", "893": "firefox", "894": "firefox", "895": "firefox", "896": "firefox", "897": "firefox", "898": "firefox", "899": "firefox", "900": "firefox", "901": "firefox", "902": "firefox", "903": "firefox", "904": "firefox", "905": "firefox", "906": "firefox", "907": "firefox", "908": "firefox", "909": "firefox", "910": "firefox", "911": "firefox", "912": "firefox", "913": "firefox", "914": "firefox", "915": "firefox", "916": "firefox", "917": "firefox", "918": "firefox", "919": "firefox", "920": "firefox", "921": "firefox", "922": "firefox", "923": "firefox", "924": "firefox", "925": "firefox", "926": "firefox", "927": "firefox", "928": "firefox", "929": "firefox", "930": "firefox", "931": "firefox", "932": "firefox", "933": "firefox", "934": "firefox", "935": "safari", "936": "safari", "937": "safari", "938": "safari", "939": "safari", "940": "safari", "941": "safari", "942": "safari", "943": "safari", "944": "safari", "945": "safari", "946": "safari", "947": "safari", "948": "safari", "949": "safari", "950": "safari", "951": "safari", "952": "safari", "953": "safari", "954": "safari", "955": "safari", "956": "safari", "957": "safari", "958": "safari", "959": "safari", "960": "safari", "961": "safari", "962": "safari", "963": "safari", "964": "safari", "965": "safari", "966": "safari", "967": "safari", "968": "opera", "969": "opera", "970": "opera", "971": "opera", "972": "opera", "973": "opera", "974": "opera", "975": "opera", "976": "opera", "977": "opera", "978": "opera", "979": "opera", "980": "opera", "981": "opera", "982": "opera", "983": "opera", "984": "opera"}} -------------------------------------------------------------------------------- /image/0180a5748681abe7254ce6734aa64de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/0180a5748681abe7254ce6734aa64de.png -------------------------------------------------------------------------------- /image/58e820362dd287f6668e011e20a1020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/58e820362dd287f6668e011e20a1020.png -------------------------------------------------------------------------------- /image/60430e4e61d28d0e79da9d58e46037f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/60430e4e61d28d0e79da9d58e46037f.png -------------------------------------------------------------------------------- /image/jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/jp.png -------------------------------------------------------------------------------- /image/jp0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/jp0.jpg -------------------------------------------------------------------------------- /image/jp00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/jp00.png -------------------------------------------------------------------------------- /image/jp1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/jp1.jpg -------------------------------------------------------------------------------- /image/jp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/jp2.png -------------------------------------------------------------------------------- /image/固定截屏.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/固定截屏.gif -------------------------------------------------------------------------------- /image/截屏文字识别.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/image/截屏文字识别.gif -------------------------------------------------------------------------------- /j_temp/ocrtemp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fandesfyf/Jamscreenshot/d093047771756d21e44e9cb0a8cedfc11b295be5/j_temp/ocrtemp.png -------------------------------------------------------------------------------- /jamWidgets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from PyQt5.QtCore import Qt, pyqtSignal, QStandardPaths, QUrl 4 | from PyQt5.QtGui import QTextCursor, QDesktopServices 5 | from PyQt5.QtGui import QPainter, QPen, QIcon, QFont,QImage,QPixmap 6 | from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QTextEdit 7 | from PyQt5.QtGui import QPainter, QColor, QLinearGradient,QMovie,QPolygon 8 | from PyQt5.QtCore import Qt, QTimer,QSize,QPoint 9 | from PyQt5.QtWidgets import QApplication, QLabel, QWidget 10 | 11 | from PyQt5.QtCore import Qt, pyqtSignal, QStandardPaths, QUrl,QTimer 12 | from PyQt5.QtGui import QPainter, QPen, QIcon, QFont 13 | from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QTextEdit, QFileDialog, QMenu 14 | import numpy as np 15 | import cv2 16 | import jamresourse 17 | from jampublic import linelabel,TipsShower,OcrimgThread 18 | from jam_transtalater import Translator 19 | from jamspeak import Speaker 20 | class FramelessEnterSendQTextEdit(QTextEdit): # 小窗,翻译,文字识别,语音 21 | clear_signal = pyqtSignal() 22 | showm_signal = pyqtSignal(str) 23 | del_myself_signal = pyqtSignal(int) 24 | 25 | def __init__(self, parent=None, enter_tra=False, autoresetid=0): 26 | super().__init__(parent) 27 | self.parent = parent 28 | self.action = self.show 29 | self.moving = False 30 | self.autoreset = autoresetid 31 | self.hsp = os.path.join(QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation), 32 | "JamtoolsSimpleModehistory.txt") 33 | if os.path.exists(self.hsp): 34 | with open(self.hsp, "r", encoding="utf-8")as f: 35 | self.history = f.read().split("<\n\n<<>>\n\n>") 36 | else: 37 | self.history = [] 38 | self.history_pos = len(self.history) 39 | self.document = self.document() 40 | self.document.contentsChanged.connect(self.textAreaChanged) 41 | self.setMouseTracking(True) 42 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 43 | self.setFont(QFont('', 8)) 44 | self.setPlaceholderText('...') 45 | self.setStyleSheet("QPushButton{color:black}" 46 | "QPushButton:hover{color:green}" 47 | "QPushButton:hover{background-color:rgb(200,200,100)}" 48 | "QPushButton{background-color:rgb(239,239,239)}" 49 | "QScrollBar{width:3px;border:none; background-color:rgb(200,200,200);" 50 | "border-radius: 8px;}" 51 | ) 52 | self.setGeometry(QApplication.desktop().width()//2,QApplication.desktop().height()//2,100,100) 53 | self.menu_size = 28 54 | self.label = linelabel() 55 | self.label.setGeometry(self.x() + self.width(),self.y(), 28, self.height()) 56 | self.label.move_signal.connect(self.move_signal_callback) 57 | self.colse_botton = QPushButton('X', self.label) 58 | self.colse_botton.setToolTip('关闭') 59 | self.colse_botton.resize(self.menu_size, self.menu_size) 60 | self.colse_botton.clicked.connect(self.hide) 61 | self.colse_botton.show() 62 | self.colse_botton.setStyleSheet( 63 | "QPushButton{color:white}" 64 | "QPushButton{background-color:rgb(239,0,0)}" 65 | "QPushButton:hover{color:green}" 66 | "QPushButton:hover{background-color:rgb(150,50,0)}" 67 | "QPushButton{border-radius:0};") 68 | self.tra_botton = QPushButton(QIcon(":./tra.png"),'', self.label) 69 | self.tra_botton.resize(self.menu_size, self.menu_size) 70 | self.tra_botton.clicked.connect(self.tra) 71 | self.tra_botton.setToolTip('翻译/快捷键Ctrl+回车') 72 | self.tra_botton.show() 73 | 74 | self.copy_botton = QPushButton(QIcon(":./copy.png"),'', self.label) 75 | self.copy_botton.resize(self.menu_size, self.menu_size) 76 | self.copy_botton.clicked.connect(self.copy_text) 77 | self.copy_botton.setToolTip('复制内容到剪切板') 78 | self.copy_botton.show() 79 | 80 | self.detail_botton = QPushButton('详', self.label) 81 | self.detail_botton.resize(self.menu_size, self.menu_size) 82 | self.detail_botton.clicked.connect(self.detail) 83 | self.detail_botton.setToolTip('跳转百度翻译网页版查看详细解析') 84 | self.detail_botton.show() 85 | 86 | self.speak_botton = QPushButton('听', self.label) 87 | self.speak_botton.resize(self.menu_size, self.menu_size) 88 | self.speak_botton.clicked.connect(self.speak) 89 | self.speak_botton.setToolTip('播放音频/快捷键F4') 90 | self.speak_botton.show() 91 | 92 | self.clear_botton = QPushButton(QIcon(":./clear.png"), "", self.label) 93 | self.clear_botton.resize(self.menu_size, self.menu_size) 94 | self.clear_botton.clicked.connect(self.clear) 95 | self.clear_botton.setToolTip('清空') 96 | self.clear_botton.show() 97 | self.last_botton = QPushButton('<', self.label) 98 | self.last_botton.resize(self.menu_size//2, self.menu_size//2) 99 | self.last_botton.clicked.connect(self.last_history) 100 | self.last_botton.setToolTip('上一个历史记录Ctrl+←') 101 | self.last_botton.show() 102 | self.next_botton = QPushButton('>', self.label) 103 | self.next_botton.resize(self.menu_size//2, self.menu_size//2) 104 | self.next_botton.clicked.connect(self.next_history) 105 | self.next_botton.setToolTip('下一个历史记录Ctrl+→') 106 | self.next_botton.show() 107 | 108 | 109 | 110 | self.setToolTip('Ctrl+回车可快速翻译,拖动边框可改变位置') 111 | self.clear_signal.connect(self.clear) 112 | self.textAreaChanged() 113 | self.activateWindow() 114 | self.setFocus() 115 | 116 | if enter_tra: 117 | self.action = self.tra 118 | def move(self,x,y,active=False): 119 | super().move(x,y) 120 | self.label.move(self.x()+self.width(), self.y()) 121 | def move_signal_callback(self,x,y): 122 | if self.x() != x-self.width() or self.y() != y: 123 | self.move(x-self.width(),y) 124 | def copy_text(self): 125 | text = self.toPlainText().lstrip("\n").rstrip("\n") 126 | if len(text): 127 | clipboard = QApplication.clipboard() 128 | clipboard.setText(text) 129 | 130 | def detail(self): 131 | text = self.toPlainText().split("翻译结果")[0].lstrip("\n").rstrip("\n") 132 | url = 'https://fanyi.baidu.com/#auto/zh/' + text 133 | QDesktopServices.openUrl(QUrl(url)) 134 | 135 | def tra(self): 136 | self.showm_signal.emit("正在翻译..") 137 | text = self.toPlainText() 138 | if len(text) == 0: 139 | print("无文本") 140 | return 141 | text = re.sub(r'[^\w]', '', text).replace('_', '') 142 | print(text) 143 | n = 0 144 | for i in text: 145 | if self.is_alphabet(i): 146 | n += 1 147 | if n / len(text) > 0.4: 148 | print("is en") 149 | fr = "英语" 150 | to = "中文" 151 | else: 152 | fr = "中文" 153 | to = "英语" 154 | self.translator = Translator() 155 | self.translator.result_signal.connect(self.get_tra_resultsignal) 156 | self.translator.translate(self.toPlainText(), fr, to) 157 | 158 | def is_alphabet(self, uchar): 159 | """判断一个unicode是否是英文字母""" 160 | if (u'\u0041' <= uchar <= u'\u005a') or (u'\u0061' <= uchar <= u'\u007a'): 161 | return True 162 | else: 163 | return False 164 | 165 | def speak(self): 166 | text = self.toPlainText().split("翻译结果")[0].lstrip("\n").rstrip("\n") 167 | if text != "": 168 | Speaker().speak(text) 169 | 170 | def textAreaChanged(self, minsize=200, recheck=True,border=30): 171 | self.document.adjustSize() 172 | newWidth = self.document.size().width() + border 173 | newHeight = self.document.size().height() + border//2 174 | winwidth, winheight = QApplication.desktop().width(), QApplication.desktop().height() 175 | if newWidth != self.width(): 176 | if newWidth < minsize: 177 | self.setFixedWidth(minsize) 178 | elif newWidth > winwidth // 2: 179 | self.setFixedWidth(winwidth // 2 + border) 180 | else: 181 | self.setFixedWidth(newWidth) 182 | if self.x() + self.width() > winwidth: 183 | self.move(winwidth - border - self.width(), self.y()) 184 | if newHeight != self.height(): 185 | if newHeight < minsize: 186 | self.setFixedHeight(minsize) 187 | elif newHeight > winheight * 2 // 3: 188 | self.setFixedHeight(winheight * 2 // 3 + 15) 189 | else: 190 | self.setFixedHeight(newHeight) 191 | if self.y() + self.height() > winheight: 192 | self.move(self.x(), winheight - border - self.height()) 193 | if recheck: 194 | self.textAreaChanged(recheck=False) 195 | self.textAreaChanged(recheck=False) 196 | self.adjustBotton() 197 | 198 | def adjustBotton(self): 199 | self.label.setGeometry(self.x()+self.width(), self.y(), 28, self.height()) 200 | self.colse_botton.move(0, 1) 201 | self.tra_botton.move(0, self.height() - self.tra_botton.height()) 202 | self.speak_botton.move(self.tra_botton.x(), self.tra_botton.y() - self.speak_botton.height()) 203 | self.detail_botton.move(self.speak_botton.x(), self.speak_botton.y() - self.detail_botton.height()) 204 | self.clear_botton.move(self.detail_botton.x(), self.detail_botton.y() - self.clear_botton.height()) 205 | self.copy_botton.move(self.clear_botton.x(), self.clear_botton.y() - self.copy_botton.height()) 206 | self.last_botton.move(self.copy_botton.x(), self.copy_botton.y() - self.last_botton.height()) 207 | self.next_botton.move(self.copy_botton.x() + self.copy_botton.width() - self.next_botton.width(), 208 | self.copy_botton.y() - self.next_botton.height()) 209 | 210 | def get_tra_resultsignal(self, text,fr,to): 211 | self.moveCursor(QTextCursor.End) 212 | self.insertPlainText("\n\n翻译结果:\n{}".format(text)) 213 | self.addhistory() 214 | 215 | def insertPlainText(self, text): 216 | super(FramelessEnterSendQTextEdit, self).insertPlainText(text) 217 | self.show() 218 | 219 | 220 | 221 | def wheelEvent(self, e) -> None: 222 | super(FramelessEnterSendQTextEdit, self).wheelEvent(e) 223 | angle = e.angleDelta() / 8 224 | angle = angle.y() 225 | if QApplication.keyboardModifiers() == Qt.ControlModifier: 226 | if angle > 0 and self.windowOpacity() < 1: 227 | self.setWindowOpacity(self.windowOpacity() + 0.1 if angle > 0 else -0.1) 228 | elif angle < 0 and self.windowOpacity() > 0.2: 229 | self.setWindowOpacity(self.windowOpacity() - 0.1) 230 | 231 | 232 | 233 | 234 | def keyPressEvent(self, e): 235 | super(FramelessEnterSendQTextEdit, self).keyPressEvent(e) 236 | # print(e.key()) 237 | if e.key() == Qt.Key_Return: 238 | try: 239 | if QApplication.keyboardModifiers() in (Qt.ShiftModifier, Qt.ControlModifier, Qt.AltModifier): 240 | self.action() 241 | else: 242 | pass 243 | except: 244 | print('回车失败') 245 | return 246 | elif e.key() ==16777267: 247 | self.speak() 248 | elif e.key() == Qt.Key_S and QApplication.keyboardModifiers() == Qt.ControlModifier: 249 | print("save") 250 | self.addhistory() 251 | elif QApplication.keyboardModifiers() not in (Qt.ShiftModifier, Qt.ControlModifier, Qt.AltModifier): 252 | self.history_pos = len(self.history) 253 | elif QApplication.keyboardModifiers() == Qt.ControlModifier and e.key() == Qt.Key_Left: 254 | self.last_history() 255 | elif QApplication.keyboardModifiers() == Qt.ControlModifier and e.key() == Qt.Key_Right: 256 | self.next_history() 257 | 258 | 259 | def addhistory(self): 260 | text = self.toPlainText() 261 | if text not in self.history and len(text.replace(" ", "").replace("\n", "")): 262 | self.history.append(text) 263 | mode = "r+" 264 | if not os.path.exists(self.hsp): 265 | mode = "w+" 266 | with open(self.hsp, mode, encoding="utf-8")as f: 267 | hislist = f.read().split("<\n\n<<>>\n\n>") 268 | hislist.append(text) 269 | if len(hislist) > 20: 270 | hislist = hislist[-20:] 271 | self.history = self.history[-20:] 272 | newhis = "<\n\n<<>>\n\n>".join(hislist) 273 | f.seek(0) 274 | f.truncate() 275 | f.write(newhis) 276 | self.history_pos = len(self.history) 277 | 278 | def keyenter_connect(self, action): 279 | self.action = action 280 | 281 | def next_history(self): 282 | if self.history_pos < len(self.history) - 1: 283 | hp = self.history_pos 284 | self.clear() 285 | self.history_pos = hp + 1 286 | self.setText(self.history[self.history_pos]) 287 | # print("next h", self.history_pos, len(self.history)) 288 | 289 | def last_history(self): 290 | hp = self.history_pos 291 | self.addhistory() 292 | self.history_pos = hp 293 | if self.history_pos > 0: 294 | hp = self.history_pos 295 | self.clear() 296 | self.history_pos = hp - 1 297 | self.setText(self.history[self.history_pos]) 298 | # print("last h", self.history_pos, len(self.history)) 299 | def showEvent(self,e): 300 | super().showEvent(e) 301 | self.label.show() 302 | def hide(self) -> None: 303 | self.addhistory() 304 | super(FramelessEnterSendQTextEdit, self).hide() 305 | self.label.hide() 306 | if self.autoreset: 307 | print('删除', self.autoreset - 1) 308 | self.del_myself_signal.emit(self.autoreset - 1) 309 | self.label.close() 310 | self.close() 311 | 312 | def closeEvent(self, e) -> None: 313 | super(FramelessEnterSendQTextEdit, self).closeEvent(e) 314 | self.label.close() 315 | def clear(self, notsave=False): 316 | save = not notsave 317 | if save: 318 | self.addhistory() 319 | self.history_pos = len(self.history) 320 | super(FramelessEnterSendQTextEdit, self).clear() 321 | class Hung_widget(QLabel): 322 | button_signal = pyqtSignal(str) 323 | def __init__(self,parent=None,funcs = []): 324 | super().__init__() 325 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 326 | self.setMouseTracking(True) 327 | size = 30 328 | self.buttonsize = size 329 | self.buttons = [] 330 | self.setAttribute(Qt.WA_TranslucentBackground) 331 | self.setStyleSheet("background-color: rgba(255, 255, 255, 0); border-radius: 6px;") # 设置背景色和边框 332 | for i,func in enumerate(funcs): 333 | if str(func).endswith(("png","jpg")): 334 | botton = QPushButton(QIcon(func), '', self) 335 | else: 336 | botton = QPushButton(str(func), self) 337 | botton.clicked.connect(lambda checked, index=func: self.button_signal.emit(index)) 338 | botton.setGeometry(0,i*size,size,size) 339 | botton.setStyleSheet("""QPushButton { 340 | border: 2px solid #8f8f91; 341 | background-color: qradialgradient( 342 | cx: -0.3, cy: 0.4, 343 | fx: -0.3, fy: 0.4, 344 | radius: 1.35, 345 | stop: 0 #fff, 346 | stop: 1 #888 347 | ); 348 | color: white; 349 | font-size: 16px; 350 | padding: 6px; 351 | } 352 | 353 | QPushButton:hover { 354 | background-color: qradialgradient( 355 | cx: -0.3, cy: 0.4, 356 | fx: -0.3, fy: 0.4, 357 | radius: 1.35, 358 | stop: 0 #fff, 359 | stop: 1 #bbb 360 | ); 361 | }""") 362 | self.buttons.append(botton) 363 | self.resize(size,size*len(funcs)) 364 | 365 | 366 | def set_ontop(self,on_top=True): 367 | if on_top: 368 | self.setWindowFlag(Qt.WindowStaysOnTopHint, False) 369 | self.setWindowFlag(Qt.Tool, False) 370 | else: 371 | self.setWindowFlag(Qt.WindowStaysOnTopHint, True) 372 | self.setWindowFlag(Qt.Tool, True) 373 | def clear(self): 374 | self.clearMask() 375 | self.hide() 376 | super().clear() 377 | 378 | def closeEvent(self, e): 379 | self.clear() 380 | super().closeEvent(e) 381 | 382 | class Loading_label(QLabel): 383 | def __init__(self, parent=None,size = 100,text=None): 384 | super().__init__(parent) 385 | self.giflabel = QLabel(parent = self,text=text if text is not None else "") 386 | self.giflabel.resize(size, size) 387 | self.giflabel.setAlignment(Qt.AlignCenter) 388 | self.gif = QMovie(':./load.gif') 389 | self.gif.setScaledSize(QSize(size, size)) 390 | self.giflabel.setMovie(self.gif) 391 | def resizeEvent(self, a0) -> None: 392 | 393 | size = min(self.width(),self.height())//3 394 | if size < 50: 395 | size = min(self.width(),self.height())-5 396 | 397 | self.gif.setScaledSize(QSize(size, size)) 398 | self.giflabel.resize(size, size) 399 | self.giflabel.move(self.width()//2-self.giflabel.width()//2,self.height()//2-self.giflabel.height()//2) 400 | return super().resizeEvent(a0) 401 | 402 | def start(self): 403 | self.gif.start() 404 | self.show() 405 | def stop(self): 406 | self.gif.stop() 407 | self.hide() 408 | class Freezer(QLabel): 409 | def __init__(self, parent=None, img=None, x=0, y=0, listpot=0): 410 | super().__init__() 411 | self.hung_widget = Hung_widget(funcs =[":/exit.png",":/ontop.png",":/OCR.png",":/copy.png",":/saveicon.png"]) 412 | self.tips_shower = TipsShower(" ",(QApplication.desktop().width()//2,50,120,50)) 413 | self.tips_shower.hide() 414 | self.text_shower = FramelessEnterSendQTextEdit(self, enter_tra=True) 415 | self.text_shower.hide() 416 | self.origin_imgpix = img 417 | self.showing_imgpix = self.origin_imgpix 418 | self.ocr_res_imgpix = None 419 | self.listpot = listpot 420 | self.setPixmap(self.showing_imgpix) 421 | self.settingOpacity = False 422 | self.setWindowOpacity(0.95) 423 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 424 | self.setMouseTracking(True) 425 | self.drawRect = True 426 | # self.setContextMenuPolicy(Qt.CustomContextMenu) 427 | self.setGeometry(x, y, self.showing_imgpix.width(), self.showing_imgpix.height()) 428 | self.show() 429 | self.drag = self.resize_the_window = False 430 | self.on_top = True 431 | self.p_x = self.p_y = 0 432 | self.setToolTip("Ctrl+滚轮可以调节透明度") 433 | # self.setMaximumSize(QApplication.desktop().size()) 434 | self.timer = QTimer(self) # 创建一个定时器 435 | self.timer.setInterval(200) # 设置定时器的时间间隔为1秒 436 | self.timer.timeout.connect(self.check_mouse_leave) # 定时器超时时触发check_mouse_leave函数 437 | 438 | self.hung_widget.button_signal.connect(self.hw_signalcallback) 439 | self.hung_widget.show() 440 | self.move(x, y) 441 | self.ocr_status = "waiting" 442 | self.ocr_res_info = [] 443 | 444 | def hw_signalcallback(self,s): 445 | print("callback",s) 446 | s = s.lower() 447 | self.tips_shower.set_pos(self.x(),self.y()) 448 | if "exit" in s:#退出 449 | self.clear() 450 | elif "ocr" in s:#文字识别 451 | self.tips_shower.setText("文字识别中...",color=Qt.green) 452 | self.ocr() 453 | elif "ontop" in s: 454 | self.tips_shower.setText("{}置顶...".format("取消"if self.on_top else "设置"),color=Qt.green) 455 | self.change_ontop() 456 | elif "copy" in s: 457 | clipboard = QApplication.clipboard() 458 | try: 459 | clipboard.setPixmap(self.showing_imgpix) 460 | except: 461 | self.tips_shower.setText("复制失败",color=Qt.green) 462 | else: 463 | self.tips_shower.setText("已复制图片",color=Qt.green) 464 | elif "save" in s: 465 | self.tips_shower.setText("图片另存为...",color=Qt.green) 466 | img = self.showing_imgpix 467 | path, l = QFileDialog.getSaveFileName(self, "另存为", QStandardPaths.writableLocation( 468 | QStandardPaths.PicturesLocation), "png Files (*.png);;" 469 | "jpg file(*.jpg);;jpeg file(*.JPEG);; bmp file(*.BMP );;ico file(*.ICO);;" 470 | ";;all files(*.*)") 471 | if path: 472 | img.save(path) 473 | def ocr(self): 474 | if self.ocr_status == "ocr": 475 | self.tips_shower.setText("取消识别...",color=Qt.green) 476 | self.ocr_status = "abort" 477 | self.Loading_label.stop() 478 | self.text_shower.hide() 479 | self.showing_imgpix = self.origin_imgpix 480 | self.setPixmap(self.showing_imgpix.scaled(self.width(), self.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 481 | 482 | return 483 | elif self.ocr_status == "show":#正在展示结果,取消展示 484 | self.tips_shower.setText("退出文字识别...",color=Qt.green) 485 | self.ocr_status = "waiting" 486 | self.Loading_label.stop() 487 | self.text_shower.hide() 488 | self.showing_imgpix = self.origin_imgpix 489 | self.setPixmap(self.showing_imgpix.scaled(self.width(), self.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 490 | return 491 | self.ocr_status = "ocr" 492 | if not os.path.exists("j_temp"): 493 | os.mkdir("j_temp") 494 | self.pixmap().save("j_temp/tempocr.png", "PNG") 495 | cv_image = cv2.imread("j_temp/tempocr.png") 496 | self.ocrthread = OcrimgThread(cv_image) 497 | self.ocrthread.result_show_signal.connect(self.ocr_res_signalhandle) 498 | self.ocrthread.boxes_info_signal.connect(self.orc_boxes_info_callback) 499 | self.ocrthread.det_res_img.connect(self.det_res_img_callback) 500 | self.ocrthread.start() 501 | self.Loading_label = Loading_label(self) 502 | self.Loading_label.setGeometry(0, 0, self.width(), self.height()) 503 | self.Loading_label.start() 504 | 505 | self.text_shower.setPlaceholderText("正在识别,请耐心等待...") 506 | self.text_shower.move(self.x(), self.y()+self.height()) 507 | self.text_shower.show() 508 | self.text_shower.clear() 509 | QApplication.processEvents() 510 | def orc_boxes_info_callback(self,text_boxes): 511 | if self.ocr_status == "ocr": 512 | for tb in text_boxes: 513 | tb["select"]=False 514 | self.ocr_res_info = text_boxes 515 | print("rec orc_boxes_info_callback") 516 | 517 | def det_res_img_callback(self,piximg): 518 | if self.ocr_status == "ocr": 519 | print("rec det_res_img_callback") 520 | self.showing_imgpix = piximg 521 | self.ocr_res_imgpix = piximg 522 | self.setPixmap(piximg.scaled(self.width(), self.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 523 | 524 | def ocr_res_signalhandle(self,text): 525 | if self.ocr_status == "ocr": 526 | self.text_shower.setPlaceholderText("") 527 | self.text_shower.insertPlainText(text) 528 | self.Loading_label.stop() 529 | self.ocr_status = "show" 530 | def contextMenuEvent(self, event): 531 | menu = QMenu(self) 532 | quitAction = menu.addAction("退出") 533 | copyaction = menu.addAction('复制') 534 | saveaction = menu.addAction('另存为') 535 | topaction = menu.addAction('(取消)置顶') 536 | rectaction = menu.addAction('(取消)边框') 537 | 538 | action = menu.exec_(self.mapToGlobal(event.pos())) 539 | if action == quitAction: 540 | self.clear() 541 | elif action == saveaction: 542 | img = self.showing_imgpix 543 | path, l = QFileDialog.getSaveFileName(self, "另存为", QStandardPaths.writableLocation( 544 | QStandardPaths.PicturesLocation), "png Files (*.png);;" 545 | "jpg file(*.jpg);;jpeg file(*.JPEG);; bmp file(*.BMP );;ico file(*.ICO);;" 546 | ";;all files(*.*)") 547 | if path: 548 | img.save(path) 549 | elif action == copyaction: 550 | clipboard = QApplication.clipboard() 551 | try: 552 | clipboard.setPixmap(self.showing_imgpix) 553 | except: 554 | print('复制失败') 555 | elif action == topaction: 556 | self.change_ontop() 557 | elif action == rectaction: 558 | self.drawRect = not self.drawRect 559 | self.update() 560 | 561 | def change_ontop(self): 562 | if self.on_top: 563 | self.on_top = False 564 | self.setWindowFlag(Qt.WindowStaysOnTopHint, False) 565 | self.setWindowFlag(Qt.Tool, False) 566 | self.show() 567 | else: 568 | self.on_top = True 569 | self.setWindowFlag(Qt.WindowStaysOnTopHint, True) 570 | self.setWindowFlag(Qt.Tool, True) 571 | self.show() 572 | def setWindowOpacity(self,opacity): 573 | super().setWindowOpacity(opacity) 574 | self.hung_widget.setWindowOpacity(opacity) 575 | 576 | def wheelEvent(self, e): 577 | if self.isVisible(): 578 | angleDelta = e.angleDelta() / 8 579 | dy = angleDelta.y() 580 | if self.settingOpacity: 581 | if dy > 0: 582 | if (self.windowOpacity() + 0.1) <= 1: 583 | self.setWindowOpacity(self.windowOpacity() + 0.1) 584 | else: 585 | self.setWindowOpacity(1) 586 | elif dy < 0 and (self.windowOpacity() - 0.1) >= 0.11: 587 | self.setWindowOpacity(self.windowOpacity() - 0.1) 588 | else: 589 | if 2 * QApplication.desktop().width() >= self.width() >= 50: 590 | # 获取鼠标所在位置相对于窗口的坐标 591 | old_pos = e.pos() 592 | old_width = self.width() 593 | old_height = self.height() 594 | w = self.width() + dy * 5 595 | if w < 50: w = 50 596 | if w > 2 * QApplication.desktop().width(): w = 2 * QApplication.desktop().width() 597 | scale = self.showing_imgpix.height() / self.showing_imgpix.width() 598 | h = w * scale 599 | s = self.width() / w # 缩放比例 600 | self.setPixmap(self.showing_imgpix.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) 601 | self.resize( w, h) 602 | delta_x = -(w - old_width)*old_pos.x()/old_width 603 | delta_y = -(h - old_height)*old_pos.y()/old_height 604 | self.move(self.x() + delta_x, self.y() + delta_y) 605 | QApplication.processEvents() 606 | 607 | self.update() 608 | def move(self,x,y): 609 | super().move(x,y) 610 | hw_w = self.hung_widget.width() 611 | hw_h = self.hung_widget.height() 612 | hw_x = self.x()+self.width() 613 | hw_y = self.y()+self.height()-hw_h 614 | 615 | if self.x()+self.width() > QApplication.desktop().width() - hw_w: 616 | hw_x = self.x()-hw_w 617 | self.hung_widget.move(hw_x,hw_y) 618 | self.text_shower.move(self.x(), self.y()+self.height())#主动移动 619 | 620 | def resizeEvent(self, a0): 621 | super().resizeEvent(a0) 622 | if hasattr(self,"Loading_label"): 623 | self.Loading_label.setGeometry(0, 0, self.width(), self.height()) 624 | def draw_ocr_select_result(self,ids = []): 625 | qpixmap = self.ocr_res_imgpix.copy() 626 | painter = QPainter(qpixmap) 627 | 628 | for i,text_box in enumerate(self.ocr_res_info): 629 | if i in ids: 630 | pen = QPen(Qt.green) 631 | else: 632 | pen = QPen(Qt.red) 633 | pen.setWidth(2) 634 | painter.setPen(pen) 635 | contour = text_box["box"] 636 | points = [] 637 | for point in contour: 638 | x, y = point 639 | points.append(QPoint(x, y)) 640 | polygon = QPolygon(points + [points[0]]) 641 | painter.drawPolyline(polygon) 642 | painter.end() 643 | return qpixmap 644 | def check_select_ocr_box(self,x,y): 645 | select_ids = [] 646 | change = False 647 | for i,text_box in enumerate(self.ocr_res_info): 648 | contour = text_box["box"] 649 | dist = cv2.pointPolygonTest(contour, (x,y), False) 650 | if dist >= 0: 651 | text_box["select"] = ~text_box["select"] 652 | change = True 653 | if text_box["select"]: 654 | select_ids.append(i) 655 | 656 | return select_ids,change 657 | def update_ocr_text(self,ids): 658 | match_text_box = [] 659 | for i,text_box in enumerate(self.ocr_res_info): 660 | if i in ids: 661 | match_text_box.append(text_box) 662 | if hasattr(self,"ocrthread"): 663 | res = self.ocrthread.get_match_text(match_text_box) 664 | if res is not None: 665 | return res 666 | return None 667 | def update_ocr_select_result(self,x,y): 668 | select_ids,changed = self.check_select_ocr_box(x,y) 669 | if changed: 670 | pix = self.draw_ocr_select_result(ids = select_ids) 671 | self.showing_imgpix = pix 672 | self.setPixmap(pix.scaled(self.width(), self.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 673 | update_res = self.update_ocr_text(select_ids) 674 | if update_res is not None: 675 | # 更新结果 676 | self.text_shower.move(self.x(), self.y()+self.height()) 677 | self.text_shower.show() 678 | self.text_shower.clear() 679 | self.text_shower.insertPlainText(update_res) 680 | return changed 681 | def mousePressEvent(self, event): 682 | if event.button() == Qt.LeftButton: 683 | if self.ocr_status=="show": 684 | sx,sy = self.origin_imgpix.width()/self.width(),self.origin_imgpix.height()/self.height() 685 | realx,realy = event.x()*sx,event.y()*sy 686 | changed = self.update_ocr_select_result(realx,realy) 687 | if changed: 688 | return 689 | if event.x() > self.width() - 20 and event.y() > self.height() - 20: 690 | self.resize_the_window = True 691 | self.setCursor(Qt.SizeFDiagCursor) 692 | else: 693 | self.setCursor(Qt.SizeAllCursor) 694 | self.drag = True 695 | self.p_x, self.p_y = event.x(), event.y() 696 | # self.resize(self.width()/2,self.height()/2) 697 | # self.setPixmap(self.pixmap().scaled(self.pixmap().width()/2,self.pixmap().height()/2)) 698 | 699 | def mouseReleaseEvent(self, event): 700 | if event.button() == Qt.LeftButton: 701 | self.setCursor(Qt.ArrowCursor) 702 | self.drag = self.resize_the_window = False 703 | def underMouse(self) -> bool: 704 | return super().underMouse() 705 | def mouseMoveEvent(self, event): 706 | if self.isVisible(): 707 | if self.drag: 708 | self.move(event.x() + self.x() - self.p_x, event.y() + self.y() - self.p_y) 709 | elif self.resize_the_window: 710 | if event.x() > 10 and event.y() > 10: 711 | w = event.x() 712 | scale = self.showing_imgpix.height() / self.showing_imgpix.width() 713 | h = w * scale 714 | self.resize(w, h) 715 | self.setPixmap(self.showing_imgpix.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) 716 | elif event.x() > self.width() - 20 and event.y() > self.height() - 20: 717 | self.setCursor(Qt.SizeFDiagCursor) 718 | else: 719 | self.setCursor(Qt.ArrowCursor) 720 | def enterEvent(self,e): 721 | super().enterEvent(e) 722 | self.timer.stop() 723 | self.hung_widget.show() 724 | def leaveEvent(self,e): 725 | super().leaveEvent(e) 726 | self.timer.start() 727 | self.settingOpacity = False 728 | 729 | def check_mouse_leave(self): 730 | if not self.underMouse() and not self.hung_widget.underMouse(): 731 | self.hung_widget.hide() 732 | self.timer.stop() 733 | def keyPressEvent(self, e): 734 | if e.key() == Qt.Key_Escape: 735 | self.clear() 736 | elif e.key() == Qt.Key_Control: 737 | self.settingOpacity = True 738 | 739 | def keyReleaseEvent(self, e) -> None: 740 | if e.key() == Qt.Key_Control: 741 | self.settingOpacity = False 742 | 743 | def paintEvent(self, event): 744 | super().paintEvent(event) 745 | if self.drawRect: 746 | painter = QPainter(self) 747 | painter.setPen(QPen(Qt.green, 1, Qt.SolidLine)) 748 | painter.drawRect(0, 0, self.width() - 1, self.height() - 1) 749 | painter.end() 750 | 751 | def clear(self): 752 | self.clearMask() 753 | self.hide() 754 | if hasattr(self,"Loading_label"): 755 | self.Loading_label.stop() 756 | self.text_shower.clear() 757 | self.text_shower.hide() 758 | del self.showing_imgpix 759 | self.hung_widget.clear() 760 | super().clear() 761 | # jamtools.freeze_imgs[self.listpot] = None 762 | 763 | def closeEvent(self, e): 764 | self.clear() 765 | 766 | e.ignore() 767 | 768 | 769 | -------------------------------------------------------------------------------- /jam_transtalater.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author : Fandes 3 | import random 4 | import requests 5 | import sys 6 | import hashlib 7 | import http.client 8 | from urllib.parse import quote 9 | 10 | from PyQt5.QtCore import QRect, Qt, QThread, pyqtSignal, QStandardPaths, QTimer, QSettings, QFileInfo, \ 11 | QUrl, QObject, QSize 12 | from PyQt5.QtGui import QPixmap, QPainter, QPen, QIcon, QFont, QImage, QTextCursor, QColor, QDesktopServices, QMovie 13 | from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QToolTip, QAction, QTextEdit, QLineEdit, \ 14 | QMessageBox, QFileDialog, QMenu, QSystemTrayIcon, QGroupBox, QComboBox, QCheckBox, QSpinBox, QTabWidget, \ 15 | QDoubleSpinBox, QLCDNumber, QScrollArea, QWidget, QToolBox, QRadioButton, QTimeEdit, QListWidget, QDialog, \ 16 | QProgressBar, QTextBrowser 17 | from PyQt5.QtNetwork import QLocalSocket, QLocalServer 18 | from threading import Thread 19 | from jampublic import get_UserAgent 20 | 21 | class Translator(QObject): 22 | result_signal = pyqtSignal(str,str,str)#result,from_lan,to_lan 23 | status_signal = pyqtSignal(str) 24 | def __init__(self, parent=None): 25 | super().__init__() 26 | self.fromLang = '自动检测' 27 | self.toLang = '中文' 28 | self.engine = "YouDao" 29 | self.parent = parent 30 | self.text = ' ' 31 | self.thread = None 32 | #支持的引擎:百度BaiDu,有道Youdao 33 | bd_lang_dict = {'自动检测': 'auto', '中文': 'zh', '英语': 'en', '文言文': 'wyw', '粤语': 'yue', '日语': 'jp', '德语': 'de', 34 | '韩语': 'kor', 35 | '法语': 'fra', '俄语': 'ru', '泰语': 'th', '意大利语': 'it', '葡萄牙语': 'pt', '西班牙语': 'spa'} 36 | yd_lang_dict = {'自动检测': 'AUTO', '中文': 'ZH_CN', '英语': 'EN', '日语': 'JA','韩语': 'KR','法语': 'FR', 37 | '俄语': 'RU', '西班牙语': 'SP'} 38 | self.translate_engine_info = {"YouDao":yd_lang_dict, 39 | "BaiDu":bd_lang_dict} 40 | self.tra_result = None 41 | 42 | def get_available_langs(self,engine="YouDao"): 43 | return list(self.translate_engine_info[engine].keys()) 44 | 45 | def translate(self,text,from_lang = '自动检测',to_lang = "中文",engine = "YouDao"): 46 | pl={"YouDao":self.Youdaotra, 47 | "BaiDu":self.Bdtra} 48 | if engine in pl: 49 | self.fromLang = from_lang 50 | self.toLang = to_lang 51 | self.engine = engine 52 | self.text = text 53 | func = pl[engine] 54 | print("开始翻译") 55 | self.thread = Thread(target=func) 56 | self.thread.start() 57 | self.status_signal.emit("正在翻译...") 58 | else: 59 | print("调用错误") 60 | self.status_signal.emit("调用错误") 61 | def wait(self): 62 | self.thread.join() 63 | 64 | def Youdaotra(self): 65 | try: 66 | headers = {"User-Agent":get_UserAgent()} 67 | mode_dict = self.translate_engine_info["YouDao"] 68 | if self.fromLang == "自动检测": 69 | mode = "AUTO" 70 | else: 71 | mode = mode_dict[self.fromLang]+"2"+mode_dict[self.toLang] 72 | 73 | url = "http://fanyi.youdao.com/translate?&doctype=json&type={}&i={}".format(mode,self.text) 74 | res = requests.get(url,headers=headers).json() 75 | print(res) 76 | result = res["translateResult"][0][0]["tgt"] 77 | result_type_dict = dict(zip(mode_dict.values(),mode_dict.keys())) 78 | from_lan,to_lan = res["type"].split("2") 79 | from_lan,to_lan = result_type_dict[from_lan],result_type_dict[to_lan] 80 | self.tra_result = result 81 | self.result_signal.emit(result,from_lan,to_lan) 82 | self.status_signal.emit("翻译完成!") 83 | except Exception as e: 84 | print(e,__file__) 85 | self.status_signal.emit("翻译失败!") 86 | 87 | def Bdtra(self): 88 | print('Bdtra翻译开始') 89 | try: 90 | mode_dict = self.translate_engine_info["BaiDu"] 91 | bd_tra = BaiduTranslate(self.text,mode_dict[self.fromLang],mode_dict[self.toLang]) 92 | result = bd_tra.tra() 93 | if result is not None: 94 | result,from_lan,to_lan = result 95 | result_type_dict = dict(zip(mode_dict.values(),mode_dict.keys())) 96 | from_lan,to_lan = result_type_dict[from_lan],result_type_dict[to_lan] 97 | self.tra_result = result 98 | self.result_signal.emit(result,from_lan,to_lan) 99 | self.status_signal.emit("翻译完成!") 100 | else: 101 | raise Exception("识别结果错误") 102 | except Exception as e: 103 | print(e,__file__) 104 | self.status_signal.emit("翻译出错!") 105 | 106 | def get_lang(self): 107 | try: 108 | dictl = {'自动检测': 'auto', '中文': 'zh', '英语': 'en', '文言文': 'wyw', '粤语': 'yue', '日语': 'jp', '德语': 'de', 109 | '韩语': 'kor', 110 | '法语': 'fra', '俄语': 'ru', '泰语': 'th', '意大利语': 'it', '葡萄牙语': 'pt', '西班牙语': 'spa'} 111 | self.fromLang = dictl[self.fromLang] 112 | self.toLang = dictl[self.toLang] 113 | except: 114 | print('auto') 115 | 116 | def show_detal(self): 117 | self.get_lang() 118 | url = 'https://fanyi.baidu.com/#' + self.fromLang + '/' + self.toLang + '/' + self.text 119 | QDesktopServices.openUrl(QUrl(url)) 120 | 121 | class BaiduTranslate(QObject): 122 | resultsignal = pyqtSignal(str) 123 | showm_singal = pyqtSignal(str) 124 | change_item_signal = pyqtSignal(str) 125 | 126 | def __init__(self, text, from_lan, to_lan): 127 | super().__init__() 128 | self.text = text 129 | self.toLang = to_lan 130 | #由于被人恶意调用,我的百度翻译调用量已经用完,这个appid已经不可用了,可以将自己的appid在设置里面填入. 131 | self.appid = QSettings('Fandes', 'jamtools').value('tran_appid', '20190928000337891', str) 132 | self.secretKey = QSettings('Fandes', 'jamtools').value('tran_secretKey', 'SiNITAufl_JCVpk7fAUS', str) 133 | salt = str(random.randint(32768, 65536)) 134 | sign = self.appid + self.text + salt + self.secretKey 135 | m1 = hashlib.md5() 136 | m1.update(sign.encode(encoding='utf-8')) 137 | sign = m1.hexdigest() 138 | q= quote(self.text) 139 | self.re_url = '/api/trans/vip/translate?appid=' + self.appid + '&q=' + q + '&from=' + from_lan + '&to={0}&salt=' + str( 140 | salt) + '&sign=' + sign 141 | self.geturl = self.re_url.format(self.toLang) 142 | # self.args={"sign": sign,"salt":salt, "appid": self.appid,"to": to_lan,"from":from_lan ,"q":q} 143 | 144 | def tra(self,replay = 3): 145 | 146 | if len(str(self.text).replace(" ", "").replace("\n", "")) == 0: 147 | print("空翻译") 148 | self.resultsignal.emit("没有文本!") 149 | return 150 | try: 151 | httpClient0 = http.client.HTTPConnection('api.fanyi.baidu.com') 152 | httpClient0.request('GET', self.geturl) 153 | response = httpClient0.getresponse() 154 | except: 155 | print(sys.exc_info()) 156 | self.showm_singal.emit("翻译出错!请确保网络畅通!{}".format(sys.exc_info()[0])) 157 | else: 158 | s = response.read().decode('utf-8') 159 | print(s) 160 | s = eval(s) 161 | text = '' 162 | # print(s) 163 | f_l = s['from'] 164 | t_l = s['to'] 165 | if f_l == t_l: 166 | if t_l == 'zh': 167 | self.geturl = self.re_url.format('en') 168 | try: 169 | # jamtools.tra_to.setCurrentText('英语') 170 | self.change_item_signal.emit("英语") 171 | except: 172 | print(sys.exc_info()) 173 | else: 174 | self.geturl = self.re_url.format('zh') 175 | try: 176 | self.change_item_signal.emit("中文") 177 | # jamtools.tra_to.setCurrentText('中文') 178 | except: 179 | print(sys.exc_info()) 180 | if replay > 0: 181 | return self.tra(replay-1) 182 | else: 183 | return None 184 | for line in s['trans_result']: 185 | temp = line['dst'] + '\n' 186 | text += temp 187 | return text,f_l,t_l 188 | -------------------------------------------------------------------------------- /jampublic.py: -------------------------------------------------------------------------------- 1 | #!usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/11/13 22:42 4 | # @Author : Fandes 5 | # @FileName: public.py 6 | # @Software: PyCharm 7 | import hashlib 8 | import http.client 9 | import os 10 | import random 11 | import re 12 | import sys 13 | import time 14 | import cv2 15 | import requests 16 | from PyQt5.QtCore import QRect, Qt, QThread, pyqtSignal, QStandardPaths, QTimer, QSettings, QFileInfo, \ 17 | QUrl, QObject, QSize 18 | from PyQt5.QtCore import QRect, Qt, QThread, pyqtSignal, QSettings, QSizeF, QStandardPaths, QUrl 19 | from PyQt5.QtCore import QTimer 20 | from PyQt5.QtGui import QColor, QBrush, QTextDocument, QTextCursor, QDesktopServices,QPixmap 21 | from PyQt5.QtGui import QPainter, QPen, QIcon, QFont,QImage 22 | from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QTextEdit, QWidget 23 | from urllib.parse import quote 24 | import numpy as np 25 | from fake_useragent import UserAgent 26 | 27 | from jamspeak import Speaker 28 | from PaddleOCRModel.PaddleOCRModel import det_rec_functions as OcrDetector 29 | APP_ID = QSettings('Fandes', 'jamtools').value('BaiduAI_APPID', '17302981', str) # 获取的 ID,下同 30 | API_KEY = QSettings('Fandes', 'jamtools').value('BaiduAI_APPKEY', 'wuYjn1T9GxGIXvlNkPa9QWsw', str) 31 | SECRECT_KEY = QSettings('Fandes', 'jamtools').value('BaiduAI_SECRECT_KEY', '89wrg1oEiDzh5r0L63NmWeYNZEWUNqvG', str) 32 | print("platform is", sys.platform) 33 | PLATFORM_SYS = sys.platform 34 | CONFIG_DICT = {"last_pic_save_name":"{}".format( str(time.strftime("%Y-%m-%d_%H.%M.%S", time.localtime())))} 35 | 36 | def get_apppath(): 37 | p = sys.path[0].replace("\\", "/").rstrip("/") if os.path.isdir(sys.path[0]) else os.path.split(sys.path[0])[0] 38 | # print("apppath",p) 39 | if sys.platform == "darwin" and p.endswith("MacOS"): 40 | p = os.path.join(p.rstrip("MacOS"), "Resources") 41 | return p 42 | 43 | 44 | apppath = get_apppath() 45 | def get_request_session(url="https://github.com"): 46 | # 获取系统的代理设置 47 | proxies = requests.utils.get_environ_proxies(url) 48 | 49 | # 创建一个 session 对象 50 | session = requests.session() 51 | # 设置代理配置 52 | session.proxies = proxies 53 | return session 54 | def get_UserAgent(): 55 | ua = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36 Edg/108.0.1462.54" 56 | try: 57 | ua_file = os.path.join(apppath,"fake_useragent_0.1.11.json") 58 | if os.path.exists(ua_file): 59 | ua = UserAgent(path=os.path.join(apppath,"fake_useragent_0.1.11.json"),verify_ssl=False).random 60 | else: 61 | ua = UserAgent(verify_ssl=False).random 62 | except Exception as e: 63 | print(e,"get_UserAgent") 64 | return ua 65 | def gethtml(url, times=3): # 下载一个链接 66 | try: 67 | ua = get_UserAgent() 68 | session = get_request_session(url) 69 | response = session.get(url, headers={"User-Agent": ua}, timeout=8, verify=False) 70 | response.encoding = 'utf-8' 71 | if response.status_code == 200: 72 | return response.text 73 | 74 | except Exception as e: 75 | error_msg = "{}".format(sys.exc_info()) 76 | print(error_msg, '重试中') 77 | time.sleep(1) 78 | if times > 0: 79 | return gethtml(url, times=times - 1) 80 | else: 81 | return error_msg 82 | class TipsShower(QLabel): 83 | def __init__(self, text, targetarea=(0, 0, 0, 0), parent=None, fontsize=35, timeout=1000): 84 | super().__init__(parent) 85 | self.parent = parent 86 | self.area = list(targetarea) 87 | self.timeout = timeout 88 | self.rfont = QFont('', fontsize) 89 | self.setFont(self.rfont) 90 | self.setAttribute(Qt.WA_TransparentForMouseEvents, True) 91 | self.setAttribute(Qt.WA_TranslucentBackground, True) 92 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 93 | self.timer = QTimer(self) 94 | self.timer.timeout.connect(self.hide) 95 | self.setText(text) 96 | 97 | self.show() 98 | 99 | self.setStyleSheet("color:white") 100 | def set_pos(self,x,y): 101 | self.area[0],self.area[1]=[x,y] 102 | def setText(self, text, autoclose=True, font: QFont = None, color: QColor = None) -> None: 103 | super(TipsShower, self).setText(text) 104 | print("settext") 105 | self.adjustSize() 106 | x, y, w, h = self.area 107 | if x < QApplication.desktop().width() - x - w: 108 | self.move(x + w + 5, y) 109 | else: 110 | self.move(x - self.width() - 5, y) 111 | self.show() 112 | if autoclose: 113 | self.timer.start(self.timeout) 114 | if font is not None: 115 | print("更换字体") 116 | self.setFont(font) 117 | if font is not None: 118 | self.setStyleSheet("color:{}".format(color.name())) 119 | 120 | def hide(self) -> None: 121 | super(TipsShower, self).hide() 122 | self.timer.stop() 123 | self.setFont(self.rfont) 124 | self.setStyleSheet("color:white") 125 | 126 | def textAreaChanged(self, minsize=0): 127 | self.document.adjustSize() 128 | newWidth = self.document.size().width() + 25 129 | newHeight = self.document.size().height() + 15 130 | if newWidth != self.width(): 131 | if newWidth < minsize: 132 | self.setFixedWidth(minsize) 133 | else: 134 | self.setFixedWidth(newWidth) 135 | if newHeight != self.height(): 136 | if newHeight < minsize: 137 | self.setFixedHeight(minsize) 138 | else: 139 | self.setFixedHeight(newHeight) 140 | 141 | 142 | 143 | class linelabel(QLabel): 144 | move_signal = pyqtSignal(int, int) 145 | def __init__(self, parent=None): 146 | super(linelabel, self).__init__(parent=parent) 147 | self.setMouseTracking(True) 148 | self.moving = False 149 | # self.setAttribute(Qt.WA_TransparentForMouseEvents, True) 150 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 151 | self.setStyleSheet("QPushButton{color:black}" 152 | "QPushButton:hover{color:green}" 153 | "QPushButton:hover{background-color:rgb(200,200,100)}" 154 | "QPushButton{background-color:rgb(239,239,239)}" 155 | "QScrollBar{width:3px;border:none; background-color:rgb(200,200,200);" 156 | "border-radius: 8px;}" 157 | ) 158 | def paintEvent(self, e): 159 | super(linelabel, self).paintEvent(e) 160 | painter = QPainter(self) 161 | brush = QBrush(Qt.Dense7Pattern) 162 | painter.setBrush(brush) 163 | painter.drawRect(0, 0, self.width(), self.height()) 164 | painter.end() 165 | 166 | def mouseReleaseEvent(self, e): 167 | super().mouseReleaseEvent(e) 168 | if e.button() == Qt.LeftButton: 169 | self.moving = False 170 | self.setCursor(Qt.ArrowCursor) 171 | self.update() 172 | def mousePressEvent(self, e): 173 | if e.button() == Qt.LeftButton: 174 | self.moving = True 175 | self.dx = e.x() 176 | self.dy = e.y() 177 | self.setCursor(Qt.SizeAllCursor) 178 | self.update() 179 | 180 | def mouseMoveEvent(self, e): 181 | super().mouseMoveEvent(e) 182 | if self.isVisible(): 183 | if self.moving: 184 | self.move(e.x() + self.x() - self.dx, e.y() + self.y() - self.dy) 185 | self.update() 186 | self.move_signal.emit(self.x(),self.y()) 187 | 188 | self.setCursor(Qt.SizeAllCursor) 189 | 190 | 191 | 192 | 193 | class mutilocr(QThread): 194 | """多图片文字识别线程""" 195 | statusbarsignal = pyqtSignal(str) 196 | ocr_signal = pyqtSignal(str, str) 197 | 198 | def __init__(self, files): 199 | super(mutilocr, self).__init__() 200 | self.files = files 201 | self.threadlist = [] 202 | self.filename = "" 203 | 204 | def run(self) -> None: 205 | for file in self.files: 206 | self.statusbarsignal.emit('开始识别图片') 207 | filename = os.path.basename(file) 208 | self.filename = filename 209 | with open(file, 'rb') as f: 210 | img_bytes = f.read() 211 | # 从字节数组读取图像 212 | np_array = np.frombuffer(img_bytes, np.uint8) 213 | img = cv2.imdecode(np_array, cv2.IMREAD_COLOR) 214 | print("正在识别图片:\t" + filename) 215 | 216 | self.statusbarsignal.emit('正在识别: ' + filename) 217 | self.ocr_signal.emit(self.filename, "\n>>>>识别图片:{}<<<<\n".format(filename)) 218 | th = OcrimgThread(img) 219 | th.result_show_signal.connect(self.mutil_cla_signalhandle) 220 | th.start() 221 | th.wait() 222 | self.threadlist.append(th) 223 | 224 | def mutil_cla_signalhandle(self, text): 225 | """一个结果回调""" 226 | self.ocr_signal.emit(self.filename, text) 227 | print("已识别{}".format(self.filename)) 228 | 229 | 230 | class OcrimgThread(QThread): 231 | """文字识别线程""" 232 | # simple_show_signal = pyqtSignal(str) 233 | result_show_signal = pyqtSignal(str) 234 | statusbar_signal = pyqtSignal(str) 235 | det_res_img = pyqtSignal(QPixmap)# 返回文字监测结果 236 | boxes_info_signal = pyqtSignal(list)# 返回识别信息结果 237 | def __init__(self, image): 238 | super(QThread, self).__init__() 239 | self.image = image # img 240 | self.ocr_result = None 241 | self.ocr_sys = None 242 | # self.simple_show_signal.connect(jamtools.simple_show) 243 | def get_match_text(self,match_text_boxes): 244 | if self.ocr_sys is not None: 245 | return self.ocr_sys.get_format_text(match_text_boxes) 246 | def run(self): 247 | self.statusbar_signal.emit('正在识别文字...') 248 | try: 249 | self.ocr_sys = OcrDetector(self.image,use_dnn = False,version=3)# 支持v2和v3版本的 250 | stime = time.time() 251 | # 得到检测框 252 | dt_boxes = self.ocr_sys.get_boxes() 253 | image = self.ocr_sys.draw_boxes(dt_boxes[0],self.image) 254 | # cv2.imwrite("testocr.png",image) 255 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 256 | # 创建QImage对象 257 | height, width, channel = image.shape 258 | bytesPerLine = 3 * width 259 | qimage = QImage(image.data, width, height, bytesPerLine, QImage.Format_RGB888) 260 | 261 | # 创建QPixmap对象 262 | qpixmap = QPixmap.fromImage(qimage) 263 | self.det_res_img.emit(qpixmap) 264 | 265 | dettime = time.time() 266 | print(len(dt_boxes[0])) 267 | if len(dt_boxes[0])==0: 268 | text="<没有识别到文字>" 269 | else: 270 | # 识别 results: 单纯的识别结果,results_info: 识别结果+置信度 原图 271 | # 识别模型固定尺寸只能100长度,需要处理可以根据自己场景导出模型 1000 272 | # onnx可以支持动态,不受限 273 | results, results_info = self.ocr_sys.recognition_img(dt_boxes) 274 | # print(f'results :{str(results)}') 275 | print("识别时间:",time.time()-dettime,dettime - stime) 276 | match_text_boxes = self.ocr_sys.get_match_text_boxes(dt_boxes[0],results) 277 | text= self.ocr_sys.get_format_text(match_text_boxes) 278 | self.boxes_info_signal.emit(match_text_boxes) 279 | # print(text) 280 | except Exception as e: 281 | print("Unexpected error:",e, "jampublic l326") 282 | text = str(sys.exc_info()[0]) 283 | self.statusbar_signal.emit('识别出错!{}'.format(text)) 284 | # print(text) 285 | if text == '': 286 | text = '没有识别到文字' 287 | self.ocr_result = text 288 | self.result_show_signal.emit(text) 289 | self.statusbar_signal.emit('识别完成!') 290 | 291 | print("识别完成") 292 | 293 | 294 | class Transparent_windows(QLabel): 295 | def __init__(self, x=0, y=0, w=0, h=0, color=Qt.red, havelabel=False): 296 | super().__init__() 297 | self.setGeometry(x - 5, y - 5, w + 10, h + 10) 298 | self.area = (x, y, w, h) 299 | self.x, self.y, self.w, self.h = x, y, w, h 300 | self.color = color 301 | self.setAttribute(Qt.WA_TransparentForMouseEvents, True) 302 | self.setAttribute(Qt.WA_TranslucentBackground, True) 303 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint) 304 | if havelabel: 305 | self.label = QLabel(self) 306 | self.label.setGeometry(self.w - 55, 6 + self.h + 1, 60, 14) 307 | self.label.setStyleSheet("color: green;border: 2 px;font-weight:bold;") 308 | 309 | def setGeometry(self, x, y, w, h): 310 | super(Transparent_windows, self).setGeometry(x - 5, y - 5, w + 10, h + 10) 311 | self.area = (x, y, w, h) 312 | 313 | def paintEvent(self, e): 314 | super().paintEvent(e) 315 | x, y, w, h = self.area 316 | p = QPainter(self) 317 | p.setPen(QPen(self.color, 2, Qt.SolidLine)) 318 | p.drawRect(QRect(3, 3, w + 4, h + 4)) 319 | p.end() 320 | 321 | 322 | class Commen_Thread(QThread): 323 | def __init__(self, action, *args): 324 | super(QThread, self).__init__() 325 | self.action = action 326 | self.args = args 327 | 328 | def run(self): 329 | print('start_thread params:{}'.format(self.args)) 330 | if self.args: 331 | print(self.args) 332 | if len(self.args) == 1: 333 | self.action(self.args[0]) 334 | elif len(self.args) == 2: 335 | self.action(self.args[0], self.args[1]) 336 | elif len(self.args) == 3: 337 | self.action(self.args[0], self.args[1], self.args[2]) 338 | elif len(self.args) == 4: 339 | self.action(self.args[0], self.args[1], self.args[2], self.args[3]) 340 | else: 341 | self.action() 342 | 343 | 344 | 345 | if __name__ == '__main__': 346 | # app = QApplication(sys.argv) 347 | # w = Transparent_windows(20, 20, 500, 200) 348 | # w.show() 349 | import json 350 | a = gethtml("https://raw.githubusercontent.com/fandesfyf/JamTools/main/ci_scripts/versions.json") 351 | print(json.loads(a)) 352 | # w.setGeometry() 353 | # sys.exit(app.exec_()) 354 | -------------------------------------------------------------------------------- /jamroll_screenshot.py: -------------------------------------------------------------------------------- 1 | import math 2 | import operator 3 | import os 4 | import random 5 | import sys 6 | import time 7 | from functools import reduce 8 | 9 | import cv2 10 | from numpy import zeros, uint8, asarray, vstack, float32 11 | from PIL import Image 12 | from PyQt5.QtCore import Qt, pyqtSignal, QStandardPaths, QTimer, QSettings, QObject 13 | from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor 14 | from PyQt5.QtWidgets import QApplication, QLabel 15 | from pynput.mouse import Controller as MouseController 16 | from pynput.mouse import Listener as MouseListenner 17 | from pynput import mouse 18 | 19 | from jampublic import Commen_Thread, TipsShower, CONFIG_DICT 20 | 21 | if not os.path.exists("j_temp"): 22 | os.mkdir("j_temp") 23 | 24 | class PicMatcher: # 图像匹配类 25 | def __init__(self, nfeatures=500, draw=False): 26 | self.SIFT = cv2.xfeatures2d_SIFT.create(nfeatures=nfeatures) # 生成sift算子 27 | self.bf = cv2.BFMatcher() # 生成图像匹配器 28 | self.draw = draw 29 | 30 | def match(self, im1, im2): # 图像匹配函数,获得图像匹配的点以及匹配程度 31 | print("start match") 32 | # 提取特征并计算描述子 33 | kps1, des1 = self.SIFT.detectAndCompute(im1, None) 34 | kps2, des2 = self.SIFT.detectAndCompute(im2, None) 35 | # kps1, des1 = SURF.detectAndCompute(im1, None) 36 | # kps2, des2 = SURF.detectAndCompute(im2, None) 37 | 38 | # BFMatcher with default params 39 | matches = self.bf.knnMatch(des1, des2, k=2) 40 | good = [] 41 | tempgoods = [] 42 | distances = {} # 储存距离计数的数组 43 | for m, n in matches: 44 | # print(m.distance) 45 | if m.distance == 0: 46 | pos0 = kps1[m.queryIdx].pt 47 | pos1 = kps2[m.trainIdx].pt 48 | if pos1[0] == pos0[0] and pos0[1] > pos1[1]: # 筛选拼接点 49 | d = int(pos0[1] - pos1[1]) 50 | if d in distances: 51 | distances[d] += 1 52 | else: 53 | distances[d] = 0 54 | tempgoods.append(m) 55 | # print(distances) 56 | sorteddistance = sorted(distances.items(), key=lambda kv: kv[1], reverse=True) 57 | max1y = 0 58 | max2y = 0 59 | try: 60 | distancesmode = sorteddistance[0][0] 61 | if sorteddistance[0][1] < 0: # 4为特征点数,当大于4时才认为特征明显 62 | print("rt 0 0 0") 63 | return 0, 0, 0 64 | except: # 一般捕获的是完全没有匹配的情况即distances为空 65 | print(sys.exc_info(), 113) 66 | return 0, 0, 0 67 | for match in tempgoods: 68 | pos0 = kps1[match.queryIdx].pt 69 | pos1 = kps2[match.trainIdx].pt 70 | if int(pos0[1] - pos1[1]) == distancesmode: 71 | if pos0[1] > max1y: 72 | max1y = pos0[1] 73 | if pos1[1] > max2y: 74 | max2y = pos1[1] 75 | good.append(match) 76 | print(len(good), distances, max1y, max2y) 77 | if self.draw: 78 | self.paint(im1, im2, kps1, kps2, good) 79 | return distancesmode, max1y, max2y 80 | 81 | def paint(self, im1, im2, kps1, kps2, good): 82 | img3 = cv2.drawMatches(im1, kps1, im2, kps2, good, None, flags=2) 83 | 84 | # 新建一个空图像用于绘制特征点 85 | img_sift1 = zeros(im1.shape, uint8) 86 | img_sift2 = zeros(im2.shape, uint8) 87 | 88 | # 绘制特征点 89 | cv2.drawKeypoints(im1, kps1, img_sift1) 90 | cv2.drawKeypoints(im2, kps2, img_sift2) 91 | # 展示 92 | # cv2.namedWindow("im1", cv2.WINDOW_NORMAL) 93 | # cv2.namedWindow("im2", cv2.WINDOW_NORMAL) 94 | # cv2.resizeWindow("im1", im2.shape[1], im2.shape[0]) 95 | # cv2.resizeWindow("im2", im2.shape[1], im2.shape[0]) 96 | # cv2.imshow("im1", img_sift1) 97 | # cv2.imshow("im2", img_sift2) 98 | # cv2.imshow("match{}".format(random.randint(0, 8888)), img3) 99 | # cv2.waitKey(1 ) 100 | cv2.imwrite("j_temp/match{}.png".format(time.time()), img3) 101 | print("write a frame") 102 | 103 | 104 | class Splicing_shots(QObject): # 滚动截屏主类 105 | showm_signal = pyqtSignal(str) 106 | statusBar_signal = pyqtSignal(str) 107 | 108 | def __init__(self, parent: QLabel = None, draw=False): 109 | super(Splicing_shots, self).__init__() 110 | self.parent = parent 111 | 112 | self.clear_timer = QTimer() # 后台清理器,暂时不用 113 | self.clear_timer.timeout.connect(self.setup) 114 | # self.showrect = Transparent_windows() 115 | self.settings = QSettings('Fandes', 'jamtools') 116 | self.picMatcher = PicMatcher(nfeatures=self.settings.value('screenshot/roll_nfeatures', 500, type=int), 117 | draw=draw) 118 | # self.init_splicing_shots_thread = Commen_Thread(self.init_splicing_shots) 119 | # self.init_splicing_shots_thread.start() 120 | self.init_splicing_shots() 121 | 122 | def init_splicing_shots(self): 123 | self.finalimg = None 124 | self.img_list = [] 125 | self.roll_speed = self.settings.value('screenshot/roll_speed', 3, type=int) 126 | self.in_rolling = False 127 | if not os.path.exists(QStandardPaths.writableLocation( 128 | QStandardPaths.PicturesLocation) + '/JamPicture/screenshots'): 129 | os.mkdir(QStandardPaths.writableLocation( 130 | QStandardPaths.PicturesLocation) + '/JamPicture/screenshots') 131 | 132 | def setup(self): 133 | if self.clear_timer.isActive(): 134 | self.clear_timer.stop() 135 | print('clear') 136 | self.finalimg = None 137 | self.img_list = [] 138 | self.roll_speed = self.settings.value('screenshot/roll_speed', 3, type=int) 139 | self.in_rolling = False 140 | if not os.path.exists("j_temp"): 141 | os.mkdir("j_temp") 142 | 143 | def is_same(self, img1, img2): # 判断两张图片的相似度,用于判断结束条件 144 | h1 = img1.histogram() 145 | h2 = img2.histogram() 146 | result = math.sqrt(reduce(operator.add, list(map(lambda a, b: (a - b) ** 2, h1, h2))) / len(h1)) 147 | if result <= 5: 148 | return True 149 | else: 150 | return False 151 | 152 | def match_and_merge(self): # 图像寻找拼接点并拼接的主函数,运行于后台线程,从self.img_list中取数据进行拼接 153 | same = 0 154 | while not len(self.img_list): # 首次运行时进行判断是否开始收到数据 155 | time.sleep(0.1) 156 | try: 157 | self.finalimg = self.img_list.pop(0) 158 | compare_img1 = cv2.cvtColor(self.finalimg, cv2.COLOR_RGB2GRAY) 159 | except: 160 | print("线程启动过早", sys.exc_info()) 161 | return 162 | 163 | while self.in_rolling or len(self.img_list): # 如果正在滚动或者self.img_list有图片没有被拼接 164 | if len(self.img_list): # 如果有图片 165 | rgbimg2 = self.img_list.pop(0) # 取出图片 166 | compare_img2 = cv2.cvtColor(rgbimg2, cv2.COLOR_RGB2GRAY) # 预处理 167 | distance, m1, m2 = self.picMatcher.match(compare_img1, compare_img2) # 调用picMatcher的match方法获取拼接匹配信息 168 | # compare_img2 = self.offset(compare_img2, distance) 169 | if distance == 0: 170 | print("重复!", same) 171 | same += 1 172 | if same >= 3: 173 | print("roller same break") 174 | break 175 | continue 176 | else: 177 | same = 0 178 | 179 | finalheightforcutting = self.finalimg.shape[0] 180 | imgheight = rgbimg2.shape[0] 181 | finalheightforcutting -= imgheight - int(m1) # 拼接图片的裁剪高度 182 | i1 = self.finalimg[:finalheightforcutting, :, :] 183 | i2 = rgbimg2[int(m2):, :, :] 184 | self.finalimg = vstack((i1, i2)) 185 | compare_img1 = compare_img2 186 | print("sucesmerge a img") 187 | else: # 等待图片到来 188 | # print("waiting for img") 189 | time.sleep(0.05) # 后台线程没有收到图片时,睡眠一下避免占用过高 190 | self.in_rolling = False 191 | print("end merge") 192 | CONFIG_DICT["last_pic_save_name"]="{}".format( str(time.strftime("%Y-%m-%d_%H.%M.%S", time.localtime()))) 193 | cv2.imwrite("j_temp/{}.png".format(CONFIG_DICT["last_pic_save_name"]), self.finalimg) 194 | 195 | print("长图片保存到j_temp/{}.png".format(CONFIG_DICT["last_pic_save_name"])) 196 | # cv2.imshow("finalimg", self.finalimg) 197 | # cv2.waitKey(0) 198 | 199 | def auto_roll(self, area): # 自动滚动模式 200 | x, y, w, h = area 201 | self.rollermask.tips.setText("单击停止") 202 | QApplication.processEvents() 203 | speed = round(1 / self.roll_speed, 2) 204 | screen = QApplication.primaryScreen() 205 | winid = QApplication.desktop().winId() 206 | 207 | def onclick(x, y, button, pressed): # 点击退出的函数句柄 208 | if pressed: 209 | print("click to stop") 210 | self.in_rolling = False 211 | listener.stop() 212 | 213 | controler = MouseController() # 鼠标控制器 214 | listener = MouseListenner(on_click=onclick) # 鼠标监听器 215 | self.match_thread = Commen_Thread(self.match_and_merge) # 把match_and_merge放入一个线程中 216 | self.in_rolling = True 217 | i = 0 218 | controler.position = (area[0] + int(area[2] / 2), area[1] + int(area[3] / 2)) # 控制鼠标点击到滚动区域中心 219 | oldimg = Image.new("RGB", (128, 128), "#FF0f00") 220 | listener.start() 221 | while self.in_rolling: 222 | st = time.time() 223 | pix = screen.grabWindow(winid, x, y, w, h) # 截屏 224 | newimg = Image.fromqpixmap(pix) # 转化为pil的格式 225 | img = cv2.cvtColor(asarray(newimg), cv2.COLOR_RGB2BGR) 226 | self.img_list.append(img) # 图片数据存入self.img_list中被后台的拼接线程使用 227 | 228 | # cv2.imshow("FSd", img) 229 | # cv2.waitKey(0) 230 | if i >= 1: 231 | if i == 1: 232 | self.match_thread.start() # 当截第二张图片时拼接线程才启动 233 | if self.is_same(oldimg, newimg): # 每帧检查是否停止 234 | self.in_rolling = False 235 | i += 1 236 | break 237 | oldimg = newimg 238 | controler.scroll(dx=0, dy=-3) # 控制鼠标滚动 239 | time.sleep(speed) # 通过sleep控制自动滚动速度 240 | # cv2.imwrite('j_temp/{0}.png'.format(i), img) 241 | i += 1 242 | print("结束滚动,共截屏{}张".format(i)) 243 | listener.stop() 244 | # self.showrect.hide() 245 | self.match_thread.wait() 246 | 247 | def inthearea(self, pos, area): # 点在区域中 248 | if area[0] < pos[0] < area[0] + area[2] and area[1] < pos[1] < area[1] + area[3]: 249 | return True 250 | else: 251 | return False 252 | 253 | def scroll_to_roll(self, area): # 手动滚动模式 254 | x, y, w, h = area 255 | self.rollermask.tips.setText("向下滚动,单击结束") 256 | QApplication.processEvents() 257 | screen = QApplication.primaryScreen() 258 | winid = QApplication.desktop().winId() 259 | self.id = self.rid = 0 260 | self.a_step = 0 261 | 262 | def onclick(x, y, button, pressed): 263 | if pressed: 264 | 265 | pix = screen.grabWindow(winid, x, y, w, h) 266 | newimg = Image.fromqpixmap(pix) 267 | img = cv2.cvtColor(asarray(newimg), cv2.COLOR_RGB2BGR) 268 | self.img_list.append(img) 269 | else: 270 | print("click to stop", len(self.img_list)) 271 | self.in_rolling = False 272 | listener.stop() 273 | 274 | def on_scroll(px, py, x_axis, y_axis): 275 | # print(px, py, x_axis, y_axis) 276 | # if not self.inthearea((px,py),area): 277 | # return 278 | self.a_step += 1 279 | if self.a_step < 2: 280 | return 281 | else: 282 | self.a_step = 0 283 | if y_axis < 0: 284 | if self.rid >= self.id: # 当滚动id与真实id一样时说明 285 | pix = screen.grabWindow(winid, x, y, w, h) # 滚动段距离进行截屏 286 | newimg = Image.fromqpixmap(pix) 287 | img = cv2.cvtColor(asarray(newimg), cv2.COLOR_RGB2BGR) 288 | self.img_list.append(img) 289 | # cv2.imwrite("j_temp/{}.png".format(self.id), img) 290 | self.id += 1 # 记录当前滚动的id 291 | self.rid = self.id 292 | else: # 不一样时说明用户往回滚了,跳过 293 | print("跳过") 294 | self.rid += 1 295 | else: # 方向往回滚时id-1,以记录往回的步数 296 | self.rid -= 1 297 | print("方向错误") 298 | 299 | listener = MouseListenner(on_click=onclick, on_scroll=on_scroll) # 鼠标监听器,传入函数句柄 300 | self.match_thread = Commen_Thread(self.match_and_merge) # 也是把拼接函数放入后台线程中 301 | self.in_rolling = True 302 | i = 0 303 | listener.start() # 鼠标监听器启动 304 | self.match_thread.start() # 拼接线程启动 305 | while self.in_rolling: # 等待结束滚动 306 | time.sleep(0.2) 307 | listener.stop() 308 | # self.showrect.hide() 309 | self.match_thread.wait() # 等待拼接线程结束 310 | 311 | def roll_manager(self, area): # 滚动截屏控制器,控制滚动截屏的模式(自动还是手动滚) 312 | x, y, w, h = area 313 | self.mode = 1 314 | 315 | def on_click(x, y, button, pressed): # 用户点击了屏幕说明用户想自动滚 316 | print(x, y, button) 317 | if button == mouse.Button.left: 318 | if area[0] < x < area[0] + area[2] and area[1] < y < area[1] + area[3] and not pressed: 319 | self.mode = 1 320 | lis.stop() 321 | elif button == mouse.Button.right: 322 | self.mode = 2 323 | lis.stop() 324 | 325 | def on_scroll(x, y, button, pressed): # 用户滚动了鼠标说明用户想要手动滚 326 | 327 | self.mode = 0 328 | lis.stop() 329 | 330 | self.rollermask = roller_mask(area) # 滚动截屏遮罩层初始化 331 | # self.showrect.setGeometry(x, y, w, h) 332 | # self.showrect.show() 333 | pix = QApplication.primaryScreen().grabWindow(QApplication.desktop().winId(), x, y, w, h) # 先截一张图片 334 | newimg = Image.fromqpixmap(pix) 335 | img = cv2.cvtColor(asarray(newimg), cv2.COLOR_RGB2BGR) 336 | self.img_list.append(img) 337 | QApplication.processEvents() 338 | lis = MouseListenner(on_click=on_click, on_scroll=on_scroll) # 鼠标监听器初始化并启动 339 | lis.start() 340 | print("等待用户开始") 341 | lis.join() 342 | lis.stop() 343 | if self.mode == 1: # 判断用户选择的模式 344 | print("auto_roll") 345 | self.auto_roll(area) 346 | elif self.mode == 2: 347 | print("exit roller") 348 | return 1 349 | else: 350 | print("scroll_to_roll") 351 | self.scroll_to_roll(area) 352 | self.showm_signal.emit("长截图完成") 353 | self.rollermask.hide() 354 | return 0 355 | 356 | 357 | class roller_mask(QLabel): # 滚动截屏遮罩层 358 | def __init__(self, area): 359 | super(roller_mask, self).__init__() 360 | transparentpix = QPixmap(QApplication.desktop().size()) 361 | transparentpix.fill(Qt.transparent) 362 | self.area = area 363 | 364 | self.tips = TipsShower("单击自动滚动;\n或手动下滚;", area) 365 | 366 | self.setAttribute(Qt.WA_TransparentForMouseEvents, True) 367 | self.setAttribute(Qt.WA_TranslucentBackground, True) 368 | self.setWindowFlags(Qt.FramelessWindowHint) 369 | self.setPixmap(transparentpix) 370 | self.showFullScreen() 371 | 372 | def settext(self, text: str, autoclose=True): # 设置提示文字 373 | self.tips.setText(text, autoclose) 374 | 375 | def paintEvent(self, e): # 绘制遮罩层 376 | super().paintEvent(e) 377 | x, y, w, h = self.area 378 | 379 | painter = QPainter(self) 380 | painter.setPen(QPen(Qt.red, 2, Qt.SolidLine)) 381 | painter.drawRect(x - 1, y - 1, w + 2, h + 2) 382 | painter.setPen(Qt.NoPen) 383 | painter.setBrush(QColor(0, 0, 0, 120)) 384 | painter.drawRect(0, 0, x, self.height()) 385 | painter.drawRect(x, 0, self.width() - x, y) 386 | painter.drawRect(x + w, y, self.width() - x - w, self.height() - y) 387 | painter.drawRect(x, y + h, w, self.height() - y - h) 388 | painter.end() 389 | 390 | 391 | if __name__ == '__main__': 392 | app = QApplication(sys.argv) 393 | s = Splicing_shots() 394 | # s.img_list = [cv2.imread("j_temp/{}.png".format(name)) for name in range(45, 51)] 395 | # s.match_and_merge() 396 | s.roll_manager((400, 60, 500, 600)) 397 | # t = TipsShower("按下以开始") 398 | sys.exit(app.exec_()) 399 | -------------------------------------------------------------------------------- /jamspeak.py: -------------------------------------------------------------------------------- 1 | #!usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/9/2 22:52 4 | # @Author : Fandes 5 | # @FileName: jamspeak.py 6 | # @Software: PyCharm 7 | import os 8 | import random 9 | import sys 10 | import time 11 | 12 | import requests 13 | import pyttsx3 14 | if sys.platform == "win32": 15 | import pyttsx3.drivers 16 | import pyttsx3.drivers.sapi5 17 | 18 | try: 19 | speaker_engine = pyttsx3.init() 20 | except: 21 | e = sys.exc_info() 22 | if "libespeak.so" in e[1].args[0]: 23 | try: 24 | os.system("sudo apt-get install espeak -y") 25 | except Exception as ex: 26 | print("linux下驱动未安装,请尝试使用`sudo apt-get install espeak`命令进行安装",ex) 27 | print(e, __file__) 28 | speaker_engine = None 29 | class Speaker(): 30 | def speak(self, text="none", lan="en"): 31 | try: 32 | self.stop() 33 | speaker_engine.say(text) 34 | # speaker_engine.startLoop(False) 35 | speaker_engine.runAndWait() 36 | except: 37 | e = sys.exc_info() 38 | if "libespeak.so" in e[1].args[0]: 39 | try: 40 | os.system("sudo apt-get install espeak -y") 41 | except Exception as ex: 42 | print("linux下驱动未安装,请尝试使用`sudo apt-get install espeak`命令进行安装",ex) 43 | print(e, __file__) 44 | def stop(self): 45 | if speaker_engine is not None and speaker_engine.isBusy(): 46 | speaker_engine.stop() 47 | def is_alphabet(self, uchar): 48 | """判断一个unicode是否是英文字母""" 49 | if (u'\u0041' <= uchar <= u'\u005a') or (u'\u0061' <= uchar <= u'\u007a'): 50 | return True 51 | else: 52 | return False 53 | 54 | 55 | if __name__ == '__main__': 56 | s = Speaker() 57 | s.speak() 58 | -------------------------------------------------------------------------------- /requirement.txt: -------------------------------------------------------------------------------- 1 | Wheel 2 | Pillow 3 | pynput 4 | requests 5 | pyttsx3 6 | PyQt5==5.15.2 7 | PyQt5-sip==12.8.1 8 | PyQt5-stubs==5.14.2.2 9 | numpy 10 | fake-useragent==0.1.11 11 | chardet 12 | comtypes 13 | opencv-contrib-python 14 | 15 | --------------------------------------------------------------------------------