Key Takeaways
- Leverage Renderscript for Efficient Image Processing: Utilize Android’s Renderscript API for high-performance image manipulation tasks such as embossing, which allows for rapid processing and maintains app portability across different Android devices.
- Understanding the Embossing Algorithm: Grasp how embossing simulates a 3D effect by comparing pixel intensities, using an algorithm that adjusts gray levels based on the difference in intensity between adjacent pixels to create a relief effect.
- Renderscript Primer: Familiarize with Renderscript’s structure, which combines a C99-based language with LLVM compilers and a runtime for accessing graphics and compute engines, enhancing the app’s performance significantly.
- Develop Dual-Language Applications: Prepare to write both Java and C99 code when implementing Renderscript in applications, utilizing specific Android package types for creating and managing Renderscript contexts and memory allocations.
- Building the Embossing App: Follow detailed steps to set up and deploy an Android app that uses Renderscript to emboss images, including handling potential build issues and optimizing the script for efficient execution.
- Practical Application and Enhancement Suggestions: Apply the embossing technique in various app scenarios such as interface design and explore enhancements like adjusting the light source direction in the embossing script for varied effects.
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 astatic RenderScript create(Context ctx)
method that is passed a reference to your current activity, and that returns a reference to aRenderscript
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.
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 oneAllocation
instance), and the output allocation identifies where results are to be stored (and is wrapped by the otherAllocation
instance). - An
rs_script
directive typically follows and identifies theScriptC_
-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;
andint height;
, and pass an image’s width and height from your Java code (via calls toset_
-prefixed methods on theScriptC_
-prefixed object) to these variables. - A
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 overloadedrsForEach()
functions. This function is responsible for identifying the number of cores, creating threads that run on these cores, and more. Each thread invokesroot()
.
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 theEmbossImage
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 aBitmap
instance with specific mipmap behavior and usage.bmIn
is passed tob
identifying the inputBitmap
instance (containing the image) as the instance to be wrapped.Allocation.MipmapControl.MIPMAP_NONE
is passed tomips
, indicating that no mipmaps are used.Allocation.USAGE_SCRIPT
is passed tousage
, 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 bytype
and no mipmaps generated by default.allocIn.getType()
is passed totype
, indicating that the layout for the memory to be allocated is specified by the previously created input allocation.
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 invokingContext
‘sResources 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 theres/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).
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 allocIn
, allocOut
, 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 #pragma
s 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
andy
identify the zero-based location of the element passed tov_in
. For a 1D allocation, no argument would be passed toy
. For a bitmap, arguments are passed to both parameters.
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 runEmbossImage
. 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 theC: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.
EmbossImage
project:
- Create the
EmbossImage
project by executingandroid create project -t 1 -p C:prjdevEmbossImage -a EmbossImage -k ca.tutortutor.embossimage
. - Replace the skeletal contents of
EmbossImagesrccatutortutorembossimageEmbossImage.java
with Listing 2. - Introduce an
emboss.rs
file with Listing 3’s contents into thesrc
directory. - Create a
drawable
subdirectory ofEmbossImageres
. Copy theleopard.jpg
file that’s included in this article’s code archive to this directory. - With
EmbossImage
as the current directory, executeant debug
to build the app.
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 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.