Spaces:
Sleeping
Sleeping
File size: 5,847 Bytes
e40db0e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | """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]
|