File size: 4,328 Bytes
7fe60a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594d19b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7fe60a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""
logos/baker.py - The SPCW Encoder (Fractal Compression)
"""

import numpy as np
from .fractal_engine import ContextAction

class BreadBaker:
    """
    Encodes image tiles into Prime Atoms using Quadtree Decomposition.
    """
    
    def __init__(self, variance_threshold=30.0, min_block_size=8):
        self.variance_threshold = variance_threshold
        self.min_block_size = min_block_size
    
    def bake(self, tile: np.ndarray, prefix_path: list = None) -> list:
        """
        Compress a tile into atom definitions.
        Returns list of dicts: {'path_bits': [], 'payload': bytes}
        """
        self.atoms = []
        if prefix_path is None:
            prefix_path = []
            
        self._recursive_decompose(tile, level=0, path_bits=prefix_path)
        return self.atoms

    def _recursive_decompose(self, block: np.ndarray, level: int, path_bits: list):
        h, w = block.shape[:2]
        if h == 0 or w == 0: return

        mean_val = np.mean(block, axis=(0, 1))
        variance = np.var(block)
        
        # Stop condition: Low variance OR too small OR max depth
        # Max local depth 5 (512 -> 16px blocks)
        is_leaf = (variance < self.variance_threshold) or (w <= self.min_block_size) or (level >= 5)
        
        if is_leaf:
            self._emit_atom(mean_val, path_bits)
        else:
            # Subdivide
            half_h, half_w = h // 2, w // 2
            # TL, TR, BL, BR
            self._recursive_decompose(block[:half_h, :half_w], level + 1, path_bits + [0b00])
            self._recursive_decompose(block[:half_h, half_w:], level + 1, path_bits + [0b01])
            self._recursive_decompose(block[half_h:, :half_w], level + 1, path_bits + [0b10])
            self._recursive_decompose(block[half_h:, half_w:], level + 1, path_bits + [0b11])

    def _emit_atom(self, rgb_mean, path_bits):
        r, g, b = int(rgb_mean[0]), int(rgb_mean[1]), int(rgb_mean[2])
        matrix_key = (b << 16) | (g << 8) | r 
        
        # --- ATOM 1: WARM UP (Harmonic Resonance) ---
        # We must raise heat_state from 0 to 1 so that GF4 multiplication (Dissolution)
        # has a non-zero scalar. 0 * Input = 0 (No Change).
        # ContextAction.PERSIST (00) increments heat.
        
        warm_action = ContextAction.PERSIST
        warm_ctrl = (warm_action.value << 6) & 0xC0
        warm_payload = bytes([warm_ctrl])
        
        self.atoms.append({
            "path_bits": path_bits,
            "payload": warm_payload
        })
        
        # --- ATOM 2: STRUCTURE (Matrix Key) ---
        # Pack payload compatible with LogoFractalEngine vector decoder
        # We pack matrix_key (RGB) into the vector slots
        action = ContextAction.CHANGE_1
        control_byte = (action.value << 6) & 0xC0
        
        payload_bytes = bytearray()
        payload_bytes.append(control_byte)
        
        # Convert RGB integer to 2-bit vector sequence
        vector = [(matrix_key >> (i*2)) & 0b11 for i in range(16)]
        
        # Pack vector into 4 bytes
        # Note: reconstruction expects this exact packing to retrieve matrix_key
        # Byte order matters. We pack vector[0..3] into first byte, etc.
        # This aligns with `_payload_to_vector` in fractal_engine.
        
        # Re-verify _payload_to_vector in fractal_engine:
        # for byte in payload[:8]:
        #    append((byte >> 6) & 0b11) ...
        
        # So we pack vector[0] into MSB of byte 0?
        # No, `_payload_to_vector` extracts MSB first: `(byte >> 6) & 0b11`.
        # So we should pack vector[0] at MSB.
        
        current_vector_idx = 0
        for _ in range(4): # 4 bytes to hold 16 pairs
            b_val = 0
            if current_vector_idx < 16: b_val |= (vector[current_vector_idx] & 0b11) << 6
            if current_vector_idx+1 < 16: b_val |= (vector[current_vector_idx+1] & 0b11) << 4
            if current_vector_idx+2 < 16: b_val |= (vector[current_vector_idx+2] & 0b11) << 2
            if current_vector_idx+3 < 16: b_val |= (vector[current_vector_idx+3] & 0b11)
            
            payload_bytes.append(b_val)
            current_vector_idx += 4
            
        self.atoms.append({
            "path_bits": path_bits,
            "payload": bytes(payload_bytes)
        })