4
4
"""
5
5
6
6
from io import BytesIO
7
- from math import ceil
7
+ import math
8
8
import os
9
9
import logging
10
10
from pathlib import Path
@@ -160,6 +160,26 @@ def flush_images():
160
160
flush_images ()
161
161
162
162
163
+ def _resample (
164
+ image_obj , data , out_shape , transform , * , resample = None , alpha = 1 ):
165
+ """
166
+ Convenience wrapper around `._image.resample` to resample *data* to
167
+ *out_shape* (with a third dimension if *data* is RGBA) that takes care of
168
+ allocating the output array and fetching the relevant properties from the
169
+ Image object *image_obj*.
170
+ """
171
+ out = np .zeros (out_shape + data .shape [2 :], data .dtype ) # 2D->2D, 3D->3D.
172
+ if resample is None :
173
+ resample = image_obj .get_resample ()
174
+ _image .resample (data , out , transform ,
175
+ _interpd_ [image_obj .get_interpolation ()],
176
+ resample ,
177
+ alpha ,
178
+ image_obj .get_filternorm (),
179
+ image_obj .get_filterrad ())
180
+ return out
181
+
182
+
163
183
def _rgb_to_rgba (A ):
164
184
"""
165
185
Convert an RGB image to RGBA, as required by the image resample C++
@@ -320,32 +340,30 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
320
340
- clipped_bbox .y0 )
321
341
.scale (magnification , magnification ))
322
342
323
- # So that the image is aligned with the edge of the axes, we want
324
- # to round up the output width to the next integer. This also
325
- # means scaling the transform just slightly to account for the
326
- # extra subpixel.
343
+ # So that the image is aligned with the edge of the axes, we want to
344
+ # round up the output width to the next integer. This also means
345
+ # scaling the transform slightly to account for the extra subpixel.
327
346
if (t .is_affine and round_to_pixel_border and
328
347
(out_width_base % 1.0 != 0.0 or out_height_base % 1.0 != 0.0 )):
329
- out_width = int ( ceil (out_width_base ) )
330
- out_height = int ( ceil (out_height_base ) )
348
+ out_width = math . ceil (out_width_base )
349
+ out_height = math . ceil (out_height_base )
331
350
extra_width = (out_width - out_width_base ) / out_width_base
332
351
extra_height = (out_height - out_height_base ) / out_height_base
333
352
t += Affine2D ().scale (1.0 + extra_width , 1.0 + extra_height )
334
353
else :
335
354
out_width = int (out_width_base )
336
355
out_height = int (out_height_base )
356
+ out_shape = (out_height , out_width )
337
357
338
358
if not unsampled :
339
- if A .ndim not in (2 , 3 ):
340
- raise ValueError ("Invalid shape {} for image data"
341
- .format (A .shape ))
359
+ if not (A .ndim == 2 or A .ndim == 3 and A .shape [- 1 ] in (3 , 4 )):
360
+ raise ValueError (f"Invalid shape { A .shape } for image data" )
342
361
343
362
if A .ndim == 2 :
344
363
# if we are a 2D array, then we are running through the
345
364
# norm + colormap transformation. However, in general the
346
365
# input data is not going to match the size on the screen so we
347
366
# have to resample to the correct number of pixels
348
- # need to
349
367
350
368
# TODO slice input array first
351
369
inp_dtype = A .dtype
@@ -363,18 +381,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
363
381
# Cast to float64
364
382
if A .dtype not in (np .float32 , np .float16 ):
365
383
if A .dtype != np .float64 :
366
- cbook ._warn_external ("Casting input data from "
367
- "'{0 }' to 'float64' for "
368
- " imshow". format ( A . dtype ) )
384
+ cbook ._warn_external (
385
+ f"Casting input data from ' { A . dtype } ' to "
386
+ f"'float64' for imshow" )
369
387
scaled_dtype = np .float64
370
388
else :
371
389
# probably an integer of some type.
372
390
da = a_max .astype (np .float64 ) - a_min .astype (np .float64 )
373
- if da > 1e8 :
374
- # give more breathing room if a big dynamic range
375
- scaled_dtype = np .float64
376
- else :
377
- scaled_dtype = np .float32
391
+ # give more breathing room if a big dynamic range
392
+ scaled_dtype = np .float64 if da > 1e8 else np .float32
378
393
379
394
# scale the input data to [.1, .9]. The Agg
380
395
# interpolators clip to [0, 1] internally, use a
@@ -383,16 +398,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
383
398
# over / under.
384
399
# This may introduce numeric instabilities in very broadly
385
400
# scaled data
386
- A_scaled = np . empty ( A . shape , dtype = scaled_dtype )
387
- A_scaled [:] = A
401
+ # Always copy, and don't allow array subtypes.
402
+ A_scaled = np . array ( A , dtype = scaled_dtype )
388
403
# clip scaled data around norm if necessary.
389
404
# This is necessary for big numbers at the edge of
390
405
# float64's ability to represent changes. Applying
391
406
# a norm first would be good, but ruins the interpolation
392
407
# of over numbers.
393
408
self .norm .autoscale_None (A )
394
- dv = (np .float64 (self .norm .vmax ) -
395
- np .float64 (self .norm .vmin ))
409
+ dv = np .float64 (self .norm .vmax ) - np .float64 (self .norm .vmin )
396
410
vmid = self .norm .vmin + dv / 2
397
411
fact = 1e7 if scaled_dtype == np .float64 else 1e4
398
412
newmin = vmid - dv * fact
@@ -406,7 +420,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
406
420
else :
407
421
a_max = np .float64 (newmax )
408
422
if newmax is not None or newmin is not None :
409
- A_scaled = np .clip (A_scaled , newmin , newmax )
423
+ np .clip (A_scaled , newmin , newmax , out = A_scaled )
410
424
411
425
A_scaled -= a_min
412
426
# a_min and a_max might be ndarray subclasses so use
@@ -417,18 +431,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
417
431
if a_min != a_max :
418
432
A_scaled /= ((a_max - a_min ) / 0.8 )
419
433
A_scaled += 0.1
420
- A_resampled = np .zeros ((out_height , out_width ),
421
- dtype = A_scaled .dtype )
422
434
# resample the input data to the correct resolution and shape
423
- _image .resample (A_scaled , A_resampled ,
424
- t ,
425
- _interpd_ [self .get_interpolation ()],
426
- self .get_resample (), 1.0 ,
427
- self .get_filternorm (),
428
- self .get_filterrad ())
429
-
430
- # we are done with A_scaled now, remove from namespace
431
- # to be sure!
435
+ A_resampled = _resample (self , A_scaled , out_shape , t )
436
+
437
+ # done with A_scaled now, remove from namespace to be sure!
432
438
del A_scaled
433
439
# un-scale the resampled data to approximately the
434
440
# original range things that interpolated to above /
@@ -443,73 +449,35 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
443
449
if isinstance (self .norm , mcolors .NoNorm ):
444
450
A_resampled = A_resampled .astype (A .dtype )
445
451
446
- mask = np .empty (A .shape , dtype = np .float32 )
447
- if A .mask .shape == A .shape :
448
- # this is the case of a nontrivial mask
449
- mask [:] = np .where (A .mask , np .float32 (np .nan ),
450
- np .float32 (1 ))
451
- else :
452
- mask [:] = 1
453
-
452
+ mask = (np .where (A .mask , np .float32 (np .nan ), np .float32 (1 ))
453
+ if A .mask .shape == A .shape # nontrivial mask
454
+ else np .ones_like (A , np .float32 ))
454
455
# we always have to interpolate the mask to account for
455
456
# non-affine transformations
456
- out_mask = np .zeros ((out_height , out_width ),
457
- dtype = mask .dtype )
458
- _image .resample (mask , out_mask ,
459
- t ,
460
- _interpd_ [self .get_interpolation ()],
461
- True , 1 ,
462
- self .get_filternorm (),
463
- self .get_filterrad ())
464
- # we are done with the mask, delete from namespace to be sure!
457
+ out_alpha = _resample (self , mask , out_shape , t , resample = True )
458
+ # done with the mask now, delete from namespace to be sure!
465
459
del mask
466
- # Agg updates the out_mask in place. If the pixel has
467
- # no image data it will not be updated (and still be 0
468
- # as we initialized it), if input data that would go
469
- # into that output pixel than it will be `nan`, if all
470
- # the input data for a pixel is good it will be 1, and
471
- # if there is _some_ good data in that output pixel it
472
- # will be between [0, 1] (such as a rotated image).
473
-
474
- out_alpha = np .array (out_mask )
475
- out_mask = np .isnan (out_mask )
460
+ # Agg updates out_alpha in place. If the pixel has no image
461
+ # data it will not be updated (and still be 0 as we initialized
462
+ # it), if input data that would go into that output pixel than
463
+ # it will be `nan`, if all the input data for a pixel is good
464
+ # it will be 1, and if there is _some_ good data in that output
465
+ # pixel it will be between [0, 1] (such as a rotated image).
466
+ out_mask = np .isnan (out_alpha )
476
467
out_alpha [out_mask ] = 1
477
-
478
468
# mask and run through the norm
479
469
output = self .norm (np .ma .masked_array (A_resampled , out_mask ))
480
470
else :
481
- # Always convert to RGBA, even if only RGB input
482
471
if A .shape [2 ] == 3 :
483
472
A = _rgb_to_rgba (A )
484
- elif A .shape [2 ] != 4 :
485
- raise ValueError ("Invalid shape {} for image data"
486
- .format (A .shape ))
487
-
488
- output = np .zeros ((out_height , out_width , 4 ), dtype = A .dtype )
489
- output_a = np .zeros ((out_height , out_width ), dtype = A .dtype )
490
-
491
473
alpha = self .get_alpha ()
492
474
if alpha is None :
493
475
alpha = 1
494
-
495
- #resample alpha channel
496
- alpha_channel = A [..., 3 ]
497
- _image .resample (
498
- alpha_channel , output_a , t ,
499
- _interpd_ [self .get_interpolation ()],
500
- self .get_resample (), alpha ,
501
- self .get_filternorm (), self .get_filterrad ())
502
-
503
- #resample rgb channels
504
- A = _rgb_to_rgba (A [..., :3 ])
505
- _image .resample (
506
- A , output , t ,
507
- _interpd_ [self .get_interpolation ()],
508
- self .get_resample (), alpha ,
509
- self .get_filternorm (), self .get_filterrad ())
510
-
511
- #recombine rgb and alpha channels
512
- output [..., 3 ] = output_a
476
+ output_alpha = _resample ( # resample alpha channel
477
+ self , A [..., 3 ], out_shape , t , alpha = alpha )
478
+ output = _resample ( # resample rgb channels
479
+ self , _rgb_to_rgba (A [..., :3 ]), out_shape , t , alpha = alpha )
480
+ output [..., 3 ] = output_alpha # recombine rgb and alpha
513
481
514
482
# at this point output is either a 2D array of normed data
515
483
# (of int or float)
0 commit comments