Bitmap / ColourWheel / Custom Controls / RenderScript

Colour Wheel – Part 2

In Android Weekly issue # 297 there was a link to a library named ColorPickerPreference which I found interesting. One aspect of it was a colour picker which included a colour wheel but I was mildly disappointed to see that it did not include a mechanism of adjusting the brightness value, and was implemented using a bitmap resource which was only provided at a single density, meaning that it will not necessarily scale well on different devices. In this short series we’ll look at how we can actually render a colour wheel dynamically.

In the previous article we looked at the maths behind dynamically generating a colour wheel graphic but found that while it looked really nice, it actually took around 2 seconds to render on an Pixel XL. While we may be able to cope with this as a one-off, the fact that we need to re-generate the image in response to the brightness changing will give a really poor user experience.

The issue we have is that we have to perform a few floating point calculations for each pixel, and in a 896 x 896 pixel image this needs repeating over eight hundred thousand times. Floating point operations are computationally expensive on the CPU, and that is the cause of this 2 second rendering time. However, there is another processor on all modern Android devices which is much better suited to handling large numbers of floating point operations: the GPU.

The Graphics Processing Unit (GPU) is specifically designed to handle floating point operations unlike a CPU which is designed to be more general purpose. Whereas a CPU may have multiple cores, the cores in a GPU are much simpler (because they are more dedicated) and are named Arithmetic Logic Units (ALUs), and there are usually many more of them so they can handle a simultaneous calculations across all the ALUs. The Pixel XL has an Adreno 530 GPU which has 256 ALUs. That sounds perfect for speeding up our rendering speed, so how do we go about moving our calculation to the GPU? While we could use OpenGL, the better option in this case is to use RenderScript, which is a compute engine which runs on the GPU.

To enable renderscript for our project it is best to use the support library version and we need to enable it in our build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.stylingandroid.colourwheel"
        minSdkVersion 14
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        renderscriptTargetApi 14
        renderscriptSupportModeEnabled true

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

We will need to create a custom RenderScript kernel which will perform the calculation that we looked at previously. A kernel is a function that will be called for each individual pixel and will return the colour value for that pixel. The kernel will be executed in parallel for different pixels across all of the ALUs, so unlike or Java solution which calculated each pixel sequentially, the RenderScript kernels will execute in parallel.

The script itself is written in C:

#pragma version(1)
#pragma rs java_package_name(com.stylingandroid.colourwheel)

#include "hsv.rsh"

const static uchar4 transparent = {0, 0, 0, 0};

float centreX;
float centreY;
float radius;
float brightness = 1.0f;

void colourWheel(rs_script script, rs_allocation allocation, float brightness_value) {
    centreX = rsAllocationGetDimX(allocation) / 2.0f;
    centreY = rsAllocationGetDimY(allocation) / 2.0f;
    radius = min(centreX, centreY);
    brightness = brightness_value;
    rsForEach(script, allocation, allocation);
}

uchar4 RS_KERNEL root(uchar4 in, int32_t x, int32_t y) {
    uchar4 out;
    float xOffset = x - centreX;
    float yOffset = y - centreY;
    float centreOffset = hypot(xOffset, yOffset);
    if (centreOffset <= radius) {
        float centreAngle = fmod(degrees(atan2(yOffset, xOffset)) + 360.0f, 360.0f);
        float3 colourHsv;
        colourHsv.x = centreAngle;
        colourHsv.y = centreOffset / radius;
        colourHsv.z = brightness;
        out = rsPackColorTo8888(hsv2Argb(colourHsv, 255.0f));
    } else {
        out = transparent;
    }
    return out;
}

First need to make a couple of declarations. The first identifies the version of renderscript that we're using. This is currently fixed at 1 (line 1).

Next we need to specify the package name. The renderscript compilation will generate some JVM wrappers that we can call from our Java / Kotlin, and this is the package name the components will be created under (line 2).

We include a header file which contains a function that I converted from here to convert HSV colour values to ARGB (line 4).

Next we have some global variables that we can access throughout the script. We define a transparent colour consisting of four unsigned chars, one for each of the alpha, red, green, and blue components (line 6).

Next we create global variables to hold the centreX, centreY, radius, and brightness values which will be fixed for each pixel calculation (lines 8-11). While we could calculate these for each pixel, rather than repeat the same calculations over and over, it is far more efficient to calculate them up front and re-use them throughout.

Next is the colourWheel() function (lines 13-19) which is the main entry point to the script. It takes three arguments. The first is a reference to the script instance itself; the second is an allocation which is a block of memory which contains the bitmap data we are going to work on; and the third is the fixed brightness value for the colour wheel that we're going to generate, which gets stored to a global variable (line 17).

We get the x and y dimensions of the bitmap from the allocation and halve these to get the centreX and centreY values (lines 14 & 15), then we get the minimum of these to determine the radius (line 16). Now re can actually run the kernel which is done by calling the rsForEach() function (line 18). This takes the script reference, and an input and output allocation. In many cases we'll want to preform some kind of processing on an input image to produce a different output image, however in this case we are generating the output independently of any input, so we use the same allocation for both input and output.

The kernel itself is the root() method (lines 21-37) and this name cannot be changed. The rsForEach() function will find this function from the rs_script instance that was passed in an invoke it for each pixel. It will then iterate through the pixels in the input allocation, invoke the kernel for each, and write the return value of the kernel to the corresponding pixel in the output allocation. In our case we use the same allocation, and it will overwrite whatever may be already there.

The actual body of the root() function is functionally identical to the code block inside the x and y loops from the Kotlin sample. We determine the x and y offsets from the centre (lines 23 & 24), then use Pythagoras' Theorem to determine the centreOffset, then we check if the pixel falls within the radius (line 26). If it does, we determine the angle θ by getting the arctan of the opposite divided by the adjacent, then converting to degrees, and correcting to ensure we get a value from 0° and 360° (line 27). Then we set the hue, saturation, and brightness values (lines 29-31). Finally we convert the HSV values back to ARGB, and then we need to use rsPackColorTo8888() to convert the float4 (equivalent to a Java float[4]) returned by hsv2Argb() to a uchar4 (equivalent to a Java byte[4]) which is the required output format.

If the pixel falls outside the radius then the value is set to the transparent colour that we defined earlier (line 34).

Finally we return the new value (line 36), and this will be written to the corresponding pixel in the output allocation.

So that's our RenderScript script, but calling it from Kotlin requires a little bit of effort, and we'll cover that part in the concluding article in this series.

The code that we have will compile, but the RenderScript won't actually be invoked, so we won't get the benefits just yet. Nonetheless the source code is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Hey Mark, Big Fan!! I guess since 2012…

    I wrote an renderscript script to get an HSV wheel 3 years back.

    https://github.com/SandeepDhull1990/HSVWheelView-Android/blob/master/app/src/main/rs/file.rs

    It was giving me problems on Moto E and some other low end devices at the that time, but it used to run fine on Moto g, and nexus devices..

    https://stackoverflow.com/questions/28636045/renderscript-generating-corrupted-result-without-using-cpu-driver-on-some-device

    I resolved the issue on Moto E by using an command from this above question.
    I am yet not clear why I was facing issue. Please explain

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.