# Câmera por cadeia ECC quadro a quadro (para cenas de pouca textura, ex.: K-maps).
# uso: python ecc_cadeia.py <id> <saida.json> <t_ancora> [--de t0] [--ate t1] [--margem 270] [--modelo affine|homog]
# A âncora usa a câmera json; cada passo alinha o quadro f com f±1 (ECC multiescala) começando pelo movimento do json.
import sys, json, math, cv2, numpy as np
P = "<SUA_CENTRAL>/07-videos/projetos/2026-10-17-subzmotion-recriacao/"
ORIG = "<SUA_CENTRAL>/05-pesquisa/referencias/2026-10-17-subzmotion/midia/05-DeMZpQ5gv-t.mp4"
a = sys.argv; cid = a[1]; out = a[2]; tanc = float(a[3])
d = json.load(open(P + "camera/cena-%s.json" % cid, encoding="utf-8")); ch = d["chaves"]
def opt(flag, dflt):
    return a[a.index(flag) + 1] if flag in a else dflt
t0 = float(opt("--de", d["ini"])); t1 = float(opt("--ate", d["fim"])); MG = float(opt("--margem", 270)); modelo = opt("--modelo", "homog")
def interp(lst, t):
    ts = [x for x, y in lst]
    if t <= ts[0]: return lst[0][1]
    if t >= ts[-1]: return lst[-1][1]
    for i in range(1, len(ts)):
        if ts[i] >= t:
            u = (t - ts[i-1]) / (ts[i] - ts[i-1]); return lst[i-1][1] * (1-u) + lst[i][1] * u
def json_cam(t): return [interp(ch[n], t) for n in ("zoom", "mx", "my", "giro")]
def S_de(z, mx, my, g):
    s = 2 ** z; r = math.radians(g); A = s * np.array([[math.cos(r), -math.sin(r)], [math.sin(r), math.cos(r)]])
    T = np.eye(3); T[:2, :2] = A; T[:2, 2] = np.array([540, 540]) - A @ np.array([mx, my]); return T
CANT = np.float32([[-MG, -MG], [1080 + MG, -MG], [-MG, 1080 + MG], [1080 + MG, 1080 + MG]])
def decompor(H):
    H = H / H[2, 2]; c = np.linalg.inv(H) @ np.array([540, 540, 1.0]); w = c[:2] / c[2]
    def Hp(x, y):
        v = H @ np.array([x, y, 1.0]); return v[:2] / v[2]
    e = 0.5; J = np.column_stack([(Hp(w[0] + e, w[1]) - Hp(w[0] - e, w[1])) / (2 * e), (Hp(w[0], w[1] + e) - Hp(w[0], w[1] - e)) / (2 * e)])
    s = math.sqrt(abs(np.linalg.det(J))); g = math.degrees(math.atan2(J[1, 0] - J[0, 1], J[0, 0] + J[1, 1]))
    S = S_de(math.log2(s), w[0], w[1], g); Pm = H @ np.linalg.inv(S); Pm /= Pm[2, 2]
    cp = cv2.perspectiveTransform(CANT[None], Pm)[0] - CANT
    return [math.log2(s), float(w[0]), float(w[1]), g], cp.ravel().tolist()
cap = cv2.VideoCapture(ORIG); cache = {}
def quadro(f):
    if f not in cache:
        cap.set(cv2.CAP_PROP_POS_FRAMES, f); ok, q = cap.read(); cache[f] = cv2.cvtColor(q, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
    return cache[f]
def ecc(fa, fb, H0):
    """H tela(fa)->tela(fb). ECC acha W com  I_b(W x) ~ I_a(x)?  Usamos template = fb, input = fa: W leva coords de fb -> fa."""
    A = quadro(fa); B = quadro(fb)
    W = np.linalg.inv(H0)  # fb -> fa
    W = W / W[2, 2]
    cc = -1
    for sc, bl in ((0.125, 5), (0.25, 5), (0.5, 3)):
        As = cv2.GaussianBlur(cv2.resize(A, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
        Bs = cv2.GaussianBlur(cv2.resize(B, None, fx=sc, fy=sc, interpolation=cv2.INTER_AREA), (bl, bl), 0)
        Ssc = np.diag([sc, sc, 1.0]); Ws = Ssc @ W @ np.linalg.inv(Ssc)
        try:
            if modelo == "homog":
                cc, Ws = cv2.findTransformECC(Bs, As, Ws.astype(np.float32), cv2.MOTION_HOMOGRAPHY, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
            else:
                cc, W2 = cv2.findTransformECC(Bs, As, Ws[:2].astype(np.float32), cv2.MOTION_AFFINE, (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 150, 1e-6), None, 1)
                Ws = np.vstack([W2, [0, 0, 1]])
        except cv2.error:
            return None, -1
        W = np.linalg.inv(Ssc) @ Ws @ Ssc
    return np.linalg.inv(W), cc
f0 = int(math.ceil(t0 * 24 - 1e-6)); f1 = int(math.floor(t1 * 24 + 1e-6)); fa = int(round(tanc * 24))
sol = {fa: (S_de(*json_cam(fa / 24.0)), 1.0)}
for passo, lim in ((1, f1), (-1, f0)):
    f = fa
    while (f < lim if passo > 0 else f > lim):
        g = f + passo
        H0 = S_de(*json_cam(g / 24.0)) @ np.linalg.inv(S_de(*json_cam(f / 24.0)))   # movimento do json
        H, cc = ecc(f, g, H0)
        if H is None or cc < 0.5:
            H, cc = H0, -1   # sem alinhamento: segue o json neste passo
        elif "--sim" in a:
            # projeta o passo numa similaridade (sem cisalhamento acumulado), preservando o centro da tela
            H = H / H[2, 2]; A2 = H[:2, :2]; sc = math.sqrt(abs(np.linalg.det(A2))); rr = math.atan2(A2[1, 0] - A2[0, 1], A2[0, 0] + A2[1, 1])
            c = H @ np.array([540, 540, 1.0]); c = c[:2] / c[2]
            R2 = sc * np.array([[math.cos(rr), -math.sin(rr)], [math.sin(rr), math.cos(rr)]])
            Hs = np.eye(3); Hs[:2, :2] = R2; Hs[:2, 2] = c - R2 @ np.array([540, 540]); H = Hs
        sol[g] = (H @ sol[f][0], cc); f = g
linhas = []
for f in range(f0, f1 + 1):
    cam, cp = decompor(sol[f][0]); jc = json_cam(f / 24.0)
    linhas.append({"t": round(f / 24.0, 4), "zoom": cam[0], "mx": cam[1], "my": cam[2], "giro": cam[3], "cp": cp, "met": "ecc %.2f" % sol[f][1]})
    print("%.3f ecc%5.2f z%6.3f M(%7.1f,%7.1f) g%6.2f pin %5.1f | json z%6.3f M(%7.1f,%7.1f) g%6.2f" % (f / 24.0, sol[f][1], cam[0], cam[1], cam[2], cam[3], max(abs(v) for v in cp), jc[0], jc[1], jc[2], jc[3]))
json.dump({"cena": cid, "ancoras": [tanc], "margem": MG, "quadros": linhas}, open(out, "w"), indent=0)
