I'm trying to scale a tiny image (something like 20x40 pixels) to an arbitrarily large image. I'm doing the scaling by setting the ImageView.scaleType property, but while it does scale the image, it fades from one original pixel color to the next, creating a large blurry version of the image.
Here's some context for the question. I'm trying to create an old school 16-bit style game for Android. I want to store the sprites/etc as tiny files, and then scale them larger depending on the user's screen size. That way, I don't have to create a ton of different images at different sizes for each sprite. Right now I'm just working on a proof of concept, so I'm trying to scale a tiny 20x40 image file to the size of the entire screen.
*edit: I figured out how to use the method Reflog recommended. However, it still does some fading, just far less than the default algorithm.
*edit2: Got it! I had to turn anti-aliasing off
*edit3: This mysteriously stopped working for me, and I found another place where dithering/auto scaling needed to be disabled. Added the options lines.
#Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setDither(false);
paint.setAntiAlias(false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDither = false;
options.inScaled = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tinyimg, options);
canvas.drawBitmap(bitmap, null, new RectF(getLeft(), getTop(), getRight()/2, getBottom()/2), paint);
}
Use:
paint.setDither(false);
g.drawBitmap(img, src, dst, paint);
to use NN scaling, this will reduce blurring, but also will reduce the quality of the scaling.
Maybe this code can help you:
/**
* #param bitmap Bitmap original
* #param finWid width of the new Bitmap (width of the cut)
* #param finHei height of the new Bitmap (height of the cut)
* #param angulo angle of rotation in degrees
* #param sx scale X
* #param sy scale Y
* #param tx Translate X
* #param ty Translate Y
* #param config {#link Config} of the new Bitmap
* #return {#link Bitmap} transformed
*/
public Bitmap createTransPixBitmap(Bitmap bitmap, int finWid, int finHei, float angulo, float sx, float sy, float tx, float ty, Config config){
return Bitmap.createBitmap(createTransPixArray(bitmap, bitmap.getWidth(), bitmap.getHeight(), finWid, finHei, angulo, sx, sy, tx, ty), finWid, finHei, config);
}
public int[] createTransPixArray(Bitmap bitmap, int width, int height, int finWid, int finHei, float angulo, float sx, float sy, float tx, float ty){
float scaWid = width*sx;
float scaHei = height*sy;
int[] ori = new int[width*height];
bitmap.getPixels(ori, 0, width, 0, 0, width, height);
bitmap.recycle();
bitmap = null;
System.gc();
return transformPix(ori, width, height, scaWid, scaHei, finWid, finHei, angulo, tx, ty);
}
/**
* Core function for apply scale, translate and rotation (cut the image if you want too)
* #param ori original color array
* #param wid original width
* #param hei original height
* #param scaWid scaled original width
* #param scaHei scaled original height
* #param finWid width of the new Bitmap
* #param finHei height of the new Bitmap
* #param angulo rotation in degrees
* #param tx Translate X
* #param ty Translate Y
* #return int[] of colors
*/
private int[] transformPix(int[] ori, int wid, int hei, float scaWid, float scaHei, int finWid, int finHei, float angulo, float tx, float ty){
int[] fin = new int[finWid*finHei];
double sin = Math.sin(Math.toRadians(angulo));
double cos = Math.cos(Math.toRadians(angulo));
int dx = (int)((scaWid-finWid)/2);
int dy = (int)((scaHei-finHei)/2);
/* No se como explicar esto, solo puedo decir que el proceso de escalado es en sentido inverso
* escala, rota, translada y corta, todo al mismo tiempo :D
*/
for(int y = 0; y < finHei; y++){
for(int x = 0; x < finWid; x++){
int tempX = (int)Math.floor(((x+dx-((float)scaWid/2))*((float)wid/scaWid))+0.5f-tx);
int tempY = (int)Math.floor(((y+dy-((float)scaHei/2))*((float)hei/scaHei))+0.5f-ty);
int tempRX = (int)Math.floor(((cos*(float)tempX)+(sin*(float)tempY))+0.5f+((float)wid/2));
int tempRY = (int)Math.floor(((cos*(float)tempY)-(sin*(float)tempX))+0.5f+((float)hei/2));
if((tempRX >= 0 && tempRX < wid) && (tempRY >= 0 && tempRY < hei))
fin[x+(y*finWid)] = ori[tempRX+(tempRY*wid)];
}
}
ori = null;
return fin;
}
This function just scale the color array (if you don't want the extra features):
private int[] scale(int[] ori, int wid, int hei, int finWid, int finHei){
int[] fin = new int[finWid*finHei];
for(int y = 0; y < finHei; y++){
for(int x = 0; x < finWid; x++){
int temp = (int)(x*(wid/finWid))+((int)(y*(hei/finHei))*wid);
fin[x+(y*finWid)] = ori[temp];
}
}
return fin;
}
I hope this is useful, and I gladly would appreciate some advice for improving this code.
Related
i have following problem. I have a bitmap and on this bitmap certain draw operations can be done. Like drawing a line, arrwow, etc.
This works pretty well, but the bitmap can be zoomed, dragged and rotated. For this a drawMatrix is used. Everything this works pretty good but, for drawing i need the original coordinates so that the drawings come in the right size, etc.
I can get this original coordinates with the following function. But it doesn't work with rotations. Does anybody have an idea, how to solve this?
/**
* When we have moved or zoomed we need the original image coordinates calculated from
* the screen coordinates. This function gives that back
*/
private Point translateScreenCoordsToOriginalImageCoords(MotionEvent e) {
float[] m = new float[9];
drawMatrix.getValues(m);
float transX = m[Matrix.MTRANS_X] * -1;
float transY = m[Matrix.MTRANS_Y] * -1;
float scaleX = m[Matrix.MSCALE_X];
float scaleY = m[Matrix.MSCALE_Y];
// TODO function does not work when rotated by 90 degree
if (scaleX == 0.0) {
scaleX = -1.0f;
scaleY = -1.0f;
}
int lastTouchX = (int) ((e.getX() + transX) / scaleX);
int lastTouchY = (int) ((e.getY() + transY) / scaleY);
lastTouchX = Math.abs(lastTouchX);
lastTouchY = Math.abs(lastTouchY);
return new Point(lastTouchX, lastTouchY);
}
This is how scaling is performed
/**
* Scale the canvas by
*
* #param scaleFactor e.g 2 is double
* #param focusX the center x point
* #param focusY the center y point
*
* #return true if scaling is performed
*/
private boolean doScale(float scaleFactor, int focusX, int focusY) {
if (this.scaleBoundaries(scaleFactor))
return false;
Matrix transformationMatrix = new Matrix();
// Zoom focus is where the fingers are centered
transformationMatrix.postTranslate(-focusX, -focusY);
transformationMatrix.postScale(scaleFactor, scaleFactor);
transformationMatrix.postTranslate(focusX, focusY);
drawMatrix.postConcat(transformationMatrix);
invalidate();
return true;
}
e.g. This is the draw function of a Line
#Override
public void draw(Canvas c, Matrix drawMatrix, boolean translateCoordiantes) {
float pts[] = new float[] {this.start.x, this.start.y, this.end.x, this.end.y};
if (translateCoordiantes)
drawMatrix.mapPoints(pts);
c.drawLine(pts[0], pts[1], pts[2], pts[3], this.paint);
}
Wow pskink thanks a lot! That was the answer! To get the orignal coordinates back, use:
private Point translateCoordinatesBack(MotionEvent event) {
Matrix inverse = new Matrix();
drawMatrix.invert(inverse);
float[] pts = {event.getX(), event.getY()};
inverse.mapPoints(pts);
return new Point((int) pts[0], (int) pts[1]);
}
I am having a problem. I have a post draw listener where I draw a scaled version of a bitmap. The problem stems from the fact that I tend to do some scaling every time a zoom happens (zoom in scale up, zoom out scale down). The problem is I am not able to recycle the bitmap because when I try to do so after drawing
canvas.draw(scaledbitmap,0,0,null);
scaledBitmap.recycle()
i get the Canvas canot draw recycled bitmap exception
Does anyone know how I would go about recycling a bitmap after I am done with it so that another can be scaled afterwards and I don't get the OutOfMemoryException crash.
Some Code to show you exactly how I am using it:
private SpenDrawListener mPosteDrawListener = new SpenDrawListener() {
#Override
public void onDraw(Canvas canvas, float x, float y, float ratio,
float frameStartX, float frameStartY, RectF updateRect) {
if(mLineDrawingBitmap == null)
mLineDrawingBitmap = loadLineDrawingBitmap(mLineDrawingFileName);
Bitmap bm = Bitmap.createScaledBitmap(mLineDrawingBitmap, (int)(mLineDrawingBitmap.getWidth() * ratio), (int)(mLineDrawingBitmap.getHeight() * ratio), true);
/*
float pointX = (mScreenRect.width() - bm.getWidth()) / 2;
float pointY = mScreenRect.height() / 2 - bm.getHeight();
*/
float pointX = frameStartX - (x * ratio);
float pointY = frameStartY - (y * ratio);
//canvas.drawBitmap(bm, 0, 0,null);
canvas.drawBitmap(bm, pointX, pointY, null);
//bm.recycle();
}
};
After several hours of trial and error I have figured out how to scale nicely AND not crash the app with an OutOfMemoryException: Below is the code for drawing and scaling at runtime with no crash (as long as the image is not too large when decoded). I will use my own PostDrawListener but I believe it can be used anywhere with minor modification
private SpenDrawListener mPosteDrawListener = new SpenDrawListener() {
#Override
public void onDraw(Canvas canvas, float x, float y, float ratio,
float frameStartX, float frameStartY, RectF updateRect) {
if(mLineDrawingBitmap == null)
mLineDrawingBitmap = loadLineDrawingBitmap(mLineDrawingFileName);
float pointX = frameStartX - (x * ratio);
float pointY = frameStartY - (y * ratio);
//Create a new Matrix
Matrix m = new Matrix();
//Use any scaling ratio you want
m.postScale(ratio, ratio);
//Use any translation you want
m.postTranslate(pointX, pointY);
//when using below call you will not be creating a new bitmap, just
//using the original with runtime modifications
canvas.drawBitmap(mLineDrawingBitmap, m, null);
}
};
I am trying to set a region of pixels in a mutable bitmap a different color in my android app. Unfortunately I cannot get setPixels() to work properly. I am constantly getting ArrayOutOfBoundsExceptions. I think it may have something to do with the stride, but I'm really not sure. That is the only parameter that I still don't understand. The only other post I have seen on setPixels (not setPixel) is here: drawBitmap() and setPixels(): what's the stride? and it did not help me. I tried setting the stride as 0, as the width of the bitmap, as the width of the bitmap - the area i'm trying to draw and it still crashes. Here is my code:
public void updateBitmap(byte[] buf, int offset, int x, int y, int width, int height) {
// transform byte[] to int[]
IntBuffer intBuf = ByteBuffer.wrap(buf).asIntBuffer();
int[] intarray = new int[intBuf.remaining()];
intBuf.get(intarray);
int stride = ??????
screenBitmap.setPixels(intarray, offset, stride, x, y, width, height); // crash here
My bitmap is mutable, so I know that is not the problem. I am also certain that my byte array is being properly converted to an integer array. But I keep getting ArrayOutOfBoundsExceptions and I don't understand why. Please help me figure this out
EDIT -
here is how I construct the fake input:
int width = 1300;
int height = 700;
byte[] buf = new byte[width * height * 4 * 4]; // adding another * 4 here seems to work... why?
for (int i = 0; i < width * height * 4 * 4; i+=4) {
buf[i] = (byte)255;
buf[i + 1] = 3;
buf[i + 2] = (byte)255;
buf[i + 3] = 3;
}
//(byte[] buf, int offset, int x, int y, int width, int height) - for reference
siv.updateBitmap(buf, 0, 0, 0, width, height);
So the width and height are the correct amount of ints (at least it should be).
EDIT2 - here is the code for the original creation of screenBitmap:
public Bitmap createABitmap() {
int w = 1366;
int h = 766;
byte[] buf = new byte[h * w * 4];
for (int i = 0; i < h * w * 4;i+=4) {
buf[i] = (byte)255;
buf[i+1] = (byte)255;
buf[i+2] = 0;
buf[i+3] = 0;
}
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
IntBuffer intBuf = ByteBuffer.wrap(buf).asIntBuffer();
int[] intarray = new int[intBuf.remaining()];
intBuf.get(intarray);
Bitmap bmp = Bitmap.createBitmap(metrics, w, h, itmap.Config.valueOf("ARGB_8888"));
bmp.setPixels(intarray, 0, w, 0, 0, w, h);
return bmp;
}
It seems to work in this instance, not sure what the difference is
Short
I think, stride is the width of the image contained in pixels.
long
What I take from the documentation, the algorithm should work somehow like this:
public void setPixels (int[] pixels,
int offset,
int stride,
int x,
int y,
int width,
int height)
for (int rowIndex = 0; rowIndex < height; rowIndex++) {
int rowStart = offset + stride * rowIndex; // row position in pixels
int bitmapY = y + rowIndex; // y position in the bitmap
for (int columnIndex = 0; columnIndex < width; columnIndex++) {
int bitmapX = x + columnIndex; // x position in the bitmap
int pixelsIndex = rowStart + columnIndex; // position in pixels
setPixel(bitmapX, bitmapY, pixels[pixelsIndex]);
}
}
}
At least this is what I would do with these arguments because is allows you to have one pixels as source and cut out different images in different sizes.
Feel free to correct the algorithm if I am wrong.
So, say you have an image with pixelsWidth and pixelsHeight in pixels.
Then you want to copy a section at pixelsX and pixelsY with width and height to a bitmap at position bitmapX and bitmapY.
This should be the call:
bitmap.setPixels(
pixelsWidth * pixelsY + pixelsX, // offset
pixelsWidth, // stride
bitmapX, // x
bitmapY, // y
width,
height)
probably it should be:
screenBitmap.setPixels(intarray, 0, width / 4, x, y, width / 4, height);
because you have converted byte to int. your error is ArrayOutOfBoundsExceptions. check whether the size intBuf.remaining() = width * height / 4.
If you're trying to draw on a bitmap, your best approach would be to abandon this method and use a canvas instead:
Canvas canvas = new Canvas(screenBitmap);
You can then draw specific points (if you want to draw a pixel), or other shapes like rectangles, circles, etc:
canvas.drawPoint(x, y, paint);
etc
Hope this helps.
I have an already decoded bitmap that I would like to temporarily scale before drawing it on a canvas. So decoding a file and setting the size before is out of the question. I would like to keep the size of the existing bitmap and just scale it to be smaller before drawing it on the canvas. Is this posible?
using Matrix postScale(sx, sy, px, py) scales it correctly but doesn't position it right. And canvas.drawBitmap doesn't have an option with matrix and x & y position from what I can see.
Any suggestions?
Here is the code:
public static Bitmap scaleBitmap(Bitmap bitmap, int width, int height) {
final int bitmapWidth = bitmap.getWidth();
final int bitmapHeight = bitmap.getHeight();
final float scale = Math.min((float) width / (float) bitmapWidth,
(float) height / (float) bitmapHeight);
final int scaledWidth = (int) (bitmapWidth * scale);
final int scaledHeight = (int) (bitmapHeight * scale);
final Bitmap decoded = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true);
final Canvas canvas = new Canvas(decoded);
return decoded;
}
Please note: Pass the bitmap to scale and it's new height and width.
I'm making a game for Android and I need to rotate an image. When I rotate it obviously it's dimensions change. For example when it's rotated 45 degrees (it's square but I'd like this to work for any rectangle so it's a more general solution) it's width and height become the length of the diagonal, which is longer than the original. After some algebra you can work out that the scale factor is sqrt(2). But the only way I know of to rotate a bitmap is with a matrix. Ex:
matrix.postRotate(degrees);
rotated = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
Using this method the size of the bitmap remains constant so to fit the rotated image in the content of the image must shrink. Which causes my problem.
What I have now should work but when run doesn't. Probably because it's overly complex, never the less, here it is:
float totalRotated = 0;
public void rotate(float degrees){
if(mBitmap != null){
float increment = (float)((mBitmap.getWidth()/45.0)*(Math.sqrt(2)-1));
totalRotated += degrees;
totalRotated -= (float)((int)totalRotated/360)*360;
matrix.reset();
matrix.setRotate(totalRotated);
rotated = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
rotated = Bitmap.createScaledBitmap(rotated, (int)(mBitmap.getWidth()+(((Math.abs(Math.abs(((int)totalRotated%90)-45)-45)))*increment)), (int)(mBitmap.getHeight()+(((Math.abs(Math.abs(((int)totalRotated%90)-45)-45)))*increment)), true);
}
}
Using the Log.d function I was able to determine that the dimensions set in the last statement are what I expect them to be but the image doesn't change size. Since this doesn't even work, I need a better way to do this or a way to fix my method. Also my method only works for squares. So, how can I do this?
EDIT:
My method does work, I just didn't call setBounds() This can't be the only way to do it though, this is so inefficient.
It's not clear what you're looking for, so here's a function based on yours that attempts to compute the proper width and height of the new bitmap and do the rotation by creating just a single bitmap.
float totalRotated = 0;
public void rotate(float degrees){
if(mBitmap != null){
// compute the absolute rotation
totalRotated = (totalRotated + degrees) % 360;
// precompute some trig functions
double radians = Math.toRadians(totalRotated);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
// figure out total width and height of new bitmap
int newWidth = mBitmap.getWidth() * cos + mBitmap.getHeight() * sin;
int newHeight = mBitmap.getWidth() * sin + mBitmap.getHeight() * cos;
// set up matrix
matrix.reset();
matrix.setRotate(totalRotated);
// create new bitmap by rotating mBitmap
rotated = Bitmap.createBitmap(mBitmap, 0, 0,
newWidth, newHeight, matrix, true);
}
}
I tried Gabe's solution and got the same errors that Ramesh and Regis got. This worked for me:
double radians = Math.toRadians(totalRotated);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
// figure out total width and height of new bitmap
final int width = mBitmap.getWidth();
final int height = mBitmap.getHeight();
final int newWidth = (int) (width * cos + height * sin);
final int newHeight = (int) (width * sin + height * cos);
// set up matrix
final Matrix tf = new Matrix();
tf.postRotate((float) Math.toDegrees(radians), width / 2, height / 2);
tf.postTranslate(
(newWidth - width) / 2,
(newHeight - height) / 2);
// create new bitmap by rotating mBitmap with canvas
final Bitmap rotatedBmp = Bitmap.createBitmap(
newWidth, newHeight, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(rotatedBmp);
canvas.drawBitmap(mBitmap, tf, null);