自定义Camera系列之:GLSurfaceView + Camera2
一、前言
假如你要使用 OpenGL ES 来渲染相机的话,使用 GLSurfaceView 将是一个很常用的选择。
这里介绍 GLSurfaceView + Camera2
的组合。
如果你对 Camera2 的相关类和接口还不熟悉,可以先看看下面这些介绍:
- CameraManager详解
- CameraDevice详解
- CameraCharacteristics详解
- CameraCaptureSession详解
- CaptureRequest和CaptureResult
为什么选择 GLSurfaceView
?
GLSurfaceView
继承自 SurfaceView
。相比 SurfaceView,它加入了 EGL 的管理,并自带了渲染线程。另外它定义了用户需要实现的 Renderer 接口,客户端只需要将实现了渲染函数的 Renderer 的实现类设置给 GLSurfaceView 即可(策略模式)。
下面是该应用的简要截图:
二、相机开发步骤
我们选择将 Camera 和 View 分开,Camera 的相关操作由 Camera2Proxy
类完成,而 View 持有一个 Camera2Proxy 对象。这样 Camera2Proxy 也是可以重复利用的。
注意: 避免篇幅过长,下面每个小模块的示例代码在最后统一给出。
1. 打开相机
通过 CameraManager 的 openCamera()
方法打开相机,并在 CameraDevice.StateCallback
回调中获取 CameraDevice 对象。需要指定打开的相机 cameraId。
注意:
CameraCharacteristics.LENS_FACING_FRONT
通常表示后置摄像头,CameraCharacteristics.LENS_FACING_BACK
通常表示前置摄像头。
2. 相机配置
在 Camera2 API 中,相机的一些通用配置是通过 CameraCharacteristics
类完成,针对不同的请求(预览&拍照等),我们还可以通过 CaptureRequest
类单独配置。
我们可以设置 闪光模式、聚焦模式、曝光强度、预览图片格式和大小、拍照图片格式和大小 等等信息。
3. 设置相机预览时的显示方向
设置好了预览的显示方向和大小,预览的画面才不会产生拉伸等现象。
4. 开始预览、停止预览
可以通过 CameraCaptureSession
的 setRepeatingRequest()
重复发送预览的请求来实现预览,通过 stopRepeating()
方法来停止发送。
5. 释放相机
相机是很耗费系统资源的东西,用完一定要释放。
6. 点击聚焦
简单的说,就是根据用户在 view 上的触摸点,映射到相机坐标系中对应的点,然后通过 CaptureRequest.Builder
的 CaptureRequest.CONTROL_AF_REGIONS
字段设置聚焦的区域。
7. 双指放大缩小
通过 View 的点击事件,获取到双指之间的间距,并通过 CaptureRequest.Builder
的 CaptureRequest.SCALER_CROP_REGION
字段设置缩放。
8. 拍照
新建一个 ImageReader
对象作为拍照的输出目标,通过创建一个拍照的 CaptureRequest,并通过 CameraCaptureSession
的 capture()
方法来发送单次请求。
注意,预览的时候是通过 CameraCaptureSession
的 setRepeatingRequest()
来发送重复请求,注意区分。
9. 其它
诸如设置预览回调、切换前后摄像头的操作等直接看下面的实现把。另外对于 聚焦模式、闪光灯模式 等没有详细去介绍了,感兴趣的可以另外搜索相关模块。毕竟相机要介绍完全的话还是一块很大的东西。
9. Camera2Proxy 类
下面代码还用到了 OrientationEventListener
,这里之前没介绍,是通过传感器来获取当前手机的方向的,用于 拍照 的时候设置图片的选择使用,后面会介绍。
package com.afei.camerademo.camera;import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Size;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;public class Camera2Proxy {private static final String TAG = "Camera2Proxy";private Activity mActivity;private int mCameraId = CameraCharacteristics.LENS_FACING_FRONT; // 要打开的摄像头IDprivate Size mPreviewSize; // 预览大小private CameraManager mCameraManager; // 相机管理者private CameraCharacteristics mCameraCharacteristics; // 相机属性private CameraDevice mCameraDevice; // 相机对象private CameraCaptureSession mCaptureSession;private CaptureRequest.Builder mPreviewRequestBuilder; // 相机预览请求的构造器private CaptureRequest mPreviewRequest;private Handler mBackgroundHandler;private HandlerThread mBackgroundThread;private ImageReader mImageReader;private Surface mPreviewSurface;private OrientationEventListener mOrientationEventListener;private int mDisplayRotate = 0;private int mDeviceOrientation = 0; // 设备方向,由相机传感器获取private int mZoom = 1; // 缩放/*** 打开摄像头的回调*/private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {@Overridepublic void onOpened(@NonNull CameraDevice camera) {Log.d(TAG, "onOpened");mCameraDevice = camera;initPreviewRequest();}@Overridepublic void onDisconnected(@NonNull CameraDevice camera) {Log.d(TAG, "onDisconnected");releaseCamera();}@Overridepublic void onError(@NonNull CameraDevice camera, int error) {Log.e(TAG, "Camera Open failed, error: " + error);releaseCamera();}};@TargetApi(Build.VERSION_CODES.M)public Camera2Proxy(Activity activity) {mActivity = activity;mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);mOrientationEventListener = new OrientationEventListener(mActivity) {@Overridepublic void onOrientationChanged(int orientation) {mDeviceOrientation = orientation;}};}@SuppressLint("MissingPermission")public void openCamera(int width, int height) {Log.v(TAG, "openCamera");startBackgroundThread(); // 对应 releaseCamera() 方法中的 stopBackgroundThread()mOrientationEventListener.enable();try {mCameraCharacteristics = mCameraManager.getCameraCharacteristics(Integer.toString(mCameraId));StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);// 拍照大小,选择能支持的一个最大的图片大小Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), newCompareSizesByArea());Log.d(TAG, "picture size: " + largest.getWidth() + "*" + largest.getHeight());mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, 2);// 预览大小,根据上面选择的拍照图片的长宽比,选择一个和控件长宽差不多的大小mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height, largest);Log.d(TAG, "preview size: " + mPreviewSize.getWidth() + "*" + mPreviewSize.getHeight());// 打开摄像头mCameraManager.openCamera(Integer.toString(mCameraId), mStateCallback, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}public void releaseCamera() {Log.v(TAG, "releaseCamera");if (null != mCaptureSession) {mCaptureSession.close();mCaptureSession = null;}if (mCameraDevice != null) {mCameraDevice.close();mCameraDevice = null;}if (mImageReader != null) {mImageReader.close();mImageReader = null;}mOrientationEventListener.disable();stopBackgroundThread(); // 对应 openCamera() 方法中的 startBackgroundThread()}public void setImageAvailableListener(ImageReader.OnImageAvailableListener onImageAvailableListener) {if (mImageReader == null) {Log.w(TAG, "setImageAvailableListener: mImageReader is null");return;}mImageReader.setOnImageAvailableListener(onImageAvailableListener, null);}public void setPreviewSurface(SurfaceHolder holder) {mPreviewSurface = holder.getSurface();}public void setPreviewSurface(SurfaceTexture surfaceTexture) {surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());mPreviewSurface = new Surface(surfaceTexture);}private void initPreviewRequest() {try {mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);mPreviewRequestBuilder.addTarget(mPreviewSurface); // 设置预览输出的 SurfacemCameraDevice.createCaptureSession(Arrays.asList(mPreviewSurface, mImageReader.getSurface()),new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mCaptureSession = session;// 设置连续自动对焦mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);// 设置自动曝光mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);// 设置完后自动开始预览mPreviewRequest = mPreviewRequestBuilder.build();startPreview();}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {Log.e(TAG, "ConfigureFailed. session: mCaptureSession");}}, mBackgroundHandler); // handle 传入 null 表示使用当前线程的 Looper} catch (CameraAccessException e) {e.printStackTrace();}}public void startPreview() {if (mCaptureSession == null || mPreviewRequestBuilder == null) {Log.w(TAG, "startPreview: mCaptureSession or mPreviewRequestBuilder is null");return;}try {// 开始预览,即一直发送预览的请求mCaptureSession.setRepeatingRequest(mPreviewRequest, null, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}public void stopPreview() {if (mCaptureSession == null || mPreviewRequestBuilder == null) {Log.w(TAG, "stopPreview: mCaptureSession or mPreviewRequestBuilder is null");return;}try {mCaptureSession.stopRepeating();} catch (CameraAccessException e) {e.printStackTrace();}}public void captureStillPicture() {try {CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);captureBuilder.addTarget(mImageReader.getSurface());captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(mDeviceOrientation));// 预览如果有放大,拍照的时候也应该保存相同的缩放Rect zoomRect = mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION);if (zoomRect != null) {captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);}mCaptureSession.stopRepeating();mCaptureSession.abortCaptures();final long time = System.currentTimeMillis();mCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() {@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull TotalCaptureResult result) {Log.w(TAG, "onCaptureCompleted, time: " + (System.currentTimeMillis() - time));try {mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);mCaptureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}startPreview();}}, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}private int getJpegOrientation(int deviceOrientation) {if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);// Round device orientation to a multiple of 90deviceOrientation = (deviceOrientation + 45) / 90 * 90;// Reverse device orientation for front-facing camerasboolean facingFront = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;if (facingFront) deviceOrientation = -deviceOrientation;// Calculate desired JPEG orientation relative to camera orientation to make// the image upright relative to the device orientationint jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;return jpegOrientation;}public boolean isFrontCamera() {return mCameraId == CameraCharacteristics.LENS_FACING_BACK;}public Size getPreviewSize() {return mPreviewSize;}public void switchCamera(int width, int height) {mCameraId ^= 1;releaseCamera();openCamera(width, height);}private Size chooseOptimalSize(Size[] sizes, int viewWidth, int viewHeight, Size pictureSize) {int totalRotation = getRotation();boolean swapRotation = totalRotation == 90 || totalRotation == 270;int width = swapRotation ? viewHeight : viewWidth;int height = swapRotation ? viewWidth : viewHeight;return getSuitableSize(sizes, width, height, pictureSize);}private int getRotation() {int displayRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();switch (displayRotation) {case Surface.ROTATION_0:displayRotation = 90;break;case Surface.ROTATION_90:displayRotation = 0;break;case Surface.ROTATION_180:displayRotation = 270;break;case Surface.ROTATION_270:displayRotation = 180;break;}int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);mDisplayRotate = (displayRotation + sensorOrientation + 270) % 360;return mDisplayRotate;}private Size getSuitableSize(Size[] sizes, int width, int height, Size pictureSize) {int minDelta = Integer.MAX_VALUE; // 最小的差值,初始值应该设置大点保证之后的计算中会被重置int index = 0; // 最小的差值对应的索引坐标float aspectRatio = pictureSize.getHeight() * 1.0f / pictureSize.getWidth();Log.d(TAG, "getSuitableSize. aspectRatio: " + aspectRatio);for (int i = 0; i < sizes.length; i++) {Size size = sizes[i];// 先判断比例是否相等if (size.getWidth() * aspectRatio == size.getHeight()) {int delta = Math.abs(width - size.getWidth());if (delta == 0) {return size;}if (minDelta > delta) {minDelta = delta;index = i;}}}return sizes[index];}public void handleZoom(boolean isZoomIn) {if (mCameraDevice == null || mCameraCharacteristics == null || mPreviewRequestBuilder == null) {return;}int maxZoom = mCameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM).intValue()* 10;Rect rect = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);if (isZoomIn && mZoom < maxZoom) {mZoom++;} else if (mZoom > 1) {mZoom--;}int minW = rect.width() / maxZoom;int minH = rect.height() / maxZoom;int difW = rect.width() - minW;int difH = rect.height() - minH;int cropW = difW * mZoom / 100;int cropH = difH * mZoom / 100;cropW -= cropW & 3;cropH -= cropH & 3;Rect zoomRect = new Rect(cropW, cropH, rect.width() - cropW, rect.height() - cropH);mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);mPreviewRequest = mPreviewRequestBuilder.build();startPreview(); // 需要重新 start preview 才能生效}public void focusOnPoint(double x, double y, int width, int height) {if (mCameraDevice == null || mPreviewRequestBuilder == null) {return;}// 1. 先取相对于view上面的坐标int previewWidth = mPreviewSize.getWidth();int previewHeight = mPreviewSize.getHeight();if (mDisplayRotate == 90 || mDisplayRotate == 270) {previewWidth = mPreviewSize.getHeight();previewHeight = mPreviewSize.getWidth();}// 2. 计算摄像头取出的图像相对于view放大了多少,以及有多少偏移double tmp;double imgScale;double verticalOffset = 0;double horizontalOffset = 0;if (previewHeight * width > previewWidth * height) {imgScale = width * 1.0 / previewWidth;verticalOffset = (previewHeight - height / imgScale) / 2;} else {imgScale = height * 1.0 / previewHeight;horizontalOffset = (previewWidth - width / imgScale) / 2;}// 3. 将点击的坐标转换为图像上的坐标x = x / imgScale + horizontalOffset;y = y / imgScale + verticalOffset;if (90 == mDisplayRotate) {tmp = x;x = y;y = mPreviewSize.getHeight() - tmp;} else if (270 == mDisplayRotate) {tmp = x;x = mPreviewSize.getWidth() - y;y = tmp;}// 4. 计算取到的图像相对于裁剪区域的缩放系数,以及位移Rect cropRegion = mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION);if (cropRegion == null) {Log.w(TAG, "can't get crop region");cropRegion = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);}int cropWidth = cropRegion.width();int cropHeight = cropRegion.height();if (mPreviewSize.getHeight() * cropWidth > mPreviewSize.getWidth() * cropHeight) {imgScale = cropHeight * 1.0 / mPreviewSize.getHeight();verticalOffset = 0;horizontalOffset = (cropWidth - imgScale * mPreviewSize.getWidth()) / 2;} else {imgScale = cropWidth * 1.0 / mPreviewSize.getWidth();horizontalOffset = 0;verticalOffset = (cropHeight - imgScale * mPreviewSize.getHeight()) / 2;}// 5. 将点击区域相对于图像的坐标,转化为相对于成像区域的坐标x = x * imgScale + horizontalOffset + cropRegion.left;y = y * imgScale + verticalOffset + cropRegion.top;double tapAreaRatio = 0.1;Rect rect = new Rect();rect.left = clamp((int) (x - tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width());rect.right = clamp((int) (x + tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width());rect.top = clamp((int) (y - tapAreaRatio / 2 * cropRegion.height()), 0, cropRegion.height());rect.bottom = clamp((int) (y + tapAreaRatio / 2 * cropRegion.height()), 0, cropRegion.height());// 6. 设置 AF、AE 的测光区域,即上述得到的 rectmPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 1000)});mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 1000)});mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START);try {// 7. 发送上述设置的对焦请求,并监听回调mCaptureSession.capture(mPreviewRequestBuilder.build(), mAfCaptureCallback, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}private final CameraCaptureSession.CaptureCallback mAfCaptureCallback = new CameraCaptureSession.CaptureCallback() {private void process(CaptureResult result) {Integer state = result.get(CaptureResult.CONTROL_AF_STATE);if (null == state) {return;}if (state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.FLASH_MODE_OFF);startPreview();}}@Overridepublic void onCaptureProgressed(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull CaptureResult partialResult) {process(partialResult);}@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull TotalCaptureResult result) {process(result);}};private void startBackgroundThread() {if (mBackgroundThread == null || mBackgroundHandler == null) {mBackgroundThread = new HandlerThread("CameraBackground");mBackgroundThread.start();mBackgroundHandler = new Handler(mBackgroundThread.getLooper());}}private void stopBackgroundThread() {mBackgroundThread.quitSafely();try {mBackgroundThread.join();mBackgroundThread = null;mBackgroundHandler = null;} catch (InterruptedException e) {e.printStackTrace();}}private int clamp(int x, int min, int max) {if (x > max) return max;if (x < min) return min;return x;}/*** Compares two {@code Size}s based on their areas.*/static class CompareSizesByArea implements Comparator<Size> {@Overridepublic int compare(Size lhs, Size rhs) {// We cast here to ensure the multiplications won't overflowreturn Long.signum((long) lhs.getWidth() * lhs.getHeight() -(long) rhs.getWidth() * rhs.getHeight());}}
}
三、Camera2GLSurfaceView
通过上面的介绍,对于相机的操作应该有了一定的了解了,接下来完成 View 这部分。
需求分析:
Camera2GLSurfaceView
是要继承GLSurfaceView
的。- 我们需要重写
onMeasure
使得Camera2GLSurfaceView
的宽高可以和相机预览尺寸相匹配,这样就不会有画面被拉伸的感觉了。 - 我们需要在
Camera2GLSurfaceView
中完成对相机的打开、关闭等操作,值得庆幸的是我们可以通过上面的Camera2Proxy
很容易的做到。 - 我们需要重写
onTouchEvent
方法,来实现单点聚焦,双指放大缩小的功能。 - 我们要实现
GLSurfaceView.Renderer
接口,并在其中完成相机的渲染。
实现:
Camera2GLSurfaceView
主要是在 GLSurfaceView.Renderer
的几个回调方法中打开和释放相机,另外就是重写 onMeasure
,onTouchEvent
那几个方法。
package com.afei.camerademo.glsurfaceview;import android.app.Activity;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;import com.afei.camerademo.camera.Camera2Proxy;import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;public class Camera2GLSurfaceView extends GLSurfaceView implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {private static final String TAG = "Camera2GLSurfaceView";private Camera2Proxy mCameraProxy;private SurfaceTexture mSurfaceTexture;private CameraDrawer mDrawer;private int mRatioWidth = 0;private int mRatioHeight = 0;private float mOldDistance;private int mTextureId = -1;public Camera2GLSurfaceView(Context context) {this(context, null);}public Camera2GLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {mCameraProxy = new Camera2Proxy((Activity) context);setEGLContextClientVersion(2);setRenderer(this);setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mTextureId = OpenGLUtils.getExternalOESTextureID();mSurfaceTexture = new SurfaceTexture(mTextureId);mSurfaceTexture.setOnFrameAvailableListener(this);mCameraProxy.setPreviewSurface(mSurfaceTexture);mDrawer = new CameraDrawer();Log.d(TAG, "onSurfaceCreated. width: " + getWidth() + ", height: " + getHeight());mCameraProxy.openCamera(getWidth(), getHeight());}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {Log.d(TAG, "onSurfaceChanged. thread: " + Thread.currentThread().getName());Log.d(TAG, "onSurfaceChanged. width: " + width + ", height: " + height);int previewWidth = mCameraProxy.getPreviewSize().getWidth();int previewHeight = mCameraProxy.getPreviewSize().getHeight();if (width > height) {setAspectRatio(previewWidth, previewHeight);} else {setAspectRatio(previewHeight, previewWidth);}GLES20.glViewport(0, 0, width, height);}@Overridepublic void onDrawFrame(GL10 gl) {GLES20.glClearColor(0, 0, 0, 0);GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);mSurfaceTexture.updateTexImage();mDrawer.draw(mTextureId, mCameraProxy.isFrontCamera());}@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {requestRender();}private void setAspectRatio(int width, int height) {if (width < 0 || height < 0) {throw new IllegalArgumentException("Size cannot be negative.");}mRatioWidth = width;mRatioHeight = height;post(new Runnable() {@Overridepublic void run() {requestLayout(); // must run in UI thread}});}public Camera2Proxy getCameraProxy() {return mCameraProxy;}public SurfaceTexture getSurfaceTexture() {return mSurfaceTexture;}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = MeasureSpec.getSize(widthMeasureSpec);int height = MeasureSpec.getSize(heightMeasureSpec);if (0 == mRatioWidth || 0 == mRatioHeight) {setMeasuredDimension(width, height);} else {if (width < height * mRatioWidth / mRatioHeight) {setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);} else {setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (event.getPointerCount() == 1) {// 点击聚焦mCameraProxy.focusOnPoint((int) event.getX(), (int) event.getY(), getWidth(), getHeight());return true;}switch (event.getAction() & MotionEvent.ACTION_MASK) {case MotionEvent.ACTION_POINTER_DOWN:mOldDistance = getFingerSpacing(event);break;case MotionEvent.ACTION_MOVE:float newDistance = getFingerSpacing(event);if (newDistance > mOldDistance) {mCameraProxy.handleZoom(true);} else if (newDistance < mOldDistance) {mCameraProxy.handleZoom(false);}mOldDistance = newDistance;break;default:break;}return super.onTouchEvent(event);}private static float getFingerSpacing(MotionEvent event) {float x = event.getX(0) - event.getX(1);float y = event.getY(0) - event.getY(1);return (float) Math.sqrt(x * x + y * y);}}
CameraDrawer
自定义的一个类,将绘制的代码抽离出来了,主要是 draw()
方法。
涉及到很多关于 OpenGL ES 的知识,限于篇幅很难全部解释清楚,建议预备好这部分的相关知识或者自行搜索。
package com.afei.camerademo.glsurfaceview;import android.opengl.GLES11Ext;
import android.opengl.GLES20;import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;public class CameraDrawer {public final String VERTEX_SHADER = "" +"attribute vec4 vPosition;" +"attribute vec2 inputTextureCoordinate;" +"varying vec2 textureCoordinate;" +"void main()" +"{"+"gl_Position = vPosition;"+"textureCoordinate = inputTextureCoordinate;" +"}";public final String FRAGMENT_SHADER = "" +"#extension GL_OES_EGL_image_external : require\n"+"precision mediump float;" +"varying vec2 textureCoordinate;\n" +"uniform samplerExternalOES s_texture;\n" +"void main() {" +" gl_FragColor = texture2D( s_texture, textureCoordinate );\n" +"}";private FloatBuffer mVertexBuffer;private FloatBuffer mBackTextureBuffer;private FloatBuffer mFrontTextureBuffer;private ByteBuffer mDrawListBuffer;private int mProgram;private int mPositionHandle;private int mTextureHandle;private static final float VERTEXES[] = {-1.0f, 1.0f,-1.0f,-1.0f,1.0f, -1.0f,1.0f, 1.0f,};// 后置摄像头使用的纹理坐标private static final float TEXTURE_BACK[] = {0.0f, 1.0f,1.0f, 1.0f,1.0f, 0.0f,0.0f, 0.0f,};// 前置摄像头使用的纹理坐标private static final float TEXTURE_FRONT[] = {1.0f, 1.0f,0.0f, 1.0f,0.0f, 0.0f,1.0f, 0.0f,};private static final byte VERTEX_ORDER[] = { 0, 1, 2, 3 }; // order to draw verticesprivate final int VERTEX_SIZE = 2;private final int VERTEX_STRIDE = VERTEX_SIZE * 4;public CameraDrawer() {// init float buffer for vertex coordinatesmVertexBuffer = ByteBuffer.allocateDirect(VERTEXES.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();mVertexBuffer.put(VERTEXES).position(0);// init float buffer for texture coordinatesmBackTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_BACK.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();mBackTextureBuffer.put(TEXTURE_BACK).position(0);mFrontTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_FRONT.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();mFrontTextureBuffer.put(TEXTURE_FRONT).position(0);// init byte buffer for draw listmDrawListBuffer = ByteBuffer.allocateDirect(VERTEX_ORDER.length).order(ByteOrder.nativeOrder());mDrawListBuffer.put(VERTEX_ORDER).position(0);mProgram = OpenGLUtils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");mTextureHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");}public void draw(int texture, boolean isFrontCamera) {GLES20.glUseProgram(mProgram); // 指定使用的programGLES20.glEnable(GLES20.GL_CULL_FACE); // 启动剔除GLES20.glActiveTexture(GLES20.GL_TEXTURE0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture); // 绑定纹理GLES20.glEnableVertexAttribArray(mPositionHandle);GLES20.glVertexAttribPointer(mPositionHandle, VERTEX_SIZE, GLES20.GL_FLOAT, false, VERTEX_STRIDE, mVertexBuffer);GLES20.glEnableVertexAttribArray(mTextureHandle);if (isFrontCamera) {GLES20.glVertexAttribPointer(mTextureHandle, VERTEX_SIZE, GLES20.GL_FLOAT, false, VERTEX_STRIDE, mFrontTextureBuffer);} else {GLES20.glVertexAttribPointer(mTextureHandle, VERTEX_SIZE, GLES20.GL_FLOAT, false, VERTEX_STRIDE, mBackTextureBuffer);}// 真正绘制的操作GLES20.glDrawElements(GLES20.GL_TRIANGLE_FAN, VERTEX_ORDER.length, GLES20.GL_UNSIGNED_BYTE, mDrawListBuffer);GLES20.glDisableVertexAttribArray(mPositionHandle);GLES20.glDisableVertexAttribArray(mTextureHandle);}
}
OpenGLUtils
上面还用到了一个 OpenGLUtils 类,这里简单的封装了一些创建程序,加载 shader 等公用操作。
package com.afei.camerademo.glsurfaceview;import android.content.res.Resources;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.util.Log;import java.io.IOException;
import java.io.InputStream;import javax.microedition.khronos.opengles.GL10;public class OpenGLUtils {private static final String TAG = "OpenGLUtils";public static int getExternalOESTextureID() {int[] texture = new int[1];GLES20.glGenTextures(1, texture, 0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);return texture[0];}public static int loadShader(int type, String source) {// 1. create shaderint shader = GLES20.glCreateShader(type);if (shader == GLES20.GL_NONE) {Log.e(TAG, "create shared failed! type: " + type);return GLES20.GL_NONE;}// 2. load shader sourceGLES20.glShaderSource(shader, source);// 3. compile shared sourceGLES20.glCompileShader(shader);// 4. check compile statusint[] compiled = new int[1];GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);if (compiled[0] == GLES20.GL_FALSE) { // compile failedLog.e(TAG, "Error compiling shader. type: " + type + ":");Log.e(TAG, GLES20.glGetShaderInfoLog(shader));GLES20.glDeleteShader(shader); // delete shadershader = GLES20.GL_NONE;}return shader;}public static int createProgram(String vertexSource, String fragmentSource) {// 1. load shaderint vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);if (vertexShader == GLES20.GL_NONE) {Log.e(TAG, "load vertex shader failed! ");return GLES20.GL_NONE;}int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);if (fragmentShader == GLES20.GL_NONE) {Log.e(TAG, "load fragment shader failed! ");return GLES20.GL_NONE;}// 2. create gl programint program = GLES20.glCreateProgram();if (program == GLES20.GL_NONE) {Log.e(TAG, "create program failed! ");return GLES20.GL_NONE;}// 3. attach shaderGLES20.glAttachShader(program, vertexShader);GLES20.glAttachShader(program, fragmentShader);// we can delete shader after attachGLES20.glDeleteShader(vertexShader);GLES20.glDeleteShader(fragmentShader);// 4. link programGLES20.glLinkProgram(program);// 5. check link statusint[] linkStatus = new int[1];GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);if (linkStatus[0] == GLES20.GL_FALSE) { // link failedLog.e(TAG, "Error link program: ");Log.e(TAG, GLES20.glGetProgramInfoLog(program));GLES20.glDeleteProgram(program); // delete programreturn GLES20.GL_NONE;}return program;}public static String loadFromAssets(String fileName, Resources resources) {String result = null;try {InputStream is = resources.getAssets().open(fileName);int length = is.available();byte[] data = new byte[length];is.read(data);is.close();result = new String(data, "UTF-8");result.replace("\\r\\n", "\\n");} catch (IOException e) {e.printStackTrace();}return result;}
}
四、GLSurfaceCamera2Activity
接下来,我们把写好的 Camera2GLSurfaceView
放在 Activity 或者 Fragment 中使用就行了。
注意相机使用前,需要申请相关权限,以及权限的动态申请。
1. AndroidManifest.xml
相机相关权限如下,动态权限的申请代码很多,这里不详细介绍了,不清楚的可以看这篇博客:Android动态权限申请
<uses-permission android:name="android.permission.CAMERA"/><uses-feature android:name="android.hardware.camera"/><uses-feature android:name="android.hardware.camera.autofocus"/>
2. 拍照功能
需要注意的是,前置摄像头是存在左右镜像的,因此针对前置摄像头我们需要手机进行一个左右镜像的操作。
下面是完整的 GLSurfaceCamera2Activity
代码:
package com.afei.camerademo.glsurfaceview;import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.Image;
import android.media.ImageReader;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;import com.afei.camerademo.ImageUtils;
import com.afei.camerademo.R;
import com.afei.camerademo.camera.Camera2Proxy;import java.nio.ByteBuffer;public class GLSurfaceCamera2Activity extends AppCompatActivity implements View.OnClickListener {private static final String TAG = "GLSurfaceCamera2Act";private ImageView mCloseIv;private ImageView mSwitchCameraIv;private ImageView mTakePictureIv;private ImageView mPictureIv;private Camera2GLSurfaceView mCameraView;private Camera2Proxy mCameraProxy;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_glsurface_camera2);initView();}private void initView() {mCloseIv = findViewById(R.id.toolbar_close_iv);mCloseIv.setOnClickListener(this);mSwitchCameraIv = findViewById(R.id.toolbar_switch_iv);mSwitchCameraIv.setOnClickListener(this);mTakePictureIv = findViewById(R.id.take_picture_iv);mTakePictureIv.setOnClickListener(this);mPictureIv = findViewById(R.id.picture_iv);mPictureIv.setOnClickListener(this);mPictureIv.setImageBitmap(ImageUtils.getLatestThumbBitmap());mCameraView = findViewById(R.id.camera_view);mCameraProxy = mCameraView.getCameraProxy();}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.toolbar_close_iv:finish();break;case R.id.toolbar_switch_iv:mCameraProxy.switchCamera(mCameraView.getWidth(), mCameraView.getHeight());mCameraProxy.startPreview();break;case R.id.take_picture_iv:mCameraProxy.setImageAvailableListener(mOnImageAvailableListener);mCameraProxy.captureStillPicture(); // 拍照break;case R.id.picture_iv:Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);startActivity(intent);break;}}private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {@Overridepublic void onImageAvailable(ImageReader reader) {new ImageSaveTask().execute(reader.acquireNextImage()); // 保存图片}};private class ImageSaveTask extends AsyncTask<Image, Void, Bitmap> {@Overrideprotected Bitmap doInBackground(Image... images) {ByteBuffer buffer = images[0].getPlanes()[0].getBuffer();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);long time = System.currentTimeMillis();if (mCameraProxy.isFrontCamera()) {Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);Log.d(TAG, "BitmapFactory.decodeByteArray time: " + (System.currentTimeMillis() - time));time = System.currentTimeMillis();// 前置摄像头需要左右镜像Bitmap rotateBitmap = ImageUtils.rotateBitmap(bitmap, 0, true, true);Log.d(TAG, "rotateBitmap time: " + (System.currentTimeMillis() - time));time = System.currentTimeMillis();ImageUtils.saveBitmap(rotateBitmap);Log.d(TAG, "saveBitmap time: " + (System.currentTimeMillis() - time));rotateBitmap.recycle();} else {ImageUtils.saveImage(bytes);Log.d(TAG, "saveBitmap time: " + (System.currentTimeMillis() - time));}images[0].close();return ImageUtils.getLatestThumbBitmap();}@Overrideprotected void onPostExecute(Bitmap bitmap) {mPictureIv.setImageBitmap(bitmap);}}}
附上 ImageUtils
代码:
package com.afei.camerademo;import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;public class ImageUtils {private static final String TAG = "ImageUtils";private static final String GALLERY_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator + "Camera";private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss");public static Bitmap rotateBitmap(Bitmap source, int degree, boolean flipHorizontal, boolean recycle) {if (degree == 0) {return source;}Matrix matrix = new Matrix();matrix.postRotate(degree);if (flipHorizontal) {matrix.postScale(-1, 1); // 前置摄像头存在水平镜像的问题,所以有需要的话调用这个方法进行水平镜像}Bitmap rotateBitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);if (recycle) {source.recycle();}return rotateBitmap;}public static void saveBitmap(Bitmap bitmap) {String fileName = DATE_FORMAT.format(new Date(System.currentTimeMillis())) + ".jpg";File outFile = new File(GALLERY_PATH, fileName);Log.d(TAG, "saveImage. filepath: " + outFile.getAbsolutePath());FileOutputStream os = null;try {os = new FileOutputStream(outFile);boolean success = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);if (success) {insertToDB(outFile.getAbsolutePath());}} catch (IOException e) {e.printStackTrace();} finally {if (os != null) {try {os.close();} catch (IOException e) {e.printStackTrace();}}}}public static void insertToDB(String picturePath) {ContentValues values = new ContentValues();ContentResolver resolver = MyApp.getInstance().getContentResolver();values.put(MediaStore.Images.ImageColumns.DATA, picturePath);values.put(MediaStore.Images.ImageColumns.TITLE, picturePath.substring(picturePath.lastIndexOf("/") + 1));values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis());values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);}}
五、项目地址
部分没有贴出来的代码,可在下面地址中找到。
地址:
https://github.com/afei-cn/CameraDemo/tree/master/app/src/main/java/com/afei/camerademo/glsurfaceview
其它:
自定义Camera系列之:SurfaceView + Camera
自定义Camera系列之:TextureView + Camera
自定义Camera系列之:GLSurfaceViewView + Camera
自定义Camera系列之:SurfaceView + Camera2
自定义Camera系列之:TextureView + Camera2
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- 任意输入一个正整数,求各位数字之和。例如:输入12690,输出:“12690各位数字之和为18”
...
2024/5/1 2:09:25 - bomblab CSAPP(深入理解计算机系统)
CSAPP_lab之bomblab 文章目录CSAPP_lab之bomblab前言一、phase_1二、phase_2三、phase_3四、phase_4五、phase_5六、phase_6总结前言 bomblab是这我最喜欢的lab之一,主要思路通过反汇编代码找到拆除炸弹的答案。 拆除炸弹总共需要连续输入6次(phase_1、…...
2024/4/30 19:31:36 - 10.14日总结
初步学习概况: python:学习完第五章字典部分, stuff{gold coin:42,arrow:12,rope:1,dagger:1,torch:6} def addto(inventory,addeditems):for x in addeditems:inventory.setdefault(x,0)inventory[x] 1return inventorydef display(invent…...
2024/4/21 17:49:08 - 20211027_配置中心服务功能梳理(@Service+@Contract/@Inject+ServiceLocator/@PostConstruct/静态内部类的使用)
一、背景 今天在梳理模块代码功能一页纸的时候,发现了工程中的一个子模块功能,大概的功能描述是这样的:“app服务启动后,一个服务(ConfigChangeLintener)通过监听配置中心的变更消息(是否开启某功能)&#…...
2024/4/21 17:49:08 - 硬件描述语言HDL和汇编语言、c语言的区别
个人感觉: 硬件描述语言(vhdl等):是为了制造cpu(类似的芯片),设计人员使用hdl设计和安排寄存器和时序电路如何组合,然后最终会生成门级网表,然后通过相关软件等生成最终物…...
2024/4/21 17:49:08 - 2016级移动应用开发在线测试13-Location、Sensor & Network
有趣有内涵的文章第一时间送达!喝酒I创作I分享生活中总有些东西值得分享@醉翁猫咪 1. 充分利用智能手机的GPS定位信息,创造了O2O的商业模式,打通了线上与线下的信息流和商流,极大地推动了移动互联网的迅猛发展,下面关于GPS技术描述错误的是() 您的回答为:GPS需要地面基站…...
2024/4/19 13:56:26 - 索引的使用
1. 验证索引提升查询效率 注:以下的查询语句基于tb_item 表,由黑马提供。本文不提供具体的数据和数据链接。 1.1 根据 id 查询 select * from tb_item where id 1999\G; 上图是上述查询语句的查询结果信息,从执行的时间来看,该…...
2024/4/20 13:45:35 - flume安装教程(欢迎指教)
flume 安装1.下载并解压 apache-flume-1.6.0-bin.tar.gz2.进入conf3.修改环境变量4.重载环境变量5.检验flume 状态未完待续1.下载并解压 apache-flume-1.6.0-bin.tar.gz tar -zxvf apache-flume-1.6.0-bin.tar.gz2.进入conf cd apache-flume-1.6.0-bin/conf cp flume-env.sh.…...
2024/4/20 13:45:34 - Arduino mega 2560 上传项目总是出错最全解决方案以及串口测试教程
一:串口识别和驱动安装需要保证准确无误 1.运行arduino mega 2560 首先需要安装串口驱动,这样我们用USB将板子和我们计算机连接的时候,使得我们的计算机能够识别连接的串口并且能够连接上,以便实现程序下载、上传、串口通讯。 注…...
2024/4/23 12:21:17 - 基本Dos操作
D: #盘符切换 dir #查看当前目录所有文件 cd #切换目录 cd /d E:\目录名 cd .. #返回上一级目录 cls #清理屏幕(clean screen) exit #退出终端 ipconfig #查看电脑ip # 打开应用 calc #打开计算器 mspaint #打开画图工具 not…...
2024/4/20 7:58:20 - Termux-api调用api后没没反应:
今天玩弄安卓终端,通过f-droid安装的termux,遇到这个调用失败问题在各学习平台网站没有找见答案!!!,在百度贴吧里找见的。。。多亏了这位大哥。(写一篇兴许能帮助到感兴趣而遇到这个坑的小伙伴&…...
2024/4/19 20:12:32 - 【优化求解】基于水循环算法WCA求解约束单目标matlab代码
1 简介 水循环优化算 法 是 由 HadiEskandar等 人 于2012年提出,其理论受…...
2024/4/20 13:45:30 - java内部类
这是我学习Java内部类的笔记 1.为什么使用内部类? 使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现, 对于内部类都没有影…...
2024/4/25 13:43:00 - Hadoop的Windows开发环境配置
将 "软件\Windows依赖\hadoop-3.1.0"目录拷贝到D:\devsoft目录下 把hadoop-3.1.0\bin目录下的 hadoop.dll 和 winutils.exe 放到系统盘的 C:\Windows\System32 下 配置HADOOP_USER_NAME 此电脑->右键 属性-> 高级系统设置->环境变量->新建系统变量 …...
2024/4/21 17:49:05 - Vikings:1 walkthrough
Vikings:1 walkthrough–vulnhub 假设我们已经得到ssh用户名密码: username:floki password:fm0usboatbuilde7 ssh -l floki 192.168.1.5 readme.txt boat 意思就是说:科拉茨 - 猜想 (num)&…...
2024/4/21 17:49:05 - v4l2应用框架-摄像头v4l2编程
1.简介 V4L2支持三种方式来采集图像,内存映射(mmap),直接读取方式(read)和用户指针,内存映射一般用于连续视频数据的采集,直接读取的方式相对速度慢一些,常用于静态图片数据的采集;用户指针直接…...
2024/4/23 3:06:51 - 个人网站性能优化经历(9)网站添加实用功能
自己搭建了一个基于SpringBootSpring SecurityMyBatisMySQLRedisThymeleaf的博客网站 上线个人云服务器后,发现服务器访问慢。个人服务器是1核2G的,1M宽带,虽然服务器是低配的,但是可以通过优化代码,中间件等手段&…...
2024/4/21 17:49:02 - 攻防世界逆向高手题之tar-tar-binks
攻防世界逆向高手题之tar-tar-binks 继续开启全栈梦想之逆向之旅~ 这题是攻防世界逆向高手题的tar-tar-binks 照例下载附件,结果有两个附件,一个压缩包,一个常规文件。压缩包在windows下直接解压竟然还是报错的,kali上则可以解压…...
2024/4/21 17:49:01 - 一.计算机网络体系结构
一.计算机网络体系结构 1.计算机网络 将一个分散的,具有独立功能的计算机系统,通过通信设备与线路连接起来,由功能完善 的软件实现资源共享和信息传递的系统。计算机网络是互连的,自治的计算机集合。 2.计算机网络功能 1.数据…...
2024/4/21 17:49:01 - Zookeeper-day01-简单介绍
1:概念 Zookeeper 是一个开源的分布式的,为分布式框架提供协调服务的Apache 项目。 2:工作机制 3:特点 4:数据结构 ZooKeeper 数据模型的结构与Unix 文件系统很类似,整体上可以看作是一棵树,每…...
2024/4/21 17:49:00
最新文章
- OceanBase V4.3 发布—— 迈向实时分析 AP 的重要里程
OceanBase在2023年初,发布了4.x架构的第一个重要版本,V4.1。该版本采用了单机分布式一体化架构,并在该架构的基础上,将代表数据库可靠性的RTO降低至 8 秒以内,从而确保在意外故障发生后,系统能够在极短时间…...
2024/5/1 2:34:08 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - Ubuntu磁盘扩容
使用 df -h命令查看系统磁盘控件的使用情况: [samspobosrv:~]$ df -h Filesystem Size Used Avail Use% Mounted on udev 7.8G 0 7.8G 0% /dev tmpfs 1.6G 1.7M 1.…...
2024/4/30 2:45:14 - 腾讯云轻量服务器流量不够用了会怎么样?
腾讯云轻量应用服务器是限制月流量的,如果当月流量不够用了,流量超额了怎么办?流量超额后,需要另外支付流量费,如果你的腾讯云账号余额,就会自动扣除对应的流量费,如果余额不足,轻量…...
2024/4/30 5:18:15 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/4/29 23:16:47 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/4/30 18:14:14 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/29 2:29:43 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/4/30 18:21:48 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/27 17:58:04 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/27 14:22:49 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/28 1:28:33 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/30 9:43:09 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/27 17:59:30 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/4/25 18:39:16 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/28 1:34:08 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/26 19:03:37 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/29 20:46:55 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/30 22:21:04 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/4/26 23:04:58 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/27 23:24:42 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/28 5:48:52 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/30 9:42:22 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/4/30 9:43:22 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/30 9:42:49 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57