Miksowanie obrazów

Autor: Wojciech Muła
Dodany:17.02.2002
Aktualizacja:17.07.2005

Treść

Wprowadzenie

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

Równanie 1 (algorytm ogólny)

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

Równanie 1 (x86)

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

Równanie 1 (MMX)

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.

Równanie 2 (x86)

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

Równanie 2 (MMX)

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

Przyspieszenie

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
...

Uproszczony crossfade

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