A Python wrapper for libjpeg-turbo that enables efficient JPEG image decoding and encoding.
- libjpeg-turbo 3.0 or later (required for PyTurboJPEG 2.0+)
- numpy
Important: PyTurboJPEG 2.0+ requires libjpeg-turbo 3.0 or later as it uses the new function-based TurboJPEG 3 API. For libjpeg-turbo 2.x compatibility, please use PyTurboJPEG 1.x.
brew install jpeg-turbo
pip install -U git+https://github.com/lilohuang/PyTurboJPEG.git- Download the libjpeg-turbo official installer
- Install PyTurboJPEG:
pip install -U git+https://github.com/lilohuang/PyTurboJPEG.git
- Download the libjpeg-turbo official installer
- Install PyTurboJPEG:
pip install -U git+https://github.com/lilohuang/PyTurboJPEG.git
from turbojpeg import TurboJPEG
# Use default library installation
jpeg = TurboJPEG()
# Or specify library path explicitly
# jpeg = TurboJPEG(r'D:\turbojpeg.dll') # Windows
# jpeg = TurboJPEG('/usr/lib64/libturbojpeg.so') # Linux
# jpeg = TurboJPEG('/usr/local/lib/libturbojpeg.dylib') # macOSimport cv2
from turbojpeg import TurboJPEG, TJPF_GRAY, TJFLAG_FASTUPSAMPLE, TJFLAG_FASTDCT
jpeg = TurboJPEG()
# Basic decoding to BGR array
with open('input.jpg', 'rb') as f:
bgr_array = jpeg.decode(f.read())
cv2.imshow('bgr_array', bgr_array)
cv2.waitKey(0)
# Fast decoding (lower accuracy, higher speed)
with open('input.jpg', 'rb') as f:
bgr_array = jpeg.decode(f.read(), flags=TJFLAG_FASTUPSAMPLE|TJFLAG_FASTDCT)
# Decode with direct rescaling (1/2 size)
with open('input.jpg', 'rb') as f:
bgr_array_half = jpeg.decode(f.read(), scaling_factor=(1, 2))
# Get available scaling factors
scaling_factors = jpeg.scaling_factors
# Decode to grayscale
with open('input.jpg', 'rb') as f:
gray_array = jpeg.decode(f.read(), pixel_format=TJPF_GRAY)# Get image properties without full decoding (backward compatible)
with open('input.jpg', 'rb') as f:
width, height, jpeg_subsample, jpeg_colorspace = jpeg.decode_header(f.read())
# Get precision to select appropriate decode function
with open('input.jpg', 'rb') as f:
jpeg_data = f.read()
width, height, jpeg_subsample, jpeg_colorspace, precision = jpeg.decode_header(jpeg_data, return_precision=True)
# Use precision to select appropriate decode function
if precision == 8:
img = jpeg.decode(jpeg_data)
elif precision == 12:
img = jpeg.decode_12bit(jpeg_data)
elif precision == 16:
img = jpeg.decode_16bit(jpeg_data)# Decode to YUV buffer
with open('input.jpg', 'rb') as f:
buffer_array, plane_sizes = jpeg.decode_to_yuv(f.read())
# Decode to YUV planes
with open('input.jpg', 'rb') as f:
planes = jpeg.decode_to_yuv_planes(f.read())from turbojpeg import TJSAMP_GRAY, TJFLAG_PROGRESSIVE
# Basic encoding with default settings
with open('output.jpg', 'wb') as f:
f.write(jpeg.encode(bgr_array))
# Encode with grayscale subsample
with open('output_gray.jpg', 'wb') as f:
f.write(jpeg.encode(bgr_array, jpeg_subsample=TJSAMP_GRAY))
# Encode with custom quality
with open('output_quality_50.jpg', 'wb') as f:
f.write(jpeg.encode(bgr_array, quality=50))
# Encode with progressive entropy coding
with open('output_progressive.jpg', 'wb') as f:
f.write(jpeg.encode(bgr_array, quality=100, flags=TJFLAG_PROGRESSIVE))
# Encode with lossless JPEG compression
with open('output_gray.jpg', 'wb') as f:
f.write(jpeg.encode(bgr_array, lossless=True))# Scale with quality (without color conversion)
with open('input.jpg', 'rb') as f:
scaled_data = jpeg.scale_with_quality(f.read(), scaling_factor=(1, 4), quality=70)
with open('scaled_output.jpg', 'wb') as f:
f.write(scaled_data)
# Lossless crop
with open('input.jpg', 'rb') as f:
cropped_data = jpeg.crop(f.read(), 8, 8, 320, 240)
with open('cropped_output.jpg', 'wb') as f:
f.write(cropped_data)import numpy as np
# In-place decoding (reuse existing array)
img_array = np.empty((640, 480, 3), dtype=np.uint8)
with open('input.jpg', 'rb') as f:
result = jpeg.decode(f.read(), dst=img_array)
# result is the same as img_array: id(result) == id(img_array)
# In-place encoding (reuse existing buffer)
buffer_size = jpeg.buffer_size(img_array)
dest_buf = bytearray(buffer_size)
result, n_bytes = jpeg.encode(img_array, dst=dest_buf)
with open('output.jpg', 'wb') as f:
f.write(dest_buf[:n_bytes])
# result is the same as dest_buf: id(result) == id(dest_buf)import cv2
import numpy as np
import exifread
from turbojpeg import TurboJPEG
def transpose_image(image, orientation):
"""Transpose image based on EXIF Orientation tag.
See: https://www.exif.org/Exif2-2.PDF
"""
if orientation is None:
return image
val = orientation.values[0]
if val == 1: return image
elif val == 2: return np.fliplr(image)
elif val == 3: return np.rot90(image, 2)
elif val == 4: return np.flipud(image)
elif val == 5: return np.rot90(np.flipud(image), -1)
elif val == 6: return np.rot90(image, -1)
elif val == 7: return np.rot90(np.flipud(image))
elif val == 8: return np.rot90(image)
jpeg = TurboJPEG()
with open('foobar.jpg', 'rb') as f:
# Parse EXIF orientation
orientation = exifread.process_file(f).get('Image Orientation', None)
# Decode image
f.seek(0)
image = jpeg.decode(f.read())
# Apply orientation transformation
transposed_image = transpose_image(image, orientation)
cv2.imshow('transposed_image', transposed_image)
cv2.waitKey(0)import io
import numpy as np
from PIL import Image, ImageCms
from turbojpeg import TurboJPEG, TJPF_BGR
def decode_jpeg_with_color_management(jpeg_path):
"""
Decodes a JPEG and applies color management (ICC Profile to sRGB).
Args:
jpeg_path (str): Path to the input JPEG file.
Returns:
PIL.Image: The color-corrected sRGB Image object.
"""
# 1. Initialize TurboJPEG
jpeg = TurboJPEG()
with open(jpeg_path, 'rb') as f:
jpeg_data = f.read()
# 2. Get image headers and decode pixels
# Using TJPF_BGR format (OpenCV standard) for the raw buffer
width, height, _, _ = jpeg.decode_header(jpeg_data)
pixels = jpeg.decode(jpeg_data, pixel_format=TJPF_BGR)
# 3. Encapsulate into a Pillow Image object
# Key: Use 'raw' and 'BGR' decoder to correctly map BGR bytes to an RGB Image object
img = Image.frombytes('RGB', (width, height), pixels, 'raw', 'BGR')
# 4. Handle ICC Profile transformation
try:
# Extract embedded ICC Profile
icc_profile = jpeg.get_icc_profile(jpeg_data)
if icc_profile:
# Create Source and Destination Profile objects
src_profile = ImageCms.getOpenProfile(io.BytesIO(icc_profile))
dst_profile = ImageCms.createProfile("sRGB")
# Perform color transformation (similar to "Convert to Profile" in Photoshop)
# This step recalculates pixel values to align with sRGB standards
img = ImageCms.profileToProfile(
img,
src_profile,
dst_profile,
outputMode='RGB'
)
print(f"Successfully applied ICC profile from {jpeg_path}")
else:
print("No ICC profile found, assuming sRGB.")
except Exception as e:
print(f"Color Management Error: {e}. Returning original raw image.")
return img
# --- Example Usage ---
if __name__ == "__main__":
result_img = decode_jpeg_with_color_management('icc_profile.jpg')
result_img.show()
# result_img.save('output_srgb.jpg', quality=95)PyTurboJPEG 2.0+ supports 12-bit and 16-bit precision JPEG encoding and decoding using libjpeg-turbo 3.0+ APIs. This feature is ideal for medical imaging, scientific photography, and other applications requiring higher bit depth.
Requirements:
- libjpeg-turbo 3.0 or later (12-bit and 16-bit support is built-in)
Precision Modes:
- 12-bit JPEG: Supports both lossy and lossless compression
- 16-bit JPEG: Only supports lossless compression (JPEG standard limitation)
12-bit JPEG provides higher precision than standard 8-bit JPEG while maintaining compatibility with lossy compression.
import numpy as np
from turbojpeg import TurboJPEG
jpeg = TurboJPEG()
# Create 12-bit image (values range from 0 to 4095)
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
# Encode to 12-bit lossy JPEG
jpeg_data = jpeg.encode_12bit(img_12bit, quality=95)
# Decode from 12-bit JPEG
decoded_img = jpeg.decode_12bit(jpeg_data)
# Save to file
with open('output_12bit.jpg', 'wb') as f:
f.write(jpeg_data)
# Load from file
with open('output_12bit.jpg', 'rb') as f:
decoded_from_file = jpeg.decode_12bit(f.read())12-bit and 16-bit JPEG support lossless compression for perfect reconstruction:
12-bit precision with lossless compression:
import numpy as np
from turbojpeg import TurboJPEG
jpeg = TurboJPEG()
# Create 12-bit image
img_12bit = np.random.randint(0, 4096, (480, 640, 3), dtype=np.uint16)
# Encode to 12-bit lossless JPEG using encode_12bit() with lossless=True
jpeg_data = jpeg.encode_12bit(img_12bit, lossless=True)
# Decode using decode_12bit()
decoded_img = jpeg.decode_12bit(jpeg_data)
# Perfect reconstruction
assert np.array_equal(img_12bit, decoded_img) # True16-bit JPEG provides the highest precision with perfect reconstruction through lossless compression. The JPEG standard only supports 16-bit for lossless mode.
import numpy as np
from turbojpeg import TurboJPEG
jpeg = TurboJPEG()
# Create 16-bit image (values range from 0 to 65535)
img_16bit = np.random.randint(0, 65536, (480, 640, 3), dtype=np.uint16)
# Encode to 16-bit lossless JPEG
jpeg_data = jpeg.encode_16bit(img_16bit)
# Decode from 16-bit lossless JPEG
decoded_img = jpeg.decode_16bit(jpeg_data)
# Verify perfect reconstruction (lossless)
assert np.array_equal(img_16bit, decoded_img) # True
# Save to file
with open('output_16bit_lossless.jpg', 'wb') as f:
f.write(jpeg_data)
# Load from file
with open('output_16bit_lossless.jpg', 'rb') as f:
decoded_from_file = jpeg.decode_16bit(f.read())For medical and scientific applications, 12-bit JPEG provides excellent precision while maintaining file size efficiency:
import numpy as np
from turbojpeg import TurboJPEG, TJPF_GRAY, TJSAMP_GRAY
jpeg = TurboJPEG()
# Create 12-bit medical image (e.g., DICOM format)
# Medical images typically use 0-4095 range
medical_img = np.random.randint(0, 4096, (512, 512, 1), dtype=np.uint16)
# Encode with highest quality for medical applications
jpeg_medical = jpeg.encode_12bit(
medical_img,
pixel_format=TJPF_GRAY,
jpeg_subsample=TJSAMP_GRAY,
quality=100
)
# Decode for analysis
decoded_medical = jpeg.decode_12bit(jpeg_medical, pixel_format=TJPF_GRAY)
# Verify value range preservation
print(f"Original range: [{medical_img.min()}, {medical_img.max()}]")
print(f"Decoded range: [{decoded_medical.min()}, {decoded_medical.max()}]")See the LICENSE file for details.