ColorLab (all new inputs optional; defaults reproduce v1.6.0, checked against a
golden made from the v1.6.0 code):
- tone_engine=local: edge-aware Highlights/Shadows (local Laplacian), Clarity
(guided band), dark-channel Dehaze, endpoint-preserving Contrast; GPU when
available.
- wb_temperature / wb_tint; second output params_json (non-default settings);
params_json input adds onto the sliders.
- params_json-only settings for presets: point curves (master + RGB), camera
calibration, grain, post-crop vignette, B&W magenta mixer.
New nodes:
- "Color: Apply ColorLab Preset": image + params_json + strength (+ mask).
- "Color: Lightroom Preset (XMP)": reads .xmp and .lrtemplate (Lua read as data
only) from models/lightroom_presets, maps crs: settings to ColorLab (incl.
parametric curve, calibration, Kelvin white balance against as-shot) and
reports what is not supported; outputs image, params_json and report.
- "Color: Lightroom Preset Contact Sheet": labelled grid of a collection.
Preset library: collections (All, Favorites, Recent, sub-folders), favourites and
recents in .jh_presets.json, CPU thumbnails on the node's last image with an LRU
cache, served by /jh_pixelpro/lr/{presets,thumb,favorite} (listed names only).
105 lines
3.7 KiB
Python
105 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
_PACK_ROOT = Path(__file__).resolve().parent.parent
|
|
_PACK_MODULE_NAME = "comfyui_jh_pixelpro_under_test_lr_preset"
|
|
XMP = _PACK_ROOT / "tests" / "fixtures" / "lr" / "warm-film.xmp"
|
|
|
|
|
|
def _load_pack():
|
|
if _PACK_MODULE_NAME in sys.modules:
|
|
return sys.modules[_PACK_MODULE_NAME]
|
|
spec = importlib.util.spec_from_file_location(
|
|
_PACK_MODULE_NAME,
|
|
_PACK_ROOT / "__init__.py",
|
|
submodule_search_locations=[str(_PACK_ROOT)],
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[_PACK_MODULE_NAME] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _run(node, *args, **kwargs):
|
|
return node.apply(*args, **kwargs)["result"]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def node_cls():
|
|
return _load_pack().NODE_CLASS_MAPPINGS["JHPixelProLightroomPreset"]
|
|
|
|
|
|
def _image() -> torch.Tensor:
|
|
torch.manual_seed(5)
|
|
return torch.rand(1, 32, 48, 3)
|
|
|
|
|
|
def test_registered_and_simple(node_cls) -> None:
|
|
pack = _load_pack()
|
|
assert (
|
|
pack.NODE_DISPLAY_NAME_MAPPINGS["JHPixelProLightroomPreset"]
|
|
== "Color: Lightroom Preset (XMP)"
|
|
)
|
|
assert node_cls.RETURN_NAMES == ("image", "params_json", "report")
|
|
spec = node_cls.INPUT_TYPES()
|
|
assert list(spec["required"]) == ["image", "preset", "strength"]
|
|
assert spec["optional"]["tone_engine"][1]["default"] == "local"
|
|
|
|
|
|
def test_apply_from_path(node_cls) -> None:
|
|
image = _image()
|
|
out, params_json, report = _run(
|
|
node_cls(), image, "(no presets found)", 1.0, preset_path=str(XMP)
|
|
)
|
|
assert out.shape == image.shape and not torch.allclose(out, image)
|
|
params = json.loads(params_json)
|
|
assert params["tone_engine"] == "local"
|
|
assert params["basic_exposure"] == pytest.approx(0.35)
|
|
assert params["tone_curve"][0] == [0.0, 18.0]
|
|
assert "JH Warm Film" in report and "Vintage 06" in report
|
|
|
|
|
|
def test_params_json_reproduces_image_in_colorlab_and_apply(node_cls) -> None:
|
|
pack = _load_pack()
|
|
image = _image()
|
|
out, params_json, _ = _run(node_cls(), image, "x", 1.0, preset_path=str(XMP))
|
|
apply = pack.NODE_CLASS_MAPPINGS["JHPixelProColorLabApply"]()
|
|
again, _ = apply.apply(image, params_json, 1.0)
|
|
assert torch.allclose(out, again, atol=1e-6)
|
|
colorlab = pack.NODE_CLASS_MAPPINGS["JHPixelProColorLab"]
|
|
spec = colorlab.INPUT_TYPES()
|
|
sliders = {k: v[1]["default"] for k, v in spec["required"].items() if k != "image"}
|
|
sliders.update({k: v[1]["default"] for k, v in spec["optional"].items() if "default" in v[1]})
|
|
via_colorlab, chained = colorlab().apply(image, params_json=params_json, **sliders)
|
|
assert torch.allclose(out, via_colorlab, atol=1e-6)
|
|
assert json.loads(chained)["tone_curve"] == json.loads(params_json)["tone_curve"]
|
|
|
|
|
|
def test_strength_and_mask(node_cls) -> None:
|
|
image = _image()
|
|
same, half_json, _ = _run(node_cls(), image, "x", 0.0, preset_path=str(XMP))
|
|
assert torch.equal(same, image)
|
|
_, half_json, _ = _run(node_cls(), image, "x", 0.5, preset_path=str(XMP))
|
|
assert json.loads(half_json)["basic_exposure"] == pytest.approx(0.175)
|
|
mask = torch.zeros(1, 32, 48)
|
|
mask[:, :, :24] = 1.0
|
|
out, _, _ = _run(node_cls(), image, "x", 1.0, preset_path=str(XMP), mask=mask)
|
|
assert torch.allclose(out[:, :, 24:], image[:, :, 24:])
|
|
|
|
|
|
def test_missing_preset_explains_where(node_cls) -> None:
|
|
with pytest.raises(ValueError, match="lightroom_presets"):
|
|
_run(node_cls(), _image(), "(no presets found)", 1.0)
|
|
|
|
|
|
def test_is_changed_tracks_file(node_cls) -> None:
|
|
key = node_cls.IS_CHANGED("x", preset_path=str(XMP))
|
|
assert str(XMP) in key and key.split(":")[-1].isdigit()
|