| Autor: | Wojciech Muła |
|---|---|
| Dodany: | 17.02.2002 |
| Aktualizacja: | 17.07.2005 |
Contents
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