上篇文章,我们已经将人像分割的ncnn-android-yolov8-seg
项目运行起来了,后续文章我们会抽取出Demo
中的核心代码,在自己的项目中,来接入人体识别和人像分割功能。
先来看下效果,整个图像的是相机的原图,左上角部分,是我们进行人像识别、人像分割后,处理得到的图像 (未做镜像处理,所以暂时和原图左右是相反的)
那我们要怎么在自己的项目中,实现人像分割功能呢 ?
我们看ncnn-android-yolov8-seg
的源码,可以发现, 这个项目里的相机也是用c/c++
,但是在我们项目中,使用的Java
层的Camera API
来实现的。要想在自己项目里集成ncnn
,那就需要把ncnn-android-yolov8-seg
里的核心代码给抽离,然后对接到Java
的Camera API
中。
那需要怎么做呢 ? 接下来我们先来看一下它的源码
首先,我们来分析一下 Digital2Slave/ncnn-android-yolov8-seg Demo
中的源码
加载模型是在Java_com_tencent_yolov8ncnn_Yolov8Ncnn_loadModel
方法中,这里附上源码
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov8ncnn_Yolov8Ncnn_loadModel(JNIEnv* env, jobject thiz, jobject assetManager, jint modelid, jint cpugpu)
{
if (modelid < 0 || modelid > 6 || cpugpu < 0 || cpugpu > 1)
{
return JNI_FALSE;
}
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
__android_log_print(ANDROID_LOG_DEBUG, "ncnn", "loadModel %p", mgr);
const char* modeltypes[] =
{
"n",
"s",
};
const int target_sizes[] =
{
320,
320,
};
const float mean_vals[][3] =
{
{103.53f, 116.28f, 123.675f},
{103.53f, 116.28f, 123.675f},
};
const float norm_vals[][3] =
{
{ 1 / 255.f, 1 / 255.f, 1 / 255.f },
{ 1 / 255.f, 1 / 255.f, 1 / 255.f },
};
const char* modeltype = modeltypes[(int)modelid];
int target_size = target_sizes[(int)modelid];
bool use_gpu = (int)cpugpu == 1;
// reload
{
ncnn::MutexLockGuard g(lock);
if (use_gpu && ncnn::get_gpu_count() == 0)
{
// no gpu
delete g_yolo;
g_yolo = 0;
}
else
{
if (!g_yolo)
g_yolo = new Yolo;
g_yolo->load(mgr, modeltype, target_size, mean_vals[(int)modelid], norm_vals[(int)modelid], use_gpu);
}
}
return JNI_TRUE;
}
根据modelid
,来选择modeltypes
中具体的某个模型。
const char* modeltypes[] =
{
"n",
"s",
};
const char* modeltype = modeltypes[(int)modelid];
这里的模型类别是和项目中asserts
文件夹下的模型对应的,yolov8
是个模型簇,从小到大包括:yolov8n
、yolov8s
、yolov8m
、yolov8l
、yolov8x
等。
通常yolov8n
速度最快,具体见下表
接着来看Java_com_tencent_yolov8ncnn_Yolov8Ncnn_loadModel
方法,
还有一个参数cpugpu
是用来决定使用CPU
还是GPU
,0
为CPU
,1
为GPU
。
bool use_gpu = (int)cpugpu == 1;
最后,调用g_yolo->load()
来初始化模型
g_yolo->load(mgr, modeltype, target_size, mean_vals[(int)modelid], norm_vals[(int)modelid], use_gpu);
ncnn-android-yolov8-seg
Demo中的相机操作,都是通过NDK
的Camera API
来完成的
static MyNdkCamera* g_camera = 0;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
__android_log_print(ANDROID_LOG_DEBUG, "ncnn", "JNI_OnLoad");
g_camera = new MyNdkCamera;
return JNI_VERSION_1_4;
}
打开相机就是调用g_camera->open()
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov8ncnn_Yolov8Ncnn_openCamera(JNIEnv* env, jobject thiz, jint facing)
{
if (facing < 0 || facing > 1)
return JNI_FALSE;
__android_log_print(ANDROID_LOG_DEBUG, "ncnn", "openCamera %d", facing);
g_camera->open((int)facing);
return JNI_TRUE;
}
关闭相机是调用g_camera->close()
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov8ncnn_Yolov8Ncnn_closeCamera(JNIEnv* env, jobject thiz)
{
__android_log_print(ANDROID_LOG_DEBUG, "ncnn", "closeCamera");
g_camera->close();
return JNI_TRUE;
}
人体检测是和NDK
的Camera
相关联的,在相机回调的on_image_render
方法中,完成了人体检测
来看一下on_image_render
的源码,主要是通过g_yolo->detect()
进行人体检测,g_yolo->draw()
标注人体位置,用框框出来。
void MyNdkCamera::on_image_render(cv::Mat& rgb) const
{
// nanodet
{
ncnn::MutexLockGuard g(lock);
//cv::resize()
if (g_yolo)
{
__android_log_print(ANDROID_LOG_DEBUG, "myncnn", "g_yolo:true");
auto start = std::chrono::high_resolution_clock::now();
std::vector<Object> objects;
g_yolo->detect(rgb, objects); //人体检测
start = std::chrono::high_resolution_clock::now();
g_yolo->draw(rgb, objects); //标注人体位置,用框框出来
}
else
{
__android_log_print(ANDROID_LOG_DEBUG, "myncnn", "g_yolo:false");
draw_unsupported(rgb);
}
}
draw_fps(rgb); //绘制当前多少帧率
}
那么on_image_render
方法是什么时候被调用的呢 ? 来看ndkcamera.cpp
中的on_image
方法
可以看到,这里会对NV21
图像做裁剪和旋转操作,再转成RGB
格式,然后才传递给on_image_render()
方法处理
// crop and rotate nv21
cv::Mat nv21_croprotated(roi_h + roi_h / 2, roi_w, CV_8UC1);
{
const unsigned char* srcY = nv21 + nv21_roi_y * nv21_width + nv21_roi_x;
unsigned char* dstY = nv21_croprotated.data;
ncnn::kanna_rotate_c1(srcY, nv21_roi_w, nv21_roi_h, nv21_width, dstY, roi_w, roi_h, roi_w, rotate_type);
const unsigned char* srcUV = nv21 + nv21_width * nv21_height + nv21_roi_y * nv21_width / 2 + nv21_roi_x;
unsigned char* dstUV = nv21_croprotated.data + roi_w * roi_h;
ncnn::kanna_rotate_c2(srcUV, nv21_roi_w / 2, nv21_roi_h / 2, nv21_width, dstUV, roi_w / 2, roi_h / 2, roi_w, rotate_type);
}
// nv21_croprotated to rgb
cv::Mat rgb(roi_h, roi_w, CV_8UC3);
ncnn::yuv420sp2rgb(nv21_croprotated.data, roi_w, roi_h, rgb.data);
on_image_render(rgb);
也就是说,从相机中得到的NV21
数据,会先进行旋转,然后转成RGB
格式,再交由g_yolo->detect()
进行人体检测,通过g_yolo->draw()
来标注人体位置。
到这里,我们核心源码就分析的差不多了,那我们怎么将该功能集成到自己的项目中呢 ?
我们在下一篇文章中来实现下 : Android 在自己的项目接入OpenCV+YOLOv8+NCNN,实现人像分割-CSDN博客。
OpenCV相关
NCNN+YOLO8相关