pyxllib 0.3.197__py3-none-any.whl → 3.201.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. pyxllib/__init__.py +14 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +537 -541
  5. pyxllib/algo/intervals.py +964 -964
  6. pyxllib/algo/matcher.py +389 -389
  7. pyxllib/algo/newbie.py +166 -166
  8. pyxllib/algo/pupil.py +629 -629
  9. pyxllib/algo/shapelylib.py +67 -67
  10. pyxllib/algo/specialist.py +241 -241
  11. pyxllib/algo/stat.py +494 -494
  12. pyxllib/algo/treelib.py +145 -149
  13. pyxllib/algo/unitlib.py +62 -66
  14. pyxllib/autogui/__init__.py +5 -5
  15. pyxllib/autogui/activewin.py +246 -246
  16. pyxllib/autogui/all.py +9 -9
  17. pyxllib/autogui/autogui.py +846 -852
  18. pyxllib/autogui/uiautolib.py +362 -362
  19. pyxllib/autogui/virtualkey.py +102 -102
  20. pyxllib/autogui/wechat.py +827 -827
  21. pyxllib/autogui/wechat_msg.py +421 -421
  22. pyxllib/autogui/wxautolib.py +84 -84
  23. pyxllib/cv/__init__.py +5 -5
  24. pyxllib/cv/expert.py +267 -267
  25. pyxllib/cv/imfile.py +159 -159
  26. pyxllib/cv/imhash.py +39 -39
  27. pyxllib/cv/pupil.py +9 -9
  28. pyxllib/cv/rgbfmt.py +1525 -1525
  29. pyxllib/cv/slidercaptcha.py +137 -137
  30. pyxllib/cv/trackbartools.py +251 -251
  31. pyxllib/cv/xlcvlib.py +1040 -1040
  32. pyxllib/cv/xlpillib.py +423 -423
  33. pyxllib/data/echarts.py +236 -240
  34. pyxllib/data/jsonlib.py +85 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1111 -1127
  37. pyxllib/data/sqlite.py +568 -568
  38. pyxllib/data/sqllib.py +297 -297
  39. pyxllib/ext/JLineViewer.py +505 -505
  40. pyxllib/ext/__init__.py +6 -6
  41. pyxllib/ext/demolib.py +251 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/qt.py +449 -449
  45. pyxllib/ext/robustprocfile.py +493 -497
  46. pyxllib/ext/seleniumlib.py +76 -76
  47. pyxllib/ext/tk.py +173 -173
  48. pyxllib/ext/unixlib.py +821 -827
  49. pyxllib/ext/utools.py +345 -351
  50. pyxllib/ext/webhook.py +124 -119
  51. pyxllib/ext/win32lib.py +40 -40
  52. pyxllib/ext/wjxlib.py +91 -88
  53. pyxllib/ext/wpsapi.py +124 -124
  54. pyxllib/ext/xlwork.py +9 -9
  55. pyxllib/ext/yuquelib.py +1110 -1105
  56. pyxllib/file/__init__.py +17 -17
  57. pyxllib/file/docxlib.py +757 -761
  58. pyxllib/file/gitlib.py +309 -309
  59. pyxllib/file/libreoffice.py +165 -165
  60. pyxllib/file/movielib.py +144 -148
  61. pyxllib/file/newbie.py +10 -10
  62. pyxllib/file/onenotelib.py +1469 -1469
  63. pyxllib/file/packlib/__init__.py +330 -330
  64. pyxllib/file/packlib/zipfile.py +2441 -2441
  65. pyxllib/file/pdflib.py +422 -426
  66. pyxllib/file/pupil.py +185 -185
  67. pyxllib/file/specialist/__init__.py +681 -685
  68. pyxllib/file/specialist/dirlib.py +799 -799
  69. pyxllib/file/specialist/download.py +193 -193
  70. pyxllib/file/specialist/filelib.py +2825 -2829
  71. pyxllib/file/xlsxlib.py +3122 -3131
  72. pyxllib/file/xlsyncfile.py +341 -341
  73. pyxllib/prog/__init__.py +5 -5
  74. pyxllib/prog/cachetools.py +58 -64
  75. pyxllib/prog/deprecatedlib.py +233 -233
  76. pyxllib/prog/filelock.py +42 -42
  77. pyxllib/prog/ipyexec.py +253 -253
  78. pyxllib/prog/multiprogs.py +940 -940
  79. pyxllib/prog/newbie.py +451 -451
  80. pyxllib/prog/pupil.py +1208 -1197
  81. pyxllib/prog/sitepackages.py +33 -33
  82. pyxllib/prog/specialist/__init__.py +348 -391
  83. pyxllib/prog/specialist/bc.py +203 -203
  84. pyxllib/prog/specialist/browser.py +497 -497
  85. pyxllib/prog/specialist/common.py +347 -347
  86. pyxllib/prog/specialist/datetime.py +198 -198
  87. pyxllib/prog/specialist/tictoc.py +240 -240
  88. pyxllib/prog/specialist/xllog.py +180 -180
  89. pyxllib/prog/xlosenv.py +110 -108
  90. pyxllib/stdlib/__init__.py +17 -17
  91. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  92. pyxllib/stdlib/tablepyxl/style.py +303 -303
  93. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  94. pyxllib/text/__init__.py +8 -8
  95. pyxllib/text/ahocorasick.py +36 -39
  96. pyxllib/text/airscript.js +754 -744
  97. pyxllib/text/charclasslib.py +121 -121
  98. pyxllib/text/jiebalib.py +267 -267
  99. pyxllib/text/jinjalib.py +27 -32
  100. pyxllib/text/jsa_ai_prompt.md +271 -271
  101. pyxllib/text/jscode.py +922 -922
  102. pyxllib/text/latex/__init__.py +158 -158
  103. pyxllib/text/levenshtein.py +303 -303
  104. pyxllib/text/nestenv.py +1215 -1215
  105. pyxllib/text/newbie.py +300 -300
  106. pyxllib/text/pupil/__init__.py +8 -8
  107. pyxllib/text/pupil/common.py +1121 -1121
  108. pyxllib/text/pupil/xlalign.py +326 -326
  109. pyxllib/text/pycode.py +47 -47
  110. pyxllib/text/specialist/__init__.py +8 -8
  111. pyxllib/text/specialist/common.py +112 -112
  112. pyxllib/text/specialist/ptag.py +186 -186
  113. pyxllib/text/spellchecker.py +172 -172
  114. pyxllib/text/templates/echart_base.html +10 -10
  115. pyxllib/text/templates/highlight_code.html +16 -16
  116. pyxllib/text/templates/latex_editor.html +102 -102
  117. pyxllib/text/vbacode.py +17 -17
  118. pyxllib/text/xmllib.py +741 -747
  119. pyxllib/xl.py +42 -39
  120. pyxllib/xlcv.py +17 -17
  121. pyxllib-3.201.1.dist-info/METADATA +296 -0
  122. pyxllib-3.201.1.dist-info/RECORD +125 -0
  123. {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
  124. pyxllib/ext/old.py +0 -663
  125. pyxllib-0.3.197.dist-info/METADATA +0 -48
  126. pyxllib-0.3.197.dist-info/RECORD +0 -126
  127. {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/WHEEL +0 -0
pyxllib/cv/xlcvlib.py CHANGED
@@ -1,1040 +1,1040 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2021/08/25 15:57
6
-
7
- import base64
8
-
9
- import PIL.Image
10
- import cv2
11
- import filetype
12
- import humanfriendly
13
- import numpy as np
14
- import requests
15
-
16
-
17
- from pyxllib.prog.newbie import round_int, RunOnlyOnce
18
- from pyxllib.prog.pupil import EnchantBase, EnchantCvt
19
- from pyxllib.algo.geo import rect_bounds, warp_points, reshape_coords, quad_warp_wh, get_warp_mat, rect2polygon
20
- from pyxllib.file.specialist import XlPath
21
-
22
- _show_win_num = 0
23
-
24
-
25
- def _rgb_to_bgr_list(a):
26
- # 类型转为list
27
- if isinstance(a, np.ndarray):
28
- a = a.tolist()
29
- elif not isinstance(a, (list, tuple)):
30
- a = [a]
31
- if len(a) > 2:
32
- a[:3] = [a[2], a[1], a[0]]
33
- return a
34
-
35
-
36
- class xlcv(EnchantBase):
37
-
38
- @classmethod
39
- @RunOnlyOnce
40
- def enchant(cls):
41
- """ 把xlcv的功能嵌入cv2中
42
-
43
- 不太推荐使用该类,可以使用CvImg类更好地解决问题。
44
- """
45
- # 虽然只绑定cv2,但其他相关的几个库的方法上,最好也不要重名
46
- names = cls.check_enchant_names([np.ndarray, PIL.Image, PIL.Image.Image])
47
- names -= {'concat'}
48
- cls._enchant(cv2, names, EnchantCvt.staticmethod2modulefunc)
49
-
50
- @staticmethod
51
- def __1_read():
52
- pass
53
-
54
- @staticmethod
55
- def read(file, flags=None, **kwargs) -> np.ndarray:
56
- """
57
- :param file: 支持非文件路径参数,会做类型转换
58
- 因为这个接口的灵活性,要判断file参数类型等,速度会慢一点点
59
- 如果需要效率,可以显式使用imread、Image.open等明确操作类型
60
- :param flags:
61
- -1,按照图像原样读取,保留Alpha通道(第4通道)
62
- 0,将图像转成单通道灰度图像后读取
63
- 1,将图像转换成3通道BGR彩色图像
64
-
65
- 220426周二14:20,注,有些图位深不是24而是48,读到的不是uint8而是uint16
66
- 目前这个接口没做适配,需要下游再除以256后arr.astype('uint8')
67
- """
68
- from pyxllib.cv.xlpillib import xlpil
69
-
70
- if xlcv.is_cv2_image(file):
71
- im = file
72
- elif XlPath.safe_init(file):
73
- # https://www.yuque.com/xlpr/pyxllib/imread
74
- # + np.frombuffer
75
- im = cv2.imdecode(np.fromfile(str(file), dtype=np.uint8), -1 if flags is None else flags)
76
- if im is None: # 在文件类型名写错时,可能会读取失败
77
- raise ValueError(f'{file} {filetype.guess(file)}')
78
- elif xlpil.is_pil_image(file):
79
- im = xlpil.to_cv2_image(file)
80
- else:
81
- raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
82
-
83
- if im.dtype == np.uint16:
84
- # uint16类型,统一转为uint8。就我目前所会遇到的所有需求,都不需要用到uint16,反而会带来很多麻烦。
85
- im = cv2.convertScaleAbs(im, alpha=255. / 65535.)
86
-
87
- im = xlcv.cvt_channel(im, flags)
88
-
89
- return im
90
-
91
- @staticmethod
92
- def read_from_buffer(buffer, flags=None, *, b64decode=False):
93
- """ 从二进制流读取图片
94
- 这个二进制流指,图片以png、jpg等某种格式存储为文件时,其对应的文件编码
95
-
96
- :param bytes|str buffer:
97
- :param b64decode: 是否需要先进行base64解码
98
- """
99
- if b64decode:
100
- buffer = base64.b64decode(buffer)
101
- buffer = np.frombuffer(buffer, dtype=np.uint8)
102
- im = cv2.imdecode(buffer, -1 if flags is None else flags)
103
- return xlcv.cvt_channel(im, flags)
104
-
105
- @staticmethod
106
- def read_from_url(url, flags=None, *, b64decode=False):
107
- """ 从url直接获取图片到内存中
108
- """
109
- content = requests.get(url).content
110
- return xlcv.read_from_buffer(content, flags, b64decode=b64decode)
111
-
112
- @staticmethod
113
- def read_from_strokes(strokes, margin=10, color=(0, 0, 0), bgcolor=(255, 255, 255), thickness=2):
114
- """ 将联机手写笔划数据转成图片
115
-
116
- :param strokes: n个笔划,每个笔画包含不一定要一样长的m个点,每个点是(x, y)的结构
117
- :param margin: 图片边缘
118
- :param color: 前景笔划颜色,默认黑色
119
- :param bgcolor: 背景颜色,默认白色
120
- :param thickness: 笔划粗度
121
- """
122
- # 1 边界
123
- minx = min([p[0] for s in strokes for p in s])
124
- miny = min([p[1] for s in strokes for p in s])
125
- for stroke in strokes:
126
- for p in stroke:
127
- p[0] -= minx - margin
128
- p[1] -= miny - margin
129
-
130
- maxx = max([p[0] for s in strokes for p in s])
131
- maxy = max([p[1] for s in strokes for p in s])
132
-
133
- # 2 画出图片
134
- canvas = np.zeros((maxy + margin*2, maxx + margin*2, 3), dtype=np.uint8)
135
- canvas[:, :] = bgcolor
136
-
137
- # 画出每个笔划轨迹
138
- for stroke in strokes:
139
- for i in range(len(stroke) - 1):
140
- cv2.line(canvas, tuple(stroke[i]), tuple(stroke[i + 1]), color, thickness=thickness)
141
-
142
- return canvas
143
-
144
- @staticmethod
145
- def __2_attrs():
146
- pass
147
-
148
- @staticmethod
149
- def imsize(im):
150
- """ 图片尺寸,统一返回(height, width),不含通道 """
151
- return im.shape[:2]
152
-
153
- @staticmethod
154
- def n_channels(im):
155
- """ 通道数 """
156
- if im.ndim == 3:
157
- return im.shape[2]
158
- else:
159
- return 1
160
-
161
- @staticmethod
162
- def height(im):
163
- """ 注意PIL.Image.Image本来就有height、width属性,所以不用自定义这两个方法 """
164
- return im.shape[0]
165
-
166
- @staticmethod
167
- def width(im):
168
- return im.shape[1]
169
-
170
- @staticmethod
171
- def __3_write__(self):
172
- pass
173
-
174
- @staticmethod
175
- def to_pil_image(pic, mode=None):
176
- """ 我也不懂torch里这个实现为啥这么复杂,先直接哪来用,没深究细节
177
-
178
- Convert a tensor or an ndarray to PIL Image. (删除了tensor的转换功能)
179
-
180
- See :class:`~torchvision.transforms.ToPILImage` for more details.
181
-
182
- Args:
183
- pic (Tensor or numpy.ndarray): Image to be converted to PIL Image.
184
- mode (`PIL.Image mode`_): color space and pixel depth of input data (optional).
185
-
186
- .. _PIL.Image mode: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#concept-modes
187
-
188
- Returns:
189
- PIL Image: Image converted to PIL Image.
190
- """
191
- # BGR 转为 RGB
192
- if pic.ndim > 2 and pic.shape[2] >= 3:
193
- pic = pic.copy() # 221127周日20:47,发现一个天坑bug,如果没有这句copy修正,会修改原始的pic图片数据
194
- pic[:, :, [0, 1, 2]] = pic[:, :, [2, 1, 0]]
195
-
196
- # 以下是原版实现代码
197
- if pic.ndim not in {2, 3}:
198
- raise ValueError('pic should be 2/3 dimensional. Got {} dimensions.'.format(pic.ndim))
199
- if pic.ndim == 2:
200
- # if 2D image, add channel dimension (HWC)
201
- pic = np.expand_dims(pic, 2)
202
-
203
- npim = pic
204
-
205
- if npim.shape[2] == 1:
206
- expected_mode = None
207
- npim = npim[:, :, 0]
208
- if npim.dtype == np.uint8:
209
- expected_mode = 'L'
210
- elif npim.dtype == np.int16:
211
- expected_mode = 'I;16'
212
- elif npim.dtype == np.int32:
213
- expected_mode = 'I'
214
- elif npim.dtype == np.float32:
215
- expected_mode = 'F'
216
- if mode is not None and mode != expected_mode:
217
- raise ValueError("Incorrect mode ({}) supplied for input type {}. Should be {}"
218
- .format(mode, np.dtype, expected_mode))
219
- mode = expected_mode
220
-
221
- elif npim.shape[2] == 2:
222
- permitted_2_channel_modes = ['LA']
223
- if mode is not None and mode not in permitted_2_channel_modes:
224
- raise ValueError("Only modes {} are supported for 2D inputs".format(permitted_2_channel_modes))
225
-
226
- if mode is None and npim.dtype == np.uint8:
227
- mode = 'LA'
228
-
229
- elif npim.shape[2] == 4:
230
- permitted_4_channel_modes = ['RGBA', 'CMYK', 'RGBX']
231
- if mode is not None and mode not in permitted_4_channel_modes:
232
- raise ValueError("Only modes {} are supported for 4D inputs".format(permitted_4_channel_modes))
233
-
234
- if mode is None and npim.dtype == np.uint8:
235
- mode = 'RGBA'
236
- else:
237
- permitted_3_channel_modes = ['RGB', 'YCbCr', 'HSV']
238
- if mode is not None and mode not in permitted_3_channel_modes:
239
- raise ValueError("Only modes {} are supported for 3D inputs".format(permitted_3_channel_modes))
240
- if mode is None and npim.dtype == np.uint8:
241
- mode = 'RGB'
242
-
243
- if mode is None:
244
- raise TypeError('Input type {} is not supported'.format(npim.dtype))
245
-
246
- return PIL.Image.fromarray(npim, mode=mode)
247
-
248
- @staticmethod
249
- def is_cv2_image(im):
250
- return isinstance(im, np.ndarray) and im.ndim in {2, 3}
251
-
252
- @staticmethod
253
- def cvt_channel(im, flags=None):
254
- """ 确保图片目前是flags指示的通道情况
255
-
256
- :param flags:
257
- 0, 强制转为黑白图
258
- 1,强制转为BGR三通道图 (BGRA转BGR默认黑底填充?)
259
- 2,强制转为BGRA四通道图
260
- """
261
- if flags is None or flags == -1: return im
262
- n_c = xlcv.n_channels(im)
263
- tags = ['GRAY', 'BGR', 'BGRA']
264
- im_flag = {1: 0, 3: 1, 4: 2}[n_c]
265
- if im_flag != flags:
266
- im = cv2.cvtColor(im, getattr(cv2, f'COLOR_{tags[im_flag]}2{tags[flags]}'))
267
- return im
268
-
269
- @staticmethod
270
- def write(im, file, if_exists=None, ext=None):
271
- file = XlPath(file)
272
- if ext is None:
273
- ext = file.suffix
274
- data = cv2.imencode(ext=ext, img=im)[1]
275
- if file.exist_preprcs(if_exists):
276
- file.write_bytes(data.tobytes())
277
- return file
278
-
279
- @staticmethod
280
- def show(im):
281
- """ 类似Image.show,可以用计算机本地软件打开查看图片 """
282
- xlcv.to_pil_image(im).show()
283
-
284
- @staticmethod
285
- def to_buffer(im, ext='.jpg', *, b64encode=False) -> bytes:
286
- flag, buffer = cv2.imencode(ext, im)
287
- buffer = bytes(buffer)
288
- if b64encode:
289
- buffer = base64.b64encode(buffer)
290
- return buffer
291
-
292
- @staticmethod
293
- def imshow2(im, winname=None, flags=1):
294
- """ 展示窗口
295
-
296
- :param winname: 未输入时,则按test1、test2依次生成窗口
297
- :param flags:
298
- cv2.WINDOW_NORMAL,0,输入2等偶数值好像也等价于输入0,可以自动拉伸窗口大小
299
- cv2.WINDOW_AUTOSIZE,1,输入3等奇数值好像等价于1
300
- cv2.WINDOW_OPENGL,4096
301
- :return:
302
- """
303
- if winname is None:
304
- n = _show_win_num + 1
305
- winname = f'test{n}'
306
- cv2.namedWindow(winname, flags)
307
- cv2.imshow(winname, im)
308
-
309
- @staticmethod
310
- def display(im):
311
- """ 在jupyter中展示 """
312
- try:
313
- from IPython.display import display
314
- display(xlcv.to_pil_image(im))
315
- except ModuleNotFoundError:
316
- pass
317
-
318
- @staticmethod
319
- def __4_plot():
320
- pass
321
-
322
- @staticmethod
323
- def bg_color(im, edge_size=5, binary_img=None):
324
- """ 智能判断图片背景色
325
-
326
- 对全图二值化后,考虑最外一层宽度为edge_size的环中,0、1分布最多的作为背景色
327
- 然后取全部背景色的平均值返回
328
-
329
- :param im: 支持黑白图、彩图
330
- :param edge_size: 边缘宽度,宽度越高一般越准确,但也越耗性能
331
- :param binary_img: 运算中需要用二值图,如果外部已经计算了,可以直接传入进来,避免重复运算
332
- :return: color
333
-
334
- TODO 可以写个获得前景色,道理类似,只是最后在图片中心去取平均值
335
- """
336
- from itertools import chain
337
-
338
- # 1 获得二值图,区分前背景
339
- if binary_img is None:
340
- gray_img = xlcv.read(im, 0)
341
- _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
342
-
343
- # 2 分别存储点集
344
- n, m = im.shape[:2]
345
- colors0, colors1 = [], []
346
- for i in range(n):
347
- if i < edge_size or i >= n - edge_size:
348
- js = range(m)
349
- else:
350
- js = chain(range(edge_size), range(m - edge_size, m))
351
- for j in js:
352
- if binary_img[i, j]:
353
- colors1.append(im[i, j])
354
- else:
355
- colors0.append(im[i, j])
356
-
357
- # 3 计算平均像素
358
- # 以数量多的作为背景像素
359
- colors = colors0 if len(colors0) > len(colors1) else colors1
360
- return np.mean(np.array(colors), axis=0, dtype='int').tolist()
361
-
362
- @staticmethod
363
- def get_plot_color(im):
364
- """ 获得比较适合的作画颜色
365
-
366
- TODO 可以根据背景色智能推导画线用的颜色,目前是固定红色
367
- """
368
- if im.ndim == 3:
369
- return 0, 0, 255
370
- elif im.ndim == 2:
371
- return 255 # 灰度图,默认先填白色
372
-
373
- @staticmethod
374
- def get_plot_args(im, color=None):
375
- # 1 作图颜色
376
- if not color:
377
- color = xlcv.get_plot_color(im)
378
-
379
- # 2 画布
380
- if len(color) >= 3 and im.ndim <= 2:
381
- dst = cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)
382
- else:
383
- dst = np.array(im)
384
-
385
- return dst, color
386
-
387
- @staticmethod
388
- def lines(im, lines, color=None, thickness=1, line_type=cv2.LINE_AA, shift=None):
389
- """ 在src图像上画系列线段
390
- """
391
- # 1 判断 lines 参数内容
392
- lines = np.array(lines).reshape(-1, 4)
393
- if not lines.size:
394
- return im
395
-
396
- # 2 参数
397
- dst, color = xlcv.get_plot_args(im, color)
398
-
399
- # 3 画线
400
- if lines.any():
401
- for line in lines:
402
- x1, y1, x2, y2 = line
403
- cv2.line(dst, (x1, y1), (x2, y2), color, thickness, line_type)
404
- return dst
405
-
406
- @staticmethod
407
- def circles(im, circles, color=None, thickness=1, center=False):
408
- """ 在图片上画圆形
409
-
410
- :param im: 要作画的图
411
- :param circles: 要画的圆形参数 (x, y, 半径 r)
412
- :param color: 画笔颜色
413
- :param center: 是否画出圆心
414
- """
415
- # 1 圆 参数
416
- circles = np.array(circles, dtype=int).reshape(-1, 3)
417
- if not circles.size:
418
- return im
419
-
420
- # 2 参数
421
- dst, color = xlcv.get_plot_args(im, color)
422
-
423
- # 3 作画
424
- for x in circles:
425
- cv2.circle(dst, (x[0], x[1]), x[2], color, thickness)
426
- if center:
427
- cv2.circle(dst, (x[0], x[1]), 2, color, thickness)
428
-
429
- return dst
430
-
431
- __5_resize = """
432
- """
433
-
434
- @staticmethod
435
- def reduce_area(im, area):
436
- """ 根据面积上限缩小图片
437
-
438
- 即图片面积超过area时,按照等比例缩小到面积为area的图片
439
- """
440
- h, w = xlcv.imsize(im)
441
- s = h * w
442
- if s > area:
443
- r = (area / s) ** 0.5
444
- size = int(r * h), int(r * w)
445
- im = xlcv.resize2(im, size)
446
- return im
447
-
448
- @staticmethod
449
- def adjust_shape(im, min_length=None, max_length=None):
450
- """ 限制图片的最小边,最长边
451
-
452
- >>> a = np.zeros((100, 200,3), np.uint8)
453
- >>> xlcv.adjust_shape(a, 101).shape
454
- (101, 202, 3)
455
- >>> xlcv.adjust_shape(a, max_length=150).shape
456
- (75, 150, 3)
457
- """
458
- # 1 参数预计算
459
- h, w = im.shape[:2]
460
- x, y = min(h, w), max(h, w) # 短边记为x, 长边记为y
461
- a, b = min_length, max_length # 小阈值记为a, 大阈值记为b
462
- r = 1 # 需要进行的缩放比例
463
-
464
- # 2 判断缩放系数r
465
- if a and b: # 同时考虑放大缩小,不可能真的放大和缩小的,要计算逻辑,调整a、b值
466
- if y * a > x * b:
467
- raise ValueError(f'无法满足缩放要求 {x}x{y} limit {a} {b}')
468
-
469
- if a and x < a:
470
- r = a / x # 我在想有没可能有四舍五入误差,导致最终resize后获得的边长小了?
471
- elif b and y > b:
472
- r = b / y
473
-
474
- # 3 缩放图片
475
- if r != 1:
476
- im = cv2.resize(im, None, fx=r, fy=r)
477
-
478
- return im
479
-
480
- @staticmethod
481
- def resize2(im, dsize, **kwargs):
482
- """
483
- :param dsize: (h, w)
484
- :param kwargs:
485
- interpolation=cv2.INTER_CUBIC
486
- """
487
- # if 'interpolation' not in kwargs:
488
- # kwargs['interpolation'] = cv2.INTER_CUBIC
489
- return cv2.resize(im, tuple(dsize[::-1]), **kwargs)
490
-
491
- @staticmethod
492
- def reduce_filesize(im, filesize=None, suffix='.jpg'):
493
- """ 按照保存后的文件大小来压缩im
494
-
495
- :param filesize: 单位Bytes
496
- 可以用 300*1024 来表示 300KB
497
- 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
498
- :param suffix: 使用的图片类型
499
-
500
- >> reduce_filesize(im, 300*1024, 'jpg')
501
- """
502
-
503
- # 1 工具
504
- def get_file_size(im):
505
- success, buffer = cv2.imencode(suffix, im)
506
- return len(buffer)
507
-
508
- # 2 然后开始循环处理
509
- while filesize:
510
- r = get_file_size(im) / filesize
511
- if r <= 1:
512
- break
513
-
514
- # 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
515
- rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
516
- im = cv2.resize(im, (int(im.shape[1] * rate), int(im.shape[0] * rate)))
517
- return im
518
-
519
- @staticmethod
520
- def __5_warp():
521
- pass
522
-
523
- @staticmethod
524
- def warp(im, warp_mat, dsize=None, *, view_rate=False, max_zoom=1, reserve_struct=False):
525
- """ 对图像进行透视变换
526
-
527
- :param im: np.ndarray的图像数据
528
- TODO 支持PIL.Image格式?
529
- :param warp_mat: 变换矩阵
530
- :param dsize: 目标图片尺寸
531
- 没有任何输入时,同原图
532
- 如果有指定,则会决定最终的图片大小
533
- 如果使用了view_rate、max_zoom,会改变变换矩阵所展示的内容
534
- :param view_rate: 视野比例,默认不开启,当输入非0正数时,几个数值功能效果如下
535
- 1,关注原图四个角点位置在变换后的位置,确保新的4个点依然在目标图中
536
- 为了达到该效果,会增加【平移】变换,以及自动控制dsize
537
- 2,将原图依中心面积放到至2倍,记录新的4个角点变换后的位置,确保变换后的4个点依然在目标图中
538
- 0.5,同理,只是只关注原图局部的一半位置
539
- :param max_zoom: 默认1倍,当设置时(只在开启view_rate时有用),会增加【缩小】变换,限制view_rate扩展的上限
540
- :param reserve_struct: 是否保留原来im的数据类型返回,默认True
541
- 关掉该功能可以提高性能,此时返回结果统一为 np 矩阵
542
- :return: 见 reserve_struct
543
- """
544
- from math import sqrt
545
-
546
- # 1 得到3*3的变换矩阵
547
- warp_mat = np.array(warp_mat)
548
- if warp_mat.shape[0] == 2:
549
- if warp_mat.shape[1] == 2:
550
- warp_mat = np.concatenate([warp_mat, [[0], [0]]], axis=1)
551
- warp_mat = np.concatenate([warp_mat, [[0, 0, 1]]], axis=0)
552
-
553
- # 2 view_rate,视野比例改变导致的变换矩阵规则变化
554
- if view_rate:
555
- # 2.1 视野变化后的四个角点
556
- h, w = im.shape[:2]
557
- y, x = h / 2, w / 2 # 图片中心点坐标
558
- h1, w1 = view_rate * h / 2, view_rate * w / 2
559
- l, t, r, b = [-w1 + x, -h1 + y, w1 + x, h1 + y]
560
- pts1 = np.array([[l, t], [r, t], [r, b], [l, b]])
561
- # 2.2 变换后角点位置产生的外接矩形
562
- left, top, right, bottom = rect_bounds(warp_points(pts1, warp_mat))
563
- # 2.3 增加平移变换确保左上角在原点
564
- warp_mat = np.dot([[1, 0, -left], [0, 1, -top], [0, 0, 1]], warp_mat)
565
- # 2.4 控制面积变化率
566
- h2, w2 = (bottom - top, right - left)
567
- if max_zoom:
568
- rate = w2 * h2 / w / h # 目标面积比原面积
569
- if rate > max_zoom:
570
- r = 1 / sqrt(rate / max_zoom)
571
- warp_mat = np.dot([[r, 0, 0], [0, r, 0], [0, 0, 1]], warp_mat)
572
- h2, w2 = round(h2 * r), round(w2 * r)
573
- if not dsize:
574
- dsize = (w2, h2)
575
-
576
- # 3 标准操作,不做额外处理,按照原图默认的图片尺寸展示
577
- if dsize is None:
578
- dsize = (im.shape[1], im.shape[0])
579
- dst = cv2.warpPerspective(im, warp_mat, dsize)
580
-
581
- # 4 返回值
582
- return dst
583
-
584
- @staticmethod
585
- def pad(im, pad_size, constant_values=0, mode='constant', **kwargs):
586
- r""" 拓宽图片上下左右尺寸
587
-
588
- 基于np.pad,定制、简化了针对图片类型数据的操作
589
- https://numpy.org/doc/stable/reference/generated/numpy.pad.html
590
-
591
- :pad_size: 输入单个整数值,对四周pad相同尺寸,或者输入四个值的list,表示对上、下、左、右的扩展尺寸
592
-
593
- >>> a = np.ones([2, 2])
594
- >>> b = np.ones([2, 2, 3])
595
- >>> xlcv.pad(a, 1).shape # 上下左右各填充1行/列
596
- (4, 4)
597
- >>> xlcv.pad(b, 1).shape
598
- (4, 4, 3)
599
- >>> xlcv.pad(a, [1, 2, 3, 0]).shape # 上填充1,下填充2,左填充3,右不填充
600
- (5, 5)
601
- >>> xlcv.pad(b, [1, 2, 3, 0]).shape
602
- (5, 5, 3)
603
- >>> xlcv.pad(a, [1, 2, 3, 0])
604
- array([[0., 0., 0., 0., 0.],
605
- [0., 0., 0., 1., 1.],
606
- [0., 0., 0., 1., 1.],
607
- [0., 0., 0., 0., 0.],
608
- [0., 0., 0., 0., 0.]])
609
- >>> xlcv.pad(a, [1, 2, 3, 0], 255)
610
- array([[255., 255., 255., 255., 255.],
611
- [255., 255., 255., 1., 1.],
612
- [255., 255., 255., 1., 1.],
613
- [255., 255., 255., 255., 255.],
614
- [255., 255., 255., 255., 255.]])
615
- """
616
- # 0 参数检查
617
- if im.ndim < 2 or im.ndim > 3:
618
- raise ValueError
619
-
620
- if isinstance(pad_size, int):
621
- ltrb = [pad_size] * 4
622
- else:
623
- ltrb = pad_size
624
-
625
- # 1 pad_size转成np.pad的格式
626
- pad_width = [(ltrb[0], ltrb[1]), (ltrb[2], ltrb[3])]
627
- if im.ndim == 3:
628
- pad_width.append((0, 0))
629
-
630
- dst = np.pad(im, pad_width, mode, constant_values=constant_values, **kwargs)
631
-
632
- return dst
633
-
634
- @staticmethod
635
- def _get_subrect_image(im, pts, fill=0):
636
- """
637
- :return:
638
- dst_img 按外接四边形截取的子图
639
- new_pts 新的变换后的点坐标
640
- """
641
- # 1 计算需要pad的宽度
642
- x1, y1, x2, y2 = [round_int(v) for v in rect_bounds(pts)]
643
- h, w = im.shape[:2]
644
- pad = [-y1, y2 - h, -x1, x2 - w] # 各个维度要补充的宽度
645
- pad = [max(0, v) for v in pad] # 负数宽度不用补充,改为0
646
-
647
- # 2 pad并定位rect局部图
648
- tmp_img = xlcv.pad(im, pad, fill) if max(pad) > 0 else im
649
- dst_img = tmp_img[y1 + pad[0]:y2, x1 + pad[2]:x2] # 这里越界不会报错,只是越界的那个维度shape为0
650
- new_pts = [(pt[0] - x1, pt[1] - y1) for pt in pts]
651
- return dst_img, new_pts
652
-
653
- @staticmethod
654
- def get_sub(im, pts, *, fill=0, warp_quad=False):
655
- """ 从src_im取一个子图
656
-
657
- :param im: 原图
658
- 可以是图片路径、np.ndarray、PIL.Image对象
659
- TODO 目前只支持np.ndarray、pil图片输入,返回统一是np.ndarray
660
- :param pts: 子图位置信息
661
- 只有两个点,认为是矩形的两个对角点
662
- 只有四个点,认为是任意四边形
663
- 同理,其他点数量,默认为多边形的点集
664
- :param fill: 支持pts越界选取,此时可以设置fill自动填充的颜色值
665
- TODO fill填充一个rgb颜色的时候应该会不兼容报错,还要想办法优化
666
- :param warp_quad: 当pts为四个点时,是否进行仿射变换矫正
667
- 默认是截图pts的外接四边形区域
668
- 一般写 True、'average',也可以写'max'、'min',详见 quad_warp_wh()
669
- :return: 子图
670
- 文件、np.ndarray --> np.ndarray
671
- PIL.Image --> PIL.Image
672
- """
673
- dst, pts = xlcv._get_subrect_image(xlcv.read(im), reshape_coords(pts, 2), fill)
674
- if len(pts) == 4 and warp_quad:
675
- w, h = quad_warp_wh(pts, method=warp_quad)
676
- warp_mat = get_warp_mat(pts, rect2polygon([[0, 0], [w, h]]))
677
- dst = xlcv.warp(dst, warp_mat, (w, h))
678
- return dst
679
-
680
- @staticmethod
681
- def trim(im, *, border=0, color=None):
682
- """ 如果想用cv2实现,可以参考: https://stackoverflow.com/questions/49907382/how-to-remove-whitespace-from-an-image-in-opencv
683
- 目前为了偷懒节省代码量,就直接调用pil的版本了
684
- """
685
- from pyxllib.cv.xlpillib import xlpil
686
- im = xlcv.to_pil_image(im)
687
- im = xlpil.trim(im, border=border, color=color)
688
- return xlpil.to_cv2_image(im)
689
-
690
- @staticmethod
691
- def keep_subtitles(im, judge_func=None, trim_color=(255, 255, 255)):
692
- """ 保留(白色)字幕,去除背景,并会裁剪图片缩小尺寸
693
-
694
- 是比较业务级的一个功能,主要是这段代码挺有学习价值的,有其他变形需求,
695
- 可以修改judge_func,也可以另外写函数,这个仅供参考
696
- """
697
-
698
- def fore_pixel(rgb):
699
- """ 把图片变成白底黑字
700
-
701
- 判断像素是不是前景,是返回0,不是返回255
702
- """
703
- if sum(rgb > 170) == 3 and rgb.max() - rgb.min() < 30:
704
- return [0, 0, 0]
705
- else:
706
- return [255, 255, 255]
707
-
708
- if judge_func is None:
709
- judge_func = fore_pixel
710
-
711
- im2 = np.apply_along_axis(judge_func, 2, im).astype('uint8')
712
- if trim_color:
713
- im2 = xlcv.trim(im2, color=trim_color)
714
- return im2
715
-
716
- @classmethod
717
- def concat(cls, images, *, pad=5, pad_color=None):
718
- """ 拼接输入的多张图为一张图,请自行确保尺寸匹配
719
-
720
- :param images: 一维或二维list数组存储的np.array矩阵
721
- 一维的时候,默认按行拼接,变成 n*1 的图片。如果要显式按列拼接,请再套一层list,输入 [1, n] 的list
722
- :param pad: 图片之间的间隔,也可以输入 [5, 10] 表示左右间隔5像素,上下间隔10像素
723
- :param pad_color: 填充颜色,默认白色
724
-
725
- # TODO 下标编号
726
- """
727
- if pad_color is None:
728
- pad_color = 255
729
-
730
- if isinstance(pad, int):
731
- pad = [pad, pad]
732
-
733
- def hstack(imgs):
734
- """ 水平拼接图片 """
735
- patches = []
736
-
737
- max_length = max([im.shape[0] for im in imgs])
738
- max_channel = max([xlcv.n_channels(im) for im in imgs])
739
- if pad[1]:
740
- board = np.ones([max_length, pad[1]], dtype='uint8') * pad_color
741
- if max_channel == 3:
742
- board = xlcv.cvt_channel(board, 1)
743
- else:
744
- board = None
745
-
746
- for im in imgs:
747
- h = im.shape[0]
748
- if h != max_length:
749
- im = xlcv.pad(im, [0, max_length - h, 0, 0])
750
- if max_channel == 3:
751
- im = xlcv.cvt_channel(im, 1)
752
- patches += [im]
753
- if board is not None:
754
- patches += [board]
755
- if board is None:
756
- return np.hstack(patches)
757
- else:
758
- return np.hstack(patches[:-1])
759
-
760
- def vstack(imgs):
761
- patches = []
762
-
763
- max_length = max([im.shape[1] for im in imgs])
764
- max_channel = max([xlcv.n_channels(im) for im in imgs])
765
- if pad[0]:
766
- board = np.ones([pad[0], max_length], dtype='uint8') * pad_color
767
- if max_channel == 3:
768
- board = xlcv.cvt_channel(board, 1)
769
- else:
770
- board = None
771
-
772
- for im in imgs:
773
- w = im.shape[1]
774
- if w != max_length:
775
- im = xlcv.pad(im, [0, 0, 0, max_length - w])
776
- if max_channel == 3:
777
- im = xlcv.cvt_channel(im, 1)
778
- patches += [im]
779
- if board is not None:
780
- patches += [board]
781
-
782
- if board is None:
783
- return np.vstack(patches)
784
- else:
785
- return np.vstack(patches[:-1])
786
-
787
- if isinstance(images[0], list):
788
- images = [hstack(imgs) for imgs in images]
789
- return vstack(images)
790
-
791
- @classmethod
792
- def deskew_image(cls, image):
793
- """ 歪斜图像矫正 """
794
- # pip install deskew
795
- from deskew import determine_skew # 用来做图像倾斜矫正的,这个库不大,就自动安装了
796
-
797
- gray = xlcv.reduce_area(xlcv.read(image, 0), 1000*1000) # 转成灰度图,并且可以适当缩小计算图片
798
- angle = determine_skew(gray) # 计算图像的倾斜角度
799
- (h, w) = image.shape[:2] # 获取图像尺寸
800
- center = (w // 2, h // 2) # 计算图像中心(默认按中心旋转了,以后如果有需要按非中心点旋转的,可以再改)
801
- M = cv2.getRotationMatrix2D(center, angle, 1.0) # 计算旋转矩阵
802
- # 旋转图像
803
- deskewed_image = cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
804
- return deskewed_image # 返回纠偏后的图像
805
-
806
- def __6_替换颜色(self):
807
- pass
808
-
809
- @staticmethod
810
- def replace_color_by_mask(im, dst_color, mask):
811
- # TODO mask可以设置权重,从而产生类似渐变的效果?
812
- c = _rgb_to_bgr_list(dst_color)
813
- if len(c) == 1:
814
- im2 = xlcv.read(im, 0)
815
- im2[np.where((mask == [255]))] = c[0]
816
- elif len(c) == 3:
817
- if xlcv.n_channels(im) == 4:
818
- x, y = im[:, :, :3].copy(), im[:, :, 3:4]
819
- x[np.where((mask == [255]))] = c
820
- im2 = np.concatenate([x, y], axis=2)
821
- else:
822
- im2 = xlcv.read(im, 1)
823
- im2[np.where((mask == [255]))] = c
824
- elif len(c) == 4:
825
- im2 = xlcv.read(im, 2)
826
- im2[np.where((mask == [255]))] = c
827
- else:
828
- raise ValueError(f'dst_color参数值有问题 {dst_color}')
829
-
830
- return im2
831
-
832
- @staticmethod
833
- def replace_color(im, src_color, dst_color, *, tolerate=5):
834
- """ 替换图片中的颜色
835
-
836
- :param src_color: 原本颜色 (r, g, b)
837
- :param dst_color: 目标颜色 (r, g, b)
838
- 可以设为None,表示删除颜色,即设置透明底,此时返回格式默认为BGRA
839
- :param tolerate: 查找原本颜色的时候,容许的误差距离,在误差距离内的像素,仍然进行替换
840
- 这里的距离指单个数值允许的绝对距离
841
-
842
- 这个算法有几个比较麻烦的考虑点
843
- 1、原始格式不确定,对输入src_color是1个值、3个值、4个值等,处理逻辑不一样
844
- 2、cv2默认是bgr格式,但人的使用习惯,src_color习惯用rgb格式
845
-
846
- 为了工程化实现简洁,内部统一转成BGRA格式处理了
847
-
848
- 【使用示例】
849
- # 1 原始图是单通道灰度图
850
- im1 = xlcv.read('1.png', 0)
851
- im2 = xlcv.replace_color(im1, 50, 255) # 找到灰度为50的部分,变成白色
852
- im2 = xlcv.replace_color(im1, 50, [0, 0, 255]) # 变成蓝色(图片自动升为3通道彩图)
853
- # 变成白色,并且设置A=0,完全透明(透明色一般设为白色,但不一定要白色,只是完全透明时,设什么颜色其实都不太所谓)
854
- im2 = xlcv.replace_color(im1, 50, [255, 255, 255, 0])
855
-
856
- # 虽然原始图是单通道,但也可以用RGB、RGBA的机制检索位置,然后dst_color三种范式都支持
857
- im2 = xlcv.replace_color(im1, [50, 50, 50], 255)
858
- # src_color使用RGBA格式时,原图最后一个值是255,不透明
859
- im2 = xlcv.replace_color(im1, [50, 50, 50, 255], 255)
860
-
861
- # 2 原始图是RGB三通道彩色图
862
- im1 = xlcv.read('1.png', 1)
863
- im2 = xlcv.replace_color(im1, 40, 255, tolerate=20) # 原图是彩图,依然可以用40±20的灰度机制检索mask
864
- im2 = xlcv.replace_color(im1, [32, 60, 47], [0, 0, 255]) # 变成蓝色(最常见、正常用法)
865
-
866
- # 效果类似,全部枚举有3*3*3=27种组合,都是支持的
867
- # 其实src_color指定了匹配模式,dst_color指定了目标值,是相互独立的两种功能指定
868
-
869
- # 3 原始图是RGBA四通道图
870
- im1 = xlcv.read('1.png', 2)
871
- # 注意RGBA在目标值指定为RGB模式时,不会自动降级,这个比较特殊,不会改变原有的A通道情况
872
- im2 = xlcv.replace_color(im1, [32, 60, 47], [0, 0, 255])
873
- """
874
-
875
- def set_color(a):
876
- a = _rgb_to_bgr_list(a)
877
- # 确定有4个值
878
- if len(a) == 1:
879
- a = a * 3
880
- if len(a) == 3:
881
- a.append(255) # A通道默认255,不透明
882
- return np.array(a, dtype='uint8')
883
-
884
- def set_range_color(arr, tolerate):
885
- arr = arr.astype('int16')
886
- a = (arr - tolerate).clip(0, 255)
887
- b = (arr + tolerate).clip(0, 255)
888
- return a.astype('uint8'), b.astype('uint8')
889
-
890
- im1 = xlcv.read(im, 2)
891
- a, b = set_range_color(set_color(src_color), tolerate)
892
- mask = cv2.inRange(im1, a, b)
893
- return xlcv.replace_color_by_mask(im, dst_color, mask)
894
-
895
- @staticmethod
896
- def replace_background_color(im, dst_color):
897
- gray_img = xlcv.read(im, 0)
898
- _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
899
- return xlcv.replace_color_by_mask(im, dst_color, 255 - binary_img)
900
-
901
- @staticmethod
902
- def replace_foreground_color(im, dst_color):
903
- gray_img = xlcv.read(im, 0)
904
- _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
905
- return xlcv.replace_color_by_mask(im, dst_color, binary_img)
906
-
907
- @staticmethod
908
- def replace_ground_color(im, foreground_color, background_color):
909
- """ 替换前景、背景色
910
- 使用了二值图的方式来做mask
911
- """
912
- gray_img = xlcv.read(im, 0)
913
- _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
914
- if binary_img.mean() > 128: # 背景色应该比前景多的多,如果平均值大于128,说明黑底白字的模式反了
915
- binary_img = ~binary_img
916
- # 0作为背景,255作为前景
917
- im = xlcv.replace_color_by_mask(im, background_color, 255 - binary_img)
918
- im = xlcv.replace_color_by_mask(im, foreground_color, binary_img)
919
- return im
920
-
921
- def __7_other(self):
922
- pass
923
-
924
- @staticmethod
925
- def count_pixels(im):
926
- """ 统计image中各种rgb出现的次数 """
927
- colors, counts = np.unique(im.reshape(-1, 3), axis=0, return_counts=True)
928
- colors = [[tuple(c), cnt] for c, cnt in zip(colors, counts)]
929
- colors.sort(key=lambda x: -x[1])
930
- return colors
931
-
932
- @staticmethod
933
- def color_desc(im, color_num=10):
934
- """ 描述一张图的颜色分布,这个速度还特别慢,没有优化 """
935
- from collections import Counter
936
- from pyxllib.cv.rgbfmt import RgbFormatter
937
-
938
- colors = xlcv.count_pixels(im)
939
- total = sum([cnt for _, cnt in colors])
940
- colors2 = Counter()
941
- for c, cnt in colors[:10000]:
942
- c0 = RgbFormatter(*c).find_similar_std_color()
943
- colors2[c0.to_tuple()] += cnt
944
-
945
- for c, cnt in colors2.most_common(color_num):
946
- desc = RgbFormatter(*c).relative_color_desc()
947
- print(desc, f'{cnt / total:.2%}')
948
-
949
-
950
- class CvImg(np.ndarray):
951
- def __new__(cls, input_array, info=None):
952
- """ 从np.ndarray继承的固定写法
953
- https://numpy.org/doc/stable/user/basics.subclassing.html
954
-
955
- 该类使用中完全等价np.ndarray,但额外增加了xlcv中的功能
956
- """
957
- # Input array is an already formed ndarray instance
958
- # We first cast to be our class type
959
- obj = np.asarray(input_array).view(cls)
960
- # add the new attribute to the created instance
961
- obj.info = info
962
- # Finally, we must return the newly created object:
963
- return obj
964
-
965
- def __array_finalize__(self, obj):
966
- # see InfoArray.__array_finalize__ for comments
967
- if obj is None: return
968
- self.info = getattr(obj, 'info', None)
969
-
970
- @classmethod
971
- def read(cls, file, flags=None, **kwargs):
972
- return cls(xlcv.read(file, flags, **kwargs))
973
-
974
- @classmethod
975
- def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
976
- return cls(xlcv.read_from_buffer(buffer, flags, b64decode=b64decode))
977
-
978
- @classmethod
979
- def read_from_url(cls, url, flags=None, *, b64decode=False):
980
- return cls(xlcv.read_from_url(url, flags, b64decode=b64decode))
981
-
982
- @property
983
- def imsize(self):
984
- # 这里几个属性本来可以直接调用xlcv的实现,但为了性能,这里复写一遍
985
- return self.shape[:2]
986
-
987
- @property
988
- def n_channels(self):
989
- if self.ndim == 3:
990
- return self.shape[2]
991
- else:
992
- return 1
993
-
994
- @property
995
- def height(self):
996
- return self.shape[0]
997
-
998
- @property
999
- def width(self):
1000
- return self.shape[1]
1001
-
1002
- def __getattr__(self, item):
1003
- """ 对cv2、xlcv的类层级接口封装
1004
-
1005
- 这里使用了较高级的实现方法
1006
- 好处:从而每次开发只需要在xlcv写一遍
1007
- 坏处:没有代码接口提示...
1008
-
1009
- 注意,同名方法,比如size,会优先使用np.ndarray的版本
1010
- 所以为了区分度,xlcv有imsize来代表xlcv的size版本
1011
-
1012
- 并且无法直接使用 cv2.resize, 因为np.ndarray已经有了resize
1013
-
1014
- 这里没有做任何安全性检查,请开发使用者自行分析使用合理性
1015
- """
1016
-
1017
- def warp_func(*args, **kwargs):
1018
- func = getattr(cv2, item, getattr(xlcv, item, None)) # 先在cv2找方法,再在xlcv找方法
1019
- if func is None:
1020
- raise ValueError(f'不存在的方法名 {item}')
1021
- res = func(self, *args, **kwargs)
1022
- if isinstance(res, np.ndarray): # 返回是原始图片格式,打包后返回
1023
- return type(self)(res) # 自定义方法必须自己再转成CvImage格式,否则会变成np.ndarray
1024
- elif isinstance(res, tuple): # 如果是tuple类型,则里面的np.ndarray类型也要处理
1025
- res2 = []
1026
- for x in res:
1027
- if isinstance(x, np.ndarray):
1028
- res2.append(type(self)(x))
1029
- else:
1030
- res2.append(x)
1031
- return tuple(res2)
1032
- return res
1033
-
1034
- return warp_func
1035
-
1036
-
1037
- if __name__ == '__main__':
1038
- import fire
1039
-
1040
- fire.Fire()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2021/08/25 15:57
6
+
7
+ import base64
8
+
9
+ import PIL.Image
10
+ import cv2
11
+ import filetype
12
+ import humanfriendly
13
+ import numpy as np
14
+ import requests
15
+
16
+
17
+ from pyxllib.prog.newbie import round_int, RunOnlyOnce
18
+ from pyxllib.prog.pupil import EnchantBase, EnchantCvt
19
+ from pyxllib.algo.geo import rect_bounds, warp_points, reshape_coords, quad_warp_wh, get_warp_mat, rect2polygon
20
+ from pyxllib.file.specialist import XlPath
21
+
22
+ _show_win_num = 0
23
+
24
+
25
+ def _rgb_to_bgr_list(a):
26
+ # 类型转为list
27
+ if isinstance(a, np.ndarray):
28
+ a = a.tolist()
29
+ elif not isinstance(a, (list, tuple)):
30
+ a = [a]
31
+ if len(a) > 2:
32
+ a[:3] = [a[2], a[1], a[0]]
33
+ return a
34
+
35
+
36
+ class xlcv(EnchantBase):
37
+
38
+ @classmethod
39
+ @RunOnlyOnce
40
+ def enchant(cls):
41
+ """ 把xlcv的功能嵌入cv2中
42
+
43
+ 不太推荐使用该类,可以使用CvImg类更好地解决问题。
44
+ """
45
+ # 虽然只绑定cv2,但其他相关的几个库的方法上,最好也不要重名
46
+ names = cls.check_enchant_names([np.ndarray, PIL.Image, PIL.Image.Image])
47
+ names -= {'concat'}
48
+ cls._enchant(cv2, names, EnchantCvt.staticmethod2modulefunc)
49
+
50
+ @staticmethod
51
+ def __1_read():
52
+ pass
53
+
54
+ @staticmethod
55
+ def read(file, flags=None, **kwargs) -> np.ndarray:
56
+ """
57
+ :param file: 支持非文件路径参数,会做类型转换
58
+ 因为这个接口的灵活性,要判断file参数类型等,速度会慢一点点
59
+ 如果需要效率,可以显式使用imread、Image.open等明确操作类型
60
+ :param flags:
61
+ -1,按照图像原样读取,保留Alpha通道(第4通道)
62
+ 0,将图像转成单通道灰度图像后读取
63
+ 1,将图像转换成3通道BGR彩色图像
64
+
65
+ 220426周二14:20,注,有些图位深不是24而是48,读到的不是uint8而是uint16
66
+ 目前这个接口没做适配,需要下游再除以256后arr.astype('uint8')
67
+ """
68
+ from pyxllib.cv.xlpillib import xlpil
69
+
70
+ if xlcv.is_cv2_image(file):
71
+ im = file
72
+ elif XlPath.safe_init(file):
73
+ # https://www.yuque.com/xlpr/pyxllib/imread
74
+ # + np.frombuffer
75
+ im = cv2.imdecode(np.fromfile(str(file), dtype=np.uint8), -1 if flags is None else flags)
76
+ if im is None: # 在文件类型名写错时,可能会读取失败
77
+ raise ValueError(f'{file} {filetype.guess(file)}')
78
+ elif xlpil.is_pil_image(file):
79
+ im = xlpil.to_cv2_image(file)
80
+ else:
81
+ raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
82
+
83
+ if im.dtype == np.uint16:
84
+ # uint16类型,统一转为uint8。就我目前所会遇到的所有需求,都不需要用到uint16,反而会带来很多麻烦。
85
+ im = cv2.convertScaleAbs(im, alpha=255. / 65535.)
86
+
87
+ im = xlcv.cvt_channel(im, flags)
88
+
89
+ return im
90
+
91
+ @staticmethod
92
+ def read_from_buffer(buffer, flags=None, *, b64decode=False):
93
+ """ 从二进制流读取图片
94
+ 这个二进制流指,图片以png、jpg等某种格式存储为文件时,其对应的文件编码
95
+
96
+ :param bytes|str buffer:
97
+ :param b64decode: 是否需要先进行base64解码
98
+ """
99
+ if b64decode:
100
+ buffer = base64.b64decode(buffer)
101
+ buffer = np.frombuffer(buffer, dtype=np.uint8)
102
+ im = cv2.imdecode(buffer, -1 if flags is None else flags)
103
+ return xlcv.cvt_channel(im, flags)
104
+
105
+ @staticmethod
106
+ def read_from_url(url, flags=None, *, b64decode=False):
107
+ """ 从url直接获取图片到内存中
108
+ """
109
+ content = requests.get(url).content
110
+ return xlcv.read_from_buffer(content, flags, b64decode=b64decode)
111
+
112
+ @staticmethod
113
+ def read_from_strokes(strokes, margin=10, color=(0, 0, 0), bgcolor=(255, 255, 255), thickness=2):
114
+ """ 将联机手写笔划数据转成图片
115
+
116
+ :param strokes: n个笔划,每个笔画包含不一定要一样长的m个点,每个点是(x, y)的结构
117
+ :param margin: 图片边缘
118
+ :param color: 前景笔划颜色,默认黑色
119
+ :param bgcolor: 背景颜色,默认白色
120
+ :param thickness: 笔划粗度
121
+ """
122
+ # 1 边界
123
+ minx = min([p[0] for s in strokes for p in s])
124
+ miny = min([p[1] for s in strokes for p in s])
125
+ for stroke in strokes:
126
+ for p in stroke:
127
+ p[0] -= minx - margin
128
+ p[1] -= miny - margin
129
+
130
+ maxx = max([p[0] for s in strokes for p in s])
131
+ maxy = max([p[1] for s in strokes for p in s])
132
+
133
+ # 2 画出图片
134
+ canvas = np.zeros((maxy + margin*2, maxx + margin*2, 3), dtype=np.uint8)
135
+ canvas[:, :] = bgcolor
136
+
137
+ # 画出每个笔划轨迹
138
+ for stroke in strokes:
139
+ for i in range(len(stroke) - 1):
140
+ cv2.line(canvas, tuple(stroke[i]), tuple(stroke[i + 1]), color, thickness=thickness)
141
+
142
+ return canvas
143
+
144
+ @staticmethod
145
+ def __2_attrs():
146
+ pass
147
+
148
+ @staticmethod
149
+ def imsize(im):
150
+ """ 图片尺寸,统一返回(height, width),不含通道 """
151
+ return im.shape[:2]
152
+
153
+ @staticmethod
154
+ def n_channels(im):
155
+ """ 通道数 """
156
+ if im.ndim == 3:
157
+ return im.shape[2]
158
+ else:
159
+ return 1
160
+
161
+ @staticmethod
162
+ def height(im):
163
+ """ 注意PIL.Image.Image本来就有height、width属性,所以不用自定义这两个方法 """
164
+ return im.shape[0]
165
+
166
+ @staticmethod
167
+ def width(im):
168
+ return im.shape[1]
169
+
170
+ @staticmethod
171
+ def __3_write__(self):
172
+ pass
173
+
174
+ @staticmethod
175
+ def to_pil_image(pic, mode=None):
176
+ """ 我也不懂torch里这个实现为啥这么复杂,先直接哪来用,没深究细节
177
+
178
+ Convert a tensor or an ndarray to PIL Image. (删除了tensor的转换功能)
179
+
180
+ See :class:`~torchvision.transforms.ToPILImage` for more details.
181
+
182
+ Args:
183
+ pic (Tensor or numpy.ndarray): Image to be converted to PIL Image.
184
+ mode (`PIL.Image mode`_): color space and pixel depth of input data (optional).
185
+
186
+ .. _PIL.Image mode: https://pillow.readthedocs.io/en/latest/handbook/concepts.html#concept-modes
187
+
188
+ Returns:
189
+ PIL Image: Image converted to PIL Image.
190
+ """
191
+ # BGR 转为 RGB
192
+ if pic.ndim > 2 and pic.shape[2] >= 3:
193
+ pic = pic.copy() # 221127周日20:47,发现一个天坑bug,如果没有这句copy修正,会修改原始的pic图片数据
194
+ pic[:, :, [0, 1, 2]] = pic[:, :, [2, 1, 0]]
195
+
196
+ # 以下是原版实现代码
197
+ if pic.ndim not in {2, 3}:
198
+ raise ValueError('pic should be 2/3 dimensional. Got {} dimensions.'.format(pic.ndim))
199
+ if pic.ndim == 2:
200
+ # if 2D image, add channel dimension (HWC)
201
+ pic = np.expand_dims(pic, 2)
202
+
203
+ npim = pic
204
+
205
+ if npim.shape[2] == 1:
206
+ expected_mode = None
207
+ npim = npim[:, :, 0]
208
+ if npim.dtype == np.uint8:
209
+ expected_mode = 'L'
210
+ elif npim.dtype == np.int16:
211
+ expected_mode = 'I;16'
212
+ elif npim.dtype == np.int32:
213
+ expected_mode = 'I'
214
+ elif npim.dtype == np.float32:
215
+ expected_mode = 'F'
216
+ if mode is not None and mode != expected_mode:
217
+ raise ValueError("Incorrect mode ({}) supplied for input type {}. Should be {}"
218
+ .format(mode, np.dtype, expected_mode))
219
+ mode = expected_mode
220
+
221
+ elif npim.shape[2] == 2:
222
+ permitted_2_channel_modes = ['LA']
223
+ if mode is not None and mode not in permitted_2_channel_modes:
224
+ raise ValueError("Only modes {} are supported for 2D inputs".format(permitted_2_channel_modes))
225
+
226
+ if mode is None and npim.dtype == np.uint8:
227
+ mode = 'LA'
228
+
229
+ elif npim.shape[2] == 4:
230
+ permitted_4_channel_modes = ['RGBA', 'CMYK', 'RGBX']
231
+ if mode is not None and mode not in permitted_4_channel_modes:
232
+ raise ValueError("Only modes {} are supported for 4D inputs".format(permitted_4_channel_modes))
233
+
234
+ if mode is None and npim.dtype == np.uint8:
235
+ mode = 'RGBA'
236
+ else:
237
+ permitted_3_channel_modes = ['RGB', 'YCbCr', 'HSV']
238
+ if mode is not None and mode not in permitted_3_channel_modes:
239
+ raise ValueError("Only modes {} are supported for 3D inputs".format(permitted_3_channel_modes))
240
+ if mode is None and npim.dtype == np.uint8:
241
+ mode = 'RGB'
242
+
243
+ if mode is None:
244
+ raise TypeError('Input type {} is not supported'.format(npim.dtype))
245
+
246
+ return PIL.Image.fromarray(npim, mode=mode)
247
+
248
+ @staticmethod
249
+ def is_cv2_image(im):
250
+ return isinstance(im, np.ndarray) and im.ndim in {2, 3}
251
+
252
+ @staticmethod
253
+ def cvt_channel(im, flags=None):
254
+ """ 确保图片目前是flags指示的通道情况
255
+
256
+ :param flags:
257
+ 0, 强制转为黑白图
258
+ 1,强制转为BGR三通道图 (BGRA转BGR默认黑底填充?)
259
+ 2,强制转为BGRA四通道图
260
+ """
261
+ if flags is None or flags == -1: return im
262
+ n_c = xlcv.n_channels(im)
263
+ tags = ['GRAY', 'BGR', 'BGRA']
264
+ im_flag = {1: 0, 3: 1, 4: 2}[n_c]
265
+ if im_flag != flags:
266
+ im = cv2.cvtColor(im, getattr(cv2, f'COLOR_{tags[im_flag]}2{tags[flags]}'))
267
+ return im
268
+
269
+ @staticmethod
270
+ def write(im, file, if_exists=None, ext=None):
271
+ file = XlPath(file)
272
+ if ext is None:
273
+ ext = file.suffix
274
+ data = cv2.imencode(ext=ext, img=im)[1]
275
+ if file.exist_preprcs(if_exists):
276
+ file.write_bytes(data.tobytes())
277
+ return file
278
+
279
+ @staticmethod
280
+ def show(im):
281
+ """ 类似Image.show,可以用计算机本地软件打开查看图片 """
282
+ xlcv.to_pil_image(im).show()
283
+
284
+ @staticmethod
285
+ def to_buffer(im, ext='.jpg', *, b64encode=False) -> bytes:
286
+ flag, buffer = cv2.imencode(ext, im)
287
+ buffer = bytes(buffer)
288
+ if b64encode:
289
+ buffer = base64.b64encode(buffer)
290
+ return buffer
291
+
292
+ @staticmethod
293
+ def imshow2(im, winname=None, flags=1):
294
+ """ 展示窗口
295
+
296
+ :param winname: 未输入时,则按test1、test2依次生成窗口
297
+ :param flags:
298
+ cv2.WINDOW_NORMAL,0,输入2等偶数值好像也等价于输入0,可以自动拉伸窗口大小
299
+ cv2.WINDOW_AUTOSIZE,1,输入3等奇数值好像等价于1
300
+ cv2.WINDOW_OPENGL,4096
301
+ :return:
302
+ """
303
+ if winname is None:
304
+ n = _show_win_num + 1
305
+ winname = f'test{n}'
306
+ cv2.namedWindow(winname, flags)
307
+ cv2.imshow(winname, im)
308
+
309
+ @staticmethod
310
+ def display(im):
311
+ """ 在jupyter中展示 """
312
+ try:
313
+ from IPython.display import display
314
+ display(xlcv.to_pil_image(im))
315
+ except ModuleNotFoundError:
316
+ pass
317
+
318
+ @staticmethod
319
+ def __4_plot():
320
+ pass
321
+
322
+ @staticmethod
323
+ def bg_color(im, edge_size=5, binary_img=None):
324
+ """ 智能判断图片背景色
325
+
326
+ 对全图二值化后,考虑最外一层宽度为edge_size的环中,0、1分布最多的作为背景色
327
+ 然后取全部背景色的平均值返回
328
+
329
+ :param im: 支持黑白图、彩图
330
+ :param edge_size: 边缘宽度,宽度越高一般越准确,但也越耗性能
331
+ :param binary_img: 运算中需要用二值图,如果外部已经计算了,可以直接传入进来,避免重复运算
332
+ :return: color
333
+
334
+ TODO 可以写个获得前景色,道理类似,只是最后在图片中心去取平均值
335
+ """
336
+ from itertools import chain
337
+
338
+ # 1 获得二值图,区分前背景
339
+ if binary_img is None:
340
+ gray_img = xlcv.read(im, 0)
341
+ _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
342
+
343
+ # 2 分别存储点集
344
+ n, m = im.shape[:2]
345
+ colors0, colors1 = [], []
346
+ for i in range(n):
347
+ if i < edge_size or i >= n - edge_size:
348
+ js = range(m)
349
+ else:
350
+ js = chain(range(edge_size), range(m - edge_size, m))
351
+ for j in js:
352
+ if binary_img[i, j]:
353
+ colors1.append(im[i, j])
354
+ else:
355
+ colors0.append(im[i, j])
356
+
357
+ # 3 计算平均像素
358
+ # 以数量多的作为背景像素
359
+ colors = colors0 if len(colors0) > len(colors1) else colors1
360
+ return np.mean(np.array(colors), axis=0, dtype='int').tolist()
361
+
362
+ @staticmethod
363
+ def get_plot_color(im):
364
+ """ 获得比较适合的作画颜色
365
+
366
+ TODO 可以根据背景色智能推导画线用的颜色,目前是固定红色
367
+ """
368
+ if im.ndim == 3:
369
+ return 0, 0, 255
370
+ elif im.ndim == 2:
371
+ return 255 # 灰度图,默认先填白色
372
+
373
+ @staticmethod
374
+ def get_plot_args(im, color=None):
375
+ # 1 作图颜色
376
+ if not color:
377
+ color = xlcv.get_plot_color(im)
378
+
379
+ # 2 画布
380
+ if len(color) >= 3 and im.ndim <= 2:
381
+ dst = cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)
382
+ else:
383
+ dst = np.array(im)
384
+
385
+ return dst, color
386
+
387
+ @staticmethod
388
+ def lines(im, lines, color=None, thickness=1, line_type=cv2.LINE_AA, shift=None):
389
+ """ 在src图像上画系列线段
390
+ """
391
+ # 1 判断 lines 参数内容
392
+ lines = np.array(lines).reshape(-1, 4)
393
+ if not lines.size:
394
+ return im
395
+
396
+ # 2 参数
397
+ dst, color = xlcv.get_plot_args(im, color)
398
+
399
+ # 3 画线
400
+ if lines.any():
401
+ for line in lines:
402
+ x1, y1, x2, y2 = line
403
+ cv2.line(dst, (x1, y1), (x2, y2), color, thickness, line_type)
404
+ return dst
405
+
406
+ @staticmethod
407
+ def circles(im, circles, color=None, thickness=1, center=False):
408
+ """ 在图片上画圆形
409
+
410
+ :param im: 要作画的图
411
+ :param circles: 要画的圆形参数 (x, y, 半径 r)
412
+ :param color: 画笔颜色
413
+ :param center: 是否画出圆心
414
+ """
415
+ # 1 圆 参数
416
+ circles = np.array(circles, dtype=int).reshape(-1, 3)
417
+ if not circles.size:
418
+ return im
419
+
420
+ # 2 参数
421
+ dst, color = xlcv.get_plot_args(im, color)
422
+
423
+ # 3 作画
424
+ for x in circles:
425
+ cv2.circle(dst, (x[0], x[1]), x[2], color, thickness)
426
+ if center:
427
+ cv2.circle(dst, (x[0], x[1]), 2, color, thickness)
428
+
429
+ return dst
430
+
431
+ __5_resize = """
432
+ """
433
+
434
+ @staticmethod
435
+ def reduce_area(im, area):
436
+ """ 根据面积上限缩小图片
437
+
438
+ 即图片面积超过area时,按照等比例缩小到面积为area的图片
439
+ """
440
+ h, w = xlcv.imsize(im)
441
+ s = h * w
442
+ if s > area:
443
+ r = (area / s) ** 0.5
444
+ size = int(r * h), int(r * w)
445
+ im = xlcv.resize2(im, size)
446
+ return im
447
+
448
+ @staticmethod
449
+ def adjust_shape(im, min_length=None, max_length=None):
450
+ """ 限制图片的最小边,最长边
451
+
452
+ >>> a = np.zeros((100, 200,3), np.uint8)
453
+ >>> xlcv.adjust_shape(a, 101).shape
454
+ (101, 202, 3)
455
+ >>> xlcv.adjust_shape(a, max_length=150).shape
456
+ (75, 150, 3)
457
+ """
458
+ # 1 参数预计算
459
+ h, w = im.shape[:2]
460
+ x, y = min(h, w), max(h, w) # 短边记为x, 长边记为y
461
+ a, b = min_length, max_length # 小阈值记为a, 大阈值记为b
462
+ r = 1 # 需要进行的缩放比例
463
+
464
+ # 2 判断缩放系数r
465
+ if a and b: # 同时考虑放大缩小,不可能真的放大和缩小的,要计算逻辑,调整a、b值
466
+ if y * a > x * b:
467
+ raise ValueError(f'无法满足缩放要求 {x}x{y} limit {a} {b}')
468
+
469
+ if a and x < a:
470
+ r = a / x # 我在想有没可能有四舍五入误差,导致最终resize后获得的边长小了?
471
+ elif b and y > b:
472
+ r = b / y
473
+
474
+ # 3 缩放图片
475
+ if r != 1:
476
+ im = cv2.resize(im, None, fx=r, fy=r)
477
+
478
+ return im
479
+
480
+ @staticmethod
481
+ def resize2(im, dsize, **kwargs):
482
+ """
483
+ :param dsize: (h, w)
484
+ :param kwargs:
485
+ interpolation=cv2.INTER_CUBIC
486
+ """
487
+ # if 'interpolation' not in kwargs:
488
+ # kwargs['interpolation'] = cv2.INTER_CUBIC
489
+ return cv2.resize(im, tuple(dsize[::-1]), **kwargs)
490
+
491
+ @staticmethod
492
+ def reduce_filesize(im, filesize=None, suffix='.jpg'):
493
+ """ 按照保存后的文件大小来压缩im
494
+
495
+ :param filesize: 单位Bytes
496
+ 可以用 300*1024 来表示 300KB
497
+ 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
498
+ :param suffix: 使用的图片类型
499
+
500
+ >> reduce_filesize(im, 300*1024, 'jpg')
501
+ """
502
+
503
+ # 1 工具
504
+ def get_file_size(im):
505
+ success, buffer = cv2.imencode(suffix, im)
506
+ return len(buffer)
507
+
508
+ # 2 然后开始循环处理
509
+ while filesize:
510
+ r = get_file_size(im) / filesize
511
+ if r <= 1:
512
+ break
513
+
514
+ # 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
515
+ rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
516
+ im = cv2.resize(im, (int(im.shape[1] * rate), int(im.shape[0] * rate)))
517
+ return im
518
+
519
+ @staticmethod
520
+ def __5_warp():
521
+ pass
522
+
523
+ @staticmethod
524
+ def warp(im, warp_mat, dsize=None, *, view_rate=False, max_zoom=1, reserve_struct=False):
525
+ """ 对图像进行透视变换
526
+
527
+ :param im: np.ndarray的图像数据
528
+ TODO 支持PIL.Image格式?
529
+ :param warp_mat: 变换矩阵
530
+ :param dsize: 目标图片尺寸
531
+ 没有任何输入时,同原图
532
+ 如果有指定,则会决定最终的图片大小
533
+ 如果使用了view_rate、max_zoom,会改变变换矩阵所展示的内容
534
+ :param view_rate: 视野比例,默认不开启,当输入非0正数时,几个数值功能效果如下
535
+ 1,关注原图四个角点位置在变换后的位置,确保新的4个点依然在目标图中
536
+ 为了达到该效果,会增加【平移】变换,以及自动控制dsize
537
+ 2,将原图依中心面积放到至2倍,记录新的4个角点变换后的位置,确保变换后的4个点依然在目标图中
538
+ 0.5,同理,只是只关注原图局部的一半位置
539
+ :param max_zoom: 默认1倍,当设置时(只在开启view_rate时有用),会增加【缩小】变换,限制view_rate扩展的上限
540
+ :param reserve_struct: 是否保留原来im的数据类型返回,默认True
541
+ 关掉该功能可以提高性能,此时返回结果统一为 np 矩阵
542
+ :return: 见 reserve_struct
543
+ """
544
+ from math import sqrt
545
+
546
+ # 1 得到3*3的变换矩阵
547
+ warp_mat = np.array(warp_mat)
548
+ if warp_mat.shape[0] == 2:
549
+ if warp_mat.shape[1] == 2:
550
+ warp_mat = np.concatenate([warp_mat, [[0], [0]]], axis=1)
551
+ warp_mat = np.concatenate([warp_mat, [[0, 0, 1]]], axis=0)
552
+
553
+ # 2 view_rate,视野比例改变导致的变换矩阵规则变化
554
+ if view_rate:
555
+ # 2.1 视野变化后的四个角点
556
+ h, w = im.shape[:2]
557
+ y, x = h / 2, w / 2 # 图片中心点坐标
558
+ h1, w1 = view_rate * h / 2, view_rate * w / 2
559
+ l, t, r, b = [-w1 + x, -h1 + y, w1 + x, h1 + y]
560
+ pts1 = np.array([[l, t], [r, t], [r, b], [l, b]])
561
+ # 2.2 变换后角点位置产生的外接矩形
562
+ left, top, right, bottom = rect_bounds(warp_points(pts1, warp_mat))
563
+ # 2.3 增加平移变换确保左上角在原点
564
+ warp_mat = np.dot([[1, 0, -left], [0, 1, -top], [0, 0, 1]], warp_mat)
565
+ # 2.4 控制面积变化率
566
+ h2, w2 = (bottom - top, right - left)
567
+ if max_zoom:
568
+ rate = w2 * h2 / w / h # 目标面积比原面积
569
+ if rate > max_zoom:
570
+ r = 1 / sqrt(rate / max_zoom)
571
+ warp_mat = np.dot([[r, 0, 0], [0, r, 0], [0, 0, 1]], warp_mat)
572
+ h2, w2 = round(h2 * r), round(w2 * r)
573
+ if not dsize:
574
+ dsize = (w2, h2)
575
+
576
+ # 3 标准操作,不做额外处理,按照原图默认的图片尺寸展示
577
+ if dsize is None:
578
+ dsize = (im.shape[1], im.shape[0])
579
+ dst = cv2.warpPerspective(im, warp_mat, dsize)
580
+
581
+ # 4 返回值
582
+ return dst
583
+
584
+ @staticmethod
585
+ def pad(im, pad_size, constant_values=0, mode='constant', **kwargs):
586
+ r""" 拓宽图片上下左右尺寸
587
+
588
+ 基于np.pad,定制、简化了针对图片类型数据的操作
589
+ https://numpy.org/doc/stable/reference/generated/numpy.pad.html
590
+
591
+ :pad_size: 输入单个整数值,对四周pad相同尺寸,或者输入四个值的list,表示对上、下、左、右的扩展尺寸
592
+
593
+ >>> a = np.ones([2, 2])
594
+ >>> b = np.ones([2, 2, 3])
595
+ >>> xlcv.pad(a, 1).shape # 上下左右各填充1行/列
596
+ (4, 4)
597
+ >>> xlcv.pad(b, 1).shape
598
+ (4, 4, 3)
599
+ >>> xlcv.pad(a, [1, 2, 3, 0]).shape # 上填充1,下填充2,左填充3,右不填充
600
+ (5, 5)
601
+ >>> xlcv.pad(b, [1, 2, 3, 0]).shape
602
+ (5, 5, 3)
603
+ >>> xlcv.pad(a, [1, 2, 3, 0])
604
+ array([[0., 0., 0., 0., 0.],
605
+ [0., 0., 0., 1., 1.],
606
+ [0., 0., 0., 1., 1.],
607
+ [0., 0., 0., 0., 0.],
608
+ [0., 0., 0., 0., 0.]])
609
+ >>> xlcv.pad(a, [1, 2, 3, 0], 255)
610
+ array([[255., 255., 255., 255., 255.],
611
+ [255., 255., 255., 1., 1.],
612
+ [255., 255., 255., 1., 1.],
613
+ [255., 255., 255., 255., 255.],
614
+ [255., 255., 255., 255., 255.]])
615
+ """
616
+ # 0 参数检查
617
+ if im.ndim < 2 or im.ndim > 3:
618
+ raise ValueError
619
+
620
+ if isinstance(pad_size, int):
621
+ ltrb = [pad_size] * 4
622
+ else:
623
+ ltrb = pad_size
624
+
625
+ # 1 pad_size转成np.pad的格式
626
+ pad_width = [(ltrb[0], ltrb[1]), (ltrb[2], ltrb[3])]
627
+ if im.ndim == 3:
628
+ pad_width.append((0, 0))
629
+
630
+ dst = np.pad(im, pad_width, mode, constant_values=constant_values, **kwargs)
631
+
632
+ return dst
633
+
634
+ @staticmethod
635
+ def _get_subrect_image(im, pts, fill=0):
636
+ """
637
+ :return:
638
+ dst_img 按外接四边形截取的子图
639
+ new_pts 新的变换后的点坐标
640
+ """
641
+ # 1 计算需要pad的宽度
642
+ x1, y1, x2, y2 = [round_int(v) for v in rect_bounds(pts)]
643
+ h, w = im.shape[:2]
644
+ pad = [-y1, y2 - h, -x1, x2 - w] # 各个维度要补充的宽度
645
+ pad = [max(0, v) for v in pad] # 负数宽度不用补充,改为0
646
+
647
+ # 2 pad并定位rect局部图
648
+ tmp_img = xlcv.pad(im, pad, fill) if max(pad) > 0 else im
649
+ dst_img = tmp_img[y1 + pad[0]:y2, x1 + pad[2]:x2] # 这里越界不会报错,只是越界的那个维度shape为0
650
+ new_pts = [(pt[0] - x1, pt[1] - y1) for pt in pts]
651
+ return dst_img, new_pts
652
+
653
+ @staticmethod
654
+ def get_sub(im, pts, *, fill=0, warp_quad=False):
655
+ """ 从src_im取一个子图
656
+
657
+ :param im: 原图
658
+ 可以是图片路径、np.ndarray、PIL.Image对象
659
+ TODO 目前只支持np.ndarray、pil图片输入,返回统一是np.ndarray
660
+ :param pts: 子图位置信息
661
+ 只有两个点,认为是矩形的两个对角点
662
+ 只有四个点,认为是任意四边形
663
+ 同理,其他点数量,默认为多边形的点集
664
+ :param fill: 支持pts越界选取,此时可以设置fill自动填充的颜色值
665
+ TODO fill填充一个rgb颜色的时候应该会不兼容报错,还要想办法优化
666
+ :param warp_quad: 当pts为四个点时,是否进行仿射变换矫正
667
+ 默认是截图pts的外接四边形区域
668
+ 一般写 True、'average',也可以写'max'、'min',详见 quad_warp_wh()
669
+ :return: 子图
670
+ 文件、np.ndarray --> np.ndarray
671
+ PIL.Image --> PIL.Image
672
+ """
673
+ dst, pts = xlcv._get_subrect_image(xlcv.read(im), reshape_coords(pts, 2), fill)
674
+ if len(pts) == 4 and warp_quad:
675
+ w, h = quad_warp_wh(pts, method=warp_quad)
676
+ warp_mat = get_warp_mat(pts, rect2polygon([[0, 0], [w, h]]))
677
+ dst = xlcv.warp(dst, warp_mat, (w, h))
678
+ return dst
679
+
680
+ @staticmethod
681
+ def trim(im, *, border=0, color=None):
682
+ """ 如果想用cv2实现,可以参考: https://stackoverflow.com/questions/49907382/how-to-remove-whitespace-from-an-image-in-opencv
683
+ 目前为了偷懒节省代码量,就直接调用pil的版本了
684
+ """
685
+ from pyxllib.cv.xlpillib import xlpil
686
+ im = xlcv.to_pil_image(im)
687
+ im = xlpil.trim(im, border=border, color=color)
688
+ return xlpil.to_cv2_image(im)
689
+
690
+ @staticmethod
691
+ def keep_subtitles(im, judge_func=None, trim_color=(255, 255, 255)):
692
+ """ 保留(白色)字幕,去除背景,并会裁剪图片缩小尺寸
693
+
694
+ 是比较业务级的一个功能,主要是这段代码挺有学习价值的,有其他变形需求,
695
+ 可以修改judge_func,也可以另外写函数,这个仅供参考
696
+ """
697
+
698
+ def fore_pixel(rgb):
699
+ """ 把图片变成白底黑字
700
+
701
+ 判断像素是不是前景,是返回0,不是返回255
702
+ """
703
+ if sum(rgb > 170) == 3 and rgb.max() - rgb.min() < 30:
704
+ return [0, 0, 0]
705
+ else:
706
+ return [255, 255, 255]
707
+
708
+ if judge_func is None:
709
+ judge_func = fore_pixel
710
+
711
+ im2 = np.apply_along_axis(judge_func, 2, im).astype('uint8')
712
+ if trim_color:
713
+ im2 = xlcv.trim(im2, color=trim_color)
714
+ return im2
715
+
716
+ @classmethod
717
+ def concat(cls, images, *, pad=5, pad_color=None):
718
+ """ 拼接输入的多张图为一张图,请自行确保尺寸匹配
719
+
720
+ :param images: 一维或二维list数组存储的np.array矩阵
721
+ 一维的时候,默认按行拼接,变成 n*1 的图片。如果要显式按列拼接,请再套一层list,输入 [1, n] 的list
722
+ :param pad: 图片之间的间隔,也可以输入 [5, 10] 表示左右间隔5像素,上下间隔10像素
723
+ :param pad_color: 填充颜色,默认白色
724
+
725
+ # TODO 下标编号
726
+ """
727
+ if pad_color is None:
728
+ pad_color = 255
729
+
730
+ if isinstance(pad, int):
731
+ pad = [pad, pad]
732
+
733
+ def hstack(imgs):
734
+ """ 水平拼接图片 """
735
+ patches = []
736
+
737
+ max_length = max([im.shape[0] for im in imgs])
738
+ max_channel = max([xlcv.n_channels(im) for im in imgs])
739
+ if pad[1]:
740
+ board = np.ones([max_length, pad[1]], dtype='uint8') * pad_color
741
+ if max_channel == 3:
742
+ board = xlcv.cvt_channel(board, 1)
743
+ else:
744
+ board = None
745
+
746
+ for im in imgs:
747
+ h = im.shape[0]
748
+ if h != max_length:
749
+ im = xlcv.pad(im, [0, max_length - h, 0, 0])
750
+ if max_channel == 3:
751
+ im = xlcv.cvt_channel(im, 1)
752
+ patches += [im]
753
+ if board is not None:
754
+ patches += [board]
755
+ if board is None:
756
+ return np.hstack(patches)
757
+ else:
758
+ return np.hstack(patches[:-1])
759
+
760
+ def vstack(imgs):
761
+ patches = []
762
+
763
+ max_length = max([im.shape[1] for im in imgs])
764
+ max_channel = max([xlcv.n_channels(im) for im in imgs])
765
+ if pad[0]:
766
+ board = np.ones([pad[0], max_length], dtype='uint8') * pad_color
767
+ if max_channel == 3:
768
+ board = xlcv.cvt_channel(board, 1)
769
+ else:
770
+ board = None
771
+
772
+ for im in imgs:
773
+ w = im.shape[1]
774
+ if w != max_length:
775
+ im = xlcv.pad(im, [0, 0, 0, max_length - w])
776
+ if max_channel == 3:
777
+ im = xlcv.cvt_channel(im, 1)
778
+ patches += [im]
779
+ if board is not None:
780
+ patches += [board]
781
+
782
+ if board is None:
783
+ return np.vstack(patches)
784
+ else:
785
+ return np.vstack(patches[:-1])
786
+
787
+ if isinstance(images[0], list):
788
+ images = [hstack(imgs) for imgs in images]
789
+ return vstack(images)
790
+
791
+ @classmethod
792
+ def deskew_image(cls, image):
793
+ """ 歪斜图像矫正 """
794
+ # pip install deskew
795
+ from deskew import determine_skew # 用来做图像倾斜矫正的,这个库不大,就自动安装了
796
+
797
+ gray = xlcv.reduce_area(xlcv.read(image, 0), 1000*1000) # 转成灰度图,并且可以适当缩小计算图片
798
+ angle = determine_skew(gray) # 计算图像的倾斜角度
799
+ (h, w) = image.shape[:2] # 获取图像尺寸
800
+ center = (w // 2, h // 2) # 计算图像中心(默认按中心旋转了,以后如果有需要按非中心点旋转的,可以再改)
801
+ M = cv2.getRotationMatrix2D(center, angle, 1.0) # 计算旋转矩阵
802
+ # 旋转图像
803
+ deskewed_image = cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
804
+ return deskewed_image # 返回纠偏后的图像
805
+
806
+ def __6_替换颜色(self):
807
+ pass
808
+
809
+ @staticmethod
810
+ def replace_color_by_mask(im, dst_color, mask):
811
+ # TODO mask可以设置权重,从而产生类似渐变的效果?
812
+ c = _rgb_to_bgr_list(dst_color)
813
+ if len(c) == 1:
814
+ im2 = xlcv.read(im, 0)
815
+ im2[np.where((mask == [255]))] = c[0]
816
+ elif len(c) == 3:
817
+ if xlcv.n_channels(im) == 4:
818
+ x, y = im[:, :, :3].copy(), im[:, :, 3:4]
819
+ x[np.where((mask == [255]))] = c
820
+ im2 = np.concatenate([x, y], axis=2)
821
+ else:
822
+ im2 = xlcv.read(im, 1)
823
+ im2[np.where((mask == [255]))] = c
824
+ elif len(c) == 4:
825
+ im2 = xlcv.read(im, 2)
826
+ im2[np.where((mask == [255]))] = c
827
+ else:
828
+ raise ValueError(f'dst_color参数值有问题 {dst_color}')
829
+
830
+ return im2
831
+
832
+ @staticmethod
833
+ def replace_color(im, src_color, dst_color, *, tolerate=5):
834
+ """ 替换图片中的颜色
835
+
836
+ :param src_color: 原本颜色 (r, g, b)
837
+ :param dst_color: 目标颜色 (r, g, b)
838
+ 可以设为None,表示删除颜色,即设置透明底,此时返回格式默认为BGRA
839
+ :param tolerate: 查找原本颜色的时候,容许的误差距离,在误差距离内的像素,仍然进行替换
840
+ 这里的距离指单个数值允许的绝对距离
841
+
842
+ 这个算法有几个比较麻烦的考虑点
843
+ 1、原始格式不确定,对输入src_color是1个值、3个值、4个值等,处理逻辑不一样
844
+ 2、cv2默认是bgr格式,但人的使用习惯,src_color习惯用rgb格式
845
+
846
+ 为了工程化实现简洁,内部统一转成BGRA格式处理了
847
+
848
+ 【使用示例】
849
+ # 1 原始图是单通道灰度图
850
+ im1 = xlcv.read('1.png', 0)
851
+ im2 = xlcv.replace_color(im1, 50, 255) # 找到灰度为50的部分,变成白色
852
+ im2 = xlcv.replace_color(im1, 50, [0, 0, 255]) # 变成蓝色(图片自动升为3通道彩图)
853
+ # 变成白色,并且设置A=0,完全透明(透明色一般设为白色,但不一定要白色,只是完全透明时,设什么颜色其实都不太所谓)
854
+ im2 = xlcv.replace_color(im1, 50, [255, 255, 255, 0])
855
+
856
+ # 虽然原始图是单通道,但也可以用RGB、RGBA的机制检索位置,然后dst_color三种范式都支持
857
+ im2 = xlcv.replace_color(im1, [50, 50, 50], 255)
858
+ # src_color使用RGBA格式时,原图最后一个值是255,不透明
859
+ im2 = xlcv.replace_color(im1, [50, 50, 50, 255], 255)
860
+
861
+ # 2 原始图是RGB三通道彩色图
862
+ im1 = xlcv.read('1.png', 1)
863
+ im2 = xlcv.replace_color(im1, 40, 255, tolerate=20) # 原图是彩图,依然可以用40±20的灰度机制检索mask
864
+ im2 = xlcv.replace_color(im1, [32, 60, 47], [0, 0, 255]) # 变成蓝色(最常见、正常用法)
865
+
866
+ # 效果类似,全部枚举有3*3*3=27种组合,都是支持的
867
+ # 其实src_color指定了匹配模式,dst_color指定了目标值,是相互独立的两种功能指定
868
+
869
+ # 3 原始图是RGBA四通道图
870
+ im1 = xlcv.read('1.png', 2)
871
+ # 注意RGBA在目标值指定为RGB模式时,不会自动降级,这个比较特殊,不会改变原有的A通道情况
872
+ im2 = xlcv.replace_color(im1, [32, 60, 47], [0, 0, 255])
873
+ """
874
+
875
+ def set_color(a):
876
+ a = _rgb_to_bgr_list(a)
877
+ # 确定有4个值
878
+ if len(a) == 1:
879
+ a = a * 3
880
+ if len(a) == 3:
881
+ a.append(255) # A通道默认255,不透明
882
+ return np.array(a, dtype='uint8')
883
+
884
+ def set_range_color(arr, tolerate):
885
+ arr = arr.astype('int16')
886
+ a = (arr - tolerate).clip(0, 255)
887
+ b = (arr + tolerate).clip(0, 255)
888
+ return a.astype('uint8'), b.astype('uint8')
889
+
890
+ im1 = xlcv.read(im, 2)
891
+ a, b = set_range_color(set_color(src_color), tolerate)
892
+ mask = cv2.inRange(im1, a, b)
893
+ return xlcv.replace_color_by_mask(im, dst_color, mask)
894
+
895
+ @staticmethod
896
+ def replace_background_color(im, dst_color):
897
+ gray_img = xlcv.read(im, 0)
898
+ _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
899
+ return xlcv.replace_color_by_mask(im, dst_color, 255 - binary_img)
900
+
901
+ @staticmethod
902
+ def replace_foreground_color(im, dst_color):
903
+ gray_img = xlcv.read(im, 0)
904
+ _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
905
+ return xlcv.replace_color_by_mask(im, dst_color, binary_img)
906
+
907
+ @staticmethod
908
+ def replace_ground_color(im, foreground_color, background_color):
909
+ """ 替换前景、背景色
910
+ 使用了二值图的方式来做mask
911
+ """
912
+ gray_img = xlcv.read(im, 0)
913
+ _, binary_img = cv2.threshold(gray_img, np.mean(gray_img), 255, cv2.THRESH_BINARY)
914
+ if binary_img.mean() > 128: # 背景色应该比前景多的多,如果平均值大于128,说明黑底白字的模式反了
915
+ binary_img = ~binary_img
916
+ # 0作为背景,255作为前景
917
+ im = xlcv.replace_color_by_mask(im, background_color, 255 - binary_img)
918
+ im = xlcv.replace_color_by_mask(im, foreground_color, binary_img)
919
+ return im
920
+
921
+ def __7_other(self):
922
+ pass
923
+
924
+ @staticmethod
925
+ def count_pixels(im):
926
+ """ 统计image中各种rgb出现的次数 """
927
+ colors, counts = np.unique(im.reshape(-1, 3), axis=0, return_counts=True)
928
+ colors = [[tuple(c), cnt] for c, cnt in zip(colors, counts)]
929
+ colors.sort(key=lambda x: -x[1])
930
+ return colors
931
+
932
+ @staticmethod
933
+ def color_desc(im, color_num=10):
934
+ """ 描述一张图的颜色分布,这个速度还特别慢,没有优化 """
935
+ from collections import Counter
936
+ from pyxllib.cv.rgbfmt import RgbFormatter
937
+
938
+ colors = xlcv.count_pixels(im)
939
+ total = sum([cnt for _, cnt in colors])
940
+ colors2 = Counter()
941
+ for c, cnt in colors[:10000]:
942
+ c0 = RgbFormatter(*c).find_similar_std_color()
943
+ colors2[c0.to_tuple()] += cnt
944
+
945
+ for c, cnt in colors2.most_common(color_num):
946
+ desc = RgbFormatter(*c).relative_color_desc()
947
+ print(desc, f'{cnt / total:.2%}')
948
+
949
+
950
+ class CvImg(np.ndarray):
951
+ def __new__(cls, input_array, info=None):
952
+ """ 从np.ndarray继承的固定写法
953
+ https://numpy.org/doc/stable/user/basics.subclassing.html
954
+
955
+ 该类使用中完全等价np.ndarray,但额外增加了xlcv中的功能
956
+ """
957
+ # Input array is an already formed ndarray instance
958
+ # We first cast to be our class type
959
+ obj = np.asarray(input_array).view(cls)
960
+ # add the new attribute to the created instance
961
+ obj.info = info
962
+ # Finally, we must return the newly created object:
963
+ return obj
964
+
965
+ def __array_finalize__(self, obj):
966
+ # see InfoArray.__array_finalize__ for comments
967
+ if obj is None: return
968
+ self.info = getattr(obj, 'info', None)
969
+
970
+ @classmethod
971
+ def read(cls, file, flags=None, **kwargs):
972
+ return cls(xlcv.read(file, flags, **kwargs))
973
+
974
+ @classmethod
975
+ def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
976
+ return cls(xlcv.read_from_buffer(buffer, flags, b64decode=b64decode))
977
+
978
+ @classmethod
979
+ def read_from_url(cls, url, flags=None, *, b64decode=False):
980
+ return cls(xlcv.read_from_url(url, flags, b64decode=b64decode))
981
+
982
+ @property
983
+ def imsize(self):
984
+ # 这里几个属性本来可以直接调用xlcv的实现,但为了性能,这里复写一遍
985
+ return self.shape[:2]
986
+
987
+ @property
988
+ def n_channels(self):
989
+ if self.ndim == 3:
990
+ return self.shape[2]
991
+ else:
992
+ return 1
993
+
994
+ @property
995
+ def height(self):
996
+ return self.shape[0]
997
+
998
+ @property
999
+ def width(self):
1000
+ return self.shape[1]
1001
+
1002
+ def __getattr__(self, item):
1003
+ """ 对cv2、xlcv的类层级接口封装
1004
+
1005
+ 这里使用了较高级的实现方法
1006
+ 好处:从而每次开发只需要在xlcv写一遍
1007
+ 坏处:没有代码接口提示...
1008
+
1009
+ 注意,同名方法,比如size,会优先使用np.ndarray的版本
1010
+ 所以为了区分度,xlcv有imsize来代表xlcv的size版本
1011
+
1012
+ 并且无法直接使用 cv2.resize, 因为np.ndarray已经有了resize
1013
+
1014
+ 这里没有做任何安全性检查,请开发使用者自行分析使用合理性
1015
+ """
1016
+
1017
+ def warp_func(*args, **kwargs):
1018
+ func = getattr(cv2, item, getattr(xlcv, item, None)) # 先在cv2找方法,再在xlcv找方法
1019
+ if func is None:
1020
+ raise ValueError(f'不存在的方法名 {item}')
1021
+ res = func(self, *args, **kwargs)
1022
+ if isinstance(res, np.ndarray): # 返回是原始图片格式,打包后返回
1023
+ return type(self)(res) # 自定义方法必须自己再转成CvImage格式,否则会变成np.ndarray
1024
+ elif isinstance(res, tuple): # 如果是tuple类型,则里面的np.ndarray类型也要处理
1025
+ res2 = []
1026
+ for x in res:
1027
+ if isinstance(x, np.ndarray):
1028
+ res2.append(type(self)(x))
1029
+ else:
1030
+ res2.append(x)
1031
+ return tuple(res2)
1032
+ return res
1033
+
1034
+ return warp_func
1035
+
1036
+
1037
+ if __name__ == '__main__':
1038
+ import fire
1039
+
1040
+ fire.Fire()