mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-07-26 20:32:04 +08:00
color calibration
This commit is contained in:
@@ -33,6 +33,51 @@ function agnos_init {
|
||||
# StarPilot variables
|
||||
sudo chmod 0777 /cache
|
||||
|
||||
# Weston loads display color correction from /data/misc/display/color_cal/color_cal.
|
||||
# Prefer a factory /persist/comma/color_cal blob when present. Otherwise, derive a
|
||||
# Weston-compatible calibration blob from the device's legacy dwo gamma tables.
|
||||
COLOR_CAL_SRC="/persist/comma/color_cal"
|
||||
DWO_GAMMA_SRC="/persist/comma/dwo_gamma_curves"
|
||||
COLOR_CAL_DST_DIR="/data/misc/display/color_cal"
|
||||
COLOR_CAL_DST="${COLOR_CAL_DST_DIR}/color_cal"
|
||||
COLOR_CAL_HASH_PATH="${COLOR_CAL_DST_DIR}/source.sha256"
|
||||
DWO_REFERENCE="$DIR/tools/reference_dwo_gamma_curves.txt"
|
||||
DWO_GENERATOR="$DIR/tools/generate_color_cal_from_dwo.py"
|
||||
COLOR_CAL_UPDATED=0
|
||||
COLOR_CAL_TMP=""
|
||||
DWO_SOURCE_HASH=""
|
||||
|
||||
if [ -f "$COLOR_CAL_SRC" ]; then
|
||||
sudo mkdir -p "$COLOR_CAL_DST_DIR"
|
||||
if [ ! -f "$COLOR_CAL_DST" ] || ! cmp -s "$COLOR_CAL_SRC" "$COLOR_CAL_DST"; then
|
||||
sudo cp "$COLOR_CAL_SRC" "$COLOR_CAL_DST"
|
||||
sudo chown -R comma:comma "$COLOR_CAL_DST_DIR"
|
||||
sudo chmod 664 "$COLOR_CAL_DST"
|
||||
sudo rm -f "$COLOR_CAL_HASH_PATH"
|
||||
COLOR_CAL_UPDATED=1
|
||||
fi
|
||||
elif [ -f "$DWO_GAMMA_SRC" ] && [ -f "$DWO_REFERENCE" ] && [ -f "$DWO_GENERATOR" ]; then
|
||||
DWO_SOURCE_HASH="$(cat "$DWO_GAMMA_SRC" "$DWO_REFERENCE" "$DWO_GENERATOR" | sha256sum | awk '{print $1}')"
|
||||
if [ ! -f "$COLOR_CAL_DST" ] || [ ! -f "$COLOR_CAL_HASH_PATH" ] || [ "$(cat "$COLOR_CAL_HASH_PATH" 2>/dev/null)" != "$DWO_SOURCE_HASH" ]; then
|
||||
COLOR_CAL_TMP="$(mktemp)"
|
||||
if python3 "$DWO_GENERATOR" --reference "$DWO_REFERENCE" --input "$DWO_GAMMA_SRC" --output "$COLOR_CAL_TMP"; then
|
||||
sudo mkdir -p "$COLOR_CAL_DST_DIR"
|
||||
if [ ! -f "$COLOR_CAL_DST" ] || ! cmp -s "$COLOR_CAL_TMP" "$COLOR_CAL_DST"; then
|
||||
sudo cp "$COLOR_CAL_TMP" "$COLOR_CAL_DST"
|
||||
COLOR_CAL_UPDATED=1
|
||||
fi
|
||||
printf '%s' "$DWO_SOURCE_HASH" | sudo tee "$COLOR_CAL_HASH_PATH" >/dev/null
|
||||
sudo chown -R comma:comma "$COLOR_CAL_DST_DIR"
|
||||
sudo chmod 664 "$COLOR_CAL_DST" "$COLOR_CAL_HASH_PATH"
|
||||
fi
|
||||
rm -f "$COLOR_CAL_TMP"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$COLOR_CAL_UPDATED" = "1" ] && systemctl is-active --quiet weston.service; then
|
||||
sudo systemctl restart weston.service
|
||||
fi
|
||||
|
||||
# Check if AGNOS update is required
|
||||
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
|
||||
AGNOS_PY="$DIR/system/hardware/tici/agnos.py"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import binascii
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
IGNORED_TAIL = b"\x00\x00\x00\x00\x00"
|
||||
VALUES_COUNT = 13
|
||||
VALUES_FMT = f"<{VALUES_COUNT}e"
|
||||
VALUES_SIZE = struct.calcsize(VALUES_FMT)
|
||||
|
||||
|
||||
def read_color_cal(path: str) -> tuple[float, list[float], list[float], bytes]:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
if len(data) < VALUES_SIZE:
|
||||
raise ValueError(f"{path} is too short: expected at least {VALUES_SIZE} bytes, got {len(data)}")
|
||||
|
||||
values = struct.unpack(VALUES_FMT, data[:VALUES_SIZE])
|
||||
gamma = float(values[0])
|
||||
ccm = [float(v) for v in values[1:10]]
|
||||
wb = [float(v) for v in values[10:13]]
|
||||
return gamma, ccm, wb, data[VALUES_SIZE:]
|
||||
|
||||
|
||||
def write_color_cal(path: str, gamma: float, ccm: list[float], wb: list[float], tail: bytes = IGNORED_TAIL) -> None:
|
||||
if len(ccm) != 9:
|
||||
raise ValueError("CCM must contain exactly 9 values")
|
||||
if len(wb) != 3:
|
||||
raise ValueError("WB gains must contain exactly 3 values")
|
||||
|
||||
payload = struct.pack(VALUES_FMT, gamma, *ccm, *wb)
|
||||
data = payload + tail
|
||||
out = Path(path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(data)
|
||||
|
||||
|
||||
def cmd_analyze(args: argparse.Namespace) -> int:
|
||||
gamma, ccm, wb, tail = read_color_cal(args.path)
|
||||
data = Path(args.path).read_bytes()
|
||||
print(f"path: {args.path}")
|
||||
print(f"size: {len(data)}")
|
||||
print(f"hex: {binascii.hexlify(data).decode()}")
|
||||
print(f"gamma: {gamma:.4f}")
|
||||
print("ccm:")
|
||||
print(f" [{ccm[0]:.4f}, {ccm[1]:.4f}, {ccm[2]:.4f}]")
|
||||
print(f" [{ccm[3]:.4f}, {ccm[4]:.4f}, {ccm[5]:.4f}]")
|
||||
print(f" [{ccm[6]:.4f}, {ccm[7]:.4f}, {ccm[8]:.4f}]")
|
||||
print(f"wb: [R={wb[0]:.4f}, G={wb[1]:.4f}, B={wb[2]:.4f}]")
|
||||
print(f"ignored_tail_hex: {tail.hex()}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_generate(args: argparse.Namespace) -> int:
|
||||
tail = binascii.unhexlify(args.tail_hex) if args.tail_hex else IGNORED_TAIL
|
||||
write_color_cal(args.output, args.gamma, args.ccm, args.wb, tail=tail)
|
||||
print(f"wrote {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_from_hex(args: argparse.Namespace) -> int:
|
||||
data = binascii.unhexlify(args.hex_string)
|
||||
out = Path(args.output)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(data)
|
||||
print(f"wrote {args.output}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Inspect and generate comma/Weston color_cal files.")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
analyze = subparsers.add_parser("analyze", help="Decode an existing color_cal file")
|
||||
analyze.add_argument("path")
|
||||
analyze.set_defaults(func=cmd_analyze)
|
||||
|
||||
generate = subparsers.add_parser("generate", help="Generate a color_cal file from gamma/CCM/WB values")
|
||||
generate.add_argument("output")
|
||||
generate.add_argument("--gamma", type=float, required=True)
|
||||
generate.add_argument("--ccm", type=float, nargs=9, required=True)
|
||||
generate.add_argument("--wb", type=float, nargs=3, required=True)
|
||||
generate.add_argument("--tail-hex", default="", help="Optional trailing bytes as hex; Weston ignores these")
|
||||
generate.set_defaults(func=cmd_generate)
|
||||
|
||||
from_hex = subparsers.add_parser("from-hex", help="Write a raw color_cal blob from a hex string")
|
||||
from_hex.add_argument("output")
|
||||
from_hex.add_argument("hex_string")
|
||||
from_hex.set_defaults(func=cmd_from_hex)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import math
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EPS = 1e-9
|
||||
COLOR_CAL_FMT = "<13e"
|
||||
IGNORED_TAIL = b"\x00\x00\x00\x00\x00"
|
||||
SAMPLES = [i / 47.0 for i in range(1, 48)]
|
||||
WEIGHTS = [0.6 + 1.8 * x + (8.0 if x >= 0.985 else 0.0) for x in SAMPLES]
|
||||
|
||||
|
||||
def parse_dwo_gamma_curves(path: str) -> list[list[float]]:
|
||||
rows: list[list[float]] = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
values = [int(x, 16) for x in stripped.split()]
|
||||
if len(values) < 2:
|
||||
raise ValueError(f"{path}: gamma row must contain at least two values")
|
||||
if values[0] > values[-1]:
|
||||
values.reverse()
|
||||
max_value = max(values)
|
||||
if max_value <= 0:
|
||||
raise ValueError(f"{path}: gamma row max must be positive")
|
||||
rows.append([v / max_value for v in values])
|
||||
|
||||
if len(rows) != 3:
|
||||
raise ValueError(f"{path}: expected 3 gamma rows, got {len(rows)}")
|
||||
|
||||
row_len = len(rows[0])
|
||||
if any(len(row) != row_len for row in rows):
|
||||
raise ValueError(f"{path}: gamma rows must all have the same length")
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def interp(curve: list[float], x: float) -> float:
|
||||
x = min(max(x, 0.0), 1.0)
|
||||
pos = x * (len(curve) - 1)
|
||||
lo = int(pos)
|
||||
hi = min(lo + 1, len(curve) - 1)
|
||||
frac = pos - lo
|
||||
return curve[lo] * (1.0 - frac) + curve[hi] * frac
|
||||
|
||||
|
||||
def invert_interp(curve: list[float], y: float) -> float:
|
||||
y = min(max(y, 0.0), 1.0)
|
||||
if y <= curve[0]:
|
||||
return 0.0
|
||||
if y >= curve[-1]:
|
||||
return 1.0
|
||||
lo = 0
|
||||
hi = len(curve) - 1
|
||||
while hi - lo > 1:
|
||||
mid = (lo + hi) // 2
|
||||
if curve[mid] < y:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
y0 = curve[lo]
|
||||
y1 = curve[hi]
|
||||
if abs(y1 - y0) < EPS:
|
||||
return lo / (len(curve) - 1)
|
||||
frac = (y - y0) / (y1 - y0)
|
||||
return (lo + frac) / (len(curve) - 1)
|
||||
|
||||
|
||||
def apply_target_panel(curve: list[float], a: float, gamma: float, x: float) -> float:
|
||||
corrected = min(max(a * (x ** gamma), 0.0), 1.0)
|
||||
return interp(curve, corrected)
|
||||
|
||||
|
||||
def fit_channel_amplitude(reference_curve: list[float], target_curve: list[float], gamma: float) -> tuple[float, float]:
|
||||
# Search for the per-channel amplitude that makes the target panel resemble the
|
||||
# reference panel once Weston applies a common gamma pre-distortion.
|
||||
lo = 0.85
|
||||
hi = 1.15
|
||||
best_a = 1.0
|
||||
best_err = float("inf")
|
||||
for _ in range(5):
|
||||
steps = 60
|
||||
step = (hi - lo) / steps
|
||||
for idx in range(steps + 1):
|
||||
a = lo + idx * step
|
||||
err = 0.0
|
||||
for x, w in zip(SAMPLES, WEIGHTS):
|
||||
ref = interp(reference_curve, x)
|
||||
actual = apply_target_panel(target_curve, a, gamma, x)
|
||||
diff = actual - ref
|
||||
err += w * diff * diff
|
||||
if err < best_err:
|
||||
best_err = err
|
||||
best_a = a
|
||||
lo = max(0.6, best_a - step * 1.5)
|
||||
hi = min(1.4, best_a + step * 1.5)
|
||||
return best_a, best_err
|
||||
|
||||
|
||||
def fit_color_cal(reference_curves: list[list[float]], target_curves: list[list[float]]) -> tuple[float, list[float], list[float]]:
|
||||
best_gamma = 1.0
|
||||
best_amps = [1.0, 1.0, 1.0]
|
||||
best_err = float("inf")
|
||||
|
||||
gamma_values = [0.82 + 0.004 * i for i in range(96)] # 0.82 .. 1.20
|
||||
for gamma in gamma_values:
|
||||
amps: list[float] = []
|
||||
err = 0.0
|
||||
for ref_curve, target_curve in zip(reference_curves, target_curves):
|
||||
amp, channel_err = fit_channel_amplitude(ref_curve, target_curve, gamma)
|
||||
amps.append(amp)
|
||||
err += channel_err
|
||||
# Bias toward identity when multiple fits are similar so a "good" panel stays unchanged.
|
||||
identity_penalty = sum((amp - 1.0) ** 2 for amp in amps) * 3.0 + (gamma - 1.0) ** 2 * 12.0
|
||||
err += identity_penalty
|
||||
if err < best_err:
|
||||
best_err = err
|
||||
best_gamma = gamma
|
||||
best_amps = amps
|
||||
|
||||
# The calibration file stores white-balance gains; Weston multiplies by their inverse.
|
||||
wb_gains = []
|
||||
for amp in best_amps:
|
||||
amp = max(amp, 0.6)
|
||||
gain = amp ** (-2.2 / best_gamma)
|
||||
wb_gains.append(min(max(gain, 0.7), 1.45))
|
||||
|
||||
return best_gamma, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], wb_gains
|
||||
|
||||
|
||||
def write_color_cal(path: str, gamma: float, ccm: list[float], wb_gains: list[float]) -> None:
|
||||
payload = struct.pack(COLOR_CAL_FMT, gamma, *ccm, *wb_gains)
|
||||
Path(path).write_bytes(payload + IGNORED_TAIL)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Generate a Weston color_cal blob from dwo_gamma_curves.")
|
||||
parser.add_argument("--reference", required=True, help="Reference dwo_gamma_curves file from a good panel")
|
||||
parser.add_argument("--input", required=True, help="Device dwo_gamma_curves file to adapt")
|
||||
parser.add_argument("--output", required=True, help="Output color_cal path")
|
||||
parser.add_argument("--print-only", action="store_true", help="Print the generated parameters instead of writing")
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
reference_curves = parse_dwo_gamma_curves(args.reference)
|
||||
input_curves = parse_dwo_gamma_curves(args.input)
|
||||
gamma, ccm, wb_gains = fit_color_cal(reference_curves, input_curves)
|
||||
|
||||
if args.print_only:
|
||||
print(f"gamma={gamma:.6f}")
|
||||
print("ccm=" + " ".join(f"{v:.6f}" for v in ccm))
|
||||
print("wb=" + " ".join(f"{v:.6f}" for v in wb_gains))
|
||||
else:
|
||||
output = Path(args.output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_color_cal(str(output), gamma, ccm, wb_gains)
|
||||
|
||||
digest = hashlib.sha256(Path(args.input).read_bytes()).hexdigest()[:12]
|
||||
print(f"source_sha256={digest}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
0217 0210 020C 0208 0200 01F8 01F1 01EA 01DC 01CD 01BE 01AF 019F 018F 017E 016B 0157 013F 0131 0122 010E 00F5 00E6 00D1 0000
|
||||
0219 0212 020E 020A 0202 01F9 01F0 01E7 01D7 01C6 01B5 01A3 0191 0180 016D 0158 0142 0128 0119 010A 00F7 00E1 00D5 00C6 0000
|
||||
0219 0210 0208 0200 01F1 01E3 01D4 01C7 01AF 0197 017E 0166 014C 0134 0118 00FA 00D9 00B6 00A0 008B 0071 0059 0048 002E 0000
|
||||
Reference in New Issue
Block a user