This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,46 @@
# flake8: NOQA
"""
visual
-------------
Handle visual properties for meshes, including color and texture
"""
from .color import (
ColorVisuals,
random_color,
to_rgba,
DEFAULT_COLOR,
interpolate,
uv_to_color,
uv_to_interpolated_color,
linear_color_map,
)
from .texture import TextureVisuals
from .objects import create_visual, concatenate
from . import color
from . import texture
from . import objects
from . import material
from .. import resolvers
# explicitly list imports in __all__
# as otherwise flake8 gets mad
__all__ = [
"color",
"texture",
"resolvers",
"TextureVisuals",
"ColorVisuals",
"random_color",
"to_rgba",
"create_visual",
"DEFAULT_COLOR",
"interpolate",
"linear_color_map",
"uv_to_color",
"uv_to_interpolated_color",
]
@@ -0,0 +1,58 @@
"""
base.py
-------------
The base class for `Visual` objects
"""
import abc
from ..util import ABC
class Visuals(ABC):
"""
Parent of Visual classes.
"""
@property
@abc.abstractmethod
def kind(self):
pass
@abc.abstractmethod
def update_vertices(self, mask):
pass
@abc.abstractmethod
def update_faces(self, mask):
pass
@abc.abstractmethod
def concatenate(self, other):
pass
@abc.abstractmethod
def __hash__(self):
pass
@abc.abstractmethod
def copy(self):
pass
def __add__(self, other):
"""
Concatenate two ColorVisuals objects into a single object.
Parameters
-----------
other : Visuals
Other visual to concatenate
Returns
-----------
result : Visuals
Object containing information from current
object and other in the order (self, other)
"""
return self.concatenate(other)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,351 @@
import numpy as np
from ..constants import log
from ..exceptions import ExceptionWrapper
from ..typed import ArrayLike, Number, Optional
from .color import linear_to_srgb, srgb_to_linear
try:
from PIL.Image import Image, fromarray
except BaseException as E:
Image = ExceptionWrapper(E)
fromarray = ExceptionWrapper(E)
def specular_to_pbr(
specularFactor: Optional[ArrayLike] = None,
glossinessFactor: Optional[Number] = None,
specularGlossinessTexture: Optional["Image"] = None,
diffuseTexture: Optional["Image"] = None,
diffuseFactor: Optional[ArrayLike] = None,
**kwargs,
) -> dict:
"""
Convert the KHR_materials_pbrSpecularGlossiness to a
metallicRoughness visual.
Parameters
-----------
specularFactor : list[float]
Specular color values. Ignored if specularGlossinessTexture
is present and defaults to [1.0, 1.0, 1.0].
glossinessFactor : float
glossiness factor in range [0, 1], scaled
specularGlossinessTexture if present.
Defaults to 1.0.
specularGlossinessTexture : PIL.Image
Texture with 4 color channels. With [0,1,2] representing
specular RGB and 3 glossiness.
diffuseTexture : PIL.Image
Texture with 4 color channels. With [0,1,2] representing diffuse
RGB and 3 opacity.
diffuseFactor: float
Diffuse RGBA color. scales diffuseTexture if present.
Defaults to [1.0, 1.0, 1.0, 1.0].
Returns
----------
kwargs : dict
Constructor args for a PBRMaterial object.
Containing:
- either baseColorTexture or baseColorFactor
- either metallicRoughnessTexture or metallicFactor and roughnessFactor
"""
# based on:
# https://github.com/KhronosGroup/glTF/blob/89427b26fcac884385a2e6d5803d917ab5d1b04f/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness/examples/convert-between-workflows-bjs/js/babylon.pbrUtilities.js#L33-L64
if isinstance(Image, ExceptionWrapper):
log.debug("unable to convert specular-glossy material without pillow!")
result = {}
if isinstance(diffuseTexture, dict):
result["baseColorTexture"] = diffuseTexture
if diffuseFactor is not None:
result["baseColorFactor"] = diffuseFactor
return result
dielectric_specular = np.array([0.04, 0.04, 0.04], dtype=np.float32)
epsilon = 1e-6
def solve_metallic(diffuse, specular, one_minus_specular_strength):
if isinstance(specular, float) and specular < dielectric_specular[0]:
return 0.0
if len(diffuse.shape) == 2:
diffuse = diffuse[..., None]
if len(specular.shape) == 2:
specular = specular[..., None]
a = dielectric_specular[0]
b = (
diffuse * one_minus_specular_strength / (1.0 - dielectric_specular[0])
+ specular
- 2.0 * dielectric_specular[0]
)
c = dielectric_specular[0] - specular
D = b * b - 4.0 * a * c
D = np.clip(D, epsilon, None)
metallic = np.clip((-b + np.sqrt(D)) / (2.0 * a), 0.0, 1.0)
if isinstance(metallic, np.ndarray):
metallic[specular < dielectric_specular[0]] = 0.0
return metallic
def get_perceived_brightness(rgb):
return np.sqrt(np.dot(rgb[..., :3] ** 2, [0.299, 0.587, 0.114]))
def toPIL(img, mode=None):
if isinstance(img, Image):
return img
if img.dtype == np.float32 or img.dtype == np.float64:
img = (np.clip(img, 0.0, 1.0) * 255.0).astype(np.uint8)
return fromarray(img)
def get_float(val):
if isinstance(val, float):
return val
if isinstance(val, np.ndarray) and len(val.shape) == 1:
return val[0]
return val.tolist()
def get_diffuse(diffuseFactor, diffuseTexture):
diffuseFactor = (
diffuseFactor if diffuseFactor is not None else [1.0, 1.0, 1.0, 1.0]
)
diffuseFactor = np.array(diffuseFactor, dtype=np.float32)
if diffuseTexture is not None:
if diffuseTexture.mode == "BGR":
diffuseTexture = diffuseTexture.convert("RGB")
elif diffuseTexture.mode == "BGRA":
diffuseTexture = diffuseTexture.convert("RGBA")
diffuse = np.array(diffuseTexture) / 255.0
# diffuseFactor must be applied to linear scaled colors .
# Sometimes, diffuse texture is only 2 channels, how do we know
# if they are encoded sRGB or linear?
diffuse = convert_texture_srgb2lin(diffuse)
if len(diffuse.shape) == 2:
diffuse = diffuse[..., None]
if diffuse.shape[-1] == 1:
diffuse = diffuse * diffuseFactor
elif diffuse.shape[-1] == 2:
alpha = diffuse[..., 1:2]
diffuse = diffuse[..., :1] * diffuseFactor
if diffuseFactor.shape[-1] == 3:
# this should actually not happen, but it seems like many materials are not complying with the spec
diffuse = np.concatenate([diffuse, alpha], axis=-1)
else:
diffuse[..., -1:] *= alpha
elif diffuse.shape[-1] == diffuseFactor.shape[-1]:
diffuse = diffuse * diffuseFactor
elif diffuse.shape[-1] == 3 and diffuseFactor.shape[-1] == 4:
diffuse = (
np.concatenate([diffuse, np.ones_like(diffuse[..., :1])], axis=-1)
* diffuseFactor
)
else:
log.warning(
"`diffuseFactor` and `diffuseTexture` have incompatible shapes: "
+ f"{diffuseFactor.shape} and {diffuse.shape}"
)
else:
diffuse = diffuseFactor if diffuseFactor is not None else [1, 1, 1, 1]
diffuse = np.array(diffuse, dtype=np.float32)
return diffuse
def get_specular_glossiness(
specularFactor, glossinessFactor, specularGlossinessTexture
):
if specularFactor is None:
specularFactor = [1.0, 1.0, 1.0]
specularFactor = np.array(specularFactor, dtype=np.float32)
if glossinessFactor is None:
glossinessFactor = 1.0
glossinessFactor = np.array([glossinessFactor], dtype=np.float32)
# specularGlossinessTexture should be a texture with 4 channels,
# 3 sRGB channels for specular and 1 linear channel for glossiness.
# in practice, it can also have just 1, 2, or 3 channels which are then to
# be multiplied with the provided factors
if specularGlossinessTexture is not None:
if specularGlossinessTexture.mode == "BGR":
specularGlossinessTexture = specularGlossinessTexture.convert("RGB")
elif specularGlossinessTexture.mode == "BGRA":
specularGlossinessTexture = specularGlossinessTexture.convert("RGBA")
specularGlossinessTexture = np.array(specularGlossinessTexture) / 255.0
specularTexture, glossinessTexture = None, None
if len(specularGlossinessTexture.shape) == 2:
# use the one channel as a multiplier for specular and glossiness
specularTexture = glossinessTexture = specularGlossinessTexture.reshape(
specularGlossinessTexture.shape[0],
specularGlossinessTexture.shape[1],
1,
)
elif specularGlossinessTexture.shape[-1] == 1:
# use the one channel as a multiplier for specular and glossiness
specularTexture = glossinessTexture = specularGlossinessTexture[
..., np.newaxis
]
elif specularGlossinessTexture.shape[-1] == 3:
# all channels are specular, glossiness is only a factor
specularTexture = specularGlossinessTexture[..., :3]
elif specularGlossinessTexture.shape[-1] == 2:
# first channel is specular, last channel is glossiness
specularTexture = specularGlossinessTexture[..., :1]
glossinessTexture = specularGlossinessTexture[..., 1:2]
elif specularGlossinessTexture.shape[-1] == 4:
# first 3 channels are specular, last channel is glossiness
specularTexture = specularGlossinessTexture[..., :3]
glossinessTexture = specularGlossinessTexture[..., 3:]
if specularTexture is not None:
# specular texture channels are sRGB
specularTexture = convert_texture_srgb2lin(specularTexture)
specular = specularTexture * specularFactor
else:
specular = specularFactor
if glossinessTexture is not None:
# glossiness texture channel is linear
glossiness = glossinessTexture * glossinessFactor
else:
glossiness = glossinessFactor
one_minus_specular_strength = 1.0 - np.max(specular, axis=-1, keepdims=True)
else:
specular = specularFactor if specularFactor is not None else [1.0, 1.0, 1.0]
specular = np.array(specular, dtype=np.float32)
glossiness = glossinessFactor if glossinessFactor is not None else 1.0
glossiness = np.array(glossiness, dtype=np.float32)
one_minus_specular_strength = 1.0 - max(specular[:3])
return specular, glossiness, one_minus_specular_strength
if diffuseTexture is not None and specularGlossinessTexture is not None:
# reshape to the size of the largest texture
max_shape = tuple(
max(diffuseTexture.size[i], specularGlossinessTexture.size[i])
for i in range(2)
)
if (
diffuseTexture.size[0] != max_shape[0]
or diffuseTexture.size[1] != max_shape[1]
):
diffuseTexture = diffuseTexture.resize(max_shape)
if (
specularGlossinessTexture.size[0] != max_shape[0]
or specularGlossinessTexture.size[1] != max_shape[1]
):
specularGlossinessTexture = specularGlossinessTexture.resize(max_shape)
def convert_texture_srgb2lin(texture):
"""
Wrapper for srgb2lin that converts color values from sRGB to linear.
If texture has 2 or 4 channels, the last channel (alpha) is left unchanged.
"""
result = texture.copy()
color_channels = result.shape[-1]
# only scale the color channels, not the alpha channel
if color_channels == 4 or color_channels == 2:
color_channels -= 1
result[..., :color_channels] = srgb_to_linear(result[..., :color_channels])
return result
def convert_texture_lin2srgb(texture):
"""
Wrapper for lin2srgb that converts color values from linear to sRGB.
If texture has 2 or 4 channels, the last channel (alpha) is left unchanged.
"""
result = texture.copy()
color_channels = result.shape[-1]
# only scale the color channels, not the alpha channel
if color_channels == 4 or color_channels == 2:
color_channels -= 1
result[..., :color_channels] = linear_to_srgb(result[..., :color_channels])
return result
diffuse = get_diffuse(diffuseFactor, diffuseTexture)
specular, glossiness, one_minus_specular_strength = get_specular_glossiness(
specularFactor, glossinessFactor, specularGlossinessTexture
)
metallic = solve_metallic(
get_perceived_brightness(diffuse),
get_perceived_brightness(specular),
one_minus_specular_strength,
)
if not isinstance(metallic, np.ndarray):
metallic = np.array(metallic, dtype=np.float32)
diffuse_rgb = diffuse[..., :3]
base_color_from_diffuse = diffuse_rgb * (
one_minus_specular_strength
/ (1.0 - dielectric_specular[0])
/ np.clip((1.0 - metallic), epsilon, None)
)
base_color_from_specular = (specular - dielectric_specular * (1.0 - metallic)) * (
1.0 / np.clip(metallic, epsilon, None)
)
mm = metallic * metallic
base_color = mm * base_color_from_specular + (1.0 - mm) * base_color_from_diffuse
base_color = np.clip(base_color, 0.0, 1.0)
# get opacity
try:
if diffuse.shape == (4,):
# opacity is a single scalar value
opacity = diffuse[-1]
if base_color.shape == (3,):
# simple case with one color and diffuse with opacity
# add on the opacity from the diffuse color
base_color = np.append(base_color, opacity)
elif len(base_color.shape) == 3:
# stack opacity to match the base color array
dim = base_color.shape
base_color = np.dstack(
(
base_color,
np.full(np.prod(dim[:2]), opacity).reshape((dim[0], dim[1], 1)),
)
)
elif diffuse.shape[-1] == 4:
opacity = diffuse[..., -1]
base_color = np.concatenate([base_color, opacity[..., None]], axis=-1)
except BaseException:
log.error("unable to get opacity", exc_info=True)
result = {}
if len(base_color.shape) > 1:
# convert back to sRGB
result["baseColorTexture"] = toPIL(
convert_texture_lin2srgb(base_color),
mode=("RGB" if base_color.shape[-1] == 3 else "RGBA"),
)
else:
result["baseColorFactor"] = base_color.tolist()
if len(metallic.shape) > 1 or len(glossiness.shape) > 1:
if len(glossiness.shape) == 1:
glossiness = np.tile(glossiness, (metallic.shape[0], metallic.shape[1], 1))
if len(metallic.shape) == 1:
metallic = np.tile(metallic, (glossiness.shape[0], glossiness.shape[1], 1))
# we need to use RGB textures, because 2 channel textures can cause problems
result["metallicRoughnessTexture"] = toPIL(
np.concatenate(
[np.zeros_like(metallic), 1.0 - glossiness, metallic], axis=-1
),
mode="RGB",
)
result["metallicFactor"] = 1.0
result["roughnessFactor"] = 1.0
else:
result["metallicFactor"] = get_float(metallic)
result["roughnessFactor"] = get_float(1.0 - glossiness)
return result
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
"""
objects.py
--------------
Deal with objects which hold visual properties, like
ColorVisuals and TextureVisuals.
"""
import numpy as np
from .color import ColorVisuals, color_to_uv
from .material import pack
from .texture import TextureVisuals
def create_visual(**kwargs):
"""
Create Visuals object from keyword arguments.
Parameters
-----------
face_colors : (n, 3|4) uint8
Face colors
vertex_colors : (n, 3|4) uint8
Vertex colors
mesh : trimesh.Trimesh
Mesh object
Returns
----------
visuals : ColorVisuals
Visual object created from arguments
"""
return ColorVisuals(**kwargs)
def concatenate(visuals, *args):
"""
Concatenate multiple visual objects.
Parameters
----------
visuals : ColorVisuals or list
Visuals to concatenate
*args : ColorVisuals or list
More visuals to concatenate
Returns
----------
concat : Visuals
If all are color
"""
# get a flat list of Visuals objects
if len(args) > 0:
visuals = np.append(visuals, args)
else:
visuals = np.array(visuals)
# if there are any texture visuals convert all to texture
if any(v.kind == "texture" for v in visuals):
# first collect materials and UV coordinates
mats = []
uvs = []
for v in visuals:
if v.kind == "texture":
mats.append(v.material)
if v.uv is None:
# otherwise use zeros
uvs.append(np.zeros((len(v.mesh.vertices), 2)) + 0.5)
else:
# if uvs are of correct shape use them
uvs.append(v.uv)
else:
# create a material and UV coordinates from vertex colors
color_mat, color_uv = color_to_uv(vertex_colors=v.vertex_colors)
mats.append(color_mat)
uvs.append(color_uv)
# pack the materials and UV coordinates into one
new_mat, new_uv = pack(materials=mats, uvs=uvs)
return TextureVisuals(material=new_mat, uv=new_uv)
# convert all visuals to the first valid kind
kind = next((v.kind for v in visuals if v.kind is not None), None)
if kind == "face":
colors = np.vstack([v.face_colors for v in visuals])
return ColorVisuals(face_colors=colors)
elif kind == "vertex":
colors = np.vstack([v.vertex_colors for v in visuals])
return ColorVisuals(vertex_colors=colors)
return ColorVisuals()
@@ -0,0 +1,350 @@
import copy
import numpy as np
from .. import caching, grouping, util
from . import color
from .base import Visuals
from .material import PBRMaterial, SimpleMaterial, empty_material # NOQA
class TextureVisuals(Visuals):
def __init__(self, uv=None, material=None, image=None, face_materials=None):
"""
Store a single material and per-vertex UV coordinates
for a mesh.
If passed UV coordinates and a single image it will
create a SimpleMaterial for the image.
Parameters
--------------
uv : (n, 2) float
UV coordinates for the mesh
material : Material
Store images and properties
image : PIL.Image
Can be passed to automatically create material
"""
# store values we care about enough to hash
self.vertex_attributes = caching.DataStore()
# cache calculated values
self._cache = caching.Cache(self.vertex_attributes.__hash__)
# should be (n, 2) float
self.uv = uv
if material is None:
if image is None:
self.material = empty_material()
else:
# if an image is passed create a SimpleMaterial
self.material = SimpleMaterial(image=image)
else:
# if passed assign
self.material = material
self.face_materials = face_materials
def _verify_hash(self):
"""
Dump the cache if anything in self.vertex_attributes
has changed.
"""
self._cache.verify()
@property
def kind(self):
"""
Return the type of visual data stored
Returns
----------
kind : str
What type of visuals are defined
"""
return "texture"
@property
def defined(self):
"""
Check if any data is stored
Returns
----------
defined : bool
Are UV coordinates and images set?
"""
ok = self.material is not None
return ok
def __hash__(self):
"""
Get a CRC of the stored data.
Returns
--------------
crc : int
Hash of items in self.vertex_attributes
"""
return self.vertex_attributes.__hash__()
@property
def uv(self):
"""
Get the stored UV coordinates.
Returns
------------
uv : (n, 2) float or None
Pixel position per-vertex.
"""
return self.vertex_attributes.get("uv", None)
@uv.setter
def uv(self, values):
"""
Set the UV coordinates.
Parameters
--------------
values : (n, 2) float or None
Pixel locations on a texture per- vertex
"""
if values is None:
self.vertex_attributes.pop("uv")
else:
self.vertex_attributes["uv"] = np.asanyarray(values, dtype=np.float64)
def copy(self, uv=None):
"""
Return a copy of the current TextureVisuals object.
Returns
----------
copied : TextureVisuals
Contains the same information in a new object
"""
if uv is None:
uv = self.uv
if uv is not None:
uv = uv.copy()
copied = TextureVisuals(
uv=uv,
material=self.material.copy(),
face_materials=copy.copy(self.face_materials),
)
return copied
def to_color(self):
"""
Convert textured visuals to a ColorVisuals with vertex
color calculated from texture.
Returns
-----------
vis : trimesh.visuals.ColorVisuals
Contains vertex color from texture
"""
# find the color at each UV coordinate
colors = self.material.to_color(self.uv)
# create ColorVisuals from result
vis = color.ColorVisuals(vertex_colors=colors)
return vis
def face_subset(self, face_index):
"""
Get a copy of
"""
if self.uv is not None:
indices = np.unique(self.mesh.faces[face_index].flatten())
return self.copy(self.uv[indices])
else:
return self.copy()
def update_vertices(self, mask):
"""
Apply a mask to remove or duplicate vertex properties.
Parameters
------------
mask : (len(vertices),) bool or (n,) int
Mask which can be used like: `vertex_attribute[mask]`
"""
# collect updated masked values
updates = {}
for key, value in self.vertex_attributes.items():
# DataStore will convert None to zero-length array
if len(value) == 0:
continue
try:
# store the update
updates[key] = value[mask]
except BaseException:
# usual reason is an incorrect size or index
util.log.warning(f"failed to update visual: `{key}`")
# clear all values from the vertex attributes
self.vertex_attributes.clear()
# apply the updated values
self.vertex_attributes.update(updates)
def update_faces(self, mask):
"""
Apply a mask to remove or duplicate face properties,
not applicable to texture visuals.
"""
def concatenate(self, others):
"""
Concatenate this TextureVisuals object with others
and return the result without modifying this visual.
Parameters
-----------
others : (n,) Visuals
Other visual objects to concatenate
Returns
-----------
concatenated : TextureVisuals
Concatenated visual objects
"""
from .objects import concatenate
return concatenate(self, others)
def unmerge_faces(faces, *args, **kwargs):
"""
Textured meshes can come with faces referencing vertex
indices (`v`) and an array the same shape which references
vertex texture indices (`vt`) and sometimes even normal (`vn`).
Vertex locations with different values of any of these can't
be considered the "same" vertex, and for our simple data
model we need to not combine these vertices.
Parameters
-------------
faces : (n, d) int
References vertex indices
*args : (n, d) int
Various references of corresponding values
This is usually UV coordinates or normal indexes
maintain_faces : bool
Do not alter original faces and return no-op masks.
Returns
-------------
new_faces : (m, d) int
New faces for masked vertices
mask_v : (p,) int
A mask to apply to vertices
mask_* : (p,) int
A mask to apply to vt array to get matching UV coordinates
Returns as many of these as args were passed
"""
# unfortunately Python2 doesn't let us put named kwargs
# after an `*args` sequence so we have to do this ugly get
maintain_faces = kwargs.get("maintain_faces", False)
# don't alter faces
if maintain_faces:
# start with not altering faces at all
result = [faces]
# find the maximum index referenced by faces
max_idx = faces.max()
# add a vertex mask which is just ordered
result.append(np.arange(max_idx + 1))
# now given the order is fixed do our best on the rest of the order
for arg in args:
# create a mask of the attribute-vertex mapping
# note that these might conflict since we're not unmerging
masks = np.full((3, max_idx + 1), -1, dtype=np.int64)
# set the mask using the unmodified face indexes
for i, f, a in zip(range(3), faces.T, arg.T):
masks[i][f] = a
# find the most commonly occurring attribute (i.e. UV coordinate)
# and use that index note that this is doing a float conversion
# and then median before converting back to int: could also do this as
# a column diff and sort but this seemed easier and is fast enough
# turn default attribute value of -1 to nan before median computation
# and use nanmedian to compute the median ignoring the nan values
masks_nan = np.where(masks != -1, masks, np.nan)
result.append(np.nanmedian(masks_nan, axis=0).astype(np.int64))
return result
# stack into pairs of (vertex index, texture index)
stackable = [np.asanyarray(faces).reshape(-1)]
# append multiple args to the correlated stack
# this is usually UV coordinates (vt) and normals (vn)
for arg in args:
stackable.append(np.asanyarray(arg).reshape(-1))
# unify them into rows of a numpy array
stack = np.column_stack(stackable)
# find unique pairs: we're trying to avoid merging
# vertices that have the same position but different
# texture coordinates
unique, inverse = grouping.unique_rows(stack)
# only take the unique pairs
pairs = stack[unique]
# try to maintain original vertex order
order = pairs[:, 0].argsort()
# apply the order to the pairs
pairs = pairs[order]
# we re-ordered the vertices to try to maintain
# the original vertex order as much as possible
# so to reconstruct the faces we need to remap
remap = np.zeros(len(order), dtype=np.int64)
remap[order] = np.arange(len(order))
# the faces are just the inverse with the new order
new_faces = remap[inverse].reshape((-1, faces.shape[1]))
# the mask for vertices and masks for other args
result = [new_faces]
result.extend(pairs.T)
return result
def power_resize(image, resample=1, square=False):
"""
Resize a PIL image so every dimension is a power of two.
Parameters
------------
image : PIL.Image
Input image
resample : int
Passed to Image.resize
square : bool
If True, upsize to a square image
Returns
-------------
resized : PIL.Image
Input image resized
"""
# what is the current resolution of the image in pixels
size = np.array(image.size, dtype=np.int64)
# what is the resolution of the image upsized to the nearest
# power of two on each axis: allow rectangular textures
new_size = (2 ** np.ceil(np.log2(size))).astype(np.int64)
# make every dimension the largest
if square:
new_size = np.ones(2, dtype=np.int64) * new_size.max()
# if we're not powers of two upsize
if (size != new_size).any():
return image.resize(tuple(new_size), resample=resample)
return image.copy()