SSEで画像処理 --- グレースケール化

下記記事にてSSEで簡単な画像処理をしてみました。
SSEで画像処理 --- 導入 - 何でもプログラミング

今回はカラー画像をグレースケール化する処理を実装してみたいと思います。

入力はRGBA32bitを仮定しています。(RGB24bitの場合、48Byte毎に処理する必要があり、さらに複雑になります。)

float化して計算

R、G、Bを各々分解し、係数を掛けて加算を繰り返します。

簡単のため、RGB各々にグレー値を設定して返す内容になっています。(Gray8bitで返す場合は、4 * 16Byteの幅でループを回して結果を詰める必要があります。)

aligned_vector<byte> ToGray(const aligned_vector<byte>& src)
{
    aligned_vector<byte> dst(src.size());

    // 係数
    __m128 rc = _mm_set1_ps(0.299f);
    __m128 gc = _mm_set1_ps(0.587f);
    __m128 bc = _mm_set1_ps(0.114f);

    // マッピング配列(RGBA -> R32, G32, B32)
    __m128i mapR = _mm_setr_epi8(0, -1, -1, -1, 4, -1, -1, -1,  8, -1, -1, -1, 12, -1, -1, -1);
    __m128i mapG = _mm_setr_epi8(1, -1, -1, -1, 5, -1, -1, -1,  9, -1, -1, -1, 13, -1, -1, -1);
    __m128i mapB = _mm_setr_epi8(2, -1, -1, -1, 6, -1, -1, -1, 10, -1, -1, -1, 14, -1, -1, -1);
    for (int i = 0; i < (int)src.size(); i += 16)
    {
        __m128i s = _mm_load_si128((__m128i*)(src.data() + i));

        // RGBA -> R32, G32, B32
        __m128i ri = _mm_shuffle_epi8(s, mapR);
        __m128i gi = _mm_shuffle_epi8(s, mapG);
        __m128i bi = _mm_shuffle_epi8(s, mapB);

        // float化
        __m128 r = _mm_cvtepi32_ps(ri);
        __m128 g = _mm_cvtepi32_ps(gi);
        __m128 b = _mm_cvtepi32_ps(bi);

        // 0.299 * R + 0.587 * G + 0.114 * B
        __m128 gray = _mm_mul_ps(rc, r);
        gray = _mm_add_ps(gray, _mm_mul_ps(gc, g));
        gray = _mm_add_ps(gray, _mm_mul_ps(bc, b));

        // int化
        __m128i grayi = _mm_cvttps_epi32(gray);

        // RGB各々にgray値設定 (gray, 0, 0, 0) -> (gray, gray, gray, 0)
        __m128i d = _mm_add_epi32(grayi, _mm_slli_epi32(grayi, 8));
        d = _mm_add_epi32(d, _mm_slli_epi32(grayi, 16));

        _mm_store_si128((__m128i*)(dst.data() + i), d);
    }
    return dst;
}
f:id:any-programming:20170319005947j:plain f:id:any-programming:20170501160605j:plain


整数演算のみで計算

疑似的に小数演算が出来るよう、大きな定数倍した状態で計算し、最後にその定数で割ります。

定数が大きすぎるとオーバーフローが起きてしまうため、今回は適当に22bitのシフトで計算しています。

aligned_vector<byte> ToGray(const aligned_vector<byte>& src)
{
    aligned_vector<byte> dst(src.size());

    // 係数(小数の係数を定数倍)
    __m128i rc = _mm_set1_epi32((int)((1 << 22) * 0.299));
    __m128i gc = _mm_set1_epi32((int)((1 << 22) * 0.587));
    __m128i bc = _mm_set1_epi32((int)((1 << 22) * 0.114));

    __m128i mapR = _mm_setr_epi8(0, -1, -1, -1, 4, -1, -1, -1,  8, -1, -1, -1, 12, -1, -1, -1);
    __m128i mapG = _mm_setr_epi8(1, -1, -1, -1, 5, -1, -1, -1,  9, -1, -1, -1, 13, -1, -1, -1);
    __m128i mapB = _mm_setr_epi8(2, -1, -1, -1, 6, -1, -1, -1, 10, -1, -1, -1, 14, -1, -1, -1);
    for (int i = 0; i < (int)src.size(); i += 16)
    {
        __m128i s = _mm_load_si128((__m128i*)(src.data() + i));
        __m128i r = _mm_shuffle_epi8(s, mapR);
        __m128i g = _mm_shuffle_epi8(s, mapG);
        __m128i b = _mm_shuffle_epi8(s, mapB);

        __m128i gray = _mm_mullo_epi32(rc, r);
        gray = _mm_add_epi32(gray, _mm_mullo_epi32(gc, g));
        gray = _mm_add_epi32(gray, _mm_mullo_epi32(bc, b));

        // 定数倍されているものを、元に戻す
        gray = _mm_srli_epi32(gray, 22);
        
        __m128i d = _mm_add_epi32(gray, _mm_slli_epi32(gray, 8));
        d = _mm_add_epi32(d, _mm_slli_epi32(gray, 16));

        _mm_store_si128((__m128i*)(dst.data() + i), d);
    }
    return dst;
}


パフォーマンス

float版と整数版では、特に速度の差はありませんでした。

このレベルになるとメモリのバンド幅がボトルネックになるケースもあり、SSE化しなくともマルチスレッド化しただけで最速になることもあります。