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.
- pyxllib/__init__.py +14 -21
- pyxllib/algo/__init__.py +8 -8
- pyxllib/algo/disjoint.py +54 -54
- pyxllib/algo/geo.py +537 -541
- pyxllib/algo/intervals.py +964 -964
- pyxllib/algo/matcher.py +389 -389
- pyxllib/algo/newbie.py +166 -166
- pyxllib/algo/pupil.py +629 -629
- pyxllib/algo/shapelylib.py +67 -67
- pyxllib/algo/specialist.py +241 -241
- pyxllib/algo/stat.py +494 -494
- pyxllib/algo/treelib.py +145 -149
- pyxllib/algo/unitlib.py +62 -66
- pyxllib/autogui/__init__.py +5 -5
- pyxllib/autogui/activewin.py +246 -246
- pyxllib/autogui/all.py +9 -9
- pyxllib/autogui/autogui.py +846 -852
- pyxllib/autogui/uiautolib.py +362 -362
- pyxllib/autogui/virtualkey.py +102 -102
- pyxllib/autogui/wechat.py +827 -827
- pyxllib/autogui/wechat_msg.py +421 -421
- pyxllib/autogui/wxautolib.py +84 -84
- pyxllib/cv/__init__.py +5 -5
- pyxllib/cv/expert.py +267 -267
- pyxllib/cv/imfile.py +159 -159
- pyxllib/cv/imhash.py +39 -39
- pyxllib/cv/pupil.py +9 -9
- pyxllib/cv/rgbfmt.py +1525 -1525
- pyxllib/cv/slidercaptcha.py +137 -137
- pyxllib/cv/trackbartools.py +251 -251
- pyxllib/cv/xlcvlib.py +1040 -1040
- pyxllib/cv/xlpillib.py +423 -423
- pyxllib/data/echarts.py +236 -240
- pyxllib/data/jsonlib.py +85 -89
- pyxllib/data/oss.py +72 -72
- pyxllib/data/pglib.py +1111 -1127
- pyxllib/data/sqlite.py +568 -568
- pyxllib/data/sqllib.py +297 -297
- pyxllib/ext/JLineViewer.py +505 -505
- pyxllib/ext/__init__.py +6 -6
- pyxllib/ext/demolib.py +251 -246
- pyxllib/ext/drissionlib.py +277 -277
- pyxllib/ext/kq5034lib.py +12 -12
- pyxllib/ext/qt.py +449 -449
- pyxllib/ext/robustprocfile.py +493 -497
- pyxllib/ext/seleniumlib.py +76 -76
- pyxllib/ext/tk.py +173 -173
- pyxllib/ext/unixlib.py +821 -827
- pyxllib/ext/utools.py +345 -351
- pyxllib/ext/webhook.py +124 -119
- pyxllib/ext/win32lib.py +40 -40
- pyxllib/ext/wjxlib.py +91 -88
- pyxllib/ext/wpsapi.py +124 -124
- pyxllib/ext/xlwork.py +9 -9
- pyxllib/ext/yuquelib.py +1110 -1105
- pyxllib/file/__init__.py +17 -17
- pyxllib/file/docxlib.py +757 -761
- pyxllib/file/gitlib.py +309 -309
- pyxllib/file/libreoffice.py +165 -165
- pyxllib/file/movielib.py +144 -148
- pyxllib/file/newbie.py +10 -10
- pyxllib/file/onenotelib.py +1469 -1469
- pyxllib/file/packlib/__init__.py +330 -330
- pyxllib/file/packlib/zipfile.py +2441 -2441
- pyxllib/file/pdflib.py +422 -426
- pyxllib/file/pupil.py +185 -185
- pyxllib/file/specialist/__init__.py +681 -685
- pyxllib/file/specialist/dirlib.py +799 -799
- pyxllib/file/specialist/download.py +193 -193
- pyxllib/file/specialist/filelib.py +2825 -2829
- pyxllib/file/xlsxlib.py +3122 -3131
- pyxllib/file/xlsyncfile.py +341 -341
- pyxllib/prog/__init__.py +5 -5
- pyxllib/prog/cachetools.py +58 -64
- pyxllib/prog/deprecatedlib.py +233 -233
- pyxllib/prog/filelock.py +42 -42
- pyxllib/prog/ipyexec.py +253 -253
- pyxllib/prog/multiprogs.py +940 -940
- pyxllib/prog/newbie.py +451 -451
- pyxllib/prog/pupil.py +1208 -1197
- pyxllib/prog/sitepackages.py +33 -33
- pyxllib/prog/specialist/__init__.py +348 -391
- pyxllib/prog/specialist/bc.py +203 -203
- pyxllib/prog/specialist/browser.py +497 -497
- pyxllib/prog/specialist/common.py +347 -347
- pyxllib/prog/specialist/datetime.py +198 -198
- pyxllib/prog/specialist/tictoc.py +240 -240
- pyxllib/prog/specialist/xllog.py +180 -180
- pyxllib/prog/xlosenv.py +110 -108
- pyxllib/stdlib/__init__.py +17 -17
- pyxllib/stdlib/tablepyxl/__init__.py +10 -10
- pyxllib/stdlib/tablepyxl/style.py +303 -303
- pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
- pyxllib/text/__init__.py +8 -8
- pyxllib/text/ahocorasick.py +36 -39
- pyxllib/text/airscript.js +754 -744
- pyxllib/text/charclasslib.py +121 -121
- pyxllib/text/jiebalib.py +267 -267
- pyxllib/text/jinjalib.py +27 -32
- pyxllib/text/jsa_ai_prompt.md +271 -271
- pyxllib/text/jscode.py +922 -922
- pyxllib/text/latex/__init__.py +158 -158
- pyxllib/text/levenshtein.py +303 -303
- pyxllib/text/nestenv.py +1215 -1215
- pyxllib/text/newbie.py +300 -300
- pyxllib/text/pupil/__init__.py +8 -8
- pyxllib/text/pupil/common.py +1121 -1121
- pyxllib/text/pupil/xlalign.py +326 -326
- pyxllib/text/pycode.py +47 -47
- pyxllib/text/specialist/__init__.py +8 -8
- pyxllib/text/specialist/common.py +112 -112
- pyxllib/text/specialist/ptag.py +186 -186
- pyxllib/text/spellchecker.py +172 -172
- pyxllib/text/templates/echart_base.html +10 -10
- pyxllib/text/templates/highlight_code.html +16 -16
- pyxllib/text/templates/latex_editor.html +102 -102
- pyxllib/text/vbacode.py +17 -17
- pyxllib/text/xmllib.py +741 -747
- pyxllib/xl.py +42 -39
- pyxllib/xlcv.py +17 -17
- pyxllib-3.201.1.dist-info/METADATA +296 -0
- pyxllib-3.201.1.dist-info/RECORD +125 -0
- {pyxllib-0.3.197.dist-info → pyxllib-3.201.1.dist-info}/licenses/LICENSE +190 -190
- pyxllib/ext/old.py +0 -663
- pyxllib-0.3.197.dist-info/METADATA +0 -48
- pyxllib-0.3.197.dist-info/RECORD +0 -126
- {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()
|