Nicht wie hell das Bild ist, sondern wie viel davon schwarz
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
121ee62be9
commit
791c00dacc
@@ -6,8 +6,10 @@ namespace Flaro.Core.Tests;
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Mit erfundenen Messreihen statt mit Filmen.</b> 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 %.
|
||||
/// </remarks>
|
||||
public class AbspannkanteTests
|
||||
{
|
||||
@@ -20,7 +22,7 @@ public class AbspannkanteTests
|
||||
{
|
||||
var proben = new List<Bildprobe>();
|
||||
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<double?>(stelle >= echteKante ? 18 : 125);
|
||||
return Task.FromResult<double?>(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<double?>(null));
|
||||
410, 420, Regeln.Mindestanteil, _ => Task.FromResult<double?>(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<double?>(18);
|
||||
return Task.FromResult<double?>(85);
|
||||
});
|
||||
|
||||
Assert.Multiple(() =>
|
||||
@@ -165,32 +167,35 @@ public class AbspannkanteTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Die Schwelle an gemessenen Abspannwerten, nicht an erfundenen.</summary>
|
||||
/// <summary>Der Anteil dunkler Pixel, an gemessenen Werten.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Der Fehler, für den diese Prüfungen stehen.</b> 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.
|
||||
/// <b>Wofür diese Prüfungen stehen.</b> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Nachgemessen</b> an erzeugtem 1080p-Material mit zwei Spalten Namen,
|
||||
/// <b>Der Anteil dunkler Pixel kennt dieses Problem nicht.</b> 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
|
||||
/// (<c>scale=160:-2,signalstats</c>):
|
||||
/// (<c>format=gray,blackframe=amount=0:threshold=24</c>):
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <item><description>Handlung: <b>126</b></description></item>
|
||||
/// <item><description>Abspann, realistische Schriftdichte (1,3 % Weiss): <b>32</b></description></item>
|
||||
/// <item><description>Abspann, dicht gesetzt (9,5 % Weiss): <b>47</b></description></item>
|
||||
/// <item><description>Handlung: <b>0 %</b></description></item>
|
||||
/// <item><description>Abspann, schriftreich: <b>84 %</b></description></item>
|
||||
/// <item><description>Abspann, schlank: <b>87 %</b></description></item>
|
||||
/// <item><description>dunkle Nachtszene: <b>37 %</b></description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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.
|
||||
/// </remarks>
|
||||
private static List<Bildprobe> Fenster(double abspannAb, double imAbspann, double imInhalt = 126)
|
||||
private static List<Bildprobe> Fenster(
|
||||
double abspannAb, double imAbspann, double imInhalt = 0)
|
||||
{
|
||||
var proben = new List<Bildprobe>();
|
||||
for (var t = 1500.0; t <= 2700; t += 5)
|
||||
@@ -208,62 +214,44 @@ public class AbspannSchwellenTests
|
||||
return proben;
|
||||
}
|
||||
|
||||
/// <summary>Der Fall, an dem es scheiterte: ein schriftreicher Abspann.</summary>
|
||||
/// <summary>Der schriftreiche Abspann — der Fall, an dem es scheiterte.</summary>
|
||||
[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));
|
||||
}
|
||||
|
||||
/// <summary>**Eine dunkle Nachtszene ist kein Abspann.**</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Mit der alten festen Schwelle von 35 ist diese Prüfung rot: 47 gilt
|
||||
/// dort als hell, und die Folge bekam keine Marke.
|
||||
/// </remarks>
|
||||
[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));
|
||||
}
|
||||
|
||||
/// <summary>Und die Schwelle liegt zwischen beidem, nicht daneben.</summary>
|
||||
[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");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>**Ein dunkler Film wird dadurch nicht zum Dauerabspann.**</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// 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 <c>Dunkelschwelle</c> 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.
|
||||
/// </remarks>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Helle Handlung bis zum Ende bleibt Handlung.</summary>
|
||||
/// <summary>Und ein heller Film bis zum Ende bleibt Handlung.</summary>
|
||||
[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<Bildprobe>();
|
||||
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<Bildprobe>();
|
||||
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<Bildprobe>();
|
||||
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"));
|
||||
|
||||
Reference in New Issue
Block a user