[关闭]
@w460461339 2017-06-09T19:30:25.000000Z 字数 13953 阅读 2311

研究生毕业设计8:Android Studio+Opencv+Dlib 人脸检测以及五官识别

研究生毕业设计 Android


0. 写在前面

按照 《研究生毕业设计7》中的方法进行本次项目的build时,总是过不了。应该是cmake文件写的有问题,接下来还是需要好好学些cmake文件怎么写才行呀。

1. 项目结构预览

structure.png-35.9kB
这里可以看到,项目中并没有jni文件夹,这也是这个方法和之前方法最不同的一点。

2. 导入opencv

方法和前面一样,这里再说一遍。

opencv用的版本是3.1.0
1、 File -> new -> import module 选择openc目录下sdk下的java文件夹

2、 导入后build会有错误,将左侧android视图切换到project视图,把openCVLibrary310文件夹下的build.gradle打开,修改里面的android平台以及buildtools等参数,使其符合自己的设置。直到不报错

3、 File -> project structure -> app -> dependencies 点击绿色加好,选择3 module 之类的,将openCVLibrary310导入加入到dependencies即可。

至此,opencv添加完成。(注意,这里不需要再导入cpu架构包了)

3. MainActivity

功能:

1. 通过摄像头拍照获取图像数据。
2. 将图像数据保存,并切换到处理Activity。
3.1 布局
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context="com.wzf.face.landmarks2.MainActivity">
  8. <Button
  9. android:layout_below="@+id/javaCVLMD"
  10. android:onClick="click1"
  11. android:layout_width="60dp"
  12. android:layout_height="40dp"
  13. android:layout_alignParentTop="true"
  14. android:layout_alignParentLeft="true"
  15. android:layout_alignParentStart="true"
  16. android:text="按钮1"
  17. />
  18. <org.opencv.android.JavaCameraView
  19. android:layout_width="match_parent"
  20. android:layout_height="wrap_content"
  21. android:id="@+id/javaCVLMD"/>
  22. </RelativeLayout>
3.2 MainActivity.java
  1. public class MainActivity extends AppCompatActivity implements CameraBridgeViewBase.CvCameraViewListener2 {
  2. public static final String TAG="LMD";
  3. JavaCameraView javaCameraView;
  4. Mat mRGBA;
  5. /*
  6. 接受onResume里面的回调信息,
  7. 如果是success,表示opencv加载成功,那么使能显示控件。
  8. 否则执行默认操作
  9. */
  10. BaseLoaderCallback baseLoaderCallback=new BaseLoaderCallback(this) {
  11. @Override
  12. public void onManagerConnected(int status) {
  13. switch (status){
  14. case BaseLoaderCallback.SUCCESS:{
  15. javaCameraView.enableView();
  16. break;
  17. }
  18. default:{
  19. super.onManagerConnected(status);
  20. break;
  21. }
  22. }
  23. }
  24. };
  25. static{
  26. System.loadLibrary("MyLibs");
  27. }
  28. /**
  29. 获取控件
  30. **/
  31. @Override
  32. protected void onCreate(Bundle savedInstanceState) {
  33. super.onCreate(savedInstanceState);
  34. setContentView(R.layout.activity_main);
  35. javaCameraView=(JavaCameraView)findViewById(R.id.javaCVLMD);
  36. javaCameraView.setVisibility(View.VISIBLE);
  37. javaCameraView.setCvCameraViewListener(this);
  38. }
  39. /**
  40. 三个activity的生命周期方法,
  41. 用来判断什么时候应该开启opencv的相机,
  42. 什么时候应该将相机显示控件释放。
  43. **/
  44. @Override
  45. protected void onPause() {
  46. super.onPause();
  47. if(javaCameraView!=null){
  48. javaCameraView.disableView();
  49. }
  50. }
  51. @Override
  52. protected void onDestroy() {
  53. super.onDestroy();
  54. if(javaCameraView!=null){
  55. javaCameraView.disableView();
  56. }
  57. }
  58. @Override
  59. protected void onResume() {
  60. super.onResume();
  61. if(OpenCVLoader.initDebug()){
  62. Log.i(TAG,"成功");
  63. baseLoaderCallback.onManagerConnected(BaseLoaderCallback.SUCCESS);
  64. }else{
  65. Log.i(TAG,"失败");
  66. OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0,this,baseLoaderCallback);
  67. }
  68. }
  69. /**
  70. opencv相机监听接口的三个方法的实现,负责将相机帧数据进行初始化,返回到控件以及释放
  71. **/
  72. @Override
  73. public void onCameraViewStarted(int width, int height) {
  74. mRGBA=new Mat(height,width, CvType.CV_8UC4);
  75. }
  76. @Override
  77. public void onCameraViewStopped() {
  78. mRGBA.release();
  79. }
  80. @Override
  81. public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
  82. mRGBA=inputFrame.rgba();
  83. return mRGBA;
  84. }
  85. /**
  86. 按钮点击事件,点击后,先保存数据,再跳转到另一个activity
  87. **/
  88. public void click1(View view){
  89. saveImage(mRGBA);
  90. Intent intent=new Intent(this,DetailActivity.class);
  91. startActivity(intent);
  92. }
  93. /**
  94. 保存图片的方法
  95. **/
  96. public void saveImage(Mat subImg){
  97. Bitmap bmp=null;
  98. try{
  99. bmp= Bitmap.createBitmap(subImg.cols(),subImg.rows(),Bitmap.Config.ARGB_8888);
  100. Utils.matToBitmap(subImg,bmp);
  101. }catch (Exception e){
  102. Log.d(TAG,e.getMessage());
  103. }
  104. subImg.release();
  105. FileOutputStream out=null;
  106. String fileName="frame.png";
  107. File sd=new File(Environment.getExternalStorageDirectory()+"/frames");
  108. boolean success=true;
  109. if(!sd.exists()){
  110. success=sd.mkdir();
  111. }
  112. if(success){
  113. File dest=new File(sd,fileName);
  114. try{
  115. out=new FileOutputStream(dest);
  116. bmp.compress(Bitmap.CompressFormat.PNG,100,out);
  117. }catch (Exception e){
  118. Log.d(TAG,e.getMessage());
  119. }finally {
  120. try{
  121. if(out!=null){
  122. out.close();
  123. Log.d(TAG,"成功赋值");
  124. }
  125. }catch (Exception e){
  126. Log.d(TAG,e.getMessage());
  127. }
  128. }
  129. }
  130. }
  131. }

4. DetailActivity

4.0 在清单文件中注册

创建完activity后,不要忘了在清单文件中进行注册。

4.1 布局文件
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical" android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <ImageView
  6. android:layout_width="match_parent"
  7. android:layout_height="wrap_content"
  8. android:id="@+id/iv_detail"/>
  9. <Button
  10. android:text="来吧!"
  11. android:layout_width="wrap_content"
  12. android:layout_height="wrap_content"
  13. android:id="@+id/btn_detail" />
  14. </LinearLayout>
4.2 DetailActivity
  1. public class DetailActivity extends AppCompatActivity {
  2. ImageView imageView;
  3. Button btnProceess;
  4. Bitmap bitmapInput,bitmapOutput;
  5. Mat matInput,matOutput;
  6. // 加载本地方法库
  7. static{
  8. System.loadLibrary("MyLibs");
  9. }
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.activity_detail);
  14. // 获得控件
  15. imageView=(ImageView)findViewById(R.id.iv_detail);
  16. btnProceess=(Button)findViewById(R.id.btn_detail);
  17. // 获得MainActivity中保存的图片的路径
  18. String photoPath= Environment.getExternalStorageDirectory()+"/frames/frame.png";
  19. // 设置Bitmap的显示信息
  20. BitmapFactory.Options options=new BitmapFactory.Options();
  21. options.inPreferredConfig=Bitmap.Config.ARGB_8888;
  22. bitmapInput=BitmapFactory.decodeFile(photoPath,options);
  23. // 展示原图
  24. imageView.setImageBitmap(bitmapInput);
  25. // 将Bitmap转化为Mat图片,并创建等大小的新的Bitmap
  26. matInput=convertBitmap2Mat(bitmapInput);
  27. matOutput=new Mat(matInput.rows(),matInput.cols(), CvType.CV_8UC3);
  28. // 按钮事件监听器
  29. btnProceess.setOnClickListener(new View.OnClickListener() {
  30. @Override
  31. public void onClick(View v) {
  32. boolean boo= OpenCVLoader.initDebug();
  33. if(boo){
  34. Toast.makeText(DetailActivity.this,"Calling native function",Toast.LENGTH_SHORT).show();
  35. // 调用本地方法,进行landmarks检测
  36. NativeClass.LandmarkDetection(matInput.getNativeObjAddr(),matOutput.getNativeObjAddr());
  37. Toast.makeText(DetailActivity.this,"Calling native function222",Toast.LENGTH_SHORT).show();
  38. // 将Mat转换为Bitmap,并进行显示
  39. bitmapOutput=convertMat2Bitmap(matOutput);
  40. imageView.setImageBitmap(bitmapOutput);
  41. }else{
  42. Toast.makeText(DetailActivity.this,"失败",Toast.LENGTH_SHORT).show();
  43. }
  44. }
  45. });
  46. }
  47. // Mat转换成Bitmap
  48. Bitmap convertMat2Bitmap(Mat img){
  49. int width=img.width();
  50. int height=img.height();
  51. Bitmap bmp;
  52. bmp=Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
  53. Mat tmp;
  54. tmp=img.channels()==1?new Mat(width,height,CvType.CV_8UC1,new Scalar(1)):new Mat(width,height,CvType.CV_8UC3,new Scalar(3));
  55. try{
  56. if(img.channels()==3){
  57. Imgproc.cvtColor(img,tmp,Imgproc.COLOR_RGB2BGRA);
  58. }else if(img.channels()==1){
  59. Imgproc.cvtColor(img,tmp,Imgproc.COLOR_GRAY2BGRA);
  60. }
  61. Utils.matToBitmap(tmp,bmp);
  62. }catch(Exception e){
  63. Log.d("Exception",e.getMessage());
  64. }
  65. return bmp;
  66. }
  67. //bitmap转换成Mat
  68. Mat convertBitmap2Mat(Bitmap rgbaImg){
  69. Mat rgbaMat=new Mat(rgbaImg.getHeight(),rgbaImg.getWidth(),CvType.CV_8UC4);
  70. Bitmap bmp32=rgbaImg.copy(Bitmap.Config.ARGB_8888,true);
  71. Utils.bitmapToMat(bmp32,rgbaMat);
  72. Mat rgbNewMat=new Mat(rgbaImg.getHeight(),rgbaImg.getWidth(),CvType.CV_8UC3);
  73. Imgproc.cvtColor(rgbaMat,rgbNewMat, Imgproc.COLOR_RGB2BGR,3);
  74. return rgbNewMat;
  75. }
  76. }

5. NativeClass

  1. package com.wzf.face.landmarks2;
  2. public class NativeClass {
  3. public native static void LandmarkDetection(long addrInput,long addrOutput);
  4. }

至此,java部分的内容及基本算是完成了

6. 得到NativeClass对应的.h文件以及jni文件夹。

从android studio的命令行中,输入

cd app/src/main
javah -d jni -classpath ../../build/intermediates/classes/debuge com.wzf.face.landmarks2.NativeClass (这个是nativeclass的copy reference结果,即包名+类名)

下一步很关键!

在完成上述两个命令后,如果没有出错的话,之后你会获得一个jni文件夹,里面有一个名字还算长的.h文件。将该jni文件夹复制,然后随便找一个地方进行放置。完成后删除项目中的jni文件夹。

jni.png-43.1kB
比如我就把它放到了一个名为generateLibs的文件夹下。

7. 写.cpp与.h文件

进入jni文件夹,拷贝.h文件一份,将其重命名为.cpp文件。

7.1 修改.h文件内容
  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. // 以下是需要到的库文件
  3. #include <iostream>
  4. #include <jni.h>
  5. #include <opencv2/opencv.hpp>
  6. #include <opencv2/highgui/highgui.hpp>
  7. #include <dlib/opencv.h>
  8. #include <dlib/image_processing/frontal_face_detector.h>
  9. #include <dlib/image_processing/render_face_detections.h>
  10. #include <dlib/image_processing.h>
  11. #include <dlib/gui_widgets.h>
  12. /* Header for class com_wzf_face_landmarks2_NativeClass */
  13. // 定义命名空间
  14. using namespace cv;
  15. using namespace dlib;
  16. using namespace std;
  17. #ifndef _Included_com_wzf_face_landmarks2_NativeClass
  18. #define _Included_com_wzf_face_landmarks2_NativeClass
  19. #ifdef __cplusplus
  20. extern "C" {
  21. #endif
  22. // 声明两个方法
  23. void faceDetectiononDlib(Mat& img,Mat& dst);
  24. void renderToMat(std::vector<full_object_detection>& dets, Mat& dst);
  25. /*
  26. * Class: com_wzf_face_landmarks2_NativeClass
  27. * Method: LandmarkDetection
  28. * Signature: (JJ)V
  29. */
  30. // 仔细看会发下,这个方法就对应于我们在Nativeclass.java中声明的方法
  31. JNIEXPORT void JNICALL Java_com_wzf_face_landmarks2_NativeClass_LandmarkDetection
  32. (JNIEnv *, jclass, jlong, jlong);
  33. #ifdef __cplusplus
  34. }
  35. #endif
  36. #endif
7.2 修改.cpp文件
  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. // 别忘了添加自己的库依赖(com_wzf_face_landmarks2_NativeClass.h)
  3. #include <jni.h>
  4. #include <com_wzf_face_landmarks2_NativeClass.h>
  5. #include "com_wzf_face_landmarks2_NativeClass.h"
  6. /* Header for class com_wzf_face_landmarksdetection_NativeClass */
  7. /*
  8. * Class: com_wzf_face_landmarksdetection_NativeClass
  9. * Method: LandmarkDetection
  10. * Signature: (JJ)V
  11. */
  12. // 同理,对应于NativeClass.java中声明的方法
  13. JNIEXPORT void JNICALL Java_com_wzf_face_landmarks2_NativeClass_LandmarkDetection
  14. (JNIEnv *env, jclass thiz, jlong addrInput, jlong addrOutput){
  15. // 获取传入的参数,进行强转
  16. Mat& image=*(Mat*) addrInput;
  17. Mat& dst=*(Mat*) addrOutput;
  18. // 利用dlib进行检测
  19. faceDetectiononDlib(image,dst);
  20. }
  21. void faceDetectiononDlib(Mat& img, Mat& dst){
  22. try{
  23. // 获取检测器
  24. frontal_face_detector detector=get_frontal_face_detector();
  25. shape_predictor pose_model;
  26. // 加载参数
  27. deserialize("storage/emulated/0/shape_predictor_68_face_landmarks.dat")>>pose_model;
  28. // 将opencv图像转化为dlib图像
  29. cv_image<bgr_pixel> cimg(img);
  30. // 检测,面部信息存储在faces中
  31. std::vector<dlib::rectangle> faces=detector(cimg);
  32. std::vector<full_object_detection> shapes;
  33. // 这步不是很懂,存储面部的landmarks信息?
  34. for(unsigned long i=0;i<faces.size();i++){
  35. shapes.push_back(pose_model(cimg,faces[i]));
  36. }
  37. // 将结果赋值到dst对象内
  38. dst = img.clone();
  39. // 将landmarks进行绘图,以便观看显示
  40. renderToMat(shapes,dst);
  41. }catch(serialization_error& e) {
  42. cout<<endl<<e.what()<<endl;
  43. }
  44. }
  45. // 对landmarks进行绘图显示。
  46. void renderToMat(std::vector<full_object_detection>& dets, Mat& dst){
  47. Scalar color;
  48. int sz = 3;
  49. color = Scalar(0,255,0);
  50. //chin line
  51. for(unsigned long idx = 0; idx < dets.size(); idx++){
  52. for (unsigned long i = 1; i <= 16; ++i)
  53. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  54. //line on top of nose
  55. for (unsigned long i = 28; i <= 30; ++i)
  56. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  57. //left eyebrow
  58. for (unsigned long i = 18; i <= 21; ++i)
  59. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  60. //right eyebrow
  61. for (unsigned long i = 23; i <= 26; ++i)
  62. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  63. //bottom of nose
  64. for (unsigned long i = 31; i <= 35; ++i)
  65. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  66. cv::line(dst, Point(dets[idx].part(30).x(),dets[idx].part(30).y()), Point(dets[idx].part(35).x(), dets[idx].part(35).y()), color, sz);
  67. //left eye
  68. for (unsigned long i = 37; i <= 41; ++i)
  69. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  70. cv::line(dst, Point(dets[idx].part(36).x(),dets[idx].part(36).y()), Point(dets[idx].part(41).x(), dets[idx].part(41).y()), color, sz);
  71. //right eye
  72. for (unsigned long i = 43; i <= 47; ++i)
  73. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  74. cv::line(dst, Point(dets[idx].part(42).x(),dets[idx].part(42).y()), Point(dets[idx].part(47).x(), dets[idx].part(47).y()), color, sz);
  75. //lips out part
  76. for (unsigned long i = 49; i <= 59; ++i)
  77. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  78. cv::line(dst, Point(dets[idx].part(48).x(),dets[idx].part(48).y()), Point(dets[idx].part(59).x(), dets[idx].part(59).y()), color, sz);
  79. //lips inside part
  80. for (unsigned long i = 61; i <= 67; ++i)
  81. cv::line(dst, Point(dets[idx].part(i).x(), dets[idx].part(i).y()), Point(dets[idx].part(i - 1).x(), dets[idx].part(i - 1).y()), color, sz);
  82. cv::line(dst, Point(dets[idx].part(60).x(),dets[idx].part(60).y()), Point(dets[idx].part(67).x(), dets[idx].part(67).y()), color, sz);
  83. }
  84. }
7.3 注意

在复制粘贴别人代码的时候,请千万注意,方法名是否和自己定义的java类中的方法名以及包名对的上。 我就是在这里卡了很久的= - 。

8. 导入dlib包以及opencv包

8.1 导入dlib

去dlib官网下载dlib,解压即可,我用的是19.1

解压后,将解压出来的文件夹放置在jni文件夹下,如果名字不是dlib的话,重命名为dlib(只是为了写mk文件方便)。
dlib1.png-44.3kB
dlib2.png-56.6kB

8.2 导入opencv

1、与jni文件夹同级时,创建文件夹,名为third_party.

2、在third_party内,创建文件夹,名为opencv

3、将“opencv安装路径”\sdk\native 下的三个文件夹拷贝至2中创建的文件夹内。
opencv1.png-43.7kB
opencv2.png-34.6kB

至此,导入完成。

9. Android.mk和Application.mk撰写

9.1 Android.mk
  1. LOCAL_PATH := $(call my-dir)
  2. # 加载dlib库
  3. include $(CLEAR_VARS)
  4. LOCAL_MODULE :=dlib
  5. LOCAL_C_INCLUDES :=$(LOCAL_PATH)/dlib
  6. LOCAL_SRC_FILES += \
  7. ../$(LOCAL_PATH)/dlib/dlib/threads/threads_kernel_shared.cpp \
  8. ../$(LOCAL_PATH)/dlib/dlib/entropy_decoder/entropy_decoder_kernel_2.cpp \
  9. ../$(LOCAL_PATH)/dlib/dlib/base64/base64_kernel_1.cpp \
  10. ../$(LOCAL_PATH)/dlib/dlib/threads/threads_kernel_1.cpp \
  11. ../$(LOCAL_PATH)/dlib/dlib/threads/threads_kernel_2.cpp
  12. LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
  13. include $(BUILD_STATIC_LIBRARY)
  14. #加载opencv库
  15. TOP_LEVEL_PATH :=$(abspath $(LOCAL_PATH)/..)
  16. ####$(info TOP Level Path: $(TOP_LEVEL_PATH))#####
  17. EXT_INSTALL_PATH = $(TOP_LEVEL_PATH)/third_party
  18. OPENCV_PATH = $(EXT_INSTALL_PATH)/opencv/jni
  19. OPENCV_INCLUDE_DIR = $(OPENCV_PATH)/include
  20. include $(CLEAR_VARS)
  21. OPENCV_INSTALL_MODULES :=on
  22. OPENCV_CAMERA_MODULES :=on
  23. OPENCV_LIB_TYPE :=SHARED
  24. include $(OPENCV_PATH)/OpenCV.mk
  25. # 给自己的库取个名字
  26. LOCAL_MODULE := MyLibs
  27. LOCAL_C_INCLUDES += \ $(OPENCV_INCLUDE_DIR)
  28. # 指定源文件,即之前编写的.cpp文件
  29. LOCAL_SRC_FILES := com_wzf_face_landmarks2_NativeClass.cpp
  30. LOCAL_LDLIBS += -lm -llog -ldl -lz -ljnigraphics
  31. LOCAL_CPPFLAGS += -fexceptions -frtti -std=c++11
  32. LOCAL_STATIC_LIBRARIES +=dlib
  33. include $(BUILD_SHARED_LIBRARY)
9.2 Application.mk
  1. NDK_TOOLCHAIN_VERSION := clang
  2. #指定自己目标的cpu库
  3. APP_ABI := arm64-v8a
  4. APP_CPPFLAGS := -std=c++11 -frtti -fexceptions
  5. # 指定目标android level
  6. APP_PLATFORM := android-23
  7. APP_STL := gnustl_static

10. 生成libs和obj文件夹

1、 在jni和third_party所在的文件件内打开命令行窗口。
nkd.png-149.9kB
2、 确保你已经安装了ndk,并可以在任何地方运行(配置环境变量就好),教程自行google.
3、 在命令行窗口内输入ndk-build,若没有错误,等待即可。
ndk2.png-14.8kB
4、 若有错误,也是自己编写的.cpp或者.h文件中的问题,根据提示修改即可。
5、完成后即可看见libs和obj两个文件夹。

11. 导入,运行

1、 进入libs文件夹,里面有你目标cpu架构的
cpu.png-31.7kB
2、 在android studio中,右击app,创建JNI文件夹,并命名为jniLibs。
jnilibs.png-20kB
3、 将1中的cpu架构文件夹,粘贴到2中生成的jniLibs文件夹下,即可。
jniLibs2.png-10.5kB

12. 结果展示

Result.png-167.6kB

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注