Search code examples
javaandroidimage-processingrenderscriptandroid-renderscript

Android: dynamically create a mask


In my android app, I have a car from which the user can click and select different panels. The image is relatively complicated (as opposed to the one pasted here) so its difficult to overlay the buttons in the correct spots. In addition there are a lot of different images. The solution I would like to try:

(First image represents the colours used to determine which panel was clicked, second image represents the mask generated and the last image the 'result').

The only problem I'm having is: How do I dynamically create the mask? I thought of using a floodfill type method to create a new canvas with the 'mask' of the selected panels. But, I worry that it might be too computationally heavy. Any simpler suggestions? [my representation of the car images[1]

UPDATE: Ok so I've come pretty far. As expected, the creation of the mask too way too long (2-4 seconds for a small image). But, then I discovered RenderScripts!! I think I can still get this to work. The only little snag that I have now is: How do I pass in the colours that have been pressed?

My current code looks like this:

// create a bitmap for the mask.
ImageView img = (ImageView) findViewById (mask);
img.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(img.getDrawingCache());

// Create a tiny bitmap to store the colours of the panels that are 
//'selected'
Bitmap.Config conf = Bitmap.Config.ARGB_8888; // see other conf types
Bitmap myBitmap = Bitmap.createBitmap(pickedPanels.size(), 1, conf);
int [] myInts = new int[pickedPanels.size()];
for (int i = 0; i<pickedPanels.size(); i++){
    myInts[i] = pickedPanels.get(i).intValue();
}
myBitmap.setPixels(myInts, 0, myBitmap.getWidth(), 0, 0, 
myBitmap.getWidth(),0);

//Run thescript and set the output
final RenderScript rs = RenderScript.create(this);
final Allocation input = Allocation.createFromBitmap(rs, bitmap, 
Allocation.MipmapControl.MIPMAP_NONE,Allocation.USAGE_SCRIPT);
final Allocation output = Allocation.createTyped(rs, input.getType());
        final ScriptC_singlesource script = new 
ScriptC_singlesource(rs);
script.set_image(Allocation.createFromBitmap(rs, myBitmap, 
Allocation.MipmapControl.MIPMAP_NONE,Allocation.USAGE_SCRIPT));
script.set_imgWidth(pickedPanels.size());
script.forEach_root(input, output);
output.copyTo(bitmap);
img.setImageBitmap(bitmap);

ImageView destim = (ImageView) findViewById (dest);
destim.setDrawingCacheEnabled(true);
destim.setImageBitmap(bitmap);

and this is the script:

#pragma version(1)
#pragma rs java_package_name(za.co.overtake)

rs_allocation image;
int imgWidth;

uchar4 RS_KERNEL root(uchar4 in, uint32_t x, uint32_t y) {

for(int col = 0; col < imgWidth; col++){
    const uchar4 colour = *(const uchar4*)rsGetElementAt(image, col,0);
     if (in.r == colour.r && in.g == colour.g && in.b == colour.b){
        in.r = 255;
        in.g = 0;
        in.b = 0;
        break;
    } else {
       in.r = 0;
       in.g = 255;
       in.b = 0;
       rsDebug("HELLLLLP>>", colour);
    }

}
return in;
}

But, when I try and read the pixel values from myBitmap (or image in the script), RGB is always 0.

(Sorry for the bad naming, etc. I've been going crazy trying to figure this out)


Solution

  • Ok, finally got this figured out. In my renderscript code I have:

    #pragma version(1)
    #pragma rs java_package_name(za.co.overtake)
    
    int*reds;
    int*greens;    
    int*blues;
    int imgWidth;
    
    uchar4 RS_KERNEL root(uchar4 in, uint32_t x, uint32_t y) {
       bool colourme = false;
       for(int col = 0; col < imgWidth; col++){
    
            const int red = reds[col];
            const int green = greens[col];
            const int blue = blues[col];
    
             if (in.r == red && in.g == green && in.b == blue){
                colourme = true;
            } 
        }
         if (colourme) {
            in.r = 255;
            in.g = 0;
            in.b = 0;
            in.a = 50;
         } else {
             in.r = 0;
             in.g = 0;
             in.b = 0;
             in.a = 0;
         }
        return in;
    }
    

    Then in Java

    public void showDamagedPanels(int dest, int mask) {
    
        int noOfColours = pickedPanels.size();
        if (noOfColours > 0) {
            ImageView img = (ImageView) findViewById (mask);
            img.setDrawingCacheEnabled(true);
            Bitmap bitmap = Bitmap.createBitmap(img.getDrawingCache());
            img.setDrawingCacheEnabled(false);
    
            int [] reds = new int[noOfColours];
            int [] greens = new int[noOfColours];
            int [] blues = new int[noOfColours];
    
            for (int i = 0; i< noOfColours; i++){
                int colour = pickedPanels.get(i);
                reds[i] = (colour >> 16) & 0xFF;
                greens[i] = (colour >> 8) & 0xFF;
                blues[i] = (colour >> 0) & 0xFF;
            }
    
            final RenderScript rs = RenderScript.create(this);
            final Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);
            final Allocation output = Allocation.createTyped(rs, input.getType());
            final ScriptC_singlesource script = new ScriptC_singlesource(rs);
    
            Allocation red = Allocation.createSized(rs, Element.I32(rs), reds.length);
            red.copyFrom(reds);
            script.bind_reds(red);
    
            Allocation green = Allocation.createSized(rs, Element.I32(rs), greens.length);
            green.copyFrom(greens);
            script.bind_greens(green);
    
            Allocation blue = Allocation.createSized(rs, Element.I32(rs), blues.length);
            blue.copyFrom(blues);
            script.bind_blues(blue);
    
            script.set_imgWidth(pickedPanels.size());
            script.forEach_root(input, output);
            output.copyTo(bitmap);
    
            ImageView destim = (ImageView) findViewById (dest);
            destim.setDrawingCacheEnabled(true);
            destim.setImageBitmap(bitmap);
        } else {
            ImageView destim = (ImageView) findViewById (dest);
            destim.setImageBitmap(null);
        }
    
    
    }
    

    where dest is the overlay image and mask in the image acting as the mask. So basically, when a panel is clicked - place its colour in pickedPanels. Then call the showPanels method, which calls the script. The script checks the colours and sets the resulting image red or clear.

    Update: For anyone trying to use this, but having some issues: It is possible to do this without the renderscript code, but it does run a bit slower - though it has been ok in my case for small images.

    private Bitmap changeColor(Bitmap src, Set<Integer> pickedPanelsList) {
            int fine = getResources().getColor(R.color.colorAccent);
    
            int width = src.getWidth();
            int height = src.getHeight();
            int[] pixels = new int[width * height];
            // get pixel array from source
            src.getPixels(pixels, 0, width, 0, 0, width, height);
    
            Bitmap bmOut = Bitmap.createBitmap(width, height, src.getConfig());
    
            int AGood = 100, RGood = Color.red(fine), GGood = Color.green(fine), BGood = Color.blue(fine);
            int ABad = 100, RBad = Color.red(Color.RED), GBad = Color.green(Color.RED), BBad = Color.blue(Color.RED);
            int pixel;
    
            // iteration through pixels
            for (int y = 0; y < height; ++y) {
                for (int x = 0; x < width; ++x) {
                    // get current index in 2D-matrix
                    int index = y * width + x;
                    pixel = pixels[index];
                    if(pickedPanelsList.contains(pixel)){
                        pixels[index] = Color.argb(ABad, RBad, GBad, BBad);
                    } else if (Color.alpha(pixel) > 0){
                        pixels[index] = Color.argb(AGood, RGood, GGood, BGood);
                    }
                }
            }
            bmOut.setPixels(pixels, 0, width, 0, 0, width, height);
            return bmOut;
        }
    

    Here, the picked panel set is all the colours that should be coloured red (or chosen) and the bitmap is the mask (if I remember correctly, I did this a while ago). I've also found that doing a slight blur on the result makes the image look nicer - since it will obviously be less jagged.