Batch Resize of Textures in Python
16 Feb 26 (4d ago)
Load the textures, hit render, and wait for the output. This is fine for final renders, but for look dev where you need several iterations at a certain speed and stability to gauge your shading setup, you need some lesser version of the textures—especially if you are dealing with 40MB-100MB EXR UDIM files.
Some renderers perform their own optimizations, such as Arnold converting files to .tx format, but not quite for others.
Thus, I make my own optimizations by simply resizing the textures.
The Setup
I prefer keeping two versions at a time so I can just swap them easily with renaming. This way, you don't have to mess with your shading paths.
During look dev:
- tex(resized)- tex_raw(original)
For final render:
- tex(previouslytex_raw)- tex_resize(previously justtex)
The Script
Have the script below next to the tex folder (which contains your textures). Run it and it should create an output folder where you have your resized versions.
The percentage parameter handles how much you resize the output image from the original image (takes 0-100 number). It also takes into account the file extensions, so an .exr will never be converted to a .jpg.
Requirements:
pip install numpy OpenEXR opencv-python Pillow
import os
import numpy as np
import OpenEXR
import Imath
import cv2
from PIL import Image
def process_all_textures(input_subfolder, percentage, quality=95):
# 1. Setup paths
script_dir = os.path.dirname(os.path.abspath(__file__))
src_dir = os.path.join(script_dir, input_subfolder)
output_base = os.path.join(script_dir, "output", input_subfolder)
if not os.path.exists(src_dir):
print(f"Error: Folder '{input_subfolder}' not found next to script.")
return
if not os.path.exists(output_base):
os.makedirs(output_base)
# 2. DEFINED TEXTURE EXTENSIONS ONLY
# This ignores .doc, .3ds, .blend, .txt, etc.
valid_texture_exts = ('.exr', '.jpg', '.jpeg', '.png', '.tiff', '.tif', '.bmp', '.tga', '.webp')
# 3. Filter the file list
all_files = os.listdir(src_dir)
texture_files = [f for f in all_files if f.lower().endswith(valid_texture_exts)]
ignored_count = len(all_files) - len(texture_files)
if not texture_files:
print(f"No valid images found in {src_dir}. (Ignored {ignored_count} non-image files)")
return
print(f"--- Starting Batch: {len(texture_files)} textures found (Ignoring {ignored_count} files) ---")
for filename in texture_files:
src_path = os.path.join(src_dir, filename)
output_path = os.path.join(output_base, filename)
ext = os.path.splitext(filename)[1].lower()
scale = percentage / 100.0
# --- EXR PROCESSING ---
if ext == '.exr':
try:
exr_file = OpenEXR.InputFile(src_path)
header = exr_file.header()
dw = header['dataWindow']
orig_w, orig_h = (dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)
new_w, new_h = int(orig_w * scale), int(orig_h * scale)
resized_data = {}
for channel_name, channel_info in header['channels'].items():
pixel_type = channel_info.type
dtype = np.float16 if pixel_type == Imath.PixelType(Imath.PixelType.HALF) else np.float32
raw_channel = np.frombuffer(exr_file.channel(channel_name, pixel_type), dtype=dtype)
channel_2d = raw_channel.reshape(orig_h, orig_w)
# Resize math
channel_32 = channel_2d.astype(np.float32)
resized_32 = cv2.resize(channel_32, (new_w, new_h), interpolation=cv2.INTER_AREA)
resized_data[channel_name] = resized_32.astype(dtype).tobytes()
# Header with RLE Compression for medium file size
new_header = OpenEXR.Header(new_w, new_h)
new_header['channels'] = header['channels']
new_header['compression'] = Imath.Compression(Imath.Compression.RLE_COMPRESSION)
out = OpenEXR.OutputFile(output_path, new_header)
out.writePixels(resized_data)
out.close()
print(f"[EXR] Resized: {filename}")
except Exception as e:
print(f"[SKIP] Error processing EXR {filename}: {e}")
# --- STANDARD IMAGE PROCESSING ---
else:
try:
with Image.open(src_path) as img:
new_w, new_h = int(img.width * scale), int(img.height * scale)
resized_img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
save_params = {}
if ext in ['.jpg', '.jpeg']:
save_params['quality'] = quality
save_params['optimize'] = True
resized_img.save(output_path, **save_params)
print(f"[{ext[1:].upper()}] Resized: {filename}")
except Exception as e:
print(f"[SKIP] Error processing {filename}: {e}")
print(f"\n--- Batch Complete ---")
print(f"Results saved to: output/{input_subfolder}")
# --- EXECUTION ---
process_all_textures("tex", 25)