isLinXu
Pack FlorenceForge source for embedded HF Spaces deployment
e40db0e
Raw
History Blame Contribute Delete
5.85 kB
"""FlorenceForge 图像缓存层
从 ``dataset.py`` 抽出的按字节预算 LRU 图像 payload 缓存。
设计要点:
- 缓存的是 RGB ``bytes`` payload,而非可变的 PIL ``Image`` 对象,避免调用方的数据
增强或多线程处理修改共享对象。
- 按总字节预算淘汰(LRU),避免固定条数缓存高分辨率图像导致 RSS 放大。
- 默认预算为 256 MiB,可通过环境变量 ``FLORENCE_FORGE_IMAGE_CACHE_MAX_BYTES`` 调整。
``_load_image_cached`` 暴露 ``cache_clear`` / ``cache_info`` / ``cache_bytes`` /
``set_cache_max_bytes`` 几个属性,保留测试与调试入口。``dataset.py`` 重新导出
``_load_image_cached`` 以保持历史导入路径与单测 patch 目标不变。
"""
import os
import threading
from collections import OrderedDict, namedtuple
from pathlib import Path
from typing import Tuple
from PIL import Image
_ImagePayloadCacheInfo = namedtuple(
"_ImagePayloadCacheInfo",
["hits", "misses", "maxsize", "currsize"],
)
_IMAGE_PAYLOAD_CACHE_DEFAULT_MAX_BYTES = int(
os.environ.get("FLORENCE_FORGE_IMAGE_CACHE_MAX_BYTES", str(256 * 1024 * 1024))
)
_image_payload_cache: "OrderedDict[str, Tuple[Tuple[int, int], bytes]]" = OrderedDict()
_image_payload_cache_hits = 0
_image_payload_cache_misses = 0
_image_payload_cache_bytes = 0
_image_payload_cache_max_bytes = _IMAGE_PAYLOAD_CACHE_DEFAULT_MAX_BYTES
_image_payload_cache_lock = threading.RLock()
def _load_image_payload_cached(image_path: str) -> Tuple[Tuple[int, int], bytes]:
"""缓存图像 RGB payload,避免跨调用复用可变 PIL Image 对象
使用按字节预算的 LRU 缓存策略,避免固定条数缓存高分辨率 RGB bytes
导致 RSS 放大。默认预算可通过 FLORENCE_FORGE_IMAGE_CACHE_MAX_BYTES 调整。
Args:
image_path: 图像文件路径
Returns:
(图像尺寸, RGB 字节) 元组
Raises:
FileNotFoundError: 当图像文件不存在时
IOError: 当图像文件无法解码时
"""
global _image_payload_cache_hits
global _image_payload_cache_misses
global _image_payload_cache_bytes
if not os.path.exists(image_path):
raise FileNotFoundError(f"图像文件不存在: {image_path}")
cache_key = str(Path(image_path).resolve())
with _image_payload_cache_lock:
cached = _image_payload_cache.get(cache_key)
if cached is not None:
_image_payload_cache_hits += 1
_image_payload_cache.move_to_end(cache_key)
return cached
_image_payload_cache_misses += 1
try:
with Image.open(image_path) as img:
rgb = img.convert('RGB')
payload = rgb.tobytes()
except Exception as e:
raise IOError(f"无法加载图像 {image_path}: {e}") from e
payload_size = len(payload)
result = (rgb.size, payload)
with _image_payload_cache_lock:
if _image_payload_cache_max_bytes <= 0 or payload_size > _image_payload_cache_max_bytes:
return result
existing = _image_payload_cache.get(cache_key)
if existing is not None:
_image_payload_cache.move_to_end(cache_key)
return existing
_image_payload_cache[cache_key] = result
_image_payload_cache_bytes += payload_size
while _image_payload_cache_bytes > _image_payload_cache_max_bytes and _image_payload_cache:
_, (_, evicted_payload) = _image_payload_cache.popitem(last=False)
_image_payload_cache_bytes -= len(evicted_payload)
return result
def _image_payload_cache_clear() -> None:
global _image_payload_cache_hits
global _image_payload_cache_misses
global _image_payload_cache_bytes
with _image_payload_cache_lock:
_image_payload_cache.clear()
_image_payload_cache_hits = 0
_image_payload_cache_misses = 0
_image_payload_cache_bytes = 0
def _image_payload_cache_info() -> _ImagePayloadCacheInfo:
with _image_payload_cache_lock:
return _ImagePayloadCacheInfo(
hits=_image_payload_cache_hits,
misses=_image_payload_cache_misses,
maxsize=_image_payload_cache_max_bytes,
currsize=len(_image_payload_cache),
)
def _image_payload_cache_current_bytes() -> int:
with _image_payload_cache_lock:
return _image_payload_cache_bytes
def _set_image_payload_cache_max_bytes(max_bytes: int) -> int:
"""设置图像 payload 缓存预算,返回旧预算,供测试/诊断使用。"""
global _image_payload_cache_bytes
global _image_payload_cache_max_bytes
old_value = _image_payload_cache_max_bytes
with _image_payload_cache_lock:
_image_payload_cache_max_bytes = max(0, int(max_bytes))
while _image_payload_cache_bytes > _image_payload_cache_max_bytes and _image_payload_cache:
_, (_, evicted_payload) = _image_payload_cache.popitem(last=False)
_image_payload_cache_bytes -= len(evicted_payload)
return old_value
def _load_image_cached(image_path: str) -> Image.Image:
"""加载图像并复用缓存的 RGB payload。
每次调用都返回新的 PIL Image,避免调用方的数据增强或多线程处理修改共享对象。
cache_clear/cache_info 代理到底层 payload 缓存,保留测试和调试入口。
"""
size, payload = _load_image_payload_cached(image_path)
return Image.frombytes('RGB', size, payload)
_load_image_cached.cache_clear = _image_payload_cache_clear # type: ignore[attr-defined]
_load_image_cached.cache_info = _image_payload_cache_info # type: ignore[attr-defined]
_load_image_cached.cache_bytes = _image_payload_cache_current_bytes # type: ignore[attr-defined]
_load_image_cached.set_cache_max_bytes = _set_image_payload_cache_max_bytes # type: ignore[attr-defined]