【问题标题】:Preserve the image quality when decode stream in android在android中解码流时保持图像质量
【发布时间】:2014-01-07 00:22:28
【问题描述】:

我在 sdcard 上有一张图片,需要在图片视图中显示

问题是解码后质量似乎变差了。有什么方法可以在保持质量的同时保留记忆吗?

或者,如果我使用更大的图像,是否有任何方法可以通过缩放来保留内存(避免加载太大的位图)? (我需要保持原图的大小)

感谢您的帮助。

public Bitmap decodeFile(String pubKey, int bookPageID, int type)
        throws IOException {
    Bitmap b = null;
    File f = null;
    String uri = null;
    FileInputStream fis = null;

    Log.d(TAG,"pageID to read: " + bookPageID);

    IRIssue issue = Broker.model.issueDataStore.getIRIssue(pubKey);

    String imageFolder = IRConstant.issueFolder(issue.year, issue.month, issue.day, issue.pubKey);

    // pageID - 1 since the page is an array (start at 0) , but page ID start at 1
    if (type == 2){
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].graphicUri;
    }else {
        uri = imageFolder + issue.vol[0].pages[bookPageID - 1].textUri;
    }

    f = new File(uri);

    Log.d(TAG,"is file: " + uri + " exist?" + f.exists());

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inJustDecodeBounds = false;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, options);
    fis.close();

    return b;
}

【问题讨论】:

  • 试试这个 options.inScaled = false; options.inDither = false;
  • 仍然报错“位图太大,无法上传到纹理中”
  • 我发现问题是位图大小> 2048 * 2048 引起的,如何解决,我试过缩放但质量不好?谢谢
  • 如何缩放图像?位图的 options.inSampleSize 对图像进行缩放,不影响质量。
  • 我的方法是 1. 通过使用 injustdecodebounds = true 解码位图来获取图像边界 2. 通过我在下面提供的方法找出 insamplesize 3. 而不是使用 BitmapFactory.decodeStream(fis , 空, 选项);有适当的选项。

标签: android image android-layout bitmap android-largeheap


【解决方案1】:

以下代码使用了来自Displaying Bitmaps Efficiently的几个概念
首先,位图读取是在后台线程中完成的,我在inputStream 上使用标记/重置(用BufferedInputstream 包装)当我们试图找出图像的大小时,不会从流中读取超出必要的内容在计算比例因子时使用。下面的示例代码对图像进行二次采样以匹配 320x240 像素的大小。在非示例代码中,可以让简单的回调接口将位图从onPostExecute 发送到实现类(回调接口实现者)。或者直接将视图作为成员提供给AsyncTask,并在onPostExecute 中设置位图。

使用(我设备上下载的图像示例)调用代码:

BitmapTask task = new BitmapTask(getContentResolver());
task.execute(Uri.parse("file:///storage/emulated/0/Download/download.jpg"));

有问题的课程

private static class BitmapTask extends AsyncTask<Uri, Void, Bitmap> {

    // prevent mem leaks
    private WeakReference<ContentResolver> mWeakContentResolver;

    public BitmapTask(ContentResolver resolver) {
        mWeakContentResolver = new WeakReference<ContentResolver>(resolver);
    }

    @Override
    protected Bitmap doInBackground(Uri... params) {
        Bitmap bitmap = null;
        ContentResolver resolver = mWeakContentResolver.get();
        if (resolver != null) {
            BufferedInputStream stream = null;
            try {
                stream = new BufferedInputStream(
                        resolver.openInputStream(params[0]));
                stream.mark(1 * 1024);
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = true;
                // Find out size of image
                BitmapFactory.decodeStream(stream, null, options);
                try {
                    stream.reset();
                } catch (IOException e) {
                    Log.d(TAG, "reset failed");
                }
                int imageHeight = options.outHeight;
                int imageWidth = options.outWidth;
                String imageType = options.outMimeType;
                Log.d(TAG, "w, h, mime " + imageWidth + " , " + imageHeight
                        + " , " + imageType);
                options.inJustDecodeBounds = false;
                // Calculate down scale factor
                options.inSampleSize = calculateInSampleSize(options, 320,
                        240);
                return BitmapFactory.decodeStream(stream, null, options);
            } catch (FileNotFoundException e) {
                bitmap = null;
            } finally {
                IOUtils.closeStreamSilently(stream);
            }
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        Log.d(TAG,
                "bitmap result: "
                        + ((result != null) ? "" + result.getByteCount()
                                : "0"));
        result.recycle();
    }
}

public static int calculateInSampleSize(BitmapFactory.Options options,
        int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

编辑: 对于大型输入流,标记/重置技术可能存在问题,SkImageDecoder::Factory returned null 有时可以在日志中看到,导致位图为空,其他关于此事的 SO 问题: SkImageDecoder::Factory returned null。可以通过在返回doInBackground 之前再次重新初始化流变量stream = new resolver.openInputStream(params[0])); 来修复它

编辑 2: 如果您必须保留图像大小但又不想限制内存使用量,您可以使用 options.inPreferredConfig = Bitmap.Config.RGB_565; 将每个像素的内存减半,但请记住图像可能不再有很好的质量(实验!)。

【讨论】:

    【解决方案2】:

    GridViewActivity.java

    public class GridViewActivity extends Activity implements OnItemClickListener {
        private String[] filepathstring;
        private File[] listfile;
        GridView grid_sdcard;
        File file;
        ImageView image;
        GridViewAdapter adapter;
        int select;
        int sele;
    
    
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            this.requestWindowFeature(Window.FEATURE_NO_TITLE);
            setContentView(R.layout.gridview_activity);
    
            image=(ImageView)convertView.findViewById(R.id.image_show);
            grid_sdcard=(GridView)findViewById(R.id.grid_sdcard);
    
    
            if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
            {
            Toast.makeText(this,"Error! No SDCARD Found!", Toast.LENGTH_LONG).show();   
            }
            else
            {
                file=new File(Environment.getExternalStorageDirectory() + File.separator +"eMENU Images/");
                file.mkdirs();
                Toast.makeText(GridViewActivity.this,"Past Image Here:", Toast.LENGTH_LONG).show();
            }
            if(file.isDirectory())
            {
                listfile=file.listFiles();
                for(int i=0;i<listfile.length;i++)
                {
                    filepathstring[i]=listfile[i].getAbsolutePath();
                }
            }
            adapter=new GridViewAdapter(GridViewActivity.this,filepathstring);
            grid_sdcard.setAdapter(adapter);
            grid_sdcard.setOnItemClickListener(this);
        }
    
        @Override
        public void onItemClick(AdapterView<?> parent, View v, int position, long id1) {
            final String image=filepathstring[position];
            Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
            imageshow.setImageBitmap(bitmap);
        }
    }
    

    GridViewAdapter.java

    public class GridViewAdapter extends BaseAdapter {
        String[] filepathlist;
        Context context;
    
    
        public GridViewAdapter(Context con, String[] filepathstring) {
            context=con;
            filepathlist=filepathstring;
        }
    
        @Override
        public int getCount() {
    
            return filepathlist.length;
        }
    
        @Override
        public Object getItem(int position) {
            // TODO Auto-generated method stub
            return position;
        }
    
        @Override
        public long getItemId(int position) {
            // TODO Auto-generated method stub
            return position;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
    
            if(convertView==null)
            {
                LayoutInflater inflater=(LayoutInflater)convertView.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView=inflater.inflate(R.layout.griadview_adapter,null);
            }
            ImageView imageshow=(ImageView)convertView.findViewById(R.id.image_show);
            Bitmap bitmap=BitmapFactory.decodeFile(filepathlist[position]);
            imageshow.setImageBitmap(bitmap);
            return convertView;
        }
    }
    

    【讨论】:

    • @user782104 为什么这被标记为正确答案?在 OPs 问题中没有关于 Grid / GridView / GridAdapter 的声明,那么这怎么可能是正确的?
    【解决方案3】:

    一种高度可配置的快速方法是使用WebView 而不是ImageView

    WebView mWebView = (WebView) findViewById(R.id.webview);
    mWebView.getSettings().setAllowFileAccess(true);
    mWebView.getSettings().setBuiltInZoomControls(true);
    String base = Environment.getExternalStorageDirectory().getAbsolutePath().toString();
    String imagePath = "file://" + base + "/myImage.png";//replace with the name of the image you are accessing
    String html = "<html><head></head><body><img src=\"" + imagePath + "\"></body></html>";
    mWebView.loadDataWithBaseURL("", html, "text/html","utf-8", "");
    

    【讨论】:

    • 使用webview解决了什么问题?上面 90% 的代码是特定于网络的,10% 是特定于图像的。
    • @Magnus,这首先简化了代码,因为解码位图由WebView 处理。此外,OP 可以编辑CSS 以确保正确匹配。
    • ImageView 中的 setImageUri 和 setScaleType 也是这样做的
    • @Magnus,听起来你应该发布一个答案。
    【解决方案4】:

    在解码时添加 options.injustdecodeBounds = true 因为这表明您只想要边界而不是整个位图。这将避免内存错误,因为您只会加载实际需要的图像大小。

    第二件事是根据您的需要缩放该位图,并且要做到这一点而不会失真,您必须以保持纵横比的方式对其进行缩放。在单击图片时,您可以设置单击图像的固定比例,然后仅以该比例缩放该图像。如果它不在您的手中,您可以使用以下方法获取 insample 大小,然后将图像解码为特定大小而不会失真。

    private int calculateSampleSize(int width, int height, int targetWidth, int targetHeight) {
    float bitmapWidth = width;
    float bitmapHeight = height;
    
    int bitmapResolution = (int) (bitmapWidth * bitmapHeight);
    int targetResolution = targetWidth * targetHeight;
    
    int sampleSize = 1;
    
    if (targetResolution == 0) {
        return sampleSize;
    }
    
    for (int i = 1; (bitmapResolution / i) > targetResolution; i *= 2) {
        sampleSize = i;
    }
    
    return sampleSize;
    

    }

    如果您发现任何改进,请提供任何反馈。

    【讨论】:

      【解决方案5】:

      BitmapFactory 有一个 inSampleSize 属性,旨在解决这个问题。参考文档:http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize

      这篇文章是关于高效处理位图的:http://developer.android.com/training/displaying-bitmaps/index.html

      【讨论】:

        【解决方案6】:

        我使用一个自定义的 BitmapHandler 类来解决这个问题:

        public class BitmapHandler {
            private static int IMAGE_MAX_SIZE = 540;  //This can be set to whatever you see fit
            private static String TAG = "BitmapHandler.java";
        
            public BitmapHandler(Context ctx){
                    WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
                    Display display = wm.getDefaultDisplay();
                    Point size = new Point();
                    display.getSize(size);
                    int width = size.x;
                    int height = size.y;
                    Log.v(TAG, "Screen width: " + width + " height: " + height);
                    IMAGE_MAX_SIZE = (Math.min(width, height))*4; //Try playing with this multiplier number to get different degrees of scaling
            }
        
            public Bitmap decodeFileAsPath(String uri) {
                    // Create a file out of the uri
                    File f = null;
                    Log.v(TAG, "Incoming uri: " + uri);
                    f = new File(uri);
        
                    if (f.equals(null)){
                            Log.v(TAG, "File is null!");
                    }
                    return decodeFile(f);
            }
        
            private Bitmap decodeFile(File f) {
                    Bitmap b = null;
                    try {
                            // Decode image size
                            BitmapFactory.Options o = new BitmapFactory.Options();
                            o.inJustDecodeBounds = true;
                            o.inScaled = false;
        
                            FileInputStream fis = new FileInputStream(f);
                            BitmapFactory.decodeStream(fis, null, o);
                            fis.close();
        
                            int scale = 1;
                            Log.v(TAG, "Decode Image height: " + o.outHeight + " and width: " + o.outWidth);
        
                            if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
                                    scale = (int) Math.pow(
                                                    2,
                                                    (int) Math.round(Math.log(IMAGE_MAX_SIZE
                                                                    / (double) Math.max(o.outHeight, o.outWidth))
                                                                    / Math.log(0.5)));
                            }
                            Log.v(TAG, "Final scale: " + scale);
                            // Decode with inSampleSize
                            BitmapFactory.Options o2 = new BitmapFactory.Options();
                            o2.inScaled = false;
                            o2.inSampleSize = scale;
                            fis = new FileInputStream(f);
                            b = BitmapFactory.decodeStream(fis, null, o2);
                            fis.close();
                    } catch (IOException e) {
                            Log.v(TAG, e.getMessage());
                    }
                    return b;
            }
        }
        

        这会动态缩放您的图像,同时尝试防止 OutOfMemoryException

        【讨论】:

        • 我正在处理杂志图片并允许用户设置最大缩放级别,例如150%, 200%..etc...所以每个设备的图像大小应该是相同的。
        • 您可以通过设置宽度和高度属性并使用 android:scaleType:"fitCenter" 在 XML 中进行设置。这样,应用程序上的图像将始终是一种尺寸
        • @SalGad decodeFileAsPath 是不需要的,因为if (f.equals(null)) 永远不会发生,等于将此文件对象与另一个文件对象进行比较,如果f 本来是空的,你有一个此处为 NPE,从而使该方法变得不必要。您可以使用 decodeFile(new File(uri)); 包装对 decodeFile 的调用,并将 decodeFile 设为 public(并删除 decodeFileAsPath)。只是一个友好的想法。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-27
        • 1970-01-01
        • 2019-10-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多