主頁 > 後端開發 > Java版人臉跟蹤三部曲之三:編碼實戰

Java版人臉跟蹤三部曲之三:編碼實戰

2023-07-09 07:46:29 後端開發

歡迎訪問我的GitHub

這里分類和匯總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 作為《Java版人臉跟蹤三部曲》系列的終篇,本文會與大家一起寫出完整的人臉跟蹤應用代碼
  • 前文《開發設計》中,已經對人臉跟蹤的核心技術、應用主流程、例外處理等方方面面做了詳細設計,建議您簡單回顧一下
  • 接下來,自頂向下,先整體設計好主框架和關鍵類

程式主框架和關鍵類

  • 聽欣宸嘮叨了兩篇文章,終于要看具體代碼了,整體上看,最關鍵的三個類如下圖:
    在這里插入圖片描述
  • 可見把功能、流程、知識點梳理清楚后,代碼其實并不多,而且各司其職,分工明確,接下來開始編碼,ObejctTracker負責實作跟蹤功能,就從它開始

ObejctTracker.java:跟蹤能力的提供者

  • 從前面的圖中可知,與跟蹤有關的服務都是ObejctTracker類提供的,此類涉及知識點略多,在撰寫代碼前,先做一下簡單的設計
  • 從功能看,ObejctTracker會對外提供如下兩個方法:
方法名 作用 入參 回傳 內部實作
createTrackedObject 主程式如果從視頻幀中首次次檢測到人臉,就會呼叫createTrackedObject方法,表示開始跟蹤了 mRgba:出現人臉的圖片
region:人臉在圖片中的位置
提取人臉的hue,生成直方圖
objectTracking 開始跟蹤后,主程式從攝像頭取到的每一幀圖片后,都會呼叫此方法,用于得到人臉在這一幀中的位置 mRgba:圖片 人臉在輸入圖片中位置 用人臉hue直方圖對輸入圖片進行計算,得到反向投影圖,在反向投影圖上做CamShift計算得到人臉位置
  • 除了上述兩個對外方法,ObejctTracker內部還要準備如下兩個輔助方法:
方法名 作用 入參 回傳 內部實作
rgba2Hue 將RGB顏色空間的圖片轉為HSV,再提取出hue通道,生成直方圖 rgba:人臉圖片 List<Mat>:直方圖
lostTrace 對比objectTracking方法回傳的結果與上次出現的位置,確定人有沒有跟丟 lastRect:上次出現的位置
currentRect:objectTracking方法檢測到的當前幀上的位置
true表示跟丟了,false表示沒有跟丟 對比兩個矩形的差距是否超過一個門限,正常情況下連續兩幀中的人臉差別不會太大,所以一旦差別大了就表示跟丟了,currentRect的位置上不是人臉
  • 還有幾個成員變數也很重要:
    // 每一幀影像的反向投影圖都用這個成員變數來保存
    private Mat prob;

    // 保存最近一次確認的頭像的位置,每當新的一幀到來時,都從這個位置開始追蹤(也就是反向投影圖做CamShift計算的起始位置)
    private Rect trackRect;

    // 直方圖,在跟丟之前,每一幀影像都要用到這個直方圖來生成反向投影
    private Mat hist;
  • 設計完成,現在可以給出完整的ObejctTracker.java原始碼了:
package com.bolingcavalry.grabpush.extend;

import lombok.extern.slf4j.Slf4j;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.video.Video;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

/**
 * @author willzhao
 * @version 1.0
 * @description TODO
 * @date 2022/1/8 21:21
 */
@Slf4j
public class ObjectTracker {

    /**
     * 上一個矩形和當前矩形的差距達到多少的時候,才算跟丟,您可以自行調整
     */
    private static final double LOST_GATE = 0.8d;

    // [0.0, 256.0]表示直方圖能表示像素值從0.0到256的像素
    private static final MatOfFloat RANGES = new MatOfFloat(0f, 256f);

    private Mat mask;

    // 保存用來追蹤的每一幀的反向投影圖
    private Mat prob;

    // 保存最近一次確認的頭像的位置,每當新的一幀到來時,都從這個位置開始追蹤(也就是反向投影圖做CamShift計算的起始位置)
    private Rect trackRect;

    // 直方圖
    private Mat hist;


    public ObjectTracker(Mat rgba) {
        hist = new Mat();
        trackRect = new Rect();
        mask = new Mat(rgba.size(), CvType.CV_8UC1);
        prob = new Mat(rgba.size(), CvType.CV_8UC1);
    }

    /**
     * 將攝像頭傳來的圖片提取出hue通道,放入hueList中
     * 將攝像頭傳來的RGB顏色空間的圖片轉為HSV顏色空間,
     * 然后檢查HSV三個通道的值是否在指定范圍內,mask中記錄了檢查結果
     * 再將hsv中的hue提取出來
     * @param rgba
     */
    private List<Mat> rgba2Hue(Mat rgba) {
        // 實體化Mat,顯然,hsv是三通道,hue是hsv三通道其中的一個,所以hue是一通道
        Mat hsv = new Mat(rgba.size(), CvType.CV_8UC3);
        Mat hue = new Mat(rgba.size(), CvType.CV_8UC1);

        // 1. 先轉換
        // 轉換顏色空間,RGB到HSV
        Imgproc.cvtColor(rgba, hsv, Imgproc.COLOR_RGB2HSV);

        int vMin = 65, vMax = 256, sMin = 55;
        //inRange函式的功能是檢查輸入陣列每個元素大小是否在2個給定數值之間,可以有多通道,mask保存0通道的最小值,也就是h分量
        //這里利用了hsv的3個通道,比較h,0~180,s,smin~256,v,min(vmin,vmax),max(vmin,vmax),如果3個通道都在對應的范圍內,
        //則mask對應的那個點的值全為1(0xff),否則為0(0x00).
        Core.inRange(
                hsv,
                new Scalar(0, sMin, Math.min(vMin, vMax)),
                new Scalar(180, 256, Math.max(vMin, vMax)),
                mask
        );

        // 2. 再提取
        // 把hsv的資料放入hsvList中,用于稍后提取出其中的hue
        List<Mat> hsvList = new Vector<>();
        hsvList.add(hsv);

        // 準備好hueList,用于接收通道
        // hue初始化為與hsv大小深度一樣的矩陣,色調的度量是用角度表示的,紅綠藍之間相差120度,反色相差180度
        hue.create(hsv.size(), hsv.depth());

        List<Mat> hueList = new Vector<>();
        hueList.add(hue);

        // 描述如何提取:從目標的0位置提取到目的地的0位置
        MatOfInt from_to = new MatOfInt(0, 0);

        // 提取操作:將hsv第一個通道(也就是色調)的數復制到hue中,0索引陣列
        Core.mixChannels(hsvList, hueList, from_to);

        return hueList;
    }

    /**
     * 當外部呼叫方確定了人臉在圖片中的位置后,就可以呼叫createTrackedObject開始跟蹤,
     * 該方法中會先生成人臉的hue的直方圖,用于給后續幀生成反向投影
     * @param mRgba
     * @param region
     */
    public void createTrackedObject(Mat mRgba, Rect region) {
        hist.release();

        //將攝像頭的視頻幀轉化成hsv,然后再提取出其中的hue通道
        List<Mat> hueList = rgba2Hue(mRgba);

        // 人臉區域的mask
        Mat tempMask = mask.submat(region);

        // histSize表示這個直方圖分成多少份(即多少個直方柱),就是 bin的個數
        MatOfInt histSize = new MatOfInt(25);
        // 只要頭像區域的資料
        List<Mat> images = Collections.singletonList(hueList.get(0).submat(region));
        // 計算頭像的hue直方圖,結果在hist中
        Imgproc.calcHist(images, new MatOfInt(0), tempMask, hist, histSize, RANGES);

        // 將hist矩陣進行陣列范圍歸一化,都歸一化到0~255
        Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);

        // 這個trackRect記錄了人臉最后一次出現的位置,后面新的幀到來時,就從trackRect位置開始做CamShift計算
        trackRect = region;
    }

    /**
     * 在開始跟蹤后,每當攝像頭新的一幀到來時,外部就會呼叫objectTracking,將新的幀傳入,
     * 此時,會用前面準備好的人臉hue直方圖,將新的幀計算出反向投影圖,
     * 再在反向投影圖上執行CamShift計算,找到密度最大處,即人臉在新的幀上的位置,
     * 將這個位置作為回傳值,回傳
     * @param mRgba 新的一幀
     * @return 人臉在新的一幀上的位置
     */
    public Rect objectTracking(Mat mRgba) {
        // 新的圖片,提取hue
        List<Mat> hueList;
        try {
           // 實測此處可能拋出例外,要注意捕獲,避免程式退出
            hueList = rgba2Hue(mRgba);
        } catch (CvException cvException) {
            log.error("cvtColor exception", cvException);
            trackRect = null;
            return null;
        }

        // 用頭像直方圖在新圖片的hue通道資料中計算反向投影,
        Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, prob, RANGES, 1.0);
        // 計算兩個陣列的按位連接(dst = src1 & src2)計算兩個陣列或陣列和標量的每個元素的逐位連接,
        Core.bitwise_and(prob, mask, prob, new Mat());

        // 在反向投影上進行CamShift計算,回傳值就是密度最大處,即追蹤結果
        RotatedRect rotatedRect = Video.CamShift(prob, trackRect, new TermCriteria(TermCriteria.EPS, 10, 1));

        // 轉為Rect物件
        Rect camShiftRect = rotatedRect.boundingRect();

        // 比較追蹤前和追蹤后的資料,如果出現太大偏差,就認為追蹤失敗
        if (lostTrace(trackRect, camShiftRect)) {
            log.info("lost trace!");
            trackRect = null;
            return null;
        }

        // 將本次最終到的目標作為下次追蹤的物件
        trackRect = camShiftRect;

        return camShiftRect;
    }

    /**
     * 變化率的絕對值
     * @param last 變化前
     * @param current 變化后
     * @return
     */
    private static double changeRate(int last, int current) {
        return Math.abs((double)(current-last)/(double) last);
    }

    /**
     * 本次和上一次寬度或者高度的變化率,一旦超過閾值就認為跟蹤失敗
     * @param lastRect
     * @param currentRect
     * @return
     */
    private static boolean lostTrace(Rect lastRect, Rect currentRect) {
        // 0不能做除數,如果發現0就認跟丟了
        if (lastRect.width<1 || lastRect.height<1) {
            return true;
        }

        double widthChangeRate = changeRate(lastRect.width, currentRect.width);

        if (widthChangeRate>LOST_GATE) {
            log.info("1. lost trace, old [{}], new [{}], rate [{}]", lastRect.width, currentRect.width, widthChangeRate);
            return true;
        }

        double heightChangeRate = changeRate(lastRect.height, currentRect.height);

        if (heightChangeRate>LOST_GATE) {
            log.info("2. lost trace, old [{}], new [{}], rate [{}]", lastRect.height, currentRect.height, heightChangeRate);
            return true;
        }

        return false;
    }
}
  • 最核心的跟蹤服務已經完成,接下來要實作完整業務邏輯,即:CamShiftDetectService.java

CamShiftDetectService.java:業務邏輯的提供者

  • 有了核心能力,接下來要做的就是在業務中使用這個能力,前文已設計好完整的業務邏輯,這里先簡單回顧一下:
    在這里插入圖片描述
  • 可見主要業務流程可以用兩個狀態+行為來表示:
  1. 還未開始跟蹤:對每一幀做人臉檢測,一旦檢測到,就進入跟蹤狀態,并呼叫ObjectTracker.createTrackedObject生成人臉的hue直方圖
  2. 已處于跟蹤狀態:對每一幀影像,都呼叫ObjectTracker.objectTracking去檢查人臉在影像中的位置,直到到跟丟了為止,一旦跟丟了,就重新進入到還未開始跟蹤的狀態
  • 現在我們已經清楚了CamShiftDetectService.java要做的具體事情,接下來看看有哪些重要方法:
方法名 作用 入參 回傳 內部實作
init 被主程式呼叫的初始化方法,在應用啟動的時候會呼叫一次 加載人臉檢測的模型
convert 每當主程式從攝像頭拿到新的一幀后,都會呼叫此方法 frame:來自攝像頭的最新一幀 被處理后的幀,會被主程式展現在預覽視窗 convert方法內部實作了前面提到的兩種狀態和行為(還未開始跟蹤、已處于跟蹤狀態)
releaseOutputResource 程式結束前,被主程式呼叫的釋放資源的方法 釋放一些成員變數的資源
  • 再來看看有哪些重要的成員變數,如下所示,isInTracing表示當前是否處于跟蹤狀態,classifier用于檢測人臉:
/**
     * 每一幀原始圖片的物件
     */
    private Mat grabbedImage = null;

    /**
     * 分類器
     */
    private CascadeClassifier classifier;

    /**
     * 轉換器
     */
    private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();

    /**
     * 模型檔案的下載地址
     */
    private String modelFilePath;

    /**
     * 存放RGBA圖片Mat
     */
    private Mat mRgba;

    /**
     * 存放灰度圖片的Mat,僅用在人臉檢測的時候
     */
    private Mat mGray;

    /**
     * 跟蹤服務類
     */
    private ObjectTracker objectTracker;

    /**
     * 表示當前是否正在跟蹤目標
     */
    private boolean isInTracing = false;
  • 現在可以給出CamShiftDetectService.java的完整代碼了:
package com.bolingcavalry.grabpush.extend;

import com.bolingcavalry.grabpush.Util;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.RectVector;
import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
import java.io.File;
import static org.bytedeco.opencv.global.opencv_imgproc.CV_BGR2GRAY;
import static org.bytedeco.opencv.global.opencv_imgproc.cvtColor;

@Slf4j
public class CamShiftDetectService implements DetectService {

    /**
     * 每一幀原始圖片的物件
     */
    private Mat grabbedImage = null;

    /**
     * 分類器
     */
    private CascadeClassifier classifier;

    /**
     * 轉換器
     */
    private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();

    /**
     * 模型檔案的下載地址
     */
    private String modelFilePath;

    /**
     * 存放RGBA圖片Mat
     */
    private Mat mRgba;

    /**
     * 存放灰度圖片的Mat,僅用在人臉檢測的時候
     */
    private Mat mGray;

    /**
     * 跟蹤服務類
     */
    private ObjectTracker objectTracker;

    /**
     * 表示當前是否正在跟蹤目標
     */
    private boolean isInTracing = false;

    /**
     * 構造方法,在此指定模型檔案的下載地址
     * @param modelFilePath
     */
    public CamShiftDetectService(String modelFilePath) {
        this.modelFilePath = modelFilePath;
    }

    /**
     * 音頻采樣物件的初始化
     * @throws Exception
     */
    @Override
    public void init() throws Exception {
        log.info("開始加載模型檔案");
        // 模型檔案下載后的完整地址
        String classifierName = new File(modelFilePath).getAbsolutePath();

        // 根據模型檔案實體化分類器
        classifier = new CascadeClassifier(classifierName);

        if (classifier == null) {
            log.error("Error loading classifier file [{}]", classifierName);
            System.exit(1);
        }

        log.info("模型檔案加載完畢,初始化完成");
    }



    @Override
    public Frame convert(Frame frame) {
        // 由幀轉為Mat
        grabbedImage = converter.convert(frame);

        // 初始化灰度Mat
        if (null==mGray) {
            mGray = Util.initGrayImageMat(grabbedImage);
        }

        // 初始化RGBA的Mat
        if (null==mRgba) {
            mRgba = Util.initRgbaImageMat(grabbedImage);
        }

        // 如果未在追蹤狀態
        if (!isInTracing) {
            // 存放檢測結果的容器
            RectVector objects = new RectVector();

            // 當前圖片轉為灰度圖片
            cvtColor(grabbedImage, mGray, CV_BGR2GRAY);

            // 開始檢測
            classifier.detectMultiScale(mGray, objects);

            // 檢測結果總數
            long total = objects.size();

            // 當前實體是只追蹤一人,因此一旦檢測結果不等于一,就不處理,您可以根據自己業務情況修改此處
            if (total!=1) {
                objects.close();
                return frame;
            }

            log.info("start new trace");

            Rect r = objects.get(0);
            int x = r.x(), y = r.y(), w = r.width(), h = r.height();

            // 得到opencv的mat,其格式是RGBA
            org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);

            // 在buildJavacvBGR2OpenCVRGBA方法內部,有可能在執行native方法的是否發生例外,要做針對性處理
            if (null==openCVRGBAMat) {
                objects.close();
                return frame;
            }

            // 如果第一次追蹤,要實體化objectTracker
            if (null==objectTracker) {
                objectTracker = new ObjectTracker(openCVRGBAMat);
            }

            // 創建跟蹤目標
            objectTracker.createTrackedObject(openCVRGBAMat, new org.opencv.core.Rect(x, y, w, h));
            // 根據本次檢測結果給原圖示注人臉矩形框
            Util.rectOnImage(grabbedImage, x, y, w, h);

            // 釋放檢測結果資源
            objects.close();

            // 修改標志,表示當前正在跟蹤
            isInTracing = true;

            // 將標注過的圖片轉為幀,回傳
            return converter.convert(grabbedImage);
        }

        // 代碼走到這里,表示已經在追蹤狀態了

        // 得到opencv的mat,其格式是RGBA
        org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);

        // 在buildJavacvBGR2OpenCVRGBA方法內部,有可能在執行native方法的是否發生例外,要做針對性處理
        if (null==openCVRGBAMat) {
            return frame;
        }

        // 基于上一次的檢測結果開始跟蹤
        org.opencv.core.Rect rotatedRect = objectTracker.objectTracking(openCVRGBAMat);

        // 如果rotatedRect為空,表示跟蹤失敗,此時要修改狀態為"未跟蹤"
        if (null==rotatedRect) {
            isInTracing = false;
            // 回傳原始幀
            return frame;
        }

        // 代碼能走到這里,表示跟蹤成功,拿到的新的一幀上的目標的位置,此時就在新位置上
//        Util.rectOnImage(grabbedImage, rotatedRect.x, rotatedRect.y, rotatedRect.width, rotatedRect.height);
        // 矩形框的整體向下放一些(總高度的五分之一),另外跟蹤得到的高度過大,畫出的矩形框把脖子也框上了,這里改用寬度作為高度
        Util.rectOnImage(grabbedImage, rotatedRect.x, rotatedRect.y + rotatedRect.height/5, rotatedRect.width, rotatedRect.width);
        return converter.convert(grabbedImage);
    }

    /**
     * 程式結束前,釋放人臉識別的資源
     */
    @Override
    public void releaseOutputResource() {
        if (null!=grabbedImage) {
            grabbedImage.release();
        }

        if (null!=mGray) {
            mGray.release();
        }

        if (null!=mRgba) {
            mRgba.release();
        }

        if (null==classifier) {
            classifier.close();
        }
    }
}
  • 至此·,功能已經完成得七七八八,再來寫完主程式就可以運行了;

PreviewCameraWithCamShift.java:主程式

  • 《JavaCV的攝像頭實戰之一:基礎》創建的simple-grab-push工程中已經準備好了父類AbstractCameraApplication,所以本篇繼續使用該工程,創建子類PreviewCameraWithCamShift實作那些抽象方法即可
  • 編碼前先回顧父類的基礎結構,如下圖,粗體是父類定義的各個方法,紅色塊都是需要子類來實作抽象方法,所以接下來,咱們以本地視窗預覽為目標實作這三個紅色方法即可:
    在這里插入圖片描述
  • 新建檔案PreviewCameraWithCamShift.java,這是AbstractCameraApplication的子類,其代碼很簡單,接下來按上圖順序依次說明
  • 先定義CanvasFrame型別的成員變數previewCanvas,這是展示視頻幀的本地視窗:
protected CanvasFrame previewCanvas
  • 把前面創建的DetectService作為成員變數,后面檢測的時候會用到:
    /**
     * 檢測工具介面
     */
    private DetectService detectService;
  • PreviewCameraWithCamShift的構造方法,接受DetectService的實體:
    /**
     * 不同的檢測工具,可以通過構造方法傳入
     * @param detectService
     */
    public PreviewCameraWithCamShift(DetectService detectService) {
        this.detectService = detectService;
    }
  • 然后是初始化操作,可見是previewCanvas的實體化和引數設定,還有檢測、識別的初始化操作:
    @Override
    protected void initOutput() throws Exception {
        previewCanvas = new CanvasFrame("攝像頭預覽", CanvasFrame.getDefaultGamma() / grabber.getGamma());
        previewCanvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        previewCanvas.setAlwaysOnTop(true);

        // 檢測服務的初始化操作
        detectService.init();
    }
  • 接下來是output方法,定義了拿到每一幀視頻資料后做什么事情,這里呼叫了detectService.convert檢測人臉并識別性別,然后在本地視窗顯示:
    @Override
    protected void output(Frame frame) {
        // 原始幀先交給檢測服務處理,這個處理包括物體檢測,再將檢測結果標注在原始圖片上,
        // 然后轉換為幀回傳
        Frame detectedFrame = detectService.convert(frame);
        // 預覽視窗上顯示的幀是標注了檢測結果的幀
        previewCanvas.showImage(detectedFrame);
    }
  • 最后是處理視頻的回圈結束后,程式退出前要做的事情,先關閉本地視窗,再釋放檢測服務的資源:
    @Override
    protected void releaseOutputResource() {
        if (null!= previewCanvas) {
            previewCanvas.dispose();
        }

        // 檢測工具也要釋放資源
        detectService.releaseOutputResource();
    }
  • 由于檢測有些耗時,所以兩幀之間的間隔時間要低于普通預覽:
    @Override
    protected int getInterval() {
        return super.getInterval()/8;
    }
  • 至此,功能已開發完成,再寫上main方法,代碼如下,請注意人臉檢測所需的模型檔案的路徑來自系統變數:
    public static void main(String[] args) {
        String modelFilePath = System.getProperty("model.file.path");
        log.info("模型檔案本地路徑:{}", modelFilePath);
        new PreviewCameraWithCamShift(new CamShiftDetectService(modelFilePath)).action(1000);
    }
  • 至此,《Java版人臉跟蹤三部曲》的代碼已經全部寫完了,可以像《Java版人臉跟蹤三部曲之一:極速體驗》文中那樣運行起來了

運行程式要注意的地方

  1. 下載opencv在windows環境的元件:https://download.csdn.net/download/boling_cavalry/75121158,我這里下載后放在:C:\study\javacv\lib\opencv_java453.dll
  2. 人臉檢測的模型檔案,在GitHub下載,地址是:https://raw.github.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml,我這里下載后放在:C:\study\javacv\model\haarcascade_frontalface_alt.xml
  3. 運行程式的時候,不論是打包成jar,還是直接在IDEA中運行,都要添加下面這兩個命令引數,才能確保應用加載到dll和模型檔案(請按照您自己的存盤位置修改下面引數的值):
  • -Djava.library.path=C:\study\javacv\lib
  • -Dmodel.file.path=C:\study\javacv\model\haarcascade_frontalface_alt.xml
  • 程式運行起來后,具體的效果與像《Java版人臉跟蹤三部曲之一:極速體驗》中一模一樣,這里就不再贅述了,您自行驗證就好
  • 其實本篇不運行程式,還有一個原因就是要過年了,用來檢測人臉的群眾演員臨時漲價,要兩份盒飯,欣宸實在是負擔不起...

原始碼下載

  • 《JavaCV人臉識別三部曲》的完整原始碼可在GitHub下載到,地址和鏈接資訊如下表所示(https://github.com/zq2599/blog_demos):
名稱 鏈接 備注
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個檔案夾,本篇的原始碼在javacv-tutorials檔案夾下,如下圖紅框所示:
    在這里插入圖片描述
  • javacv-tutorials里面有多個子工程,《JavaCV的攝像頭實戰》系列的代碼在simple-grab-push工程下:
    在這里插入圖片描述
  • 至此,《Java版人臉跟蹤三部曲》完美收官,但是《JavaCV的攝像頭實戰》系列還會繼續呈現更多精彩內容,歡迎關注;

歡迎關注博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/556834.html

標籤:其他

上一篇:P3378 【模板】二叉堆

下一篇:返回列表

標籤雲
其他(162242) Python(38272) JavaScript(25528) Java(18293) C(15239) 區塊鏈(8275) C#(7972) AI(7469) 爪哇(7425) MySQL(7291) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5876) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4614) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2438) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) HtmlCss(1993) .NET技术(1986) 功能(1967) Web開發(1951) C++(1942) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1882) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Java版人臉跟蹤三部曲之三:編碼實戰

    ### 歡迎訪問我的GitHub > 這里分類和匯總了欣宸的全部原創(含配套原始碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 作為《Java版人臉跟蹤三部曲》系列的終 ......

    uj5u.com 2023-07-09 07:46:29 more
  • P3378 【模板】二叉堆

    #[[洛谷]P3378 【模板】堆](https://www.luogu.com.cn/problem/P3378 "[【洛谷】P3378 【模板】堆]") ##方法一 手寫堆 - 最小堆插入 從新增的最后一個結點的父結點開始,用要插入元素向下過濾上層結點(相當于要插入的元素向上滲透) ```c++ ......

    uj5u.com 2023-07-09 07:46:20 more
  • 讓golang程式生成coredump檔案并進行除錯

    今天講講怎么讓golang程式生成coredump檔案,并且進行除錯的。 別看我寫了不少golang的博客,其實我平時寫c++的時間更多,所以也算和coredump是老相識了。`core dump`檔案實際上是行程在某個時間點時的記憶體映像,當時行程使用的記憶體是啥樣就會被原樣保存下來存在檔案系統的某個 ......

    uj5u.com 2023-07-09 07:46:12 more
  • C語言:資料結構之單鏈表(二)

    上一篇隨筆談了談單鏈表是什么東西,然后進行了初始化,這篇隨筆就開始對其進行操作了,首先是增,刪,改,查的增。 增,顧名思義就是要增加新的元素,單鏈表是鏈式的,那就要考慮怎么去加新元素,有三種,從頭部添加,從尾部添加,從中間添加。先說說從尾部添加,這個比較好理解,直接在尾部放一個結點然后連起來就好了。 ......

    uj5u.com 2023-07-09 07:45:59 more
  • python筆記:第四章使用字典

    ## 1.1 概述 > 說白了就是鍵值對的映射關系 > > 不會丟失資料本身關聯的結構,但不關注資料的順序 > > 是一種可變型別 ```py 格式:dic = {鍵:值, 鍵:值} ``` * 鍵的型別:字典的鍵可以是任何不可變的型別,如浮點數,字串,元組 ## 1.2 函式dict 可以從其他 ......

    uj5u.com 2023-07-09 07:45:17 more
  • Python潮流周刊#10:Twitter 的強敵 Threads 是用 Python 開發的!

    你好,我是貓哥。這里每周分享優質的 Python 及通用技術內容,大部分為英文,已在小標題注明。(標題取自其中一則分享,不代表全部內容都是該主題,特此宣告。) 首發于我的博客:[https://pythoncat.top/posts/2023-07-08-weekly](https://python ......

    uj5u.com 2023-07-09 07:45:00 more
  • Python Django 模版全解與實戰

    **本文首先介紹了Django模板系統的基礎知識,接著探討了如何安裝和配置Django模板系統,然后深入決議了Django模板的基本結構、標簽和過濾器的用法,闡述了如何在模板中展示模型資料,最后使用一個實際專案的例子來演示如何在實際開發中使用Django模板系統。** ## Django模板系統的簡 ......

    uj5u.com 2023-07-09 07:44:49 more
  • 從零用python flask框架寫一個簡易的網站

    要用Python寫一個網站,你可以使用Python的Web框架來開發。常見的Python Web框架包括Django、Flask、Bottle等。以下是一個簡單的使用Flask框架開發的示例。 ### 1. 安裝Flask 在開始開發之前,你需要安裝Flask框架。你可以使用以下命令來安裝: ``` ......

    uj5u.com 2023-07-09 07:44:44 more
  • python中zeros函式和ones函式的詳細用法

    在使用Python進行資料分析和科學計算時,經常需要創建和操作多維陣列。NumPy是Python中一個常見的數學庫,它提供了許多方便的函式來創建、操作和處理多維陣列。 NumPy中常用的兩個函式是zeros()函式和ones()函式。這些函式可以幫助我們快速創建特定維度和形狀的多維陣列,并設定初始值 ......

    uj5u.com 2023-07-09 07:44:27 more
  • 【7月最新實作】使用Python獲取全網招聘資料,實作可視化分析

    哈嘍兄弟們,今天來實作采集一下最新的qcwu招聘資料。因為網站嘛,大家都爬來爬去的,人家就會經常更新,所以代碼對應的也要經常重新去寫。 對于會的人來說,當然無所謂,任他更新也攔不住,但是對于不會的小伙伴來說,網站一更新,當場自閉。 所以這期是出給不會的小伙伴的,我還錄制了視頻進行詳細講解,跟原始碼一起 ......

    uj5u.com 2023-07-09 07:44:23 more