Skip to content

Commit

Permalink
Use TurboJpeg for faster loading of jpeg files.
Browse files Browse the repository at this point in the history
Make previous/next toolbar buttons autorepeat.
Create Texture class as new end-point for ImageLoader.
Move ImageData class to its own file.
Move Performance class to its own file.
Create stub for future windows thumbnail loading.
  • Loading branch information
cmbruns committed Jun 23, 2024
1 parent 200e3d3 commit 2f3ae09
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 303 deletions.
103 changes: 98 additions & 5 deletions test/loading_performance_test.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,86 @@
"""
Compare speed of different ways of handling images
* pre-multiply alpha
* using numpy
* using PIL
* using OpenGL
* not pre-multiplying
* srgb to linear
* not doing that
* numpy
* ?
* pre-rotate orientation
* not
* PIL
* OpenGL
* load large jpeg
* PIL
* (intermediate) PIL thumbnail/draft
* PIL-SIMD
* Turbo-JPEG
"""
import numpy
import pkg_resources
from OpenGL import GL

from PIL.Image import Resampling
from PySide6.QtGui import QSurfaceFormat, QOpenGLContext, QOffscreenSurface
from PySide6.QtWidgets import QApplication
import turbojpeg
from turbojpeg import TJFLAG_FASTUPSAMPLE, TJFLAG_FASTDCT

from vmg.image_loader import Performance, ImageData
from vmg.image_data import ImageData
from vmg.performance import Performance


jpeg = turbojpeg.TurboJPEG()


def main():
file_name = pkg_resources.resource_filename("vmg.images", "hopper.gif")
profile_image(file_name)
hopper_name = pkg_resources.resource_filename("vmg.images", "hopper.gif")
for file_name in [
# hopper_name,
# r"\\diskstation\Public\Pictures\2024\WaterLeak\R0016689.JPG",
# r"C:\Users\cmbruns\Pictures\Space_Needle_panorama_large.jpg",
r"C:\Users\cmbruns\Pictures\_Bundles_for_Berlin__More_production!.jpg", # 30kx42k
# r"C:\Users\cmbruns\Pictures\borf3.jpg",
]:
# profile_image(file_name)
turbo_jpeg_case(file_name)


def profile_image(file_name):
print(f"Loading file {file_name} :")
with Performance(message="check existence", indent=1):
image_data = ImageData(str(file_name))
file_exists = image_data.file_is_readable()
_file_exists = image_data.file_is_readable()
with Performance(message="open PIL image", indent=1):
image_data.open_pil_image()
with Performance(message="read PIL metadata", indent=1):
image_data.read_pil_metadata()
with Performance(message="load PIL image", indent=1):
# image_data.pil_image.thumbnail([200, 200], resample=Resampling.NEAREST)
bytes1 = image_data.pil_image.tobytes()
with Performance(message="convert to numpy", indent=1):
numpy_image = numpy.array(image_data.pil_image)


def turbo_jpeg_case(file_name):
with Performance(message="copy jpeg file to memory", indent=1):
with open(file_name, "rb") as in_file:
jpeg_bytes = in_file.read()
with Performance(message="load with turbojpeg SCALE (1, 4)", indent=1):
bgr_array = jpeg.decode(jpeg_bytes, scaling_factor=(1, 4))
with Performance(message="load with turbojpeg scale (1, 2)", indent=1):
bgr_array = jpeg.decode(jpeg_bytes, scaling_factor=(1, 2))
with Performance(message="load with turbojpeg FAST", indent=1):
bgr_array = jpeg.decode(jpeg_bytes, flags=TJFLAG_FASTUPSAMPLE | TJFLAG_FASTDCT)
with Performance(message="load with turbojpeg", indent=1):
bgr_array = jpeg.decode(jpeg_bytes)


def opengl_something():
Expand All @@ -35,9 +98,39 @@ def opengl_something():
surface.create()
assert surface.isValid()
gl_context.makeCurrent(surface)
fbo = GL.glGenFramebuffers(1)
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, fbo)
color_texture = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_2D, color_texture)
GL.glTexImage2D(
GL.GL_TEXTURE_2D,
0,
GL.GL_RGB,
800, 600,
0,
GL.GL_RGB,
GL.GL_UNSIGNED_BYTE,
None,
)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
GL.glBindTexture(GL.GL_TEXTURE_2D, 0);
GL.glFramebufferTexture2D(
GL.GL_FRAMEBUFFER,
GL.GL_COLOR_ATTACHMENT0,
GL.GL_TEXTURE_2D,
color_texture,
0
)
assert GL.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) == GL.GL_FRAMEBUFFER_COMPLETE
# GL.glClearColor(1, 1, 1, 1)
# GL.glClear(GL.GL_COLOR_BUFFER_BIT)
#
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
GL.glDeleteFramebuffers(1, [fbo, ])
gl_context.doneCurrent()


if __name__ == "__main__":
main()
opengl_something()
# opengl_something()
147 changes: 147 additions & 0 deletions vmg/image_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from math import cos, radians, sin
from os import access, R_OK
from os.path import isfile

import numpy
from OpenGL import GL
from PIL import Image, ExifTags
from PySide6 import QtCore
import turbojpeg

from vmg.frame import DimensionsOmp
from vmg.texture import Texture


class ImageData(QtCore.QObject):
def __init__(self, file_name: str, parent=None):
super().__init__(parent=parent)
self.file_name = str(file_name)
self.pil_image = Image.open(self.file_name)
self.numpy_image = None
self.texture = None
self.exif = {}
self.xmp = {}
self.size_raw = [0, 0]
self.size_omp = DimensionsOmp(0, 0)
self._raw_rot_ont = numpy.eye(3, dtype=numpy.float32)
self._raw_rot_omp = numpy.eye(2, dtype=numpy.float32)
self._is_360 = False

def file_is_readable(self) -> bool:
file_name = self.file_name
if not isfile(file_name):
return False
if not access(file_name, R_OK):
return False
return True

@property
def is_360(self) -> bool:
return self._is_360

def load_jpeg_image(self) -> bool:
# TODO: split into smaller parts
try:
jpeg = turbojpeg.TurboJPEG() # TODO: maybe cache this
with open(self.file_name, "rb") as in_file:
jpeg_bytes = in_file.read()
bgr_array = jpeg.decode(jpeg_bytes)
self.texture = Texture.from_numpy(bgr_array, tex_format=GL.GL_BGR)
return True
except ...:
return False

def open_pil_image(self) -> bool:
try:
self.pil_image = Image.open(self.file_name)
return True
except ...:
return False

def read_pil_metadata(self):
raw_width, raw_height = self.pil_image.size # Unrotated dimension
self.size_raw = (raw_width, raw_height)
exif0 = self.pil_image.getexif()
exif = {
ExifTags.TAGS[k]: v
for k, v in exif0.items()
if k in ExifTags.TAGS
}
for ifd_id in ExifTags.IFD:
try:
ifd = exif0.get_ifd(ifd_id)
if ifd_id == ExifTags.IFD.GPSInfo:
resolve = ExifTags.GPSTAGS
else:
resolve = ExifTags.TAGS
for k, v in ifd.items():
tag = resolve.get(k, k)
exif[tag] = v
except KeyError:
pass
try:
xmp = self.pil_image.getxmp() # noqa
except AttributeError:
xmp = {}
self.xmp = xmp
self.exif = exif
orientation_code: int = exif.get("Orientation", 1)
self._raw_rot_omp = self.rotation_for_exif_orientation.get(orientation_code, numpy.eye(2, dtype=numpy.float32))
self.size_omp = DimensionsOmp(*[abs(x) for x in (self.raw_rot_omp.T @ self.size_raw)])
if self.size_omp.x == 2 * self.size_omp.y:
try:
self._is_360 = True
try:
# TODO: InitialViewHeadingDegrees
desc = xmp["xmpmeta"]["RDF"]["Description"]
heading = radians(float(desc["PoseHeadingDegrees"]))
pitch = radians(float(desc["PosePitchDegrees"]))
roll = radians(float(desc["PoseRollDegrees"]))
m = numpy.array([
[cos(roll), -sin(roll), 0],
[sin(roll), cos(roll), 0],
[0, 0, 1],
], dtype=numpy.float32)
m = m @ [
[1, 0, 0],
[0, cos(pitch), sin(pitch)],
[0, -sin(pitch), cos(pitch)],
]
m = m @ [
[cos(heading), 0, sin(heading)],
[0, 1, 0],
[-sin(heading), 0, cos(heading)],
]
self._raw_rot_ont = m
except (KeyError, TypeError):
pass
if exif["Model"].lower().startswith("ricoh theta"):
# print("360")
pass # TODO 360 image
except KeyError:
pass
else:
self._is_360 = False

@property
def raw_rot_omp(self) -> numpy.array:
return self._raw_rot_omp

@property
def raw_rot_ont(self) -> numpy.array:
return self._raw_rot_ont

@property
def size(self) -> DimensionsOmp:
return self.size_omp

rotation_for_exif_orientation = {
1: numpy.array([[1, 0], [0, 1]], dtype=numpy.float32),
2: numpy.array([[-1, 0], [0, 1]], dtype=numpy.float32),
3: numpy.array([[-1, 0], [0, -1]], dtype=numpy.float32),
4: numpy.array([[1, 0], [0, -1]], dtype=numpy.float32),
5: numpy.array([[0, 1], [1, 0]], dtype=numpy.float32),
6: numpy.array([[0, 1], [-1, 0]], dtype=numpy.float32),
7: numpy.array([[0, -1], [-1, 0]], dtype=numpy.float32),
8: numpy.array([[0, -1], [1, 0]], dtype=numpy.float32),
}
Loading

0 comments on commit 2f3ae09

Please sign in to comment.