Live Camera Demo Plugin
Demonstrates how to create live camera feed in ATAK
This plugin was developed as a stepping stone to isolate the camera functionality of the Convolutional Neural Network plugin. The goal of this plugin is to introduce how one can integrate a live camera feed into an ATAK Dropdown pane using OpenCV and ncnn. The ncnn
library provides us with the necessary matrix conversions to properly rotate and map the image pixels to the display and an established set of Java Native Interface (JNI) methods to incorporate into the plugin. When loaded, the plugin can be found in the ATAK tools as a video camera icon as shown in the title.
How to build and run this plugin
When building/running this plugin first ensure you have the listed dependencies then proceed to the Quick Reference section.
Dependencies
- Download and install CMake.
- Windows: advised to download the latest
*.msi
on the page linked above and follow the default options in the installer. Ensure it is included in your environment path. - Linux: run
sudo apt install cmake
in a terminal session
- Windows: advised to download the latest
- Restart all terminal sessions and ensure installation works.
The command
cmake --version
should result incmake version <VERSION_NUMBER>
. MAY REQUIRE SYSTEM REBOOT - Specify your version of CMake in the
cmake_version
variable of thelocal.properties
file.
- Download and install CMake.
OpenCV for Android version 4.6.0
- Click the link above to download OpenCV version 4.6.0 for Android.
- Extract the zip archive to place the contents in:
- Windows:
C:\tak\AndroidLibs\opencv-4.6.0-android-sdk
- Linux:
/<ATAK_SDK_PATH>/atak-civ/AndroidLibs/OpenCV-android-sdk
- Windows:
- Specify your install path in the
opencv_dir
variable of thelocal.properties
file.
- Click the link above to download ncnn build number 20221128 .
- Extract the zip archive to place the contents in:
- Windows:
C:\tak\AndroidLibs\ncnn-20221128-android-vulkan
- Linux:
/<ATAK_SDK_PATH>/atak-civ/AndroidLibs/ncnn-20221128-android-vulkan
- Windows:
- Specify your install path in the
ncnn_dir
variable of thelocal.properties
file.
- Download and install Ninja
- Windows:
- Click the link above to navigate to the GitHub ninja release page.
- Select the latest asset
*.zip
download (at the time of writing we downloaded v1.11.1) - Extract the zip archive to your desired path. It should only have one executable
ninja.exe
. We placed the executable atc:\tak\win-ninja\ninja.exe
. - Add
ninja.exe
to System Environment Variables. Open Environment Variables window. Find “Path” in the “System variables” view. Select “Edit” then press “New”. Add the complete path to the folder holding your ninja executableC:\tak\win-ninja
.
- Linux:
- Run
sudo apt install ninja-build
in terminal. - Locate installation directory with
which ninja
command
- Run
- Windows:
- Ensure install works. Open up a terminal session and enter
ninja --version
. You should see1.11.1
or the version you installed.
MAY REQUIRE SYSTEM REBOOT.
- Download and install Ninja
Quick Reference
Build the application signing keys which are required by the Android Operating System (OS) for security when installing software packages. At the bottom of the IDE there should be a Terminal tab you can open to launch a terminal session in the root folder of the plugin.
# Run the following commands in your Android Studio Terminal # Generate Debug signing key: set "alias", "keypass", and "storepass" flag values as desired keytool -genkeypair -dname "CN=Android Debug,O=Android,C=US" -validity 9999 -keystore debug.keystore -alias androiddebugkey -keypass android -storepass android # Generate Release signing key: set "alias", "keypass", and "storepass" flag values as desired keytool -genkeypair -dname "CN=Android Release,O=Android,C=US" -validity 9999 -keystore release.keystore -alias androidreleasekey -keypass android -storepass android
Edit the
demo-camera/local.properties
file to add the following lines.<ANDROID_SDK_PATH>
and thesdk.dir
should already be filled out by the IDE with the default Android SDK file path. The key here is to specify the paths to your signing keys, OpenCV library, andncnn
library. It is also required to specify the CMake version you have installed.<ABSOLUTE_PLUGIN_PATH>
should be a complete file path to the root plugin folder; example plugin path:C\:\\tak\\atak-civ-sdk-4.5.1.13\\atak-civ\\learnatak\\demo-camera
NOTE: Ensure your directory path is escaped properly on Windows. Follow the default sdk.dir
for formatting reference or the example path above. The most common mistake is forgetting the first escape character C\:
. Gradle Exceptions will be thrown until your paths are correctly formatted.
sdk.dir=<ANDROID_SDK_PATH>
takDebugKeyFile=<ABSOLUTE_PLUGIN_PATH>\\debug.keystore
takDebugKeyFilePassword=android
takDebugKeyAlias=androiddebugkey
takDebugKeyPassword=android
takReleaseKeyFile=<ABSOLUTE_PLUGIN_PATH>\\release.keystore
takReleaseKeyFilePassword=android
takReleaseKeyAlias=androidreleasekey
takReleaseKeyPassword=android
cmake_version=3.23.2
opencv_dir="C:\\tak\\AndroidLibs\\opencv-4.6.0-android-sdk"
ncnn_dir="C:\\tak\\AndroidLibs\\ncnn-20221128-android-vulkan"
Logcat Filter
Copy and paste this in the filter field of the Logcat to make it easier to follow the feed of log messages that are printed by the demo camera plugin to help further your understanding of the code in the project.
# FILTER
package:com.atakmap.app.civ tag:DemoCameraDropDownReceiver tag:NdkCamera tag:ncnn
# Debug Filter reduce noise
-tag:NdkCameraWindow -tag:GLMapRenderer -tag:DisplayDeviceRepository -tag:HWComposer -tag:LocSvc_ApiV02 -tag:GEL_DELAYED_EVENT_DEBUG -tag:CommsMapComponentCommo -tag:NetworkScheduler -tag:ConnectivityService -tag:ActivityManager -tag:AiAiEcho -tag:Dt2FileWatcher -tag:Thread-23 -tag:ColorDisplayService -tag:NetworkUtils -tag:CarrierServices -tag:SEE -tag:VSC -tag:ASH -tag:atakmap.app.ci -tag:-tag:NdkCameraWindow -tag:GLMapRenderer -tag:DisplayDeviceRepository -tag:HWComposer -tag:LocSvc_ApiV02 -tag:GEL_DELAYED_EVENT_DEBUG -tag:CommsMapComponentCommo -tag:NetworkScheduler -tag:ConnectivityService -tag:ActivityManager -tag:AiAiEcho -tag:Dt2FileWatcher -tag:Thread-23 -tag:ColorDisplayService -tag:NetworkUtils -tag:CarrierServices -tag:SEE -tag:VSC -tag:ASH -tag:atakmap.app.ci -tag:CHRE
Potential Errors
1. OpenCV not provided
If you see the following stack trace error when trying to build the application there is an issue with your installation of OpenCV.
If you have installed and extracted the library as outlined in the dependencies section it is likely just an issue with the opencv_dir
path provided in the local.properties
file.
CMake Error at CMakeLists.txt:6 (find_package):
By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "OpenCV", but CMake did not find one. Could not find a package configuration file provided by "OpenCV" with any of the following names:
OpenCVConfig.cmake
opencv-config.cmake
Add the installation prefix of "OpenCV" to CMAKE_PREFIX_PATH or set "OpenCV_DIR" to a directory containing one of the above files. If "OpenCV" provides a separate development package or SDK, be sure it has been installed.
2. CMake Error and Ninja
If you see the following stack trace error when trying to build the application there is an issue with your installation of ninja. First attempt to restart Android Studio to resolve this issue, if that doesn’t work then a complete computer reboot should fix this issue.
CMake Error: CMake was unable to find a build program corresponding to "Ninja". CMAKE_MAKE_PROGRAM is not set. You probably need to select a different build tool.
Plugin Code Overview
The rest of this document will provide a high level overview to help introduce and orient developers to the source code that drives this plugin. The goal is to help readers gain confidence to understand how to modify this project to meet their needs.
flowchart TB dropDownVisible --> createCamera createCamera --> cppCreateCamera surfaceChanged --> openCamera --> cppOpenCamera --> ACameraCaptureSession --> AImageReader_ImageListener --> onImage --> ANativeWindow --> videoView surfaceChanged -- "surface/canvas" --> setOutputWindow --> cppSetWindow --> ANativeWindow subgraph "JAVA" subgraph "DropDown" videoView dropDownVisible("onDropDownVisible()") surfaceChanged("VideoView.surfaceChanged()") end end subgraph "JNI" createCamera openCamera setOutputWindow end subgraph "C++" subgraph "yolocnn.cpp" cppCreateCamera("createCamera()") onImage("NdkCamera.on_image()") cppSetWindow("NdkCamera.set_window()") ANativeWindow ACameraCaptureSession AImageReader_ImageListener cppOpenCamera("NdkCamera.open()") end end
The diagram above shows how the plugin pane utilizes our Java Native Interface NcnnYolov7 which leverages the Android Camera Native Development Kit(NDK) C++ interface to capture frames from the device’s camera feed to display on within a layout defined View resource. The diagram only focuses on the camera capture session initialization in an attempt to simplify the steps involved and help you understand the general approach to understand how Java function calls in the plugin’s main dropdown receiver class (DemoCameraDropDownReceiver.java) make their way through the JNI Java (NcnnYolov7.java) and C++ (yolocnn.cpp) interface classes to the actual native development implementation (ndkcamera.cpp) to feed imagery back to the Java managed content pane.
1. Java Plugin Entry Point
The best place to start understanding the plugin’s implementation details is with the primary dropdown receiver as that is where the top level function calls are made for integrating the native camera feed into the ATAK plugin view.
The following snippet is key to initializing the native camera feed, providing our native implementation information about the device’s configuration (orientation), and providing the canvas to the surfaceView
which enables the native code to render the image feed within our plugin’s UI panel. The mVideoView
is a SurfaceView
in our plugin’s layout, and is initialized in the constructor of the drop down receiver. We provide our native camera implementation the preferred display orientation based on the ATAK preference to help determine the proper translation of the pixels to ensure the image feed always displays right side up. to the plugin pane to the preferred orientation based on the orientation of ATAK. Given the aspect ratio of processed imagery we set the surface view’s format to PixelFormat.RGBA_8888
to ensure the images are properly rendered in the pane.
ncnnyolov7.createCamera();
ncnnyolov7.setAssetManager(pluginCtx.getAssets());
int orientation = AtakPreferenceFragment.getOrientation(getMapView().getContext());
ncnnyolov7.setPrefOrientation(orientation);
// when visible again re-create the surface
mVideoView.getHolder().setFormat(PixelFormat.RGBA_8888);
mVideoView.getHolder().addCallback(this);
Since C++ doesn’t have any garbage collection it is important our plugin properly cleans up memory usage when the native camera feed is no longer needed. The snippet below shows the necessary steps required to properly release the native resources by closing the camera session, destroying / destructing the native camera object, and releasing the render window asset. That is followed by releasing the resources associated with the rendering surface. This is done in the rendering surface’s destruction callback since that will be called when the plugin pane UI is closed and the UI pane can no longer render imagery from the camera.
public void surfaceDestroyed(SurfaceHolder holder) {
// release the surface view as it will be re-created when the plugin becomes visible
Log.i(TAG, "Surface destroyed");
ncnnyolov7.closeCamera();
ncnnyolov7.destroyCamera();
ncnnyolov7.releaseOutputWindow(holder.getSurface());
mVideoView.getHolder().removeCallback(this);
mVideoView.getHolder().getSurface().release();
}
To handle orientation changes of the device and any structural changes to the render surface, the code below ensures the native camera implementation has a valid object to render images on and starts a new camera capture session.
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, String.format(Locale.US, "Surface changed to format %s width %d height %d",
format, width, height));
ncnnyolov7.setOutputWindow(holder.getSurface());
Log.i(TAG, "openCamera");
ncnnyolov7.openCamera();
}
2. JNI Mapping Java to C++
There are two files in this project that make up the Java Native Interface (JNI) mapping to enable our Java plugin to make calls to our native implementation derived from the sample project of the ncnn
library. The Java mapping can be found at NcnnYolov7.java with the accompanying C++ mapping at yolocnn.cpp. Each function defined in the NcnnYolov7
class that includes native
in the function signature has an accompanying JNIExport <RETURN-TYPE> JNICALL Java_com_atakmap_android_democamera_NcnnYolov7_<FUNCTION-NAME>
implementation in the C++ file.
These C++ mapped functions primary serve as a way to interact with a global pointer to an MyNdkCamera
class which helps interact with the Android Camera NDK to perform the necessary steps to setup or destroy a native camera feed to render within a custom plugin/application layout. Some of the functions also work directly with the native window NDK to assist the g_camera
pointer in setting up the image feed.
These functions are well-described by their names and follow their logical workflows. For example the camera must be created before it can be opened (createCamera() must be invoked before openCamera()) and cameras must be closed before they can be destroy (closeCamera() must be invoked before destroyCamera()).
createCamera()
Java, C++: create a newMyNdkCamera
object and assign it to the global camera pointerg_camera
openCamera()
Java, C++: executes theMyNdkCamera
open procedure to begin capturing imagescloseCamera()
Java, C++: executes theMyNdkCamera
close procedures to shutdown the native image capturedestroyCamera()
Java, C++: cleans up theMyNdkCamera
resources, destroys the camera object and clears the global pointersetOutputWindow(Surface surface)
Java, C++: allows the Java plugin to provide a reference to the layout surface for the native camera stream to render the camera stream framesreleaseOutputWindow(Surface surface)
Java, C++: allows the Java plugin to clear the reference to the layout surface for the native camera stream to avoid an attempt to render to a non-existent resourcesetAssetManager(AssetManager assetManager)
Java, C++: allows the Java plugin to provide more info on the device’s current orientation to the native camera logicsetPrefOrientation(int orientation)
Java, C++: allows the Java plugin to provide more info on the device ATAK application’s configured orientation to help translate the captured image to the proper display rotation
3. Native Camera Code C++
The primary class that executes all the Android Camera NDK function calls to setup a native camera rendering stream is the MyNdkCamera
which inherits from the NdkCameraWindow
which inherits from the NdkCamera
class. These classes help separate the concerns of the setup for the camera stream.
NdkCamera
is responsible for opening and closing the camera feed as well as providing the primary drivingon_image
callback to process every frame captured by the camera feedNdkCameraWindow
is responsible for providing the logic to render the image frames onto the layout surface which is why it implements the functionssetAssetManager(AssetManager assetManager)
andsetPrefOrientation(int orientation)
to translate/transform the raw image pixels to render in the proper orientation within the plugin window. It also implements the functions to maintain knowledge of the rendering surface withsetOutputWindow(Surface surface)
andreleaseOutputWindow(Surface surface)
MyNdkCamera
is responsible for annotating the rendered image and in the Demo CNN plugin does additional processing on the image frames as they are displayed.
We won’t cover the details of the Android Native Development Kit in this document, and advise you leverage the modifications made to these classes within your own plugins that require a live stream of camera frames to be displayed within the plugin’s layout. It is advised to add any additional image processing within the MyNdkCamera::on_image_render(cv::Mat& rgb)
.