智能证件照制作——基于人脸检测与自动人像分割轻松制作个人证件照(C++实现)
前言
1.关于证件照,有好多种制作办法,最常见的是使用PS来做图像处理,或者下载各种证件照相关的APP,一键制作,基本的步骤是先按人脸为基准切出适合的尺寸,然后把人像给抠出来,对人像进行美化处理,然后替换上要使用的背景色,比如蓝色或红色。
2.我这里也按着上面的步骤来用代码实现,先是人脸检测,剪切照片,替换背景色,美化和修脸暂时还没有时间写完。
3.因为是考虑到要移植到移动端(安卓和iOS),这里使用了ncnn做推理加速库,之前做过一些APP,加速库都选了ncnn,不管在安卓或者iOS上,性能都是不错的。
4.我的开发环境是win10, vs2019, opencv4.5, ncnn,如果要启用GPU加速,所以用到VulkanSDK,实现语言是C++。
5.先上效果图,对于背景纯度的要求不高,如果使用场景背景复杂的话,也可以完美抠图。
原始图像:
原图:
自动剪切出来的证件照:
原图:
自动剪切出来的证件照:
一.项目创建
1.使用vs2019新建一个C++项目,把OpenC和NCNN库导入,NCNN可以下载官方编译好的库,我也会在后面上传我使用的库和源码以及用到的模型。
2.如果要启用GPU推理,就要安装VulkanSDK,安装的步骤可以参考我之前的博客。
二.人脸检测
1.人脸检测这里面使用 SCRFD ,它带眼睛,鼻子,嘴角五个关键点的坐标,这个可以用做证件照参考点,人脸检测库这个也可以用libfacedetection,效果都差不多,如果是移动端最好选择SCRFD。
代码实现:
推理代码
#include "scrfd.h"#include <string.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <ncnn/cpu.h> //安卓才用到static inline float intersection_area(const FaceObject& a, const FaceObject& b)
{cv::Rect_<float> inter = a.rect & b.rect;return inter.area();
}static void qsort_descent_inplace(std::vector<FaceObject>& faceobjects, int left, int right)
{int i = left;int j = right;float p = faceobjects[(left + right) / 2].prob;while (i <= j){while (faceobjects[i].prob > p)i++;while (faceobjects[j].prob < p)j--;if (i <= j){// swapstd::swap(faceobjects[i], faceobjects[j]);i++;j--;}}// #pragma omp parallel sections{// #pragma omp section{if (left < j) qsort_descent_inplace(faceobjects, left, j);}
// #pragma omp section{if (i < right) qsort_descent_inplace(faceobjects, i, right);}}
}static void qsort_descent_inplace(std::vector<FaceObject>& faceobjects)
{if (faceobjects.empty())return;qsort_descent_inplace(faceobjects, 0, faceobjects.size() - 1);
}static void nms_sorted_bboxes(const std::vector<FaceObject>& faceobjects, std::vector<int>& picked, float nms_threshold)
{picked.clear();const int n = faceobjects.size();std::vector<float> areas(n);for (int i = 0; i < n; i++){areas[i] = faceobjects[i].rect.area();}for (int i = 0; i < n; i++){const FaceObject& a = faceobjects[i];int keep = 1;for (int j = 0; j < (int)picked.size(); j++){const FaceObject& b = faceobjects[picked[j]];// intersection over unionfloat inter_area = intersection_area(a, b);float union_area = areas[i] + areas[picked[j]] - inter_area;// float IoU = inter_area / union_areaif (inter_area / union_area > nms_threshold)keep = 0;}if (keep)picked.push_back(i);}
}static ncnn::Mat generate_anchors(int base_size, const ncnn::Mat& ratios, const ncnn::Mat& scales)
{int num_ratio = ratios.w;int num_scale = scales.w;ncnn::Mat anchors;anchors.create(4, num_ratio * num_scale);const float cx = 0;const float cy = 0;for (int i = 0; i < num_ratio; i++){float ar = ratios[i];int r_w = round(base_size / sqrt(ar));int r_h = round(r_w * ar); //round(base_size * sqrt(ar));for (int j = 0; j < num_scale; j++){float scale = scales[j];float rs_w = r_w * scale;float rs_h = r_h * scale;float* anchor = anchors.row(i * num_scale + j);anchor[0] = cx - rs_w * 0.5f;anchor[1] = cy - rs_h * 0.5f;anchor[2] = cx + rs_w * 0.5f;anchor[3] = cy + rs_h * 0.5f;}}return anchors;
}static void generate_proposals(const ncnn::Mat& anchors, int feat_stride, const ncnn::Mat& score_blob, const ncnn::Mat& bbox_blob, const ncnn::Mat& kps_blob, float prob_threshold, std::vector<FaceObject>& faceobjects)
{int w = score_blob.w;int h = score_blob.h;// generate face proposal from bbox deltas and shifted anchorsconst int num_anchors = anchors.h;for (int q = 0; q < num_anchors; q++){const float* anchor = anchors.row(q);const ncnn::Mat score = score_blob.channel(q);const ncnn::Mat bbox = bbox_blob.channel_range(q * 4, 4);// shifted anchorfloat anchor_y = anchor[1];float anchor_w = anchor[2] - anchor[0];float anchor_h = anchor[3] - anchor[1];for (int i = 0; i < h; i++){float anchor_x = anchor[0];for (int j = 0; j < w; j++){int index = i * w + j;float prob = score[index];if (prob >= prob_threshold){// insightface/detection/scrfd/mmdet/models/dense_heads/scrfd_head.py _get_bboxes_single()float dx = bbox.channel(0)[index] * feat_stride;float dy = bbox.channel(1)[index] * feat_stride;float dw = bbox.channel(2)[index] * feat_stride;float dh = bbox.channel(3)[index] * feat_stride;// insightface/detection/scrfd/mmdet/core/bbox/transforms.py distance2bbox()float cx = anchor_x + anchor_w * 0.5f;float cy = anchor_y + anchor_h * 0.5f;float x0 = cx - dx;float y0 = cy - dy;float x1 = cx + dw;float y1 = cy + dh;FaceObject obj;obj.rect.x = x0;obj.rect.y = y0;obj.rect.width = x1 - x0 + 1;obj.rect.height = y1 - y0 + 1;obj.prob = prob;if (!kps_blob.empty()){const ncnn::Mat kps = kps_blob.channel_range(q * 10, 10);obj.landmark[0].x = cx + kps.channel(0)[index] * feat_stride;obj.landmark[0].y = cy + kps.channel(1)[index] * feat_stride;obj.landmark[1].x = cx + kps.channel(2)[index] * feat_stride;obj.landmark[1].y = cy + kps.channel(3)[index] * feat_stride;obj.landmark[2].x = cx + kps.channel(4)[index] * feat_stride;obj.landmark[2].y = cy + kps.channel(5)[index] * feat_stride;obj.landmark[3].x = cx + kps.channel(6)[index] * feat_stride;obj.landmark[3].y = cy + kps.channel(7)[index] * feat_stride;obj.landmark[4].x = cx + kps.channel(8)[index] * feat_stride;obj.landmark[4].y = cy + kps.channel(9)[index] * feat_stride;}faceobjects.push_back(obj);}anchor_x += feat_stride;}anchor_y += feat_stride;}}
}SCRFD::SCRFD()
{}int SCRFD::detect(const cv::Mat& rgb, std::vector<FaceObject>& faceobjects, float prob_threshold, float nms_threshold)
{int width = rgb.cols;int height = rgb.rows;// insightface/detection/scrfd/configs/scrfd/scrfd_500m.pyconst int target_size = 640;// pad to multiple of 32int w = width;int h = height;float scale = 1.f;if (w > h){scale = (float)target_size / w;w = target_size;h = h * scale;}else{scale = (float)target_size / h;h = target_size;w = w * scale;}ncnn::Mat in = ncnn::Mat::from_pixels_resize(rgb.data, ncnn::Mat::PIXEL_RGB, width, height, w, h);// pad to target_size rectangleint wpad = (w + 31) / 32 * 32 - w;int hpad = (h + 31) / 32 * 32 - h;ncnn::Mat in_pad;ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2, ncnn::BORDER_CONSTANT, 0.f);const float mean_vals[3] = {127.5f, 127.5f, 127.5f};const float norm_vals[3] = {1/128.f, 1/128.f, 1/128.f};in_pad.substract_mean_normalize(mean_vals, norm_vals);ncnn::Extractor ex = scrfd_net.create_extractor();ex.input("input.1", in_pad);std::vector<FaceObject> faceproposals;// stride 8{ncnn::Mat score_blob, bbox_blob, kps_blob;ex.extract("score_8", score_blob);ex.extract("bbox_8", bbox_blob);if (has_kps)ex.extract("kps_8", kps_blob);const int base_size = 16;const int feat_stride = 8;ncnn::Mat ratios(1);ratios[0] = 1.f;ncnn::Mat scales(2);scales[0] = 1.f;scales[1] = 2.f;ncnn::Mat anchors = generate_anchors(base_size, ratios, scales);std::vector<FaceObject> faceobjects32;generate_proposals(anchors, feat_stride, score_blob, bbox_blob, kps_blob, prob_threshold, faceobjects32);faceproposals.insert(faceproposals.end(), faceobjects32.begin(), faceobjects32.end());}// stride 16{ncnn::Mat score_blob, bbox_blob, kps_blob;ex.extract("score_16", score_blob);ex.extract("bbox_16", bbox_blob);if (has_kps)ex.extract("kps_16", kps_blob);const int base_size = 64;const int feat_stride = 16;ncnn::Mat ratios(1);ratios[0] = 1.f;ncnn::Mat scales(2);scales[0] = 1.f;scales[1] = 2.f;ncnn::Mat anchors = generate_anchors(base_size, ratios, scales);std::vector<FaceObject> faceobjects16;generate_proposals(anchors, feat_stride, score_blob, bbox_blob, kps_blob, prob_threshold, faceobjects16);faceproposals.insert(faceproposals.end(), faceobjects16.begin(), faceobjects16.end());}// stride 32{ncnn::Mat score_blob, bbox_blob, kps_blob;ex.extract("score_32", score_blob);ex.extract("bbox_32", bbox_blob);if (has_kps)ex.extract("kps_32", kps_blob);const int base_size = 256;const int feat_stride = 32;ncnn::Mat ratios(1);ratios[0] = 1.f;ncnn::Mat scales(2);scales[0] = 1.f;scales[1] = 2.f;ncnn::Mat anchors = generate_anchors(base_size, ratios, scales);std::vector<FaceObject> faceobjects8;generate_proposals(anchors, feat_stride, score_blob, bbox_blob, kps_blob, prob_threshold, faceobjects8);faceproposals.insert(faceproposals.end(), faceobjects8.begin(), faceobjects8.end());}// sort all proposals by score from highest to lowestqsort_descent_inplace(faceproposals);// apply nms with nms_thresholdstd::vector<int> picked;nms_sorted_bboxes(faceproposals, picked, nms_threshold);int face_count = picked.size();faceobjects.resize(face_count);for (int i = 0; i < face_count; i++){faceobjects[i] = faceproposals[picked[i]];// adjust offset to original unpaddedfloat x0 = (faceobjects[i].rect.x - (wpad / 2)) / scale;float y0 = (faceobjects[i].rect.y - (hpad / 2)) / scale;float x1 = (faceobjects[i].rect.x + faceobjects[i].rect.width - (wpad / 2)) / scale;float y1 = (faceobjects[i].rect.y + faceobjects[i].rect.height - (hpad / 2)) / scale;x0 = std::max(std::min(x0, (float)width - 1), 0.f);y0 = std::max(std::min(y0, (float)height - 1), 0.f);x1 = std::max(std::min(x1, (float)width - 1), 0.f);y1 = std::max(std::min(y1, (float)height - 1), 0.f);faceobjects[i].rect.x = x0;faceobjects[i].rect.y = y0;faceobjects[i].rect.width = x1 - x0;faceobjects[i].rect.height = y1 - y0;if (has_kps){float x0 = (faceobjects[i].landmark[0].x - (wpad / 2)) / scale;float y0 = (faceobjects[i].landmark[0].y - (hpad / 2)) / scale;float x1 = (faceobjects[i].landmark[1].x - (wpad / 2)) / scale;float y1 = (faceobjects[i].landmark[1].y - (hpad / 2)) / scale;float x2 = (faceobjects[i].landmark[2].x - (wpad / 2)) / scale;float y2 = (faceobjects[i].landmark[2].y - (hpad / 2)) / scale;float x3 = (faceobjects[i].landmark[3].x - (wpad / 2)) / scale;float y3 = (faceobjects[i].landmark[3].y - (hpad / 2)) / scale;float x4 = (faceobjects[i].landmark[4].x - (wpad / 2)) / scale;float y4 = (faceobjects[i].landmark[4].y - (hpad / 2)) / scale;faceobjects[i].landmark[0].x = std::max(std::min(x0, (float)width - 1), 0.f);faceobjects[i].landmark[0].y = std::max(std::min(y0, (float)height - 1), 0.f);faceobjects[i].landmark[1].x = std::max(std::min(x1, (float)width - 1), 0.f);faceobjects[i].landmark[1].y = std::max(std::min(y1, (float)height - 1), 0.f);faceobjects[i].landmark[2].x = std::max(std::min(x2, (float)width - 1), 0.f);faceobjects[i].landmark[2].y = std::max(std::min(y2, (float)height - 1), 0.f);faceobjects[i].landmark[3].x = std::max(std::min(x3, (float)width - 1), 0.f);faceobjects[i].landmark[3].y = std::max(std::min(y3, (float)height - 1), 0.f);faceobjects[i].landmark[4].x = std::max(std::min(x4, (float)width - 1), 0.f);faceobjects[i].landmark[4].y = std::max(std::min(y4, (float)height - 1), 0.f);}}return 0;
}int SCRFD::readModels(std::string param_path, std::string model_path, bool use_gpu)
{bool has_gpu = false;#if NCNN_VULKANncnn::create_gpu_instance();has_gpu = ncnn::get_gpu_count() > 0;
#endifbool to_use_gpu = has_gpu && use_gpu;scrfd_net.opt.use_vulkan_compute = to_use_gpu;int rp = scrfd_net.load_param(param_path.c_str());int rb = scrfd_net.load_model(model_path.c_str());if (rp < 0 || rb < 0){return 1;}return 0;
}
2.把检测的结果画出来。
int SCRFD::draw(cv::Mat& rgb, const std::vector<FaceObject>& faceobjects)
{for (size_t i = 0; i < faceobjects.size(); i++){const FaceObject& obj = faceobjects[i];cv::rectangle(rgb, obj.rect, cv::Scalar(0, 255, 0));if (has_kps){cv::circle(rgb, obj.landmark[0], 2, cv::Scalar(0, 255, 255), -1);cv::circle(rgb, obj.landmark[1], 2, cv::Scalar(0, 0, 255), -1);cv::circle(rgb, obj.landmark[2], 2, cv::Scalar(255, 255, 0), -1);cv::circle(rgb, obj.landmark[3], 2, cv::Scalar(255, 255, 0), -1);cv::circle(rgb, obj.landmark[4], 2, cv::Scalar(255, 255, 0), -1);}char text[256];sprintf(text, "%.1f%%", obj.prob * 100);int baseLine = 0;cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);int x = obj.rect.x;int y = obj.rect.y - label_size.height - baseLine;if (y < 0)y = 0;if (x + label_size.width > rgb.cols)x = rgb.cols - label_size.width;cv::rectangle(rgb, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)), cv::Scalar(255, 255, 255), -1);cv::putText(rgb, text, cv::Point(x, y + label_size.height), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);}return 0;
}
3.检测效果
三.证件照剪切
1.筛选人脸,如果有一张图像有多张人脸的话,取最大最正的脸的坐标来做基准点。
代码:
int faceFind(const cv::Mat& cv_src, std::vector<FaceObject> &face_object, cv::Rect& cv_rect, std::vector<cv::Point> &five_point)
{//只检测到一张脸if (face_object.size() == 1){if (face_object[0].prob > 0.7){for (int i = 0; i < 5; ++i){five_point.push_back(face_object[0].landmark[i]);}cv_rect = face_object[0].rect;return 0;}}//检测到多张脸else if (face_object.size() >= 2){cv::Rect max_rect;for (int i = 0; i < face_object.size(); ++i){if (face_object[i].prob >= 0.7){cv::Rect rect = face_object[i].rect;if (max_rect.area() <= rect.area()){max_rect = rect;}}}for (int i = 0; i < face_object.size(); ++i){if (face_object[i].prob >= 0.7){cv::Rect rect = face_object[i].rect;if (max_rect.area() == rect.area()){for (int j = 0; j < 5; ++j){five_point.push_back(face_object[0].landmark[j]);}cv_rect = rect;}}}return 0;}return 1;
}
效果:
2.上面取基准的方法只是一个比较简单的方法,如果算力够的话,或者需要精度更高的话,这里可以加入更多关键点和头部姿态估计和判断。然后用头部姿态估计来判断图像或者摄像头头里的人脸是否摆正了。
3.以人脸为基准剪切出证件照的尺寸图像,先把脸基准中心,计算上下左右的尺寸,然后按比例剪切出合适的证件照的尺寸。
代码:
int faceLocation(const cv::Mat cv_src,cv::Mat& cv_dst, std::vector<cv::Point>& five_point, cv::Rect &cv_rect)
{float w_block = cv_rect.width / 5.5;float h_block = cv_rect.height / 8;//头部cv::Rect face_rect;face_rect.x = cv_rect.x - (w_block * 0.8);//加上双耳的大小face_rect.y = cv_rect.y - (h_block * 2);face_rect.width = cv_rect.width + (w_block * 1.6);face_rect.height = cv_rect.height + (h_block * 2);//人脸离左边边框的距离int tl_face_w = face_rect.tl().x;int tr_face_w = cv_src.cols - (face_rect.width + face_rect.tl().x);int t_face_h = face_rect.tl().y;int b_face_h = cv_src.rows - face_rect.br().y;//算出头像的位置int w_scale = face_rect.width / 7;int h_scale = face_rect.height / 10;cv::Rect id_rect;//判断位置if (tl_face_w >= (w_scale * 2) && tr_face_w >= (w_scale * 2) && t_face_h >= (h_scale * 0.5) && b_face_h > (h_scale * 5)){//判断眼睛的位置std::cout << five_point.size() << std::endl;if (abs(five_point.at(0).y - five_point.at(1).y) < 8){id_rect.x = ((face_rect.x - w_scale * 3) <= 0) ? 0 : (face_rect.x - w_scale * 3);id_rect.y = ((face_rect.y - h_scale * 3) < 0) ? 0 : (face_rect.y - h_scale * 3);id_rect.width = (w_scale * 13) + id_rect.x > cv_src.size().width ? cv_src.size().width - id_rect.x : w_scale * 13;id_rect.height = (h_scale * 19) + id_rect.y > cv_src.size().height ? cv_src.size().height - id_rect.y : h_scale * 19;cv_dst = cv_src(id_rect);return 0;}}return -1;
}
效果:
四.抠图与背景替换
1.经过上面的步骤,已经得到一个证件照的图像,现在要把头像抠出来就可以做背景替换了。
int matting(cv::Mat &cv_src, ncnn::Net& net, ncnn::Mat &alpha)
{int width = cv_src.cols;int height = cv_src.rows;ncnn::Mat in_resize = ncnn::Mat::from_pixels_resize(cv_src.data, ncnn::Mat::PIXEL_RGB, width, height, 256,256);const float meanVals[3] = { 127.5f, 127.5f, 127.5f };const float normVals[3] = { 0.0078431f, 0.0078431f, 0.0078431f };in_resize.substract_mean_normalize(meanVals, normVals);ncnn::Mat out;ncnn::Extractor ex = net.create_extractor();ex.set_vulkan_compute(true);ex.input("input", in_resize);ex.extract("output", out);ncnn::resize_bilinear(out, alpha, width, height);return 0;
}
2.替换背景色。
void replaceBG(const cv::Mat cv_src, ncnn::Mat &alpha,cv::Mat &cv_matting, std::vector<int> &bg_color)
{int width = cv_src.cols;int height = cv_src.rows;cv_matting = cv::Mat::zeros(cv::Size(width, height), CV_8UC3);float* alpha_data = (float*)alpha.data;for (int i = 0; i < height; i++){for (int j = 0; j < width; j++){float alpha_ = alpha_data[i * width + j];cv_matting.at < cv::Vec3b>(i, j)[0] = cv_src.at < cv::Vec3b>(i, j)[0] * alpha_ + (1 - alpha_) * bg_color[0];cv_matting.at < cv::Vec3b>(i, j)[1] = cv_src.at < cv::Vec3b>(i, j)[1] * alpha_ + (1 - alpha_) * bg_color[1];cv_matting.at < cv::Vec3b>(i, j)[2] = cv_src.at < cv::Vec3b>(i, j)[2] * alpha_ + (1 - alpha_) * bg_color[2];}}
}
3.效果图。
原图:
证件照:
原图(背景比较复杂的原图):
证件照:
动漫头像:
五.结语
1.这只是个可以实现功能的demo,如果想要应用到商业上,还有很多细节上的处理,比如果头部姿态估计,眼球检测(是否闭眼),皮肤美化,瘦脸,换装等,这些功能有时间我会去试之后放上来。
2.这个demo改改可以在安卓上运行,demo我在安卓上测试过,速度和精度都有不错的表现。
3.整个工程和源码的地址:https://download.csdn.net/download/matt45m/67756246
智能证件照制作——基于人脸检测与自动人像分割轻松制作个人证件照(C++实现)相关推荐
- 基于单片机的水壶自动加热系统_基于烟雾检测火灾自动报警系统
著作权归作者所有. 商业转载请联系作者获得授权,非商业转载清注明出处. 作者:胡皓 王兴 链接:基于烟雾检测火灾自动报警系统 - 中国知网 来源:中国知网 摘要:讨论了用MC14468离子型烟雾检测报 ...
- android人脸特征提取,基于人脸检测和特征提取的移动人像采集系统
摘要: 目前公安部门使用的人脸识别系统大多属于台式设备和专业器材,而且是在成像条件相对较好,取得被拍照人员良好配合的情况下进行人像采集,软件算法针对的是约束条件下采集得到的人像照片.但是,公安警务还涉 ...
- 基于图像分割网络HRNet实现人像分割
基于图像分割网络HRNet实现人像分割 人像分割是图像分割领域非常常见的应用,PaddleSeg推出了在大规模人像数据上训练的人像分割PPSeg模型,满足在服务端.移动端.Web端多种使用场景的需求. ...
- 智能识别系统----视频人脸检测(一)
文章目录 项目目录 提取人脸 特征提取 PCA LDA LBPH+直方图特征 训练分类器 SVC 可视化 利用分类器进行视频人像分类 有空的时候把项目部署到github上 项目目录 提取人脸 首先编写 ...
- java 人脸检测_Java+OpenCV实现人脸检测并自动拍照
java+opencv实现人脸检测,调用笔记本摄像头实时抓拍,人脸会用红色边框标识出来,并且将抓拍的目录存放在src下,图片名称是时间戳. 环境配置:win7 64位,jdk1.8 CameraBas ...
- 【CV】基于UNet网络实现的人像分割 | 附数据集
文章来源于AI算法与图像处理,作者AI_study 今天要分享的是人像分割相关的内容,如果你喜欢的话,欢迎三连哦 主要内容 人像分割简介 UNet的简介 UNet实现人像分割 人像分割简介 人像分割的 ...
- unet训练自己的数据集_基于UNet网络实现的人像分割 | 附数据集
点击上方↑↑↑"OpenCV学堂"关注我 来源:公众号 AI算法与图像处理 授权 以后我会在公众号分享一些关于算法的应用(美颜相关的),工作之后,发现更重要的能力如何理解业务并将算 ...
- python人像精细分割_基于UNet网络实现的人像分割 | 附数据集
点击上方"AI算法与图像处理",选择加"星标"或"置顶" 重磅干货,第一时间送达 以后我会在公众号分享一些关于算法的应用(美颜相关的),工作 ...
- 基于UNet网络实现的人像分割 | 附数据集
点击上方"AI算法与图像处理",选择加"星标"或"置顶" 重磅干货,第一时间送达 以后我会在公众号分享一些关于算法的应用(美颜相关的),工作 ...
- 电子琴节奏包制作_XR情报局:如何在网页端轻松制作Beat Saber关卡?
小青|编辑 大家好,"XR情报局"第六期又和大家见面啦!今天将向大家分享:如何快速简单地制作<Beat Saber>关卡. 我要分享的方法对于不熟悉游戏mod制作的小白 ...
最新文章
- Science: 四万张大脑图像首次揭示人脑白质的基因基础
- Windows Server 2003 服务应用大全之DNS服务使用详解
- C#中文件和byte[]互换问题
- GDCM:gdcm::Unpacker12Bits的测试程序
- 干货福利:AI人工智能学习资料教程包.zip
- linux命令行安装libxml,Ubuntu 14.04下libxml2的安装和使用
- 台式计算机优点英语作文,跪求一篇英语作文 题目:论计算机的优缺点
- 从零开始学 Web 之 Ajax(五)同步异步请求,数据格式
- 对于web项目前台和后台bug定位分析
- FileUpload1.PostedFile.FileName取不到完整路径
- 一个IT技术人如果转型做自由职业可以做哪些方向?
- java protected用法_深入理解Java的protected修饰符
- html5 sencha,HTML5开发实战——Sencha Touch篇(1)
- POJ 1753 Flip Game(递归枚举)
- H5 Canvas实现荣誉证书生成器
- Secret Layer Ligh(数据加密成图片)v2.7.2绿色版
- pandas添加一行数据的方法
- 【计算机网络笔记1】计算机网络和因特网
- Linux 网络唤醒
- Centos 安装 KVM虚拟化工具 超云服务器 VMware
热门文章
- python去掉左边的空格_Python去除字符串左边空格
- 中医基础理论第二章藏象(心)
- 程序员的自我修养之数学基础11:期望、方差、常见分布(均匀分布、二项分布、泊松分布、正态分布)
- 百旺如何看是否清卡_百旺税控怎么看反写成功
- 语音转文字的测试用例
- 2022-01-08:数组中只有0和1,每过1代,0旁边只有1个1,当前0会变成1。每过1代,0旁边有2个1,当前0还是0。 比如10001,经过1代,会变成11011,再过1代,还是11011 。
- 适合程序员的英文名字
- idea繁体字-中文输入法变繁体字
- oracle自增序列及其触发器
- 阿里ACP云计算认证快速通关分享