Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/source/en/model_doc/glpn.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ A list of official Hugging Face and community (indicated by 🌎) resources to h
[[autodoc]] GLPNImageProcessor
- preprocess

## GLPNImageProcessorFast

[[autodoc]] GLPNImageProcessorFast
- preprocess

## GLPNModel

[[autodoc]] GLPNModel
Expand Down
2 changes: 1 addition & 1 deletion src/transformers/models/auto/image_processing_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
("gemma3n", ("SiglipImageProcessor", "SiglipImageProcessorFast")),
("git", ("CLIPImageProcessor", "CLIPImageProcessorFast")),
("glm4v", ("Glm4vImageProcessor", "Glm4vImageProcessorFast")),
("glpn", ("GLPNImageProcessor", None)),
("glpn", ("GLPNImageProcessor", "GLPNImageProcessorFast")),
("got_ocr2", ("GotOcr2ImageProcessor", "GotOcr2ImageProcessorFast")),
("grounding-dino", ("GroundingDinoImageProcessor", "GroundingDinoImageProcessorFast")),
("groupvit", ("CLIPImageProcessor", "CLIPImageProcessorFast")),
Expand Down
1 change: 1 addition & 0 deletions src/transformers/models/glpn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .configuration_glpn import *
from .feature_extraction_glpn import *
from .image_processing_glpn import *
from .image_processing_glpn_fast import *
from .modeling_glpn import *
else:
import sys
Expand Down
32 changes: 32 additions & 0 deletions src/transformers/models/glpn/image_processing_glpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
valid_images,
validate_preprocess_arguments,
)
from ...processing_utils import ImagesKwargs
from ...utils import TensorType, filter_out_non_signature_kwargs, logging, requires_backends


Expand All @@ -49,6 +50,17 @@
logger = logging.get_logger(__name__)


class GLPNImageProcessorKwargs(ImagesKwargs, total=False):
"""
size_divisor (`int`, *optional*, defaults to 32):
When `do_resize` is `True`, images are resized so their height and width are rounded down to the closest
multiple of `size_divisor`.
"""

size_divisor: int
resample: PILImageResampling


@requires(backends=("vision",))
class GLPNImageProcessor(BaseImageProcessor):
r"""
Expand All @@ -69,6 +81,7 @@ class GLPNImageProcessor(BaseImageProcessor):
"""

model_input_names = ["pixel_values"]
valid_kwargs = GLPNImageProcessorKwargs

def __init__(
self,
Expand Down Expand Up @@ -223,6 +236,25 @@ def preprocess(
to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format) for image in images
]

if return_tensors:
shapes = {tuple(img.shape) for img in images}
if len(shapes) > 1:
max_height = max(img.shape[-2] for img in images)
max_width = max(img.shape[-1] for img in images)

padded_images = []
for img in images:
h, w = img.shape[-2:]
pad_h = max_height - h
pad_w = max_width - w
if pad_h > 0 or pad_w > 0:
# Pad bottom and right to reach max dimensions
# np.pad format: ((before, after), ...) for each dimension
# For (C, H, W) format: no padding on channels, pad height and width
img = np.pad(img, ((0, 0), (0, pad_h), (0, pad_w)), mode="constant", constant_values=0)
padded_images.append(img)
images = padded_images

data = {"pixel_values": images}
return BatchFeature(data=data, tensor_type=return_tensors)

Expand Down
172 changes: 172 additions & 0 deletions src/transformers/models/glpn/image_processing_glpn_fast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# coding=utf-8
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Fast Image processor class for GLPN."""

from typing import Optional, Union

import torch
from torchvision.transforms.v2 import functional as F

from ...image_processing_utils import BatchFeature
from ...image_processing_utils_fast import BaseImageProcessorFast, group_images_by_shape, reorder_images
from ...image_utils import (
IMAGENET_STANDARD_MEAN,
IMAGENET_STANDARD_STD,
PILImageResampling,
SizeDict,
)
from ...utils import (
TensorType,
auto_docstring,
requires_backends,
)
from .image_processing_glpn import GLPNImageProcessorKwargs


@auto_docstring
class GLPNImageProcessorFast(BaseImageProcessorFast):
"""
Fast image processor for GLPN using the Torch/TorchVision backend.

Performs:
- Crop H,W down to the nearest multiple of `size_divisor` (default 32)
- Rescale [0,255] → [0,1]
- (No normalization by default)
"""

do_resize = True
do_rescale = True
do_normalize = False
resample = PILImageResampling.BILINEAR
size_divisor = 32
image_mean = IMAGENET_STANDARD_MEAN
image_std = IMAGENET_STANDARD_STD
interpolation = F.InterpolationMode.BILINEAR
valid_kwargs = GLPNImageProcessorKwargs

def __init__(self, **kwargs) -> None:
if "ensure_multiple_of" in kwargs and "size_divisor" not in kwargs:
kwargs = dict(kwargs)
kwargs["size_divisor"] = kwargs.pop("ensure_multiple_of")
# ensure resample default for validation
kwargs.setdefault("resample", PILImageResampling.BILINEAR)
kwargs.setdefault("size", {"height": 480, "width": 640})
super().__init__(**kwargs)

def _preprocess(
self,
images: list["torch.Tensor"],
do_resize: bool,
size: Optional[dict] = None,
size_divisor: Optional[int] = None,
interpolation: Optional["F.InterpolationMode"] = None,
do_rescale: bool = True,
rescale_factor: Optional[float] = 1 / 255,
do_normalize: bool = False,
image_mean: Optional[Union[float, list[float]]] = None,
image_std: Optional[Union[float, list[float]]] = None,
disable_grouping: Optional[bool] = None,
return_tensors: Optional[Union[str, TensorType]] = None,
resample: Optional[PILImageResampling] = None,
**kwargs,
) -> BatchFeature:
"""
GLPN fast preprocessing:
- crop to floored multiple of size_divisor
- rescale [0,1]
- normalize (off by default)
"""
# avoid validation error: inject dummy size/resample for validate_preprocess_arguments

if resample is None and interpolation is None:
resample = self.resample

grouped_images, grouped_index = group_images_by_shape(images, disable_grouping=disable_grouping)
processed_groups = {}
sd = size_divisor if size_divisor is not None else self.size_divisor

for shape, stacked_images in grouped_images.items():
if do_resize:
# Calculate target size (nearest multiple of size_divisor)
_, _, h, w = stacked_images.shape
new_h = (h // sd) * sd
new_w = (w // sd) * sd

if (new_h, new_w) != (h, w):
target_size = SizeDict(height=new_h, width=new_w)
stacked_images = self.resize(
stacked_images, size=target_size, interpolation=interpolation, antialias=True
)
stacked_images = self.rescale_and_normalize(
stacked_images, do_rescale, rescale_factor, do_normalize, image_mean, image_std
)
processed_groups[shape] = stacked_images

reordered = reorder_images(processed_groups, grouped_index)

# Pad to max size if there are heterogeneous shapes
shapes = {tuple(img.shape) for img in reordered}
if len(shapes) > 1:
reordered = self.pad(reordered, pad_size=None)

processed = torch.stack(reordered, dim=0) if return_tensors else reordered

return BatchFeature(data={"pixel_values": processed}, tensor_type=return_tensors)

# ensure only slow keys are serialized
def to_dict(self):
output_dict = super().to_dict()

keys_to_keep = {
"image_processor_type",
"_processor_class",
"do_resize",
"size_divisor",
"resample",
"do_rescale",
"default_to_square",
"data_format",
}

for key in list(output_dict.keys()):
if key not in keys_to_keep:
output_dict[key] = None

return output_dict

def post_process_depth_estimation(self, outputs, target_sizes=None):
"""
Convert raw model outputs to final depth predictions.
Mirrors slow GLPN: PyTorch interpolate w/ bicubic, align_corners=False.
"""
requires_backends(self, "torch")
predicted_depth = outputs.predicted_depth

results = []
target_sizes = target_sizes or [None] * predicted_depth.shape[0]
for depth, target_size in zip(predicted_depth, target_sizes):
if target_size is not None:
# Add batch and channel dimensions for interpolation
depth_4d = depth[None, None, ...]
resized = torch.nn.functional.interpolate(
depth_4d, size=target_size, mode="bicubic", align_corners=False
)
depth = resized.squeeze(0).squeeze(0)
results.append({"predicted_depth": depth})

return results


__all__ = ["GLPNImageProcessorFast"]
84 changes: 78 additions & 6 deletions tests/models/glpn/test_image_processing_glpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import numpy as np

from transformers.testing_utils import require_torch, require_vision
from transformers.utils import is_torch_available, is_vision_available
from transformers.utils import is_torch_available, is_torchvision_available, is_vision_available

from ...test_image_processing_common import ImageProcessingTestMixin, prepare_image_inputs

Expand All @@ -31,6 +31,9 @@

from transformers import GLPNImageProcessor

if is_torchvision_available():
from transformers import GLPNImageProcessorFast


class GLPNImageProcessingTester:
def __init__(
Expand Down Expand Up @@ -87,19 +90,32 @@ def prepare_image_inputs(self, equal_resolution=False, numpify=False, torchify=F
torchify=torchify,
)

def prepare_depth_outputs(self):
if not is_torch_available():
return None
depth_tensors = prepare_image_inputs(
batch_size=self.batch_size,
num_channels=1,
min_resolution=self.min_resolution,
max_resolution=self.max_resolution,
equal_resolution=True,
torchify=True,
)
depth_tensors = [depth_tensor.squeeze(0) for depth_tensor in depth_tensors]
stacked_depth_tensors = torch.stack(depth_tensors, dim=0)
return type("DepthOutput", (), {"predicted_depth": stacked_depth_tensors})


@require_torch
@require_vision
class GLPNImageProcessingTest(ImageProcessingTestMixin, unittest.TestCase):
image_processing_class = GLPNImageProcessor if is_vision_available() else None
fast_image_processing_class = GLPNImageProcessorFast if is_torchvision_available() else None

def setUp(self):
super().setUp()
self.image_processor_tester = GLPNImageProcessingTester(self)

@property
def image_processor_dict(self):
return self.image_processor_tester.prepare_image_processor_dict()
self.image_processor_dict = self.image_processor_tester.prepare_image_processor_dict()

def test_image_processor_properties(self):
image_processing = self.image_processing_class(**self.image_processor_dict)
Expand All @@ -115,7 +131,6 @@ def test_call_pil(self):
image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False)
for image in image_inputs:
self.assertIsInstance(image, Image.Image)

# Test not batched input (GLPNImageProcessor doesn't support batching)
encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
Expand Down Expand Up @@ -161,3 +176,60 @@ def test_call_numpy_4_channels(self):
expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
self.assertTrue(tuple(encoded_images.shape) == (1, *expected_output_image_shape))
self.image_processing_class.num_channels = 3

def test_slow_fast_equivalence(self):
# Verify that the fast (torchvision) and slow (PIL) paths give identical pixel outputs
if self.fast_image_processing_class is None:
self.skipTest("TorchVision not available")

# Random RGB test image
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
slow = self.image_processing_class(**self.image_processor_dict)
fast = self.fast_image_processing_class(**self.image_processor_dict)

out_slow = slow(images=image, return_tensors="pt")["pixel_values"]
out_fast = fast(images=image, return_tensors="pt")["pixel_values"]

torch.testing.assert_close(out_slow, out_fast, atol=1e-7, rtol=1e-5)

def test_slow_fast_equivalence_batched(self):
# Verify that fast and slow processors handle batched heterogeneous images identically
if self.fast_image_processing_class is None:
self.skipTest("TorchVision not available")

# Create batch of images with different resolutions
image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, torchify=True)

slow = self.image_processing_class(**self.image_processor_dict)
fast = self.fast_image_processing_class(**self.image_processor_dict)

out_slow = slow(images=image_inputs, return_tensors="pt")["pixel_values"]
out_fast = fast(images=image_inputs, return_tensors="pt")["pixel_values"]

# Check shapes match (padding should make them equal)
self.assertEqual(out_slow.shape, out_fast.shape)

# Check pixel values are close
torch.testing.assert_close(out_slow, out_fast, atol=1e-1, rtol=1e-3)
self.assertLessEqual(torch.mean(torch.abs(out_slow - out_fast)).item(), 5e-3)

def test_post_process_depth_equivalence(self):
# Check that both processors produce equivalent post-processed depth maps
if self.fast_image_processing_class is None:
self.skipTest("TorchVision not available")

outputs = self.image_processor_tester.prepare_depth_outputs()
slow = self.image_processing_class(**self.image_processor_dict)
fast = self.fast_image_processing_class(**self.image_processor_dict)

# target_sizes simulate resized inference outputs
target_sizes = [(240, 320)] * self.image_processor_tester.batch_size
processed_slow = slow.post_process_depth_estimation(outputs, target_sizes=target_sizes)
processed_fast = fast.post_process_depth_estimation(outputs, target_sizes=target_sizes)

# Compare per-sample predicted depth tensors
for pred_slow, pred_fast in zip(processed_slow, processed_fast):
depth_slow = pred_slow["predicted_depth"]
depth_fast = pred_fast["predicted_depth"]
torch.testing.assert_close(depth_fast, depth_slow, atol=1e-1, rtol=1e-3)
self.assertLessEqual(torch.mean(torch.abs(depth_fast.float() - depth_slow.float())).item(), 5e-3)