Create flc_core.py
Browse files- flc_core.py +244 -0
flc_core.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, struct, math
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageDraw
|
| 4 |
+
|
| 5 |
+
PHI = (1.0 + 5.0**0.5) / 2.0
|
| 6 |
+
MAGIC = b"FLC1\x00\x00\x00\x00"
|
| 7 |
+
VER_V2 = 2
|
| 8 |
+
|
| 9 |
+
# --- Fibonacci & Frequency Logic ---
|
| 10 |
+
def fibonacci_sequence(n):
|
| 11 |
+
fibs = [1, 2]
|
| 12 |
+
while len(fibs) < n:
|
| 13 |
+
fibs.append(fibs[-1] + fibs[-2])
|
| 14 |
+
return np.array(fibs[:n], dtype=np.int64)
|
| 15 |
+
|
| 16 |
+
def fibonacci_sequence_std(n):
|
| 17 |
+
fibs = [1, 1]
|
| 18 |
+
while len(fibs) < n:
|
| 19 |
+
fibs.append(fibs[-1] + fibs[-2])
|
| 20 |
+
return np.array(fibs[:n], dtype=np.int64)
|
| 21 |
+
|
| 22 |
+
def fibonacci_frequency_boundaries(n_coeffs: int, n_bands: int):
|
| 23 |
+
if n_bands < 2: return [0, n_coeffs]
|
| 24 |
+
fibs = fibonacci_sequence(n_bands).astype(np.float64)
|
| 25 |
+
w = fibs / (fibs.sum() + 1e-12)
|
| 26 |
+
cum = np.cumsum(w)
|
| 27 |
+
b = [0]
|
| 28 |
+
for i in range(n_bands - 1):
|
| 29 |
+
b.append(int(round(n_coeffs * cum[i])))
|
| 30 |
+
b.append(n_coeffs)
|
| 31 |
+
# Ensure strict monotonicity
|
| 32 |
+
for i in range(1, len(b)):
|
| 33 |
+
if b[i] <= b[i-1]: b[i] = b[i-1] + 1
|
| 34 |
+
return b
|
| 35 |
+
|
| 36 |
+
# --- Orthonormal DCT Engine ---
|
| 37 |
+
def dct_ortho_1d(x: np.ndarray) -> np.ndarray:
|
| 38 |
+
N = x.shape[0]
|
| 39 |
+
v = np.concatenate([x, x[::-1]])
|
| 40 |
+
V = np.fft.fft(v)
|
| 41 |
+
k = np.arange(N)
|
| 42 |
+
X = np.real(V[:N] * np.exp(-1j * np.pi * k / (2 * N)))
|
| 43 |
+
X *= 2.0
|
| 44 |
+
X[0] *= (1.0 / math.sqrt(4 * N))
|
| 45 |
+
X[1:] *= (1.0 / math.sqrt(2 * N))
|
| 46 |
+
return X
|
| 47 |
+
|
| 48 |
+
def idct_ortho_1d(X: np.ndarray) -> np.ndarray:
|
| 49 |
+
N = X.shape[0]
|
| 50 |
+
x0, xr = X[0] * math.sqrt(4 * N), X[1:] * math.sqrt(2 * N)
|
| 51 |
+
c = np.empty(N, dtype=np.complex128)
|
| 52 |
+
c[0], c[1:] = x0 / 2.0, xr / 2.0
|
| 53 |
+
k = np.arange(N)
|
| 54 |
+
c = c * np.exp(1j * np.pi * k / (2 * N))
|
| 55 |
+
V = np.zeros(2 * N, dtype=np.complex128)
|
| 56 |
+
V[:N] = c
|
| 57 |
+
V[N+1:] = np.conj(c[1:][::-1])
|
| 58 |
+
return np.fft.ifft(V).real[:N]
|
| 59 |
+
|
| 60 |
+
def dct_blocks_ortho(x_blocks: np.ndarray) -> np.ndarray:
|
| 61 |
+
return np.array([dct_ortho_1d(b) for b in x_blocks])
|
| 62 |
+
|
| 63 |
+
def idct_blocks_ortho(X_blocks: np.ndarray) -> np.ndarray:
|
| 64 |
+
return np.array([idct_ortho_1d(B) for B in X_blocks])
|
| 65 |
+
|
| 66 |
+
# --- Fibonacci Coding (Bit IO) ---
|
| 67 |
+
class BitWriter:
|
| 68 |
+
def __init__(self):
|
| 69 |
+
self.buf, self.acc, self.nbits = bytearray(), 0, 0
|
| 70 |
+
def write_bit(self, b: int):
|
| 71 |
+
self.acc = (self.acc << 1) | (b & 1)
|
| 72 |
+
self.nbits += 1
|
| 73 |
+
if self.nbits == 8:
|
| 74 |
+
self.buf.append(self.acc); self.acc = 0; self.nbits = 0
|
| 75 |
+
def finish(self):
|
| 76 |
+
if self.nbits: self.buf.append(self.acc << (8 - self.nbits))
|
| 77 |
+
return bytes(self.buf)
|
| 78 |
+
|
| 79 |
+
class BitReader:
|
| 80 |
+
def __init__(self, data: bytes):
|
| 81 |
+
self.data, self.i, self.acc, self.nbits = data, 0, 0, 0
|
| 82 |
+
def read_bit(self):
|
| 83 |
+
if self.nbits == 0:
|
| 84 |
+
self.acc = self.data[self.i]; self.i += 1; self.nbits = 8
|
| 85 |
+
b = (self.acc >> (self.nbits - 1)) & 1
|
| 86 |
+
self.nbits -= 1
|
| 87 |
+
return b
|
| 88 |
+
|
| 89 |
+
def fib_encode_nonneg(bw, n):
|
| 90 |
+
m = int(n) + 1
|
| 91 |
+
fibs = [1, 2]
|
| 92 |
+
while fibs[-1] <= m: fibs.append(fibs[-1] + fibs[-2])
|
| 93 |
+
bits = [0] * (len(fibs) - 1)
|
| 94 |
+
for i in reversed(range(len(bits))):
|
| 95 |
+
if fibs[i] <= m: bits[i] = 1; m -= fibs[i]
|
| 96 |
+
for i in range(max((i for i, b in enumerate(bits) if b), default=0) + 1):
|
| 97 |
+
bw.write_bit(bits[i])
|
| 98 |
+
bw.write_bit(1)
|
| 99 |
+
|
| 100 |
+
def fib_decode_nonneg(br):
|
| 101 |
+
fibs, bits, prev = [1, 2], [], 0
|
| 102 |
+
while True:
|
| 103 |
+
b = br.read_bit(); bits.append(b)
|
| 104 |
+
if prev == 1 and b == 1: break
|
| 105 |
+
prev = b
|
| 106 |
+
if len(bits) > len(fibs): fibs.append(fibs[-1] + fibs[-2])
|
| 107 |
+
m = sum(fibs[i] for i, bi in enumerate(bits[:-1]) if bi)
|
| 108 |
+
return m - 1
|
| 109 |
+
|
| 110 |
+
def rle_fib_encode_ints(ints):
|
| 111 |
+
bw = BitWriter()
|
| 112 |
+
zrun = 0
|
| 113 |
+
for v in ints:
|
| 114 |
+
if v == 0: zrun += 1; continue
|
| 115 |
+
if zrun: bw.write_bit(0); fib_encode_nonneg(bw, zrun); zrun = 0
|
| 116 |
+
bw.write_bit(1); fib_encode_nonneg(bw, (v << 1) ^ (v >> 63))
|
| 117 |
+
if zrun: bw.write_bit(0); fib_encode_nonneg(bw, zrun)
|
| 118 |
+
return bw.finish()
|
| 119 |
+
|
| 120 |
+
def rle_fib_decode_ints(payload, n_out):
|
| 121 |
+
br, out, i = BitReader(payload), np.zeros(n_out, dtype=np.int64), 0
|
| 122 |
+
while i < n_out:
|
| 123 |
+
if br.read_bit() == 0: i = min(n_out, i + fib_decode_nonneg(br))
|
| 124 |
+
else:
|
| 125 |
+
u = fib_decode_nonneg(br)
|
| 126 |
+
out[i] = (u >> 1) ^ (-(u & 1)); i += 1
|
| 127 |
+
return out
|
| 128 |
+
|
| 129 |
+
# --- Quantization & Spiral Visuals ---
|
| 130 |
+
def band_quantize_dct(coeffs, boundaries, base_step):
|
| 131 |
+
q = np.zeros_like(coeffs, dtype=np.int32)
|
| 132 |
+
for bi in range(len(boundaries) - 1):
|
| 133 |
+
a, b = boundaries[bi], boundaries[bi + 1]
|
| 134 |
+
step = base_step * (PHI ** bi)
|
| 135 |
+
q[:, a:b] = np.round(coeffs[:, a:b] / step)
|
| 136 |
+
return q
|
| 137 |
+
|
| 138 |
+
def band_dequantize_dct(q, boundaries, base_step):
|
| 139 |
+
coeffs = np.zeros_like(q, dtype=np.float64)
|
| 140 |
+
for bi in range(len(boundaries) - 1):
|
| 141 |
+
a, b = boundaries[bi], boundaries[bi + 1]
|
| 142 |
+
step = base_step * (PHI ** bi)
|
| 143 |
+
coeffs[:, a:b] = q[:, a:b] * step
|
| 144 |
+
return coeffs
|
| 145 |
+
|
| 146 |
+
def hologram_spectrum_image(zints, max_symbols=262144):
|
| 147 |
+
z = zints[:max_symbols]; v = np.tanh(z / 32.0)
|
| 148 |
+
theta = (2 * math.pi / (PHI**2)) * np.arange(v.size) + 2.0 * math.pi * (v * 0.25)
|
| 149 |
+
r = 1.0 + 0.35 * np.abs(v)
|
| 150 |
+
syms = r * np.cos(theta) + 1j * r * np.sin(theta)
|
| 151 |
+
N = int(2**math.ceil(math.log2(math.sqrt(syms.size or 1))))
|
| 152 |
+
U = np.pad(syms, (0, N*N - syms.size)).reshape(N, N)
|
| 153 |
+
mag = np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(U))))
|
| 154 |
+
mag = (mag - mag.min()) / (mag.max() - mag.min() + 1e-12)
|
| 155 |
+
return (mag * 255).astype(np.uint8)
|
| 156 |
+
|
| 157 |
+
def bytes_to_fib_spiral_image(data, max_pixels=262144):
|
| 158 |
+
arr = np.frombuffer(data, dtype=np.uint8)[:max_pixels]
|
| 159 |
+
fibs = fibonacci_sequence_std(32)
|
| 160 |
+
sizes, area = [], 0
|
| 161 |
+
for s in fibs:
|
| 162 |
+
sizes.append(int(s)); area += s*s
|
| 163 |
+
if area >= arr.size: break
|
| 164 |
+
# Simple tile placement logic for demo
|
| 165 |
+
tiles, minx, miny, maxx, maxy = [], 0, 0, 0, 0
|
| 166 |
+
curr_x, curr_y = 0, 0
|
| 167 |
+
for i, s in enumerate(sizes):
|
| 168 |
+
d = (i-1)%4
|
| 169 |
+
if i>0:
|
| 170 |
+
if d==0: curr_x = maxx; curr_y = miny
|
| 171 |
+
elif d==1: curr_x = maxx-s; curr_y = maxy
|
| 172 |
+
elif d==2: curr_x = minx-s; curr_y = maxy-s
|
| 173 |
+
else: curr_x = minx; curr_y = miny-s
|
| 174 |
+
tiles.append((curr_x, curr_y, s))
|
| 175 |
+
minx, miny = min(minx, curr_x), min(miny, curr_y)
|
| 176 |
+
maxx, maxy = max(maxx, curr_x+s), max(maxy, curr_y+s)
|
| 177 |
+
|
| 178 |
+
W, H = maxx-minx, maxy-miny
|
| 179 |
+
img = np.zeros((H, W), dtype=np.uint8)
|
| 180 |
+
idx = 0
|
| 181 |
+
for x, y, s in tiles:
|
| 182 |
+
take = min(s*s, arr.size - idx)
|
| 183 |
+
if take <= 0: break
|
| 184 |
+
block = np.pad(arr[idx:idx+take], (0, s*s-take)).reshape(s, s)
|
| 185 |
+
img[H-(y-miny+s):H-(y-miny), x-minx:x-minx+s] = block
|
| 186 |
+
idx += take
|
| 187 |
+
return img, tiles, (minx, miny, maxx, maxy)
|
| 188 |
+
|
| 189 |
+
# --- High Level API ---
|
| 190 |
+
def flc_encode_file(in_path, out_flc, preview_png=None, unzip_gif=None, block_len=1024, n_bands=10, base_step=0.004, **kwargs):
|
| 191 |
+
raw = open(in_path, "rb").read()
|
| 192 |
+
x = (np.frombuffer(raw, dtype=np.uint8).astype(np.float64) - 127.5) / 127.5
|
| 193 |
+
pad = (-x.size) % block_len
|
| 194 |
+
X = np.pad(x, (0, pad)).reshape(-1, block_len)
|
| 195 |
+
C = dct_blocks_ortho(X)
|
| 196 |
+
bnds = fibonacci_frequency_boundaries(block_len, n_bands)
|
| 197 |
+
Q = band_quantize_dct(C, bnds, base_step)
|
| 198 |
+
payload = rle_fib_encode_ints(np.diff(Q.flatten(), prepend=0))
|
| 199 |
+
|
| 200 |
+
header = struct.pack("<8sH Q I I H d d H", MAGIC, VER_V2, len(raw), block_len, X.shape[0], n_bands, base_step, 127.5, len(bnds))
|
| 201 |
+
with open(out_flc, "wb") as f:
|
| 202 |
+
f.write(header); f.write(struct.pack("<"+ "I"*len(bnds), *bnds))
|
| 203 |
+
f.write(struct.pack("<I", len(payload))); f.write(payload)
|
| 204 |
+
|
| 205 |
+
if unzip_gif: # Generate the unzip frames
|
| 206 |
+
frames = []
|
| 207 |
+
for t in range(1, n_bands + 1):
|
| 208 |
+
Q_p = np.zeros_like(Q)
|
| 209 |
+
for bi in range(t): Q_p[:, bnds[bi]:bnds[bi+1]] = Q[:, bnds[bi]:bnds[bi+1]]
|
| 210 |
+
X_p = idct_blocks_ortho(band_dequantize_dct(Q_p, bnds, base_step))
|
| 211 |
+
recon = np.clip((X_p.flatten()[:len(raw)] * 127.5) + 127.5, 0, 255).astype(np.uint8)
|
| 212 |
+
|
| 213 |
+
# Create Frame
|
| 214 |
+
h_img = Image.fromarray(hologram_spectrum_image(Q_p.flatten())).resize((256, 256))
|
| 215 |
+
s_img_arr, _, _ = bytes_to_fib_spiral_image(recon.tobytes())
|
| 216 |
+
s_img = Image.fromarray(s_img_arr).resize((256, 256))
|
| 217 |
+
|
| 218 |
+
frame = Image.new("RGB", (512, 280), (10, 10, 15))
|
| 219 |
+
frame.paste(h_img, (0, 24)); frame.paste(s_img, (256, 24))
|
| 220 |
+
frames.append(frame)
|
| 221 |
+
frames[0].save(unzip_gif, save_all=True, append_images=frames[1:], duration=100, loop=0)
|
| 222 |
+
|
| 223 |
+
return {"n_bytes": len(raw), "payload_len": len(payload), "ratio": len(payload)/len(raw)}
|
| 224 |
+
|
| 225 |
+
def flc_decode_file(in_flc, out_path):
|
| 226 |
+
blob = open(in_flc, "rb").read()
|
| 227 |
+
h_sz = struct.calcsize("<8sH Q I I H d d H")
|
| 228 |
+
magic, ver, n_bytes, b_len, n_blks, n_bnds, step, mu, bnd_l = struct.unpack_from("<8sH Q I I H d d H", blob)
|
| 229 |
+
off = h_sz
|
| 230 |
+
bnds = struct.unpack_from("<" + "I"*bnd_l, blob, off); off += 4*bnd_l
|
| 231 |
+
p_len = struct.unpack_from("<I", blob, off)[0]; off += 4
|
| 232 |
+
d = rle_fib_decode_ints(blob[off:off+p_len], n_blks * b_len)
|
| 233 |
+
Q = np.cumsum(d).reshape(n_blks, b_len)
|
| 234 |
+
X = idct_blocks_ortho(band_dequantize_dct(Q, bnds, step))
|
| 235 |
+
res = np.clip((X.flatten()[:n_bytes] * 127.5) + mu, 0, 255).astype(np.uint8)
|
| 236 |
+
with open(out_path, "wb") as f: f.write(res.tobytes())
|
| 237 |
+
return {"n_bytes": n_bytes}
|
| 238 |
+
|
| 239 |
+
def cosine_similarity_bytes(a, b):
|
| 240 |
+
x = np.frombuffer(a, dtype=np.uint8).astype(float)
|
| 241 |
+
y = np.frombuffer(b, dtype=np.uint8).astype(float)
|
| 242 |
+
n = min(len(x), len(y))
|
| 243 |
+
x, y = x[:n]-x[:n].mean(), y[:n]-y[:n].mean()
|
| 244 |
+
return np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12)
|