2020-05-28

虹软人脸识别 - 采用数据库存取人脸特征数据

前几天有个朋友遇到了个问题,他在使用虹软的人脸识别引擎时,想更换一下人脸识别的存储方式,原本demo中使用的是文件的方式进行存储,而他想要通过数据库的方式进行存储。由于他刚接触Android不久,对数据库这块不甚了解,再加之听上去要存特征数据,听上去就很难的样子,可愁坏了他。其实虹软的人脸特征数据就是一个byte[],存储起来还是相当方便的。我为他写了一个demo,顺便与大家分享一下,希望能帮到有需要的人,本文将分为以下几点讲述。

  • 两种数据库存储方式
  • 封装数据库操作接口
  • ArcFaceDemo接入

一、两种数据库操作方式

方案一:使用原生数据库存储人脸特征数据

1. 建表

首先我们需要建一张表来存储特征数据,但是光存储特征数据还是不够的,不足以满足人脸识别的需要,因此我们还需要人脸姓名、人脸图片等数据。下面我创建了一个User表,以id为主键,并自增长,faceName用TEXT类型存储,把人脸图片与特征都用Blob存储。

注意:对于人脸库中人脸较多的场景,请将注册图存储至本地,数据库中仅保留文件路径,否则会占用很多内存。

public class FaceDatabaseHelper extends SQLiteOpenHelper {

    private static final String TAG = "FaceDatabaseHelper";

    public FaceDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建Face表 ID 名字 照片 特征
        db.execSQL("CREATE TABLE IF NOT EXISTS Face" +
                "(id INTEGER PRIMARY KEY AUTOINCREMENT, faceName TEXT, facePic Blob , faceFeature Blob)");
        //建库成功后给出提示
        Log.i(TAG, "dataBase Create Success");
    }


    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

2. 新增人脸数据

表已经建好了,接下来就是往表中插入数据,需要注意的是,这里的最后一个参数需要传入jpeg格式的数据。

    public long addFace(String faceName, byte[] faceFeature, byte[] facePic) {
        //存人脸
        ContentValues values = new ContentValues();
        long index = -1;
        //写入表
        try {
            values.put("faceName", faceName);
            values.put("faceFeature", faceFeature);
            values.put("facePic", facePic);
            index = db.insert("Face", null, values);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return index;
    }

3. 加载人脸数据

人脸数据已经成功的插入到了表中,接下来我们就需要将表内所有的数据加载到内存。

    public ArrayList<FaceEntity> selectAllFaces() {
        ArrayList<FaceEntity> dataList = new ArrayList<>();
        Cursor cursor = db.query("Face", null, null, null, null, null, null);
        while (cursor.moveToNext()) {
            FaceEntity data = new FaceEntity();
            int id = cursor.getInt(cursor.getColumnIndex("id"));
            String faceName = cursor.getString(cursor.getColumnIndex("faceName"));
            byte[] facePic = cursor.getBlob(cursor.getColumnIndex("facePic"));
            byte[] faceFeature = cursor.getBlob(cursor.getColumnIndex("faceFeature"));
            data.setId(id);
            data.setFaceName(faceName);
            data.setFacePic(facePic);
            data.setFaceFeature(faceFeature);
            dataList.add(data);
        }
        cursor.close();
        return dataList;
    }

4. 清空人脸数据

支持删除所有人脸或单张人脸。

public void deleteAllFace() {
    db.execSQL("delete from Face");
}
public void deleteFaceById(int id) {
    db.delete("Face", "id=?", new String[]{"" + id});
}

方案二:使用Room框架存储人脸特征数据

上面使用的是原生数据库操作接口,这边还提供一种使用Android官方推荐的Room框架操作数据库的示例。

1. 添加依赖

首先我们要在app下的build.gradle文件内添加room的依赖。

implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'

2. 建表

Room的数据库操作相比于原生的方式来说简单的多,只需要对实体类进行注解即可。

注意:对于人脸库中人脸较多的场景,请将注册图存储至本地,数据库中仅保留文件路径,否则会占用很多内存。


// entity声明定义,并且指定了映射数据表明
@Entity(tableName = "Face")
public class FaceEntity {
    // 设置主键,并且定义自增增
    @PrimaryKey(autoGenerate = true)
    public int id;
    // 字段映射具体的数据表字段名
    @ColumnInfo(name = "faceName")
    private String faceName;

    @ColumnInfo(name = "faceFeature")
    private byte[] faceFeature;

    @ColumnInfo(name = "facePic")
    private byte[] facePic;
    
    // getter/setter就不贴了
    ...
}

3. 建库

每次打开数据库文件都会产生比较大的开销,所以将FaceRoomDatabase设计成单例。

//注解指定了database的表映射实体数据以及版本等信息
@Database(entities = {FaceEntity.class}, version = 1)
public abstract class FaceRoomDatabase extends RoomDatabase {
    //RoomDatabase提供直接访问底层数据库实现,我们通过定义抽象方法返回具体Dao
    //然后进行数据库增删该查的实现。
    public abstract FaceDao userDao();

    private static volatile FaceRoomDatabase instance;
    
    public static FaceRoomDatabase getInstance(Context context) {
        if (instance == null) {
            synchronized (FaceRoomDatabase.class) {
                if (instance == null) {
                    instance = Room.databaseBuilder(context.getApplicationContext(), FaceRoomDatabase.class, "RoomFaceDB.db")
                            .build();
                }
            }
        }
        return instance;
    }
}

4. 数据库操作

基于Room框架的数据库操作较为简单,就不一一例举,统一放在一起进行说明。

@Dao
public interface FaceDao {
    //返回Long数据表示,插入条目的主键值(uid)
    @Insert
    Long addFace(FaceEntity user);

    //获取所有人脸数据
    @Query("SELECT * FROM Face")
    List<FaceEntity> getAllFace();

    //删除所有人脸数据
    @Query("DELETE FROM Face")
    int deleteAll();

    //删除指定人脸
    @Delete
    int delete(FaceEntity user);
}

二、封装数据库操作接口

2.1 接口声明

为了代码切换更便捷,能够在代码中快速切换使用Room或直接使用SQLite进行数据库操作:

    // ROOM方式
    private static FaceDatabaseAccessObject dao = new RoomFaceDao();
    // 直接使用SQLite方式
//    private static FaceDatabaseAccessObject dao = new SQLiteFaceDao();

我们可以预先定义一套接口FaceDatabaseAccessObject

public interface FaceDatabaseAccessObject {
    /**
     * 初始化
     */
    void init();

    /**
     * 插入一个人脸
     *
     * @param userEntity 人脸信息
     * @return index
     */
    long insert(FaceEntity userEntity);

    /**
     * 获取所有人脸
     *
     * @return 所有人脸
     */
    List<FaceEntity> getAll();

    /**
     * 清空所有人脸
     */
    void clearAll();

    /**
     * 回收资源操作
     */
    void release();
}

2.2 SQLite实现FaceDatabaseAccessObject接口

public class SQLiteFaceDao implements FaceDatabaseAccessObject {
    private static FaceDatabaseManager faceDataBaseManager;

    @Override
    public void init() {
        faceDataBaseManager = new FaceDatabaseManager(ArcFaceApp.getApplication());
    }

    @Override
    public long insert(FaceEntity userEntity) {
        if (faceDataBaseManager == null) {
            return -1;
        }
        return faceDataBaseManager.addFace(userEntity.getFaceName(), userEntity.getFaceFeature(), userEntity.getFacePic());
    }

    @Override
    public List<FaceEntity> getAll() {
        if (faceDataBaseManager == null) {
            return null;
        }
        return faceDataBaseManager.selectAllFaces();
    }

    @Override
    public void clearAll() {
        if (faceDataBaseManager == null) {
            return;
        }
        faceDataBaseManager.deleteAllFace();
    }

    @Override
    public void release() {
        if (faceDataBaseManager == null) {
            return;
        }
        faceDataBaseManager.release();
    }
}

2.3 ROOM实现FaceDatabaseAccessObject接口

public class RoomFaceDao implements FaceDatabaseAccessObject {
    private static FaceDao dao;
    private FaceRoomDatabase appDatabase;

    @Override
    public void init() {
        appDatabase = FaceRoomDatabase.getInstance(ArcFaceApp.getApplication());
        dao = appDatabase.userDao();
    }

    @Override
    public long insert(FaceEntity userEntity) {
        return dao == null ? -1 : dao.addFace(userEntity);
    }

    @Override
    public List<FaceEntity> getAll() {
        return dao == null ? null : dao.getAllFace();
    }

    @Override
    public void clearAll() {
        if (dao == null) {
            return;
        }
        dao.deleteAll();
    }

    @Override
    public void release() {
        if (appDatabase != null) {
            appDatabase.close();
        }
    }
}

三、ArcFaceDemo接入

数据库操作已经实现,接下来就要实际运用到项目中,首先是人脸注册,我们直接修改Demo中FaceServer类的register方法。

3.1 注册

// 创建一个头像的Bitmap,存放旋转结果图
Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24);
// 录入人脸
ByteArrayOutputStream baos = new ByteArrayOutputStream();
headBmp.compress(Bitmap.CompressFormat.JPEG, 50, baos);
byte[] facePic = baos.toByteArray();

FaceEntity user = new FaceEntity();
user.setFaceName(userName);
user.setFaceFeature(faceFeature.getFeatureData());
user.setFacePic(facePic);
dao.insert(user);

3.2 查询

上面我们已经完成了注册,接下来就是查询,由于Room不推荐在主线程中进行UI操作,我们新创建一个线程,将原本Demo中的initFaceList替换为initFaceListByDataBase

    private void initFaceListByDataBase() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                faceRegisterInfoList = new ArrayList<>();
                List<FaceEntity> userTableList = dao.getAll();
                for (FaceEntity userInfo : userTableList) {
                    faceRegisterInfoList.add(new FaceRegisterInfo(userInfo.getFaceFeature(), userInfo.getFaceName(), userInfo.getFacePic()));
                }
            }
        }).start();
    }

3.3 删除

删除人脸同样属于IO操作,推荐放在子线程中处理。

    public int clearAllFaces(Context context) {
        synchronized (this) {
            if (context == null) {
                return 0;
            }
            int number = 0;
            if (faceRegisterInfoList != null) {
                number = faceRegisterInfoList.size();
                faceRegisterInfoList.clear();
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    dao.clearAll();
                }
            }).start();
            return number;
        }
    }

3.4 显示

由于原本图片的加载模式是加载本地文件,而现在图片是直接存储在数据库的,因此需要对CompareResult进行修改,同时要对FaceSearchResultAdapter的图片加载做一些修改。

public class CompareResult {
    private String userName;
    private float similar;
    private int trackId;
    private byte[] facePic;

    public CompareResult(String userName, float similar) {
        this.userName = userName;
        this.similar = similar;
    }

    public CompareResult(String userName, float similar, byte[] facePic) {
        this.userName = userName;
        this.similar = similar;
        this.facePic = facePic;
    }
}
public CompareResult getTopOfFaceLib(FaceFeature faceFeature) {
   //......
   //.....
    if (maxSimilarIndex != -1) {
        //return new CompareResult(faceRegisterInfoList.get(maxSimilarIndex).getName(), maxSimilar);
        return new CompareResult(faceRegisterInfoList.get(maxSimilarIndex).getName(), maxSimilar, faceRegisterInfoList.get(maxSimilarIndex).getFacePic());
    }
    return null;
}

FaceSearchResultAdapter内修改注册照显示的图像加载。

@Override
public void onBindViewHolder(@NonNull CompareResultHolder holder, int position) {
    if (compareResultList == null) {
        return;
    }
//        File imgFile = new File(FaceServer.ROOT_PATH + File.separator + FaceServer.SAVE_IMG_DIR + File.separator + compareResultList.get(position).getUserName() + FaceServer.IMG_SUFFIX);
//        Glide.with(holder.imageView)
//                .load(imgFile)
//                .into(holder.imageView);
    Glide.with(holder.imageView)
            .load(compareResultList.get(position).getFacePic())
            .into(holder.imageView);
    holder.textView.setText(compareResultList.get(position).getUserName());
}

3.5 效果

至此为止我们已经搭建好数据库,并且完成了识别界面相关的代码修改,下面是实际运行的效果。

image

四、附录

本文通过两种数据库操作方式介绍了人脸特征数据的存取,供大家参考,若有不对的地方,请大家指正!

如果对您有所帮助,可以为我的demo点个star!

提示:本文示例代码在使用前需先修改com.arcsoft.arcfacedemo.common.Constants.java中的APP_ID与SDK_KEY。

源码地址:https://github.com/1244975831/ArcFaceDemoDatabase

相关产品在虹软人脸识别开放平台进一步了解喔

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348