summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Keller <tjkeller.xyz>2025-05-25 21:38:37 -0500
committerTim Keller <tjkeller.xyz>2025-05-25 21:38:37 -0500
commite1a6fc09afc088dcb67263ed5923f5be41c32c31 (patch)
treea3f80830180881ed4b55687b0870f83bdc006b2b
parent31f1940ec8c4aba6a0c21b20eff9657d5d11cf80 (diff)
downloadimmich-frame-e1a6fc09afc088dcb67263ed5923f5be41c32c31.tar.xz
immich-frame-e1a6fc09afc088dcb67263ed5923f5be41c32c31.zip
use lazy caching texture list to limit number of images loaded at one time
-rw-r--r--immich.py22
-rw-r--r--lazycachelist.py126
-rw-r--r--pix.py7
m---------static10
-rw-r--r--texture.py26
-rw-r--r--window.py17
6 files changed, 182 insertions, 26 deletions
diff --git a/immich.py b/immich.py
index 63e6f63..e73b34e 100644
--- a/immich.py
+++ b/immich.py
@@ -1,5 +1,6 @@
import requests
from io import BytesIO
+from queue import Queue
from texture import ImageTexture
@@ -8,6 +9,7 @@ class ImmichConnector:
def __init__(self, server_url, api_key):
self.server_url = server_url.removesuffix("/")
self.api_key = api_key
+ self.texture_load_queue = Queue()
def _request(self, endpoint):
return requests.get(f"{self.server_url}/api/{endpoint}", headers={ "x-api-key": self.api_key })
@@ -34,12 +36,14 @@ class ImmichConnector:
mimetype = response.headers.get("Content-Type")
return image_data, mimetype
- def load_texture(self, pd, key, exif=None, size="preview"):
- image_data, _ = self.load_image(key, size)
- it = ImageTexture(image_data, exif)
- pd.textures.append(it)
- print(f"Loaded image {key}")
-
- def idle(self, pd):
- for asset in self.load_album_assets("bac029a5-972b-4519-bce0-a0d74add3969"):
- self.load_texture(pd, asset["id"], asset["exifInfo"])
+ def load_texture_async(self, texture_list, image_texture):
+ self.texture_load_queue.put((texture_list, image_texture))
+
+ def idle(self):
+ size = "preview" # TODO
+ while True:
+ texture_list, image_texture = self.texture_load_queue.get()
+ if not texture_list.index_in_cache_range(image_texture.asset_index):
+ continue # Texture was never loaded so it doesn't need to be free'd
+ image_data, _ = self.load_image(image_texture.asset_key, size)
+ image_texture.initialize(image_data)
diff --git a/lazycachelist.py b/lazycachelist.py
new file mode 100644
index 0000000..0c37ba3
--- /dev/null
+++ b/lazycachelist.py
@@ -0,0 +1,126 @@
+from dataclasses import dataclass
+
+from texture import ImageTextureImmichAsset
+
+@dataclass
+class Album:
+ id: str
+ range_start: int
+ range_end: int
+ assets_list: list[str] = None
+
+ @property
+ def assets_count(self):
+ return end - start
+
+
+class LazyCachingTextureList():
+ def __init__(self, immich_connector, album_ids, max_cache_items=100):
+ self.immich_connector = immich_connector
+ assert max_cache_items >= 20, "Minimum cache items is 20" # Double small radius
+
+ # Ring buffer
+ self.cache_length = max_cache_items
+ self.cache = [None] * max_cache_items
+ self.radius_small = 10
+ self.radius_large = int(max_cache_items / 2)
+ self.cache_items_behind = 0
+ self.cache_items_ahead = 0
+ self.cache_index = 0
+ self.asset_index = 0
+ self.asset_count = 0
+
+ self.cached_items = 0
+
+ self.album_keys = album_ids
+ self.albums = self._get_albums()
+
+ def index_in_cache_range(self, index):
+ index_range_low = (self.asset_index - self.cache_items_behind) % self.asset_count
+ index_range_high = (self.asset_index + self.cache_items_ahead ) % self.asset_count
+ if index_range_low > index_range_high:
+ return index_range_low <= index or index <= index_range_high
+ return index_range_low <= index <= index_range_high
+
+ def _get_albums(self):
+ albums = []
+ self.asset_count = i = 0
+ albums_info = self.immich_connector.load_all_albums()
+ for album_info in albums_info:
+ id = album_info["id"]
+ if id not in self.album_keys:
+ continue
+ asset_count = album_info["assetCount"]
+ albums.append(Album(id, i, i + asset_count))
+ i += asset_count + 1
+ self.asset_count += asset_count
+ #self.asset_count = sum(( album.assets_count for album in self.albums ))
+ return albums
+
+ def _get_album_asset_by_index(self, asset_index):
+ if asset_index < 0:
+ asset_index = asset_count - asset_index
+ for album in self.albums:
+ if album.range_start <= asset_index <= album.range_end:
+ if album.assets_list is None:
+ album.assets_list = self.immich_connector.load_album_assets(album.id)
+ return album.assets_list[asset_index - album.range_start]
+ raise IndexError("Index out of bounds")
+
+ def _fill_cache(self, current_radius, final_radius, step):
+ if current_radius >= final_radius:
+ return current_radius
+
+ for i in range(current_radius * step, final_radius * step, step):
+ cache_index = (self.cache_index + i) % self.cache_length
+ asset_index = (self.asset_index + i) % self.asset_count
+ if self.cache[cache_index]:
+ self.cache[cache_index].free() # Since this is a ring buffer, textures outside of range can just get free'd here
+ asset = self._get_album_asset_by_index(asset_index)
+ tex = ImageTextureImmichAsset(asset, asset_index)
+ self.immich_connector.load_texture_async(self, tex)
+ self.cache[cache_index] = tex
+ self.cached_items += 1
+
+ return max(current_radius, final_radius)
+
+ def _update_cache_get_item(self, asset_index):
+ prev_asset_index = self.asset_index
+ self.asset_index = asset_index % self.asset_count
+ # Movement is the distance between the previous and current index in the assets list
+ # Since the list wraps around, fastest method is just to get the abs min of the 3 cases
+ movement = min(
+ asset_index - prev_asset_index, # No list wrap
+ asset_index - self.asset_count, # Wrap backwards (0 -> -1)
+ self.asset_count - prev_asset_index, # Wrap forwards (-1 -> 0)
+ key=abs)
+ self.cache_index = (self.cache_index + movement) % self.cache_length
+
+ ahead = max(0, self.cache_items_ahead - movement)
+ behind = max(0, self.cache_items_behind + movement)
+
+ if ahead + behind > self.cache_length:
+ if movement < 0:
+ ahead += movement
+ else:
+ behind -= movement
+
+ #print("AHEAD/BEHIND/CACHE_I/ASSET_I/MOVEMENT:", ahead, behind, self.cache_index, self.asset_index, movement)
+ # TODO if ahead is 0 then clear queue
+
+ ahead = self._fill_cache(ahead, self.radius_small, +1) # Fill small radius ahead of cache_index
+ behind = self._fill_cache(behind, self.radius_small, -1) # Fill small radius behind cache_index
+ ahead = self._fill_cache(ahead, self.radius_large, +1) # Fill large radius ahead of cache_index
+
+ self.cache_items_ahead = ahead
+ self.cache_items_behind = behind
+ return self.cache[self.cache_index]
+
+ def __len__(self):
+ return self.asset_count
+
+ def __getitem__(self, index):
+ i = index % self.asset_count
+ if abs(index) > i:
+ raise IndexError("Index out of bounds")
+ return self._update_cache_get_item(index)
diff --git a/pix.py b/pix.py
index aa1495f..4064892 100644
--- a/pix.py
+++ b/pix.py
@@ -3,6 +3,7 @@ import signal
from threading import Thread
from OpenGL.GLUT import glutLeaveMainLoop
+from lazycachelist import LazyCachingTextureList
from window import PixDisplay
from immich import ImmichConnector
from flaskapi import app
@@ -19,10 +20,12 @@ def handle_sigint(sig, frame):
if __name__ == "__main__":
- pd = PixDisplay()
immich_connector = ImmichConnector("http://192.168.1.13", "m5nqOoBc4uhAba21gZdCP3z8D3JT4GPxDXL2psd52EA")
+ album_keys = [ "38617851-6b57-44f1-b5f7-82577606afc4" ]
+ lazy_texture_list = LazyCachingTextureList(immich_connector, album_keys, 30)
+ pd = PixDisplay(lazy_texture_list)
- t1 = Thread(target=immich_connector.idle, daemon=True, args=(pd,))
+ t1 = Thread(target=immich_connector.idle, daemon=True)
t1.start()
app.config["pix_display"] = pd
diff --git a/static b/static
-Subproject f86d11c3ce1f04ee89da235d78447aed6d6d713
+Subproject 51be71d8943f6e5e0c4b28358f227860c73d53a
diff --git a/texture.py b/texture.py
index ebd1a49..3a56b5f 100644
--- a/texture.py
+++ b/texture.py
@@ -3,6 +3,7 @@ from OpenGL.GLUT import *
from OpenGL.GLU import *
from PIL import Image, ExifTags
+
class ImageTexture:
def __init__(self, image_source, exif=None):
self.id = None
@@ -13,6 +14,7 @@ class ImageTexture:
self.width = img.width
self.height = img.height
self._img_data = img.tobytes("raw", "RGBA", 0, -1) # Convert image data to bytes
+ self.initialized = True
def gl_init(self):
if self.id:
@@ -26,7 +28,14 @@ class ImageTexture:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
del self._img_data # No longer needed, clear mem
- print(f"Loaded texture {self.id}")
+ #print(f"Loaded texture {self.id}")
+
+ def free(self):
+ if self.id is None:
+ del self._img_data
+ else:
+ glBindTexture(GL_TEXTURE_2D, 0) # Unbind
+ glDeleteTextures([self.id])
@staticmethod
def get_exif(img):
@@ -41,3 +50,18 @@ class ImageTexture:
if orientation == 8: return img.rotate( 90, expand=True)
return img
+
+
+class ImageTextureImmichAsset(ImageTexture):
+ def __init__(self, asset, asset_index):
+ self.initialized = False
+ self.asset_index = asset_index
+ self.asset_key = asset["id"]
+ self.exif = asset.get("exif", None)
+
+ # So that free can work
+ self.id = None
+ self._img_data = None
+
+ def initialize(self, image_source):
+ super().__init__(image_source, self.exif)
diff --git a/window.py b/window.py
index 24320b9..1523f85 100644
--- a/window.py
+++ b/window.py
@@ -5,16 +5,15 @@ from time import time
from queue import Queue
from renderer import ImageRenderer, TransitionMix
-from texture import ImageTexture
class PixDisplay:
- def __init__(self):
+ def __init__(self, textures):
self.last_time = 0
self.start_time = 0
self.image_time = 0
self.paused = False
- self.textures = []
+ self.textures = textures
self.current_texture_index = 0
self.renderer = None
self.win_w = 0
@@ -42,16 +41,16 @@ class PixDisplay:
self.frame_time = int(1000 / 60) # In ms
def increment_texture_index(self, increment):
- if len(self.textures) < 2:
- return
-
self.transition_reverse = increment < 0
self.tex_prev = self.textures[self.current_texture_index]
self.current_texture_index = (self.current_texture_index + increment) % len(self.textures)
self.tex = self.textures[self.current_texture_index]
- # Ensure textures are initialized
+ if not self.tex.initialized or not self.tex_prev.initialized:
+ return
+
+ # Ensure textures are initialized for opengl
self.tex_prev.gl_init()
self.tex.gl_init()
@@ -63,10 +62,10 @@ class PixDisplay:
delta_time = current_time - self.last_time
self.last_time = current_time
- if not self.tex:
+ if not self.tex or not self.tex.initialized or not self.tex.id:
self.increment_texture_index(0)
# Draw black window if no textures are available
- if not self.tex:
+ if not self.tex.id:
glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glutSwapBuffers()