As a brief context, I am getting raw video data from Zoom SDK as separate Y, U, and V ByteBuffers and trying to convert them to bitmaps.
However, I noticed that this conversion is resulting in grayscale bitmap images with green and pink spots [as shown in the screenshot below]
the method I’m using for conversion is the following:
fun ZoomVideoSDKVideoRawData.toBitmap(): Bitmap? {
val width = streamWidth
val height = streamHeight
val yuvBytes = ByteArray(width * (height + height / 2))
val yPlane = getyBuffer()
val uPlane = getuBuffer()
val vPlane = getvBuffer()
// copy Y
yPlane.get(yuvBytes, 0, width * height)
// copy U
var offset = width * height
uPlane.get(yuvBytes, offset, width * height / 4)
// copy V
offset += width * height / 4
vPlane.get(yuvBytes, offset, width * height / 4)
// make YUV image
val yuvImage = YuvImage(yuvBytes, ImageFormat.NV21, width, height, null)
// convert image to bitmap
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, width, height), 50, out)
val imageBytes = out.toByteArray()
val image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
// return result
return image
}
any idea why the colors of the resulting bitmap are corrupted?
Thanks in advance!
Related
I would like to draw stroke around bitmap. getting left and right side point of bitmap and drawing line using path and points.
got border around bitmap using below code and set it below imageview. but when i apply padding it want draw border with appropriate padding.
private suspend fun drawBorder(
src: Bitmap,
colorToReplace: Int,
): Bitmap? {
return kotlin.runCatching {
withContext(IO) {
val width = src.width
val height = src.height
val pixels = IntArray(width * height)
// get pixel array from source
src.getPixels(pixels, 0, width, 0, 0, width , height )
val bmOut = Bitmap.createBitmap(width, height, src.config)
val canvas = Canvas(bmOut)
val paint = Paint().apply {
isAntiAlias = true
isDither = true
color = colorCode
style = Paint.Style.STROKE
strokeWidth = radius
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
if (isDashed) {
paint.pathEffect = DashPathEffect(floatArrayOf(50f, radius * 2), 0f)
}
var moved = false
val path = Path()
var pixel = 0
// iteration through pixels
for (y in height - 1 downTo 0) {
for (x in width - 1 downTo 0) {
val index = y * width + x
pixel = pixels[index]
if (pixel != colorToReplace) {
if (!moved) {
moved = true
path.moveTo(x.toFloat(), y.toFloat())
}
path.lineTo(if(isPadded) x.toFloat() + 20 else x.toFloat(), y.toFloat())
break
}
} //x
} //y
for (y in 0 until height) {
for (x in 0 until width) {
val index = y * width + x
pixel = pixels[index]
if (pixel != colorToReplace) {
path.lineTo(if(isPadded) x.toFloat() - 20 else x.toFloat(), if(isPadded) y.toFloat() - 20 else y.toFloat())
break
}
} //x
} //y
canvas.drawPath(path, paint)
bmOut
}
}.getOrNull()
}
Now i would like to draw dashed with padding like below image.
If anyone have idea please let me know. Thanks.
this is a function I used for the same, it could help you
private Bitmap addWhiteBorder(Bitmap bmp, int borderSize) {
Bitmap bmpWithBorder = Bitmap.createBitmap(bmp.getWidth() + borderSize * 2, bmp.getHeight() + borderSize * 2, bmp.getConfig());
Canvas canvas = new Canvas(bmpWithBorder);
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(bmp, borderSize, borderSize, null);
return bmpWithBorder;
}
Basically, it creates a new Bitmap adding 2 * border size to each dimension, and then paints the original Bitmap over it, offsetting it with border size.
You make your bitmap bigger than the one you are adding to it and then fill the canvas with the background you want. If you need to add other effects you can look into the canvas options for clipping the rect and adding rounded corners and such.
I have a big bitmap - sometimes with height 2000 sometimes 4000 etc. This is possible to split this big bitmap dividing by 1500 and save into array?
For example if I have bitmap with height 2300 I want to have array with two bitmaps: one 1500 height and second 800.
You can use the createBitmap() to create bitmap chunks from the original Bitmap.
The below function takes in a bitmap and the desired chunk size (1500 in your case).
And split the bitmap vertically if the width is greater than height, and horizontally otherwise.
fun getBitmaps(bitmap: Bitmap, maxSize: Int): List<Bitmap> {
val width = bitmap.width
val height = bitmap.height
val nChunks = ceil(max(width, height) / maxSize.toDouble())
val bitmaps: MutableList<Bitmap> = ArrayList()
var start = 0
for (i in 1..nChunks.toInt()) {
bitmaps.add(
if (width >= height)
Bitmap.createBitmap(bitmap, start, 0, width / maxSize, height)
else
Bitmap.createBitmap(bitmap, 0, start, width, height / maxSize)
)
start += maxSize
}
return bitmaps
}
Usage:
getBitmaps(myBitmap, 1500)
Yes, you can use Bitmap.createBitmap(bmp, offsetX, offsetY, width, height);
to create a "slice" of the bitmap starting at a particular x and y offset, and having a particular width and height.
I'll leave the math up to you.
I am using following helper class to handle sampling and rotation of the Camera Image.
object CaptureImageHelper {
/**
* This method is responsible for solving the rotation issue if exist. Also scale the images to
* 1024x1024 resolution
*
* #param context The current context
* #param selectedImage The Image URI
* #return Bitmap image results
* #throws IOException
*/
#Throws(IOException::class)
fun handleSamplingAndRotationBitmap(
context: Context,
selectedImage: Uri?,
isFrontCamera: Boolean
): Bitmap? {
val MAX_HEIGHT = 1024
val MAX_WIDTH = 1024
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
var imageStream: InputStream = context.getContentResolver().openInputStream(selectedImage!!)!!
BitmapFactory.decodeStream(imageStream, null, options)
imageStream.close()
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
imageStream = context.getContentResolver().openInputStream(selectedImage!!)!!
var img = BitmapFactory.decodeStream(imageStream, null, options)
img = rotateImageIfRequired(img!!, selectedImage, isFrontCamera)
return img
}
/**
* Calculate an inSampleSize for use in a [BitmapFactory.Options] object when decoding
* bitmaps using the decode* methods from [BitmapFactory]. This implementation calculates
* the closest inSampleSize that will result in the final decoded bitmap having a width and
* height equal to or larger than the requested width and height. This implementation does not
* ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
* results in a larger bitmap which isn't as useful for caching purposes.
*
* #param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* #param reqWidth The requested width of the resulting bitmap
* #param reqHeight The requested height of the resulting bitmap
* #return The value to be used for inSampleSize
*/
private fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int, reqHeight: Int
): Int {
// Raw height and width of image
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
// Calculate ratios of height and width to requested height and width
val heightRatio =
Math.round(height.toFloat() / reqHeight.toFloat())
val widthRatio =
Math.round(width.toFloat() / reqWidth.toFloat())
// Choose the smallest ratio as inSampleSize value, this will guarantee a final image
// with both dimensions larger than or equal to the requested height and width.
inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger inSampleSize).
val totalPixels = width * height.toFloat()
// Anything more than 2x the requested pixels we'll sample down further
val totalReqPixelsCap = reqWidth * reqHeight * 2.toFloat()
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++
}
}
return inSampleSize
}
/**
* Rotate an image if required.
*
* #param img The image bitmap
* #param selectedImage Image URI
* #return The resulted Bitmap after manipulation
*/
#Throws(IOException::class)
private fun rotateImageIfRequired(
img: Bitmap,
selectedImage: Uri,
isFrontCamera: Boolean
): Bitmap? {
val ei = ExifInterface(selectedImage.path!!)
val orientation: Int =
ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(img, 90, isFrontCamera)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(img, 180, isFrontCamera)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(img, 270, isFrontCamera)
else -> img
}
}
private fun rotateImage(
img: Bitmap,
degree: Int,
isFrontCamera: Boolean
): Bitmap? {
val matrix = Matrix()
if(isFrontCamera) {
val matrixMirrorY = Matrix()
val mirrorY = floatArrayOf(-1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f)
matrixMirrorY.setValues(mirrorY)
matrix.postConcat(matrixMirrorY)
matrix.preRotate(270f)
} else {
matrix.postRotate(degree.toFloat())
}
val rotatedImg =
Bitmap.createBitmap(img, 0, 0, img.width, img.height, matrix, true)
img.recycle()
return rotatedImg
}
}
Calling helper class
val bitmap = CaptureImageHelper.handleSamplingAndRotationBitmap(requireContext(), Uri.fromFile(cameraImage!!), false)
The issue i am getting is random. Mostly if I get rotated image below statement
ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
returns ExifInterface.ORIENTATION_ROTATE_90 which is fine and code rotate that image correctly. But sometimes Image is rotated but it Exif getAttributeInt returns ExifInterface.ORIENTATION_NORMAL. Which i believe means there is no data of Exif/Orientation against this image and it returns default value.
Exit get Attribute method
public int getAttributeInt(#NonNull String tag, int defaultValue) {
if (tag == null) {
throw new NullPointerException("tag shouldn't be null");
}
ExifAttribute exifAttribute = getExifAttribute(tag);
if (exifAttribute == null) {
return defaultValue;
}
try {
return exifAttribute.getIntValue(mExifByteOrder);
} catch (NumberFormatException e) {
return defaultValue;
}
}
I'm trying to optimise my app memory usage by following the recommended practice of using BitmapFactory.Options.inBitmap. What I want to do is slice a bitmap into stripes of height stripeHeight like this:
val stripeHeight = 10
val largeBitmap = // the original bitmap
val placeholder = // the bitmap object I want to re purpose for each stripe
val options = BitmapFactory.Options()
options.inBitmap = placeholder
val rows = largeBitmap.height / stripeHeight
for (i in 0 until rows) {
//how can I set the options object into createBitmap?
val stripe = Bitmap.createBitmap(largeBitmap, 0, i * stripeHeight, largeBitmap.width, stripeHeight)
it.onNext(stripe)
}
The problem is Bitmap.createBitmap won't take a BitmapFactory.Options in its method signature. So I'm unsure how to tell android to allocate the new bitmap into the memory space of the previous one.
One approach would be the following:
fun doBitmapProcessing() {
val STRIPE_HEIGHT = 10
//assuming your largeBitmap is a 320x240 ARGB8888 image
val largeBitmap: Bitmap = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888)
//then allocate a buffer of size 320 * 240 * 4 (4 channels in an ARGB image) bytes
val byteBuffer: ByteBuffer = ByteBuffer.allocate(320 * 240 * 4)
//create a bitmap of desired dimensions for the placeholder
val placeholder: Bitmap = Bitmap.createBitmap(320, STRIPE_HEIGHT, Bitmap.Config.ARGB_8888)
//copy the pixels from your large bitmap to a pre-allocated byte buffer
largeBitmap.copyPixelsToBuffer(byteBuffer)
//create an input stream from the pre-allocated buffer
val inputStream: InputStream = ByteArrayInputStream(byteBuffer.array())
//create the bitmap region decoder
val bitmapRegionDecoder: BitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, true)
//create options, as before
val options = BitmapFactory.Options()
options.inBitmap = placeholder
val rows = largeBitmap.height / STRIPE_HEIGHT
for (i in 0 until rows) {
//call decode stream on the input stream containing your data from *largeImage*, and the options
//in which you specify that you want the memory of *placeholder* to be overwritten.
val region: Rect = Rect(0, i * STRIPE_HEIGHT, largeBitmap.width, i * STRIPE_HEIGHT + STRIPE_HEIGHT)
val stripe: Bitmap = bitmapRegionDecoder.decodeRegion(region, options)
it.onNext(stripe)
}
}
Be sure to read the documentation of BitmapFactory.Options#inBitmap and use the result of BitmapRegionDecoder.decodeRegion(...) instead of placeholder directly, since it is not guaranteed that the same memory location will be used.
There might be another solution through Bitmap.getPixels(...) & Bitmap.setPixels:
fun doBitmapProcessing2() {
val STRIPE_HEIGHT = 10
//assuming your largeBitmap is in ARGB 8888 format
val largeBitmap: Bitmap = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888)
//create a bitmap of desired dimensions for the placeholder
val placeholder: Bitmap = Bitmap.createBitmap(320, STRIPE_HEIGHT, Bitmap.Config.ARGB_8888)
//create int pixel array in which the colors will be saved
val pixels = IntArray(320 * STRIPE_HEIGHT * 4)
val rows = largeBitmap.height / STRIPE_HEIGHT
for (i in 0 until rows) {
//save colors from desired location into the pixels array
largeBitmap.getPixels(pixels, 0, 320, 0, i * STRIPE_HEIGHT, 320, STRIPE_HEIGHT)
//write the colors saved into the array to the placeholder bitmap
placeholder.setPixels(pixels, 0, 320, 0, 0, 320, STRIPE_HEIGHT)
it.onNext(placeholder)
}
}
Although I'm not so sure about the second one. ( haven't used getPixels/setPixels before)
Hope this helps
I want my application to upload image with no size limit, but in the code, I want to resize the image into 1MB if the image size exceeds. I have tried many ways but I couldn't find any code for the requirement I have mentioned above.
For once, I have tried this:
public void scaleDown() {
int width = stdImageBmp.getWidth();
int height = stdImageBmp.getHeight();
Matrix matrix = new Matrix();
float scaleWidth = ((float) MAX_WIDTH) / width;
float scaleHeight = ((float) MAX_HEIGHT) / height;
matrix.postScale(scaleWidth, scaleHeight);
stdImageBmp = Bitmap.createBitmap(stdImageBmp, 0, 0, width, height, matrix, true);
File Image = new File("path");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//compress bmp
stdImageBmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
imgViewStd.setImageBitmap(stdImageBmp);
Log.d("resizedBitmap", stdImageBmp.toString());
width = stdImageBmp.getWidth();
height = stdImageBmp.getHeight();
System.out.println("imgWidth" + width);
System.out.println("imgHeight" + height);
}
you can use this code to resize a bitmap and for image size < 1MB i recommend use resolution of 480x640
public Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// CREATE A MATRIX FOR THE MANIPULATION
Matrix matrix = new Matrix();
// RESIZE THE BIT MAP
matrix.postScale(scaleWidth, scaleHeight);
// "RECREATE" THE NEW BITMAP
return Bitmap.createBitmap(
bm, 0, 0, width, height, matrix, false);
}
If you really want the Bitmap that scales down to the Bitmap that is the closest to a given amount of bytes, heres the method I use. (It does not uses a while loop)
NOTE: This method only works if passed bitmap is in ARGB_8888 configuration.
See: Compress bitmap to a specific byte size in Android for the conversion method.
/**
* Method to scale the Bitmap to respect the max bytes
*
* #param input the Bitmap to scale if too large
* #param maxBytes the amount of bytes the Image may be
* #return The scaled bitmap or the input if already valid
* #Note: The caller of this function is responsible for recycling once the input is no longer needed
*/
public static Bitmap scaleBitmap(final Bitmap input, final long maxBytes) {
final int currentWidth = input.getWidth();
final int currentHeight = input.getHeight();
final int currentPixels = currentWidth * currentHeight;
// Get the amount of max pixels:
// 1 pixel = 4 bytes (R, G, B, A)
final long maxPixels = maxBytes / 4; // Floored
if (currentPixels <= maxPixels) {
// Already correct size:
return input;
}
// Scaling factor when maintaining aspect ratio is the square root since x and y have a relation:
final double scaleFactor = Math.sqrt(maxPixels / (double) currentPixels);
final int newWidthPx = (int) Math.floor(currentWidth * scaleFactor);
final int newHeightPx = (int) Math.floor(currentHeight * scaleFactor);
Timber.i("Scaled bitmap sizes are %1$s x %2$s when original sizes are %3$s x %4$s and currentPixels %5$s and maxPixels %6$s and scaled total pixels are: %7$s",
newWidthPx, newHeightPx, currentWidth, currentHeight, currentPixels, maxPixels, (newWidthPx * newHeightPx));
final Bitmap output = Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true);
return output;
}
Where the Sample use would look something like:
// (1 MB)
final long maxBytes = 1024 * 1024;
// Scale it
final Bitmap scaledBitmap = BitmapUtils.scaleBitmap(yourBitmap, maxBytes);
if(scaledBitmap != yourBitmap){
// Recycle the bitmap since we can use the scaled variant:
yourBitmap.recycle();
}
// ... do something with the scaled bitmap
I've tried to comment it but the comment become too big.
So, I've tested many solutions and it seems like there are only TWO solutions for this problem, which give some results.
Lets discuss Koen solution first. What it actually does is creates a scaled JPG
Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true)
Seems like it does not compress it at all but just cuts off the resolution.
I've tested this code and when I pass MAX_IMAGE_SIZE = 1024000 it gives me 350kb compressed image out of 2.33Mb original image. Bug?
Also it lacks quality. I was unable to recognize a text on A4 sheet of paper photo made by Google Pixel.
There is another solution to this problem, which gives good quality, but lacks in speed.
A WHILE LOOP!
Basically you just loop through image size, until you get the desired size
private fun scaleBitmap() {
if (originalFile.length() > MAX_IMAGE_SIZE) {
var streamLength = MAX_IMAGE_SIZE
var compressQuality = 100
val bmpStream = ByteArrayOutputStream()
while (streamLength >= MAX_IMAGE_SIZE) {
bmpStream.use {
it.flush()
it.reset()
}
compressQuality -= 8
val bitmap = BitmapFactory.decodeFile(originalFile.absolutePath, BitmapFactory.Options())
bitmap.compress(Bitmap.CompressFormat.JPEG, compressQuality, bmpStream)
streamLength = bmpStream.toByteArray().size
}
FileOutputStream(compressedFile).use {
it.write(bmpStream.toByteArray())
}
}
}
I think that this approach will consume exponential time depending on image resolution.
9mb image takes up to 12 seconds to compress down to 1mb.
Quality is good.
You can tweak this by reducing the original bitmap resolution(which seems like a constant operation), by doing:
options.inSampleSize = 2;
What we need to do is to somehow calculate compressQuality for any image.
There should be a math around this, so we can determinate compressQuality from original image size or width + height.