@@ -2320,6 +2320,16 @@ def __init__(self, vmin=None, vmax=None, clip=False):
23202320 self ._scale = None
23212321 self .callbacks = cbook .CallbackRegistry (signals = ["changed" ])
23222322
2323+ @property
2324+ def n_input (self ):
2325+ # To be overridden by subclasses with multiple inputs
2326+ return 1
2327+
2328+ @property
2329+ def n_output (self ):
2330+ # To be overridden by subclasses with multiple outputs
2331+ return 1
2332+
23232333 @property
23242334 def vmin (self ):
23252335 return self ._vmin
@@ -3219,6 +3229,237 @@ def inverse(self, value):
32193229 return value
32203230
32213231
3232+ class MultiNorm (Normalize ):
3233+ """
3234+ A mixin class which contains multiple scalar norms
3235+ """
3236+
3237+ def __init__ (self , norms , vmin = None , vmax = None , clip = False ):
3238+ """
3239+ Parameters
3240+ ----------
3241+ norms : List of strings or `Normalize` objects
3242+ The constituent norms. The list must have a minimum length of 2.
3243+ vmin, vmax : float, None, or list of float or None
3244+ Limits of the constituent norms.
3245+ If a list, each each value is assigned to one of the constituent
3246+ norms. Single values are repeated to form a list of appropriate size.
3247+
3248+ clip : bool or list of bools, default: False
3249+ Determines the behavior for mapping values outside the range
3250+ ``[vmin, vmax]`` for the constituent norms.
3251+ If a list, each each value is assigned to one of the constituent
3252+ norms. Single values are repeated to form a list of appropriate size.
3253+
3254+ """
3255+
3256+ if isinstance (norms , str ) or not np .iterable (norms ):
3257+ raise ValueError ("A MultiNorm must be assigned multiple norms" )
3258+ norms = [n for n in norms ]
3259+ for i , n in enumerate (norms ):
3260+ if n is None :
3261+ norms [i ] = Normalize ()
3262+ elif isinstance (n , str ):
3263+ try :
3264+ scale_cls = scale ._scale_mapping [n ]
3265+ except KeyError :
3266+ raise ValueError (
3267+ "Invalid norm str name; the following values are "
3268+ f"supported: { ', ' .join (scale ._scale_mapping )} "
3269+ ) from None
3270+ norms [i ] = mpl .colorizer ._auto_norm_from_scale (scale_cls )()
3271+
3272+ # Convert the list of norms to a tuple to make it immutable.
3273+ # If there is a use case for swapping a single norm, we can add support for
3274+ # that later
3275+ self ._norms = tuple (n for n in norms )
3276+
3277+ self .callbacks = cbook .CallbackRegistry (signals = ["changed" ])
3278+
3279+ self .vmin = vmin
3280+ self .vmax = vmax
3281+ self .clip = clip
3282+
3283+ self ._id_norms = [n .callbacks .connect ('changed' ,
3284+ self ._changed ) for n in self ._norms ]
3285+
3286+ @property
3287+ def n_input (self ):
3288+ return len (self ._norms )
3289+
3290+ @property
3291+ def n_output (self ):
3292+ return len (self ._norms )
3293+
3294+ @property
3295+ def norms (self ):
3296+ return self ._norms
3297+
3298+ @property
3299+ def vmin (self ):
3300+ return tuple (n .vmin for n in self ._norms )
3301+
3302+ @vmin .setter
3303+ def vmin (self , value ):
3304+ if not np .iterable (value ):
3305+ value = [value ]* self .n_input
3306+ if len (value ) != self .n_input :
3307+ raise ValueError (f"Invalid vmin for `MultiNorm` with { self .n_input } "
3308+ " inputs." )
3309+ with self .callbacks .blocked ():
3310+ for i , v in enumerate (value ):
3311+ if v is not None :
3312+ self .norms [i ].vmin = v
3313+ self ._changed ()
3314+
3315+ @property
3316+ def vmax (self ):
3317+ return tuple (n .vmax for n in self ._norms )
3318+
3319+ @vmax .setter
3320+ def vmax (self , value ):
3321+ if not np .iterable (value ):
3322+ value = [value ]* self .n_input
3323+ if len (value ) != self .n_input :
3324+ raise ValueError (f"Invalid vmax for `MultiNorm` with { self .n_input } "
3325+ " inputs." )
3326+ with self .callbacks .blocked ():
3327+ for i , v in enumerate (value ):
3328+ if v is not None :
3329+ self .norms [i ].vmax = v
3330+ self ._changed ()
3331+
3332+ @property
3333+ def clip (self ):
3334+ return tuple (n .clip for n in self ._norms )
3335+
3336+ @clip .setter
3337+ def clip (self , value ):
3338+ if not np .iterable (value ):
3339+ value = [value ]* self .n_input
3340+ with self .callbacks .blocked ():
3341+ for i , v in enumerate (value ):
3342+ if v is not None :
3343+ self .norms [i ].clip = v
3344+ self ._changed ()
3345+
3346+ def _changed (self ):
3347+ """
3348+ Call this whenever the norm is changed to notify all the
3349+ callback listeners to the 'changed' signal.
3350+ """
3351+ self .callbacks .process ('changed' )
3352+
3353+ def __call__ (self , value , clip = None ):
3354+ """
3355+ Normalize the data and return the normalized data.
3356+ Each variate in the input is assigned to the a constituent norm.
3357+
3358+ Parameters
3359+ ----------
3360+ value
3361+ Data to normalize. Must be of length `n_input` or have a data type with
3362+ `n_input` fields.
3363+ clip : List of bools or bool, optional
3364+ See the description of the parameter *clip* in Normalize.
3365+ If ``None``, defaults to ``self.clip`` (which defaults to
3366+ ``False``).
3367+
3368+ Returns
3369+ -------
3370+ Data
3371+ Normalized input values as a list of length `n_input`
3372+
3373+ Notes
3374+ -----
3375+ If not already initialized, ``self.vmin`` and ``self.vmax`` are
3376+ initialized using ``self.autoscale_None(value)``.
3377+ """
3378+ if clip is None :
3379+ clip = self .clip
3380+ else :
3381+ if not np .iterable (clip ):
3382+ value = [value ]* self .n_input
3383+
3384+ value = self ._iterable_variates_in_data (value , self .n_input )
3385+ result = [n (v , clip = c ) for n , v , c in zip (self .norms , value , clip )]
3386+ return result
3387+
3388+ def inverse (self , value ):
3389+ """
3390+ Maps the normalized value (i.e., index in the colormap) back to image
3391+ data value.
3392+
3393+ Parameters
3394+ ----------
3395+ value
3396+ Normalized value. Must be of length `n_input` or have a data type with
3397+ `n_input` fields.
3398+ """
3399+ value = self ._iterable_variates_in_data (value , self .n_input )
3400+ result = [n .inverse (v ) for n , v in zip (self .norms , value )]
3401+ return result
3402+
3403+ def autoscale (self , A ):
3404+ """
3405+ For each constituent norm, Set *vmin*, *vmax* to min, max of the corresponding
3406+ variate in *A*.
3407+ """
3408+ with self .callbacks .blocked ():
3409+ # Pause callbacks while we are updating so we only get
3410+ # a single update signal at the end
3411+ self .vmin = self .vmax = None
3412+ self .autoscale_None (A )
3413+
3414+ def autoscale_None (self , A ):
3415+ """
3416+ If *vmin* or *vmax* are not set on any constituent norm,
3417+ use the min/max of the corresponding variate in *A* to set them.
3418+
3419+ Parameters
3420+ ----------
3421+ A
3422+ Data, must be of length `n_input` or be an np.ndarray type with
3423+ `n_input` fields.
3424+ """
3425+ with self .callbacks .blocked ():
3426+ A = self ._iterable_variates_in_data (A , self .n_input )
3427+ for n , a in zip (self .norms , A ):
3428+ n .autoscale_None (a )
3429+ self ._changed ()
3430+
3431+ def scaled (self ):
3432+ """Return whether both *vmin* and *vmax* are set on all constitient norms"""
3433+ return all ([(n .vmin is not None and n .vmax is not None ) for n in self .norms ])
3434+
3435+ @staticmethod
3436+ def _iterable_variates_in_data (data , n_input ):
3437+ """
3438+ Provides an iterable over the variates contained in the data.
3439+
3440+ An input array with n_input fields is returned as a list of length n referencing
3441+ slices of the original array.
3442+
3443+ Parameters
3444+ ----------
3445+ data : np.ndarray, tuple or list
3446+ The input array. It must either be an array with n_input fields or have
3447+ a length (n_input)
3448+
3449+ Returns
3450+ -------
3451+ list of np.ndarray
3452+
3453+ """
3454+ if isinstance (data , np .ndarray ) and data .dtype .fields is not None :
3455+ data = [data [descriptor [0 ]] for descriptor in data .dtype .descr ]
3456+ if not len (data ) == n_input :
3457+ raise ValueError ("The input to this `MultiNorm` must be of shape "
3458+ f"({ n_input } , ...), or have a data type with { n_input } "
3459+ "fields." )
3460+ return data
3461+
3462+
32223463def rgb_to_hsv (arr ):
32233464 """
32243465 Convert an array of float RGB values (in the range [0, 1]) to HSV values.
0 commit comments