Autor: | Wojciech Muła |
---|---|
Dodany: | 17.02.2002 |
Aktualizacja: | 17.07.2005 |
Treść
Miksowanie, inaczej crossfading polega na wyznaczeniu sumy ważonej dwóch obrazów (równanie 1):
P = A ⋅ u + B ⋅ (1 − u) u = 0..1
Inna postać tego równania to (równanie 2):
P = A + u ⋅ (B − A)
Przedstawione przykłady są przeznaczone dla obrazów 8bpp (grayscale) i 32bpp. Współczynnik u jest zapisany w fixed-point o podstawie 256 (u w zakresie 0..255).
Aby uzyskać uzyskać wartość 1 − u w arytmetyce fixed-point, czyli tak naprawdę 256-u, należy zanegować 8 najmłodszych bitów u.
; al == u mov ebx, eax ; xor ebx, 0x000000ff ; bl = 256-u
Dodane 17.07.2005, na postawie niegdysiejszej dyskusji na pl.comp.programming.
P = Au + B(256 − u) P, A, B, u = 0..255
Po rozwinięciu otrzymujemy: P = (B ⋅ 256 − A ⋅ u + B ⋅ u) > > 8. Można każdy ze składników sumy oddzielnie podzielić przez 256, uzyskując co prawda nico inny wynik, ale niewiele odbiegający od „oryginalnego wzoru”. Tzn. otrzymujemy coś takiego: P = B − (A ⋅ u) > > 8 + (B ⋅ u) > > 8.
Ponieważ mnożone są bajty, to mnożenia A ⋅ u oraz B ⋅ u można wykonać za pomocą jednej 32-bitowej operacji mnożenia:
byte A,B,u dword X; // dword: 32 bity bez znaku dword Au,Bu; X = ((dword)A << 16) | (dword)B; // |0|A|0|B| X *= (dword)u // |A*u|B*u| -- wyniki mnożenia 16-bitowe Au = X >> 24 // (A*u) >> 8 Bu = X >> 8 // (B*u) >> 8 P = A - Au + Bu
W asemblerze to będzie wyglądać jakoś tak (miałem ładnie działającą procedurkę, ale została utracona w ferworze porządkowania burdelu w /home...):
; al = A ; starsze części rejestrów wyzerowane ; bl = B ; cl = u shl eax, 16 mov al, bl mul ecx add cl, ah shr eax, 16 sub cl, ah ; w cl wynik
Dogodnie jest stablicować wyniki mnożenia danej wartości u oraz 256-u i wszystkich wartości pikseli (albo składowej). Ponieważ piksel ma co najwyżej 8 bitów zatem będą potrzebne 2 tablice po 256 bajtów każda.
segment .data mul_tbl1 resb 256 ; i*u mul_tbl2 resb 256 ; i*(256-u) segment .text ; wejście: al - wartość u fill_tables: push eax push ebx push ecx push edx movzx eax, al ; al = u xor ebx, ebx ; ebx = i*u xor ecx, ecx ; licznik xor edx, edx ; tmp prepare: mov dl, bh xor dl, 0xff mov [mul_tbl1 + ecx], bh ; mul_tbl1[i] = (u*i) >> 8 mov [mul_tbl2 + ecx], dl ; mul_tbl2[i] = ((1-u)*i) >> 8 add ebx, eax inc ecx test ch, ch jz prepare pop edx pop ecx pop ebx pop eax ret
Poniżej funkcja, która wykorzystuje równanie 1.
; edi - wskaźnik na obraz 1 ; esi - wskaźnik na obraz 2 ; ecx - ilość bajtów do przetworzenia ; ; funkcja dokonuje operacji: [esi] = [edi]*u + [esi]*(1-u) crossfade1: push eax push ebx push ecx push edx xor eax, eax xor ebx, ebx xor edx, edx .loop: mov al, [edi] mov bl, [esi] mov dl, [mul_tbl1 + eax] ; dl = al*u add dl, [mul_tbl2 + ebx] ; dl = al*u + bl*(1-u) inc edi mov [esi], dl inc esi dec ecx jnz .loop push ebx push ecx push edx push eax ret
Rozkaz pmaddwd jest bardzo użyteczny.
movq mm7, {u, 1-u, u, 1-u} ; to trzeba zrobić tylko raz pxor mm6, mm6 ... .crossfade: movd mm0, [edi] ; mm0 = |0 |0 |0 |0 |d0|c0|b0|a0| movd mm1, [esi] ; mm1 = |0 |0 |0 |0 |d1|c1|b1|a1| punpclkbw mm0, mm6 ; mm0 = | d0 | c0 | b0 | a0 | -- rozszerzenie zakresu punpclkbw mm1, mm6 ; mm1 = | d1 | c1 | b1 | a1 | movq mm2, mm0 movq mm3, mm1 punpcklwd mm0, mm1 ; mm0 = | b1 | b0 | a1 | a0 | punpcklwd mm2, mm3 ; mm2 = | d1 | d0 | c1 | c0 | pmaddwd mm0, mm7 ; mm0 = |u*b1+(1-u)*b0|u*a1+(1-u)*a0| pmaddwd mm2, mm7 ; mm2 = |u*d1+(1-u)*d0|u*c1+(1-u)*c0| psrld mm0, 8 ; /256 (u jest fixed-point o podstawie 256) psrld mm2, 8 packssdw mm0, mm1 ; mm0 = | d' | c' | b' | a' | ; itd. ; ... ; obsługa pętli itp.
Zamiast wykorzystywać dwie tablce, można stworzyć jedną tablicę — trzeba poszerzyć jej zakres.
signed dword mul_tbl[512]; // może być signed word, ale nie polecam - x86 szybciej czyta pamięć spod // adresów podzielnych przez 4. for (i=-255; i<256; i++) mul_tbl[i+255] = (i*u)/256;
Podczas wypełniania tablicy można wykorzystać „symetrię” wartości tablicy. Kod asemblerowy będzie wówczas bardzo krótki.
signed dword mul_tbl[512]; temp = 0; for (i=0; i<256; i++) { mul_tbl[256+i] = temp; mul_tbl[256-i] =-temp; temp += u; }
Poniżej funkcja, która wykorzystuje równanie 2 (pojedynczą tablicę).
; edi - wskaźnik na obraz 1 ; esi - wskaźnik na obraz 2 ; ecx - ilość bajtów do przetworzenia ; ; funkcja dokonuje operacji: [esi] = [edi] + u*([esi] - [edi]) crossfade2: push eax push ebx push ecx crossfade: xor ebx, ebx mov al, [edi] mov bl, [esi] sub ebx, eax add eax, [mul_tbl + ebx + 255] inc edi mov [esi], al inc esi dec ecx jnz crossfade pop ecx pop ebx pop eax
Przy wykorzystaniu instrukcji MMX nie trzeba wykorzystywać żadnych tablic.
; wejście: al - u and eax, 0x000000ff mov ah, al xor mm7, mm7 movd mm6, eax ; mm6 = |00|00|00|00|00|u |00|u | punpckldq mm6, mm6 ; mm6 = |00|u |00|u |00|u |00|u | crossfade: movq mm0, [edi] ; mm0 = |p7|p6|p5|p4|p3|p2|p1|p0| -- numery pixeli movq mm1, [esi] ; mm1 = |pf|pe|pd|pc|pb|pa|p9|p8| movq mm2, mm0 movq mm3, mm1 punpcklbw mm0, mm7 ; mm0 = |00|p3|00|p2|00|p1|00|p0| punpcklbw mm1, mm7 ; punpcklbw mm2, mm7 ; punpcklbw mm3, mm7 ; psubw mm2, mm0 ; oblicz czynnik (b-a) psubw mm3, mm1 ; pmullw mm2, mm6 ; (b-a)*u pmullw mm3, mm6 ; psrlw mm2, 8 ; /256 -- w tym przypadku trzeba dzielić psrlw mm3, 8 ; /256 -- w tablicach wyniki mnożeń już były podzielone paddw mm0, mm2 ; a += (b-a)*u/256 paddw mm1, mm2 ; a += (b-a)*u/256 packuswb mm0, mm1 movq [esi], mm0 add edi, 8 add esi, 8 dec ecx jnz crossfade
Przesunięcie bitowe w prawo jest równoważne dzieleniu — inaczej mnożeniu przez odwrotność. Wobec tego równanie 2 można przyjąć następującą postać:
P = A > > n + B − B > > n u = ½n
Zatem dzięki przesunięciom bitowym można uzyskać miksowanie obrazów, z pewnymi ustalonymi wartościami u: 0,5, 0,25, 0,125, 0,0625 itd.
Przedstawiona metoda doskonale nadaje się nie tylko dla pixeli 8bpp/32bpp, ale również 15bpp. Ponieważ kod jest bardzo szybki można tę metodę używać do motionblur.
Kod x86:
; maska = packed_byte((1 << (9-n))-1) ; ; np. dla n==2 ; ; maska = packed_byte((1 << 7)-1) = packed_byte(0b00111111) ... mov eax, [edi] ; załaduj cztery pixele mov ebx, [esi] ; shr eax, n ; and eax, maska ; eax = packed_byte(pixel1/2^n) mov edx, ebx ; shr ebx, n ; and ebx, maska ; sub edx, ebx ; edx = packed_byte(pixel2 - pixel2/2^n) add eax, ebx mov [esi], eax ...
Kod dla MMX będzie taki sam, gdyż zastosowanie masek niweluje wpływ „wsuniętych” bitów z sąsiednich bajtów.
W przypadku u = 0.5 (fixed-point u=127) można posłużyć się uproszczoną formułą:
; p = (a >> 1) + (b >> 1) ... mov eax, [edi] ; załaduj cztery pixele mov ebx, [esi] ; shr eax, 1 shr ebx, 1 and eax, 0x7f7f7f7f and ebx, 0x7f7f7f7f add eax, ebx mov [esi], eax ...
Kod jest autorstwa smoly. Dodałem tylko opis i trochę komentarzy. Autor rzecz jasna wyraził zgodę na umieszczenie swojego dzieła na tej stronie
Metoda nadaje się do crossfadingu obrazów 24/32bpp oraz obrazów grayscale o dowolnej liczbie poziomów szarości:
// odejmowanie z nasyceniem int subtract_sat(int a, int b) { int result = a - b; if (result < 0) return 0; else return result; } int thershold_A, thershold_B; int crossfade(int pix_A, int pix_B) { return substract_sat(pix_A, thershold_A) + subtract_sat(pix_B, thershold_B); }
Wartości progowe są w zakresie 0..255 i spełniają warunek:
thershold_A = 255 - thershold_B;
Implementacja smoly MMX [przy mojej niewielkiej optymalizacji]:
crossfade32bpp: mov edx, screen_A ; obrazy mov edi, screen_B ; wejściowe mov esi, output ; obraz wynikowy mov eax, 01010101h movd mm7, eax punpckldq mm7, mm7 ; mm7 = packed_byte(0x01) pxor mm0, mm0 ; mm0 = packed_byte(0x00) pcmpeqd mm1, mm1 ; mm1 = packed_byte(0xff) sub eax, eax ; eax = 0 .next_blit: iterations equ (pixel_count*4/8)-1 ; zmiana polega na kopiowaniu w kolejności ; rosnących adresów - smola robił odwrotnie xor ecx, ecx .crossfade: movq mm2, [edx+ecx*8] movq mm3, [edi+ecx*8] psubusb mm2, mm0 ; subtract_sat(pix_A, thersold_A) psubusb mm3, mm1 ; subtract_sat(pix_B, thersold_B) paddusb mm2, mm3 ; suma movq [esi+ecx*8], mm2 inc ecx cmp ecx, iterations jne .crossfade paddusb mm0, mm7 psubusb mm1, mm7 ; tutaj kopiowanie bufora na ekran, ; retrace itp. inc al ; 255 iteracji jnc .next_blit ret