İçeriğe geç

Teknik Yazılar

Yazılım ve Araçlar··6 dk okuma

RTSP Video Akışını Python'da İşlemek: Gecikme ve Buffer Sorunları

Tespit modeli doğru çalışıyor ama kutu hep hedefin arkasında kalıyorsa sorun modelde değil, okumadığınız karelerde birikiyor.

Saha testinde ilk kez fark ettik: dron hedefin üzerinden geçtikten epey sonra tespit kutusu ekranda belirdi. Model çalışıyordu, kutu doğru yerdeydi — ama o “doğru yer” birkaç saniye önceki dünyaydı. Uçuş sürerken bu fark büyüyordu; ilk saniyelerde göze batmıyor, birkaç dakika sonra görüntü tamamen geçmişte kalıyordu.

Bu, tespit modelinin değil, video okuma katmanının sorunu. RTSP akışı sabit hızda kare üretir; sizin döngünüz ise model çıkarımı yaptığı için daha yavaş dönebilir. Aradaki fark bir yerde birikmek zorunda ve o yer, siz istemeseniz de var olan bir kuyruktur. Kuyruk boşalmadığı sürece cap.read() size en yeni kareyi değil, sıradaki en eski kareyi verir.

Bu yazıda SUAS 2026 aracımızda kullandığımız çözümü anlatıyoruz: SIYI A8 mini gimbal kameradan gelen 1080p RTSP akışını Jetson Orin NX üzerinde okurken gecikmeyi biriktirmeden nasıl tuttuğumuzu, hangi ayarların gerçekten işe yaradığını ve hangilerinin sessizce hiçbir şey yapmadığını.

RTSP nedir, neden dron kamerasında karşımıza çıkıyor

RTSP (Real Time Streaming Protocol), video verisinin kendisini taşımaz; oturumu kontrol eder — “başlat”, “duraklat”, “şu akışı gönder” gibi komutları taşıyan bir protokoldür. Asıl görüntü paketleri RTP (Real-time Transport Protocol) üzerinden akar. Kamera bir RTSP sunucusu çalıştırır, siz bir URL ile bağlanırsınız.

Dron kameralarında yaygın olmasının nedeni pratik: gimbal kamera Ethernet üzerinden companion computer’a bağlanır ve tek bir adresle hem yer istasyonuna hem de tespit koduna aynı akışı sunar. ArduPilot dokümantasyonunda SIYI A8 mini ve ZR10 için verilen adres rtsp://192.168.144.25:8554/main.264, varsayılan IP ise 192.168.144.25.

OpenCV ile akışı açmak

OpenCV’nin FFmpeg arka ucu RTSP’yi doğrudan destekler. FFmpeg tarafına verilecek seçenekler OPENCV_FFMPEG_CAPTURE_OPTIONS ortam değişkeniyle geçirilir. Biçim alışılmadıktır: anahtar ile değeri noktalı virgül, çiftleri birbirinden dikey çizgi ayırır.

import os

# VideoCapture oluşturulmadan ÖNCE ayarlanmalı: OpenCV bu değişkeni
# akışı açarken okur. Biçim: "anahtar;deger|anahtar;deger"
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = (
    "rtsp_transport;tcp"
    "|fflags;nobuffer"          # ilk analiz sırasındaki tamponlamayı azaltır
    "|max_delay;300000"         # mikrosaniye -> 0.3 s
    "|reorder_queue_size;0"     # sıra bozulmuş paketleri bekleme
    "|probesize;100000"         # varsayılan 5.000.000 bayt
    "|analyzeduration;500000"   # varsayılan 5.000.000 us (5 s)
)

import cv2

URL = "rtsp://192.168.144.25:8554/main.264"
cap = cv2.VideoCapture(URL, cv2.CAP_FFMPEG)
print(cap.isOpened(), cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FPS))

probesize ve analyzeduration varsayılanları akış açılışını uzatır; düşürmek ilk kareye ulaşma süresini kısaltır. Fazla düşürürseniz FFmpeg akış formatını çözemez — kademeli düşürüp isOpened() sonucunu izleyin.

Asıl problem: kuyruk büyür, görüntü geçmişte kalır

Basit aritmetik: akış 30 FPS (kare başına ~33 ms) üretiyor, sizin döngünüz saniyede 15 kare işliyorsa (kare başına ~67 ms), her saniye 15 kare geride kalırsınız. 10 saniye sonra kuyrukta 150 kare, yani yaklaşık 5 saniyelik gecikme vardır. Bizim uçuş konfigürasyonumuzda YOLO11m 640 piksel tam-kare çıkarımla 15-20 FPS veriyor; akış hızı bunun üzerinde olduğu sürece fark kaçınılmaz olarak birikir.

Önemli nokta: bu gecikme sabit değil, büyüyen bir gecikmedir. Sabit 200 ms’lik bir gecikmeyle yaşayabilirsiniz; her dakika artan bir gecikmeyle otonom bırakma yapamazsınız.

RTSP kare kuyruğunun zamanla büyümesi

Çözüm: sadece en yeni kareyi tutan okuyucu thread

Doğru zihinsel model şu: akışı kuyruk değil, tek elemanlı bir kutu olarak ele alın. Arka planda bir thread durmadan okur ve her yeni kareyi bir öncekinin üzerine yazar. İşleme döngüsü kutuya baktığında her zaman en güncel kareyi bulur; arada kaçan kareler sessizce düşer. Bir tespit sisteminde bu tam olarak istediğimiz davranıştır — kare kaybı, güncelliğe tercih edilir.

import threading
import time
import cv2

class FreshestFrame:
    """RTSP akışını arka planda okur, yalnızca EN SON kareyi tutar.

    Kuyruk yok: her yeni kare bir öncekinin üzerine yazılır. İşleme
    yavaşsa eski kareler düşürülür, böylece gecikme birikmez.
    """

    def __init__(self, url, backend=cv2.CAP_FFMPEG, reconnect_delay=2.0):
        self.url = url
        self.backend = backend
        self.reconnect_delay = reconnect_delay

        self._lock = threading.Lock()
        self._frame = None      # en son kare (BGR ndarray)
        self._seq = 0           # sayaç: aynı kareyi iki kez işlemeyi engeller
        self._stamp = 0.0       # karenin yakalandığı an (monotonic)
        self._running = True

        self._cap = self._open()
        self._thread = threading.Thread(target=self._loop, daemon=True)
        self._thread.start()

    def _open(self):
        cap = cv2.VideoCapture(self.url, self.backend)
        # FFMPEG arka ucunda etkisiz, GStreamer/V4L2 gibi arka uçlarda işe yarar.
        cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
        return cap

    def _loop(self):
        fails = 0
        while self._running:
            if self._cap is None or not self._cap.isOpened():
                time.sleep(self.reconnect_delay)
                self._cap = self._open()
                continue

            ok, frame = self._cap.read()
            if not ok:
                fails += 1
                if fails >= 5:              # geçici hata değil, bağlantı kopmuş
                    self._cap.release()
                    self._cap = None
                    fails = 0
                continue

            fails = 0
            with self._lock:
                self._frame = frame
                self._seq += 1
                self._stamp = time.monotonic()

    def read(self, last_seq=0, timeout=1.0):
        """En son kareyi döndürür; last_seq verilirse YENİ bir kare bekler."""
        deadline = time.monotonic() + timeout
        while True:
            with self._lock:
                if self._frame is not None and self._seq > last_seq:
                    return self._seq, self._frame, self._stamp
            if time.monotonic() > deadline:
                return last_seq, None, 0.0
            time.sleep(0.002)

    def release(self):
        self._running = False
        self._thread.join(timeout=2.0)
        if self._cap is not None:
            self._cap.release()

Kullanımı, kare yaşını ölçerek:

stream = FreshestFrame("rtsp://192.168.144.25:8554/main.264")
seq = 0
try:
    while True:
        seq, frame, stamp = stream.read(last_seq=seq, timeout=2.0)
        if frame is None:
            continue                       # akış yok, döngü boşa dönmesin
        age_ms = (time.monotonic() - stamp) * 1000.0
        results = model.predict(frame, imgsz=640, conf=0.25, verbose=False)
        print(f"kare {seq} | işlemeye başlarken yaş: {age_ms:.0f} ms")
finally:
    stream.release()

O age_ms satırı yazının en değerli parçası olabilir: gecikmeyi tahmin etmeyi bırakıp ölçmeye başlarsınız. Sabit bir sayı görüyorsanız iyisiniz; sürekli artan bir sayı görüyorsanız hâlâ bir yerde kuyruk var.

CAP_PROP_BUFFERSIZE neden çoğu zaman işe yaramıyor

İnternette en sık önerilen çözüm cap.set(cv2.CAP_PROP_BUFFERSIZE, 1). OpenCV 4.x kaynağında FFmpeg arka ucunun setProperty fonksiyonu bu özelliği hiç ele almaz — yalnızca CAP_PROP_POS_MSEC, CAP_PROP_POS_FRAMES, CAP_PROP_POS_AVI_RATIO, CAP_PROP_FORMAT ve CAP_PROP_CONVERT_RGB işlenir. Yani RTSP + FFmpeg kombinasyonunda çağrı sessizce hiçbir şey yapmaz ve set() dönüş değeri False gelir.

Yine de yukarıdaki sınıfta bıraktık: aynı kod bir USB kamerayla (V4L2) veya GStreamer arka ucuyla çalıştırıldığında etkili oluyor. Kural şu: set() çağrılarının dönüş değerini kontrol edin. OpenCV desteklenmeyen özellikte hata atmaz, sadece False döner.

FFmpeg seçenekleri ve TCP/UDP kararı

Seçenek Ne yapar Ne zaman kullanılır
rtsp_transport Taşıma katmanını seçer: udp, tcp, udp_multicast, http, https Varsayılanı bilerek değiştirmek istediğinizde
rtsp_flags;prefer_tcp TCP varsa önce onu dener OpenCV zaten bunu varsayılan yapıyor
fflags;nobuffer Akış analizi sırasındaki tamponlamayı azaltır Her canlı akışta
max_delay Demux gecikme üst sınırı (mikrosaniye) Kayıp pakete uzun süre takılmasın istediğinizde
reorder_queue_size Sırası bozulmuş paketler için tampon paket sayısı UDP’de kayıp yerine güncellik istediğinizde
buffer_size Soket tamponunun bayt cinsinden üst sınırı Yüksek bit hızlı akışlarda paket düşüyorsa
timeout Soket TCP G/Ç zaman aşımı (mikrosaniye) Kopan bağlantıda sonsuza kadar beklememek için

OpenCV, rtsp:// adreslerinde libavformat sürümüne göre varsayılan olarak rtsp_flags=prefer_tcp (yeni sürümler) ya da rtsp_transport=tcp (eski sürümler) uygular. Yani hiçbir şey yapmazsanız zaten TCP’desiniz.

TCP UDP
Paket kaybı Yeniden gönderilir, kare bozulmaz Kayıp kalır, blok artefaktı görülür
Gecikme davranışı Kayıp anında gecikme sıçrar Kayıpta gecikme sabit kalır
Ağ geçidi/NAT Genelde sorunsuz Bloke edilebilir
Ne zaman Kablolu Ethernet, kayıp az; ayrıca kayıt alacaksanız Telsiz/paylaşımlı bağlantı, güncellik kare bütünlüğünden önemliyse

Dronda gimbal ile companion computer arasında kablolu Ethernet olduğu için biz TCP’de kaldık; kayıp zaten yok denecek kadar azken UDP’nin sağladığı avantaj ortadan kalkıyor.

Yeniden bağlanma: uçuşta kaçınılmaz

Kamera yeniden başlar, kablo mikro-kesinti yaşar, switch bir paket yutar. Yukarıdaki _loop bunu üstlenir: art arda 5 başarısız read() sonrası capture nesnesini kapatır, sonraki turda yeniden açar. Tek False dönüşünde bağlantıyı kapatmamak önemli — geçici kare hataları normaldir.

Bir uyarı: OpenCV’nin read() ve açılış çağrıları bloke edicidir ve varsayılan bekleme süreleri uzundur. CAP_PROP_OPEN_TIMEOUT_MSEC ve CAP_PROP_READ_TIMEOUT_MSEC bunu kısaltır, ancak ikisi de “yalnızca açılışta” ayarlanabilir: cv2.VideoCapture(url, cv2.CAP_FFMPEG, [cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000]) biçiminde parametre listesiyle verilmelidir; sonradan set() ile değişmez. Bazı opencv-python sürümlerinde bu sabitler açığa çıkmıyor, hasattr(cv2, "CAP_PROP_OPEN_TIMEOUT_MSEC") ile kontrol edin.

Sık düşülen tuzaklar

  • cap.set(CAP_PROP_BUFFERSIZE, 1) yazıp sorunu çözdüğünü sanmak. FFmpeg arka ucunda etkisizdir; dönüş değerini yazdırın.
  • Ortam değişkenini cv2.VideoCapture çağrısından sonra ayarlamak. Değişken akış açılırken okunur, sonradan yapılan değişiklik o oturumu etkilemez.
  • Ayraçları karıştırmak. rtsp_transport;tcp|fflags;nobuffer doğru; virgül veya = kullanmak seçeneği sessizce yok sayar.
  • Kodek seviyesi bayrakları demuxer sözlüğüne koymak. OPENCV_FFMPEG_CAPTURE_OPTIONS içeriği akış açılışına (avformat_open_input) gider. ffplay -flags low_delay gibi kodek bayraklarının bu yoldan geçtiğini varsaymayın; etkisini ölçmeden kabul etmeyin.
  • Kuyruğu grab() döngüsüyle “temizlemeye” çalışmak. Sabit sayıda kare atmak yalnızca o anki hız farkına uyar; hız değişince ya gecikme geri gelir ya da CPU’yu boşa yakarsınız. Thread yaklaşımı kendi kendine ayarlanır.
  • Kare yaşını hiç ölçmemek. Gözle “gecikmeli görünüyor” demek yerine tek satır zaman damgası ekleyin; hangi değişikliğin işe yaradığını ancak böyle görürsünüz.
  • Jetson’da yazılım kod çözücüde kalmak. Orin NX’te donanım kod çözücüyü kullanan bir GStreamer hattı (rtspsrc latency=0 ! rtph264depay ! h264parse ! nvv4l2decoder ! nvvidconv ! video/x-raw,format=BGRx ! videoconvert ! video/x-raw,format=BGR ! appsink drop=true max-buffers=1) CPU’yu tespit modeline bırakır; appsink’teki drop=true max-buffers=1 de aynı “en yeni kare” mantığını hattın içine taşır.

Pratik özet

  1. Akışı ana döngüde okumayın. Arka planda okuyup tek elemanlı kutuda en yeni kareyi tutan bir thread kurun; kare düşürmek gecikmeye tercih edilir.
  2. Her karenin yakalanma zamanını saklayın ve işlemeye başlarken yaşını milisaniye cinsinden yazdırın. Sabit bir sayı iyi, artan bir sayı hâlâ kuyruk var demektir.
  3. FFmpeg seçeneklerini OPENCV_FFMPEG_CAPTURE_OPTIONS ile, capture nesnesini oluşturmadan önce, anahtar;deger|anahtar;deger biçiminde verin. probesize ve analyzeduration düşürmek ilk kareye ulaşmayı hızlandırır.
  4. Kablolu Ethernet’te TCP’de kalın (OpenCV zaten varsayılan olarak TCP tercih ediyor). UDP’yi ancak telsiz/paylaşımlı bir bağlantıda ve kare bütünlüğünden vazgeçebiliyorsanız düşünün.
  5. Yeniden bağlanmayı baştan yazın: tek read() hatasında değil, art arda birkaç hatadan sonra capture’ı kapatıp yeniden açın; açılış/okuma zaman aşımlarını constructor parametresiyle kısaltın.
  • rtsp
  • opencv
  • python
  • gecikme
  • video-isleme

Zor bir mühendislik probleminiz mi var?

Yapılabilir mi, ne kadar sürer, nasıl kurulur: konuşarak başlayalım.

Projenizi Konuşalım