Android16 Camera2录像中拍照后预览模糊问题分析与解决总结

Android16 Camera2录像中拍照后预览模糊问题分析与解决总结

文章目录

一、前言

Android16 AML Y3 方案的原生相机Camera2 录像过程中按快门拍照,停止录像后立即 滑入胶片流查看预览刚拍的照片,照片很模糊 ;

继续滑动查看之前的 照片则正常。

用户假设:"是不是有滑动阶段和预览阶段,滑动阶段就会模糊?"

结论:假设成立,而且比猜的更严重 ------ 确实存在分级渲染,但预览阶段(全分辨率档)在滑动浏览时从头到尾不会被进入 ,

只有双击缩放 / 双指捏合才会触发。所以看到的一直是"滑动档"。

这里把问题现象、根因分析和修改方案做个总结,方便大家快速上手。

Android16 其他的方案好像不没有,可能是不同的系统版本差异代码差异会比较大。


二、分析解决

模糊效果热如下:

左边是视频预览正常的,右边是录像过程的图片相机中预览效果是模糊的,从相册应用查看是清晰的。

1、结论先行

问题 结论
有没有分阶段 有 。RenderSize 共三档:TINY → THUMBNAIL(屏幕档)→ FULL_RES
滑动浏览会进到哪一档 只到 THUMBNAIL 。全程 renderFullRes 调用 0 次(日志实测)
renderFullRes 什么时候才会被调 整个 FilmstripView 里只有 2 处 调用,都在缩放手势里:onDoubleTap 和 onScale 捏合。正常滑动永远到不了
那段写着"确保全分辨率"的代码呢 onScrollEnd() 打印 Ensuring that items are at full resolution.,实际调的却是 renderThumbnail() ;而 renderThumbnail() 在已经是 THUMBNAIL 时直接 return ------ 彻底的空操作(双重 bug:调错方法 + 守卫短路)
屏幕档到底多大 loadScreen 被 MAXIMUM_SMOOTH_PIXELS = 1,000,000(1MP )钳位 → 在 4K 面板上解码成 1333×750 ,再放大 2.88× 铺满 3840×2160 → 这就是"模糊"
为什么"之前的照片正常" 旧照片源分辨率更高,同样 1333×750 里承载的真实细节更多;加上新项插入时还会被降档抖动
还有第二个独立缺陷吗 有 。录像中拍照的 JPEG 只有 1920×1080 (HAL 侧硬证据),而录像是 3840×2160。本次按要求未修改

简单的说相机预览界面显示有滑动档和全分辨率查看档,如果在滑动档查看就会画面模糊。

2、三级渲染档位实测对照表

代码位置:widget/FilmstripView.java → data/PhotoItem.java → data/GlideFilmstripManager.java

按本机 3840×2160 面板计算:

档位 加载器 像素上限常量 实际解码尺寸 铺满 4K 的放大倍数
TINY loadTinyThumb 256×256 256×256 15×
THUMBNAIL(屏幕档) loadScreen 1,000,000 1333×750 2.88× ← 滑动浏览只到这里
FULL_RES loadFull 4,500,000 1920×1080(本例源) 2.00×

钳位算法:ratio = min(sqrt(maxArea / (w*h)), 1.0)

→ sqrt(1,000,000 / 8,294,400) = 0.347 → 3840×0.347 = 1333,2160×0.347 = 750。

关键:MAXIMUM_SMOOTH_PIXELS 这个 1MP 常量是按 2014 年的 1080p 手机 定的,

放到 4K 面板上就是 1/8 的屏幕像素。

3、日志证据链

renderFullRes 全程 0 次(决定性):

复制代码
$ grep -c "renderFullRes"     → 0
$ grep -c "renderThumbnail"   → 33
$ grep -c "full resolution"   → 16      ← 只打印了日志,没真正执行

renderFullRes() 里有 Log.i(TAG, "[ViewItem:N] mData.renderFullRes()"),

只要被调到就一定有日志。0 次 = 从未进入全分辨率档。

停止录像后的关键时序:

复制代码
21:29:30.623 CAM_VideoModule: stopVideoRecording          ← 停止录像
21:29:30.959 FS: [ViewItem:0] mData.renderThumbnail()     ← 新项只渲染到屏幕档(1333x750)
21:29:33.907 FS: [fling] mController.isScrolling() - true ← 用户滑入胶片流
21:29:34.721 FS: [fling] onScrollEnd() - onScrollEnd
21:29:40.570 FS: [fling] onScrollEnd() - Ensuring that items are at full resolution.  ← 命中
21:29:40.570 FS: [ViewItem:4] mData.renderThumbnail()     ← 但调的还是 renderThumbnail!

两个要点:

  1. 从照片出现到 9.6 秒后,一次"全分辨率"都没触发。 原因是 onScrollEnd() 先调 scrollCurrentItemToCenter(),只有居中才进入那段代码;用户连续滑动时居中动画一直被取消,分支进不去。
  2. 即便命中了,调的也是 renderThumbnail() ,对已经是 THUMBNAIL 的项是空操作 → 画质毫无变化。

源码侧交叉验证:

复制代码
$ grep -n "renderFullRes" widget/FilmstripView.java
224:        public void renderFullRes() {              ← 定义
2665:                renderFullRes(BUFFER_CENTER);     ← onDoubleTap → zoomAt
3011:                renderFullRes(BUFFER_CENTER);     ← onScale 捏合 → onEnterZoomView

只有缩放手势两处。 日志(0 次)与源码(无其他调用点)互相印证。

4、未修复的第二个缺陷:录像中拍照只有 1920×1080

本次按评审决定只修复渲染阶段(缺陷①),此项仅记录,建议另开单。

HAL 侧硬证据:

复制代码
21:29:28.876 EmulatedCamera_FakeCamera3: processCaptureRequest::
             mainsize_width=1920,mainsize_height=1080          ← 主图只要了 1920x1080

而同一时刻录像链路是 4K:

复制代码
21:29:25.205 USBSensorHWDec: setOutputFormat - set mjpeg 3840x2160

根因(应用侧):

VideoModule.initializeRecorder() 传的是预览尺寸 1920x1080:

java 复制代码
Size optimalSize = CameraUtil.getOptimalVideoSnapshotPictureSize(supported,
        mDesiredPreviewWidth, mDesiredPreviewHeight);   // ← 传的是【预览】尺寸 1920x1080

CameraUtil.getOptimalVideoSnapshotPictureSize() 第一条规则就是 "WYSIWYG Override"------精确匹配预览尺寸就直接返回:

java 复制代码
for (Size size : sizes) {
    if (size.height() == targetHeight && size.width() == targetWidth) {
        return size;        // ← 预览是 1920x1080,这里立刻返回 1920x1080
    }
}

HAL 其实支持 4K 静拍(kUsbAvailablePictureSize[] 第一项就是 {3840, 2160})。

5、修改方案(已实施:缺陷① 渲染阶段)

修改 1(核心):onScrollEnd() 真正进入全分辨率档

widget/FilmstripView.java --- 把名不副实的 renderThumbnail(BUFFER_CENTER) 换成 renderFullRes:

java 复制代码
if (isCurrentItemCentered()) {
    Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at full resolution.");
    renderFullRes(BUFFER_CENTER);        // ← 原来是 renderThumbnail(BUFFER_CENTER),空操作
    renderThumbnail(BUFFER_CENTER + 1);  // 邻项保持屏幕档,保证再次滑动流畅
    renderThumbnail(BUFFER_CENTER - 1);
    renderThumbnail(BUFFER_CENTER + 2);
}

修改 2:新项插入即居中时直接给全分辨率

widget/FilmstripView.java → updateViewItem():

java 复制代码
if (!mController.isScrolling() || !mIsUserScrolling) {
    if (bufferIndex == BUFFER_CENTER) {
        renderFullRes(bufferIndex);
    } else {
        renderThumbnail(bufferIndex);
    }
}

修改 3:reload() 后补一次中心项全分辨率

java 复制代码
renderAllThumbnails();
renderFullRes(BUFFER_CENTER);   // 重新进入胶片流时无需滑动/缩放即清晰

FilmstripView.java 需要修改三处代码。

修改 4(关键陷阱):VideoItem.renderFullRes() 原来是空实现

data/VideoItem.java 原本是 public void renderFullRes(View view) { },

停止录像后最新一项恰好是刚录完的视频,若不处理会把问题改坏。视频没有比"屏幕尺寸帧"更大的表示,因此委托给 renderThumbnail:

java 复制代码
@Override
public void renderFullRes(@Nonnull View view) {
    renderThumbnail(view);
}

修改 5:像素预算适配 4K 面板

data/GlideFilmstripManager.java:

常量 改前 改后 说明
MAXIMUM_SMOOTH_PIXELS 1.0MP 2.0MP 滑动档。作用于全部 5 项,只适度上调
MAXIMUM_FULL_RES_PIXELS 4.5MP 8.3MP 全分辨率档。只作用于居中 1 项,放开到可铺满 4K

6、修复效果(量化)

场景 改前 改后
录像中拍的照片(源 1920×1080) 1333×750,放大 2.88× 1920×1080,放大 2.00×(源分辨率已用尽)
照片模式拍的照片(源 3840×2160) 1333×750,放大 2.88× 3840×2160,1:1 像素级
滑动过程中(未落位) 1333×750 1886×1061
刚录完的视频项 屏幕档 屏幕档(行为不变,但不再有空渲染风险)

诚实说明 :录像中拍的照片源只有 1920×1080 ,所以改完仍是 2.00× 放大,

相比改前的 2.88× 有明显改善(纵向细节 +44%),但做不到像素级清晰 。

要彻底解决必须同时修第二个缺陷。


完整的git修改:

复制代码
diff --git a/apps/Camera2/src/com/android/camera/app/CameraController.java b/apps/Camera2/src/com/android/camera/app/CameraController.java
old mode 100644
new mode 100755
index 32002eb38..95f996e51
--- a/apps/Camera2/src/com/android/camera/app/CameraController.java
+++ b/apps/Camera2/src/com/android/camera/app/CameraController.java
@@ -174,6 +174,23 @@ public class CameraController implements CameraAgent.CameraOpenCallback, CameraP
         }
         mCameraProxy = camera;
         mRequestingCameraId = EMPTY_REQUEST;
+        // modify by lwz start, 20261015
+        // The device info snapshot is taken while the camera is being opened.
+        // Right after boot that can happen before the camera service has
+        // finished enumerating the USB/UVC camera, leaving us with a snapshot
+        // that reports no devices. A successful open proves the device is
+        // there, so re-read the info before any listener queries it.
+        if (mInfo == null || mInfo.getCharacteristics(camera.getCameraId()) == null) {
+            Log.w(TAG, "Stale camera device info, refreshing for camera "
+                    + camera.getCameraId());
+            CameraAgent agent = (mUsingNewApi && mCameraAgentNg != null)
+                    ? mCameraAgentNg : mCameraAgent;
+            CameraDeviceInfo refreshed = agent.getCameraDeviceInfo();
+            if (refreshed != null) {
+                mInfo = refreshed;
+            }
+        }
+        // modify by lwz end
         if (mCallbackReceiver != null) {
             mCallbackReceiver.onCameraOpened(camera);
         }
diff --git a/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java b/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
old mode 100644
new mode 100755
index 83363f0ed..72590b54f
--- a/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
+++ b/apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java
@@ -85,13 +85,29 @@ public final class GlideFilmstripManager {
     // Give a 20% margin for error and real conditions.
     private static final int EST_PIXELS_PER_MILLI = 100000;
 
+    // modify by lwz start, 20261015
+    // These budgets were tuned for 1080p phones. On a 4K (3840x2160 = 8.3MP)
+    // IFPD panel the original 10ms/1MP "smooth" budget means a settled
+    // full-screen preview is only ever decoded at ~1334x750 and then upscaled
+    // 2.9x, which reads as "blurry". The scrolling budget is raised only
+    // moderately (it applies to all BUFFER_SIZE=5 buffered items), while the
+    // full-res budget - which in practice only applies to the single centered
+    // item - is allowed to fill the panel.
+    //
+    // Memory (ARGB_8888, 4 bytes/pixel):
+    //   scrolling : 5 items * 2MP   * 4B = ~40MB
+    //   full res  : 1 item  * 8.3MP * 4B = ~33MB  (also capped by
+    //               MAX_GL_TEXTURE_SIZE 4096 in getMaxImageDisplaySize())
+    // The app runs with android:largeHeap="true", so this is affordable.
+
     // Estimated number of bytes that can be used to usually display a thumbnail
     // in under a frame at 60fps (16ms).
-    public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 10 /* millis */;
+    public static final int MAXIMUM_SMOOTH_PIXELS = EST_PIXELS_PER_MILLI * 20 /* millis */;
 
-    // Estimated number of bytes that can be used to generate a large thumbnail in under
-    // (about) 3 frames at 60fps (16ms).
-    public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 45 /* millis */;
+    // Budget for the image that is actually being looked at. Sized to fill a
+    // 4K panel rather than the original 45ms/4.5MP estimate.
+    public static final int MAXIMUM_FULL_RES_PIXELS = EST_PIXELS_PER_MILLI * 83 /* millis */;
+    // modify by skg end
     public static final int JPEG_COMPRESS_QUALITY = 90;
 
     private final GenericRequestBuilder<Uri, ?, ?, GlideDrawable> mTinyImageBuilder;
diff --git a/apps/Camera2/src/com/android/camera/data/VideoItem.java b/apps/Camera2/src/com/android/camera/data/VideoItem.java
old mode 100644
new mode 100755
index 8c263a328..ec152207c
--- a/apps/Camera2/src/com/android/camera/data/VideoItem.java
+++ b/apps/Camera2/src/com/android/camera/data/VideoItem.java
@@ -211,7 +211,16 @@ public class VideoItem extends FilmstripItemBase<VideoItemData> {
     }
 
     @Override
-    public void renderFullRes(@Nonnull View view) { }
+    public void renderFullRes(@Nonnull View view) {
+        // modify by skg start, 20261015
+        // This used to be an empty body. Now that the filmstrip promotes the
+        // centered item to full resolution (see FilmstripView.onScrollEnd and
+        // updateViewItem), a no-op here would leave a just-recorded video
+        // showing only its small MediaStore thumbnail. A video has no
+        // representation larger than its screen-sized frame, so render that.
+        renderThumbnail(view);
+        // modify by skg end
+    }
 
     @Override
     public void recycle(@Nonnull View view) {
diff --git a/apps/Camera2/src/com/android/camera/widget/FilmstripView.java b/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
old mode 100644
new mode 100755
index fa5476f2f..b681bc604
--- a/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
+++ b/apps/Camera2/src/com/android/camera/widget/FilmstripView.java
@@ -1716,7 +1716,18 @@ public class FilmstripView extends ViewGroup {
         // is unreliable. Load the full resolution if either value
         // reports that the item is not scrolling.
         if (!mController.isScrolling() || !mIsUserScrolling) {
-            renderThumbnail(bufferIndex);
+            // modify by skg start, 20261015
+            // The comment above says "full resolution" but this only rendered a
+            // thumbnail. A photo taken during video recording is inserted here,
+            // so if the user is already sitting on it (nothing scrolling) it was
+            // left at the ~1MP "smooth" size and looked blurry until they
+            // scrolled away and back. Promote the centered item properly.
+            if (bufferIndex == BUFFER_CENTER) {
+                renderFullRes(bufferIndex);
+            } else {
+                renderThumbnail(bufferIndex);
+            }
+            // modify by skg end
         }
 
         adjustChildZOrder();
@@ -1845,6 +1856,13 @@ public class FilmstripView extends ViewGroup {
 
         Log.d(TAG, "reload() - Ensure all items are loaded at max size.");
         renderAllThumbnails();
+        // modify by lwz start, 20261015
+        // renderAllThumbnails() only reaches RenderSize.THUMBNAIL, so after a
+        // reload the item on screen was left at the ~1MP "smooth" size. Promote
+        // the centered item to full resolution so re-entering the filmstrip
+        // shows a sharp image without requiring a scroll or a zoom gesture.
+        renderFullRes(BUFFER_CENTER);
+        // modify by lwz end
         invalidate();
 
         if (mListener != null) {
@@ -1961,12 +1979,22 @@ public class FilmstripView extends ViewGroup {
                             // we want to ensure the item that is "most in view" is
                             // the first one rendered at max size.
 
+                            // modify by lwz start, 20261015
+                            // This used to call renderThumbnail() for the centered
+                            // item, which is a no-op once the item is already at
+                            // RenderSize.THUMBNAIL - so renderFullRes() was never
+                            // reached by swiping and the settled preview stayed at
+                            // the ~1MP "smooth" size (visibly soft on a 4K panel).
+                            // The item being looked at is now promoted to full
+                            // resolution; its neighbours stay at thumbnail size so
+                            // that scrolling away again remains smooth.
                             Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at"
                                   + " full resolution.");
-                            renderThumbnail(BUFFER_CENTER);
+                            renderFullRes(BUFFER_CENTER);
                             renderThumbnail(BUFFER_CENTER + 1);
                             renderThumbnail(BUFFER_CENTER - 1);
                             renderThumbnail(BUFFER_CENTER + 2);
+                            // modify by lwz end
                         }
                     }
                 };

三、其他

1、小结

  1. 胶片流有三级渲染档位 :TINY(256×256)→ THUMBNAIL(屏幕档,1MP 钳位)→ FULL_RES(全分辨率)。滑动浏览只到屏幕档,全分辨率档只有双击/捏合缩放才触发;
  2. "确保全分辨率"的日志是假的 :onScrollEnd() 打印 Ensuring that items are at full resolution.,实际调的却是 renderThumbnail(),而该方法在已是 THUMBNAIL 时直接 return ------ 双重 bug(调错方法 + 守卫短路);
  3. 屏幕档在 4K 面板上只有 1333×750 ,放大 2.88× 铺满 3840×2160,这就是模糊的根因。MAXIMUM_SMOOTH_PIXELS(1MP)是按 2014 年 1080p 手机定的,4K 面板需要复核;
  4. 修复核心 :onScrollEnd() 居中时改调 renderFullRes;新项插入即居中时直接给全分辨率;reload() 后补一次中心项全分辨率;
  5. 关键陷阱 :VideoItem.renderFullRes() 原本是空实现,启用全分辨率路径前必须检查所有子类,否则会把视频项改坏;
  6. 第二个独立缺陷 :录像中拍照只有 1920×1080(HAL 侧硬证据),根因是 VideoModule 传了预览尺寸给 getOptimalVideoSnapshotPictureSize(),WYSIWYG 规则精确匹配预览就返回了。本次未修,建议另开单;
  7. 经验:日志文案和代码行为可能完全相反,别拿日志当需求文档;grep 计数对比(期望日志 vs 实际日志)是很高效的取证手段。

2、改动文件清单

全部位于 packages 仓(release/packages,独立 git 仓库):

文件 改动
apps/Camera2/src/com/android/camera/widget/FilmstripView.java onScrollEnd() 改调 renderFullRes;updateViewItem() 居中项升档;reload() 补中心项全分辨率
apps/Camera2/src/com/android/camera/data/VideoItem.java renderFullRes() 由空实现改为委托 renderThumbnail()
apps/Camera2/src/com/android/camera/data/GlideFilmstripManager.java MAXIMUM_SMOOTH_PIXELS 1MP→2MP;MAXIMUM_FULL_RES_PIXELS 4.5MP→8.3MP

3、验证步骤

主用例(原问题):

  1. 进入相机 → 切到录像模式 → 开始录像(4K);
  2. 录像过程中按快门拍 1~2 张;
  3. 停止录像,立即滑入胶片流查看刚拍的照片;
  4. 预期:落位后立刻清晰(1920×1080 原生,不再是 1333×750 放大),不需要来回滑动或双击缩放。

日志验证:

bash 复制代码
# 改前:0 次;改后落位时应当出现
adb logcat -s CAM_FilmstripView | grep "renderFullRes"

# 应当与 "Ensuring that items are at full resolution." 成对出现
adb logcat -s CAM_FilmstripView | grep -E "full resolution|renderFullRes"

回归用例(重点关注内存与流畅度):

用例 预期
照片模式拍照后立即预览 清晰(源 4K → 1:1 像素级)
胶片流快速连续滑动 20+ 张 不卡顿、不白屏;滑动中走 1886×1061 档
停止录像后查看最新的"视频"项 缩略图正常显示(修改 4 的回归点,务必验证)
视频项双击 / 捏合缩放 画面正常(原先是空渲染)
大相册(100+ 张)反复进出胶片流 无 OOM、无明显 GC 抖动

以上就是录像中拍照后预览模糊问题的分析与解决总结,整体下来:

问题根因是胶片流的全分辨率渲染路径在正常滑动时永远走不到,加上 4K 面板用了 1080p 时代的像素预算常量。

修复后落位即清晰,但录像中拍照的源分辨率偏低是第二个独立缺陷,需要另开单处理。

有问题欢迎评论区交流。

相关推荐
漫步人生走在路上1 小时前
用小语种SEO突破外贸B2B天花板:法语、德语市场的实测数据
开发语言·前端·javascript·人工智能·搜索引擎·ecmascript
二川bro1 小时前
签到自动化
人工智能
白日放歌须纵久1 小时前
Agent 工具契约测试工程:适配层隔离与 Mock 回放,让接口变更不再静默击穿你的工作流
人工智能
火山引擎开发者社区1 小时前
一句话,做出一个应用:豆包背后的数据库底座是什么?
人工智能
humors2211 小时前
如何识别网络假信息
网络·人工智能·ai·互联网·识别·谣言·假消息
技灵AI1 小时前
AI 奶茶广告分镜提示词怎么写:从参考图到芒果奶茶广告片(附全文)
人工智能·prompt·aigc
阿乔外贸日记1 小时前
美国买家资产包装套路与应收款尽调框架,580万应收账款案例解析
大数据·服务器·人工智能·microsoft·云计算
面壁的熊猫1 小时前
Jev-LiteMAS 论文解读:把多智能体工作流里的「决策」与「生成」拆开
人工智能
火山引擎开发者社区1 小时前
AgentKit AIOps Agent:故障一键定位,运维提效 80%
人工智能