FlexICM / flexicm /utils /codec_test.py
tianma
Add official task teachers and align TAIC train/eval configs.
bed503a
Raw History Blame
7.89 kB
"""Shared helpers for codec test / eval scripts."""
from __future__ import annotations
import os
from typing import Dict, Optional, Tuple
import torch
from flexicm.utils.alignment import Alignment
from flexicm.utils.train_utils import AverageMeter
def resolve_ckpt(path: str, repo_root: str, label: str = "checkpoint") -> str:
if not path:
raise FileNotFoundError(f"{label}: path is empty")
if not os.path.isabs(path):
path = os.path.join(repo_root, path)
placeholder = os.path.join(os.path.dirname(path), "PLACEHOLDER")
if os.path.isfile(placeholder) and not os.path.isfile(path):
raise FileNotFoundError(
f"{label}: not ready (PLACEHOLDER still present):\n {placeholder}\n"
f"Expected real weights at:\n {path}\n"
"See checkpoints/README.md"
)
if not os.path.isfile(path):
raise FileNotFoundError(f"{label}: missing file: {path}")
if os.path.basename(path) == "PLACEHOLDER" or path.endswith(".txt"):
raise FileNotFoundError(f"{label}: refusing placeholder file: {path}")
return path
def crop_feature_to_image(h: torch.Tensor, image_hw: Tuple[int, int]) -> torch.Tensor:
"""Crop decoded feature h (H/4, W/4 of padded input) to original image size / 4."""
H, W = image_hw
return h[..., : H // 4, : W // 4]
def pad_for_codec(images: torch.Tensor, divisor: int = 256, device=None):
align = Alignment(divisor=divisor, mode="pad", padding_mode="constant")
if device is not None:
align = align.to(device)
return align.align(images), align
def likelihood_bpp(likelihoods: Dict[str, torch.Tensor], num_pixels: int) -> torch.Tensor:
import math
return sum(
(torch.log(lik).sum() / (-math.log(2) * num_pixels))
for lik in likelihoods.values()
)
def actual_bitstream_bpp(strings, num_pixels: int) -> float:
"""Estimate bpp from CompressAI byte strings: [[y_bytes...], [z_bytes...]]."""
total_bits = 0
for group in strings:
for s in group:
if isinstance(s, (bytes, bytearray)):
total_bits += len(s) * 8
elif torch.is_tensor(s):
total_bits += int(s.numel() * s.element_size() * 8)
else:
total_bits += len(s) * 8
return total_bits / float(num_pixels)
@torch.no_grad()
def test_taic_loader(
model,
teacher,
loader,
criterion,
device,
align_divisor: int = 256,
run_actual_bpp: bool = False,
max_batches: Optional[int] = None,
log_every: int = 50,
):
"""Run codec test: likelihood bpp + feature distortion (+ optional real bpp)."""
model.eval()
teacher.eval()
meters = {k: AverageMeter() for k in ("loss", "bpp", "distortion", "actual_bpp")}
if run_actual_bpp:
model.update(force=True)
for i, images in enumerate(loader):
if max_batches is not None and i >= max_batches:
break
images = images.to(device)
N, _, H, W = images.shape
num_pixels = N * H * W
# Keep codec + teacher on the padded grid so Swin always sees a
# patch/window-divisible size. bpp still uses the original pixel count.
x, _ = pad_for_codec(images, divisor=align_divisor, device=device)
out = model(x)
gt = teacher.gt_features(x)
pred = teacher.pred_features(out["h"])
stats = criterion(out, pred, gt, num_pixels=num_pixels)
meters["loss"].update(stats["loss"].item(), n=N)
meters["bpp"].update(stats["bpp"].item(), n=N)
meters["distortion"].update(stats["distortion"].item(), n=N)
if run_actual_bpp:
try:
enc = model.compress(x)
dec = model.decompress(
enc["strings"], enc["shape"], x_size=(x.shape[2], x.shape[3])
)
abpp = actual_bitstream_bpp(enc["strings"], num_pixels)
meters["actual_bpp"].update(abpp, n=N)
# sanity: decoded h spatial size
_ = dec["h"]
except Exception as e:
if i == 0:
print(f"[warn] actual bpp / compress-decompress failed: {e}")
if i % log_every == 0:
msg = (
f"[{i}/{len(loader)}] bpp={meters['bpp'].avg:.4f} "
f"D={meters['distortion'].avg:.6f} loss={meters['loss'].avg:.4f}"
)
if run_actual_bpp and meters["actual_bpp"].count > 0:
msg += f" actual_bpp={meters['actual_bpp'].avg:.4f}"
print(msg)
result = {
"bpp": meters["bpp"].avg,
"distortion": meters["distortion"].avg,
"loss": meters["loss"].avg,
"num_batches": meters["bpp"].count,
}
if run_actual_bpp and meters["actual_bpp"].count > 0:
result["actual_bpp"] = meters["actual_bpp"].avg
return result
@torch.no_grad()
def test_ctaic_loader(
ext_model,
base_model,
teacher,
loader,
criterion,
device,
use_condition: bool = True,
align_divisor: int = 256,
run_actual_bpp: bool = False,
max_batches: Optional[int] = None,
log_every: int = 50,
):
"""Codec test for C-TAIC; bpp is extension-layer only (paper Sec.IV.E.2)."""
ext_model.eval()
base_model.eval()
teacher.eval()
meters = {k: AverageMeter() for k in ("loss", "bpp", "distortion", "actual_bpp")}
if run_actual_bpp:
ext_model.update(force=True)
for i, images in enumerate(loader):
if max_batches is not None and i >= max_batches:
break
images = images.to(device)
N, _, H, W = images.shape
num_pixels = N * H * W
# Keep codec + teacher on the padded grid (same rationale as TAIC test).
x, _ = pad_for_codec(images, divisor=align_divisor, device=device)
y_b = None
if use_condition:
base_out = base_model(x)
y_b = base_out["y_hat"]
out = ext_model(x, y_b_hat=y_b, use_condition=use_condition and y_b is not None)
gt = teacher.gt_features(x)
pred = teacher.pred_features(out["h"])
stats = criterion(out, pred, gt, num_pixels=num_pixels)
meters["loss"].update(stats["loss"].item(), n=N)
meters["bpp"].update(stats["bpp"].item(), n=N)
meters["distortion"].update(stats["distortion"].item(), n=N)
if run_actual_bpp:
try:
enc = ext_model.compress(
x, y_b_hat=y_b, use_condition=use_condition and y_b is not None
)
dec = ext_model.decompress(
enc["strings"],
enc["shape"],
x_size=(x.shape[2], x.shape[3]),
y_b_hat=y_b,
use_condition=use_condition and y_b is not None,
)
abpp = actual_bitstream_bpp(enc["strings"], num_pixels)
meters["actual_bpp"].update(abpp, n=N)
_ = dec["h"]
except Exception as e:
if i == 0:
print(f"[warn] actual bpp / compress-decompress failed: {e}")
if i % log_every == 0:
msg = (
f"[{i}/{len(loader)}] bpp={meters['bpp'].avg:.4f} "
f"D={meters['distortion'].avg:.6f} loss={meters['loss'].avg:.4f}"
)
if run_actual_bpp and meters["actual_bpp"].count > 0:
msg += f" actual_bpp={meters['actual_bpp'].avg:.4f}"
print(msg)
result = {
"bpp": meters["bpp"].avg,
"distortion": meters["distortion"].avg,
"loss": meters["loss"].avg,
"num_batches": meters["bpp"].count,
"use_condition": use_condition,
}
if run_actual_bpp and meters["actual_bpp"].count > 0:
result["actual_bpp"] = meters["actual_bpp"].avg
return result