Android视频采集与处理

Android中很多基本的架构都是C/S层架构,客户端提供调用接口,而实现工作则是在服务端完成。Android Camera的架构也是C/S架构,Client进程虽然不曾拥有任何实质的Camera数据,但是service端为它提供了丰富的接口,它可以轻松的获得Camera数据的地址,然后处理这些数据。两者通过Binder进行通讯。
在Android中调用摄像头需要相应的权限,需要注意的是:权限申请在Android 6.0后变成了动态申请。在本项目中使用了Camera1相关的API对摄像头进行调用,所以对Camera2相关的API不做讲述,以后有时间再补上。
学习如何使用Camera一个好的方式是阅读其他优秀项目的源码,而Android自身就带有Camera的应用,因此阅读Android自身Camera应用的源码无疑是最好的选择。
Google Camera 开源地址:Android Camera App (需要翻墙)

打开摄像头步骤为:

1、检查摄像头;
2、打开摄像头;
3、设置摄像头参数;
4、设置预览界面。
  在本文中使用一个单例CameraHolder来维持Camera相应的状态,也方便在其他地方得到Camera相关的参数。在CameraHolder中定义了Camera的三个状态:Init、Opened、Preview。三个状态的关系图如下所示:

 一、检查摄像头

在使用摄像头之前先要检查摄像头服务是否可用,下面一段代码是检查摄像头服务是否可用的。

public static void checkCameraService(Context context)
            throws CameraDisabledException {
    // Check if device policy has disabled the camera.
    DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
            Context.DEVICE_POLICY_SERVICE);
    if (dpm.getCameraDisabled(null)) {
        throw new CameraDisabledException();
    }
}

检查完摄像头服务后,还需要检查手机上摄像头的个数,如果个数为0,则说明手机上没有摄像头,这样的话也是不能直播的。在项目中,我将摄像头个数的检测和摄像头数据的初始化放在了一起,如下面代码所示:

public static List<CameraData> getAllCamerasData(boolean isBackFirst) {
    ArrayList<CameraData> cameraDatas = new ArrayList<>();
    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
    int numberOfCameras = Camera.getNumberOfCameras();
    for (int i = 0; i < numberOfCameras; i++) {
        Camera.getCameraInfo(i, cameraInfo);
        if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            CameraData cameraData = new CameraData(i, CameraData.FACING_FRONT);
            if(isBackFirst) {
                cameraDatas.add(cameraData);
            } else {
                cameraDatas.add(0, cameraData);
            }
        } else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
            CameraData cameraData = new CameraData(i, CameraData.FACING_BACK);
            if(isBackFirst) {
                cameraDatas.add(0, cameraData);
            } else {
                cameraDatas.add(cameraData);
            }
        }
    }
    return cameraDatas;
}

在上面的方法中,需要传入一个是否先开启背面摄像头的boolean变量,如果变量为true,则把背面摄像头放在列表第一个,之后打开摄像头的时候,直接获取列表中第一个摄像头相关参数,然后进行打开。这样的设计使得切换摄像头也变得十分简单,切换摄像头时,先关闭当前摄像头,然后变化摄像头列表中的顺序,然后再打开摄像头即可,也就是每次打开摄像头都打开摄像头列表中第一个摄像头参数所指向的摄像头。


二、打开摄像头


打开摄像头之前,先从摄像头列表中获取第一个摄像头参数,之后根据参数中的CameraId来打开摄像头,打开成功后改变相关状态。相关代码如下:

public synchronized Camera openCamera()
            throws CameraHardwareException, CameraNotSupportException {
    CameraData cameraData = mCameraDatas.get(0);
    if(mCameraDevice != null && mCameraData == cameraData) {
        return mCameraDevice;
    }
    if (mCameraDevice != null) {
        releaseCamera();
    }
    try {
        SopCastLog.d(TAG, "open camera " + cameraData.cameraID);
        mCameraDevice = Camera.open(cameraData.cameraID);
    } catch (RuntimeException e) {
        SopCastLog.e(TAG, "fail to connect Camera");
        throw new CameraHardwareException(e);
    }
    if(mCameraDevice == null) {
        throw new CameraNotSupportException();
    }
    mCameraData = cameraData;
    mState = State.OPENED;
    return mCameraDevice;
}

上面需要注意的是,在Android提供的Camera源码中,Camera.open(cameraData.cameraID)抛出异常则说明Camera不可用,否则说明Camera可用,但是在一些手机上Camera.open(cameraData.cameraID)不是抛出异常,而是返回null。

3、设置摄像头参数


在给摄像头设置参数后,需要记录这些参数,以方便其他地方使用。比如记录当前摄像头是否有闪光点,从而可以决定UI界面上是否显示打开闪光灯按钮。在直播项目中使用CameraData来记录这些参数,CameraData类如下所示:


 

public class CameraData {
    public static final int FACING_FRONT = 1;
    public static final int FACING_BACK = 2;

    public int cameraID;            //camera的id
    public int cameraFacing;        //区分前后摄像头
    public int cameraWidth;         //camera的采集宽度
    public int cameraHeight;        //camera的采集高度
    public boolean hasLight;        //camera是否有闪光灯
    public int orientation;         //camera旋转角度
    public boolean supportTouchFocus;   //camera是否支持手动对焦
    public boolean touchFocusMode;      //camera是否处在自动对焦模式

    public CameraData(int id, int facing, int width, int height){
        cameraID = id;
        cameraFacing = facing;
        cameraWidth = width;
        cameraHeight = height;
    }

    public CameraData(int id, int facing) {
        cameraID = id;
        cameraFacing = facing;
    }
}

给摄像头设置参数的时候,有一点需要注意:设置的参数不生效会抛出异常,因此需要每个参数单独设置,这样就避免一个参数不生效后抛出异常,导致之后所有的参数都没有设置。

设置预览界面


设置预览界面有两种方式:1、通过SurfaceView显示;2、通过GLSurfaceView显示。当为SurfaceView显示时,需要传给Camera这个SurfaceView的SurfaceHolder。当使用GLSurfaceView显示时,需要使用Renderer进行渲染,先通过OpenGL生成纹理,通过生成纹理的纹理id生成SurfaceTexture,将SurfaceTexture交给Camera,那么在Renderer中便可以使用这个纹理进行相应的渲染,最后通过GLSurfaceView显示。

通过GLSurfaceView显示流程图如下:

 

对摄像机设置SurfaceHolder和SurfaceTexture只需要调用相应的接口即可,关于使用GLSurfaceView显示的整套流程将会在OpenGL相关的文章中进行讲述。

设置预览回调
首先需要设置预览回调的图片格式。

public static void setPreviewFormat(Camera camera, Camera.Parameters parameters) {
    //设置预览回调的图片格式
    try {
        parameters.setPreviewFormat(ImageFormat.NV21);
        camera.setParameters(parameters);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

当设置预览好预览回调的图片格式后,需要设置预览回调的Callback。

Camera.PreviewCallback myCallback = new Camera.PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //得到相应的图片数据
        //Do something
    }
};
public static void setPreviewCallback(Camera camera, Camera.PreviewCallback callback) {
    camera.setPreviewCallback(callback);
}

Android推荐的PreView Format时NV21,在PreviewCallback中会返回Preview的N21图片。如果是软编的话,由于H264支持I420的图片格式,因此需要将N21格式转为I420格式,然后交给x264编码库。如果是硬编的话,由于Android硬编编码器支持I420(COLOR_FormatYUV420Planar)和NV12(COLOR_FormatYUV420SemiPlanar),因此可以将N21的图片转为I420或者NV12,然后交给硬编编码器。

设置预览图像大小
在摄像头相关处理中,一个比较重要的是屏幕显示大小和摄像头预览大小比例不一致的处理。在Android中,摄像头有一系列的Preview Size,我们需要从中选出适合的Preview Size。选择合适的摄像头Preview Size的代码如下所示:



 

public static Camera.Size getOptimalPreviewSize(Camera camera, int width, int height) {
    Camera.Size optimalSize = null;
    double minHeightDiff = Double.MAX_VALUE;
    double minWidthDiff = Double.MAX_VALUE;
    List<Camera.Size> sizes = camera.getParameters().getSupportedPreviewSizes();
    if (sizes == null) return null;
    //找到宽度差距最小的
    for(Camera.Size size:sizes){
        if (Math.abs(size.width - width) < minWidthDiff) {
            minWidthDiff = Math.abs(size.width - width);
        }
    }
    //在宽度差距最小的里面,找到高度差距最小的
    for(Camera.Size size:sizes){
        if(Math.abs(size.width - width) == minWidthDiff) {
            if(Math.abs(size.height - height) < minHeightDiff) {
                optimalSize = size;
                minHeightDiff = Math.abs(size.height - height);
            }
        }
    }
    return optimalSize;
}

public static void setPreviewSize(Camera camera, Camera.Size size, Camera.Parameters parameters) {
    try {    
        parameters.setPreviewSize(size.width, size.height);           
        camera.setParameters(parameters);
    } 
    catch (Exception e) {    
        e.printStackTrace();
    }
}

在设置好最适合的Preview Size之后,将size信息存储在CameraData中。当选择了SurfaceView显示的方式,可以将SurfaceView放置在一个LinearLayout中,然后根据摄像头Preview Size的比例改变SurfaceView的大小,从而使得两者比例一致,确保图像正常。当选择了GLSurfaceView显示的时候,可以通过裁剪纹理,使得纹理的大小比例和GLSurfaceView的大小比例保持一致,从而确保图像显示正常。

图像的旋转
在Android中摄像头出来的图像需要进行一定的旋转,然后才能交给屏幕显示,而且如果应用支持屏幕旋转的话,也需要根据旋转的状况实时调整摄像头的角度。在Android中旋转摄像头图像同样有两种方法,一是通过摄像头的setDisplayOrientation(result)方法,一是通过OpenGL的矩阵进行旋转。下面是通过setDisplayOrientation(result)方法进行旋转的代码:

public static int getDisplayRotation(Activity activity) {
    int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    switch (rotation) {
        case Surface.ROTATION_0: return 0;
        case Surface.ROTATION_90: return 90;
        case Surface.ROTATION_180: return 180;
        case Surface.ROTATION_270: return 270;
    }
    return 0;
}

public static void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
    // See android.hardware.Camera.setCameraDisplayOrientation for
    // documentation.
    Camera.CameraInfo info = new Camera.CameraInfo();
    Camera.getCameraInfo(cameraId, info);
    int degrees = getDisplayRotation(activity);
    int result;
    if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        result = (info.orientation + degrees) % 360;
        result = (360 - result) % 360; // compensate the mirror
    } else { // back-facing
        result = (info.orientation - degrees + 360) % 360;
    }
    camera.setDisplayOrientation(result);
}

通过OpenGL的矩阵进行旋转之后在OpenGL相关的文章中进行讲述。

设置预览帧率
通过Camera.Parameters中getSupportedPreviewFpsRange()可以获得摄像头支持的帧率变化范围,从中选取合适的设置给摄像头即可。相关的代码如下:

public static void setCameraFps(Camera camera, int fps) {
    Camera.Parameters params = camera.getParameters();
    int[] range = adaptPreviewFps(fps, params.getSupportedPreviewFpsRange());
    params.setPreviewFpsRange(range[0], range[1]);
    camera.setParameters(params);
}

private static int[] adaptPreviewFps(int expectedFps, List<int[]> fpsRanges) {
    expectedFps *= 1000;
    int[] closestRange = fpsRanges.get(0);
    int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);
    for (int[] range : fpsRanges) {
        if (range[0] <= expectedFps && range[1] >= expectedFps) {
            int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps);
            if (curMeasure < measure) {
                closestRange = range;
                measure = curMeasure;
            }
        }
    }
    return closestRange;
}

需要注意的是:对于Oppo和Vivo的前置摄像头,当fps不为15的时候,在弱光环境下预览图像会很黑

设置对焦方式
一般摄像头对焦的方式有两种:手动对焦和触摸对焦。下面的代码分别是设置自动对焦和触摸对焦的模式:


 

public static void setAutoFocusMode(Camera camera) {
    try {
        Camera.Parameters parameters = camera.getParameters();
        List<String> focusModes = parameters.getSupportedFocusModes();
        if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
            camera.setParameters(parameters);
        } else if (focusModes.size() > 0) {
            parameters.setFocusMode(focusModes.get(0));
            camera.setParameters(parameters);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void setTouchFocusMode(Camera camera) {
    try {
        Camera.Parameters parameters = camera.getParameters();
        List<String> focusModes = parameters.getSupportedFocusModes();
        if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            camera.setParameters(parameters);
        } else if (focusModes.size() > 0) {
            parameters.setFocusMode(focusModes.get(0));
            camera.setParameters(parameters);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

对于自动对焦这样设置后就完成了工作,但是对于触摸对焦则需要设置对应的对焦区域。
要准确地设置对焦区域,有三个步骤:一、得到当前点击的坐标位置;二、通过点击的坐标位置转换到摄像头预览界面坐标系统上的坐标;三、根据坐标生成对焦区域并且设置给摄像头。
整个摄像头预览界面定义了如下的坐标系统,对焦区域也需要对应到这个坐标系统中。

如果摄像机预览界面是通过SurfaceView显示的则比较简单,由于要确保不变形,会将SurfaceView进行拉伸,从而使得SurfaceView和预览图像大小比例一致,因此整个SurfaceView相当于预览界面,只需要得到当前点击点在整个SurfaceView上对应的坐标,然后转化为相应的对焦区域即可。
如果摄像机预览界面是通过GLSurfaceView显示的则要复杂一些,由于纹理需要进行裁剪,才能使得显示不变形,这样的话,我们要还原出整个预览界面的大小,然后通过当前点击的位置换算成预览界面坐标系统上的坐标,然后得到相应的对焦区域,然后设置给摄像机。
当设置好对焦区域后,通过调用Camera的autoFocus()方法即可完成触摸对焦。
整个过程代码量较多,请自行阅读项目源码。

设置缩放
当检测到手势缩放的时候,我们往往希望摄像头也能进行相应的缩放,其实这个实现还是比较简单的。首先需要加入缩放的手势识别,当识别到缩放的手势的时候,根据缩放的大小来对摄像头进行缩放。代码如下所示:


 

/**
 * Handles the pinch-to-zoom gesture
 */
private class ZoomGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (!mIsFocusing) {
            float progress = 0;
            if (detector.getScaleFactor() > 1.0f) {
                progress = CameraHolder.instance().cameraZoom(true);
            } else if (detector.getScaleFactor() < 1.0f) {
                progress = CameraHolder.instance().cameraZoom(false);
            } else {
                return false;
            }
            if(mZoomListener != null) {
                mZoomListener.onZoomProgress(progress);
            }
        }
        return true;
    }
}

public float cameraZoom(boolean isBig) {
    if(mState != State.PREVIEW || mCameraDevice == null || mCameraData == null) {
        return -1;
    }
    Camera.Parameters params = mCameraDevice.getParameters();
    if(isBig) {
        params.setZoom(Math.min(params.getZoom() + 1, params.getMaxZoom()));
    } else {
        params.setZoom(Math.max(params.getZoom() - 1, 0));
    }
    mCameraDevice.setParameters(params);
    return (float) params.getZoom()/params.getMaxZoom();
}

闪光灯操作
一个摄像头可能有相应的闪光灯,也可能没有,因此在使用闪光灯功能的时候先要确认是否有相应的闪光灯。检测摄像头是否有闪光灯的代码如下:

public static boolean supportFlash(Camera camera){
    Camera.Parameters params = camera.getParameters();
    List<String> flashModes = params.getSupportedFlashModes();
    if(flashModes == null) {
        return false;
    }
    for(String flashMode : flashModes) {
        if(Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {
            return true;
        }
    }
    return false;
}

切换闪光灯的代码如下:

public static void switchLight(Camera camera, Camera.Parameters cameraParameters) {
    if (cameraParameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) {
        cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
    } else {
        cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
    }
    try {
        camera.setParameters(cameraParameters);
    }catch (Exception e) {
        e.printStackTrace();
    }
}

四、开始预览


当打开了摄像头,并且设置好了摄像头相关的参数后,便可以通过调用Camera的startPreview()方法开始预览。有一个需要说明,无论是SurfaceView还是GLSurfaceView,都可以设置SurfaceHolder.Callback,当界面开始显示的时候打开摄像头并且开始预览,当界面销毁的时候停止预览并且关闭摄像头,这样的话当程序退到后台,其他应用也能调用摄像头。

private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        SopCastLog.d(SopCastConstant.TAG, "SurfaceView destroy");
        CameraHolder.instance().stopPreview();
        CameraHolder.instance().releaseCamera();
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        SopCastLog.d(SopCastConstant.TAG, "SurfaceView created");
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        SopCastLog.d(SopCastConstant.TAG, "SurfaceView width:" + width + " height:" + height);
        CameraHolder.instance().openCamera();
        CameraHolder.instance().startPreview();
    }
};



 

热门文章

暂无图片
编程学习 ·

Java输出数组的内容

Java输出数组的内容_一万个小时-CSDN博客_java打印数组内容1. 输出内容最常见的方式// List<String>类型的列表List<String> list new ArrayList<String>();list.add("First");list.add("Second");list.add("Third");list.ad…
暂无图片
编程学习 ·

母螳螂的“魅惑之术”

在它们对大蝗虫发起进攻的时候&#xff0c;我认认真真地观察了一次&#xff0c;因为它们突然像触电一样浑身痉挛起来&#xff0c;警觉地面对限前这个大家伙&#xff0c;然后放下自己优雅的身段和祈祷的双手&#xff0c;摆出了一个可怕的姿势。我被眼前的一幕吓到了&#xff0c;…
暂无图片
编程学习 ·

疯狂填词 mad_libs 第9章9.9.2

#win7 python3.7.0 import os,reos.chdir(d:\documents\program_language) file1open(.\疯狂填词_d9z9d2_r.txt) file2open(.\疯狂填词_d9z9d2_w.txt,w) words[ADJECTIVE,NOUN,VERB,NOUN] str1file1.read()#方法1 for word in words :word_replaceinput(fEnter a {word} :)str1…
暂无图片
编程学习 ·

HBASE 高可用

为了保证HBASE是高可用的,所依赖的HDFS和zookeeper也要是高可用的. 通过参数hbase.rootdir指定了连接到Hadoop的地址,mycluster表示为Hadoop的集群. HBASE本身的高可用很简单,只要在一个健康的集群其他节点通过命令 hbase-daemon.sh start master启动一个Hmaster进程,这个Hmast…
暂无图片
编程学习 ·

js事件操作语法

一、事件的绑定语法 语法形式1 事件监听 标签对象.addEventListener(click,function(){}); 语法形式2 on语法绑定 标签对象.onclick function(){} on语法是通过 等于赋值绑定的事件处理函数 , 等于赋值本质上执行的是覆盖赋值,后赋值的数据会覆盖之前存储的数据,也就是on…
暂无图片
编程学习 ·

Photoshop插件--晕影动态--选区--脚本开发--PS插件

文章目录1.插件界面2.关键代码2.1 选区2.2 动态晕影3.作者寄语PS是一款栅格图像编辑软件&#xff0c;具有许多强大的功能&#xff0c;本文演示如何通过脚本实现晕影动态和选区相关功能&#xff0c;展示从互联网收集而来的一个小插件&#xff0c;供大家学习交流&#xff0c;请勿…
暂无图片
编程学习 ·

vs LNK1104 无法打开文件“xxx.obj”

写在前面&#xff1a; 向大家推荐两本新书&#xff0c;《深度学习计算机视觉实战》和《学习OpenCV4&#xff1a;基于Python的算法实战》。 《深度学习计算机视觉实战》讲了计算机视觉理论基础&#xff0c;讲了案例项目&#xff0c;讲了模型部署&#xff0c;这些项目学会之后可以…
暂无图片
编程学习 ·

工业元宇宙的定义与实施路线图

工业元宇宙的定义与实施路线图 李正海 1 工业元宇宙 给大家做一个关于工业元宇宙的定义。对于工业&#xff0c;从设计的角度来讲&#xff0c;现在的设计人员已经做到了普遍的三维设计&#xff0c;但是进入元宇宙时代&#xff0c;就不仅仅只是三维设计了&#xff0c;我们的目…
暂无图片
编程学习 ·

【leectode 2022.1.15】完成一半题目

有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种知识点类型。 示例 1&#xff1a…
暂无图片
编程学习 ·

js 面试题总结

一、js原型与原型链 1. prototype 每个函数都有一个prototype属性&#xff0c;被称为显示原型 2._ _proto_ _ 每个实例对象都会有_ _proto_ _属性,其被称为隐式原型 每一个实例对象的隐式原型_ _proto_ _属性指向自身构造函数的显式原型prototype 3. constructor 每个prot…
暂无图片
编程学习 ·

java练习代码

打印自定义行数的空心菱形练习代码如下 import java.util.Scanner; public class daYinLengXing{public static void main(String[] args) {System.out.println("请输入行数");Scanner myScanner new Scanner(System.in);int g myScanner.nextInt();int num g%2;//…
暂无图片
编程学习 ·

RocketMQ-什么是死信队列?怎么解决

目录 什么是死信队列 死信队列的特征 死信消息的处理 什么是死信队列 当一条消息初次消费失败&#xff0c;消息队列会自动进行消费重试&#xff1b;达到最大重试次数后&#xff0c;若消费依然失败&#xff0c;则表明消费者在正常情况下无法正确地消费该消息&#xff0c;此时…
暂无图片
编程学习 ·

项目 cg day04

第4章 lua、Canal实现广告缓存 学习目标 Lua介绍 Lua语法 输出、变量定义、数据类型、流程控制(if..)、循环操作、函数、表(数组)、模块OpenResty介绍(理解配置) 封装了Nginx&#xff0c;并且提供了Lua扩展&#xff0c;大大提升了Nginx对并发处理的能&#xff0c;10K-1000K Lu…
暂无图片
编程学习 ·

输出三角形

#include <stdio.h> int main() { int i,j; for(i0;i<5;i) { for(j0;j<i;j) { printf("*"); } printf("\n"); } }
暂无图片
编程学习 ·

stm32的BOOTLOADER学习1

序言 最近计划学习stm32的BOOTLOADER学习,把学习过程记录下来 因为现在网上STM32C8T6还是比较贵的,根据我的需求flash空间小一些也可以,所以我决定使用stm32c6t6.这个芯片的空间是32kb的。 #熟悉芯片内部的空间地址 1、flash ROM&#xff1a; 大小32KB&#xff0c;范围&#xf…
暂无图片
编程学习 ·

通过awk和shell来限制IP多次访问之学不会你打死我

学不会你打死我 今天我们用shell脚本&#xff0c;awk工具来分析日志来判断是否存在扫描器来进行破解网站密码——限制访问次数过多的IP地址&#xff0c;通过Iptables来进行限制。代码在末尾 首先我们要先查看日志的格式&#xff0c;分析出我们需要筛选的内容&#xff0c;日志…
暂无图片
编程学习 ·

Python - 如何像程序员一样思考

在为计算机编写程序之前&#xff0c;您必须学会如何像程序员一样思考。学习像程序员一样思考对任何学生都很有价值。以下步骤可帮助任何人学习编码并了解计算机科学的价值——即使他们不打算成为计算机科学家。 顾名思义&#xff0c;Python经常被想要学习编程的人用作第一语言…
暂无图片
编程学习 ·

蓝桥杯python-数字三角形

问题描述 虽然我前后用了三种做法&#xff0c;但是我发现只有“优化思路_1”可以通过蓝桥杯官网中的测评&#xff0c;但是如果用c/c的话&#xff0c;每个都通得过&#xff0c;足以可见python的效率之低&#xff08;但耐不住人家好用啊&#xff08;哭笑&#xff09;&#xff09…