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/xlpillib.py CHANGED
@@ -1,423 +1,423 @@
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
- """
8
- 图方便的时候,PilImg和CvImg可以进行图片类型转换,然后互相引用彼此已有的一个实现版本
9
- 为了性能的时候,则尽量减少各种绕弯,使用最源生的代码来实现
10
- """
11
-
12
- import base64
13
- import io
14
- import os
15
- import random
16
-
17
- import cv2
18
- import numpy as np
19
- import PIL.ExifTags
20
- import PIL.Image
21
- import PIL.ImageOps
22
- import requests
23
-
24
- try:
25
- import accimage
26
- except ImportError:
27
- accimage = None
28
-
29
- from pyxllib.prog.pupil import inject_members
30
- from pyxllib.file.specialist import XlPath, get_font_file
31
- from pyxllib.cv.xlcvlib import xlcv
32
-
33
-
34
- class PilImg(PIL.Image.Image):
35
-
36
- def __1_read(self):
37
- pass
38
-
39
- @classmethod
40
- def read(cls, file, flags=None, *, apply_exif_orientation=False, **kwargs) -> 'PilImg':
41
- if PilImg.is_pil_image(file):
42
- im = file
43
- elif xlcv.is_cv2_image(file):
44
- im = xlcv.to_pil_image(file)
45
- elif XlPath.safe_init(file):
46
- im = PIL.Image.open(str(file), **kwargs)
47
- else:
48
- raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
49
- if apply_exif_orientation:
50
- im = PilImg.apply_exif_orientation(im)
51
- return PilImg.cvt_channel(im, flags)
52
-
53
- @classmethod
54
- def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
55
- """ 先用opencv实现,以后可以再研究PIL.Image.frombuffer是否有更快处理策略 """
56
- if b64decode:
57
- buffer = base64.b64decode(buffer)
58
- im = PIL.Image.open(io.BytesIO(buffer))
59
- return PilImg.cvt_channel(im, flags)
60
-
61
- @classmethod
62
- def read_from_url(cls, url, flags=None, *, b64decode=False):
63
- content = requests.get(url).content
64
- return PilImg.read_from_buffer(content, flags, b64decode=b64decode)
65
-
66
- def __2_attrs(self):
67
- pass
68
-
69
- def imsize(self):
70
- return self.size[::-1]
71
-
72
- def n_channels(self):
73
- """ 通道数 """
74
- return len(self.getbands())
75
-
76
- def __3_write(self):
77
- pass
78
-
79
- def to_cv2_image(self):
80
- """ pil图片转np图片 """
81
- y = np.array(self)
82
- y = cv2.cvtColor(y, cv2.COLOR_BGR2RGB) if y.size else None
83
- return y
84
-
85
- def is_pil_image(self):
86
- if accimage is not None:
87
- return isinstance(self, (PIL.Image.Image, accimage.Image))
88
- else:
89
- return isinstance(self, PIL.Image.Image)
90
-
91
- def write(self, path, *, if_exists=None, **kwargs):
92
- p = XlPath(path)
93
- if p.exist_preprcs(if_exists):
94
- os.makedirs(p.parent, exist_ok=True)
95
- suffix = p.suffix[1:]
96
- if suffix.lower() == 'jpg':
97
- suffix = 'jpeg'
98
- if self.mode in ('RGBA', 'P') and suffix == 'jpeg':
99
- im = self.convert('RGB')
100
- else:
101
- im = self
102
- im.save(str(p), suffix, **kwargs)
103
-
104
- def cvt_channel(self, flags=None):
105
- im = self
106
- if flags is None or flags == -1: return im
107
- n_c = im.n_channels
108
- if flags == 0 and n_c > 1:
109
- im = im.convert('L')
110
- elif flags == 1 and n_c != 3:
111
- im = im.convert('RGB')
112
- return im
113
-
114
- def to_buffer(self, ext='.jpg', *, b64encode=False):
115
- # 主要是偷懒,不想重写一遍,就直接去调用cv版本的实现了
116
- return xlcv.to_buffer(self.to_cv2_image(), ext, b64encode=b64encode)
117
-
118
- def display(self):
119
- """ 在jupyter中展示 """
120
- try:
121
- from IPython.display import display
122
- display(self)
123
- except ModuleNotFoundError:
124
- pass
125
-
126
- def __4_plot(self):
127
- pass
128
-
129
- def plot_border(self, border=1, fill='black'):
130
- """ 给图片加上边框
131
-
132
- Args:
133
- self:
134
- border: 边框的厚度
135
- fill: 边框颜色
136
-
137
- Returns: 一张新图
138
- """
139
- from PIL import ImageOps
140
- im2 = ImageOps.expand(self, border=border, fill=fill)
141
- return im2
142
-
143
- def plot_text(self, text, xy=None, font_size=10, font_type='simfang.ttf', **kwargs):
144
- """
145
- :param xy: 写入文本的起始坐标,没写入则自动写在垂直居中位置
146
- """
147
- from PIL import ImageFont, ImageDraw
148
- font_file = get_font_file(font_type)
149
- font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
150
- draw = ImageDraw.Draw(self)
151
- if xy is None:
152
- w, h = font_getsize(font, text)
153
- xy = ((self.size[0] - w) / 2, (self.size[1] - h) / 2)
154
- draw.text(xy, text, font=font, **kwargs)
155
- return self
156
-
157
- def __5_resize(self):
158
- pass
159
-
160
- def reduce_area(self, area):
161
- """ 根据面积上限缩小图片
162
-
163
- 即图片面积超过area时,按照等比例缩小到面积为area的图片
164
- """
165
- im = self
166
- h, w = PilImg.imsize(im)
167
- s = h * w
168
- if s > area:
169
- r = (area / s) ** 0.5
170
- size = int(r * h), int(r * w)
171
- im = PilImg.resize2(im, size)
172
- return im
173
-
174
- def resize2(self, size, **kwargs):
175
- """
176
- :param size: 默认是 (w, h), 这里我倒过来 (h, w)
177
- 但计算机领域,确实经常都是用 (w, h) 的格式,毕竟横轴是x,纵轴才是y
178
- :param kwargs:
179
- resample=3,插值算法;有PIL.Image.NEAREST, ~BOX, ~BILINEAR, ~HAMMING, ~BICUBIC, ~LANCZOS等
180
- 默认是 PIL.Image.BICUBIC;如果mode是"1"或"P"模式,则总是 PIL.Image.NEAREST
181
-
182
- >>> im = read(np.zeros([100, 200], dtype='uint8'), 0)
183
- >>> self.size
184
- (100, 200)
185
- >>> im2 = im.reduce_area(50*50, **kwargs)
186
- >>> im2.size
187
- (35, 70)
188
- """
189
- # 注意pil图像尺寸接口都是[w,h],跟标准的[h,w]相反
190
- return self.resize(size[::-1], **kwargs)
191
-
192
- def evaluate_image_file_size(self, suffix='.jpeg'):
193
- """ 评估图像存成文件后的大小
194
-
195
- :param suffix: 使用的图片类型
196
- :return int: 存储后的文件大小,单位为字节
197
- """
198
- im = self
199
-
200
- # save接口不支持jpg参数
201
- if suffix[0] == '.':
202
- suffix = suffix[1:]
203
- if suffix.lower() == 'jpg':
204
- suffix = 'jpeg'
205
-
206
- file = io.BytesIO()
207
- if im.mode in ('RGBA', 'P') and suffix == 'jpeg':
208
- im = im.convert('RGB')
209
- im.save(file, suffix)
210
- return len(file.getvalue())
211
-
212
- def reduce_filesize(self, filesize=None, suffix='.jpeg'):
213
- """ 按照保存后的文件大小来压缩im
214
-
215
- :param filesize: 单位Bytes
216
- int, 可以用 300*1024 来表示 300KB
217
- None, 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
218
-
219
- >> reduce_filesize(im, 300*1024, 'jpg')
220
- """
221
- im = self
222
- # 循环处理
223
- while filesize:
224
- r = xlpil.evaluate_image_file_size(im, suffix) / filesize
225
- if r <= 1:
226
- break
227
-
228
- # 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
229
- rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
230
- im = im.resize((int(im.size[0] * rate), int(im.size[1] * rate)))
231
- return im
232
-
233
- def trim(self, *, border=0, color=None):
234
- """ 默认裁剪掉白色边缘,可以配合 get_backgroup_color 裁剪掉背景色
235
-
236
- :param border: 上下左右保留多少边缘
237
- 输入一个整数,表示统一留边
238
- 也可以输入[int, int, int, int],分别表示left top right bottom留白
239
- :param color: 要裁剪的颜色,这是精确值,没有误差,如果需要模糊,可以提前预处理成精确值
240
- 这个默认值设成None,本来是想说支持灰度图,此时输入一个255的值就好
241
- 但是pil的灰度图机制好像有些不太一样,总之目前默认值还是自动会设成 (255, 255, 255)
242
- :param percent-background: TODO 控制裁剪百分比上限
243
- """
244
- from PIL import Image, ImageChops
245
- from pyxllib.algo.geo import xywh2ltrb, ltrb2xywh, ltrb_border
246
-
247
- if color is None:
248
- color = (255, 255, 255)
249
- else:
250
- color = tuple(color)
251
-
252
- # 如果图片通道数跟预设的color不同,要断言
253
- assert self.n_channels() == len(color), f'图片通道数{self.n_channels}跟预设的color{color}不同'
254
-
255
- im = self
256
- bg = Image.new(im.mode, im.size, color)
257
- diff = ImageChops.difference(im, bg)
258
- bbox = diff.getbbox() # 如果im跟bg一样,也就是裁"消失"了,此时bbox值为None
259
- if bbox:
260
- if border:
261
- ltrb = xywh2ltrb(bbox)
262
- ltrb = ltrb_border(ltrb, border, im.size)
263
- bbox = ltrb2xywh(ltrb)
264
- im = im.crop(bbox)
265
- return im
266
-
267
- def __6_warp(self):
268
- pass
269
-
270
- def __x_other(self):
271
- pass
272
-
273
- def random_direction(self):
274
- """ 假设原图片是未旋转的状态0
275
-
276
- 顺时针转90度是label=1,顺时针转180度是label2 ...
277
- """
278
- im = self
279
- label = np.random.randint(4)
280
- if label == 1:
281
- # PIL的旋转角度,是指逆时针角度;但是我这里编号是顺时针
282
- im = im.transpose(PIL.Image.ROTATE_270)
283
- elif label == 2:
284
- im = im.transpose(PIL.Image.ROTATE_180)
285
- elif label == 3:
286
- im = im.transpose(PIL.Image.ROTATE_90)
287
- return im, label
288
-
289
- def flip_direction(self, direction):
290
- """
291
- :param direction: 顺时针旋转几个90度
292
- 标记现在图片是哪个方向:0是正常,1是向右翻转,2是向下翻转,3是向左翻转
293
- """
294
- im = self
295
- direction = direction % 4
296
- if direction:
297
- im = im.transpose({1: PIL.Image.ROTATE_270,
298
- 2: PIL.Image.ROTATE_180,
299
- 3: PIL.Image.ROTATE_90}[direction])
300
- return im
301
-
302
- def apply_exif_orientation(self):
303
- """ 摆正图片角度
304
-
305
- Image.open读取图片时,是手机严格正放时拍到的图片效果,
306
- 但手机拍照时是会记录旋转位置的,即可以判断是物理空间中,实际朝上、朝下的方向,
307
- 从而识别出正拍(代号1),顺时针旋转90度拍摄(代号8),顺时针180度拍摄(代号3),顺时针270度拍摄(代号6)。
308
- windows上的图片查阅软件能识别方向代号后正确摆放;
309
- 为了让python处理图片的时候能增加这个属性的考虑,这个函数能修正识别角度返回新的图片。
310
-
311
- 我自己写过个版本,后来发现 labelme.utils.image 写过功能更强的,抄了过来~~
312
- """
313
- im = self
314
- try:
315
- exif = im._getexif()
316
- except AttributeError:
317
- exif = None
318
-
319
- if exif is None:
320
- return im
321
-
322
- exif = {
323
- PIL.ExifTags.TAGS[k]: v
324
- for k, v in exif.items()
325
- if k in PIL.ExifTags.TAGS
326
- }
327
-
328
- orientation = exif.get("Orientation", None)
329
-
330
- if orientation == 1:
331
- # do nothing
332
- return im
333
- elif orientation == 2:
334
- # left-to-right mirror
335
- return PIL.ImageOps.mirror(im)
336
- elif orientation == 3:
337
- # rotate 180
338
- return im.transpose(PIL.Image.ROTATE_180)
339
- elif orientation == 4:
340
- # top-to-bottom mirror
341
- return PIL.ImageOps.flip(im)
342
- elif orientation == 5:
343
- # top-to-left mirror
344
- return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_270))
345
- elif orientation == 6:
346
- # rotate 270
347
- return im.transpose(PIL.Image.ROTATE_270)
348
- elif orientation == 7:
349
- # top-to-right mirror
350
- return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_90))
351
- elif orientation == 8:
352
- # rotate 90
353
- return im.transpose(PIL.Image.ROTATE_90)
354
- else:
355
- return im
356
-
357
- def get_exif(self):
358
- """ 旧函数名:查看图片的Exif信息 """
359
- exif_data = self._getexif()
360
- if exif_data:
361
- exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif_data.items() if k in PIL.ExifTags.TAGS}
362
- else:
363
- exif = None
364
- return exif
365
-
366
- def rgba2rgb(self):
367
- if self.mode in ('RGBA', 'P'):
368
- # 判断图片mode模式,如果是RGBA或P等可能有透明底,则和一个白底图片合成去除透明底
369
- background = PIL.Image.new('RGBA', self.size, (255, 255, 255))
370
- # composite是合成的意思。将右图的alpha替换为左图内容
371
- self = PIL.Image.alpha_composite(background, self.convert('RGBA')).convert('RGB')
372
- return self
373
-
374
- def keep_subtitles(self, judge_func=None, trim_color=(255, 255, 255)):
375
- im = self.to_cv2_image()
376
- im = im.keep_subtitles(judge_func=judge_func, trim_color=trim_color)
377
- return im.to_pil_image()
378
-
379
-
380
- # pil相比cv,由于无法类似CvImg这样新建一个和np.ndarray等效的类,所以还是比较支持嵌入到Image中直接操作
381
- inject_members(PilImg, PIL.Image.Image)
382
-
383
- xlpil = PilImg # 与xlcv对称
384
-
385
-
386
- def font_getsize(font, text):
387
- """ 官方自带的font.getsize遇到换行符的text,计算不准确
388
- """
389
- texts = text.split('\n')
390
- sizes = [font.getsize(t) for t in texts]
391
- w = max([w for w, h in sizes])
392
- h = sum([h for w, h in sizes])
393
- return w, h
394
-
395
-
396
- def create_text_image(text, size=None, *, xy=None, font_size=14, bg_color=None, text_color=None, **kwargs):
397
- """ 生成文字图片
398
-
399
- :param size: 注意我这里顺序是 (height, width)
400
- 默认None,根据写入的文字动态生成图片大小
401
- :param bg_color: 背景图颜色,如 (0, 0, 0)
402
- 默认None,随机颜色
403
- :param text_color: 文本颜色,如 (255, 255, 255)
404
- 默认None,随机颜色
405
- """
406
- if size is None:
407
- from PIL import ImageFont, ImageDraw
408
- font_file = get_font_file(kwargs.get('font_type', 'simfang.ttf'))
409
- font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
410
- w, h = font_getsize(font, text)
411
- size = (h + 10, w + 10)
412
- xy = (5, 0) # 自动生成尺寸的情况下,文本从左上角开始写
413
-
414
- if bg_color is None:
415
- bg_color = tuple([random.randint(0, 255) for i in range(3)])
416
- if text_color is None:
417
- text_color = tuple([random.randint(0, 255) for i in range(3)])
418
-
419
- h, w = size
420
- im: PilImg = PIL.Image.new('RGB', (w, h), tuple(bg_color))
421
- im2 = im.plot_text(text, xy=xy, font_size=font_size, fill=tuple(text_color), **kwargs)
422
-
423
- return im2
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
+ """
8
+ 图方便的时候,PilImg和CvImg可以进行图片类型转换,然后互相引用彼此已有的一个实现版本
9
+ 为了性能的时候,则尽量减少各种绕弯,使用最源生的代码来实现
10
+ """
11
+
12
+ import base64
13
+ import io
14
+ import os
15
+ import random
16
+
17
+ import cv2
18
+ import numpy as np
19
+ import PIL.ExifTags
20
+ import PIL.Image
21
+ import PIL.ImageOps
22
+ import requests
23
+
24
+ try:
25
+ import accimage
26
+ except ImportError:
27
+ accimage = None
28
+
29
+ from pyxllib.prog.pupil import inject_members
30
+ from pyxllib.file.specialist import XlPath, get_font_file
31
+ from pyxllib.cv.xlcvlib import xlcv
32
+
33
+
34
+ class PilImg(PIL.Image.Image):
35
+
36
+ def __1_read(self):
37
+ pass
38
+
39
+ @classmethod
40
+ def read(cls, file, flags=None, *, apply_exif_orientation=False, **kwargs) -> 'PilImg':
41
+ if PilImg.is_pil_image(file):
42
+ im = file
43
+ elif xlcv.is_cv2_image(file):
44
+ im = xlcv.to_pil_image(file)
45
+ elif XlPath.safe_init(file):
46
+ im = PIL.Image.open(str(file), **kwargs)
47
+ else:
48
+ raise TypeError(f'类型错误或文件不存在:{type(file)} {file}')
49
+ if apply_exif_orientation:
50
+ im = PilImg.apply_exif_orientation(im)
51
+ return PilImg.cvt_channel(im, flags)
52
+
53
+ @classmethod
54
+ def read_from_buffer(cls, buffer, flags=None, *, b64decode=False):
55
+ """ 先用opencv实现,以后可以再研究PIL.Image.frombuffer是否有更快处理策略 """
56
+ if b64decode:
57
+ buffer = base64.b64decode(buffer)
58
+ im = PIL.Image.open(io.BytesIO(buffer))
59
+ return PilImg.cvt_channel(im, flags)
60
+
61
+ @classmethod
62
+ def read_from_url(cls, url, flags=None, *, b64decode=False):
63
+ content = requests.get(url).content
64
+ return PilImg.read_from_buffer(content, flags, b64decode=b64decode)
65
+
66
+ def __2_attrs(self):
67
+ pass
68
+
69
+ def imsize(self):
70
+ return self.size[::-1]
71
+
72
+ def n_channels(self):
73
+ """ 通道数 """
74
+ return len(self.getbands())
75
+
76
+ def __3_write(self):
77
+ pass
78
+
79
+ def to_cv2_image(self):
80
+ """ pil图片转np图片 """
81
+ y = np.array(self)
82
+ y = cv2.cvtColor(y, cv2.COLOR_BGR2RGB) if y.size else None
83
+ return y
84
+
85
+ def is_pil_image(self):
86
+ if accimage is not None:
87
+ return isinstance(self, (PIL.Image.Image, accimage.Image))
88
+ else:
89
+ return isinstance(self, PIL.Image.Image)
90
+
91
+ def write(self, path, *, if_exists=None, **kwargs):
92
+ p = XlPath(path)
93
+ if p.exist_preprcs(if_exists):
94
+ os.makedirs(p.parent, exist_ok=True)
95
+ suffix = p.suffix[1:]
96
+ if suffix.lower() == 'jpg':
97
+ suffix = 'jpeg'
98
+ if self.mode in ('RGBA', 'P') and suffix == 'jpeg':
99
+ im = self.convert('RGB')
100
+ else:
101
+ im = self
102
+ im.save(str(p), suffix, **kwargs)
103
+
104
+ def cvt_channel(self, flags=None):
105
+ im = self
106
+ if flags is None or flags == -1: return im
107
+ n_c = im.n_channels
108
+ if flags == 0 and n_c > 1:
109
+ im = im.convert('L')
110
+ elif flags == 1 and n_c != 3:
111
+ im = im.convert('RGB')
112
+ return im
113
+
114
+ def to_buffer(self, ext='.jpg', *, b64encode=False):
115
+ # 主要是偷懒,不想重写一遍,就直接去调用cv版本的实现了
116
+ return xlcv.to_buffer(self.to_cv2_image(), ext, b64encode=b64encode)
117
+
118
+ def display(self):
119
+ """ 在jupyter中展示 """
120
+ try:
121
+ from IPython.display import display
122
+ display(self)
123
+ except ModuleNotFoundError:
124
+ pass
125
+
126
+ def __4_plot(self):
127
+ pass
128
+
129
+ def plot_border(self, border=1, fill='black'):
130
+ """ 给图片加上边框
131
+
132
+ Args:
133
+ self:
134
+ border: 边框的厚度
135
+ fill: 边框颜色
136
+
137
+ Returns: 一张新图
138
+ """
139
+ from PIL import ImageOps
140
+ im2 = ImageOps.expand(self, border=border, fill=fill)
141
+ return im2
142
+
143
+ def plot_text(self, text, xy=None, font_size=10, font_type='simfang.ttf', **kwargs):
144
+ """
145
+ :param xy: 写入文本的起始坐标,没写入则自动写在垂直居中位置
146
+ """
147
+ from PIL import ImageFont, ImageDraw
148
+ font_file = get_font_file(font_type)
149
+ font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
150
+ draw = ImageDraw.Draw(self)
151
+ if xy is None:
152
+ w, h = font_getsize(font, text)
153
+ xy = ((self.size[0] - w) / 2, (self.size[1] - h) / 2)
154
+ draw.text(xy, text, font=font, **kwargs)
155
+ return self
156
+
157
+ def __5_resize(self):
158
+ pass
159
+
160
+ def reduce_area(self, area):
161
+ """ 根据面积上限缩小图片
162
+
163
+ 即图片面积超过area时,按照等比例缩小到面积为area的图片
164
+ """
165
+ im = self
166
+ h, w = PilImg.imsize(im)
167
+ s = h * w
168
+ if s > area:
169
+ r = (area / s) ** 0.5
170
+ size = int(r * h), int(r * w)
171
+ im = PilImg.resize2(im, size)
172
+ return im
173
+
174
+ def resize2(self, size, **kwargs):
175
+ """
176
+ :param size: 默认是 (w, h), 这里我倒过来 (h, w)
177
+ 但计算机领域,确实经常都是用 (w, h) 的格式,毕竟横轴是x,纵轴才是y
178
+ :param kwargs:
179
+ resample=3,插值算法;有PIL.Image.NEAREST, ~BOX, ~BILINEAR, ~HAMMING, ~BICUBIC, ~LANCZOS等
180
+ 默认是 PIL.Image.BICUBIC;如果mode是"1"或"P"模式,则总是 PIL.Image.NEAREST
181
+
182
+ >>> im = read(np.zeros([100, 200], dtype='uint8'), 0)
183
+ >>> self.size
184
+ (100, 200)
185
+ >>> im2 = im.reduce_area(50*50, **kwargs)
186
+ >>> im2.size
187
+ (35, 70)
188
+ """
189
+ # 注意pil图像尺寸接口都是[w,h],跟标准的[h,w]相反
190
+ return self.resize(size[::-1], **kwargs)
191
+
192
+ def evaluate_image_file_size(self, suffix='.jpeg'):
193
+ """ 评估图像存成文件后的大小
194
+
195
+ :param suffix: 使用的图片类型
196
+ :return int: 存储后的文件大小,单位为字节
197
+ """
198
+ im = self
199
+
200
+ # save接口不支持jpg参数
201
+ if suffix[0] == '.':
202
+ suffix = suffix[1:]
203
+ if suffix.lower() == 'jpg':
204
+ suffix = 'jpeg'
205
+
206
+ file = io.BytesIO()
207
+ if im.mode in ('RGBA', 'P') and suffix == 'jpeg':
208
+ im = im.convert('RGB')
209
+ im.save(file, suffix)
210
+ return len(file.getvalue())
211
+
212
+ def reduce_filesize(self, filesize=None, suffix='.jpeg'):
213
+ """ 按照保存后的文件大小来压缩im
214
+
215
+ :param filesize: 单位Bytes
216
+ int, 可以用 300*1024 来表示 300KB
217
+ None, 可以不输入,默认读取后按原尺寸返回,这样看似没变化,其实图片一读一写,是会对手机拍照的很多大图进行压缩的
218
+
219
+ >> reduce_filesize(im, 300*1024, 'jpg')
220
+ """
221
+ im = self
222
+ # 循环处理
223
+ while filesize:
224
+ r = xlpil.evaluate_image_file_size(im, suffix) / filesize
225
+ if r <= 1:
226
+ break
227
+
228
+ # 假设图片面积和文件大小成正比,如果r=4,表示长宽要各减小至1/(r**0.5)才能到目标文件大小
229
+ rate = min(1 / (r ** 0.5), 0.95) # 并且限制每轮至少要缩小至95%,避免可能会迭代太多轮
230
+ im = im.resize((int(im.size[0] * rate), int(im.size[1] * rate)))
231
+ return im
232
+
233
+ def trim(self, *, border=0, color=None):
234
+ """ 默认裁剪掉白色边缘,可以配合 get_backgroup_color 裁剪掉背景色
235
+
236
+ :param border: 上下左右保留多少边缘
237
+ 输入一个整数,表示统一留边
238
+ 也可以输入[int, int, int, int],分别表示left top right bottom留白
239
+ :param color: 要裁剪的颜色,这是精确值,没有误差,如果需要模糊,可以提前预处理成精确值
240
+ 这个默认值设成None,本来是想说支持灰度图,此时输入一个255的值就好
241
+ 但是pil的灰度图机制好像有些不太一样,总之目前默认值还是自动会设成 (255, 255, 255)
242
+ :param percent-background: TODO 控制裁剪百分比上限
243
+ """
244
+ from PIL import Image, ImageChops
245
+ from pyxllib.algo.geo import xywh2ltrb, ltrb2xywh, ltrb_border
246
+
247
+ if color is None:
248
+ color = (255, 255, 255)
249
+ else:
250
+ color = tuple(color)
251
+
252
+ # 如果图片通道数跟预设的color不同,要断言
253
+ assert self.n_channels() == len(color), f'图片通道数{self.n_channels}跟预设的color{color}不同'
254
+
255
+ im = self
256
+ bg = Image.new(im.mode, im.size, color)
257
+ diff = ImageChops.difference(im, bg)
258
+ bbox = diff.getbbox() # 如果im跟bg一样,也就是裁"消失"了,此时bbox值为None
259
+ if bbox:
260
+ if border:
261
+ ltrb = xywh2ltrb(bbox)
262
+ ltrb = ltrb_border(ltrb, border, im.size)
263
+ bbox = ltrb2xywh(ltrb)
264
+ im = im.crop(bbox)
265
+ return im
266
+
267
+ def __6_warp(self):
268
+ pass
269
+
270
+ def __x_other(self):
271
+ pass
272
+
273
+ def random_direction(self):
274
+ """ 假设原图片是未旋转的状态0
275
+
276
+ 顺时针转90度是label=1,顺时针转180度是label2 ...
277
+ """
278
+ im = self
279
+ label = np.random.randint(4)
280
+ if label == 1:
281
+ # PIL的旋转角度,是指逆时针角度;但是我这里编号是顺时针
282
+ im = im.transpose(PIL.Image.ROTATE_270)
283
+ elif label == 2:
284
+ im = im.transpose(PIL.Image.ROTATE_180)
285
+ elif label == 3:
286
+ im = im.transpose(PIL.Image.ROTATE_90)
287
+ return im, label
288
+
289
+ def flip_direction(self, direction):
290
+ """
291
+ :param direction: 顺时针旋转几个90度
292
+ 标记现在图片是哪个方向:0是正常,1是向右翻转,2是向下翻转,3是向左翻转
293
+ """
294
+ im = self
295
+ direction = direction % 4
296
+ if direction:
297
+ im = im.transpose({1: PIL.Image.ROTATE_270,
298
+ 2: PIL.Image.ROTATE_180,
299
+ 3: PIL.Image.ROTATE_90}[direction])
300
+ return im
301
+
302
+ def apply_exif_orientation(self):
303
+ """ 摆正图片角度
304
+
305
+ Image.open读取图片时,是手机严格正放时拍到的图片效果,
306
+ 但手机拍照时是会记录旋转位置的,即可以判断是物理空间中,实际朝上、朝下的方向,
307
+ 从而识别出正拍(代号1),顺时针旋转90度拍摄(代号8),顺时针180度拍摄(代号3),顺时针270度拍摄(代号6)。
308
+ windows上的图片查阅软件能识别方向代号后正确摆放;
309
+ 为了让python处理图片的时候能增加这个属性的考虑,这个函数能修正识别角度返回新的图片。
310
+
311
+ 我自己写过个版本,后来发现 labelme.utils.image 写过功能更强的,抄了过来~~
312
+ """
313
+ im = self
314
+ try:
315
+ exif = im._getexif()
316
+ except AttributeError:
317
+ exif = None
318
+
319
+ if exif is None:
320
+ return im
321
+
322
+ exif = {
323
+ PIL.ExifTags.TAGS[k]: v
324
+ for k, v in exif.items()
325
+ if k in PIL.ExifTags.TAGS
326
+ }
327
+
328
+ orientation = exif.get("Orientation", None)
329
+
330
+ if orientation == 1:
331
+ # do nothing
332
+ return im
333
+ elif orientation == 2:
334
+ # left-to-right mirror
335
+ return PIL.ImageOps.mirror(im)
336
+ elif orientation == 3:
337
+ # rotate 180
338
+ return im.transpose(PIL.Image.ROTATE_180)
339
+ elif orientation == 4:
340
+ # top-to-bottom mirror
341
+ return PIL.ImageOps.flip(im)
342
+ elif orientation == 5:
343
+ # top-to-left mirror
344
+ return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_270))
345
+ elif orientation == 6:
346
+ # rotate 270
347
+ return im.transpose(PIL.Image.ROTATE_270)
348
+ elif orientation == 7:
349
+ # top-to-right mirror
350
+ return PIL.ImageOps.mirror(im.transpose(PIL.Image.ROTATE_90))
351
+ elif orientation == 8:
352
+ # rotate 90
353
+ return im.transpose(PIL.Image.ROTATE_90)
354
+ else:
355
+ return im
356
+
357
+ def get_exif(self):
358
+ """ 旧函数名:查看图片的Exif信息 """
359
+ exif_data = self._getexif()
360
+ if exif_data:
361
+ exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif_data.items() if k in PIL.ExifTags.TAGS}
362
+ else:
363
+ exif = None
364
+ return exif
365
+
366
+ def rgba2rgb(self):
367
+ if self.mode in ('RGBA', 'P'):
368
+ # 判断图片mode模式,如果是RGBA或P等可能有透明底,则和一个白底图片合成去除透明底
369
+ background = PIL.Image.new('RGBA', self.size, (255, 255, 255))
370
+ # composite是合成的意思。将右图的alpha替换为左图内容
371
+ self = PIL.Image.alpha_composite(background, self.convert('RGBA')).convert('RGB')
372
+ return self
373
+
374
+ def keep_subtitles(self, judge_func=None, trim_color=(255, 255, 255)):
375
+ im = self.to_cv2_image()
376
+ im = im.keep_subtitles(judge_func=judge_func, trim_color=trim_color)
377
+ return im.to_pil_image()
378
+
379
+
380
+ # pil相比cv,由于无法类似CvImg这样新建一个和np.ndarray等效的类,所以还是比较支持嵌入到Image中直接操作
381
+ inject_members(PilImg, PIL.Image.Image)
382
+
383
+ xlpil = PilImg # 与xlcv对称
384
+
385
+
386
+ def font_getsize(font, text):
387
+ """ 官方自带的font.getsize遇到换行符的text,计算不准确
388
+ """
389
+ texts = text.split('\n')
390
+ sizes = [font.getsize(t) for t in texts]
391
+ w = max([w for w, h in sizes])
392
+ h = sum([h for w, h in sizes])
393
+ return w, h
394
+
395
+
396
+ def create_text_image(text, size=None, *, xy=None, font_size=14, bg_color=None, text_color=None, **kwargs):
397
+ """ 生成文字图片
398
+
399
+ :param size: 注意我这里顺序是 (height, width)
400
+ 默认None,根据写入的文字动态生成图片大小
401
+ :param bg_color: 背景图颜色,如 (0, 0, 0)
402
+ 默认None,随机颜色
403
+ :param text_color: 文本颜色,如 (255, 255, 255)
404
+ 默认None,随机颜色
405
+ """
406
+ if size is None:
407
+ from PIL import ImageFont, ImageDraw
408
+ font_file = get_font_file(kwargs.get('font_type', 'simfang.ttf'))
409
+ font = ImageFont.truetype(font=str(font_file), size=font_size, encoding="utf-8")
410
+ w, h = font_getsize(font, text)
411
+ size = (h + 10, w + 10)
412
+ xy = (5, 0) # 自动生成尺寸的情况下,文本从左上角开始写
413
+
414
+ if bg_color is None:
415
+ bg_color = tuple([random.randint(0, 255) for i in range(3)])
416
+ if text_color is None:
417
+ text_color = tuple([random.randint(0, 255) for i in range(3)])
418
+
419
+ h, w = size
420
+ im: PilImg = PIL.Image.new('RGB', (w, h), tuple(bg_color))
421
+ im2 = im.plot_text(text, xy=xy, font_size=font_size, fill=tuple(text_color), **kwargs)
422
+
423
+ return im2