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

1""" 

2@file 

3@brief Helpers around images. 

4""" 

5import os 

6import glob 

7import numpy 

8 

9 

10def zoom_img(img, factor=1., max_dim=None, out_file=None, fLOG=None): 

11 """ 

12 Zooms an image. 

13 

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 

63 

64 

65def white_to_transparency(img, out_file=None): 

66 """ 

67 Sets white color as transparency color. 

68 

69 @param img image (:epkg:`Pillow`) 

70 @param out_file stores the image into this file if not None 

71 @return image (:epkg:`Pillow`) 

72 

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() 

81 

82 x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) 

83 

84 obj = Image.fromarray(x) 

85 if out_file is not None: 

86 obj.save(out_file) 

87 return obj 

88 

89 

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 

105 

106 

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 

129 

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 

141 

142 return data_pos 

143 

144 

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. 

151 

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) 

168 

169 # zoom 

170 images = [zoom_img(img, factor=height * 1.0 / img.size[1]) 

171 for img in images] 

172 

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) 

177 

178 # concat 

179 n_rows = max(pos) + 1 

180 img_height = n_rows * height 

181 

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