从摄像头中拍摄照片而不预览

58

我正在编写一个 Android 1.5 应用程序,它会在启动后立即运行。这是一个 Service,应该可以不需要预览就拍照。该应用程序将记录某些区域的光密度。我能够拍照,但图片都是黑色的。

经过长时间的研究,我找到了有关此问题的错误线程。如果您不生成预览,则图像将是黑色的,因为Android相机需要预览来设置曝光和对焦。我已经创建了一个 SurfaceView 和监听器,但是 onSurfaceCreated() 事件从未被触发。

我猜原因是视图上没有创建表面。我还看到一些静态调用相机的示例,使用 MediaStore.CAPTURE_OR_SOMETHING 可以通过两行代码拍照并保存到所需的文件夹中,但它也无法拍照。

我是否需要使用 IPC 和 bindService() 来调用此函数?或者是否有另一种方法来实现这个功能?


请参阅_在Android上无预览拍照_。 - Alex Cohn
请查看此库:https://github.com/kevalpatel2106/android-hidden-camera,它提供了后台摄像头功能。 - Keval Patel
9个回答

53

在 Android 平台上,摄像头必须提供有效的预览界面才能流式传输视频,这真的很奇怪。这似乎表明平台架构师完全没有考虑第三方视频流应用程序。即使对于增强现实案例,图像也可以被呈现为某种视觉替代品,而不是实时的摄像头流。

无论如何,您可以简单地将预览界面调整大小为 1x1 像素,并将其放置在小部件(视觉元素)的某个角落。请注意 - 调整预览界面的大小,而不是相机帧的大小。

当然,这种技巧并不能消除不需要的数据流(用于预览),这会消耗一些系统资源和电池。


9
1×1表面有一个限制:在某些设备上(比如三星),当目标尺寸不能被4(或者可能是8)整除时,它可能会变得很慢,因为这些设备无法运行硬件图像转换器。 - Alex Cohn
有些设备上它根本不起作用,会抛出RuntimeException(至少据我所知,旧的Nexus One就是如此)。 - comodoro
2
我怀疑谷歌是有意为之,否则我无法想象他们的架构师会这样规划。 - user1914692
如果您只有服务(例如,当应用程序在后台时想要使用相机)则无法工作。 - mnl
@mnl,这在我的服务中有效。我将预览添加到窗口管理器作为系统覆盖层。 - Sam
@comodoro,它具体是什么原因导致失败的?问题出在 Nexus One 完全不支持调整预览大小吗?还是仅不支持某些尺寸?您改变了 SurfaceHolderSurfaceView 的大小吗?您用什么代码进行更改? - Sam

39
我在Android相机文档中找到了答案。

注意:可以在不先创建相机预览的情况下使用MediaRecorder并跳过此过程的前几个步骤。但是,由于用户通常喜欢在开始录制之前看到预览,因此此过程未在此处讨论。

您可以在上面的链接中找到逐步说明。 在说明之后,将会给出我提供的引用。

1
你按照逐步说明进行了吗?我知道这是最低评级的答案(令人失望),但我按照文档操作,对我来说它很好用。 - Phillip Scott Givens
6
天啊,我还在失去声望分。怎样才能删除这篇帖子?如果你看了说明书,它是有效的!我发誓,它对视频和图片都有效。我发布这个内容是因为我发现它有用。现在,一年过去了,我仍然被人踩。我试着点“删除”链接,但它只会问我是否要“投票删除”这个帖子。请不要再给我踩票了。相反,请在这里回复告诉我如何删除这个东西。 - Phillip Scott Givens
基本上你不能删除,但你可以标记它以引起版主的注意:http://meta.stackexchange.com/questions/25088/how-can-i-delete-my-post-on-stack-overflow - apanloco
1
我发现这个答案很有帮助 - 在我的应用程序中,拍摄短视频似乎比拍照更好。 - NumberFour
5
非常有趣,非常感谢。我会尝试一下的。不用担心那些“踩”票! - user2357448
显示剩余2条评论

37

实际上这是可能的,但您需要使用虚假的SurfaceView来伪造预览。

SurfaceView view = new SurfaceView(this);
c.setPreviewDisplay(view.getHolder());
c.startPreview();
c.takePicture(shutterCallback, rawPictureCallback, jpegPictureCallback);

更新于 2011 年 9 月 21 日:显然,这并不适用于每个 Android 设备。


8
使用SurfaceTexture并在4.0以上版本中设置setSurfaceTexture。 - HannahMitt
2
为了让它在每个设备上都能正常工作,必须在某个地方添加并实际创建surface,最好使用holder的回调函数。只有当视图可见且大小不为0x0时,才会调用回调函数。setAlpha(0)似乎可以,但仅适用于API 11及以上版本。 - 3c71
只是确认一下.. 在Galaxy Nexus上也无法工作 RuntimeException: takePicture 失败 - Jakob Harteg
@3c71,使用透明的PixelFormatsetAlpha(0)在我的运行Android 4.3的Sony Xperia M上并没有使其透明。预览仍然是不透明的。 - Sam
@3c71,实际上在SurfaceHolder上使用setFormat(PixelFormat.TRANSPARENT)并在SurfaceView构造函数中使用相同的PixelFormat后,setAlpha(0)确实起作用了。 - Sam
这返回的是黑色位图! - Muhammad Babar

36

拍照

在尝试隐藏预览之前,先确保此部分工作正常。

  • 正确设置预览
    • 使用 SurfaceView(用于 Android 4.0 以下的兼容性)或 SurfaceTexture(Android 4+,可设置为透明)
    • 在拍照之前设置和初始化它
    • 在设置和初始化预览之前,请等待 SurfaceViewSurfaceHolder(通过 getHolder())报告 surfaceCreated()TextureView 报告 onSurfaceTextureAvailable 给其 SurfaceTextureListener.
  • 确保预览可见:
    • 将其添加到 WindowManager
    • 确保其布局大小至少为 1x1 像素(您可能想先将其设置为 MATCH_PARENT x MATCH_PARENT 进行测试)
    • 确保其可见性为 View.VISIBLE(如果没有指定,则似乎是默认值)
    • 如果是 TextureView,请确保在 LayoutParams 中使用 FLAG_HARDWARE_ACCELERATED
  • 使用 takePicture 的 JPEG 回调,因为文档说明其他回调不受所有设备支持

故障排除

  • 如果未调用 surfaceCreated/onSurfaceTextureAvailable,则可能未显示 SurfaceView / TextureView
  • 如果 takePicture 失败,请先确保预览正常工作。您可以删除 takePicture 调用,让预览运行以查看是否在屏幕上显示。
  • 如果图片比应该暗,可能需要延迟约一秒钟再调用 takePicture,以便相机在预览开始后有时间调整其曝光。

隐藏预览

  • Make the preview View 1x1 size to minimise its visibility (or try 8x16 for possibly more reliability)

    new WindowManager.LayoutParams(1, 1, /*...*/)
    
  • Move the preview out of the centre to reduce its noticeability:

    new WindowManager.LayoutParams(width, height,
        Integer.MIN_VALUE, Integer.MIN_VALUE, /*...*/)
    
  • Make the preview transparent (only works for TextureView)

    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
        width, height, /*...*/
        PixelFormat.TRANSPARENT);
    params.alpha = 0;
    

工作示例(在索尼Xperia M,Android 4.3上测试通过)

/** Takes a single photo on service start. */
public class PhotoTakingService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        takePhoto(this);
    }

    @SuppressWarnings("deprecation")
    private static void takePhoto(final Context context) {
        final SurfaceView preview = new SurfaceView(context);
        SurfaceHolder holder = preview.getHolder();
        // deprecated setting, but required on Android versions prior to 3.0
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

        holder.addCallback(new Callback() {
            @Override
            //The preview must happen at or after this point or takePicture fails
            public void surfaceCreated(SurfaceHolder holder) {
                showMessage("Surface created");

                Camera camera = null;

                try {
                    camera = Camera.open();
                    showMessage("Opened camera");

                    try {
                        camera.setPreviewDisplay(holder);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                    camera.startPreview();
                    showMessage("Started preview");

                    camera.takePicture(null, null, new PictureCallback() {

                        @Override
                        public void onPictureTaken(byte[] data, Camera camera) {
                            showMessage("Took picture");
                            camera.release();
                        }
                    });
                } catch (Exception e) {
                    if (camera != null)
                        camera.release();
                    throw new RuntimeException(e);
                }
            }

            @Override public void surfaceDestroyed(SurfaceHolder holder) {}
            @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
        });

        WindowManager wm = (WindowManager)context
            .getSystemService(Context.WINDOW_SERVICE);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                1, 1, //Must be at least 1x1
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                0,
                //Don't know if this is a safe default
                PixelFormat.UNKNOWN);

        //Don't set the preview visibility to GONE or INVISIBLE
        wm.addView(preview, params);
    }

    private static void showMessage(String message) {
        Log.i("Camera", message);
    }

    @Override public IBinder onBind(Intent intent) { return null; }
}

这是关于这个主题的详细答案,附带一个可行的示例!谢谢。 - NumberFour
此解决方案在API级别>=23上不再适用。无法添加previewandroid.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@664dc37 -- permission denied for window type 2006 - Ruchir Baronia
@RuchirBaronia,这里有几个选项:
  1. 使用ML Kit团队使用的技术
  2. 切换到camera2 API
- Sam

22
在Android 4.0及以上版本(API级别>=14),您可以使用TextureView预览相机流并使其不可见,以便不向用户显示。以下是实现方式:
首先创建一个类来实现SurfaceTextureListener,该类将获取预览表面的create/update回调。此类还需要一个相机对象作为输入,以便在表面创建后立即调用相机的startPreview函数:
public class CamPreview extends TextureView implements SurfaceTextureListener {

  private Camera mCamera;

  public CamPreview(Context context, Camera camera) {
    super(context);
    mCamera = camera;
   }

  @Override
  public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
    setLayoutParams(new FrameLayout.LayoutParams(
        previewSize.width, previewSize.height, Gravity.CENTER));

    try{
      mCamera.setPreviewTexture(surface);
     } catch (IOException t) {}

    mCamera.startPreview();
    this.setVisibility(INVISIBLE); // Make the surface invisible as soon as it is created
  }

  @Override
  public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
      // Put code here to handle texture size change if you want to
  }

  @Override
  public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return true;
  }

  @Override
  public void onSurfaceTextureUpdated(SurfaceTexture surface) {
      // Update your view here!
  }
}

你还需要实现一个回调类来处理预览数据:
public class CamCallback implements Camera.PreviewCallback{
  public void onPreviewFrame(byte[] data, Camera camera){
     // Process the camera data here
  }
}

使用上面的CamPreview和CamCallback类,在您的活动的onCreate()或类似的启动函数中设置相机:
// Setup the camera and the preview object
Camera mCamera = Camera.open(0);
CamPreview camPreview = new CamPreview(Context,mCamera);
camPreview.setSurfaceTextureListener(camPreview);

// Connect the preview object to a FrameLayout in your UI
// You'll have to create a FrameLayout object in your UI to place this preview in
FrameLayout preview = (FrameLayout) findViewById(R.id.cameraView); 
preview.addView(camPreview);

// Attach a callback for preview
CamCallback camCallback = new CamCallback();
mCamera.setPreviewCallback(camCallback);

3
楼主表示他将在服务中使用此功能。我还在寻找一种从服务中拍照的方法。通过放置“FrameLayout”,您是指将其放置在启动服务的活动中吗?如果服务从后台调用此功能,是否会弹出附有此“FrameLayout”的活动? - NumberFour
@NumberFour:第三段代码与您的情况无关。 - Alex Cohn
2
我该如何在服务中使用这段代码??我能从服务中调用CamPreview吗?? - someone
在回调接口中进行了一些小的更改后,这对我很有效。 - shashi2459

20

有一种方法可以做到这一点,但有些棘手。需要做的是从服务中将一个SurfaceHolder连接到窗口管理器。

WindowManager wm = (WindowManager) mCtx.getSystemService(Context.WINDOW_SERVICE);
params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
            WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
            PixelFormat.TRANSLUCENT);        
wm.addView(surfaceview, params);
并且然后设置。
surfaceview.setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSPARENT);

这里的mHolder是你从SurfaceView获取的holder。

这种方式可以控制SurfaceView的透明度,使其完全透明,但相机仍然可以获取帧。

这就是我的做法。希望对你有所帮助 :)


5
我已添加了这个权限,异常已消失。<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> - Sathesh
1
哦,我一开始就有这个权限...所以我没有遇到异常 :) - Vlad
我也想看到更多的源代码,因为我对相机API还不熟悉。我是从surfaceView.getHolder()获取'mHolder'吗?我该如何创建SurfaceView?surfaceView = new SufaceView(this)可以吗?谢谢。 - NumberFour
2
当我尝试这个解决方案并退出我的应用程序时,发生了这种情况:http://i.imgur.com/g8Fmnj6.png - BVB
3
窗口管理器不会自动移除已添加的视图,所以请保存一个对SurfaceView的引用,当退出时添加 wm.removeView(surfaceview)。 - Vlad
显示剩余8条评论

13

我们通过在版本低于3.0的情况下使用虚拟SurfaceView(未添加到实际GUI中)来解决了这个问题(或者说,在平板电脑上使用相机服务并没有真正意义上的4.0版本)。在版本>= 4.0的情况下,这只能在模拟器中工作。

在这里,我们使用了SurfaceTexture(和setSurfaceTexture())而不是SurfaceView(和setSurfaceView())。至少在Nexus S上可以正常工作。

我认为这确实是Android框架的一个缺点。


3
在“Sam的工作示例”中(谢谢Sam...),如果在指令“wm.addView(preview, params);”处,会出现异常“无法添加窗口android.view.ViewRoot——该窗口类型不允许许可”,可以通过在AndroidManifest中使用此权限来解决。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

1

您可以尝试这个可行的代码。此服务点击前置图片,如果您想捕获后置相机图片,则需要取消代码中的前置相机并注释后置相机。

注意:允许应用程序使用相机和存储权限,并从活动或任何地方启动服务。

public class MyService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        CapturePhoto();
    }

    private void CapturePhoto() {

        Log.d("kkkk","Preparing to take photo");
        Camera camera = null;

        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

            int frontCamera = 1;
            //int backCamera=0;

            Camera.getCameraInfo(frontCamera, cameraInfo);

            try {
                camera = Camera.open(frontCamera);
            } catch (RuntimeException e) {
                Log.d("kkkk","Camera not available: " + 1);
                camera = null;
                //e.printStackTrace();
            }
            try {
                if (null == camera) {
                    Log.d("kkkk","Could not get camera instance");
                } else {
                    Log.d("kkkk","Got the camera, creating the dummy surface texture");
                     try {
                         camera.setPreviewTexture(new SurfaceTexture(0));
                        camera.startPreview();
                    } catch (Exception e) {
                        Log.d("kkkk","Could not set the surface preview texture");
                        e.printStackTrace();
                    }
                    camera.takePicture(null, null, new Camera.PictureCallback() {

                        @Override
                        public void onPictureTaken(byte[] data, Camera camera) {
                            File pictureFileDir=new File("/sdcard/CaptureByService");

                            if (!pictureFileDir.exists() && !pictureFileDir.mkdirs()) {
                                pictureFileDir.mkdirs();
                            }
                            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyymmddhhmmss");
                            String date = dateFormat.format(new Date());
                            String photoFile = "ServiceClickedPic_" + "_" + date + ".jpg";
                            String filename = pictureFileDir.getPath() + File.separator + photoFile;
                            File mainPicture = new File(filename);

                            try {
                                FileOutputStream fos = new FileOutputStream(mainPicture);
                                fos.write(data);
                                fos.close();
                                Log.d("kkkk","image saved");
                            } catch (Exception error) {
                                Log.d("kkkk","Image could not be saved");
                            }
                            camera.release();
                        }
                    });
                }
            } catch (Exception e) {
                camera.release();
            }
    }
}

因为你直接复制了另一个SO帖子的答案,修改了日志文本并将其发布为自己的答案,所以被踩了。https://dev59.com/sG3Xa4cB1Zd3GeqPaxZX#24027066 - pookie
但相机返回 null! - Hassan Ali Mughal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接