Ефективно XOR две изображения в целта за компилиране на Flash

Трябва да направя XOR два BitmapData обекта заедно.

Пиша на Haxe, използвайки библиотеките flash.* и целта за компилиране на AS3.

Проучих HxSL и PixelBender и нито един от тях изглежда няма побитов XOR оператор, нито имат други побитови оператори, които биха могли да се използват за създаване на XOR (но пропускам ли нещо очевидно? Бих приел всеки отговор, който дава начин за извършване на побитово XOR, като се използват само целочислени/плаващи оператори и функции, налични в HxSL или PixelBlender).

Нито един от предварително дефинираните филтри или шейдъри във Flash, които мога да намеря, изглежда не може да направи XOR на две изображения (но отново, пропускам ли нещо очевидно? Може ли XOR да се направи с комбинация от други филтри).

Не мога да намеря нищо подобно на режим на рисуване XOR за рисуване на неща върху други неща (но това не означава, че не съществува! Това също ще работи, ако съществува!)

Единственият начин, който мога да намеря в момента, е цикъл пиксел по пиксел върху изображението, но това отнема няколко секунди на изображение дори на бърза машина, за разлика от филтрите, които използвам за другите си операции за обработка на изображения , които са около сто пъти по-бързи.

Има ли някакъв по-бърз метод?


person Dewi Morgan    schedule 04.09.2014    source източник
comment
Имайте предвид, че питам за BITWISE XOR тук: същото нещо, което често се използва за показване на контури на четки, създаване на XOR маски на японски цензурни ленти, е налично като режим на рисуване в Java и т.н.   -  person Dewi Morgan    schedule 07.09.2014
comment
Колко големи са изображенията, които обработвате и как правите XOR на пиксел в момента? Тествах XORing две изображения 4160x1440 и моето решение на ActionScript е ~800ms, а PixelBlender е ~6700ms.   -  person Sly_cardinal    schedule 07.09.2014
comment
Отнема две секунди за XOR на пиксел на изображение с размери 2,5k x 2,5k пиксела, в AS3 с цикъл върху BitmapData. Опростена, показваща само частта XOR: for (y = 0; y ‹ input.height; y++) { for (x = 0; x ‹ input.height; y++) { var source_argb:Int = input.getPixel32(x, y ); var crypt_rgb:Int = crypt.getPixel(x, y); bmd.setPixel32(x, y, (argb & 0xff000000) /* Запазване на алфа / | ((source_argb ^ crypt_rgb) & 0x00ffffff) / Побитово XOR */ ); } } Какъв алгоритъм използвате, за да фалшифицирате побитово XOR в PB?   -  person Dewi Morgan    schedule 09.09.2014


Отговори (2)


Редактиране:

Играейки си с това още малко, открих, че премахването на условния и допълнителен векторен достъп в цикъла го ускорява с около 100 ms на моята машина.

Ето предишния XOR цикъл:

// Original Vector XOR code:
for (var i: int = 0; i < len; i++) {
    // XOR.
    result[i] = vec1[i] ^ vec2[i];

    if (ignoreAlpha) {
        // Force alpha of FF so we can see the result.
        result[i] |= 0xFF000000;
    }
}

Ето актуализирания XOR цикъл за векторното решение:

if (ignoreAlpha) {
    // Force alpha of FF so we can see the result.
    alphaMask = 0xFF000000;
}

// Fewer Vector accessors makes it quicker:
for (var i: int = 0; i < len; i++) {
    // XOR.
    result[i] = alphaMask | (vec1[i] ^ vec2[i]);
}

Отговор:

Ето решенията, които съм тествал за XOR две изображения във Flash.

Открих, че решението на PixelBender е около 6-10 по-бавно от това да го правиш в чист ActionScript.

Не знам дали това е защото имам бавен алгоритъм или това са просто ограниченията на опитите за фалшиви побитови операции в PixelBender.

Резултати:

  • PixelBender: ~6500ms
  • BitmapData.getVector(): ~480-500ms
  • BitmapData.getPixel32(): ~1200ms
  • BitmapData.getPixels(): ~1200ms

Ясният победител е използването на BitmapData.getVector() и след това XOR двата потока от пикселни данни.


1. Решение PixelBender

Ето как внедрих побитовото XOR в PixelBender въз основа на формулата, дадена в Wikipedia: http://en.wikipedia.org/wiki/Bitwise_operation#Mathematical_equivalents

Ето същност на окончателния PBK: https://gist.github.com/Coridyn/67a0ff75afaa0163f673

На моята машина, изпълняваща XOR на две изображения с размери 3200x1400, това отнема около 6500-6700ms.

Първо преобразувах формулата в JavaScript, за да проверя дали е правилна:

// Do it for each RGBA channel.
// Each channel is assumed to be 8bits.
function XOR(x, y){
    var result = 0;
    var bitCount = 8;   // log2(x) + 1
    for (var n = 0; n < bitCount; n++) {
        var pow2 = pow(2, n);

        var x1 = mod(floor(x / pow2), 2);
        var y1 = mod(floor(y / pow2), 2);

        var z1 = mod(x1 + y1, 2);
        result += pow2 * z1;
    }

    console.log('XOR(%s, %s) = %s', x, y, result);
    console.log('%s ^ %s = %s', x, y, (x ^ y));

    return result;
}

// Split out these functions so it's
// easier to convert to PixelBender.
function mod(x, y){
    return x % y;
}

function pow(x, y){
    return Math.pow(x, y);
}

function floor(x){
    return Math.floor(x);
}

Потвърдете, че е правилно:

// Test the manual XOR is correct.
XOR(255, 85);   // 170
XOR(170, 85);   // 255
XOR(170, 170);  // 0

След това преобразувах JavaScript в PixelBender, като развих цикъла, използвайки поредица от макроси:

// Bitwise algorithm was adapted from the "mathematical equivalents" formula on Wikipedia:
// http://en.wikipedia.org/wiki/Bitwise_operation#Mathematical_equivalents

// Macro for 2^n (it needs to be done a lot).
#define POW2(n) pow(2.0, n)

// Slight optimisation for the zeroth case - 2^0 = 1 is redundant so remove it.
#define XOR_i_0(x, y) ( mod( mod(floor(x), 2.0) + mod(floor(y), 2.0), 2.0 ) )
// Calculations for a given "iteration".
#define XOR_i(x, y, i) ( POW2(i) * ( mod( mod(floor(x / POW2(i)), 2.0) + mod(floor(y / POW2(i)), 2.0), 2.0 ) ) )

// Flash doesn't support loops.
// Unroll the loop by defining macros that call the next macro in the sequence.
// Adapted from: http://www.simppa.fi/blog/category/pixelbender/
// http://www.simppa.fi/source/LoopMacros2.pbk
#define XOR_0(x, y) XOR_i_0(x, y)
#define XOR_1(x, y) XOR_i(x, y, 1.0) + XOR_0(x, y)
#define XOR_2(x, y) XOR_i(x, y, 2.0) + XOR_1(x, y)
#define XOR_3(x, y) XOR_i(x, y, 3.0) + XOR_2(x, y)
#define XOR_4(x, y) XOR_i(x, y, 4.0) + XOR_3(x, y)
#define XOR_5(x, y) XOR_i(x, y, 5.0) + XOR_4(x, y)
#define XOR_6(x, y) XOR_i(x, y, 6.0) + XOR_5(x, y)
#define XOR_7(x, y) XOR_i(x, y, 7.0) + XOR_6(x, y)

// Entry point for XOR function.
// This will calculate the XOR the current pixels.
#define XOR(x, y) XOR_7(x, y)

// PixelBender uses floats from 0.0 to 1.0 to represent 0 to 255
// but the bitwise operations above work on ints.
// These macros convert between float and int values.
#define FLOAT_TO_INT(x) float(x) * 255.0
#define INT_TO_FLOAT(x) float(x) / 255.0

XOR за всеки канал на текущия пиксел във функцията evaluatePixel:

void evaluatePixel()
{
    // Acquire the pixel values from both images at the current location.
    float4 frontPixel = sampleNearest(inputImage, outCoord());
    float4 backPixel = sampleNearest(diffImage, outCoord());

    // Set up the output variable - RGBA.
    pixel4 result = pixel4(0.0, 0.0, 0.0, 1.0);

    // XOR each channel.
    result.r = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.r), FLOAT_TO_INT(backPixel.r)) );
    result.g = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.g), FLOAT_TO_INT(backPixel.g)) );
    result.b = INT_TO_FLOAT ( XOR(FLOAT_TO_INT(frontPixel.b), FLOAT_TO_INT(backPixel.b)) );

    // Return the result for this pixel.
    dst = result;
}

Решения на ActionScript

2. BitmapData.getVector()

Открих, че най-бързото решение е да се извлекат Vector пиксела от двете изображения и да се извърши XOR в ActionScript.

За същите два 3200x1400 това отнема около 480-500ms.

package diff
{
    import flash.display.Bitmap;
    import flash.display.DisplayObject;
    import flash.display.IBitmapDrawable;
    import flash.display.BitmapData;
    import flash.geom.Rectangle;
    import flash.utils.ByteArray;


    /**
     * @author Coridyn
     */
    public class BitDiff
    {

        /**
         * Perform a binary diff between two images.
         * 
         * Return the result as a Vector of uints (as used by BitmapData).
         * 
         * @param   image1
         * @param   image2
         * @param   ignoreAlpha
         * @return
         */
        public static function diffImages(image1: DisplayObject,
                                          image2: DisplayObject,
                                          ignoreAlpha: Boolean = true): Vector.<uint> {

            // For simplicity get the smallest common width and height of the two images
            // to perform the XOR.
            var w: Number = Math.min(image1.width, image2.width);
            var h: Number = Math.min(image1.height, image2.height);
            var rect: Rectangle = new Rectangle(0, 0, w, h);

            var vec1: Vector.<uint> = BitDiff.getVector(image1, rect);
            var vec2: Vector.<uint> = BitDiff.getVector(image2, rect);

            var resultVec: Vector.<uint> = BitDiff.diffVectors(vec1, vec2, ignoreAlpha);
            return resultVec;
        }


        /**
         * Extract a portion of an image as a Vector of uints.
         * 
         * @param   drawable
         * @param   rect
         * @return
         */
        public static function getVector(drawable: DisplayObject, rect: Rectangle): Vector.<uint> {
            var data: BitmapData = BitDiff.getBitmapData(drawable);
            var vec: Vector.<uint> = data.getVector(rect);
            data.dispose();
            return vec;
        }


        /**
         * Perform a binary diff between two streams of pixel data.
         * 
         * If `ignoreAlpha` is false then will not normalise the 
         * alpha to make sure the pixels are opaque.
         * 
         * @param   vec1
         * @param   vec2
         * @param   ignoreAlpha
         * @return
         */
        public static function diffVectors(vec1: Vector.<uint>,
                                           vec2: Vector.<uint>,
                                           ignoreAlpha: Boolean): Vector.<uint> {

            var larger: Vector.<uint> = vec1;
            if (vec1.length < vec2.length) {
                larger = vec2;
            }

            var len: Number = Math.min(vec1.length, vec2.length),
                result: Vector.<uint> = new Vector.<uint>(len, true);

            var alphaMask = 0;
            if (ignoreAlpha) {
                // Force alpha of FF so we can see the result.
                alphaMask = 0xFF000000;
            }

            // Assume same length.
            for (var i: int = 0; i < len; i++) {
                // XOR.
                result[i] = alphaMask | (vec1[i] ^ vec2[i]);
            }

            if (vec1.length != vec2.length) {
                // Splice the remaining items.
                result = result.concat(larger.slice(len));
            }

            return result;
        }

    }

}

3. BitmapData.getPixel32()

Текущият ви подход на зацикляне на BitmapData с BitmapData.getPixel32() даде подобна скорост от около 1200ms:

for (var y: int = 0; y < h; y++) {
    for (var x: int = 0; x < w; x++) {
        sourcePixel = bd1.getPixel32(x, y);
        resultPixel = sourcePixel ^ bd2.getPixel(x, y);
        result.setPixel32(x, y, resultPixel);
    }
}

4. BitmapData.getPixels()

Последният ми тест беше да опитам да повторя две ByteArray пикселни данни (много подобно на Vector решението по-горе). Това внедряване също отне около 1200 ms:

/**
 * Extract a portion of an image as a Vector of uints.
 * 
 * @param   drawable
 * @param   rect
 * @return
 */
public static function getByteArray(drawable: DisplayObject, rect: Rectangle): ByteArray {
    var data: BitmapData = BitDiff.getBitmapData(drawable);
    var pixels: ByteArray = data.getPixels(rect);
    data.dispose();
    return pixels;
}


/**
 * Perform a binary diff between two streams of pixel data.
 * 
 * If `ignoreAlpha` is false then will not normalise the 
 * alpha to make sure the pixels are opaque.
 * 
 * @param   ba1
 * @param   ba2
 * @param   ignoreAlpha
 * @return
 */
public static function diffByteArrays(ba1: ByteArray,
                                      ba2: ByteArray,
                                      ignoreAlpha: Boolean): ByteArray {

    // Reset position to start of array.
    ba1.position = 0;
    ba2.position = 0;

    var larger: ByteArray = ba1;
    if (ba1.bytesAvailable < ba2.bytesAvailable) {
        larger = ba2;
    }

    var len: Number = Math.min(ba1.length / 4, ba2.length / 4),
        result: ByteArray = new ByteArray();

    // Assume same length.
    var resultPixel:uint;
    for (var i: uint = 0; i < len; i++) {
        // XOR.
        resultPixel = ba1.readUnsignedInt() ^ ba2.readUnsignedInt();
        if (ignoreAlpha) {
            // Force alpha of FF so we can see the result.
            resultPixel |= 0xFF000000;
        }

        result.writeUnsignedInt(resultPixel);
    }

    // Seek back to the start.
    result.position = 0;
    return result;
}
person Sly_cardinal    schedule 09.09.2014
comment
И таз добра. Много благодаря. Това е ДАЛЕЧ, ДАЛЕЧ ОТВЪД това, което поисках, и е може би най-добрият и най-задълбочен отговор, който съм виждал при препълването на стека някога. Не очаквах нещо подобно. Бих бил напълно щастлив само с указател към тази уики страница. Но вместо това, освен че ми дадохте този ресурс, вие също свършихте ЦЯЛАТА тежка работа вместо мен, И начертахте набор от други възможни подходи, И ме научихте на купища неща относно писането на четливи, кратки, но сложни шейдъри в процеса. Очевидно това е правилният отговор. - person Dewi Morgan; 10.09.2014
comment
Дори да бях озвучителят, нямаше да ви крещя, ако изпуснете микрофона в края на този пост. - person Dewi Morgan; 10.09.2014
comment
Няма за какво, беше забавно и нещо малко по-различно за изпробване - никога не подценявайте силата на отлагането :P - person Sly_cardinal; 10.09.2014

Има няколко възможни опции в зависимост от това какво искате да постигнете (напр. дали XOR за канал или е просто всеки пиксел, който не е черен?).

  1. Има BitmapData.compare( ) метод, който може да ви даде много информация за двете растерни изображения. Можете да BitmapData.threshold() входните данни преди сравнение.

  2. Друг вариант е да използвате метод за рисуване с BlendMode .DIFFERENCE режим на смесване, за да изчертаете вашите две изображения в едно и също BitmapData копие. Това ще ви покаже разликата между двете изображения (еквивалентно на режима на смесване Difference във Photoshop).

  3. Ако трябва да проверите дали някой пиксел не е черен, можете да опитате да стартирате BitmapData.threshold първо и след това изчертайте резултата с режима на смесване на разликата, както по-горе за двете изображения.

Правите това за обработка на изображения или нещо друго като откриване на попадение на пиксел?

Като начало бих погледнал BitmapData и вижте какво е налично за игра.

person Sly_cardinal    schedule 04.09.2014
comment
Предполагам, че мислите за черното като 0 и всичко останало като 1 и идеята ви за XOR филтър е да покажете нещо, където някое от изображенията има нечерна стойност. За съжаление филтрите XOR са по-сложни: побитови, без загуби и обратими. XOR изображение с ключово изображение два пъти и ще получите обратно оригиналното си изображение. Например основно кодиране на изображение или рисуване на фигура върху изображение, което може да искате да премахнете отново. - person Dewi Morgan; 07.09.2014