From 791c00dacc106d9c5d2bb68d59edd14493ee8a57 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 15 Oct 2026 15:14:52 +0200 Subject: [PATCH] Nicht wie hell das Bild ist, sondern wie viel davon schwarz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemessen wurde der Mittelwert über das ganze Bild, und daran scheiterte die Abspannsuche wiederholt: weisse Schrift hebt den Mittelwert. Derselbe Abspann misst 19, wenn wenig Schrift darin steht, und 37, wenn viel darin steht — die Schwelle musste zwischen diesen Wert und die Handlung passen, und der Abstand schrumpfte mit jeder Textzeile. Zweimal nachgezogen, zweimal wieder getroffen. Jellyfin zählt stattdessen Pixel, und das ist kein besserer Schwellenwert, sondern ein besseres Merkmal. Nachgemessen an denselben erzeugten Abspännen, durch dieselbe Kette wie im Betrieb: Mittelwert Anteil dunkler Pixel Handlung 128 0 % Abspann, schriftreich 37 84 % Abspann, schlank 19 87 % dunkle Nachtszene 28 37 % Schrift belegt ein bis zehn Prozent der Fläche; der Rest bleibt schwarz. Der Anteil hängt deshalb nicht an der Schriftmenge, und der Abstand zur Handlung ist nach beiden Seiten riesig. `signalstats` weicht `blackframe=amount=0:threshold=24` — die Schwelle für das einzelne Pixel über eine Reihe von Werten eingemessen: darunter (16) fällt der Abspann selbst auf zehn Prozent ab, darüber (32) fängt die Handlung an mitzuzählen. Die Ausgabe kommt über denselben `metadata=print`-Weg wie vorher, nur unter einem anderen Schlüssel. **Und die relative Schwelle fällt ersatzlos weg.** Sie war nötig, solange der Mittelwert gemessen wurde: wie hell ein Abspann ist, hing am Film, also musste sich die Schwelle am Film ausrichten. Der Pixelanteil eicht sich selbst — `Abspannkante.Schwelle` und `Abspannregeln.Anteil` sind gestrichen, und mit ihnen die Rechnerei um den Grundton. Co-Authored-By: Claude Opus 5 --- server/src/Flaro.Api/Markenerkennung.cs | 12 +- server/src/Flaro.Core/Media/Abspannkante.cs | 115 ++++++--------- server/src/Flaro.Media/Bildabtaster.cs | 51 ++++--- .../Flaro.Core.Tests/AbspannkanteTests.cs | 138 ++++++++---------- .../Flaro.Media.Tests/BildabtasterTests.cs | 53 ++++--- 5 files changed, 170 insertions(+), 199 deletions(-) diff --git a/server/src/Flaro.Api/Markenerkennung.cs b/server/src/Flaro.Api/Markenerkennung.cs index 6f6734a..50ca154 100644 --- a/server/src/Flaro.Api/Markenerkennung.cs +++ b/server/src/Flaro.Api/Markenerkennung.cs @@ -510,17 +510,17 @@ public sealed class Markenerkennung( // weiterbauen lohnt — und das sieht man nur an vielen Dateien. log.LogInformation( "„{Titel}“: kein Abspann — {Grund}. {Proben} Probe(n), " - + "Schwelle {Schwelle:0.#}, letzte Probe {Letzte:0.#}. {Kapitel}", - name, grund, proben.Count, Abspannkante.Schwelle(proben, regeln), - proben.Count > 0 ? proben[^1].Helligkeit : double.NaN, - Kapitellage(kapitel)); + + "letzte Probe {Letzte:0.#} % dunkel (nötig: {Noetig} %). {Kapitel}", + name, grund, proben.Count, + proben.Count > 0 ? proben[^1].Dunkelanteil : double.NaN, + regeln.Mindestanteil, Kapitellage(kapitel)); return false; } var hell = Abspannkante.HellDavor(proben, stelle, regeln) ?? ab; var kante = await Abspannkante.VerfeinernAsync( - hell, stelle, Abspannkante.Schwelle(proben, regeln), - wo => abtaster.HelligkeitAsync(pfad, wo, ct)); + hell, stelle, regeln.Mindestanteil, + wo => abtaster.DunkelanteilAsync(pfad, wo, ct)); log.LogInformation("„{Titel}“: Abspann im Bild ab {Stelle:h\\:mm\\:ss}.", name, TimeSpan.FromSeconds(kante)); diff --git a/server/src/Flaro.Core/Media/Abspannkante.cs b/server/src/Flaro.Core/Media/Abspannkante.cs index 36c32c7..e43ab0b 100644 --- a/server/src/Flaro.Core/Media/Abspannkante.cs +++ b/server/src/Flaro.Core/Media/Abspannkante.cs @@ -1,10 +1,26 @@ namespace Flaro.Core.Media; -/// Ein Messpunkt aus dem Bild: wann, und wie hell. +/// Ein Messpunkt aus dem Bild: wann, und wie schwarz. +/// +/// +/// +/// Der Anteil, nicht der Mittelwert — und das ist der Unterschied +/// zwischen „funktioniert manchmal" und „funktioniert". Ein Mittelwert +/// über das ganze Bild steigt mit jeder Textzeile: derselbe Abspann misst +/// 19, wenn wenig Schrift darin steht, und 37, wenn viel darin steht. Die +/// Schwelle musste zwischen diesen Wert und die Handlung passen, und der +/// Abstand schrumpfte mit jedem Namen. +/// +/// +/// Der Anteil dunkler Pixel tut das nicht: Schrift belegt ein bis zehn +/// Prozent der Fläche, der Rest bleibt schwarz. Gemessen an denselben +/// zwei Abspännen — 84 % und 87 % gegen 0 % in der Handlung. +/// +/// /// /// Die Stelle im Film. -/// Mittlere Leuchtdichte, 0 bis 255. -public readonly record struct Bildprobe(double Sekunde, double Helligkeit); +/// Wie viel Prozent des Bildes dunkel sind, 0 bis 100. +public readonly record struct Bildprobe(double Sekunde, double Dunkelanteil); /// Die Schwellen, nach denen eine dunkle Strecke ein Abspann ist. /// @@ -15,43 +31,29 @@ public readonly record struct Bildprobe(double Sekunde, double Helligkeit); /// public readonly record struct Abspannregeln { - /// Die unterste Schwelle, unter der immer „dunkel" gilt (0–255). - /// - /// - /// Nur noch der Boden, nicht mehr die Schwelle. Sie stand lange - /// allein da, und das war falsch gemessen: das Prüfbild hatte 0,8 - /// Prozent weisse Fläche und kam auf 18. Ein echter Abspann hat mehr - /// Schrift. Nachgemessen an 1080p mit zwei Spalten Namen: bei - /// realistischer Schriftdichte 32, bei einem dichten Abspann 47. Die - /// Fünfunddreissig lag also mitten zwischen den Fällen und schnitt - /// sie auseinander. - /// - /// - /// Und seitdem im vollen Wertebereich. Der Abtaster rechnet - /// jede Datei auf 0–255 um, gleich ob sie acht oder zehn Bit hat und - /// gleich ob sie den Fernsehbereich 16–235 benutzt. Dieselben - /// gemessenen Abspänne liegen darin bei 19 und 36 statt bei 32 und 47; - /// der Boden wandert entsprechend von 35 auf 22. - /// - public double Dunkelschwelle { get; init; } - - /// Wie dunkel eine Strecke gegenüber dem Film sein muss. + /// Wie viel Prozent des Bildes dunkel sein müssen (0–100). /// /// /// - /// Dunkel ist keine absolute Grösse. Entscheidend ist nicht, wie - /// dunkel der Abspann ist, sondern dass er viel dunkler ist als das, - /// was davor lief — und das misst sich am Film selbst. Ein heller Film - /// (Grundton 126) erkennt damit auch einen schriftreichen Abspann bei - /// 47; ein durchweg dunkler Film rutscht nicht in Fehlalarme, weil - /// die Schwelle nie unter fällt. + /// Sechzig, und der Abstand ist nach beiden Seiten gross. + /// Gemessen an erzeugtem 1080p-Material mit zwei Spalten Namen, durch + /// dieselbe ffmpeg-Kette wie im Betrieb: /// + /// + /// Abspann, schriftreich: 84 % + /// Abspann, schlank: 87 % + /// dunkle Nachtszene: 37 % + /// Handlung: 0 % + /// /// - /// 0,55 heisst: knapp unter die Hälfte. Bei 126 ergibt das 69 — weit - /// über beiden gemessenen Abspannwerten und weit unter dem Inhalt. + /// Und damit entfällt die relative Schwelle. Sie war nötig, + /// solange der Mittelwert gemessen wurde: wie hell ein Abspann ist, + /// hing am Film, also musste sich die Schwelle am Film ausrichten. Der + /// Anteil dunkler Pixel eicht sich selbst — ein heller Film und ein + /// dunkler ergeben in der Handlung beide nahezu null. /// /// - public double Anteil { get; init; } + public double Mindestanteil { get; init; } /// Wie viele Sekunden hell eine dunkle Strecke nicht brechen. /// @@ -90,8 +92,7 @@ public readonly record struct Abspannregeln public static Abspannregeln Vorgabe => new() { - Dunkelschwelle = 22, - Anteil = 0.55, + Mindestanteil = 60, Nachsicht = 10, Schlusstafel = 20, Mindestlänge = 45, @@ -129,33 +130,6 @@ public static class Abspannkante /// Messpunkte über das Ende des Films, aufsteigend nach Zeit. Sie dürfen /// ungleichmässig liegen: Schlüsselbilder tun das. /// - /// Ab welcher Helligkeit eine Probe in diesem Film dunkel ist. - /// - /// - /// - /// Das obere Viertel als Grundton. Nicht der Mittelwert: der - /// zöge den Abspann selbst mit hinein. Nicht der Höchstwert: ein - /// einzelner Blitz verschöbe alles. Drei Viertel ist robust, solange - /// der Abspann nicht das ganze Fenster füllt — und das kann er nicht, - /// lässt ihm höchstens gut die - /// Hälfte davon. - /// - /// - /// Öffentlich, weil das Nachmessen dieselbe Schwelle braucht. Zweimal - /// gerechnet wären es zwei Schwellen, und die Kante läge woanders als - /// der Fund. - /// - /// - public static double Schwelle(IReadOnlyList proben, Abspannregeln regeln) - { - if (proben.Count == 0) return regeln.Dunkelschwelle; - - var sortiert = proben.Select(p => p.Helligkeit).OrderBy(h => h).ToArray(); - var grundton = sortiert[Math.Min(sortiert.Length - 1, (int)(sortiert.Length * 0.75))]; - - return Math.Max(regeln.Dunkelschwelle, grundton * regeln.Anteil); - } - public static double? Suchen(IReadOnlyList proben, Abspannregeln regeln) => Suchen(proben, regeln, out _); @@ -175,8 +149,6 @@ public static class Abspannkante return null; } - var schwelle = Schwelle(proben, regeln); - // **Von hinten.** Der Abspann endet am Dateiende; alles, was davor // dunkel war und wieder hell wurde, war eine Schwarzblende. // @@ -187,7 +159,7 @@ public static class Abspannkante // einziges weisses Schlussbild macht den ganzen Abspann unsichtbar. var letzte = proben.Count - 1; var ende = proben[letzte].Sekunde; - while (letzte > 0 && proben[letzte].Helligkeit >= schwelle) + while (letzte > 0 && proben[letzte].Dunkelanteil < regeln.Mindestanteil) { if (ende - proben[letzte].Sekunde >= regeln.Schlusstafel) { @@ -196,7 +168,7 @@ public static class Abspannkante } letzte--; } - if (proben[letzte].Helligkeit >= schwelle) + if (proben[letzte].Dunkelanteil < regeln.Mindestanteil) { grund = "das Ende ist hell"; return null; @@ -208,7 +180,7 @@ public static class Abspannkante var anfang = letzte; for (var i = letzte - 1; i >= 0; i--) { - if (proben[i].Helligkeit < schwelle) { anfang = i; continue; } + if (proben[i].Dunkelanteil >= regeln.Mindestanteil) { anfang = i; continue; } if (proben[anfang].Sekunde - proben[i].Sekunde > regeln.Nachsicht) break; } @@ -235,12 +207,11 @@ public static class Abspannkante public static double? HellDavor( IReadOnlyList proben, double dunkelAb, Abspannregeln regeln) { - var schwelle = Schwelle(proben, regeln); double? letzteHelle = null; foreach (var p in proben) { if (p.Sekunde >= dunkelAb) break; - if (p.Helligkeit >= schwelle) letzteHelle = p.Sekunde; + if (p.Dunkelanteil < regeln.Mindestanteil) letzteHelle = p.Sekunde; } return letzteHelle; } @@ -263,7 +234,7 @@ public static class Abspannkante /// /// public static async Task VerfeinernAsync( - double hell, double dunkel, double schwelle, + double hell, double dunkel, double mindestanteil, Func> messen, double genauigkeit = 4) { if (genauigkeit <= 0) genauigkeit = 1; @@ -272,9 +243,9 @@ public static class Abspannkante { var mitte = hell + (dunkel - hell) / 2; var wert = await messen(mitte); - if (wert is not { } helligkeit) return dunkel; + if (wert is not { } anteil) return dunkel; - if (helligkeit < schwelle) dunkel = mitte; + if (anteil >= mindestanteil) dunkel = mitte; else hell = mitte; } diff --git a/server/src/Flaro.Media/Bildabtaster.cs b/server/src/Flaro.Media/Bildabtaster.cs index b6d989a..ccc6360 100644 --- a/server/src/Flaro.Media/Bildabtaster.cs +++ b/server/src/Flaro.Media/Bildabtaster.cs @@ -34,7 +34,26 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger private const string Breite = "160"; - /// Die Helligkeit an den Schlüsselbildern eines Abschnitts. + /// Unter welchem Lumawert ein einzelnes Pixel dunkel heisst. + /// + /// + /// + /// Nicht der Mittelwert des Bildes, sondern jedes Pixel für sich. + /// blackframe zählt, wie viel Prozent eines Bildes darunter + /// liegen — und das ist ein ganz anderes Merkmal als der Mittelwert: + /// weisse Schrift hebt den Mittelwert, belegt aber nur ein paar + /// Prozent der Fläche. + /// + /// + /// Vierundzwanzig, gemessen über eine Reihe von Schwellen. Darunter + /// (16) fällt der Abspann selbst auf zehn Prozent ab, darüber (32) + /// fängt die Handlung an mitzuzählen. Bei 24: Abspann 84 bis 87 + /// Prozent, Handlung 0, eine dunkle Nachtszene 37. + /// + /// + private const string Dunkelschwelle = "24"; + + /// Der Dunkelanteil an den Schlüsselbildern eines Abschnitts. /// /// /// Nur Schlüsselbilder, und das ist der ganze Trick. Gemessen an @@ -42,7 +61,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger nach. + /// nach. /// public async Task> UeberblickAsync( string pfad, double ab, double dauer, CancellationToken ct = default) @@ -53,7 +72,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILoggerDie mittlere Helligkeit einer einzelnen Sekunde. + /// Der mittlere Dunkelanteil einer einzelnen Sekunde. /// /// /// Alle Bilder, nicht nur Schlüsselbilder: eine Sekunde enthält sonst @@ -61,7 +80,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger - public async Task HelligkeitAsync( + public async Task DunkelanteilAsync( string pfad, double bei, CancellationToken ct = default) { var ausgabe = await LaufenAsync( @@ -70,7 +89,7 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger p.Helligkeit); + return proben.Average(p => p.Dunkelanteil); } private async Task LaufenAsync( @@ -110,19 +129,15 @@ public sealed partial class Bildabtaster(FfmpegOptions optionen, ILogger /// Mit erfundenen Messreihen statt mit Filmen. Geprüft wird das -/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen stammen aus einer -/// Messung an einem erzeugten Film: Handlung 125, Abspann 18. +/// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen sind der Anteil +/// dunkler Pixel je Bild und stammen aus einer Messung an erzeugtem +/// 1080p-Material: Handlung 0 %, Abspann 84 bis 87 %, dunkle Nachtszene +/// 37 %. /// public class AbspannkanteTests { @@ -20,7 +22,7 @@ public class AbspannkanteTests { var proben = new List(); for (var t = von; t <= bis; t += 10) - proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 18 : 125)); + proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0)); return proben; } @@ -50,7 +52,7 @@ public class AbspannkanteTests public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht() { var proben = Reihe(0, 600, abspannAb: null); - for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Helligkeit = 10 }; + for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Dunkelanteil = 92 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null, "sie hält nicht bis zum Ende durch"); @@ -66,7 +68,7 @@ public class AbspannkanteTests { var proben = Reihe(0, 600, abspannAb: 420); for (var i = proben.Count - 5; i < proben.Count; i++) - proben[i] = proben[i] with { Helligkeit = 120 }; + proben[i] = proben[i] with { Dunkelanteil = 0 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } @@ -85,7 +87,7 @@ public class AbspannkanteTests public void Eine_kurze_helle_Tafel_bricht_nichts() { var proben = Reihe(0, 600, abspannAb: 420); - proben[50] = proben[50] with { Helligkeit = 140 }; + proben[50] = proben[50] with { Dunkelanteil = 0 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420)); } @@ -121,10 +123,10 @@ public class AbspannkanteTests const double echteKante = 417.3; var sonden = 0; - var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Dunkelschwelle, stelle => + var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Mindestanteil, stelle => { sonden++; - return Task.FromResult(stelle >= echteKante ? 18 : 125); + return Task.FromResult(stelle >= echteKante ? 85 : 0); }); Assert.Multiple(() => @@ -141,7 +143,7 @@ public class AbspannkanteTests public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund() { var kante = await Abspannkante.VerfeinernAsync( - 410, 420, Regeln.Dunkelschwelle, _ => Task.FromResult(null)); + 410, 420, Regeln.Mindestanteil, _ => Task.FromResult(null)); Assert.That(kante, Is.EqualTo(420)); } @@ -151,10 +153,10 @@ public class AbspannkanteTests public async Task Ein_enger_Bereich_wird_nicht_angefasst() { var sonden = 0; - var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Dunkelschwelle, _ => + var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Mindestanteil, _ => { sonden++; - return Task.FromResult(18); + return Task.FromResult(85); }); Assert.Multiple(() => @@ -165,32 +167,35 @@ public class AbspannkanteTests } } -/// Die Schwelle an gemessenen Abspannwerten, nicht an erfundenen. +/// Der Anteil dunkler Pixel, an gemessenen Werten. /// /// /// -/// Der Fehler, für den diese Prüfungen stehen. Die Erkennung lief -/// gegen eine feste Schwelle von 35, und die stammte aus einem Prüfbild -/// mit 0,8 Prozent weisser Fläche — Helligkeit 18. Ein echter Abspann hat -/// mehr Schrift. +/// Wofür diese Prüfungen stehen. Gemessen wurde lange der +/// Mittelwert der Helligkeit, und daran scheiterte die Erkennung +/// wiederholt: derselbe Abspann misst 19, wenn wenig Schrift darin +/// steht, und 37, wenn viel darin steht — die Schwelle musste zwischen +/// diesen Wert und die Handlung passen, und der Abstand schrumpfte mit +/// jedem Namen. Zweimal nachgezogen, zweimal wieder getroffen. /// /// -/// Nachgemessen an erzeugtem 1080p-Material mit zwei Spalten Namen, +/// Der Anteil dunkler Pixel kennt dieses Problem nicht. Schrift +/// belegt ein bis zehn Prozent der Fläche; der Rest bleibt schwarz. +/// Nachgemessen an erzeugtem 1080p-Material mit zwei Spalten Namen, /// durch dieselbe ffmpeg-Kette wie im Betrieb -/// (scale=160:-2,signalstats): +/// (format=gray,blackframe=amount=0:threshold=24): /// /// -/// Handlung: 126 -/// Abspann, realistische Schriftdichte (1,3 % Weiss): 32 -/// Abspann, dicht gesetzt (9,5 % Weiss): 47 +/// Handlung: 0 % +/// Abspann, schriftreich: 84 % +/// Abspann, schlank: 87 % +/// dunkle Nachtszene: 37 % /// /// -/// Die Fünfunddreissig lag also mitten zwischen den beiden Abspannfällen. -/// Seitdem ist sie nur noch der Boden; gemessen wird gegen den Grundton -/// des Films. +/// Sechzig Prozent trennen das sauber, und zwar nach beiden Seiten. /// /// -public class AbspannSchwellenTests +public class AbspannAnteilTests { private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; @@ -200,7 +205,8 @@ public class AbspannSchwellenTests /// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den /// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe. /// - private static List Fenster(double abspannAb, double imAbspann, double imInhalt = 126) + private static List Fenster( + double abspannAb, double imAbspann, double imInhalt = 0) { var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) @@ -208,62 +214,44 @@ public class AbspannSchwellenTests return proben; } - /// Der Fall, an dem es scheiterte: ein schriftreicher Abspann. + /// Der schriftreiche Abspann — der Fall, an dem es scheiterte. + [Test] + public void Ein_dichter_Abspann_bei_vierundachtzig_Prozent_wird_gefunden() + { + var proben = Fenster(abspannAb: 2340, imAbspann: 84); + + Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340.0).Within(5)); + } + + [Test] + public void Ein_schlanker_Abspann_bei_siebenundachtzig_Prozent_auch() + { + var proben = Fenster(abspannAb: 2340, imAbspann: 87); + + Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340.0).Within(5)); + } + + /// **Eine dunkle Nachtszene ist kein Abspann.** /// /// - /// Mit der alten festen Schwelle von 35 ist diese Prüfung rot: 47 gilt - /// dort als hell, und die Folge bekam keine Marke. - /// - [Test] - public void Ein_dichter_Abspann_bei_siebenundvierzig_wird_gefunden() - { - var proben = Fenster(abspannAb: 2340, imAbspann: 47); - - Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5)); - } - - [Test] - public void Ein_schlanker_Abspann_bei_zweiunddreissig_wird_gefunden() - { - var proben = Fenster(abspannAb: 2340, imAbspann: 32); - - Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5)); - } - - /// Und die Schwelle liegt zwischen beidem, nicht daneben. - [Test] - public void Die_Schwelle_liegt_zwischen_Handlung_und_Abspann() - { - var schwelle = Abspannkante.Schwelle(Fenster(2340, 47), Regeln); - - Assert.Multiple(() => - { - Assert.That(schwelle, Is.GreaterThan(47), "ein dichter Abspann muss darunter liegen"); - Assert.That(schwelle, Is.LessThan(126), "die Handlung muss darüber liegen"); - }); - } - - /// **Ein dunkler Film wird dadurch nicht zum Dauerabspann.** - /// - /// - /// Eine relative Schwelle kann nach unten entgleisen: bei einem Film, - /// der durchweg dämmrig ist, wäre „etwas mehr als halb so hell" bald - /// jede Nachtszene. Deshalb bleibt Dunkelschwelle als Boden — - /// hier sorgt sie dafür, dass eine Schlussszene bei 40 nichts auslöst. + /// Mit dem Mittelwert war das die heikelste Grenze: eine Nachtszene + /// misst 28, ein schlanker Abspann 19 — zwischen diesen neun Punkten + /// musste die Schwelle liegen. Nach Pixelanteilen sind es 37 gegen 87, + /// und die Frage stellt sich nicht mehr. /// [Test] public void Eine_duestere_Schlussszene_ist_kein_Abspann() { - var proben = Fenster(abspannAb: 2340, imAbspann: 40, imInhalt: 55); + var proben = Fenster(abspannAb: 2340, imAbspann: 37, imInhalt: 12); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } - /// Helle Handlung bis zum Ende bleibt Handlung. + /// Und ein heller Film bis zum Ende bleibt Handlung. [Test] public void Ohne_dunkles_Ende_kein_Fund() { - var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 0); + var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 99); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } @@ -287,8 +275,8 @@ public class AbspannSchlusstafelTests var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) { - var hell = t >= tafelAb ? 210 : t < abspannAb ? 126 : 32; - proben.Add(new Bildprobe(t, hell)); + var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85; + proben.Add(new Bildprobe(t, dunkel)); } return proben; } @@ -348,7 +336,7 @@ public class AbspannNachsichtTests for (var t = 1500.0; t <= 2700; t += takt) { var imLogo = t >= 2500 && t < 2505; - proben.Add(new Bildprobe(t, t < 2340 ? 126 : imLogo ? 180 : 32)); + proben.Add(new Bildprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85)); } return proben; } @@ -383,7 +371,7 @@ public class AbspannNachsichtTests { // Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell. var hell = t < 2340 || (t >= 2400 && t < 2460); - proben.Add(new Bildprobe(t, hell ? 126 : 32)); + proben.Add(new Bildprobe(t, hell ? 0 : 85)); } // Gefunden wird nur das Stück hinter der hellen Minute. @@ -395,14 +383,14 @@ public class AbspannNachsichtTests public void Der_Grund_wird_benannt() { var hell = new List(); - for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 126)); + for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 0)); Abspannkante.Suchen(hell, Regeln, out var grund); Assert.That(grund, Does.Contain("hell")); var kurz = new List(); for (var t = 1500.0; t <= 2700; t += 5) - kurz.Add(new Bildprobe(t, t < 2680 ? 126 : 32)); + kurz.Add(new Bildprobe(t, t < 2680 ? 0 : 85)); Abspannkante.Suchen(kurz, Regeln, out var zuKurz); Assert.That(zuKurz, Does.Contain("kurz")); diff --git a/server/tests/Flaro.Media.Tests/BildabtasterTests.cs b/server/tests/Flaro.Media.Tests/BildabtasterTests.cs index 8a04d5e..04da549 100644 --- a/server/tests/Flaro.Media.Tests/BildabtasterTests.cs +++ b/server/tests/Flaro.Media.Tests/BildabtasterTests.cs @@ -13,9 +13,9 @@ public class BildabtasterLesenTests { var ausgabe = """ frame:0 pts:30720 pts_time:2.5 - lavfi.signalstats.YAVG=125.065 + lavfi.blackframe.pblack=3 frame:1 pts:61440 pts_time:5 - lavfi.signalstats.YAVG=18.4 + lavfi.blackframe.pblack=84 """; @@ -23,8 +23,8 @@ public class BildabtasterLesenTests Assert.That(proben, Is.EqualTo(new[] { - new Bildprobe(62.5, 125.065), - new Bildprobe(65, 18.4), + new Bildprobe(62.5, 3), + new Bildprobe(65, 84), })); } @@ -38,7 +38,7 @@ public class BildabtasterLesenTests [Test] public void Ein_Wert_ohne_Zeit_faellt_heraus() { - Assert.That(Bildabtaster.Lesen("lavfi.signalstats.YAVG=18.4\n", 0), Is.Empty); + Assert.That(Bildabtaster.Lesen("lavfi.blackframe.pblack=84\n", 0), Is.Empty); } [Test] @@ -133,8 +133,8 @@ public class BildabtasterLaufTests var hell = Abspannkante.HellDavor(proben, grob!.Value, Abspannregeln.Vorgabe) ?? 0; var abtaster = Abtaster(); var kante = await Abspannkante.VerfeinernAsync( - hell, grob.Value, Abspannkante.Schwelle(proben, Abspannregeln.Vorgabe), - stelle => abtaster.HelligkeitAsync(_datei, stelle)); + hell, grob.Value, Abspannregeln.Vorgabe.Mindestanteil, + stelle => abtaster.DunkelanteilAsync(_datei, stelle)); Assert.That(kante, Is.EqualTo(90).Within(4), "erzeugt wurde der Übergang bei genau 90 Sekunden"); @@ -153,39 +153,36 @@ public class BildabtasterLaufTests /// /// /// - /// Hier stand der Fehler, und hier wurde er sichtbar. Diese - /// Prüfung verlangte früher, dass der Abspann unter der festen - /// Schwelle von 35 liegt — mit dem schmalen Balken von damals lag er - /// bei 18, und alles schien zu stimmen. Mit realistischer - /// Schriftdichte misst dieselbe Stelle 40. Die feste Schwelle - /// hätte diesen Abspann also für Handlung gehalten, und genau das - /// geschah im Bestand. + /// Hier wurde der alte Fehler sichtbar. Gemessen wurde lange + /// der Mittelwert der Helligkeit; diese Prüfung verlangte, dass der + /// Abspann unter einer festen Schwelle von 35 liegt. Mit dem schmalen + /// Balken von damals lag er bei 18, und alles schien zu stimmen — mit + /// realistischer Schriftdichte bei 40. Die feste Schwelle hielt + /// diesen Abspann also für Handlung, und genau das geschah im + /// Bestand. /// /// - /// Gemessen wird deshalb gegen die Schwelle, die der Film selbst - /// vorgibt — und die liegt hier zwischen 40 und der Handlung. + /// Nach Pixelanteilen stellt sich die Frage nicht mehr: gemessen + /// werden hier rund 90 Prozent im Abspann gegen null in der Handlung. /// /// [Test] - public async Task Eine_Sekunde_im_Abspann_liegt_unter_der_Schwelle() + public async Task Eine_Sekunde_im_Abspann_ist_fast_ganz_dunkel() { - var proben = await Abtaster().UeberblickAsync(_datei, ab: 30, dauer: 120); - var schwelle = Abspannkante.Schwelle(proben, Abspannregeln.Vorgabe); + var noetig = Abspannregeln.Vorgabe.Mindestanteil; - var dunkel = await Abtaster().HelligkeitAsync(_datei, bei: 120); - var hell = await Abtaster().HelligkeitAsync(_datei, bei: 40); + var dunkel = await Abtaster().DunkelanteilAsync(_datei, bei: 120); + var hell = await Abtaster().DunkelanteilAsync(_datei, bei: 40); Assert.Multiple(() => { - Assert.That(dunkel, Is.Not.Null.And.LessThan(schwelle)); - Assert.That(hell, Is.Not.Null.And.GreaterThan(schwelle)); - Assert.That(dunkel, Is.GreaterThan(Abspannregeln.Vorgabe.Dunkelschwelle), - "dieser Abspann ist heller als der alte feste Wert — darum ging es"); + Assert.That(dunkel, Is.Not.Null.And.GreaterThan(noetig)); + Assert.That(hell, Is.Not.Null.And.LessThan(noetig)); }); } } -/// Dieselbe Helligkeit, gleich welche Bittiefe die Datei hat. +/// Derselbe Dunkelanteil, gleich welche Bittiefe die Datei hat. /// /// /// @@ -251,8 +248,8 @@ public class BildabtasterBittiefeTests var abtaster = new Bildabtaster( FfmpegOptions.FromEnvironment(), NullLogger.Instance); - var acht = await abtaster.HelligkeitAsync(Erzeugen("yuv420p"), bei: 2); - var zehn = await abtaster.HelligkeitAsync(Erzeugen("yuv420p10le"), bei: 2); + var acht = await abtaster.DunkelanteilAsync(Erzeugen("yuv420p"), bei: 2); + var zehn = await abtaster.DunkelanteilAsync(Erzeugen("yuv420p10le"), bei: 2); Assert.Multiple(() => {