| Autor: | Wojciech Muła |
|---|---|
| Dodany: | 16.02.2002 |
| Aktualizacja: | 30.04.2008 |
Contents
|g3|g2|g1|g0| -> |00|g3|g3|g3|00|g2|g2|g2|00|g1|g1|g1|00|g0|g0|g0|
; wejście: al -- pixel 8bpp - poziom szarości
; wyjście: eax -- pixel 32bpp
; eax=|xx|xx|xx|gg|
and eax, 0x000000ff ; eax=|00|00|00|gg|
mov ah, al ; eax=|00|00|gg|gg|
shl eax, 8 ; eax=|00|gg|gg|00|
mov al, ah ; eax=|00|gg|gg|gg|
; wejście: eax -- 4 pixele 8bpp ; wyjście: mm1:mm0 -- 4 pixele 32bpp movd mm0, eax ; mm0 = 0000000044332211 puncpklbw mm0, mm0 ; mm0 = 4444333322221111 movq mm1, mm0 ; puncpklbw mm0, mm0 ; mm0 = 2222222211111111 puncpkhbw mm1, mm1 ; mm1 = 4444444433333333 ; -- gdy koniecznie trzeba wyzerować najstarsze bajty psrld mm0, 8 ; mm0 = 0022222200111111 psrld mm1, 8 ; mm1 = 0044444400333333
Zobacz przykład zamieszczony w innym artykule.
Przedstawionych zostanie kilka programów wyznaczających poziomy szarości z obrazów RGB — 24/32bpp. Zakładam, że składowe R, G i B mają szerokość 8 bitów.
Znane są mi trzy sposoby na obliczenie poziomu szarości piksela:
Pierwsza zależność to oczywiście zwykła średnia arytmetyczna, natomiast druga jest średnią ważoną — uwzględniona została czułość oka ludzkiego na poszczególne składowe. Trzecia zależność nadaje się do zastosowań gdzie szybkość przedkłada się nad jakość — jak widać wystarczą zaledwie dwa przesunięcia bitowe i dwa sumowania.
Dzielenie na x86 jest bardzo czasochłonne, dlatego też proponuję użycie tabliczki dzielenia; wyrażenie wyznaczające jasność piksela będzie miało postać:
gray = div_table[R+G+B];
Rozmiar tablicy jest równy Rmax + Gmax + Bmax = 3 ⋅ 255 = 765.
Samo przygotowanie tablicy nie wymaga nawet dzielenia, tak więc kod asemblerowy będzie krótki i szybki. Jeśli jakaś liczba n dzieli się bez reszty przez 3 i ndiv3 = k, to również (n + 1)div3 = k, oraz (n + 2)div3 = k — tak więc 3 kolejne liczby w tablicy mają tę samą wartość.
byte div_table[3*255];
// ...
for (int i=0; i<255; i++)
div_table[i*3 + 0] = div_table[i*3 + 1] = div_table[i*3 + 2] = i
Poniżej implementacja w asemblerze.
segment .data
div_table_8bpp times(3*255) db 0x00 ; tablica konwersji na obrazy 8bpp
div_table_32bpp times(3*255) dw 0x00000000 ; i 32 bpp
segment .text
; funkcja wypełnia tablicę div_table_8bpp
precalc_8bpp:
mov cl, 0
mov esi, div_table_8bpp
.fill:
mov [esi+0], cl
mov [esi+1], cl
mov [esi+2], cl
add esi, byte 3
inc cl
jnc .fill
ret
; funkcja wypełnia tablicę div_table_32bpp
precalc_32bpp:
mov ecx, 0x00000000
mov esi, div_table_32bpp
.fill:
mov [esi+0], ecx
mov [esi+4], ecx
mov [esi+8], ecx
add esi, byte 3*4
add ecx, 0x01010101
jnc .fill
ret
; esi -> wskaźnik na obraz wejściowy 32bpp
; edi -> wskaźnik na obraz wyjściowy 8bpp
; ecx = ilość pikseli
convert:
xor ebx, ebx
xor edx, edx
.loop:
mov eax, [esi] ; eax = |x|R|G|B|
mov bl, al ; ebx = |0|0|0|B|
mov dl, ah ; edx = |0|0|0|G|
shr eax, 16 ; eax = |0|0|x|R|
mov ah, 0 ; eax = |0|0|0|R|
; ***
add eax, ebx ; eax = R+B
mov al, [div_table_8bpp+eax+edx]
mov [edi], al
add esi, byte 4
inc edi
loop .loop
Dla konwersji RGB 32bpp -> grayscale kod różni się nieznacznie (pokazany fragment).
; ... ; ; *** add eax, ebx ; eax = R+G+B add eax, ecx ; mov eax, [div_table + eax*4] mov [edi], eax add esi, byte 4 add edi, byte 4 loop .loop
Przy użyciu podstawowych rozkazów MMX trochę kłopotliwe jest dodanie do siebie składowych.
Dzielenie przez 3 wbrew pozorom można wykonać bardzo łatwo, poprzez mnożenie przez odwrotność: 65536/3 = 2184 = 0x5555. Ponieważ odwrotność jest dodatkowo mnożona przez 65536, więc wynikiem będzie starsze słowo wyniku — wynik działania rozkazu pmulhw. Zarówno suma R+G+B jak i odwrotność są mniejsze od 0x8000, są więc w zapisie U2 liczbami dodatnimi. Nie będzie więc problemów z wykorzystaniem wspomnianego rozkazu.
Poniższy kod jest zoptymalizowana, tak by maksymalnie wykorzystać obliczenia równoległe. (W komentarzach suma składowych będzie oznaczana jako rgbx).
movq mm0, [edi+0] ; mm0 = |0 |b1|g1|r1|0 |b0|g0|r0| movq mm1, mm0 movq mm2, mm0 movq mm3, [edi+8] ; mm1 = |0 |b3|g3|r3|0 |b2|g2|r2| movq mm4, mm3 movq mm5, mm3 pand mm0, packed_dword(0x000000ff) ; mm0 = |0 |0 |0 |r1|0 |0 |0 |r0| pand mm3, packed_dword(0x000000ff) ; mm3 = |0 |0 |0 |r3|0 |0 |0 |r2| psrld mm1, 16 ; mm1 = |0 |0 |0 |b1|0 |0 |0 |b0| psrld mm4, 16 ; mm4 = |0 |0 |0 |b3|0 |0 |0 |b2| psrlw mm2, 8 ; mm2 = |0 |0 |0 |g1|0 |0 |0 |g0| psrlw mm5, 8 ; mm5 = |0 |0 |0 |g3|0 |0 |0 |g2| paddd mm0, mm1 ; paddd mm3, mm4 ; R+B paddd mm0, mm2 ; mm0 = | rgb1 | rgb0 | -- R+G+B paddd mm3, mm5 ; mm3 = | rgb3 | rgb2 | packssdw mm0, mm3 ; mm0 = | rgb3 | rgb2 | rgb1 | rgb0 | pmulhw mm0, packed_word(0x5555) ; mm0 = | gray3 | gray2 | gray1 | gray0 | packuswb mm0, mm0 ; mm0 = |g3|g2|g1|g0|g3|g2|g1|g0|
Użycie rozkazu PSADBW (rozszerzenie MMX) znacznie upraszcza kod.
pxor mm7, mm7 ; mm7 = packed_byte(0x00) ; piksele 0 i 1 movq mm1, [edi+0] ; mm1 = |0 |b1|g1|r1|0 |b0|g0|r0| movd mm0, mm1 ; mm0 = |0 |0 |0 |0 |0 |b0|g0|r0| psadbw mm0, mm7 ; mm0 = | 0 | 0 | 0 | rgb0 | psadbw mm1, mm7 ; mm1 = | 0 | 0 | 0 | rgb0+rgb1 | psubw mm1, mm0 ; mm1 = | 0 | 0 | 0 | rgb1 | ; piksele 2 i 3 movq mm3, [edi+8] ; mm3 = |0 |b3|g3|r3|0 |b2|g2|r2| movd mm2, mm3 ; mm2 = |0 |0 |0 |0 |0 |b2|g2|r2| psadbw mm2, mm7 ; mm2 = | 0 | 0 | 0 | rgb2 | psadbw mm3, mm7 ; mm3 = | 0 | 0 | 0 | rgb2+rgb3 | psubw mm3, mm2 ; mm3 = | 0 | 0 | 0 | rgb3 | ; łącznie punpcklwd mm0, mm1 ; mm0 = | 0 | 0 |rgb 1|rgb 0| punpcklwd mm2, mm3 ; mm2 = | 0 | 0 |rgb 3|rgb 2| punpckldq mm0, mm2 ; mm0 = |rgb 3|rgb 2|rgb 1|rgb 0| ; dzielenie przez 3 pmulhw mm0, packed_word(0x5555) packsswdw mm0, mm0
Aby maksymalnie wykorzystać obliczenia równoległe należy przetwarzać równocześnie 4 piksele. Wcześniej należy zreorganizować dane, tak by otrzymać 3 wektory:
mm0 = | r3 | r2 | r1 | r0 | mm1 = | g3 | g2 | g1 | g0 | mm2 = | b3 | b2 | b1 | b0 |
Wystarczą wtedy zaledwie 3 rozkazy klasy pmul i dwa dodawania.
Oczywiście należy przygotować wektory wag pikseli — ponieważ są to liczby ułamkowe skorzystamy z formatu fixed-point, jednym problemem jest wybór mnożnika. Wykorzystanie mnożnika 65536 wyklucza użycie standardowego zestawu instrukcji MMX (rozkaz pmulhw), bowiem 0.587 ⋅ 65536 = 0x9645, a jest to liczba ujemna (jasne jest, że rozkaz pmuluhw takich ograniczeń nie nakłada). Tak więc by móc używać standardowych instrukcji MMX powinniśmy zrezygnować z dokładności — mnożnik 256, czyli 8 bitów po przecinku, zapewnia wystarczającą dokładność.
segment .data
weight_r_8 dw 0x004c, 0x004c, 0x004c, 0x004c ; int(0.299*256)
weight_g_8 dw 0x0096, 0x0096, 0x0096, 0x0096 ; int(0.587*256)
weight_b_8 dw 0x0024, 0x0024, 0x0024, 0x0024 ; int(0.144*256)
weight_r_16 dw 0x4c8b, 0x4c8b, 0x4c8b, 0x4c8b
weight_g_16 dw 0x9645, 0x9645, 0x9645, 0x9645
weight_b_16 dw 0x24dd, 0x24dd, 0x24dd, 0x24dd
segment .text
; wejście - mm0, mm1, mm2 - zawartość j.w.
convert_MMX:
pmullw mm0, [weigt_r_8] ; przemnożenie składowych przez współczynniki
pmullw mm1, [weigt_g_8] ;
pmullw mm2, [weigt_b_8] ;
paddw mm0, mm1 ; sumowanie
paddw mm0, mm2 ; mm0 = (|gray3|gray2|gray1|gray0|)*256
psrlw mm0, 8 ; mm0 = |gray3|gray2|gray1|gray0|
packsswb mm0, mm0 ; mm0 = |g3|g2|g1|g0|g3|g2|g1|g0|
ret
convert_MMX2:
pmuluhw mm0, [weigt_r_16]; przemnożenie składowych przez współczynniki
pmuluhw mm1, [weigt_g_16];
pmuluhw mm2, [weigt_b_16];
paddw mm0, mm1 ; sumowanie
paddw mm0, mm2 ; mm0 = |gray3|gray2|gray1|gray0|
packsswb mm0, mm0 ; mm0 = |g3|g2|g1|g0|g3|g2|g1|g0|
ret
Pokażę jeszcze dwa sposoby jak przekonwertować dane wejściowe na format wymagany przez funkcje convert_MMX i convert_MMX2.
; funkcja używa podstawowych rozkazów MMX
shuffle_rgb_MMX:
movq mm0, [edi] ; mm0 = |0 |b1|g1|r1|0 |b0|g0|r0|
movq mm1, mm0
movq mm2, mm0
movq mm3, [edi+8] ; mm3 = |0 |b3|g3|r3|0 |b2|g2|r2|
movq mm4, mm3
movq mm5, mm3
pand mm0, packed_dword(0x000000ff) ; mm0 = |0 |0 |0 |r1|0 |0 |0 |r0|
pand mm3, packed_dword(0x000000ff) ; mm3 = |0 |0 |0 |r3|0 |0 |0 |r2|
psrld mm1, 16 ; mm1 = |0 |0 |0 |b1|0 |0 |0 |b0|
psrld mm4, 16 ; mm4 = |0 |0 |0 |b3|0 |0 |0 |b2|
psrlw mm2, 8 ; mm2 = |0 |0 |0 |g1|0 |0 |0 |g0|
psrlw mm5, 8 ; mm5 = |0 |0 |0 |g3|0 |0 |0 |g2|
packssdw mm0, mm3 ; mm0 = | r3 | r2 | r1 | r0 |
packssdw mm1, mm4 ; mm1 = | b3 | b2 | b1 | b0 |
packssdw mm2, mm5 ; mm2 = | g3 | g2 | g1 | g0 |
ret
; funkcja używa rozszerzonych rozkazów MMX
shuffle_rgb_MMX2:
movq mm0, [edi] ; mm0 = |0 |b1|g1|r1|0 |b0|g0|r0|
movq mm2, [edi+8] ; mm2 = |0 |b3|g3|r3|0 |b2|g2|r2|
3 1 2 0
pshufw mm0, mm0, 0b11011000 ; mm0 = |0 |b1|0 |b0|g1|r1|g0|r0|
pshufw mm2, mm2, 0b11011000 ; mm2 = |0 |b3|0 |b2|g3|r3|g2|r2|
movq mm1, mm0
movq mm3, mm2
punpcklwd mm0, mm2 ; mm0 = |g3|r3|g2|r2|g1|r1|g0|r0|
punpckhwd mm1, mm3 ; mm1 = |0 |b3|0 |b2|0 |b1|0 |b0|
movq mm2, mm0
psrlw mm0, 8 ; mm0 = |0 |g3|0 |g2|0 |g1|0 |g0|
pand mm2, packed_word(0x00ff) ; mm2 = |0 |r3|0 |r2|0 |r1|0 |r0|
ret
; 13.03.2002
; 18.10.2002
;
; gray = 0.299 * R + 0.587 * G + 0.114 * B
; gray =( 4ch * R + 95h * G + 1eh * B) >> 8
movq mm0, [edi+0h] ; mm0 = |a1|r1|g1|b1|a0|r0|g0|b0| ; załaduj
movq mm1, mm0
movq mm2, [edi+8h] ; mm1 = |a3|r3|g3|b3|a2|r2|g2|b2| ; 4 pixele
movq mm3, mm2
; obliczenie czynnika 4ch*R + 95h*G
pand mm0, {0x00ff, 0x00ff, 0x00ff, 0x00ff} ; mm0 = |0 |r1|0 |b1|0 |r0|0 |b0|
pand mm2, {0x00ff, 0x00ff, 0x00ff, 0x00ff} ; mm2 = |0 |r3|0 |b3|0 |r2|0 |b2|
pmaddwd mm0, {0x004c, 0x001e, 0x004c, 0x001e} ; mm0 = |r1*4ch + b1*1eh|r0*4ch + b0*1eh|
pmaddwd mm2, {0x004c, 0x001e, 0x004c, 0x001e} ; mm2 = |r3*4ch + b3*1eh|r2*4ch + b2*1eh|
; obliczenie czynnika 1eh*B
pand mm1, {0x0000, 0xff00, 0x0000, 0xff00} ; mm1 = |0 |0 |g1|0 |0 |0 |g0|0 |
pand mm3, {0x0000, 0xff00, 0x0000, 0xff00} ; mm3 = |0 |0 |g3|0 |0 |0 |g2|0 |
psrlw mm1, 8 ; mm1 = |0 |0 |0 |g1|0 |0 |0 |g0|
psrlw mm3, 8 ; mm3 = |0 |0 |0 |g3|0 |0 |0 |g2|
packsswd mm1, mm3 ; mm1 = |0 |g3|0 |g2|0 |g1|0 |g0|
pmullw mm1, {0x0095, 0x0095, 0x0095, 0x0095} ; mm1 = |g3*95h |g3*95h |g3*95h |g3*95h |
; sumowanie czynników i dzielenie przez 256
packssdw mm0, mm2 ; mm0 = | rb3 | rb2 | rb1 | rb0 |
paddw mm0, mm1 ; mm0 = |rb3+g3 |rb2+g2 |rb1+g1 |rb0+g0 |
psrlw mm0, 8 ; mm0 = | gray3 | gray2 | gray1 | gray0 |
Jego stosowanie jest korzystne w przypadku kodu dla x86 — dla MMX mija się z celem, zważywszy na duże kłopoty z liczeniem średniej dwóch liczb.
W MMX2 (czyli wraz z SSE) wprowadzono rozkaz PAVGB, które oblicza średnią wg wzoru (A + B + 1)/2.
; wejście: edi - wskaźnik do obrazu 32bpp
; esi - wskaźnik do obrazu 8bpp
; ecx - ilość pikseli
convert:
mov eax, [edi] ; eax = |a|b|g|r|
add ah, al ;
rcr ah, 1 ; eax = | a | b |(g+r)/2| r |
shr eax, 8 ; eax = | 0 | a | b |(g+r)/2|
add al, ah ;
rcr al, 1 ; eax = | 0 | a | b |(b+(g+r)/2)/2|
mov [esi], al
add edi, byte 4
inc esi
loop convert
ret
Aby maksymalnie wykorzystać obliczenia równoległe należy przetwarzać równocześnie 4 piksele. Wcześniej należy zreorganizować dane, tak by otrzymać 3 wektory:
mm0 = | r3 | r2 | r1 | r0 | mm1 = | g3 | g2 | g1 | g0 | mm2 = | b3 | b2 | b1 | b0 |
Wystarczą wówczas dwa wywołania PAVGB
pavgb %mm1, %mm0 pavgb %mm2, %mm0