Add JNI/Kotlin bindings for the ImageProcessor - #21830
Open
john-rocky wants to merge 1 commit into
Open
Conversation
extension/image already builds everywhere and has ObjC/Swift bindings (pytorch#20051), but nothing exposed it to Android, so Java callers still hand-roll resize and normalize loops per model. This wires the same library into the AAR. Review order: the Kotlin surface (ImageProcessorConfig.kt, ImageProcessor.kt) mirrors the ObjC one; jni_layer_image.cpp is the glue; the two build files turn the extension on for Android and link it. The output tensor is allocated on the Java side as a direct FloatBuffer and filled in place, so process() and processInto() share one native entry point. Bitmap input goes through AndroidBitmap_lockPixels (ARGB_8888 is RGBA in memory); camera input takes the semi-planar YUV planes directly. Plane capacities are checked here because the decoder only sees raw pointers. gpu_min_input_pixels is not exposed: the portable implementation has no GPU path, so the config would report a decision that cannot happen.
john-rocky
requested review from
kirklandsign,
larryliu0820 and
psiddh
as code owners
August 14, 2026 04:34
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21830
Note: Links to docs will display an error until the docs builds have been completed. ❌ 1 New Failure, 1 Cancelled JobAs of commit 59795f9 with merge base abc5586 ( CANCELLED JOB - The following job was cancelled. Please retry:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
extension/imagehas shipped since #19967 and picked up ObjC/Swift bindings in #20051, but nothing exposes it to Android — the extension isn't built into the AAR at all. This adds the JNI layer and a Kotlin surface mirroring the ObjC one, asorg.pytorch.executorch.extension.image.ImageProcessor. All the real work is @metascroy's; this only carries it across.Android callers write the resize-and-normalize step themselves today, because there is no API for it. That step is where letterbox geometry and channel order go wrong. The library already handles both. Its interleave kernel is NEON (#20137), so arm64 gets that path for free. I hit this converting vision models to
.pte: on iOS it was a few lines, on Android a loop per model.Review order
ImageProcessorConfig.kt,ImageProcessor.kt— the public surface. Config, orientation, letterbox padding and the reuse variant matchExecuTorchImageProcessor.hone for one, minusgpuMinInputPixels. Input differs where the platforms do: there is noCVPixelBufferto auto-detect, so aBitmapoverload and an explicit semi-planar YUV overload take its place.jni_layer_image.cpp— the glue.tools/cmake/preset/android.cmake,extension/android/CMakeLists.txt— one line to turn the extension on (mirroringapple_common.cmake:26), then the source and the link.ImageProcessorInstrumentationTest.kt— pure image math, so no.ptefixture is needed.Three calls I made, and I'm happy to change any of them
The output tensor is allocated Java-side. Kotlin allocates a direct
FloatBufferand native fills it in place throughprocess_into. That makesprocess()andprocessInto()one native entry point. Capacity is passed as an explicitintrather than read withGetDirectBufferCapacity, whose unit is unspecified for view buffers.gpuMinInputPixelsis not exposed. The portable implementation has no GPU path, so the field would report a decision that cannot happen on Android. The JNI pins it tokGpuNever.Plane bounds are checked in the JNI.
process_yuv_intotakes raw pointers, so it cannot see how large the Java buffers are. The decode reads the interleaved chroma plane throughuvStride * (height / 2 - 1) + width. The binding rejects a shorter buffer rather than reading past its end.Two things worth your eye
Turning on
EXECUTORCH_BUILD_EXTENSION_IMAGEfor Android makes the non-Apple branch ofextension/image/CMakeLists.txtrunFetchContentfor stb at configure time. That is a new network fetch in the AAR build. If it doesn't suit CI, I can default the flag off for Android, or vendor the two stb headers — whichever you prefer.The chroma bounds check is the part I am least sure of. Some camera HALs return a
planes[2]buffer whose capacity stops one byte short of the full interleaved plane. Strict checking turns that into anInvalidArgumentinstead of an out-of-bounds read, but it will reject input that happens to work today. Tolerating the short buffer is a one-line change if you'd rather.Verification
Built and run on a Pixel 8a (arm64, NDK 28.2.13676358,
--preset android-arm64-v8a, Release).extension_imagebuilds forarm64-v8a.libexecutorch_jni.solinks withjni_layer_image.cpp,extension_imageandjnigraphics, and all sixJava_..._ImageProcessor_*entry points export through the existingJava_*version script.ImageProcessorInstrumentationTest: 18 tests, 0 failures, 0 skipped, through the assembled AAR — so theBitmaplock, the direct-FloatBufferround trip and the exception mapping run as an app would hit them.extension_image, putting the NEON path under test: channel order, ImageNet normalization, letterbox padding upright and rotated, the pad-value fill, and the clockwise direction ofOrientation::RIGHT. 14/14.Also checked:
-Wall -Wextraclean foraarch64-linux-android26, noclang-formatdiff, the wholeorg/pytorch/executorchKotlin source set compiles with the new package, andjavap -sconfirms every native descriptor matches its C++ signature including argument order.Thanks for building the library in a shape that made this mostly wiring — happy to reshape any of it into whatever fits the tree.
This PR was authored with Claude.