Android DIY抠图——想怎么抠就怎么抠

Android 实现照片抠出人像。 人脸检测:使用OpenCV的人脸检测功能(如Haar级联分类器或深度学习模型)来定位图像中的人脸区域。图像分割:利用图像分割算法(如GrabCut或基于深度学习的分割模型)来进一步精细地分割出人像区域。特征提取:对于检测到的人脸区域,提取特征,这些特征可以是人脸的形状、纹理、颜色等信息。预处理:对输入图像进行必要的预处理,如颜色空间转换、降噪等,以改善后续处理的准确性。后处理:对分割结果进行后处理,如平滑边界、去除小区域等,以优化最终的分离效果。Imgproc.erode()函数特定的结构元素腐蚀图像。 阅读详情

下面我分享Android的一个抠图技巧,这篇文章只适合有Android基础和向量基础的小伙伴,如果朋友们刚学Android不久,建议先去了解Android自定义View、Touch机制、Canvas/Path/Paint、向量等相关知识。
先来看看效果图:这张是原图
这里写图片描述

抠出脸部图片,下面是结果:
这里写图片描述

下面我给小伙伴们讲述具体流程。

1,获取本地相册,很简单,直接代码,不解释

public void scanAlbum(Context context, AlbumScanListener listener){
        try {
            if(listener == null){
                return;
            }
            List<LDAPhoto> photos = new ArrayList<>();
            String[] projection = {MediaStore.Images.ImageColumns.DATA};
            Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null,null);
            while(cursor.moveToNext()) {
                LDAPhoto photo = new LDAPhoto();
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                File file = new File(path);
                if(FileUtil.isFileExisted(path)) {
                    photo.setPath(path);
                    photo.setCreateTime(file.lastModified());
                    photos.add(photo);
                }
            }
            listener.onCompleted(photos);
        }catch (Exception e){
            listener.onScannerFailed();
        }
    }

2,全屏预览图片,手势层与图片层分离
整个view分图片层、带透明度的手势层(Mask),mask层覆盖图片层,手势相关逻辑都在mask层展开
这里写图片描述

简而言之,就是预览图片,上面铺一层蒙层,然后在蒙层里进行相关的抠图操作,预览的时候建议不是直接加载原图,而是根据屏幕宽高对图片进行自适应缩放,比如原图可能10000x10000,而屏幕只有100x100,那直接加载原图直接oom了,可以参考我的另一篇博客,如何巧妙的读取本地图片——bitmap的常用小技巧

3手势层监听手指轨迹,生成对应闭合区域(Path)
核心思想:定义mCanvas,操作bitmap,通过onDraw,中回调的canvas绘制到屏幕。

(1)初始化画笔:
mMaskPaint是蒙层画笔,mPathPaint是手指轨迹画笔,mCirclePaint是圆滑起点和终点的画笔(手指轨迹可能不是闭合,此时应该对两端点进行圆滑处理,mPath即用户手指轨迹)
令注:mStartX,mStartY是图片的左上角坐标,mBmWidth,mBmHeight是图片宽高

 private void initPaint() {
        mMaskPaint = new Paint();//mask画笔
        mMaskPaint.setAntiAlias(true);
        mMaskPaint.setStyle(Paint.Style.FILL);
        mMaskPaint.setStrokeCap(Paint.Cap.ROUND);
        mMaskPaint.setStrokeJoin(Paint.Join.ROUND);
        mMaskPaint.setColor(Color.parseColor("#d9000000"));

        mPathPaint = new Paint();//path画笔
        mPathPaint.setAlpha(0);
        mPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mPathPaint.setPathEffect(new CornerPathEffect(mPathWidth / 2));
        mPathPaint.setAntiAlias(true);
        mPathPaint.setDither(true);
        mPathPaint.setStyle(Paint.Style.STROKE);
        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
        mPathPaint.setStrokeWidth(mPathWidth);

        mCirclePaint = new Paint();//圆滑两端的圆形画笔
        mCirclePaint.setAlpha(0);
        mCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        mCirclePaint.setPathEffect(new CornerPathEffect(mPathWidth / 2));
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setDither(true);
        mCirclePaint.setStyle(Paint.Style.FILL);

        mPath = new Path();//轨迹
        mPathRect = new RectF();//轨迹所在的矩形区域
    }

(2)初始化onDraw,第一次调用onDraw操作

         if(mBitmap == null){
            mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);
        }
        if(mIsFirstDrawMask) {
            mIsFirstDrawMask = false;
            mCanvas.drawRect(0, 0, getWidth(), getHeight(), mMaskPaint);
        }

(3)通过onTouch事件监听轨迹——非常重要
关键概述:记录用户手指按下、移动、抬起过程中的轨迹,如果在移动过程中轨迹出现闭合,则终止记录,抠出闭合区域,如果抬起是恰好闭合曲线,则抠出闭合区域,如果抬起时轨迹不闭合,则自动闭合,并抠出闭合区域。判断轨迹是否闭合,需要通过向量叉乘的知识,同时需要计算闭合轨迹的面积,如果面积过小(小于100像素),则不扣图,提示“请在图片上画出封闭区域”,

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchStart(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                touchMove(x, y);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL://用户的抬手操作也可能出发的是cancel
                touchUp(x, y);
                break;
        }
        return true;
    }

private void touchStart(int x, int y) {
        if(mIsPathClose){//曲线已经闭合
            return;
        }
        mPath.reset();
        if(mPoints == null){
            mPoints = new ArrayList<>();
        }
        mPoints.clear();
        mPathPaint.setStyle(Paint.Style.STROKE);//设置画线
        mPath.moveTo(x, y);
        mCanvas.drawPath(mPath, mPathPaint);
        mCanvas.drawCircle(x, y, mPathWidth / 2, mCirclePaint);//起点边缘圆滑
        LDAPoint point = new LDAPoint(x, y);
        mPoints.add(point);
        mLastPoint = point;
        setPathRect(point);
        invalidate();
        mHadStarted = true;
    }

 private void touchMove(int x, int y) {
        if(!mHadStarted){
            //ACTION_DOWN可能在屏幕外触发
            touchStart(x, y);
            return;
        }
        if(mIsPathClose){
            return;
        }
        if(mLastPoint != null){
            //点距超过10才记录, 防止记录的点过多
            if(!isValidDistance(x, y)){
                return;
            }
        }
        //贝塞尔曲线
        mPath.quadTo(mLastPoint.x / 2 + x / 2, mLastPoint.y / 2 + y / 2, x, y);
        LDAPoint point = new LDAPoint(x, y);
        mPoints.add(point);//记录点
        //检测是否闭合
        int pos = checkPathClose();
        //实时更新path所在矩形
        updatePathRect(point);
        mLastPoint = point;
        if(pos >= 0){//已经闭合
            mIsPathClose = true;
            getClosePath(pos + 1);
            mPath.close();
            mPathPaint.setStyle(Paint.Style.FILL);
            drawMask();
            mCanvas.drawPath(mPath, mPathPaint);
            mCanvas.drawCircle(x, y, mPathWidth / 2, mCirclePaint);//端点圆滑
        }else {
            mCanvas.drawPath(mPath, mPathPaint);
            mCanvas.drawCircle(x, y, mPathWidth / 2, mCirclePaint);
        }
        invalidate();
    }

    private void touchUp(int x, int y) {
        mHadStarted = false;
        //moving过程已经闭合
        if(mIsPathClose){ return; }
        mPathPaint.setStyle(Paint.Style.FILL);//设置填充
        mPath.quadTo(mLastPoint.x / 2 + x / 2, mLastPoint.y / 2 + y / 2, x, y);
        LDAPoint point = new LDAPoint(x, y);
        mPoints.add(point);
        updatePathRect(point);
        mPath.close();//直接闭合
        Region region = new Region();
        Rect rect = new Rect();
        rect.left = (int) mPathRect.left;
        rect.right = (int) mPathRect.right;
        rect.bottom = (int) mPathRect.bottom;
        rect.top = (int) mPathRect.top;
        region.setPath(mPath, new Region(rect));
        if(calculateArea(region) < 100){//面积判断,小于100则提示
            Toast.makeText(getContext(), "请在图片上画出封闭区域", Toast.LENGTH_SHORT);
            reset();
            invalidate();
            return;
        }
        mIsPathClose = true;
        mCanvas.drawPath(mPath, mPathPaint);
        mCanvas.drawCircle(x, y, mPathWidth / 2, mCirclePaint);//端点圆滑
        invalidate();
    }

//检查path是否已经闭合,先通过矩形相交排斥的方法进行判断,在通过向量叉乘的方法进行判断
    private int checkPathClose() {
        int size = mPoints.size() - 3;
        LDAPoint q2 = mPoints.get(mPoints.size() - 1);
        LDAPoint q1 = mPoints.get(mPoints.size() - 2);
        for(int i = 0; i < size; i++){
            LDAPoint p1 = mPoints.get(i);
            LDAPoint p2 = mPoints.get(i + 1);

            if(!isScopeIntersect(p1, p2, q1, q2)){
                continue;
            }

            float d1 = crossProduct(p1, q2, q1);
            float d2 = crossProduct(q2, p2, q1);
            float d3 = crossProduct(q1, p2, p1);
            float d4 = crossProduct(p2, q2, p1);
            //d1 * d2 >= 0 说明p1p2线段和直线q1q2有交点
            //d3 * d4 >= 0 说明q1q2线段和直线p1p2有交点
            //两者同时满足则相交
            if(d1 * d2 >= 0 && d3 * d4 >= 0){
                return i;
            }
        }
        return -1;
    }

 //矩形相交排斥,p1 p2作为对角线的矩形和q1q2作为对角线的矩形没有相交
    public boolean isScopeIntersect(LDAPoint p1, LDAPoint p2, LDAPoint q1, LDAPoint q2){
        float maxQx = q2.x > q1.x ? q2.x : q1.x;
        float minQx = q1.x < q2.x ? q1.x : q2.x;
        if(p1.x < minQx && p2.x < minQx){
            return false;
        }
        if(p1.x > maxQx && p2.x > maxQx){
            return false;
        }
        float maxQy = q2.y > q1.y ? q2.y : q1.y;
        float minQy = q1.y < q2.y ? q1.y : q2.y;
        if(p1.y < minQy && p2.y < minQy){
            return false;
        }
        if(p1.y > maxQy && p2.y > maxQy){
            return false;
        }
        return true;
    }

//向量差乘
    private float crossProduct(LDAPoint p1, LDAPoint q2, LDAPoint q1) {
        LDAPoint p = new LDAPoint();
        p.x = p1.x - q1.x;
        p.y = p1.y - q1.y;
        LDAPoint q = new LDAPoint();
        q.x = q2.x - q1.x;
        q.y = q2.y - q1.y;
        return p.x * q.y- q.x * p.y;
    }

/**
     * 计算闭合区域面积
     * @param region
     * @return
     */
    private float calculateArea(Region region) {
        RegionIterator regionIterator = new RegionIterator(region);
        float area = 0;
        Rect tmpRect= new Rect();
        //通过计算矩形数量来获取面积
        while (regionIterator.next(tmpRect)) {
            area += tmpRect.width() * tmpRect.height();
        }
        return area;
    }

4绘制轨迹——非常重要
通过回调onDraw函数进行绘制得到的轨迹,绘制之前需要对轨迹进行插值,因为如果用户手指滑动很快,得到的轨迹的点很少,导致最后得到的轨迹棱角非常明显,所以需要对轨迹进行插值,具体方法后面详述。
下面是绘制代码

 if(mIsPathClose){
            mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);
            canvas.drawBitmap(mBitmap, 0, 0, mMaskPaint);
            mCanvas.drawRect(0, 0, getWidth(), getHeight(), mMaskPaint);
            //向量角度插值
            interploatePointsWithVertor();
            if(Build.VERSION.SDK_INT >= 19) {
                fixPath();//直接修正path
                mCanvas.drawPath(mPath, mPathPaint);
            }else{//通过四周填充矩形来修正path
                mCanvas.drawPath(mPath, mPathPaint);
                fixDrawOutside();
            }
            if(mPathListener != null){
                mPathListener.onPathClosed(mPath, mPathRect);
            }
            mPathPaint.setStrokeWidth(mPathWidth);
        }

完整的onDraw函数:

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //初始化绘制--------------------------------------------
        if(mBitmap == null){
            mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);
        }
        if(mIsFirstDrawMask) {
            mIsFirstDrawMask = false;
            mCanvas.drawRect(0, 0, getWidth(), getHeight(), mMaskPaint);
        }
        //闭合轨迹绘制------------------
        if(mIsPathClose){
            mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
            mCanvas = new Canvas(mBitmap);
            canvas.drawBitmap(mBitmap, 0, 0, mMaskPaint);
            mCanvas.drawRect(0, 0, getWidth(), getHeight(), mMaskPaint);
            //向量角度插值
            interploatePointsWithVertor();
            if(Build.VERSION.SDK_INT >= 19) {
                fixPath();//直接修正path
                mCanvas.drawPath(mPath, mPathPaint);
            }else{//通过四周填充矩形来修正path
                mCanvas.drawPath(mPath, mPathPaint);
                fixDrawOutside();
            }
            if(mPathListener != null){
                mPathListener.onPathClosed(mPath, mPathRect);
            }
            mPathPaint.setStrokeWidth(mPathWidth);
        }else{
            canvas.drawBitmap(mBitmap, 0, 0, mMaskPaint);
        }
    }

(1)轨迹插值——让曲线圆滑
向量插值:当角度< 135,则进行插值C1,C2,如图,AOB为原轨迹的三个点,通过向量计算角度小于135,则取OA,OB终点C1,C2插值,最后得到的是AC1C2B四个点,从而使棱角变得圆滑,如果有必要,可以进行二度插值,比如AC1C2角度小于135,则可以在C1A和C1C2终点插值。
这里写图片描述
下面是插值代码:

    private void interploatePointsWithVertor() {
        for(int j = 0; j < 2; j++) {//二度插值
            int size = mPoints.size();
            List<LDAPoint> list = new ArrayList<>();
            for (int i = 0; i < mPoints.size(); i++) {
                LDAPoint O = mPoints.get(i % size);

                LDAPoint A, B;
                if (i != 0) {
                    A = mPoints.get((i - 1) % size);
                } else {
                    A = mPoints.get(size - 1);
                }
                if (i == size - 1) {
                    B = mPoints.get(0);
                } else {
                    B = mPoints.get(i + 1);
                }
                double angel = calculateAngle(A, O, B);//计算夹角
                if (angel < LIMIT_ANGLE) {//插入中值
                    LDAPoint cOA = calculateCenterPoint(O, A);
                    LDAPoint cOB = calculateCenterPoint(O, B);
                    list.add(cOA);
                    list.add(cOB);
                } else {
                    list.add(O);
                }
            }
            mPoints = list;
        }
        getClosePath();//更新path
    }

//计算OA向量和OB向量夹角
    public double calculateAngle(LDAPoint A, LDAPoint O, LDAPoint B){
        LDAPoint OA = getVector(O, A);
        LDAPoint OB = getVector(O, B);
        double LOA = getVectorLength(OA);
        double LOB = getVectorLength(OB);
        double product = getVectorProduct(OA, OB);
        double cos = product / (LOA * LOB);
        double angle = Math.acos(cos) * (180 / Math.PI);
        return angle;
    }

//向量点乘
 private double getVectorProduct(LDAPoint oa, LDAPoint ob) {
        return oa.x * ob.x + oa.y * ob.y;
    }

//向量长度
 private double getVectorLength(LDAPoint v) {
      return Math.sqrt(v.x * v.x + v.y * v.y);
  }

//通过两点获取向量
 private LDAPoint getVector(LDAPoint start, LDAPoint end) {
     return new LDAPoint(end.x - start.x, end.y - start.y);
 }

(2)去除超出图片的闭合区域
如下图,黄色圈圈是手指轨迹,具有非常多的不确定性,对于非法区域则需要做出非法提示,对于局部理想区域则需要抠出包含图片的区域,超出的区域则需要舍弃掉。
这里写图片描述

           if(Build.VERSION.SDK_INT >= 19) {
                fixPath();//直接修正path
                mCanvas.drawPath(mPath, mPathPaint);
            }else{//通过四周填充矩形来修正path
                mCanvas.drawPath(mPath, mPathPaint);
                fixDrawOutside();
            }

要摒弃非法区域和超出区域,其实只需要将轨迹和图片区域取交集即可,path取交集操作有API限制,当API大于19的手机,可以采用此方法,代码如下:注意,mStartX,mStartY是图片的左上角坐标,mBmWidth,mBmHeight是图片宽高

 @TargetApi(Build.VERSION_CODES.KITKAT)
    private void fixPath() {
        fixRect();
        Path path = new Path();
        path.moveTo(mPathRect.left, mPathRect.top);
        path.lineTo(mPathRect.right, mPathRect.top);
        path.lineTo(mPathRect.right, mPathRect.bottom);
        path.lineTo(mPathRect.left, mPathRect.bottom);
        path.close();
        mPath.op(path, Path.Op.INTERSECT);
    }

    private void fixRect() {
        if(mPathRect.left < mStartX){
            mPathRect.left = mStartX;
        }
        if(mPathRect.top < mStartY){
            mPathRect.top = mStartY;
        }
        if(mPathRect.right > mStartX + mBmWidth){
            mPathRect.right = mStartX + mBmWidth;
        }
        if(mPathRect.bottom > mStartY + mBmHeight){
            mPathRect.bottom = mStartY + mBmHeight;
        }
    }

当Api小于19的时候,采用四周填充法:
这里写图片描述

上面是最极端的用户轨迹,方法是,先画出轨迹,然后四周填充蒙层:
这里写图片描述


//四周填充法
    private void fixDrawOutside() {
        if(mPathRect.left < mStartX){
            Rect rect = new Rect();
            rect.right = mStartX;
            rect.bottom = getHeight();
            rect.top = 0;
            rect.left = 0;
            mCanvas.drawRect(rect, mMaskPaint);
        }
        if(mPathRect.right > mStartX + mBmWidth){
            Rect rect = new Rect();
            rect.right = getWidth();
            rect.bottom = getHeight();
            rect.top = 0;
            rect.left = mStartX + mBmWidth;
            mCanvas.drawRect(rect, mMaskPaint);
        }
        if(mPathRect.top < mStartY){
            Rect rect = new Rect();
            rect.right = getWidth();
            rect.bottom = mStartY;
            rect.top = 0;
            rect.left = 0;
            mCanvas.drawRect(rect, mMaskPaint);
        }
        if(mPathRect.bottom > mStartY + mBmHeight){
            Rect rect = new Rect();
            rect.right = getWidth();
            rect.bottom = getHeight();
            rect.top = mStartY + mBmHeight;
            rect.left = 0;
            mCanvas.drawRect(rect, mMaskPaint);
        }

    }

5,得到抠图——非常重要
以上的操作都是为了正常的预览和得到正确的轨迹,下面代码是真正从图片中把指定区域抠出来

 private void saveBitmap(Path mPath, RectF pathRect) {
        Bitmap bm = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bm);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(mPath, paint);
        //path bitmap取交集
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(mBgBitmap, mStartX, mStartY, paint);
        paint.setXfermode(null);
        canvas.save(Canvas.ALL_SAVE_FLAG );
        canvas.restoreToCount(saveCount);
        saveBitmapToLocal(bm, pathRect);//保存bitmap到本地,普通的文件流即可
    }

下面是成果展示:
手指移动过程:
这里写图片描述

手指抬起:
这里写图片描述

得到图片:
这里写图片描述

过程比较复杂,但自认为文章写的很仔细,一个流程看下来,必然了然于胸,有疑问的小朋友,欢迎评论

Android | 教你如何开发一个证件照DIY小程序 华为机器学习服务(ML Kit) 提供机器学习套件,为开发者应用机器学习能力开发各类应用提供优质体验。得益于华为长期技术积累,ML Kit 为开发者提供简单易用、服务多样、技术领先的机器学习能力,助力开发者更快更好的开发各类AI应用。 使用这个SDK开发一个证件照DIY的小程序. 适合人群: 对华为 HMS Core 有兴趣的技术人员... 阅读详情

相关推荐

家用 DIY 电子试衣镜:硬件采购清单 + 开源软件 / 算法教程(可直接照着做)

按照这个清单和教程,零基础也能在 7 天内搭建出一套完整的家用电子试衣镜,实现 “录入自己的衣服 + 网上下载 + 实时试穿” 的核心功能,总成本控制在 5000 元以内,性价比远超商用成品!

cesske的博客 379

android 利用canvas自由裁剪图片小工具

原文在这里哦~http://www.cnblogs.com/rossoneri/p/3988405.html因为有个安卓项目需要用到图片裁剪的工具,所以上网查找了一下,发现这个博主写的还不错,但是就是博主太懒了233333,没有写注释,所以我就用了他的代码,理解了代码精髓部分,把我所了解到的代码写了注释,希望帮到大家哦~原博主写的时可以运行的,但是因为它时原图显示的,太大了,所以我将博主写的一些方法

suxiaofang的博客 1973

一键抠图3:Android实现人像抠图 (Portrait Matting)

这是一键抠图项目系列之《Android实现人像抠图 (Portrait Matting)》;本篇主要分享将Python训练后的matting模型部署到Android平台,提供头发细致级别抠图效果。

AI吃大瓜的博客 3839

Android 使用opencv实现抠图

1.openCv配置从https://github.com/opencv/opencv/releases/tag/3.3.0下载对应的android版本a.  import module 选择b.还要配置so文件来调取opencv的接口,在main文件夹下创建jniLibs文件夹,如下图:把下载的native目录下libs文件下的所有文件拷入jniLibs文件下c.别忘了把module里的buil...

hardWork_yulu的博客 6556

动态抠图AndroidDemo

这是用Unity和android和ios交互实现的动态抠图demo , 配套博客 http://mp.blog.youkuaiyun.com/postedit/79261011

Android源代码:图片剪切ImageCutDemo

Android源代码:图片剪切ImageCutDemo。。。。。。。。

使用Xfermode实现图片各种形状的抠图

一般来说如果想实现一个图片只显示特定的区域或者显示特定的形状,那么可以通过使用Android中的Path或者Xfermode来实现,当然对于一些特定的简单的形状也是可以通过使用shape的标签在xml中实现的,在这里我就不叙述shape和Path的用法了,本篇博客将直接讲述如何使用Xfermode来实现图片的抠图,这个功能在App上还是经常要用到的!!先来看一下效果图吧:1、圆角矩形2、圆形3、椭圆

朱玉强的博客 2955

Modbus4J vs EasyModbus4J:哪个Java库更适合你的工业项目?

本文深入对比了Java领域两大Modbus通信库Modbus4J与EasyModbus4J。Modbus4J设计稳健、类型安全,适合复杂业务集成;EasyModbus4J基于Netty,在高并发场景下性能更优。文章从架构、性能、API及生产环境多维度分析,为工业物联网项目选型提供决策依据。

weixin_28475533的博客 310

[Android] 万能AI证件照 v1.3.2 —— 高清证件照制作工具(可离线)

Android 万能AI证件照

极简之美,独具匠心。 1480

LT9211 单路mipi转双路mipi芯片,方案成熟,提供技术支持

LT9211 单路mipi转双路mipi芯片

TEL15012949045的博客 5014

Windows下Fortran调用CGNS库的完整踩坑实录:从HDF5、CMake到VS2019+IVF2020的保姆级配置

本文详细记录了在Windows环境下使用Fortran调用CGNS库的完整配置过程,涵盖HDF5安装、CMake配置、VS2019与IVF2020集成等关键步骤。通过实战经验分享,帮助开发者解决常见编译错误和链接问题,实现高效的Fortran与CGNS库集成。

weixin_28707739的博客 349

android 控件 证件照,Android | 教你如何开发一个证件照DIY小程序

task = this.analyzer.asyncAnalyseFrame(mlFrame);task.addOnSuccessListener(new OnSuccessListener() {@Override public void onSuccess(MLImageSegmentation mlImageSegmentationResults) {// Tr...

weixin_42356282的博客 640

Vue3 - 详细实现接入使用海康威视监控云端显示及控制功能,网站集成安装海康监控及实时监控显示、回放、放大缩小画面、视频画面截图、截屏抓拍、摄像头控制,实时推流及播放器解决延迟卡顿等问题(详细代码)

vue3,nuxt3,海康威视,海康监控,摄像头监控,实时监控画面,海康插件,视频播放,回放,视频流地址,取流协议,视频sdk播放,hik,rtsp,rtmp,hls,ws,网站,网页,h5页面,后台管理系统,vue3对接海康监控设备,无法连接,报错,vue3对接海康处理接口认证规则,监控实时预览和回放功能,海康api接口,安全认证,海康开放平台,官方文档,播放海康摄像头的监控视频,海康实时监控播放详细步骤,web,黑屏,特别卡不流畅怎么办,解决无法推流,录像,连接不上,vue3,海康,vue海康视频监控,

🏆 前端领域优质创作者 1万+

Android 手机的高级终端 Termux 安装使用、busybox、安卓 手机 web

手机已经 root安装 tsu,这是一个 su 的 Termux 版本,是一个真正的 root 权限,用来在 Termux上替代su,操作不慎可能对手机有安全风险,因为官方封装了,所以安装也很简单:安装 tsu:pkg install tsu -y切换到 root:tsu退出 root:exittsu切换 root 用户时会弹出 root 授权提示,给予其 root 权限即可。在管理员身份下输入 exit 可回到普通用户身份。

freeking101的博客 10万+

学Simulink--基础微电网场景实例:基于Simulink的微电网黑启动过程建模与仿真

黑启动(Black Start)是指在全系统停电(无电压、无频率)状态下,微电网依靠内部电源自主恢复供电的能力。🌩️典型场景主网长时间故障自然灾害导致区域停电计划检修后重启❗挑战无参考电压/频率 → 逆变器无法锁相负荷逐级投入 → 需避免过流/过压多电源协调 → 防止环流或失步✅黑启动成功标准在30 秒内恢复关键负荷供电电压/频率稳定在范围内无设备损坏或保护跳闸🎯本文目标:手把手教你使用 Simulink + Simscape + Stateflow 搭建。

amy_mhd的博客 200

Labview如何0基础自学快速入门?(纯干货帖)

讲解了如何学习Labview,Labview如何0基础入门,自学Labview的方法。什么都不会如何学好Labview的分享,欢迎关注文章~文章提供《Labview宝典》pdf下载,和《Labview虚拟仪器数据采集与通信控制》pdf下载

关注我,更多技术问题欢迎私聊~ 2万+

安卓好软-----手机端 万能AI证件照 方便简单 免费

万能AI证件照是一款免费无广可制作高清证件照的软件,其原理是利用AI一键智能人像抠图及5一键换底、裁剪等功能实现轻松后期编辑及保存输出,另有200+款适合国内应用场景证件照规格可供选择使用。还提供多种图片调整调节人像,对比度、锐度、亮度、饱和度等等。还可根据自己喜好进行微调整,包含镜像、旋转、消除、矫正2及精细抠图,另配套了。提供一寸、二寸、大一寸、小二寸等常心规证件照寸照尺寸,还包含。普通话水平始测试、英语四六级、BEC、TOPIC等。身份证、驾驶证、社保卡、居住证、护照。新加波、美国、法国等多个。

mg668的博客 761

数学建模学习(78):多输入多输出回归预测模型(结合XGBoost实现)

多输入多输出回归预测问题求解

全栈川川 8130

小米平板2Android分区表,DIY:8G+128G双系统小米平板2

原标题:DIY:8G+128G双系统小米平板2十一月初在某宁入手一台16G小米平板2, 之前就了解到小米平板2可以改成4G+128G ,到货后就在某宝找改运存和内存的店铺,偶然间看到了8G+128G的改装组合。抱着试试看的想法就把平板寄过去了,经过漫长的等待。终于等到了8G+128G看似X炸天的小米平板2。因为8G+128G这个方案刚兴起,所以店家也没来得及给我改成双系统。收到后,自己摸索了几天,...

weixin_42348021的博客 3508

ICME 2026 即插即用 | 特征增强篇 | FSD:多尺度小波分解+频域特征融合,结构细节纹理均增强,性能猛涨!

本文提出了一种基于频域结构解耦和区域感知融合的双路径学习框架(FSD),用于低光图像超分辨率任务。该框架通过多尺度小波变换将输入图像分解为低频亮度结构和高频纹理细节两个正交分量,并采用双路径网络分别处理这两个分量,最后通过跨频段语义重组模块实现特征融合。实验表明,FSD模块在多项指标上显著优于现有方法,能有效提升图像的结构自然度和细节清晰度。该模块适用于对频域/结构/纹理敏感的视觉任务,可直接嵌入网络实现端到端训练。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 140

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

动态壁纸安卓_高清无水印!这4款壁纸软件,让你的手机好看又独特

手机与我们每日相伴,我们总会精心挑选各种图片设置为手机壁纸,有时换个壁纸就像换了一种心情一样。但网上搜索的壁纸大同小异,有没有风格独特又有趣的?今天就给大家推荐4款各具特色的壁纸App,保证给你耳目一新的感觉。 1、 每日壁纸杂志:主打艺术、人文元素的高质量壁纸适用平台:安卓、苹果每日壁纸杂志是一个壁纸为主题的杂志应用,内容贡献者中有很多专业人士,包括插画师、摄影师、时尚编辑等,应用内有大量高清...

weixin_39549110的博客 1839

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

编程制作动态壁纸的思路_使用这款网红APP,制作魔性手机壁纸!

https://www.zhihu.com/video/952160646545031168如何自己制作一张手机壁纸?一般人就是掏出手机,找好角度,拍摄一张图片,经过各种精修调色然后壁纸完成。而大神们一般是打开PS,三下五除二完成!作为二般人的我,则是掏出手机打开 patternator 这款免费 APP 制作一张独一无二的魔性壁纸。patternator 被 App Store 评为 2016年...

weixin_28893221的博客 451

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

黏土滤镜特效怎么调?交给这5款智能图片软件吧

▶创意不设限:除了黏土滤镜,还有好多其他小工具,随你混搭,让你的图片编辑之旅就像DIY小手工,每次都新鲜感爆棚。踏入Unidream,就像走进了一个梦幻工坊,你的照片在这里摇身一变,成了黏土艺术展上的小明星,让人一眼难忘!Remini就像个会施魔法的小伙伴,一点一滴,让你的照片“噗嗤”一下变成超细腻的黏土动画,连心情都跟着变好!这家伙就像是你的私人黏土小精灵,动动手指,照片秒变黏土风,创意满满又不失乐趣,轻松得就像下午茶时光!那么,想知道去哪可以找到有黏土滤镜的工具吗?别急,粘土滤镜ai软件免费告诉你~

qq_40427481的博客 656

fooview辅助功能 shell_fooView — 安卓最强大的悬浮球应用

本文最后更新于2019-08-13 00:28:13,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜。悬浮球最早接触还是在爱疯4上,当时可谓是惊艳住了还在玩泥巴的老夜。通过一个透明度较高的悬浮按钮实现一些功能或切换进程还是非常提高效率与体验的,现在的智能手机都比较娇贵小磕小碰就是一堆毛病,遇到底部虚拟按键失效就欲哭无泪了,这时候虚拟按键工具的作用就突显出来了,通过自定义的点击事件或者...

weixin_39947961的博客 1327

拒绝平庸,这10个鲜为人知的实用网站,让你受益无穷!

时间如白驹过隙,转眼又到了年底,翻看年初立的flag,完成的事情好像屈指可数。但是无论如何,充分利用这一年中所剩下的时间并尽力完成未完成的事,才是正经事。 话不多说,小编今天想和你分享一些「涨知识」的网站,所谓的涨知识,不仅指从书本上能够获得的知识,它还包括能够让自己增长见识、满足好奇心的信息。这些网站有为科普知识、健康生活、资讯获取、IT技术、产品工具5个类型。希望其中有一些网站你会喜欢。 1、Photopea Photopea 是一个免费且实用的高级图像编辑器。可支持打开 PSD,XCF,SKetch

huoshi122的博客 449

编程制作动态壁纸的思路_高清无水印!这4款壁纸软件,让你的手机好看又独特...

手机与我们每日相伴,我们总会精心挑选各种图片设置为手机壁纸,有时换个壁纸就像换了一种心情一样。但网上搜索的壁纸大同小异,有没有风格独特又有趣的?今天就给大家推荐4款各具特色的壁纸App,保证给你耳目一新的感觉。 1、 每日壁纸杂志:主打艺术、人文元素的高质量壁纸适用平台:安卓、苹果每日壁纸杂志是一个壁纸为主题的杂志应用,内容贡献者中有很多专业人士,包括插画师、摄影师、时尚编辑等,应用内有大量高清...

weixin_39729840的博客 1028
上一篇: Android bitmap 一些常用用法
下一篇: Android 音视频录制概述
评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值