Skip to content

STATE_CLUSTERING — Zustandsbasierte Frame-Clusterung

C++ Implementierung: runner_pipeline.cpp Phase-Enum: Phase::STATE_CLUSTERING

Übersicht

Phase 10 gruppiert Frames nach ihrem Qualitätszustand mittels K-Means-Clusterung auf einem 6-dimensionalen Zustandsvektor. Frames mit ähnlicher Qualität werden einem gemeinsamen Cluster zugeordnet. In der nächsten Phase (SYNTHETIC_FRAMES) wird pro Cluster ein synthetischer Frame erzeugt — dies reduziert die Frame-Anzahl von N auf K bei gleichzeitiger Rauschreduktion.

Mode Gate in v3.3.9: Diese Phase ist nur aktiv, wenn N >= max(frames_reduced_threshold, 50). Darunter wird Clustering deterministisch übersprungen oder auf den Reduced-Mode-Pfad begrenzt.

┌──────────────────────────────────────────────────────┐
│  1. Zustandsvektor pro Frame berechnen               │
│     v_f = [G_f, mean_Q, var_Q, CC̄, WarpVar̄, inv_f]   │
│                                                      │
│  2. z-Score Normalisierung (6 Dimensionen)           │
│                                                      │
│  3. K bestimmen: K = clip(N/10, k_min, k_max)        │
│                                                      │
│  4. K-Means Clusterung (20 Iterationen)              │
│     • Initialisierung: gleichmäßig verteilte Frames  │
│     • Assign-Labels → Update-Centers → repeat        │
│                                                      │
│  5. Degenerations-Check                              │
│     • Leere Cluster? → Quantile-Fallback             │
│                                                      │
│  Output: cluster_labels[N], cluster_sizes[K]         │
└──────────────────────────────────────────────────────┘

Reduced Mode / Clustering Gate

const bool reduced_mode = (frames.size() < cfg.assumptions.frames_reduced_threshold);
const bool clustering_gate = (frames.size() >= std::max(cfg.assumptions.frames_reduced_threshold, 50));
const bool skip_clustering = (!clustering_gate) ||
                             (reduced_mode && cfg.assumptions.reduced_mode_skip_clustering);

if (skip_clustering) {
    use_synthetic_frames = false;
    emitter.phase_end(run_id, Phase::STATE_CLUSTERING, "skipped",
                      {{"reason", "reduced_mode"}, ...});
}

Wenn reduced_mode_skip_clustering = true oder das v3.3.9-Gate N >= max(N_red, 50) nicht erfüllt ist: - Phase wird als "skipped" markiert - Alle Frames erhalten Label 0 (ein Cluster) - Synthetische Frames werden nicht erzeugt - TILE_RECONSTRUCTION-Ergebnis wird direkt als finales Bild verwendet

1. Zustandsvektor (6D)

Pro Frame wird ein 6-dimensionaler Zustandsvektor berechnet:

state_vectors[fi] = {
    G_f,                    // Globales Gewicht (Phase 5)
    mean_local,             // Mittelwert lokaler Qualitäts-Scores
    var_local,              // Varianz lokaler Qualitäts-Scores
    mean_cc_tiles,          // Mittlere Tile-Korrelation (global)
    mean_warp_var_tiles,    // Mittlere Warp-Varianz (global)
    frame_invalid_fraction  // Anteil ungültiger Tiles
};
Dimension Symbol Quelle Beschreibung
0 G_f Phase 5 Globales Frame-Gewicht
1 ⟨Q_local⟩_f Phase 8 Mittelwert der lokalen Tile-Quality-Scores
2 Var(Q_local)_f Phase 8 Varianz der lokalen Tile-Quality-Scores
3 CC̄_tiles Phase 9 Mittlere Tile-Korrelation (über alle Tiles)
4 WarpVar̄ Phase 9 Mittlere Warp-Varianz (über alle Tiles)
5 inv_frac_f Phase 9 Anteil ungültiger Tiles am Gesamtgrid

Mean/Varianz lokaler Qualität

float mean_local = 0.0f, var_local = 0.0f;
for (const auto &tm : local_metrics[fi])
    mean_local += tm.quality_score;
mean_local /= local_metrics[fi].size();
for (const auto &tm : local_metrics[fi]) {
    float diff = tm.quality_score - mean_local;
    var_local += diff * diff;
}
var_local /= local_metrics[fi].size();

2. z-Score Normalisierung

const size_t D = 6;
std::vector<float> means(D, 0.0f);
std::vector<float> stds(D, 0.0f);

// Mean + Std pro Dimension
for (size_t d = 0; d < D; ++d) {
    means[d] = sum(X[*][d]) / N;
    stds[d] = sqrt(var(X[*][d]));
}

// Normalisierung
for (size_t i = 0; i < X.size(); ++i)
    for (size_t d = 0; d < D; ++d)
        X[i][d] = (stds[d] > eps) ? ((X[i][d] - means[d]) / stds[d]) : 0.0f;
  • Alle 6 Dimensionen werden auf Mittelwert=0, Standardabweichung=1 normalisiert
  • Verhindert, dass eine Dimension die Clusterung dominiert
  • Bei std=0 (konstante Dimension): wird auf 0 gesetzt

3. Cluster-Anzahl K

int k_min = cfg.synthetic.clustering.cluster_count_range[0];
int k_max = cfg.synthetic.clustering.cluster_count_range[1];
int k_default = std::max(k_min, std::min(k_max, n_frames / 10));
n_clusters = std::min(k_default, n_frames);
K = clip(floor(N / 10), k_min, k_max)

In v3.3.9 ist zusätzlich wichtig:

  • K_min = 5 wird erst ab N >= 50 sinnvoll erreichbar,
  • deshalb ist die feste alte Beschreibung "aktiv ab N >= 200" nicht mehr korrekt,
  • der bindende Gate-Mechanismus ist das Modus-Framework plus Untergrenze 50.
N Frames k_min=3, k_max=30 K
50 clip(5, 3, 30) 5
100 clip(10, 3, 30) 10
200 clip(20, 3, 30) 20
500 clip(50, 3, 30) 30

4. K-Means Clusterung

// Initialisierung: gleichmäßig verteilte Frames als Zentren
for (int c = 0; c < n_clusters; ++c) {
    int idx = (c * n_frames) / n_clusters;
    centers[c] = X[idx];
}

// 20 Iterationen
for (int iter = 0; iter < 20; ++iter) {
    // Assign: jeder Frame zum nächsten Zentrum
    for (size_t fi = 0; fi < X.size(); ++fi) {
        float best_dist = MAX;
        for (int c = 0; c < n_clusters; ++c) {
            float dist = euclidean_distance_sq(X[fi], centers[c]);
            if (dist < best_dist) { best_dist = dist; cluster_labels[fi] = c; }
        }
    }
    // Update: neue Zentren als Mittelwert der Cluster-Mitglieder
    for (int c = 0; c < n_clusters; ++c) {
        centers[c] = mean(X[fi] where cluster_labels[fi] == c);
    }
}
  • Initialisierung: Gleichmäßig verteilte Frames (nicht k-means++)
  • Distanzmetrik: Euklidische Distanz im 6D-Raum (nach z-Normalisierung)
  • 20 Iterationen (fest, kein Konvergenzcheck)
  • Methode: "kmeans"

5. Degenerations-Fallback

bool degenerate = false;
for (int c = 0; c < n_clusters; ++c) {
    if (counts[c] <= 0) { degenerate = true; break; }
}

if (degenerate && n_clusters > 1) {
    clustering_method = "quantile";
    // Sortiere Frames nach G_f, verteile gleichmäßig auf Cluster
    std::sort(order.begin(), order.end(), by_global_weight);
    for (size_t r = 0; r < order.size(); ++r) {
        int label = (r * n_clusters) / order.size();
        cluster_labels[order[r].second] = label;
    }
}

Wenn K-Means zu leeren Clustern führt (z.B. bei sehr homogenen Daten): - Quantile-Fallback: Frames werden nach globalem Gewicht G_f sortiert und gleichmäßig auf K Cluster verteilt - Jeder Cluster enthält dann N/K Frames - Methode wird als "quantile" im Artifact vermerkt

Konfigurationsparameter

Parameter Beschreibung Default
synthetic.clustering.cluster_count_range [k_min, k_max] [3, 30]
assumptions.frames_reduced_threshold Threshold für Reduced Mode konfigurierbar
assumptions.reduced_mode_skip_clustering Clustering in Reduced Mode überspringen true

Artifact: state_clustering.json

{
  "n_clusters": 10,
  "k_min": 3,
  "k_max": 30,
  "method": "kmeans",
  "clustering_gate_threshold": "max(frames_reduced_threshold,50)",
  "cluster_labels": [0, 0, 1, 2, 0, 3, ...],
  "cluster_sizes": [12, 8, 15, 10, ...]
}

## Hinweis zu `v3.3.9`

Die eigentliche neue Aggregationslogik von `v3.3.9` sitzt nicht in der Label-Zuordnung selbst, sondern in der **massenerhaltenden** Verwendung der Cluster in der Endaggregation:

- pro Cluster/Kanal wird `M_{k,c} = sum_{f in k} G_{f,c}` geführt,
- diese Cluster-Masse geht später verbindlich in die finalen Cluster-Gewichte ein.

Das heißt: Clustering liefert weiterhin Labels und Clustergrößen, aber die Bedeutung der Cluster in den Folgephasen ist in `v3.3.9` stärker normiert als in älteren Dokumentständen.

Nächste Phase

Phase 11: SYNTHETIC_FRAMES — Synthetische Frame-Erzeugung