33operations.
44"""
55
6- import math
76import os
87import logging
98from pathlib import Path
@@ -207,12 +206,24 @@ def _resample(
207206 out = np .zeros (out_shape + data .shape [2 :], data .dtype ) # 2D->2D, 3D->3D.
208207 if resample is None :
209208 resample = image_obj .get_resample ()
209+
210+ # When an output pixel falls exactly on the edge between two input pixels, the Agg
211+ # resampler will use the right input pixel as the nearest neighbor. We want the
212+ # left input pixel to be chosen instead, so we flip the supplied transform.
213+ if interpolation == 'nearest' :
214+ transform += Affine2D ().translate (- out .shape [1 ], - out .shape [0 ]).scale (- 1 , - 1 )
215+
210216 _image .resample (data , out , transform ,
211217 _interpd_ [interpolation ],
212218 resample ,
213219 alpha ,
214220 image_obj .get_filternorm (),
215221 image_obj .get_filterrad ())
222+
223+ # Because we flipped the supplied transform, we then flip the output image back.
224+ if interpolation == 'nearest' :
225+ out = np .flip (out , axis = (0 , 1 ))
226+
216227 return out
217228
218229
@@ -393,10 +404,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
393404 if clipped_bbox is None :
394405 return None , 0 , 0 , None
395406
396- out_width_base = clipped_bbox .width * magnification
397- out_height_base = clipped_bbox .height * magnification
407+ # Define the magnified bbox after clipping
408+ magnified_extents = clipped_bbox .extents * magnification
409+ if ((not unsampled ) and round_to_pixel_border ):
410+ # Round to the nearest output pixel
411+ magnified_bbox = Bbox .from_extents ((magnified_extents + 0.5 ).astype (int ))
412+ else :
413+ magnified_bbox = Bbox .from_extents (magnified_extents )
398414
399- if out_width_base == 0 or out_height_base == 0 :
415+ if magnified_bbox . width == 0 or magnified_bbox . height == 0 :
400416 return None , 0 , 0 , None
401417
402418 if self .origin == 'upper' :
@@ -417,23 +433,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
417433
418434 t = (t0
419435 + (Affine2D ()
420- .translate (- clipped_bbox .x0 , - clipped_bbox .y0 )
421- .scale (magnification )))
422-
423- # So that the image is aligned with the edge of the Axes, we want to
424- # round up the output width to the next integer. This also means
425- # scaling the transform slightly to account for the extra subpixel.
426- if ((not unsampled ) and t .is_affine and round_to_pixel_border and
427- (out_width_base % 1.0 != 0.0 or out_height_base % 1.0 != 0.0 )):
428- out_width = math .ceil (out_width_base )
429- out_height = math .ceil (out_height_base )
430- extra_width = (out_width - out_width_base ) / out_width_base
431- extra_height = (out_height - out_height_base ) / out_height_base
432- t += Affine2D ().scale (1.0 + extra_width , 1.0 + extra_height )
433- else :
434- out_width = int (out_width_base )
435- out_height = int (out_height_base )
436- out_shape = (out_height , out_width )
436+ .scale (magnification )
437+ .translate (- magnified_bbox .x0 , - magnified_bbox .y0 )))
438+
439+ out_shape = (int (magnified_bbox .height ), int (magnified_bbox .width ))
437440
438441 if not unsampled :
439442 if not (A .ndim == 2 or A .ndim == 3 and A .shape [- 1 ] in (3 , 4 )):
@@ -560,7 +563,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
560563 t = Affine2D ().translate (
561564 int (max (subset .xmin , 0 )), int (max (subset .ymin , 0 ))) + t
562565
563- return output , clipped_bbox .x0 , clipped_bbox .y0 , t
566+ return (output ,
567+ magnified_bbox .x0 / magnification ,
568+ magnified_bbox .y0 / magnification ,
569+ t )
564570
565571 def make_image (self , renderer , magnification = 1.0 , unsampled = False ):
566572 """
@@ -1061,21 +1067,24 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
10611067 B [:, :, 0 :3 ] = A
10621068 B [:, :, 3 ] = 255
10631069 A = B
1064- l , b , r , t = self .axes .bbox .extents
1065- width = int (((round (r ) + 0.5 ) - (round (l ) - 0.5 )) * magnification )
1066- height = int (((round (t ) + 0.5 ) - (round (b ) - 0.5 )) * magnification )
1070+ magnified_extents = (self .axes .bbox .extents * magnification + 0.5 ).astype (int )
1071+ l , b , r , t = magnified_extents / magnification
1072+ width = int ((r - l ) * magnification )
1073+ height = int ((t - b ) * magnification )
10671074
10681075 invertedTransform = self .axes .transData .inverted ()
1069- x_pix = invertedTransform .transform (
1070- [(x , b ) for x in np .linspace (l , r , width )])[:, 0 ]
1071- y_pix = invertedTransform .transform (
1072- [(l , y ) for y in np .linspace (b , t , height )])[:, 1 ]
1076+ x_pix_edges = invertedTransform .transform (
1077+ [(x , b ) for x in np .linspace (l , r , width + 1 )])[:, 0 ]
1078+ y_pix_edges = invertedTransform .transform (
1079+ [(l , y ) for y in np .linspace (b , t , height + 1 )])[:, 1 ]
1080+ x_pix_centers = (x_pix_edges [:- 1 ] + x_pix_edges [1 :]) / 2
1081+ y_pix_centers = (y_pix_edges [:- 1 ] + y_pix_edges [1 :]) / 2
10731082
10741083 if self ._interpolation == "nearest" :
10751084 x_mid = (self ._Ax [:- 1 ] + self ._Ax [1 :]) / 2
10761085 y_mid = (self ._Ay [:- 1 ] + self ._Ay [1 :]) / 2
1077- x_int = x_mid .searchsorted (x_pix )
1078- y_int = y_mid .searchsorted (y_pix )
1086+ x_int = x_mid .searchsorted (x_pix_centers )
1087+ y_int = y_mid .searchsorted (y_pix_centers )
10791088 # The following is equal to `A[y_int[:, None], x_int[None, :]]`,
10801089 # but many times faster. Both casting to uint32 (to have an
10811090 # effectively 1D array) and manual index flattening matter.
@@ -1086,16 +1095,16 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
10861095 else : # self._interpolation == "bilinear"
10871096 # Use np.interp to compute x_int/x_float has similar speed.
10881097 x_int = np .clip (
1089- self ._Ax .searchsorted (x_pix ) - 1 , 0 , len (self ._Ax ) - 2 )
1098+ self ._Ax .searchsorted (x_pix_centers ) - 1 , 0 , len (self ._Ax ) - 2 )
10901099 y_int = np .clip (
1091- self ._Ay .searchsorted (y_pix ) - 1 , 0 , len (self ._Ay ) - 2 )
1100+ self ._Ay .searchsorted (y_pix_centers ) - 1 , 0 , len (self ._Ay ) - 2 )
10921101 idx_int = np .add .outer (y_int * A .shape [1 ], x_int )
10931102 x_frac = np .clip (
1094- np .divide (x_pix - self ._Ax [x_int ], np .diff (self ._Ax )[x_int ],
1103+ np .divide (x_pix_centers - self ._Ax [x_int ], np .diff (self ._Ax )[x_int ],
10951104 dtype = np .float32 ), # Downcasting helps with speed.
10961105 0 , 1 )
10971106 y_frac = np .clip (
1098- np .divide (y_pix - self ._Ay [y_int ], np .diff (self ._Ay )[y_int ],
1107+ np .divide (y_pix_centers - self ._Ay [y_int ], np .diff (self ._Ay )[y_int ],
10991108 dtype = np .float32 ),
11001109 0 , 1 )
11011110 f00 = np .outer (1 - y_frac , 1 - x_frac )
@@ -1248,22 +1257,24 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
12481257 if (padded_A [0 , 0 ] != bg ).all ():
12491258 padded_A [[0 , - 1 ], :] = padded_A [:, [0 , - 1 ]] = bg
12501259
1251- l , b , r , t = self . axes . bbox . extents
1252- width = ( round ( r ) + 0.5 ) - ( round ( l ) - 0.5 )
1253- height = ( round ( t ) + 0.5 ) - ( round ( b ) - 0.5 )
1254- width = round ( width * magnification )
1255- height = round ( height * magnification )
1260+ # Round to the nearest output pixels after magnification
1261+ l , b , r , t = ( self . axes . bbox . extents * magnification + 0.5 ). astype ( int )
1262+ width = r - l
1263+ height = t - b
1264+
12561265 vl = self .axes .viewLim
12571266
1258- x_pix = np .linspace (vl .x0 , vl .x1 , width )
1259- y_pix = np .linspace (vl .y0 , vl .y1 , height )
1260- x_int = self ._Ax .searchsorted (x_pix )
1261- y_int = self ._Ay .searchsorted (y_pix )
1267+ x_pix_edges = np .linspace (vl .x0 , vl .x1 , width + 1 )
1268+ y_pix_edges = np .linspace (vl .y0 , vl .y1 , height + 1 )
1269+ x_pix_centers = (x_pix_edges [:- 1 ] + x_pix_edges [1 :]) / 2
1270+ y_pix_centers = (y_pix_edges [:- 1 ] + y_pix_edges [1 :]) / 2
1271+ x_int = self ._Ax .searchsorted (x_pix_centers )
1272+ y_int = self ._Ay .searchsorted (y_pix_centers )
12621273 im = ( # See comment in NonUniformImage.make_image re: performance.
12631274 padded_A .view (np .uint32 ).ravel ()[
12641275 np .add .outer (y_int * padded_A .shape [1 ], x_int )]
12651276 .view (np .uint8 ).reshape ((height , width , 4 )))
1266- return im , l , b , IdentityTransform ()
1277+ return im , l / magnification , b / magnification , IdentityTransform ()
12671278
12681279 def _check_unsampled_image (self ):
12691280 return False
0 commit comments