-
Notifications
You must be signed in to change notification settings - Fork 29
/
environmentmap.py
627 lines (488 loc) · 22.9 KB
/
environmentmap.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
import os
import pathlib
from copy import deepcopy
from decimal import Decimal
import numpy as np
from scipy.ndimage.interpolation import map_coordinates, zoom
from skimage.transform import resize_local_mean, downscale_local_mean
from hdrio import imread
from .tetrahedronSolidAngle import tetrahedronSolidAngle
from .projections import *
from .xmlhelper import EnvmapXMLParser
from .rotations import rotx, roty, rotz
SUPPORTED_FORMATS = [
'angular',
'skyangular',
'latlong',
'skylatlong',
'sphere',
'cube',
]
#From Dan:
# I've generated these using the monochromatic albedo values from here:
# http://agsys.cra-cin.it/tools/solarradiation/help/Albedo.html
# (they cite some books as references). Since these monochromatic,
# I got unscaled r, g, b values from internet textures and scaled them
# so that their mean matches the expected monochromatic albedo. Hopefully
# this is a valid thing to do.
GROUND_ALBEDOS = {
"GreenGrass": np.array([ 0.291801, 0.344855, 0.113344 ]).T,
"FreshSnow": np.array([ 0.797356, 0.835876, 0.916767 ]).T,
"Asphalt": np.array([ 0.148077, 0.150000, 0.151923 ]).T,
}
def isPath(var):
return isinstance(var, str) or isinstance(var, pathlib.PurePath)
class EnvironmentMap:
def __init__(self, im, format_=None, copy=True, channels=3):
"""
Creates an EnvironmentMap.
:param im: Image path (str, pathlib.Path) or data (np.ndarray) representing
an EnvironmentMap, or the height (int) of an empty EnvironmentMap.
:param format_: EnvironmentMap format. Can be any from SUPPORTED_FORMATS.
:param copy: When a numpy array is given, should it be copied.
:param channels: Number of channels (e.g., 1=grayscale, 3=color).
:type im: str, Path, int, np.ndarray
:type format_: str
:type copy: bool
"""
if not format_ and isPath(im):
filename = os.path.splitext(str(im))[0]
metadata = EnvmapXMLParser("{}.meta.xml".format(filename))
format_ = metadata.getFormat()
assert format_ is not None, (
"Please provide format (metadata file not found).")
assert format_.lower() in SUPPORTED_FORMATS, (
"Unknown format: {}".format(format_))
self.format_ = format_.lower()
if isPath(im):
# We received the filename
self.data = imread(str(im))
elif isinstance(im, int):
# We received a single scalar
if self.format_ == 'latlong':
self.data = np.zeros((im, im*2, channels))
elif self.format_ == 'skylatlong':
self.data = np.zeros((im, im*4, channels))
elif self.format_ == 'cube':
self.data = np.zeros((im, round(3/4*im), channels))
else:
self.data = np.zeros((im, im, channels))
elif type(im).__module__ == np.__name__:
# We received a numpy array
self.data = np.asarray(im, dtype='double')
if copy:
self.data = self.data.copy()
else:
raise Exception('Could not understand input. Please provide a '
'filename (str), an height (integer) or an image '
'(np.ndarray).')
self.backgroundColor = np.zeros(self.data.shape[-1])
self.validate()
def validate(self):
# Ensure the envmap is valid
if self.format_ in ['sphere', 'angular', 'skysphere', 'skyangular']:
assert self.data.shape[0] == self.data.shape[1], (
"Sphere/Angular formats must have the same width/height")
elif self.format_ == 'latlong':
assert 2*self.data.shape[0] == self.data.shape[1], (
"LatLong format width should be twice the height")
elif self.format_ == 'skylatlong':
assert 4*self.data.shape[0] == self.data.shape[1], (
"SkyLatLong format width should be four times the height")
assert self.data.ndim == 3, "Expected 3-dim array. For grayscale, use [h,w,1]."
@classmethod
def fromSkybox(cls, top, bottom, left, right, front, back):
"""Create an environment map from skybox (cube) captures. Six images
must be provided, one for each side of the virtual cube. This function
will return an EnvironmentMap object.
All the images must be square (width==height), have the same size
and number of channels.
"""
basedim = top.shape[0]
channels = 1 if len(top.shape) == 2 else top.shape[2]
cube = np.zeros((basedim*4, basedim*3, channels), dtype=top.dtype)
cube[0:basedim, basedim:2*basedim] = top
cube[basedim:2*basedim, basedim:2*basedim] = front
cube[basedim:2*basedim, 2*basedim:3*basedim] = right
cube[3*basedim:4*basedim, basedim:2*basedim] = np.fliplr(np.flipud(back))
cube[1*basedim:2*basedim, 0:basedim] = left
cube[2*basedim:3*basedim, basedim:2*basedim] = bottom
# We ensure that there is no visible artifacts at the junction of the images
# due to the interpolation process
cube[0:basedim, basedim-1] = left[0,...] # top-left
cube[0:basedim, 2*basedim] = right[0,...][::-1] # top-right
cube[basedim-1, 2*basedim:3*basedim] = top[:,-1][::-1] #right-top
cube[2*basedim, 2*basedim:3*basedim] = bottom[:,-1] #right-bottom
cube[1*basedim-1, 0:basedim] = top[:,0] # left-top
cube[2*basedim, 0:basedim] = bottom[:,0][::-1] # left-bottom
cube[2*basedim:3*basedim, basedim-1] = left[-1,...][::-1] #bottom-left
cube[2*basedim:3*basedim, 2*basedim] = right[-1,...] #bottom-right
cube[3*basedim:, basedim-1] = left[:,0][::-1] # back-left
cube[3*basedim:, 2*basedim] = right[:,-1][::-1] # back-right
return cls(cube, format_="cube")
def __hash__(self):
"""Provide a hash of the environment map type and size.
Warning: doesn't take into account the data, just the type,
and the size (useful for soldAngles)."""
return hash((self.data.shape, self.format_))
def copy(self):
"""Returns a copy of the current environment map."""
return deepcopy(self)
def solidAngles(self):
"""Computes the solid angle subtended by each pixel."""
# If already computed, take it
if hasattr(self, '_solidAngles') and hash(self) == self._solidAngles_hash:
return self._solidAngles
# Compute coordinates of pixel borders
cols = np.linspace(0, 1, self.data.shape[1] + 1)
rows = np.linspace(0, 1, self.data.shape[0] + 1)
u, v = np.meshgrid(cols, rows)
dx, dy, dz, _ = self.image2world(u, v)
# Split each pixel into two triangles and compute the solid angle
# subtended by the two tetrahedron
a = np.vstack((dx[:-1,:-1].ravel(), dy[:-1,:-1].ravel(), dz[:-1,:-1].ravel()))
b = np.vstack((dx[:-1,1:].ravel(), dy[:-1,1:].ravel(), dz[:-1,1:].ravel()))
c = np.vstack((dx[1:,:-1].ravel(), dy[1:,:-1].ravel(), dz[1:,:-1].ravel()))
d = np.vstack((dx[1:,1:].ravel(), dy[1:,1:].ravel(), dz[1:,1:].ravel()))
omega = tetrahedronSolidAngle(a, b, c)
omega += tetrahedronSolidAngle(b, c, d)
# Get pixel center coordinates
_, _, _, valid = self.worldCoordinates()
omega[~valid.ravel()] = np.nan
self._solidAngles = omega.reshape(self.data.shape[0:2])
self._solidAngles_hash = hash(self)
return self._solidAngles
def imageCoordinates(self):
"""Returns the (u, v) coordinates for each pixel center."""
cols = np.linspace(0, 1, self.data.shape[1]*2 + 1)
rows = np.linspace(0, 1, self.data.shape[0]*2 + 1)
cols = cols[1::2]
rows = rows[1::2]
return [d.astype('float32') for d in np.meshgrid(cols, rows)]
def worldCoordinates(self):
"""Returns the (x, y, z) world coordinates for each pixel center."""
u, v = self.imageCoordinates()
x, y, z, valid = self.image2world(u, v)
return x, y, z, valid
def image2world(self, u, v):
"""Returns the (x, y, z) coordinates in the [-1, 1] interval."""
func = {
'angular': angular2world,
'skyangular': skyangular2world,
'latlong': latlong2world,
'skylatlong': skylatlong2world,
'sphere': sphere2world,
'cube': cube2world,
}.get(self.format_)
return func(u, v)
def world2image(self, x, y, z):
"""Returns the (u, v) coordinates (in the [0, 1] interval)."""
func = {
'angular': world2angular,
'skyangular': world2skyangular,
'latlong': world2latlong,
'skylatlong': world2skylatlong,
'sphere': world2sphere,
'cube': world2cube,
}.get(self.format_)
return func(x, y, z)
def interpolate(self, u, v, valid=None, order=1, filter=True):
""""Interpolate to get the desired pixel values."""
u = u.copy()
v = v.copy()
# Repeat the first and last rows/columns for interpolation purposes
h, w, d = self.data.shape
source = np.empty((h + 2, w + 2, d))
source[1:-1, 1:-1] = self.data
source[0,1:-1] = self.data[0,:]; source[0,0] = self.data[0,0]; source[0,-1] = self.data[0,-1]
source[-1,1:-1] = self.data[-1,:]; source[-1,0] = self.data[-1,0]; source[-1,-1] = self.data[-1,-1]
source[1:-1,0] = self.data[:,0]
source[1:-1,-1] = self.data[:,-1]
# To avoid displacement due to the padding
u += 0.5/self.data.shape[1]
v += 0.5/self.data.shape[0]
target = np.vstack((v.flatten()*self.data.shape[0], u.flatten()*self.data.shape[1]))
data = np.zeros((u.shape[0], u.shape[1], d))
for c in range(d):
map_coordinates(source[:,:,c], target, output=data[:,:,c].reshape(-1), cval=np.nan, order=order, prefilter=filter)
self.data = data
if valid is not None:
self.setBackgroundColor(self.backgroundColor, valid)
return self
def setBackgroundColor(self, color, valid=None):
"""Sets the area defined by ~valid to color."""
if valid is None:
_, _, _, valid = self.worldCoordinates()
assert valid.dtype == 'bool', "`valid` must be a boolean array."
assert valid.shape[:2] == self.data.shape[:2], "`valid` must be the same size as the EnvironmentMap."
self.backgroundColor = np.asarray(color)
if self.backgroundColor.size == 1 and self.data.shape[2] != self.backgroundColor.size:
self.backgroundColor = np.tile(self.backgroundColor, (self.data.shape[2],))
assert self.data.shape[2] == self.backgroundColor.size, "Channel number mismatch when setting background color"
mask = np.invert(valid)
if mask.sum() > 0:
self.data[np.tile(mask[:,:,None], (1, 1, self.data.shape[2]))] = np.tile(self.backgroundColor, (mask.sum(),))
return self
def convertTo(self, targetFormat, targetDim=None):
"""
Convert to another format.
:param targetFormat: Target format.
:param targetDim: Target dimension.
:type targetFormat: string
:type targetFormat: integer
"""
self.validate()
assert targetFormat.lower() in SUPPORTED_FORMATS, (
"Unknown format: {}".format(targetFormat))
if not targetDim:
# By default, number of rows
targetDim = self.data.shape[0]
eTmp = EnvironmentMap(targetDim, targetFormat)
dx, dy, dz, valid = eTmp.worldCoordinates()
u, v = self.world2image(dx, dy, dz)
self.format_ = targetFormat.lower()
self.interpolate(u, v, valid)
return self
def rotate(self, dcm):
"""
Rotate the environment map.
:param input: Rotation information (currently only 3x3 numpy matrix)
"""
self.validate()
assert type(dcm).__module__ == np.__name__ and dcm.ndim == 2 and dcm.shape == (3, 3)
dx, dy, dz, valid = self.worldCoordinates()
ptR = np.dot(dcm, np.vstack((dx.flatten(), dy.flatten(), dz.flatten())))
dx, dy, dz = ptR[0].reshape(dx.shape), ptR[1].reshape(dy.shape), ptR[2].reshape(dz.shape)
dx = np.clip(dx, -1, 1)
dy = np.clip(dy, -1, 1)
dz = np.clip(dz, -1, 1)
u, v = self.world2image(dx, dy, dz)
self.interpolate(u, v, valid)
return self
def resize(self, targetSize, order=1, debug=False):
"""
Resize the current environnement map to targetSize.
The energy-preserving "pixel mixing" alrogithm is used when downscaling.
`targetSize` is either the desired `height` or `(height, width)`.
`order` is only used when upsampling.
"""
self.validate()
if not isinstance(targetSize, tuple):
if self.format_ == 'latlong':
targetSize = (targetSize, 2*targetSize)
elif self.format_ == 'skylatlong':
targetSize = (targetSize, 4*targetSize)
elif self.format_ == 'cube':
targetSize = (targetSize, round(3/4*targetSize))
else:
targetSize = (targetSize, targetSize)
# downsampling
if targetSize[0] < self.data.shape[0]:
if debug == True:
old_mean = self.data.mean()
# check if integer
if (Decimal(self.data.shape[0]) / Decimal(targetSize[0])) % 1 == 0:
if debug is True:
print("integer resize")
fac = self.data.shape[0] // targetSize[0]
self.data = downscale_local_mean(self.data, (fac, fac, 1))
else:
self.data = resize_local_mean(self.data, targetSize, grid_mode=True, preserve_range=True)
if debug == True:
print("Energy difference in resize: {:.04f}".format(self.data.mean()/old_mean - 1.))
return self
# upsampling
_size = []
for i in range(2):
_size.append(targetSize[i] / self.data.shape[i] if targetSize[i] > 1. else targetSize[i])
_size.append(1.0)
self.data = zoom(self.data, _size, order=order)
return self
def toIntensity(self, mode="ITU BT.709", colorspace="linear"):
"""
Converts the environment map to grayscale.
`mode` can be either :
"ITU BT.601" (luma in sRGB),
"ITU BT.709" (assumes the envmap is linear),
mean (mean of the channels)
`colorspace`: either "sRGB" or "linear"
"""
self.validate()
if self.data.shape[2] != 3:
print("Envmap does not have 3 channels. This function won't do anything.")
return self
if colorspace.lower() == "sRGB":
if self.data.max() > 1.:
raise Exception("Error during sRGB to linear conversion: data is > 1. Please linearize "
"the data beforehand or normalize it [0, 1].")
self.data = self.data**2.2
if mode == "ITU BT.601":
self.data = 0.299*self.data[...,0] + 0.587*self.data[...,1] + 0.114*self.data[...,2]
self.data = self.data[:,:,np.newaxis]
elif mode == "ITU BT.709":
self.data = 0.2126*self.data[...,0] + 0.7152*self.data[...,1] + 0.0722*self.data[...,2]
self.data = self.data[:,:,np.newaxis]
elif mode == "mean":
self.data = np.mean(self.data, axis=2, keepdims=True)
if colorspace.lower() == "sRGB":
self.data = self.data**(1./2.2)
return self
def getHemisphere(self, normal, channels=True):
"""
normal:
"""
normal = np.asarray(normal, dtype=np.float32).reshape((-1))
assert normal.size == 3, "unknown normal shape, should have 3 elements"
normal /= np.linalg.norm(normal)
x, y, z, _ = self.worldCoordinates()
xyz = np.dstack((x, y, z))
mask = xyz.dot(normal) > 0
if channels == False:
return mask
return np.tile(mask[:,:,None], (1, 1, self.data.shape[2]))
def setHemisphereValue(self, normal, value):
"""Sets an whole hemisphere defined by `normal` to a given `value`."""
mask = self.getHemisphere(normal, channels=True)
value = np.asarray(value)
assert value.size in (self.data.shape[2], 1), ("Cannot understand value: should "
"be size 1 or channels ({})".format(self.data.shape[2]))
if value.size == 1 and self.data.shape[2] != value.size:
value = np.tile(value, (self.data.shape[2],))
self.data[mask] = np.tile(value, (mask.sum()//self.data.shape[2],))
return self
def getMeanLightVectors(self, normals):
"""Compute the mean light vector of the environment map for the normals given.
Normals should be 3xN.
Output is 3xN.
"""
self.validate()
normals = np.asarray(normals)
normals /= np.linalg.norm(normals, 1)
solidAngles = self.solidAngles()
solidAngles /= np.nansum(solidAngles) # Normalize to 1
x, y, z, _ = self.worldCoordinates()
xyz = np.dstack((x, y, z))
visibility = xyz.dot(normals) > 0
intensity = deepcopy(self).toIntensity()
meanlight = visibility * intensity.data * solidAngles[:,:,np.newaxis]
meanlight = np.nansum(xyz[...,np.newaxis] * meanlight[...,np.newaxis].transpose((0,1,3,2)), (0, 1))
return meanlight
def embed(self, vfov, rotation_matrix, image):
"""Projects an image onto the environment map.
:vfov: Vertical Field of View (degrees).
:rotation_matrix: Camera rotation matrix.
:image: The image to project."""
self.validate()
targetDim = self.data.shape[0]
targetFormat = self.format_
eTmp = EnvironmentMap(targetDim, targetFormat)
dx, dy, dz, valid = eTmp.worldCoordinates()
ar = image.shape[1]/image.shape[0]
vfov_half_rad = vfov/2.*np.pi/180.
hfov_half_rad = np.arctan(np.tan(vfov_half_rad)*ar)
fx = 0.5/np.tan(hfov_half_rad)
fy = 0.5/np.tan(vfov_half_rad)
u0 = 0.5
v0 = 0.5
# world2image for a camera
K = np.array([[fx, 0, u0],
[0, fy, v0],
[0, 0, 1]])
M = K.dot(rotation_matrix)
xyz = np.dstack((dx, dy, dz)).reshape((-1, 3)).T
# mask behind the camera
forward_vector = rotation_matrix.T.dot(np.array([0, 0, -1]).T)
mask = forward_vector.dot(xyz) <= 0
xyz[:,mask] = np.inf
uv = M.dot(xyz)
u = (uv[0,:]/uv[2,:]).reshape(self.data.shape[:2])
v = (uv[1,:]/uv[2,:]).reshape(self.data.shape[:2])
self.format_ = targetFormat.lower()
self.data = image.copy()[:,::-1,...]
self.interpolate(u, v, valid)
return self
def project(self, vfov, rotation_matrix, ar=4./3., resolution=(640, 480),
projection="perspective", mode="normal"):
"""Perform a projection onto a plane ("_simulates_" a camera).
Note: this function does not modify the foreshortening present in the
environment map.
:vfov: Vertical Field of View (degrees).
:rotation_matrix: Camera rotation matrix.
:ar: Aspect ratio (width / height), defaults to 4/3.
:resolution: Output size in (cols, rows), defaults to (640,480).
:projection: perspective (default) or orthographic.
:mode: "normal": perform crop (default)
"mask": show pixel mask in the envmap,
"normal+uv": returns (crop, u, v), where (u,v) are the coordinates
of the crop."""
self.validate()
coords = self._cameraCoordinates(vfov, rotation_matrix, ar,
resolution, projection, mode)
if mode == "mask":
return coords
target = self.copy()
if target.format_ != "latlong":
target = target.convertTo("LatLong")
# Get image coordinates from world sphere coordinates
u, v = target.world2image(coords[0,:], coords[1,:], coords[2,:])
u, v = u.reshape(resolution[::-1]), v.reshape(resolution[::-1])
crop = target.interpolate(u, v).data
if mode == "normal+uv":
return crop, u, v
return crop
def _cameraCoordinates(self, vfov, rotation_matrix, ar=4./3., resolution=(640, 480),
projection="perspective", mode="normal"):
if mode not in ("normal", "mask", "normal+uv"):
raise Exception("Unknown mode: {}.".format(mode))
if projection == "orthographic":
vfov = np.arctan(np.sin(vfov*np.pi/180.))*180/np.pi
# aspect ratio in pixels
hfov = 2 * np.arctan(np.tan(vfov*np.pi/180./2)*ar)*180/np.pi
# Project angle on the sphere to the +Z plane (distance=1 from the camera)
mu = np.tan(hfov/2.*np.pi/180.)
mv = np.tan(vfov/2.*np.pi/180.)
if mode == "mask":
x, y, z, _ = self.worldCoordinates()
xy = np.sqrt( (x**2 + y**2) / (-x**2 - y**2 + 1) )
theta = np.arctan2(x, y)
x = xy*np.sin(theta)
y = xy*np.cos(theta)
hmask = (x > -mu) & (x < mu)
vmask = (y > -mv) & (y < mv)
dmask = z < 0
mask = hmask & vmask & dmask
e = EnvironmentMap(mask[:,:,np.newaxis], self.format_)
e.rotate(rotation_matrix)
mask = e.data[:,:,0]
return mask
# Uniform sampling on the plane
dy = np.linspace(mv, -mv, resolution[1])
dx = np.linspace(-mu, mu, resolution[0])
x, y = np.meshgrid(dx, dy)
# Compute unit length vector (project back to sphere) from the plane
x, y = x.ravel(), y.ravel()
if projection == "perspective":
xy = np.sqrt( (x**2 + y**2) / (x**2 + y**2 + 1) )
theta = np.arctan2(x, y)
x = xy*np.sin(theta)
y = xy*np.cos(theta)
elif projection == "orthographic":
pass
else:
raise Exception("Unknown projection: {}.".format(projection))
z = -np.sqrt(1 - (x**2 + y**2))
coords = np.vstack((x, y, z))
# Perform rotation
coords = rotation_matrix.T.dot(coords)
return coords
def rotation_matrix(azimuth, elevation, roll=0):
"""Returns a camera rotation matrix.
:azimuth: left (negative) to right (positive) [rad]
:elevation: upward (negative) to downward (positive) [rad]
:roll: counter-clockwise (negative) to clockwise (positive) [rad]"""
return rotz(roll).dot(rotx(elevation)).dot(roty(-azimuth))
def downscaleEnvmap(nenvmap, sao, sat, times):
"""Deprecated"""
print("This function is deprecated and will be removed in an ulterior version of skylibs.")
sz = sat.shape[0]
return nenvmap.resize(sz)