Master Mobile-Friendly Image Manipulation: Emboss Images Using Android’s Renderscript API

Share this article

During your Android development career, you may have to write apps that blur, sharpen, convert to grayscale, emboss, or otherwise process images. Even if your latest app isn’t necessarily focused on manipulating imagery, you can still employ interface design techniques such as blurring background content to focus the user on important alerts in the foreground, or using grayscale to provide contrast between available and unavailable options within the interface. To perform these operations quickly while remaining portable, your Android app must use Renderscript. This article focuses on embossing images via Renderscript. It first introduces you to the embossing algorithm, then it presents a Renderscript primer, and finally it shows you how to build a simple app that embosses an image in a Renderscript context.

Understanding the Embossing Algorithm

Books written in Braille, documents sealed with hot wax to guarantee their authenticity, and coins illustrate embossing, the act of raising content in relief from a surface. Embossing helps the blind read via the Braille system of raised dots, and it provides authentication for stamped, sealed documents or minted coins. (In the past, these seals guaranteed the weights and values of precious metals used in the coins.) Along with these real-world uses, embossing also lends a three-dimensional chiseled look to images. Figure 1 presents an example. Figure 1: An embossed image also appears somewhat metallic. Think of an image as mountainous terrain. Each pixel represents an elevation: brighter pixels represent higher elevations. When an imaginary light source shines down on this terrain, the “uphills” that face the light source are lit, while the “downhills” that face away from the light source are shaded. An embossing algorithm captures this information. The algorithm scans an image in the direction that a light ray is moving. For example, if the light source is located to the image’s left, its light rays move from left to right, so the scan proceeds from left to right. During the scan, adjacent pixels (in the scan direction) are compared. The difference in intensities is represented by a specific level of gray (from black, fully shaded, to white, fully lit) within the destination image. There are eight possible scanning directions: left-to-right, right-to-left, top-to-bottom, bottom-to-top, and four diagonal directions. To simplify the code, the algorithm usually scans left-to-right and top-to-bottom, and chooses its neighbors as appropriate. For example, if the light source is above and to the left of the image, the algorithm would compare the pixel above and to the left of the current pixel with the current pixel during the left-to-right and top-to-bottom scan. The comparison is demonstrated in Listing 1’s embossing algorithm pseudocode:
FOR row = 0 TO height-1
    FOR column = 0 TO width-1
        SET current TO src.rgb[row]

        SET upperLeft TO 0
        IF row > 0 AND column > 0
           SET upperLeft TO
               src.rgb[row-1][column-1]

        SET redIntensityDiff TO
            red (current)-red (upperLeft)
        SET greenIntensityDiff TO
            green (current)-green (upperLeft)
        SET blueIntensityDiff TO
            blue (current)-blue (upperLeft)

        SET diff TO redIntensityDiff
        IF ABS(greenIntensitydiff) > ABS(diff)
           SET diff TO greenIntensityDiff
        IF ABS(blueIntensityDiff) > ABS(diff)
           SET diff TO blueIntensityDiff

        SET grayLevel TO
            MAX(MIN(128+diff, 255), 0)

        SET dst.rgb[row] TO grayLevel
    NEXT column
NEXT row
Listing 1: This algorithm embosses an image in a left-to-right and top-to-bottom manner. Listing 1’s embossing pseudocode identifies the image to be embossed as src and identifies the image to store the results of the embossing as dst. These images are assumed to be rectangular buffers of RGB pixels (rectangular buffers of RGBA pixels where the alpha component is ignored could be accommodated as well). Because pixels in the topmost row don’t have neighbors above them, and because pixels in the leftmost column don’t have neighbors to their left, there is a bit of unevenness along the top and left edges of the embossed image, as shown in Figure 1. You can eliminate this unevenness by drawing a solid border around the image. After obtaining the red, green, and blue intensities for each of the current pixels and their respective upper-left neighbors, the pseudocode calculates the difference in each intensity. The upper-left neighbor’s intensities are subtracted from the current pixel’s intensities because the light ray is moving in an upper-left to lower-right direction. The pseudocode identifies the greatest difference (which might be negative) between the current pixel and its upper-left neighbor’s three intensity differences. That’s done to obtain the best possible “chiseled” look. The difference is then converted to a level of gray between 0 and 255, and that gray level is stored in the destination image at the same location as the current pixel in the source image.

Renderscript Primer

Renderscript combines a C99-based language with a pair of Low Level Virtual Machine (LLVM) compilers, and a runtime that provides access to graphics and compute engines. You can use Renderscript to write part of your app in native code to improve 3D graphics rendering and/or data processing performance, all while keeping your app portable and avoiding use of the tedious Java Native Interface.
Note: Google introduced Renderscript in Android 2.0, but did not make an improved version publicly available until the Android 3.0 release. Because of subsequent developer feedback that favored working directly with OpenGL, Google deprecated the graphics engine portion of Renderscript starting with Android 4.1.
Embossing and other image processing operations are performed with Renderscript’s compute engine. Although you could leverage Android’s GPU-accelerated (as of Android 3.0) 2D Canvas API to perform these 2D tasks, Renderscript lets you move beyond the Dalvik virtual machine, write faster native code, and run this code via multiple threads on available CPU cores (and GPU and DSP cores in the future). When introducing Renderscript to your app, you will need to write Java code and C99-based Renderscript code, which is known as a script. The Java code works with types located in the android.renderscript package. At minimum, the types you will need to access are Renderscript (for getting a context) and Allocation (for allocating memory on behalf of Renderscript):
  • Renderscript declares a static RenderScript create(Context ctx) method that is passed a reference to your current activity, and that returns a reference to a Renderscript object. This object serves as a context that must be passed to other APIs.
  • Allocation declares methods for wrapping data on the Java side and binding this data to the Renderscript runtime, so that the script can access the data. Methods are also available for copying script results back to the Java side.
During the Android build process, LLVM’s front-end compiler compiles the script and creates a ScriptC_-prefixed class whose suffix is taken from the script file’s name. Your app instantiates this class and uses the resulting instance to initialize and invoke the script, and to retrieve script results. You don’t have to wait for script results because Renderscript takes care of this for you. The C99 code is typically stored in a single file that is organized as follows:
  • A pair of #pragma directives must be specified and typically appear at the top of the file. One #pragma identifies the Renderscript version number (currently 1) to which the file conforms. The other #pragma identifies the Java package of the app to which the file associates.
  • A pair of rs_allocation directives typically follow and identify the input and output allocations. The input allocation refers to the data that is to be processed (and which is wrapped by one Allocation instance), and the output allocation identifies where results are to be stored (and is wrapped by the other Allocation instance).
  • An rs_script directive typically follows and identifies the ScriptC_-prefixed instance on the Java side, so that Renderscript can communicate results back to the script.
  • Additional global variables can be declared that are either local to the script or bound to the Java code. For example, you could declare int width; and int height;, and pass an image’s width and height from your Java code (via calls to set_-prefixed methods on the ScriptC_-prefixed object) to these variables.
  • root() function typically follows that is executed on each CPU core for each element in the input allocation.
  • A noargument init function typically follows that performs any necessary initialization and invokes one of the overloaded rsForEach() functions. This function is responsible for identifying the number of cores, creating threads that run on these cores, and more. Each thread invokes root().
You can place these items wherever you want in the file. However, rs_allocation-based, rs_script-based, and any other global variables must be declared before they are referenced.

Embossing Meets Renderscript

Now that you have a basic understanding of the embossing algorithm and Renderscript, let’s bring them together. This section first introduces you to the Java and C99 sides of an app (a single activity and an embossing script) that demonstrates embossing via Renderscript, and then shows you how to build this app.

Exploring the Java Side of an Embossing App

Listing 2 presents the EmbossImage activity class:
package ca.tutortutor.embossimage;

import android.app.Activity;

import android.os.Bundle;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import android.renderscript.Allocation;
import android.renderscript.RenderScript;

import android.view.View;

import android.widget.ImageView;

public class EmbossImage extends Activity
{
   boolean original = true;

   @Override
   public void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      final ImageView iv = new ImageView(this);
      iv.setImageResource(R.drawable.leopard);
      setContentView(iv);
      iv.setOnClickListener(new View.OnClickListener()
                   {
                      @Override
                      public void onClick(View v)
                      {
                         if (original)
                            drawEmbossed(iv, R.drawable.leopard);
                              else
                                iv.setImageResource(R.drawable.leopard);
                              original = !original;
                         }
                });
   }

   private void drawEmbossed(ImageView iv, int imID)
   {
      Bitmap bmIn = BitmapFactory.decodeResource(getResources(), imID);
      Bitmap bmOut = Bitmap.createBitmap(bmIn.getWidth(), bmIn.getHeight(),
                                         bmIn.getConfig());
      RenderScript rs = RenderScript.create(this);
      Allocation allocIn;
      allocIn = Allocation.createFromBitmap(rs, bmIn,
                              Allocation.MipmapControl.MIPMAP_NONE,
                              Allocation.USAGE_SCRIPT);
      Allocation allocOut = Allocation.createTyped(rs, allocIn.getType());
      ScriptC_emboss script = new ScriptC_emboss(rs, getResources(),
                                                 R.raw.emboss);
      script.set_in(allocIn);
      script.set_out(allocOut);
      script.set_script(script);
      script.invoke_filter();
      allocOut.copyTo(bmOut);
      iv.setImageBitmap(bmOut);
   }
}
Listing 2: The embossing app’s activity communicates with the embossing script via script. The overriding void onCreate(Bundle bundle) method is responsible for setting up the activity’s user interface, which consists of a single android.widget.ImageView instance to which a click listener is registered. When the user clicks this view, either the embossed or the original version of this image is displayed, as determined by the current value of boolean variable original. Communication with the script occurs in the void drawEmbossed(ImageView iv, int imID) method. This method first obtains an android.graphics.Bitmap object containing the contents of the image to be embossed, and creates a Bitmap object containing an empty bitmap with the same dimensions and configuration as the other object’s bitmap. Next, a Renderscript context is obtained by executing RenderScript.create(this). The resulting Renderscript object is passed as the first argument to each of the two Allocation factory methods along with the ScriptC_emboss constructor. This context is analogous to the android.content.Context argument passed to widget constructors such as ImageView(Context context). The Dalvik virtual machine and surrounding framework control memory management. Objects and arrays are always allocated on the Java side and subsequently made available to the script by wrapping them in Allocation objects. This class provides several factory methods for returning Allocation instances, including the following pair:
  • static Allocation createFromBitmap(RenderScript rs, Bitmap b, Allocation.MipmapControl mips, int usage) creates an allocation that wraps a Bitmap instance with specific mipmap behavior and usage. bmIn is passed to b identifying the input Bitmap instance (containing the image) as the instance to be wrapped.Allocation.MipmapControl.MIPMAP_NONE is passed to mips, indicating that no mipmaps are used. Allocation.USAGE_SCRIPT is passed to usage, indicating that the allocation is bound to and accessed by the script.
  • Allocation createTyped(RenderScript rs, Type type) creates an allocation for use by the script with the size specified by type and no mipmaps generated by default.allocIn.getType() is passed to type, indicating that the layout for the memory to be allocated is specified by the previously created input allocation.
Following creation of the input and output allocations, the ScriptC_emboss class (created by LLVM during the app build process) is instantiated by invoking ScriptC_emboss(rs, getResources(), R.raw.emboss). Along with the previously created Renderscript context, the following arguments are passed to the constructor:
  • a reference to an android.content.res.Resources object (for accessing app resources). This reference is returned by invoking Context‘s Resources getResources() method.
  • R.raw.emboss (the resource ID of the embossing script). At build time, the LLVM front-end compiler compiles the script into a file (with a .bc extension) containing portable bitcode that is stored in the APK under the res/raw hierarchy. At runtime, the LLVM back-end compiler extracts this file and compiles it into device-specific code (unless the file has been compiled and cached on the device).
The LLVM front-end compiler generates a set_
-prefixed method for each non-static variable that it finds in the script. Because the embossing script contains two rs_allocation directives (named in and out) and an rs_script directive (named script), set_in()set_out(), and set_script() methods are generated and used to pass allocInallocOut, and script to the script. A script is executed by invoking an invoke_-prefixed method on an instance of the ScriptC_-prefixed class. The suffix for this method is taken from the name of the noargument init function (with a void return type) that appears in the script. In this case, that name is filter. Because Renderscript handles threading issues, allocOut.copyTo(bmOut); can be executed immediately after the invocation, to copy the script results to the output bitmap, and the resulting image is subsequently displayed via iv.setImageBitmap(bmOut);.

Exploring the C99 Side of an Embossing App

Listing 3 presents the embossing script.
#pragma version(1)
#pragma rs java_package_name(ca.tutortutor.embossimage)

rs_allocation out;
rs_allocation in;

rs_script script;

void root(const uchar4* v_in, uchar4* v_out, const void* usrData, uint32_t x,
          uint32_t y)
{
   float4 current = rsUnpackColor8888(*v_in);
   float4 upperLeft = { 0, 0, 0, 0 };
   if (x > 0 && y > 0)
      upperLeft = rsUnpackColor8888(*(uchar*) rsGetElementAt(in, x-1, y-1));
   float rDiff = current.r-upperLeft.r;
   float gDiff = current.g-upperLeft.g;
   float bDiff = current.b-upperLeft.b;
   float diff = rDiff;
   if (fabs(gDiff) > fabs(diff))
      diff = gDiff;
   if (fabs(bDiff) > fabs(diff))
      diff = bDiff;
   float grayLevel = fmax(fmin(0.5f+diff, 1.0f), 0);
   current.r = grayLevel;
   current.g = grayLevel;
   current.b = grayLevel;
   *v_out = rsPackColorTo8888(current.r, current.g, current.b, current.a);
}

void filter()
{
   rsDebug("RS_VERSION = ", RS_VERSION);
#if !defined(RS_VERSION) || (RS_VERSION < 14)
   rsForEach(script, in, out, 0);
#else
   rsForEach(script, in, out);
#endif
}
Listing 3: The filter() function is the embossing script’s entry point. The script begins with a pair of #pragmas that identify 1 as the Renderscript version to which the script targets and ca.tutortutor.embossimage as the APK package. A pair of rs_allocation globals and an rs_script global follow, providing access to their Java counterparts (passed to the script via the previously mentioned set_-prefixed method calls). The subsequently declared void root(const uchar4* v_in, uchar4* v_out, const void* usrData, uint32_t x, uint32_t y) function is invoked for each pixel (known as an element within this function) and on each available core. (Each element is processed independently of the others.) It declares the following parameters:
  • v_in contains the address of the current input allocation element being processed.
  • v_out contains the address of the equivalent output allocation element.
  • usrData contains the address of additional data that can be passed to this function. No additional data is required for embossing.
  • x and y identify the zero-based location of the element passed to v_in. For a 1D allocation, no argument would be passed to y. For a bitmap, arguments are passed to both parameters.
The root() function implements Listing 1’s embossing algorithm for the current input element, which is a 32-bit value denoting the element’s red, green, blue, and alpha components. This element is unpacked into a four-element vector of floating-point values (each between 0.0 and 1.0) via Renderscript’s float4 rsUnpackColor8888(uchar4 c) function. Next, Renderscript’s const void* rsGetElementAt(rs_allocation, uint32_t x, uint32_t y) function is called to return the element at the location passed to x and y. Specifically, the element to the northwest of the current element is obtained, and then unpacked into a four-element vector of floating-point values. Renderscript provides fabs()fmax(), and fmin() functions that are equivalent to their Listing 1 algorithm counterparts. These functions are called in subsequent logic. Ultimately, Renderscript’s uchar4 rsPackColorTo8888(float r, float g, float b, float a) function is called to pack the embossed element components into a 32-bit value that is assigned to*v_out.
Note: For a complete list of Renderscript functions, check out the online documentation or the .rsh files in your Android SDK installation.
The script concludes with the void filter() function. The name is arbitrary and corresponds to the suffix attached to the ScriptC_
-prefixed class’s invoke_-prefixed method. This function performs any needed initialization and executes the root() function on multiple cores, by calling one of Renderscript’s overloaded rsForEach() functions. In lieu of initialization, this function invokes one of Renderscript’s overloaded rsDebug() functions, which is useful for outputting information to the device log, to be accessed by invoking adb logcat at the command line, or from within Eclipse. In this case, rsDebug() is used to output the value of the RS_VERSION constant. filter() uses an #if#else#endif construct to choose an appropriate rsForEach() function call to compile based on whether the script is being compiled in the context of the Android SDK or Eclipse. RS_VERSION can contain a different value in each of these compiling contexts, and this value determines which overloaded rsForEach() functions are legal to call, as follows:
00134 #if !defined(RS_VERSION) || (RS_VERSION < 14)
00135 extern void __attribute__((overloadable))
00136     rsForEach(rs_script script, rs_allocation input,
00137               rs_allocation output, const void * usrData,
00138               const rs_script_call_t *sc);
00142 extern void __attribute__((overloadable))
00143     rsForEach(rs_script script, rs_allocation input,
00144               rs_allocation output, const void * usrData);
00145 #else
00146
00165 extern void __attribute__((overloadable))
00166     rsForEach(rs_script script, rs_allocation input, rs_allocation output,
00167               const void * usrData, size_t usrDataLen, const rs_script_call_t *);
00171 extern void __attribute__((overloadable))
00172     rsForEach(rs_script script, rs_allocation input, rs_allocation output,
00173               const void * usrData, size_t usrDataLen);
00177 extern void __attribute__((overloadable))
00178     rsForEach(rs_script script, rs_allocation input, rs_allocation output);
00179 #endif
This code fragment is an excerpt from Renderscript’s rs_core.rsh header file as viewed online. The filter() function invokes the appropriate rsForEach() function with the minimal number of arguments based on the current value of RS_VERSION, as revealed by this excerpt.

Building and Running EmbossImage

Let’s build and run EmbossImage. For brevity, this section only shows you how to build and install this app’s APK in a command-line environment. Although it doesn’t also show the Eclipse equivalent, it isn’t difficult to extrapolate the instructions for the Eclipse environment. This app was developed in the following context:
  • Windows 7 is the development platform. Change backslashes into forward slashes and remove the C: drive designator for a Linux environment.
  • The project build directory is C:prjdev. Replace the C:prjdev occurrence with your preferred equivalent.
  • Android SDK Revision 20 has been installed and an Android 4.1-based AVD associated with target ID 1 has been created. Execute android list targets to identify your equivalent target ID.
Complete the following steps to create and build an EmbossImage project:
  1. Create the EmbossImage project by executing android create project -t 1 -p C:prjdevEmbossImage -a EmbossImage -k ca.tutortutor.embossimage.
  2. Replace the skeletal contents of EmbossImagesrccatutortutorembossimageEmbossImage.java with Listing 2.
  3. Introduce an emboss.rs file with Listing 3’s contents into the src directory.
  4. Create a drawable subdirectory of EmbossImageres. Copy the leopard.jpg file that’s included in this article’s code archive to this directory.
  5. With EmbossImage as the current directory, execute ant debug to build the app.
The final step may fail with the following warning message (spread across multiple lines for readability):
WARNING: RenderScript include directory
         'C:prjdevGrayScale${android.renderscript.include.path}'
         does not exist!
[llvm-rs-cc.exe] :2:10: fatal: 'rs_core.rsh' file not found
Fortunately, Issue 34569 in Google’s Android issues database provides a remedy. Simply add the following property (spread across multiple lines for readability) to the build.xml file that’s located in the toolsant subdirectory of your Android SDK home directory to fix the problem:
<property name="android.renderscript.include.path"
          location="${android.platform.tools.dir}/renderscript/include:
                    ${android.platform.tools.dir}/renderscript/clang-include"/>
Specifically, place this <property> element after the following <path> element:
<!-- Renderscript include Path -->
<path id="android.renderscript.include.path">
  <pathelement location="${android.platform.tools.dir}/renderscript/include" />
  <pathelement location="${android.platform.tools.dir}/renderscript/clang-include" />
</path>
Rexecute ant debug and the build should succeed. If successful, you should discover an EmbossImage-debug.apk file in the EmbossImagebin directory. You can install this APK onto your emulated or real device by executing adb install binEmbossImage-debug.apk. If installation succeeds, go to the app launcher screen and locate the generic icon for the EmbossImage APK. Click this icon and the app should launch. Figure 2 shows you what the initial screen should look like on the Android emulator with an Android 4.1 AVD. Figure 2: The activity displays an unembossed leopard at startup. Click the leopard image (which is courtesy of Vera Kratochvil at PublicDomainPictures.net), and you should observe the embossed equivalent that’s shown in Figure 3. Figure 3: Clicking the activity causes an embossed leopard to emerge.

Conclusion

Embossing and other image processing operations are fairly easy to write in Renderscript. As an exercise, enhance the embossing script to support the light source coming from a direction other than the northwest (such as the northeast). This technique can be invaluable for using color, contrast, and the inherent “elevation” of pixels to build intuitive interfaces and enable advanced image manipulation.

Frequently Asked Questions (FAQs) about Mobile-Friendly Image Manipulation Using Android’s RenderScript API

What is Android’s RenderScript API and how does it work?

Android’s RenderScript API is a powerful tool that allows developers to perform high-performance computations and image manipulations directly on an Android device. It works by leveraging the device’s GPU (Graphics Processing Unit) or multi-core CPU to perform complex operations that would otherwise be too resource-intensive for the device. This makes it ideal for tasks such as image processing, computational photography, and machine learning.

How can I use RenderScript API to emboss images?

To emboss images using RenderScript API, you need to create a RenderScript context, load the image into an Allocation, apply the emboss script to the Allocation, and then copy the result back into a Bitmap. The emboss script uses a convolution matrix to create the embossed effect by manipulating the pixels of the image.

What are the benefits of using RenderScript API for image manipulation?

RenderScript API offers several benefits for image manipulation. It allows for high-performance image processing on the device, without the need for a server or cloud-based solution. This can result in faster processing times and lower data usage. Additionally, RenderScript is hardware-agnostic, meaning it can take advantage of whatever hardware capabilities are available on the device, such as a powerful GPU or multi-core CPU.

Can I use RenderScript API for other types of image manipulation besides embossing?

Yes, RenderScript API can be used for a wide variety of image manipulation tasks. This includes blurring, sharpening, edge detection, and more. The API provides a number of built-in scripts for common image processing tasks, and you can also write your own custom scripts for more specialized needs.

What are the prerequisites for using RenderScript API?

To use RenderScript API, you need to have a basic understanding of Android development and be familiar with Java or Kotlin programming languages. You also need to have the Android SDK installed on your development machine, and your device or emulator needs to be running Android 2.3 (Gingerbread) or higher.

How can I optimize my use of RenderScript API?

There are several ways to optimize your use of RenderScript API. One is to make sure you’re using the most efficient data types for your computations. Another is to take advantage of RenderScript’s ability to perform operations in parallel on multi-core CPUs or GPUs. You can also optimize your scripts by minimizing memory usage and avoiding unnecessary computations.

Are there any limitations or drawbacks to using RenderScript API?

While RenderScript API is a powerful tool, it does have some limitations. For example, it may not be the best choice for very simple image processing tasks, as the overhead of setting up a RenderScript context and Allocation can outweigh the performance benefits. Additionally, while RenderScript is hardware-agnostic, performance can still vary depending on the specific hardware capabilities of the device.

Can I use RenderScript API in conjunction with other Android APIs?

Yes, RenderScript API can be used in conjunction with other Android APIs. For example, you can use the Camera2 API to capture images, and then use RenderScript to process those images. You can also use RenderScript in conjunction with the Bitmap and Canvas APIs for drawing and displaying images.

How can I debug my RenderScript code?

Debugging RenderScript code can be a bit more challenging than debugging regular Java or Kotlin code, as you can’t use standard debugging tools like breakpoints. However, you can use the rsDebug() function to print debug messages to the Android log, which can help you identify issues in your code.

Where can I find more resources on using RenderScript API?

The official Android Developers website is a great resource for learning more about RenderScript API. It provides detailed documentation, tutorials, and sample code. You can also find helpful information on StackOverflow and other online developer communities.

Jeff FriesenJeff Friesen
View Author

Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and mobile technologies. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for SitePoint, InformIT, JavaWorld, java.net, and DevSource.

androidAndroid TutorialsTutorials
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week