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