Files
Dehaze/web_dehaze/pipeline.py
2026-06-10 17:50:42 +08:00

539 lines
19 KiB
Python

from __future__ import annotations
import hashlib
import importlib.util
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Callable
from PIL import Image
from postprocess import POSTPROCESSORS, run_postprocess
ROOT = Path(__file__).resolve().parents[1]
IMAGE_DIR = ROOT / "待去雾图片"
RESULTS_DIR = ROOT / "web_results"
CONDA_ROOT = Path(os.environ.get("CONDA_EXE", sys.executable)).resolve().parents[1]
DEFAULT_CAFFE_PYTHON = CONDA_ROOT / "envs" / "dehaze_caffe" / "bin" / "python"
DEFAULT_TORCH_PYTHON = CONDA_ROOT / "envs" / "seg_server" / "bin" / "python"
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}
DEHAZE_METHODS: dict[str, dict[str, Any]] = {
"AOD": {
"label": "AOD",
"module": "caffe",
"python_group": "caffe",
"model_file": ROOT / "AOD-Net_最好加入后处理" / "AOD_Net.caffemodel",
},
"Baidu_API": {"label": "Baidu_API", "module": "requests", "python_group": "server", "model_file": None},
"DCP": {"label": "DCP", "module": "cv2", "python_group": "server", "model_file": None},
"DehazeNet": {
"label": "DehazeNet",
"module": "caffe",
"python_group": "caffe",
"model_file": ROOT / "DehazeNet" / "DehazeNet.caffemodel",
},
"GCANet": {
"label": "GCANet",
"module": "torch",
"python_group": "torch",
"model_file": ROOT / "GCANet" / "models" / "wacv_gcanet_dehaze.pth",
},
"RefineDNet": {
"label": "RefineDNet",
"module": "torch",
"python_group": "torch",
"model_file": ROOT / "RefineDNet" / "checkpoints" / "refined_DCP_outdoor" / "60_net_Refiner_J.pth",
},
}
LogFn = Callable[[str], None]
def _noop_log(_: str) -> None:
return None
def _safe_slug(value: str) -> str:
slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", value).strip("._-")
return slug or "image"
def image_id(filename: str) -> str:
stem = Path(filename).stem
digest = hashlib.sha1(filename.encode("utf-8")).hexdigest()[:8]
return f"{_safe_slug(stem)}_{digest}"
def source_image_path(filename: str) -> Path:
if Path(filename).name != filename:
raise ValueError("Invalid image filename")
path = IMAGE_DIR / filename
if not path.exists() or path.suffix.lower() not in IMAGE_EXTENSIONS:
raise FileNotFoundError(filename)
return path
def image_result_dir(filename: str) -> Path:
return RESULTS_DIR / image_id(filename)
def dehaze_result_path(filename: str, method: str) -> Path:
return image_result_dir(filename) / "dehaze" / f"{_safe_slug(method)}.png"
def post_result_path(filename: str, source: str, processor: str, params: dict[str, Any] | None = None) -> Path:
params = params or {}
suffix = ""
if processor == "manual_sv":
suffix = f"_S{int(float(params.get('s_gain', 1.0)) * 100)}_V{int(float(params.get('v_gain', 1.0)) * 100)}"
if params.get("match_hue"):
suffix += "_H"
return image_result_dir(filename) / "postprocess" / f"{_safe_slug(source)}__{_safe_slug(processor)}{suffix}.png"
def relpath(path: Path) -> str:
return path.resolve().relative_to(ROOT).as_posix()
def python_for_group(group: str | None) -> Path:
if group == "caffe":
return Path(os.environ.get("DEHAZE_CAFFE_PYTHON", DEFAULT_CAFFE_PYTHON))
if group == "torch":
return Path(os.environ.get("DEHAZE_TORCH_PYTHON", DEFAULT_TORCH_PYTHON))
return Path(sys.executable)
def python_for_method(method: str) -> Path:
return python_for_group(DEHAZE_METHODS[method].get("python_group"))
def list_images() -> list[dict[str, Any]]:
images: list[dict[str, Any]] = []
if not IMAGE_DIR.exists():
return images
for path in sorted(IMAGE_DIR.iterdir(), key=lambda p: p.name.lower()):
if not path.is_file() or path.suffix.lower() not in IMAGE_EXTENSIONS:
continue
width = height = None
mode = ""
try:
with Image.open(path) as img:
width, height = img.size
mode = img.mode
except Exception:
pass
images.append(
{
"name": path.name,
"id": image_id(path.name),
"size": path.stat().st_size,
"width": width,
"height": height,
"mode": mode,
"path": relpath(path),
}
)
return images
def module_available(module_name: str, python_path: Path | None = None) -> bool:
python_path = python_path or Path(sys.executable)
if python_path.resolve() == Path(sys.executable).resolve():
return importlib.util.find_spec(module_name) is not None
if not python_path.exists():
return False
result = subprocess.run(
[str(python_path), "-c", f"import {module_name}"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
text=True,
timeout=20,
)
return result.returncode == 0
def capabilities() -> dict[str, Any]:
methods = {}
for key, info in DEHAZE_METHODS.items():
module_name = info.get("module")
model_file = info.get("model_file")
python_path = python_for_method(key)
python_ok = python_path.exists()
module_ok = bool(module_available(module_name, python_path)) if module_name and python_ok else False
model_ok = bool(model_file.exists()) if model_file else True
methods[key] = {
"label": info["label"],
"available": python_ok and module_ok and model_ok,
"module": module_name,
"python": str(python_path),
"python_ok": python_ok,
"module_ok": module_ok,
"model_ok": model_ok,
"model_file": relpath(model_file) if model_file else "",
}
return {
"python": sys.executable,
"image_dir": relpath(IMAGE_DIR),
"results_dir": relpath(RESULTS_DIR),
"methods": methods,
"postprocessors": POSTPROCESSORS,
}
def get_results(filename: str) -> dict[str, Any]:
source = source_image_path(filename)
dehaze = []
for method, info in DEHAZE_METHODS.items():
path = dehaze_result_path(filename, method)
dehaze.append(
{
"method": method,
"label": info["label"],
"exists": path.exists(),
"path": relpath(path) if path.exists() else "",
}
)
post = []
post_dir = image_result_dir(filename) / "postprocess"
if post_dir.exists():
for path in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()):
post.append({"name": path.stem, "exists": True, "path": relpath(path)})
return {
"image": filename,
"original": {"label": "原图", "exists": True, "path": relpath(source)},
"dehaze": dehaze,
"postprocess": post,
}
def collect_download_items(filenames: list[str] | None = None, include_original: bool = True) -> list[tuple[Path, str]]:
names = filenames or [item["name"] for item in list_images()]
items: list[tuple[Path, str]] = []
used_names: set[str] = set()
def add_file(path: Path, archive_name: str) -> None:
if not path.exists() or not path.is_file():
return
archive_name = archive_name.replace("\\", "/")
if archive_name in used_names:
stem = Path(archive_name).with_suffix("").as_posix()
suffix = Path(archive_name).suffix
index = 2
while f"{stem}_{index}{suffix}" in used_names:
index += 1
archive_name = f"{stem}_{index}{suffix}"
used_names.add(archive_name)
items.append((path, archive_name))
for filename in names:
source = source_image_path(filename)
folder = image_id(filename)
if include_original:
add_file(source, f"{folder}/original/{source.name}")
for method in DEHAZE_METHODS:
result = dehaze_result_path(filename, method)
add_file(result, f"{folder}/dehaze/{_safe_slug(method)}.png")
post_dir = image_result_dir(filename) / "postprocess"
if post_dir.exists():
for result in sorted(post_dir.glob("*.png"), key=lambda p: p.name.lower()):
add_file(result, f"{folder}/postprocess/{result.name}")
return items
def _reset_dir(path: Path) -> None:
if path.exists():
shutil.rmtree(path)
path.mkdir(parents=True, exist_ok=True)
def _prepare_rgb_png(source: Path, target_dir: Path, min_side: int | None = None) -> Path:
target_dir.mkdir(parents=True, exist_ok=True)
image = Image.open(source).convert("RGB")
if min_side and min(image.size) < min_side:
scale = min_side / float(min(image.size))
new_size = (max(1, int(round(image.size[0] * scale))), max(1, int(round(image.size[1] * scale))))
image = image.resize(new_size, Image.BICUBIC)
target = target_dir / f"{_safe_slug(source.stem)}.png"
image.save(target)
return target
def _copy_final_image(source: Path, destination: Path, original: Path, force_original_size: bool = True) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
if not force_original_size:
shutil.copy2(source, destination)
return
original_size = Image.open(original).size
image = Image.open(source).convert("RGB")
if image.size != original_size:
image = image.resize(original_size, Image.BICUBIC)
image.save(destination)
def _run_command(command: list[str], cwd: Path, log: LogFn, timeout: int | None = None) -> None:
log(f"$ {' '.join(command)}")
env = os.environ.copy()
env.setdefault("GLOG_minloglevel", "2")
process = subprocess.Popen(
command,
cwd=str(cwd),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
start = time.time()
assert process.stdout is not None
for line in process.stdout:
log(line.rstrip())
if timeout and time.time() - start > timeout:
process.kill()
raise TimeoutError(f"Command timed out after {timeout}s")
return_code = process.wait()
if return_code != 0:
raise RuntimeError(f"Command failed with code {return_code}")
def _require_module(module_name: str, python_path: Path | None = None) -> None:
python_path = python_path or Path(sys.executable)
if not module_available(module_name, python_path):
raise RuntimeError(f"Python 环境 {python_path} 缺少模块:{module_name}")
def run_dehaze_method(filename: str, method: str, options: dict[str, Any] | None = None, log: LogFn | None = None) -> Path:
options = options or {}
log = log or _noop_log
if method not in DEHAZE_METHODS:
raise ValueError(f"Unknown method: {method}")
source = source_image_path(filename)
log(f"开始 {method}: {filename}")
runners = {
"DCP": _run_dcp,
"Baidu_API": _run_baidu,
"AOD": _run_aod,
"DehazeNet": _run_dehazenet,
"GCANet": _run_gcanet,
"RefineDNet": _run_refinednet,
}
result = runners[method](source, filename, options, log)
log(f"完成 {method}: {relpath(result)}")
return result
def _run_dcp(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
_require_module("cv2")
work = image_result_dir(filename) / "work" / "DCP"
_reset_dir(work)
src_dir = work / "src"
(work / "dark").mkdir(parents=True, exist_ok=True)
(work / "trans").mkdir(parents=True, exist_ok=True)
(work / "result").mkdir(parents=True, exist_ok=True)
input_copy = _prepare_rgb_png(source, src_dir)
sz = int(options.get("sz", 10))
tx = float(options.get("tx", 0.2))
_run_command([sys.executable, "dehaze.py", str(work), str(sz), str(tx)], ROOT / "DCP_最好加入后处理", log)
generated = work / "result" / f"{input_copy.stem}_{sz}_{tx}_result.png"
if not generated.exists():
matches = sorted((work / "result").glob("*_result.png"))
if not matches:
raise FileNotFoundError("DCP did not create a result image")
generated = matches[-1]
output = dehaze_result_path(filename, "DCP")
_copy_final_image(generated, output, source)
return output
def _run_baidu(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
_require_module("requests")
script_path = ROOT / "Baidu_API_最好加入后处理" / "1_Baidu_Dehaze.py"
spec = importlib.util.spec_from_file_location("baidu_dehaze_script", script_path)
if spec is None or spec.loader is None:
raise RuntimeError("无法加载 Baidu API 脚本")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if os.environ.get("BAIDU_API_KEY"):
module.API_KEY = os.environ["BAIDU_API_KEY"]
if os.environ.get("BAIDU_SECRET_KEY"):
module.SECRET_KEY = os.environ["BAIDU_SECRET_KEY"]
token = module.get_access_token()
if not token:
raise RuntimeError("Baidu access token 获取失败")
log("Baidu access token 获取成功")
processed = module.process_image(str(source), token)
if not processed:
raise RuntimeError("Baidu API 未返回图像")
work = image_result_dir(filename) / "work" / "Baidu_API"
_reset_dir(work)
tmp = work / "baidu_result.png"
tmp.write_bytes(processed)
output = dehaze_result_path(filename, "Baidu_API")
_copy_final_image(tmp, output, source)
return output
def _run_aod(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
python_path = python_for_method("AOD")
_require_module("caffe", python_path)
work = image_result_dir(filename) / "work" / "AOD"
_reset_dir(work)
input_dir = work / "input"
output_dir = work / "output"
input_copy = _prepare_rgb_png(source, input_dir)
output_dir.mkdir(parents=True, exist_ok=True)
_run_command([str(python_path), "test/test.py", str(input_dir), str(output_dir)], ROOT / "AOD-Net_最好加入后处理", log)
generated = output_dir / f"{input_copy.stem}_AOD-Net.png"
if not generated.exists():
matches = sorted(output_dir.glob("*.png"))
if not matches:
raise FileNotFoundError("AOD did not create a result image")
generated = matches[-1]
output = dehaze_result_path(filename, "AOD")
_copy_final_image(generated, output, source)
return output
def _run_dehazenet(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
python_path = python_for_method("DehazeNet")
_require_module("caffe", python_path)
work = image_result_dir(filename) / "work" / "DehazeNet" / "img"
_reset_dir(work)
input_copy = _prepare_rgb_png(source, work / "src")
_run_command([str(python_path), "DehazeNet.py", str(work)], ROOT / "DehazeNet", log)
generated = work / "result" / f"{input_copy.stem}_result.png"
if not generated.exists():
matches = sorted((work / "result").glob("*_result.png"))
if not matches:
raise FileNotFoundError("DehazeNet did not create a result image")
generated = matches[-1]
output = dehaze_result_path(filename, "DehazeNet")
_copy_final_image(generated, output, source)
return output
def _run_gcanet(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
python_path = python_for_method("GCANet")
_require_module("torch", python_path)
work = image_result_dir(filename) / "work" / "GCANet"
_reset_dir(work)
input_dir = work / "input"
output_dir = work / "output"
input_copy = _prepare_rgb_png(source, input_dir)
output_dir.mkdir(parents=True, exist_ok=True)
_run_command(
[str(python_path), "test.py", "--task", "dehaze", "--gpu_id", "0", "--indir", str(input_dir), "--outdir", str(output_dir)],
ROOT / "GCANet",
log,
)
generated = output_dir / f"{input_copy.stem}_dehaze.png"
if not generated.exists():
matches = sorted(output_dir.glob("*.png"))
if not matches:
raise FileNotFoundError("GCANet did not create a result image")
generated = matches[-1]
output = dehaze_result_path(filename, "GCANet")
_copy_final_image(generated, output, source)
return output
def _run_refinednet(source: Path, filename: str, options: dict[str, Any], log: LogFn) -> Path:
python_path = python_for_method("RefineDNet")
_require_module("torch", python_path)
_require_module("torchvision", python_path)
work = image_result_dir(filename) / "work" / "RefineDNet"
_reset_dir(work)
dataroot = work / "dataset"
input_copy = _prepare_rgb_png(source, dataroot / "test", min_side=256)
method_name = "RefineDNet"
_run_command(
[
str(python_path),
"quick_test.py",
"--dataroot",
str(dataroot),
"--dataset_mode",
"single",
"--name",
"refined_DCP_outdoor",
"--model",
"refined_DCP",
"--phase",
"test",
"--preprocess",
"none",
"--save_image",
"--method_name",
method_name,
"--epoch",
"60",
"--gpu_ids",
"0",
],
ROOT / "RefineDNet",
log,
)
generated = dataroot / method_name / f"{input_copy.stem}_dehz.png"
if not generated.exists():
matches = sorted((dataroot / method_name).glob("*.png"))
if not matches:
raise FileNotFoundError("RefineDNet did not create a result image")
generated = matches[-1]
output = dehaze_result_path(filename, "RefineDNet")
_copy_final_image(generated, output, source)
return output
def run_postprocessors_for_source(
filename: str,
source_name: str,
processors: list[str],
params: dict[str, Any] | None = None,
reference_filename: str | None = None,
log: LogFn | None = None,
) -> list[Path]:
params = params or {}
log = log or _noop_log
if source_name == "original":
source_path = source_image_path(filename)
else:
source_path = dehaze_result_path(filename, source_name)
if not source_path.exists():
raise FileNotFoundError(f"后处理源图不存在:{source_name}")
reference_path = source_image_path(reference_filename or filename)
outputs: list[Path] = []
for processor in processors:
proc_params = params.get(processor, params)
output = post_result_path(filename, source_name, processor, proc_params)
meta = run_postprocess(processor, source_path, output, reference_path=reference_path, params=proc_params)
outputs.append(output)
log(f"后处理完成 {source_name} / {processor}: {json.dumps(meta, ensure_ascii=False)}")
return outputs
def resolve_asset(relative_path: str) -> Path:
target = (ROOT / relative_path).resolve()
allowed_roots = [IMAGE_DIR.resolve(), RESULTS_DIR.resolve()]
if not any(target == root or root in target.parents for root in allowed_roots):
raise PermissionError("Asset path is outside allowed roots")
if not target.exists() or not target.is_file():
raise FileNotFoundError(relative_path)
return target