1""" 2This module deals with making images (np arrays). It provides drawing 3methods that are difficult to do with the existing Python libraries. 4""" 5 6import numpy as np 7 8 9def blit(im1, im2, pos=None, mask=None, ismask=False): 10 """ Blit an image over another. 11 12 Blits ``im1`` on ``im2`` as position ``pos=(x,y)``, using the 13 ``mask`` if provided. If ``im1`` and ``im2`` are mask pictures 14 (2D float arrays) then ``ismask`` must be ``True``. 15 """ 16 if pos is None: 17 pos = [0, 0] 18 19 # xp1,yp1,xp2,yp2 = blit area on im2 20 # x1,y1,x2,y2 = area of im1 to blit on im2 21 xp, yp = pos 22 x1 = max(0, -xp) 23 y1 = max(0, -yp) 24 h1, w1 = im1.shape[:2] 25 h2, w2 = im2.shape[:2] 26 xp2 = min(w2, xp + w1) 27 yp2 = min(h2, yp + h1) 28 x2 = min(w1, w2 - xp) 29 y2 = min(h1, h2 - yp) 30 xp1 = max(0, xp) 31 yp1 = max(0, yp) 32 33 if (xp1 >= xp2) or (yp1 >= yp2): 34 return im2 35 36 blitted = im1[y1:y2, x1:x2] 37 38 new_im2 = +im2 39 40 if mask is None: 41 new_im2[yp1:yp2, xp1:xp2] = blitted 42 else: 43 mask = mask[y1:y2, x1:x2] 44 if len(im1.shape) == 3: 45 mask = np.dstack(3 * [mask]) 46 blit_region = new_im2[yp1:yp2, xp1:xp2] 47 new_im2[yp1:yp2, xp1:xp2] = (1.0 * mask * blitted + (1.0 - mask) * blit_region) 48 49 return new_im2.astype('uint8') if (not ismask) else new_im2 50 51 52 53def color_gradient(size,p1,p2=None,vector=None, r=None, col1=0,col2=1.0, 54 shape='linear', offset = 0): 55 """Draw a linear, bilinear, or radial gradient. 56 57 The result is a picture of size ``size``, whose color varies 58 gradually from color `col1` in position ``p1`` to color ``col2`` 59 in position ``p2``. 60 61 If it is a RGB picture the result must be transformed into 62 a 'uint8' array to be displayed normally: 63 64 65 Parameters 66 ------------ 67 68 size 69 Size (width, height) in pixels of the final picture/array. 70 71 p1, p2 72 Coordinates (x,y) in pixels of the limit point for ``col1`` 73 and ``col2``. The color 'before' ``p1`` is ``col1`` and it 74 gradually changes in the direction of ``p2`` until it is ``col2`` 75 when it reaches ``p2``. 76 77 vector 78 A vector [x,y] in pixels that can be provided instead of ``p2``. 79 ``p2`` is then defined as (p1 + vector). 80 81 col1, col2 82 Either floats between 0 and 1 (for gradients used in masks) 83 or [R,G,B] arrays (for colored gradients). 84 85 shape 86 'linear', 'bilinear', or 'circular'. 87 In a linear gradient the color varies in one direction, 88 from point ``p1`` to point ``p2``. 89 In a bilinear gradient it also varies symetrically form ``p1`` 90 in the other direction. 91 In a circular gradient it goes from ``col1`` to ``col2`` in all 92 directions. 93 94 offset 95 Real number between 0 and 1 indicating the fraction of the vector 96 at which the gradient actually starts. For instance if ``offset`` 97 is 0.9 in a gradient going from p1 to p2, then the gradient will 98 only occur near p2 (before that everything is of color ``col1``) 99 If the offset is 0.9 in a radial gradient, the gradient will 100 occur in the region located between 90% and 100% of the radius, 101 this creates a blurry disc of radius d(p1,p2). 102 103 Returns 104 -------- 105 106 image 107 An Numpy array of dimensions (W,H,ncolors) of type float 108 representing the image of the gradient. 109 110 111 Examples 112 --------- 113 114 >>> grad = color_gradient(blabla).astype('uint8') 115 116 """ 117 118 # np-arrayize and change x,y coordinates to y,x 119 w,h = size 120 121 col1 = np.array(col1).astype(float) 122 col2 = np.array(col2).astype(float) 123 124 if shape == 'bilinear': 125 if vector is None: 126 vector = np.array(p2) - np.array(p1) 127 128 m1, m2 = [ color_gradient(size, p1, vector=v, col1 = 1.0, col2 = 0, 129 shape = 'linear', offset= offset) 130 for v in [vector,-vector]] 131 132 arr = np.maximum(m1, m2) 133 if col1.size > 1: 134 arr = np.dstack(3*[arr]) 135 return arr*col1 + (1-arr)*col2 136 137 138 p1 = np.array(p1[::-1]).astype(float) 139 140 if vector is None and p2: 141 p2 = np.array(p2[::-1]) 142 vector = p2-p1 143 else: 144 vector = np.array(vector[::-1]) 145 p2 = p1 + vector 146 147 if vector: 148 norm = np.linalg.norm(vector) 149 150 M = np.dstack(np.meshgrid(range(w),range(h))[::-1]).astype(float) 151 152 if shape == 'linear': 153 154 n_vec = vector/norm**2 # norm 1/norm(vector) 155 156 p1 = p1 + offset*vector 157 arr = (M- p1).dot(n_vec)/(1-offset) 158 arr = np.minimum(1,np.maximum(0,arr)) 159 if col1.size > 1: 160 arr = np.dstack(3*[arr]) 161 return arr*col1 + (1-arr)*col2 162 163 elif shape == 'radial': 164 if r is None: 165 r = norm 166 167 if r == 0: 168 arr = np.ones((h,w)) 169 else: 170 arr = (np.sqrt(((M - p1) ** 2).sum(axis=2))) - offset * r 171 arr = arr / ((1-offset)*r) 172 arr = np.minimum(1.0, np.maximum(0, arr)) 173 174 if col1.size > 1: 175 arr = np.dstack(3*[arr]) 176 return (1-arr)*col1 + arr*col2 177 178 179def color_split(size,x=None,y=None,p1=None,p2=None,vector=None, 180 col1=0,col2=1.0, grad_width=0): 181 """Make an image splitted in 2 colored regions. 182 183 Returns an array of size ``size`` divided in two regions called 1 and 184 2 in wht follows, and which will have colors col& and col2 185 respectively. 186 187 Parameters 188 ----------- 189 190 x: (int) 191 If provided, the image is splitted horizontally in x, the left 192 region being region 1. 193 194 y: (int) 195 If provided, the image is splitted vertically in y, the top region 196 being region 1. 197 198 p1,p2: 199 Positions (x1,y1),(x2,y2) in pixels, where the numbers can be 200 floats. Region 1 is defined as the whole region on the left when 201 going from ``p1`` to ``p2``. 202 203 p1, vector: 204 ``p1`` is (x1,y1) and vector (v1,v2), where the numbers can be 205 floats. Region 1 is then the region on the left when starting 206 in position ``p1`` and going in the direction given by ``vector``. 207 208 gradient_width 209 If not zero, the split is not sharp, but gradual over a region of 210 width ``gradient_width`` (in pixels). This is preferable in many 211 situations (for instance for antialiasing). 212 213 214 Examples 215 --------- 216 217 >>> size = [200,200] 218 >>> # an image with all pixels with x<50 =0, the others =1 219 >>> color_split(size, x=50, col1=0, col2=1) 220 >>> # an image with all pixels with y<50 red, the others green 221 >>> color_split(size, x=50, col1=[255,0,0], col2=[0,255,0]) 222 >>> # An image splitted along an arbitrary line (see below) 223 >>> color_split(size, p1=[20,50], p2=[25,70] col1=0, col2=1) 224 225 """ 226 227 if grad_width or ( (x is None) and (y is None)): 228 if p2 is not None: 229 vector = (np.array(p2) - np.array(p1)) 230 elif x is not None: 231 vector = np.array([0,-1.0]) 232 p1 = np.array([x, 0]) 233 elif y is not None: 234 vector = np.array([1.0, 0.0]) 235 p1 = np.array([0,y]) 236 237 x,y = vector 238 vector = np.array([y,-x]).astype('float') 239 norm = np.linalg.norm(vector) 240 vector = max(0.1, grad_width) * vector / norm 241 return color_gradient(size,p1,vector=vector, 242 col1 = col1, col2 = col2, shape='linear') 243 else: 244 w, h = size 245 shape = (h, w) if np.isscalar(col1) else (h, w, len(col1)) 246 arr = np.zeros(shape) 247 if x: 248 arr[:,:x] = col1 249 arr[:,x:] = col2 250 elif y: 251 arr[:y] = col1 252 arr[y:] = col2 253 return arr 254 255 # if we are here, it means we didn't exit with a proper 'return' 256 print( "Arguments in color_split not understood !" ) 257 raise 258 259def circle(screensize, center, radius, col1=1.0, col2=0, blur=1): 260 """ Draw an image with a circle. 261 262 Draws a circle of color ``col1``, on a background of color ``col2``, 263 on a screen of size ``screensize`` at the position ``center=(x,y)``, 264 with a radius ``radius`` but slightly blurred on the border by ``blur`` 265 pixels 266 """ 267 offset = 1.0*(radius-blur)/radius if radius else 0 268 return color_gradient(screensize,p1=center,r=radius, col1=col1, 269 col2=col2, shape='radial', offset=offset) 270