6 Ağustos 2022 Cumartesi

Lineer Cebir ve Film Altyazılarıyla Arasındaki İlişki

Bugün bile yararlandığım doğrusal cebir konularını ve aralarındaki ilişkileri kavramamı sağlayan Prof. Dr. Metin DEMİRALP'in anısına...

Merhaba. Elimdeki bazı videolara altyazı senkronize ediyordum ki bu işlemin aslında bir lineer cebir problemi olduğunu farkettim. Üstelik oldukça basit, iki bilinmeyenli doğrusal denklemler problemi. Yazıda önce problemi tanımlayıp sonra da nasıl çözdüğümü ele alacağım.


Önce Bazı Tanım ve Önbilgiler
Altyazı senkronlarken bilinmesi gereken en önemli veri, videonun saniyedeki kare sayısıdır (frame rate). Her video aslında ard arda gösterilen karelerden oluşur. Bu değere FPS (frame per second) denir. FPS, videoyu yapan kişi tarafından ön-tanımlı değerler arasından seçilir. Örn. 23.976, 24, 25 gibi. FPS değerleri altyazıyla birlikte belirtilir ve indirirken uygun FPS'de olanı seçmek gerekir.

Örnek planetdp.org'dan alınmıştır.


Altyazı dosyaları için onlarca farklı format var ama ben ikisi üzerinde duracağım. Birincisi .sub uzantılı MicroDVD formatı. Bu dosyalar oldukça basit: İki braket içinde metnin göründüğü kare numarası, hemen yanında yine iki braket içinde metnin kaybolduğu kare numarası ve altyazı metni. Örn:

{0}{25}Merhaba

Bu, 25 FPS'lik bir videonun ilk saniyesinde "Merhaba" metnini gösterecektir. Bu dosyalar özellikle 2000'lerin başlarında popülerdi. Fark edilecektir ki, videonun FPS'i değiştiğinde bu dosyaların yeniden düzenlenmesi gerekir. 24 FPS'lik altyazı 25 FPS'lik videoda kullanılırsa, altyazı her saniye bir kare ileri gider ve 25 saniye sonra arada 1 saniye fark olur. Sinir bir durum!

Diğer altyazı, .srt uzantılı SubRip formatıdır. Bu format da oldukça basittir:
1- İlk satırda bir sıra numarası
2- HH:MM:SS,TTT biçiminde kodlanmış ve birbirinden " --> " ile ayrılmış iki zaman damgası. İlki metnin göründüğü, ikincisi kaybolduğu an.
3- Altyazı metni
4- Boş bir satır. Metnin sonunu belirler.

Örn:
1
00:00:00,000 --> 00:00:01,000
Merhaba


ilk örneğin aynısıdır. Bu format FPS'ye bağlı olmadığından, farklı FPS'lerdeki aynı videolarla kullanılabilir. Bu format da 2000'lerden beri piyasada olsa da, 2010'larda yaygınlıkta .sub formatına üstün geldi. Anlaşılmaz frame numaraları bulunmadığından okunması daha kolaydır.

Elbette altyazı formatları bunlarla sınırlı değil. Örn. özellikle anime'lerle kullanılan Aegisub formatı, bu ikisinden çok daha gelişmiş. Bildiğim kadarıyla altyazıları vektörel olarak saklıyor ve yazılar ekranın herhangi bir yerine, istenen font, büyüklük ve renkte basılabiliyor. Ama düzenlemesi metin tabanlı formatlardan zor olduğundan bu yazının kapsamını aşıyor.

Her ne kadar .srt formatı FPS'den bağımsızsa da, gördüğüm kadarıyla internette yine de bir şekilde senkronizasyonu kayan altyazılar var. Bunlar muhtemelen .sub'dan .srt'ye dönüştürülen altyazılar.


Problemin Tanımı
Tanımları verdiğime göre problemi ifade edebilirim. Genel olarak altyazılarda iki problem olabilir: (a) Altyazı ileri veya geridir. Örn. Altyazının hazırlandığı videonun başında bir giriş vardır ve bir başkası videoyu düzenlerken bunu keser. Dolayısıyla altyazı olması gerekenden geç görünür (veya erken) ama altyazıyla ses arasındaki zaman farkı sabittir. Veya (b) altyazı bir noktada sesle eşzamanlıdır ama hızlı gider veya yavaş kalır ve bir süre sonra fark artar. Yukarıda yazdığım gibi bu durum yanlış FPS'li .sub dosyalarda veya bunlardan çevrilen .srt dosyalarda görülür. Ve daha  sinir bozucu olan, iki sorunun birlikte olmasıdır. Yani altyazı hiçbir noktada sesle senkron olmaz ve fark hep değişir.

Problemin daha iyi anlaşılması için iki durumu da görselleştireyim. Hesaplama kolaylığı için videoyı 25 FPS alacağım. X ekseni zaman ve Y ekseni frame sayacı olsun. Frame sayacı her saniyede 25 artarak ilerler ve uygun altyazı eğrisi video çizgisiyle çakışık olmalıdır. Altyazının ileri veya geri olması durumunda, altyazıya ait çizgi videoya paralel ve altında veya üstündedir.


Altyazının başta tutarlı olup sonradan farkın açıldığı durumdaysa FPS uyumsuzdur. Başta tutarlı olduğuna göre çizgiler de başta kesişir ama zamanla aralarında fark oluşur çünkü altyazının eğimi videonun eğiminden farklıdır.


Ve her iki sorunun birden ortaya çıktığı durumlar:


Burada eğimle bağlantı kurduktan sonra, artık işlemleri matematikle ifade edebilirim.


İki Bilinmeyenli Denklemler ve Denklem Sistemleri
m ve c sabitler olmak üzere, y = mx + c şeklindeki bir denkleme, iki bilinmeyenli denklem denir. Somut bir örnekle, m = 3 ve c = 4 için, y = 3x + 4. Bu denklemde x'e vereceğim her değer için bir y elde ederim, mesela x = 1 ise y = 7'dir. Eğer bütün x'lere karşılık gelen y'leri bulup bunları düzlemde noktalar olarak işaretlersem, bir doğru ortaya çıkar. Doğru, dikey ekseni 4'te keser (x = 0 durumu) ve sağa doğru gittiği yani x'in arttığı her bir birim için yukarı yönde 3 birim gider. Dolayısıyla m'ye doğrunun eğim parametresi (kısaca eğimi) denebilir. m azaldıkça eğim azalır, m = 0 olursa doğru dümdüz olur ve m negatif olursa eğim aşağı doğru gider. c parametresiyle oynayarak da doğruyu aşağı veya yukarı sürüklerim.

Yukarıda videoyu bir çizgi olarak ifade etmiştim. Videonun eğimi onun FPS'i ve c = 0 olacak, çünkü videonun sesi videoyla birlikte başlıyor. Örneğin y = 25x. Grafikteki kalın mavi çizgilerin hepsi bu şekilde. Altyazı videoya uyumluysa onun da denklemi aynı olmalı. Değilse, amacım altyazının denklemini videoya uyumlu hale getirmek olmalı. Güzeel.

Altyazı ileride veya gerideyse, doğrular birbirine paraleldi. Bunu grafikte gördük. Doğrular paralel demek eğimleri aynı demek ve altyazının ileride/geride olması aslında sıfırıncı karede (yani x = 0) altyazı doğrusunun dikey ekseni sıfırda kesmemesi durumu. Yani altyazı denklemi y = 25x + c'dir. Yapmam gereken 'c'yi bulup, bunu altyazıdaki zaman damgalarından çıkarmak (veya toplamak).

Altyazıyla video arasındaki farkın sürekli açılması, altyazı doğrusunun eğiminin videonunkinden farklı olması anlamına geliyordu. Burada eğimi, yani 'm' değerini değiştirmeliyim. 'm' bir çarpan olduğuna göre, altyazının zaman damgalarıyla çarpıldığında doğru zamanı veren değeri bulmam gerek. Örn. altyazı 24 ve video 25 FPS'yse, altyazıyı uyarlamak için tüm zaman damgalarını 25/24'le çarparım. Bu işlemi yapar yapmaz altyazı senkron oluyorsa, altyazı denkleminde c = 0 olduğunu yani altyazı denkleminin y = 24x olduğunu anlarım. c sıfırdan farklıysa, eğimleri eşledikten sonra c'ler farkını toplamam da gerekir.

İki bilinmeyenli denklemler bu kadar. Peki denklem sistemleri? Denklem sistemlerinde birden fazla denklem, yani birden fazla doğru bulunur. Örn.


Bunları çözmek kolaydır. y'ler eşit olduğuna göre iki denklemi birbirine eşitleyip, denklemin solundaki dokuzu yoketmek için her iki tarafa dokuz eklerim. (Sağ taraftaki 3'ü de yok edebilirdim, o zaman solda -6 kalırdı ama negatiflerle işlem yapmak istemiyorum.) Sonra her iki taraftan x çıkarırsam x = 6 bulurum. // karakterinden sonra her iki tarafa uyguladığım işlem görülebilir.


x = 6'yı sistemin ikinci denkleminde yerine koyarsam, y = 6 - 3 = 3 bulunur. x = 6, y = 3 bir nokta belirtir ve bu, iki doğrunun kesiştiği noktadır. Bunlar doğru olduklarına göre yalnız bir noktada kesişirler. Bunu yandaki grafikte görebiliriz.

Doğruların eğimleri farklı olduğundan biri daha az artacak, öbürü daha fazla artıp diğerini yakalayacaktır. Ama, doğruların eğimleri farklıysa. Doğrular paralel olabilir, yani asla kesişmezler. Buna örnek, altyazının sesten hep geride olduğu durumdur.


Son olarak doğrular üstüste (çakışık) olabilir. Bu durumda tüm x değerleri denklemi sağlar ama bunun bir çözüm olup olmadığı probleme bağlıdır.



Matris Çarpımı ve Sistemlerin Matris Çözümleri
Matris çarpımını detaylı olarak BLAS ve LAPACK yazısında ele almıştım. Özetle, ilk matrisin birinci satırıyla ikinci matrisin birinci sütununu tek tek çarpıp değerleri topla ve sonuç matrisinin ilk elemanına yaz. Birinci satırla ikinci sütunu çarpıp topla, sonucun ikinci (ilk satır ikinci eleman) elemanına yaz... p. satır * q. sütun = p x q elemanı. İşlem, bir örnekle daha iyi anlaşılacak ama önce denklem sistemini biraz düzenleyeceğim:


Artık bunları kolayca matris olarak yazabilirim:


Birinci satır elemanları 2 ve -1, diğer matrisin birinci ve tek sütunu ile çarpılıp toplandığında 2*x + (-1)y = 9 ve ikinci satırla işlem yapıldığında x + (-1)y = 3. Yani, yukarıdaki matris çarpımı denklem sistemini karşılıyor. Üstelik hem derli toplu, hem daha şık. 2x2'lik matris denklemlerin katsayılarından oluştuğundan, buna katsayılar matrisi deniyor. Eşittirin sağındaki matris, 2x1 olduğundan aslında bir vektör ve buna sağ taraf vektörü deniyor (RHS Vector).

Denklem sisteminin çözümünün olmaması durumu, matrisin satır veya sütunlarının birbiriyle orantılı olması durumuna denk geliyor. Lineer cebirde bunun anlamı, matrisin determinantının sıfır olması veya özdeğerlerinden en az birinin sıfır olması anlamına geliyor ki, bu konulara girersem özden uzaklaşırım.

Şimdi elimde denklemini bildiğim bir video ve denklemini bilmediğim ve videonunkine uydurmak istediğim bir altyazı var. Altyazının denklemine y = m1x + c1 ve videonunkine y = m2x + c2 diyelim. m1'i öyle bir sayıyla (m' diyelim) çarpmalıyım ki m2'yi versin. Yani m1m' = m2 ve c1m' + c' = c2. Bu kısım biraz karışık ama önemli değil, çünkü daha basit bir yolla tekrar açıklayacağım:

Not: "Altyazı" kelimesi filme ait tüm yazıları ifade ediyor. Belli bir anda, ekranda görünen tekil yazıyı ayırdetmek için yazının geri kalanında bunu "metin" olarak ifade edeceğim. Altyazı ifadesi geometrik olarak y = mx + c doğrusuna karşılık gelirken, metin ifadesi belli bir x sayısı için y'nin değeri olacak.

Altyazıyı videoya uyarlamak için, öncelikle videodan, zamanını tam olarak bildiğim iki metin gerek. Bunu dinleyerek bulabilirim veya başka bir kaynaktan farklı dilde ama tam senkron bir altyazı bulabilirsem bu daha iyi. Elbette senkron olup olmadığını anlamak için, az çok anlayabileceğim bir dil olmalı (veya google translate'e bakarım) veya aradan özel adları seçip yaklaşık olarak anlayabilirim.

Peki neden iki? Diyelim elimde, videodan bir zaman damgası ve bir de uyarlamak istediğim, zamanı yanlış olan metin var. Somut örnekle; videonun 33. dakikasında "Hello" duydum ve "Merhaba" altyazıda 35. dakikada. İki dakika mı çıkarmalıyım, yoksa 35/33'le mi çarpmalıyım? Bilemem çünkü, belki ses ve metin arasındaki fark 60. dakikada 4 dakika oluyor (yani eğim farklı). m ve c'yi kesin olarak belirlemek için iki nokta, yani iki metin gerek. Burada Öklid geometrisindeki, "iki noktadan bir ve yalnız bir doğru geçer" aksiyomu önüme çıkıyor.

Öyleyse elimde iki tane metin olsun. Örn. 33. dakikada "Hello" ve 48. dakikada "Good bye". Elimdeki altyazıda 35. dakikada "Merhaba" ve 51. dakikada "Güle güle" var. Ve elimdeki senkron olmayan altyazıyı öyle bir fonksiyona vereceğim ki, çıktı olarak senkron altyazıyı üretecek. Bu fonksiyonun genel ifadesine y = mx + c demiştim. x'ler senkron olmayan altyazının zaman damgaları (girdi) ve y'ler senkronlanmış zaman damgaları (çıktı). O halde:


Bunları taraf tarafa çarpıp çıkararak m ve c'nin değerlerini bulabilirim ama bunu estetik şekilde yapacağım:


c'nin katsayısı 1 olduğundan, ikinci sütun her zaman 1. Bu noktada çözümün nasıl yapılacağını boşverip sonucu yorumlayacağım. Çözümü wolfram alpha'ya yaptırdığımda c = 3/16 ve m = 15/16 çıktı. Yani elimdeki altyazı ile videonun FPS'si arasındaki oran 15/16 (Örn. altyazı 24 ve video 22.5 FPS) ve bu oran sağlandığında 3/16 dakikalık bir kayma var. Bunu kaymayı toplamam gerekiyor.

Peki fonksiyon ne derece isabetli? Altyazı videonun her saniyesinde şaşmaz bir doğrulukla senkronize oldu mu, yoksa hata var mı? Elbette altyazının kendinden hatalı olması durumunu bir kenara bırakıyorum (örn. bazı metinlerin eksik olması). Bulduğum bu fonksiyon ne kadar güvenilir?


Robustness
Fonksiyonun güvenilirliği, altyazıyı videonun geneline hangi doğrulukla senkronize edebildiğinin bir ölçüsü. Matematik yazınında "robustness" olarak ifade edilen kavram, bir sistemin hataları ne kadar tolere edebildiği anlamına geliyor. Kimi Türkçe kaynaklarda "gürbüzlük" olarak geçiyor ama çeviri yaygın olmadığından robustness'a sadık kalacağım.

Dinleyerek belirlediğim zaman damgaları, benim ne kadar iyi dinleyip ne kadar hızlı tepki verdiğime bağlı olduğundan, hataya oldukça açık. Üstelik elimdeki örnek altyazının da ne kadar hatalı olduğunu bilmiyorum. Belki de benim tesadüfen seçtiğim bir metin, aslında altyazıda kaymıştı. Bu kaymanın 0.2 saniye bile olması ciddi kaymalara yol açabilir. Bunu grafikte incelersek:


Örn. 4. dakikadaki metin doğru ama beşinci dakikada dinlerken 0.2 sn (24 FPS'lik video için 4.8 kare) bir hata yaptım. Bu, bir saatlik bir videonun sonunda 12sn'ye kadar kaymaya yol açar. Yukarıdaki grafiğin alt bölümünde hatanın videonun tümüne etkisi görülüyor. Fakat birinci noktayı ikinciden uzakta alırsam, örneğin birinci metin 4. dakikada, ikinci metin 60. dakikada ve hata 0.2 sn:


Uçlardaki kayma görülmeyecek derecede küçüldü ve ilk grafiğe göre çok daha iyi. 60. dakikadaki 0.2 sn 100. dakikada 0.343 saniye oldu. Eğer en sondaki metinle senkronize etseydim, çok daha az olacaktı. O halde en az hata için başlardan ve sonlardan iki metin almak en mantıklısıdır.

Şimdi lineer cebire biraz geri dönüyorum. Bir lineer sistemin robust'lığı, sistemdeki matrisin determinantının mutlak değerce ne kadar büyük olduğuyla ilişkilidir (öz değerlerin aralarındaki farkların büyük olması... Neyse, sakin). Determinant tanımına ve hesaplamasına burada girmeye gerek yok. Yukarıdaki matrisin özel formundan dolayı determinant, iki zaman damgasının farkına eşit. Dolayısıyla yukarıda ifade ettiğim, "hatayı azaltmak için zaman farkını büyütmenin" matematiksel arkaplanı da sağlanmış oldu.


Python Kodu
Altyazı senkronlamak için, pratikte doğrusal denklem sistemlerini bilmek gerekmemesi, işin en güzel yanı. Python numpy kütüphanesindeki linalg.solve fonksiyonuna katsayılar matrisi ve sağ taraf vektörü verildiğinde sonucu döndürüyor. Aşağıdaki kod altyazıda olan ve olması gereken zaman damgalarını okuyup bunları time2num() ile saniyeye çeviriyor ve bu değerlerden denklem sistemini oluşturup m ve c'yi hesaplıyor.

#!/usr/bin/python3

# input should be like:
# 00:03:29,632     00:02:13,532
# 00:43:56,890     00:44:00,486
# -------------    --------------
# is in subtitle   needs to be
#
# Formula:
# m * 00:03:29,632 + c*1 = 00:02:13,532
# m * 00:43:56,890 + c*1 = 00:44:00.486
#
# [ 209.632    1 ]   [ m ] = [ 133.532 ]
# [ 2636.890   1 ] * [ c ] = [ 2640.486 ]
# Calculate m, c params with this script then apply
# this transformation with subtitle.py script.

import numpy
import re

def time2num(time):
    timei = re.sub(r",", ".", time);
    (h, m, s) = timei.split(':');
    return round(int(h) * 3600 + int(m) * 60 + float(s), 3);

if __name__ == '__main__':
    print("is in subtitle    needs to be\n--------------    -------------");

    (OL1, OG1) = input().split();
    (OL2, OG2) = input().split();

    A = numpy.array( [[time2num(OL1), 1.0], [time2num(OL2), 1.0]], float );
    b = numpy.array( [time2num(OG1), time2num(OG2)], float);

    x = numpy.linalg.solve(A, b)

    print(x);

Çıkan değerler aşağıdaki koda girdi olarak verilecek ama önce altyazı dosyası parametre olarak verilmeli. Kod, dosyadaki tüm zaman damgalarını okuyor. Burada re.search() ile regex araması yapılıyor. "-->" string'i bütün zaman damgası satırlarında var ve metinde bulunması pek olası değil. Bu satırlardan metnin görünme ve kaybolma zamanları alınıyor. time2num() ile saniyeye çevriliyor, formülle dönüştürülüp dosyanın bir kopyasına yazılıyor.

#!/usr/bin/python3

import argparse
import os
import re

# Using linsolver.py script, calculate first, what the m and c
# parameters are. Then enter them as input to this script.
# These will be used at lines 41-42

def time2num(time):
    timei = re.sub(r",", ".", time);
    (h, m, s) = timei.split(':');
    return round(int(h) * 3600 + int(m) * 60 + float(s), 3);

def num2time(numtime):
    s = round(numtime % 60, 3);
    m = int((numtime / 60) % 60);
    h = int(numtime / 3600);
    return re.sub(r"\.", ",", ("%02d:%02d:%06.3f" % (h, m, s)))

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Modify subtitle timings');
    parser.add_argument('arg_filename', type=str,
                        help='Subtitle file name to open');
    args = parser.parse_args();

    hFileIn  = open(args.arg_filename, "r", encoding="ISO-8859-9");
    hFileOut = open(''.join( os.path.splitext(args.arg_filename)[0] + ".mod.srt" ), "w", newline = "\r\n");

    print("Input m and c separated by space");
    (m, c) = input().split();

    for line in hFileIn.readlines():
        if(re.search(r"-->", line)):
            substart = time2num(line.split()[0]);
            subfinal = time2num(line.split()[2]);

            #####  Calculations here  #####
            substart = substart * m + c;
            subfinal = subfinal * m + c;

            hFileOut.write(num2time(substart) + ' --> ' + num2time(subfinal) + '\n');
        else:
            hFileOut.write(line);

    hFileIn.close();
    hFileOut.close();


Uzun lafın kısası, lineer cebir için bile, bu bilgiler gerçek hayatta ne işimize yarayacak demeden öğrenmek faydalı olabilmektedir.

Not: Bu yazı aslında Nisan 2022'de yayınlanmaya hazırdı ama önce ev taşıma, sonra mobil operatörün yaşattığı problemden dolayı MFA kodunu alamamam ve yaz tatili derken yazı tam dört ay gecikti.