Coverage for pyquickhelper/imghelper/img_helper.py: 96%
112 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1"""
2@file
3@brief Helpers around images.
4"""
5import os
6import glob
7import numpy
10def zoom_img(img, factor=1., max_dim=None, out_file=None, fLOG=None):
11 """
12 Zooms an image.
14 :param img: image or filename or pattern
15 :param factor: multiplies the image by this factor if not None
16 :param max_dim: modifies the image, the highest dimension
17 should below this number
18 :param out_file: stores the image into this file if not None
19 :param fLOG: logging function
20 :return: image
21 """
22 if isinstance(img, str):
23 if '*' in img:
24 found = glob.glob(img)
25 res = []
26 for im in found:
27 if out_file is None:
28 i = zoom_img(im, factor=factor, max_dim=max_dim, fLOG=fLOG)
29 else:
30 of = out_file.format(os.path.split(im)[-1])
31 i = zoom_img(im, factor=factor, max_dim=max_dim,
32 out_file=of, fLOG=fLOG)
33 res.append(i)
34 if len(res) == 0:
35 raise FileNotFoundError( # pragma: no cover
36 f"Unable to find anything in '{img}'.")
37 return res
38 from PIL import Image
39 obj = Image.open(img)
40 elif hasattr(img, 'size'):
41 obj = img
42 else:
43 raise TypeError( # pragma: no cover
44 f"Image should be a string or an image not {type(img)}.")
45 dx, dy = obj.size
46 if max_dim is not None:
47 if not isinstance(max_dim, int):
48 max_dim = int(max_dim)
49 facx = max_dim * 1. / max(dx, 1)
50 facy = max_dim * 1. / max(dy, 1)
51 factor = min(facx, facy)
52 if factor is not None:
53 if not isinstance(factor, float):
54 factor = int(factor)
55 dx = int(dx * factor + 0.5)
56 dy = int(dy * factor + 0.5)
57 obj = obj.resize((dx, dy))
58 if out_file is not None:
59 if fLOG is not None:
60 fLOG(f"Writing '{out_file}' dim=({dx},{dy}).")
61 obj.save(out_file)
62 return obj
65def white_to_transparency(img, out_file=None):
66 """
67 Sets white color as transparency color.
69 @param img image (:epkg:`Pillow`)
70 @param out_file stores the image into this file if not None
71 @return image (:epkg:`Pillow`)
73 Code taken from `Using PIL to make all white pixels transparent?
74 <https://stackoverflow.com/questions/765736/
75 using-pil-to-make-all-white-pixels-transparent>`_.
76 """
77 from PIL import Image
78 if isinstance(img, str):
79 img = Image.open(img)
80 x = numpy.asarray(img.convert('RGBA')).copy()
82 x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8)
84 obj = Image.fromarray(x)
85 if out_file is not None:
86 obj.save(out_file)
87 return obj
90def _optimization_criterion(target, data, weight_above=10, weight_below=1):
91 sizes = {}
92 for row, width in data:
93 if row not in sizes:
94 sizes[row] = 0
95 sizes[row] += width
96 loss = 0
97 for row, size in sizes.items():
98 if size < target:
99 loss += weight_below * (target - size)
100 else:
101 loss += weight_above * (size - target)
102 if row > 0 and sizes.get(row, 0) == 0:
103 loss += weight_below * target
104 return loss
107def _optimization_histogram_order(target, data, weight_above=10,
108 weight_below=1):
109 if len(data) < 6:
110 # we try all permutation
111 rows = [0 for d in data]
112 best_loss = None
113 best_rows = rows.copy()
114 while rows[0] == 0:
115 loss = _optimization_criterion(
116 target, zip(rows, data),
117 weight_above=weight_above,
118 weight_below=weight_below)
119 if best_loss is None or loss < best_loss:
120 best_loss = loss
121 best_rows = rows.copy()
122 i = len(rows) - 1
123 rows[i] += 1
124 while i > 0 and rows[i] >= len(data):
125 rows[i] = 0
126 i -= 1
127 rows[i] += 1
128 return best_rows
130 # generic case
131 data_pos = [0 for i in data]
132 current_row = 0
133 size = data[0]
134 for i in range(1, len(data)):
135 if size + data[i] > target:
136 current_row += 1
137 size = data[i]
138 else:
139 size += data[i]
140 data_pos[i] = current_row
142 return data_pos
145def concat_images(imgs, height=200, width=800,
146 weight_above=10, weight_below=1,
147 background=(0, 0, 0), out_file=None):
148 """
149 Concatenates images into an image with several
150 rows of images.
152 :param imgs: filename or Images (:epkg:`Pillow`)
153 :param height: height of each row (pixels)
154 :param width: width of each row (pixels)
155 :param weight_above: loss when a line is too long
156 :param weight_below: loss when a line is too short
157 :param background: background color
158 :param out_file: stores the image into this file if not None
159 :return: Image (:epkg:`Pillow`)
160 """
161 from PIL import Image
162 images = []
163 for img in imgs:
164 if isinstance(img, str):
165 images.append(Image.open(img))
166 else:
167 images.append(img)
169 # zoom
170 images = [zoom_img(img, factor=height * 1.0 / img.size[1])
171 for img in images]
173 # optimization
174 data = [img.size[0] for img in images]
175 pos = _optimization_histogram_order(
176 width, data, weight_above=weight_above, weight_below=weight_below)
178 # concat
179 n_rows = max(pos) + 1
180 img_height = n_rows * height
182 new_image = Image.new('RGB', (width, img_height), background)
183 x_row = {}
184 for row, img in zip(pos, images):
185 if row not in x_row:
186 x_row[row] = 0
187 w = x_row[row]
188 new_image.paste(img, (w, row * height))
189 x_row[row] += img.size[0]
190 if out_file is not None:
191 new_image.save(out_file)
192 return new_image