Posts v4l2 Kütüphanesi ve Linux'ta Kamera Kullanmak
Post
İptal

v4l2 Kütüphanesi ve Linux'ta Kamera Kullanmak

Bir ihtiyaç düşünelim, konusu görüntü kullanılan ortam Linux olsun. Aklınıza hemen opencv gelmiş olmalı. Zaten tonla uygulama örneği de var oh ne hoş dediğinizi duyabilirim ama siz de benim Raspberry Pi 4’de RAM bellek azlığı yüzünden yaşadıklarımı yaşasaydınız eminim farklı yollar arayışına girerdiniz.

Elimde bu sorunu düzeltmek için 2 yöntem vardı. Ya yeni çıkan 8 GB’lik modele balıklama atlamak ki bu biraz pahalı bir yöntem; veya ille opencv mi canım, bu raspistill komutu çalışıyorsa başka çalışabilecek bir yöntem bulurum demek, ki bu daha akılcı bir çözüm olduğu için bunu seçtim (aslında cüzdanım boştu!!)

Kodla biraz didiştik ve sonuçta aslında çok basit ve yeterli bir kütüphane ile karşılaştım adı da “v4l2” kendisi linuxtv adındaki biz özgür grubun desteklediği bir kütüphane olur. linuxtv olayını ayrıca araştıracağım şimdilik konumuza geri dönelim.

Bu kütüphanenin bir yazılış amacı var. Amaç, kameradaki sensörden ham veri okuyacak, bir kütüphane yazmak. Bu, bu kadarcık. Ne işleyeyim diye uğraşıyor ne kaydedeyim diye. Aslında pek çok görüntü işleme kütüphanesinin de bu kütüphaneyi kullandığını öğrenince bodoslama girdim içine. Basit bir görüntü alma işi için opencv gibi karmaşık bir kütüphane yerine bunu kullanmak nasıl olur acaba.

Giriş: V4L2’ye giriş.

V4L2 kütüphanesi aslında Video for Linux kütüphanesi olan v4l efendinin ikinci jenerasyonu oluyor. Kütüphanenin geliştirilmesinin arkasında koskoca linux camiası olması sebebi ile çekirdeğe tam entegre olacak şekilde geliştirildi. API’si çeşitli video çıktı ve girdi ortamlarıyla eşzamanlı olarak çalışmaya odaklı; TV yayın akışlarından basit webcam’lere kadar hayli geniş yelpazede cihaza ve veri tipine destek sağlıyor. Video cihazı çağrıları ise IOCTL çağrıları olarak yapılıyor. Karmaşık görünen bu yöntem kullandıkça ufuk açabilme yeteneğine sahip. Neyse işte öyle bir kütüphane ile karşı karşıya duruyoruz yani :joy:

Nasıl Kullanırım

V4L2 kütüphanesi ile veri akışı sağlamak için adım adım gitmek gerekiyor. Bu adımlar biraz da mükemmel yemeği yapmak için verilen mükemmel tarifin püf noktaları gibi :smile:

  • Cihaz için bir tanımlayıcı açılması (I/O node)
  • Cihazın özelliklerinin tanımlanması. Bunu iki şekilde yapabilirsiniz
    • Otomatik tanımlama. (Bol bol if-else kullanarak)
    • Bildiğiniz cihaz için elle girmek (Taşı taşa vurarak alet yapmak gibi bi’şey)
  • Çekim biçimini ayarlayın. Bunlar çerçeve boyutu, renk ve sıkıştırma gibi sıralanabilir. Önceki yazımda bazı renk uzaylarından bahsetmiştim. İşte orada belirttiğim YUV ve RGB uzayları v4l2 de kullanılan renk uzaylardan sadece ikisi. Bir çok renk uzayı ve tonla farklı format ve kameranıza uygun boyut seçimini yaptıktan sonra devam edebilirsiniz.
  • Cihaz girdi çıktıları tutmak için bellekte tampon oluşturun. Ve bunu aygıtın blok beslemesi ile eşzamanlayın.
  • Cihazı akış moduna geçirmeye hazırsınız afiyet olsun

Akış başladıktan sonra ise tek yapmanız gereken tamponlardaki verileri uygun şekilde evirip çevirmekten fazlası değil. Verilerin ayıklanıp, dosyaya veya framebuffer’a yazılması gerekiyor hepsi o kadar.

NOT: İşinizin bitmesi ile beraber akış modunu durdurmayı ve tampon belleğini silmeyi unutmayınız.

Şimdi adım adım yemeğimizi yapalım o zaman.

1. Cihaz tanımlama

Cihaz tanımlamasını yapmak veya cihazı sonlandırmak basit IOCTL komutlarından başka bi’şey değildir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int fd;

if( (fd = open("/dev/video0", O_RDWR)) < 0) {
// eğer video cihazı açılmaz ise sonuç -1'dir
 perror("open");
 exit(1);
}
....
...
..
.
// Görüntü alma işlemini yaptığımızı varsayalım
close(fd); // Bu kısım fd buffer'ini kapatır
return EXIT_S // başarılı bitirilme kodu

Burada önemli bir noktayı atlamadan geçmeyelim. Linux işletim sisteminde pek çok cihaz için device node objesi /dev dizini altındadır. Video cihazları yani kameraların isimleri ise /dev/videoX şeklinde tutulur (X cihazın numarası). Genelde bir cihaz bağlı olan durum için video0 bizim cihazımız olacak demektir. Ancak paralel bağlanmış birden fazla cihaz için yeni yeni device node objesi açılır ve her birisi için bu değer değişir. Bu objelerden hangisi hangi kamera olduğunu öğrenmek için ise 2. adımdaki yolu kullanılır.

2. Cihaz özelliklerini tanımlamak

Kendi kameramızın özelliğini biliyor olabiliriz ancak her v4l2 uyumlu cihazın özelliklerini bilemeyiz. İşte bu noktada devreye VIDIOC_QUERYCAP çağrısı girer. Bu çağrıyı kullanarak tanımlama yapabiliriz.

1
2
3
4
5
6
7
struct v4l2_capability camera_capability;

if(ioctl(fd, VIDIOC_QUERYCAP, &camera_capability) < 0){
// eğer video cihazı tanımlanamaz ise sonuç -1'dir
 perror("VIDIOC_QUERYCAP");
 exit(1);
}

v4l2_capability veri tipi içerisinde kameraya ait verileri tutar. Bunlar,

  • driver: Kullanılan sürücü adını içerir.
  • card: Kullanılan kamera/sensör aygıtı modelini içerir.
  • bus_info: Girdi cihazının bağlı olduğu yerdir. Biz
  • version: Sürücü yazılım sürümü.
  • capabilities: 32 bit uzunlukta bir veridir. Özellikleri içerir.

Şimdi kameraya ait özellik verisini aldık ancak bu işlem yeterli değildir. Yapmamız gereken şey bu özelliğe ait kameranın kullanırlığını test etmek. Bizim için iki önemli özellik var. Bunlar video ve frame çekimi yapıp yapamadığına bakmak. Bunlar bizim makinelerimizi basit bir webcam yapmak için kullanabileceğimizi gösteriyor.

1
2
3
4
5
6
7
8
if(!(camera_capability.capabilities & V4L2_CAP_VIDEO_CAPTURE)){
 fprintf(stderr, "The device does not handle video capturing.\n");
 exit(1);
}
if(!(camera_capability.capabilities & V4L2_CAP_STREAM)){
  fprintf(stderr, "The device does not support frame capturing.\n");
  exit(1);
}

Burada farketti iseniz iki durumu kontrol ettik. V4L2_CAP_STREAM ve V4L2_CAP_VIDEO_CAPTURE. V4L2_CAP_STREAM bizim frame çekimi yapıp yapamamamızı kontrol ediyor. Çünkü tek shot çekim yapmamız gerekebilir. V4L2_CAP_VIDEO_CAPTURE zaten desteklemiyorsa video çekimini de unutun yani. :joy: V4L2_CAP_VIDEO_CAPTURE değişkeni single-plane çekim desteğini göstermektedir ki bu da pekçok kamerada olan en temel özelliktir.

3. Cihaz çekim formatı.

Direk kodla dalıyorum.

1
2
3
4
5
6
7
8
9
10
11
struct v4l2_format video_format;

video_format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
video_format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUVY;
video_format.fmt.pix.width = 640;
video_format.fmt.pix.height = 480;

if(ioctl(fd, VIDIOC_S_FMT, &video_format) < 0){
 perror("VIDIOC_S_FMT");
 exit
}

v4l2_format objesi bizim kameramızın çekimi hangi formatta yapabileceğimizi belirleyen obje. Bu objeyi elle tanımlıyoruz. Kameramıza ait özellikleri bilmiyorsanız yapacağınız şey ise v4l2-util komutunu kullanmak.

1
$ v4l2-ctl -d /dev/videoX --list-formats-ext #X sizin cihazınızın numarası

Bu komutun çıktısı yaklaşık olarak şu olacak. v4l2-utils

Biraz konudan uzaklaştık ama hemen geri dönüyorum video_format objesi ile.

  • video_format.type: akış formatını burada belirliyoruz. Diyelim ki bizim formatımız V4L2_CAP_VIDEO_CAPTURE ise V4L2_BUF_TYPE_VIDEO_CAPTURE belirleriz. Daha fazlası için API belgelendirmesine bakılması faydalıdır. Bakın yani…
  • video_format.fmt: formata ait detaylandırmayı yapan kısımdır kendisi
    • video_format.fmt.win: Pencere çervesini yapılandır. Bunu atladım sayın ve gerekli ise sizin için API belgelendirmesine bakın.
    • video_format.fmt.pix: Pixel yapılandırmasını yapıyoruz. Burası çokomelli yani. :smile:
      • video_format.fmt.pix.pixelformat: Piksel renk uzayı formatı. YUYV benim kamera için öntanımlı tek tür. Başkası da çalışıyor olsaydı onu da yapabiirdik.
      • video_format.fmt.pix.width: Piksel genişliği.
      • video_format.fmt.pix.height: Piksel yüksekliği.

Bunu da tanımladıktan ve test ettikten sonra akışa başlamak için hazırız.

4. Verileri tamponlamak.

1
2
3
4
5
6
7
8
9
10
struct v4l2_requestbuffers reqbuff;

reqbuff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuff.memory = V4L2_MEMORY_MMAP;
reqbuff.count = 1;

if(ioctl(fd, VIDIOC_REQBUFS, &reqbuff) < 0){
 perror("VIDIOC_REQBUFS");
 exit(1);
}

Bu kodu açıklamaya gerek tek kısım V4L2_MEMORY_MMAP objesi. Bu obje veriyi tamponalayacağımız veri tiplerinden birisi. reqbuff için okuma belleği olarak bunu vermemiz bu veri tipinde tamponlama yapmamızı belirtir. Bazı tampon veri tipleri için o veriyle oynamamızı sağlayan kütüphaneler vardır. mmap bunlardan birisi olduğu için bunu kullanmakta başlangıç için yarar var. Diğer tampon veri tiplerine nereden ulaşacağınızı söylememe gerek duymuyorsunuz sanırım.

Şimdi tamponumuzun info objesini tanımlayalım.

1
2
3
4
5
6
7
8
9
10
11
struct v4l2_buffer infbuff;
memset(&infbuff, 0, sizeof(infbuff));

infbuff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
infbuff.memory = V4L2_MEMORY_MMAP;
infbuff.index = 0;

if(ioctl(fd, VIDIOC_QUERYBUF, &infbuff) < 0){
 perror("VIDIOC_QUERYBUF");
 exit(1);
}

Veri ile işlem yapmak için memory tipini belirlemiştik şimdi de mmap kullanarak da veri tanımlamasını yapalım.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void* buffer_start = mmap(
 NULL,
 infbuff.length,
 PROT_READ | PROT_WRITE,
 MAP_SHARED,
 fd,
 infbuff.m.offset
);

if(buffer_start == MAP_FAILED){
 perror("mmap");
 exit(1);
}

memset(buffer_start, 0, infbuff.length);

5. Bir kare çekim yapak mı?

Bu kısım kodun döngü ihtiva edecek kısmıdır. Şimdi bir kare çekim yapalım. Bunu yapmak için bir eylem planımız var yine.

  1. Kuyruğa aldığınız arabellek hakkında bilgi hazırlayın. Bu, yukarıda gördüğümüz başka bir v4l2_buffer yapısı gerektiriyor
  2. Cihazın akış özelliğini etkinleştirin (daha önce v4l2_capability ile kontrol ettik).
  3. Arabelleği sıraya alın. Temel olarak ara belleğinizi aygıta veriyorsunuz (gelen kuyruğa koyuyorsunuz) ve yazmasını bekleyin. Bu VIDIOC_QBUF çağrısı kullanılarak yapılır.
  4. Arabelleği ayıklayın. Cihaz bitti, ara belleğinizi okuyabilirsiniz. Bu adım VIDIOC_DQBUF çağrısı kullanılarak ele alınır: arabellek giden kuyruktan alınır. Bu aramanın cihazı bir süre askıya alınabilir. Çünkü verinin yazılması için belirli bir süre gerekiyor. Eğer ki işlemciniz sağlamsa bu süre size hissedilmeyecek kadar az gelir ancak Raspberry Pi 4 4GB’lik model için ben sanırım birkaç saniye bekliyorum.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct v4l2_buffer infbuff;
memset(&infbuff, 0, sizeof(infbuff));

infbuff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
infbuff.memory = V4L2_MEMORY_MMAP;
infbuff.index = 0; /* Tampon kuyruk listesini başlangıç için 0 yaptık. */

// tampon gelişini kontrol edelim
if(ioctl(fd, VIDIOC_QBUF, &infbuff) < 0){
 perror("VIDIOC_QBUF");
 exit(1);
}

// Akışa başlayalım
int type = infbuff.type;
if(ioctl(fd, VIDIOC_STREAMON, &type) < 0){
 perror("VIDIOC_STREAMON");
 exit(1);
}

while(True){ // Sakın ama sakın true ile yapmayın bu örnek olması açısından yapıldı
 // İlk sıradan veri çekiyoruz
 if(ioctl(fd, VIDIOC_DQBUF, &infbuff) < 0){
 perror("VIDIOC_QBUF");
 exit(1);
 }

 infbuff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 infbuff.memory = V4L2_MEMORY_MMAP;

 // Sonraki sıraya yönlendiriyoruz
 if(ioctl(fd, VIDIOC_QBUF, &infbuff) < 0){
 perror("VIDIOC_QBUF");
 exit(1);
 }
}


// Döngüden çıktık diyelim. İşte o zaman veri akışını durduruyoruz
if(ioctl(fd, VIDIOC_STREAMOFF, &type) < 0){
 perror("VIDIOC_STREAMOFF");
 exit(1);
}

Bu kadar. Basitçe bu iş için bir kod bu şekilde yazdırılıyor. Siz buradan hareketle kendinize bir uygulama yapabilirsiniz sanırım.

Daha fazla örnek isteyenler için

v4l2 belgelendirmesinden

Bu gönderi, yazar tarafından CC BY 4.0 altında lisanslanmıştır.