OpenCV4Android编译
最近的一个项目中,需要自己编译OpenCV Android版本。(想要修改其中libopencv_java.so)。
之前也做过OpenCV的完整编译,但当时必须使用OpenCV2.0版本(据说这个版本最快),想要在Android Native C层面使用OpenCV2.0 库,但OpenCV2.0并无Android版本,所以下载了OpenCV2.0 for Linux Source Code。自己写Android.mk Application.mk来编译之。将所有依赖第三方库和OpenCV本身的:cvaux,cv, cxcore, ml, highgui等全部成功编译。且highgui底层支持V4L2, libv4l, ffmpeg等。 (具体编译过程以后再讲) . 当时就体会了OpenCV编译的繁琐和复杂,以及配置选项之多。这次需要编译OpenCV4Android,有了打持久战的精神准备。
(后续,之后随项目的升级,又编译了OpenCV3.1。于是添加在这里)
1. 下载和准备工作:
编译平台:Fedora17, Fedora20
http://opencv.org/downloads.html
选择了Version 2.4.10 OpenCV for Android Source下载。
与OpenCV2.0使用autoconfig不同,OpenCV2.4 已经采用CMake管理Source。
必须预先安装的软件有:
a. jdk
b. Android SDK (revision 14以上)
c. Android NDK
d. Apache Ant. (Version 1.9以上)
e: Python.
请注意版本限制,如果版本过低,编译中会遇到问题。
Sam主编译机器时FC17, yum install ant 会被安装Apache Ant 1.8. 就遇到编译问题。后来手动下载
- http://ant.apache.org 1.9版本。才解决问题。 但还是做了个软链接 /usr/ant
副编译机采用FC 20 . yum install ant本身就是Apache Ant 1.9. 所以没有问题。
如果运行ant,可能会发现找不到库,
(Error: Could not find or load main class org.apache.tools.ant.launch.Launcher)
则需要
export CLASSPATH=.:JAVA_HOME/lib/tools.jar:/usr/share/java/ant-launcher.jar
2. 设置环境变量:
export ANDROID_NDK=/opt/android-ndk-r9d/
export ANDROID_SDK=/home/sam/android-sdks/
export ANDROID_ABI=armeabi-v7a
export ANT_HOME=/usr/local/apache-ant-1.9.4
export PATH={ANT_HOME}/bin
3. cmake产生Makefile:
#cd platforms/
#sh scripts/cmake_android_arm.sh
这个shell脚步会创建build_android_arm目录,并将Makefile文件放置在其中。
Sam需要确保Java modules包含在其中。否则不会产生libopencv_java.so
-- OpenCV modules:
-- To be built: core androidcamera flann imgproc highgui features2d calib3d ml video legacy objdetect photo gpu ocl nonfree contrib java stitching superres ts videostab
如果发现Cmake 产生的这个列表中,有些设置需要修改。可以尝试察看:
opencv-2.4.10/CMakeLists.txt, 它同时也include很多 .mk文件(opencv-2.4.10/cmake目录内)。 看看具体是什么原因。
例1:刚开始无论如何无法使Java 加入编译模块。
Unavailable: dynamicuda Java python viz
后来只好跟踪 CMakeLists.txt, 直到 OpenCVDetectApacheAnt.cmake中,才发现:
execute_process(COMMAND ${ANT_EXECUTABLE} -version
后出错。 这里ANT_EXECUTABLE就是ant.
才意识到ant未安装造成问题。
例2:
Sam想把所有Example都编译出来,但 C/C++ Examples: 显示NO。
察看CMakeLists.txt, 看到其和BUILD_EXAMPLES 相关, 于是作如下修改:
opencv-2.4.10/platforms/scripts/cmake_android_arm.sh 中添加:
-DBUILD_EXAMPLES=1
则C++ Example也被加入编译了。
4. 编译:
#cd opencv-2.4.10/platforms/build_android_arm
#make
5.OpenCV一些附加模块支持:
OpenCV在不断更新中,添加了很多模块,但这些模块并未添加入代码树中。例如:Eigen,TBB,OpenCL等。需要自己处理。
5.1: 对Eigen的支持:
platforms/scripts/cmake_android_arm.sh 中添加:
-DHAVE_EIGEN=1
此时 调用: sh scripts/cmake_android_arm.sh
关于Eigen的结果有点异常:
-- Other third-party libraries:
-- Use Eigen: YES (ver ..)
没有得到版本号,则一定没有找到对应头文件等。查看CMakeLists.txt。以下几项内容缺失。
{EIGEN_MAJOR_VERSION}.${EIGEN_MINOR_VERSION}
跟踪检查:cmake/OpenCVFindLibsPerf.cmake
其中与此相关的是:
if(WITH_EIGEN)
find_path(EIGEN_INCLUDE_PATH "Eigen/Core"
PATHS /usr/local /opt /usr ENV ProgramFiles ENV ProgramW6432
PATH_SUFFIXES include/eigen3 include/eigen2 Eigen/include/eigen3 Eigen/include/eigen2
DOC "The path to Eigen3/Eigen2 headers"
CMAKE_FIND_ROOT_PATH_BOTH)
if(EIGEN_INCLUDE_PATH)
ocv_include_directories(${EIGEN_INCLUDE_PATH})
ocv_parse_header("${EIGEN_INCLUDE_PATH}/Eigen/src/Core/util/Macros.h" EIGEN_VERSION_LINES EIGEN_WORLD_VERSION EIGEN_MAJOR_VERSIO
N EIGEN_MINOR_VERSION)
set(HAVE_EIGEN 1)
endif()
endif(WITH_EIGEN)
find_path()含义是:在PATHS 后面的目录内查找Eigen/Core目录。查到了,则目录存放在EIGEN_INCLUDE_PATH中。否则写入NOFound。
然后在Eigen/src/Core/util/Macros.h找到版本号并存储。
所以当前是因为没有Eigen Source Tree造成问题。Sam下载(https://bitbucket.org/erublee/eigen-android/get/2e2c8da72443.zip)并把它放在3rdparty/eigen中。
并修改cmake/OpenCVFindLibsPerf.cmake 相关内容为:
if(WITH_EIGEN)
find_path(EIGEN_INCLUDE_PATH "Eigen/Core"
PATHS /usr/local /opt /usr /home/sam/work/current/Research/OpenCV/opencv-2.4.10/3rdparty/eigen ENV ProgramFiles ENV Pro
gramW6432
PATH_SUFFIXES include/eigen3 include/eigen2 Eigen/include/eigen3 Eigen/include/eigen2
DOC "The path to Eigen3/Eigen2 headers"
CMAKE_FIND_ROOT_PATH_BOTH)
message(SamInfo)
message(${EIGEN_INCLUDE_PATH})
if(EIGEN_INCLUDE_PATH)
ocv_include_directories(${EIGEN_INCLUDE_PATH})
message(${EIGEN_INCLUDE_PATH})
ocv_parse_header("${EIGEN_INCLUDE_PATH}/Eigen/src/Core/util/Macros.h" EIGEN_VERSION_LINES EIGEN_WORLD_VERSION EIGEN_MAJOR_VERSIO
N EIGEN_MINOR_VERSION)
set(HAVE_EIGEN 1)
endif()
endif(WITH_EIGEN)
此时调用: sh scripts/cmake_android_arm.sh
-- Other third-party libraries:
-- Use Eigen: YES (ver 2.92.0)
5.2: 关于OpenCL:
因为OpenCL依赖于芯片支持,而Sam发现当前几个芯片(ARM)均不支持OpenCL。所以哪怕将OpenCL编译入OpenCV,其实没有使用到。
5.3: TBB支持:
platforms/scripts/cmake_android_arm.sh 中添加:
-DBUILD_TBB=ON -DWITH_TBB=ON
此时 调用: sh scripts/cmake_android_arm.sh
则会自动下载TBB for ARM来使用。并编译入OpenCV库。
6. 附加讲解:
编译过程非常明晰简单,即:
sh scripts/cmake_android_arm.sh
此时,可以在cmake_android_arm.sh中添加一些配置如下:
cmake -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DHAVE_EIGEN=1 -DWITH_V4L=1 -DHAVE_CAMV4L2=ON -DBUILD_TBB=ON -DWITH_TBB=ON -DBUILD_EXAMPLES=1 -DANDROID_ABI="armeabi-v7a" -DCMAKE_TOOLCHAIN_FILE=../android/android.toolchain.cmake $@ ../..
但这些配置均会起作用么?不一定。
当前我们要编译Android ARM版本,那OpenCV HighGUI中如何使用何种接口控制Camera呢?在Linux时代,有libv4l, V4L2等接口。在Android时代,还可以使用它们么? Sam为了测试这一点,添加了:
-DWITH_V4L=1 -DWITH_LIBV4L=1 -DHAVE_CAMV4L2=ON
下面就看看是否有效.
察看CMakeLists.txt
可以看到以下几类:
A:
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_LIBV4L "Use libv4l for Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
在这里,如果不是Android版本,WITH_V4L, WITH_LIBV4L才会被设置为ON。
B:
# ----------------------------------------------------------------------------
# Detect 3rd-party libraries
# ----------------------------------------------------------------------------
include(cmake/OpenCVFindLibsGrfmt.cmake)
include(cmake/OpenCVFindLibsGUI.cmake)
include(cmake/OpenCVFindLibsVideo.cmake)
include(cmake/OpenCVFindLibsPerf.cmake)
其中,OpenCVFindLibsVideo.cmake中有相关设置:
ocv_clear_vars(HAVE_LIBV4L HAVE_CAMV4L HAVE_CAMV4L2 HAVE_VIDEOIO)
#Sam info:先把HAVE_XXXX信息先clean掉。再根据WITH_XXX设置之。
if(WITH_V4L)
if(WITH_LIBV4L)
CHECK_MODULE(libv4l1 HAVE_LIBV4L1)
CHECK_MODULE(libv4l2 HAVE_LIBV4L2)
if(HAVE_LIBV4L1 AND HAVE_LIBV4L2)
set(HAVE_LIBV4L YES)
else()
set(HAVE_LIBV4L NO)
endif()
endif()
CHECK_INCLUDE_FILE(linux/videodev.h HAVE_CAMV4L)
CHECK_INCLUDE_FILE(linux/videodev2.h HAVE_CAMV4L2)
CHECK_INCLUDE_FILE(sys/videoio.h HAVE_VIDEOIO)
endif(WITH_V4L)
所以,在cmake_android_arm.sh中,
1:修改HAVE_CAMV4L这样的设置,并无多大用户,因为后面会被WITH_LIBV4L设置所修改。所以只设置WITH_XXXX就好。
2. 哪怕设置WITH_XXXXX。但也会被各种具体细节所修改。例如,是否Android,是否Win等。
7. Android NativeC版本编译:
Sam需要编译一个纯NativeC 程序,它使用到OpenCV2.4.10. 按照之前的认识,直接编译一个Android版本,使用libopencv_java.so即可。但在实际使用中,发现使用NativeC程序,无法正常访问Camera。后来重新编译OpenCV,并引导它改用V4L2,解决了此问题。现将过程记录如下:
7.1: 问题发现:
运行程序:发现和Camera相关处全都会抱错,提示要求重新编译OpenCV。
Sam从Camera相关第一个function查起:cvCaptureFromCAM()
在modules/highgui/include/opencv2/highgui/highgui_c.h中,有
#define cvCaptureFromCAM cvCreateCameraCapture
在modules/highgui/src/cap.cpp中,
有其实现:CV_IMPL CvCapture * cvCreateCameraCapture (int index)
它其实就是根据配置来选择调用何种接口,Sam在OpenCV2.0编译时,利用编译选项使它指向libv4l和v4l2俩接口。
7.2: 尝试修改:
既然之前使用libv4l和v4l2接口能够成功。那咱们就继续来。
在platforms/scripts/cmake_android_arm.sh中,增加以下选项
-DWITH_V4L=1 -DWITH_LIBV4L=1 -DHAVE_CAMV4L2=ON
编译后,发现并未被指向libv4l 或者v4l2。找原因:
CMakeLists.txt看到以下语句:
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_LIBV4L "Use libv4l for Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
只有非Android平台时,才会把WITH_V4L和WITH_LIBV4L置ON
所以修改之:
#Sam modify it
#OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX) )
#OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX) )
把非Android版本才允许WITH_V4l 的要求去掉。
在cmake/OpenCVFindLibsVideo.cmake中:
ocv_clear_vars(HAVE_LIBV4L HAVE_CAMV4L HAVE_CAMV4L2 HAVE_VIDEOIO)
if(WITH_V4L)
if(WITH_LIBV4L)
CHECK_MODULE(libv4l1 HAVE_LIBV4L1)
CHECK_MODULE(libv4l2 HAVE_LIBV4L2)
if(HAVE_LIBV4L1 AND HAVE_LIBV4L2)
set(HAVE_LIBV4L YES)
else()
set(HAVE_LIBV4L NO)
endif()
endif()
CHECK_INCLUDE_FILE(linux/videodev.h HAVE_CAMV4L)
CHECK_INCLUDE_FILE(linux/videodev2.h HAVE_CAMV4L2)
CHECK_INCLUDE_FILE(sys/videoio.h HAVE_VIDEOIO)
endif(WITH_V4L)
此处,会查找linux/videodev2.h,来决定HAVE_CAMV4L2为ON。
但不知为何,它总找不到。不知道CHECK_INCLUDE_FILE()找的是哪个目录。
于是强行修改:
# --- V4L ---
status(" WITH_V4L:" ${WITH_V4L})
status(" HAVE_LIBV4L:" ${HAVE_LIBV4L})
status(" HAVE_CAMV4L:" ${HAVE_CAMV4L})
status(" HAVE_CAMV4L2:" ${HAVE_CAMV4L2})
status(" HAVE_VIDEOIO:" ${HAVE_VIDEOIO})
ocv_clear_vars(HAVE_LIBV4L HAVE_CAMV4L HAVE_CAMV4L2 HAVE_VIDEOIO)
if(WITH_V4L)
if(WITH_LIBV4L)
CHECK_MODULE(libv4l1 HAVE_LIBV4L1)
CHECK_MODULE(libv4l2 HAVE_LIBV4L2)
if(HAVE_LIBV4L1 AND HAVE_LIBV4L2)
set(HAVE_LIBV4L YES)
else()
set(HAVE_LIBV4L NO)
endif()
endif()
CHECK_INCLUDE_FILE(linux/videodev.h HAVE_CAMV4L)
CHECK_INCLUDE_FILE(linux/videodev2.h HAVE_CAMV4L2)
CHECK_INCLUDE_FILE(sys/videoio.h HAVE_VIDEOIO)
#sam add it
set(HAVE_CAMV4L YES)
set(HAVE_CAMV4L2 YES)
status(" HAVE_LIBV4L:" ${HAVE_LIBV4L})
status(" HAVE_CAMV4L:" ${HAVE_CAMV4L})
status(" HAVE_CAMV4L2:" ${HAVE_CAMV4L2})
status(" HAVE_VIDEOIO:" ${HAVE_VIDEOIO})
endif(WITH_V4L)
此时,V4L2被作为底层编译进去了。
7.3: 察看原来路径:
那之前没加入V4L2 之前呢?看看OpenCV是怎么做的。
因为:HAVE_ANDROID_NATIVE_CAMERA被设置,所以走了:
cvCreateCameraCapture_Android()这路。它最终调用Camera_activity.cpp中的class CameraWrapperConnector去处理Camera。这里需要用到:
libnative_camera_r2.2.0.so
libnative_camera_r2.3.3.so
.....
libnative_camera_r4.4.0.so
但具体选择哪一个,不知道用什么规则来确定。
看起来这条路是通的阿,其实不然,这条路相信是给JNI-C中使用Camera准备的,它密切依赖于OpenCV-Manager. 对Native-C程序来说并不适用。如果强行编译使用,会被锁死。
这条路径,Sam称之为:Native-Camera 路径。
7.4:堵住原生Native-Camera路径:
Sam 在cmake/templates/opencvconfig.cmake.in中,删除:
set(OpenCV_HAVE_ANDROID_CAMERA @HAVE_opencv_androidcamera@)
在modules/highgui/CMakeList.txt中,删除:
add_definitions(-DHAVE_ANDROID_NATIVE_CAMERA)
即可堵住Native-Camera。
7.5: 局限:
此方法编译出的库,有非常大的局限性,除非必须使用NativeC方式使用OpenCV,否则不要用此方法,因为它破坏了原生JNI-C方式使用Camera的路径。不建议使用。
8. OpenCV3.1的编译:
随着项目的升级,又需要在NativeC下使用OpenCV3.1. 继续采用OpenCV for Android 为基础编译,修改其Camera路径为V4L2或者libv4l. 使之不再依赖于OpenCVManager而独立在NativeC层存在。
8.1: 下载OpenCV3.1 Source Code:
需要使用git下载。在https://github.com/itseez/opencv 页面,得到git地址,
git clone https://github.com/Itseez/opencv.git
8.2:确认版本:
modules/core/include/opencv2/core/version.hpp:
#define CV_VERSION_MAJOR 3
#define CV_VERSION_MINOR 1
#define CV_VERSION_REVISION 0
#define CV_VERSION_MINOR 1
#define CV_VERSION_REVISION 0
8.3: 增加对V4L,V4L2等的支持:
A:在platforms/scripts/cmake_android_arm.sh中,增加以下选项
-DWITH_V4L=1 -DWITH_LIBV4L=1 -DHAVE_CAMV4L2=ON
B:CMakeLists.txt修改以下语句:
#Sam modify it
#OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX) )
#OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX AND NOT ANDROID) )
OCV_OPTION(WITH_V4L "Include Video 4 Linux support" ON IF (UNIX) )
把非Android版本才允许WITH_V4l 的要求去掉。
C:在cmake/OpenCVFindLibsVideo.cmake中
if(WITH_V4L)
if(WITH_LIBV4L)
CHECK_MODULE(libv4l1 HAVE_LIBV4L1)
CHECK_MODULE(libv4l2 HAVE_LIBV4L2)
if(HAVE_LIBV4L1 AND HAVE_LIBV4L2)
set(HAVE_LIBV4L YES)
else()
set(HAVE_LIBV4L NO)
endif()
endif()
CHECK_INCLUDE_FILE(linux/videodev.h HAVE_CAMV4L)
CHECK_INCLUDE_FILE(linux/videodev2.h HAVE_CAMV4L2)
CHECK_INCLUDE_FILE(sys/videoio.h HAVE_VIDEOIO)
#sam add it
set(HAVE_CAMV4L YES)
set(HAVE_CAMV4L2 YES)
status(" HAVE_LIBV4L:" ${HAVE_LIBV4L})
status(" HAVE_CAMV4L:" ${HAVE_CAMV4L})
status(" HAVE_CAMV4L2:" ${HAVE_CAMV4L2})
status(" HAVE_VIDEOIO:" ${HAVE_VIDEOIO})
endif(WITH_V4L)
8.4:处理编译问题:
根据以上设置,会在调用时cvCreateCameraCapture()调用cvCreateCameraCapture_V4L().
且modules/videoio/cap_v4l.cpp会被编译进来。
但NDK中的v4l2的头文件比较老,会导致一些错误(未定义错误)
Sam直接在cap_v4l.cpp中添加如下语句解决之:
//sam add it
#define V4L2_PIX_FMT_SGBRG8 v4l2_fourcc('G', 'B', 'R', 'G')
#define V4L2_PIX_FMT_SGBRG8 v4l2_fourcc('G', 'B', 'R', 'G')
#define V4L2_CTRL_CLASS_CAMERA 0x009a0000
#define V4L2_CID_CAMERA_CLASS_BASE (V4L2_CTRL_CLASS_CAMERA | 0x900)
#define V4L2_CID_FOCUS_ABSOLUTE (V4L2_CID_CAMERA_CLASS_BASE+10)
#define V4L2_CID_CAMERA_CLASS_BASE (V4L2_CTRL_CLASS_CAMERA | 0x900)
#define V4L2_CID_FOCUS_ABSOLUTE (V4L2_CID_CAMERA_CLASS_BASE+10)
#define V4L2_CID_FOCUS_AUTO (V4L2_CID_CAMERA_CLASS_BASE+12)
8.5: 处理FFMPEG库添加问题:
Sam并不是用正规做法加入FFMPEG支持,所以,在OpenCV编译时,常会发现找不到ffmpeg 符号的问题:
例如:
Linking CXX executable ../../bin/opencv_perf_videoio
../../lib/armeabi-v7a/libopencv_videoio.a(cap_ffmpeg.cpp.o):cap_ffmpeg.cpp:function InternalFFMpegRegister::~InternalFFMpegRegister(): error: undefined reference to 'av_lockmgr_register'
../../lib/armeabi-v7a/libopencv_videoio.a(cap_ffmpeg.cpp.o):cap_ffmpeg.cpp:function InternalFFMpegRegister::~InternalFFMpegRegister(): error: undefined reference to 'av_lockmgr_register'
Sam的解决方法是:
在类似以下文件:
modules/videoio/CMakeFiles/opencv_perf_videoio.dir/link.txt
加入以下语句:
-lffmpeg -L../../3rdparty/ffmpeg/android/armv7-a/
这个方法并不正规。但先这么处理了。(Sam把单独编译的ffmpeg放在了platforms/build_android_arm/3rdparty/ffmpeg/android)
附录:
错误记录:
[ 73%] Building OpenCV Android library project
/usr/bin/ant -q -noinput -k debug
[subant] No sub-builds to iterate on
Target '-compile' failed with message 'The following error occurred while executing this line:
/home/sam/android-sdks/tools/ant/build.xml:734: Class not found: javac1.8'.
Cannot execute '-dex' - '-compile' failed or was not executed.
Cannot execute '-package' - '-dex' failed or was not executed.
Cannot execute '-do-debug' - '-package' failed or was not executed.
Cannot execute 'debug' - '-do-debug' failed or was not executed.
BUILD FAILED
/home/sam/android-sdks/tools/ant/build.xml:720: The following error occurred while executing this line:
/home/sam/android-sdks/tools/ant/build.xml:734: Class not found: javac1.8
Total time: 3 seconds
make[2]: *** [bin/classes.jar] Error 1
make[2]: Leaving directory `/home/sam/work/current/Research/OpenCV/opencv-2.4.10/platforms/build_android_arm'
make[1]: *** [modules/java/CMakeFiles/opencv_java.dir/all] Error 2
make[1]: Leaving directory `/home/sam/work/current/Research/OpenCV/opencv-2.4.10/platforms/build_android_arm'
make: *** [all] Error 2
[sam@KingOfLinux build_android_arm]$ ant -version
Apache Ant(TM) version 1.8.3 compiled on February 29 2012
[sam@KingOfLinux build_android_arm]$ javac -version
javac 1.8.0_20
这个错误是因为ANT 版本太低造成的。
Sam升级到1.9版本即可。
_________________________________________________________________________________________________________________________________________________
每一个不曾起舞的日子,都是对生命的辜负。
But it is the same with man as with the tree. The more he seeks to rise into the height and light, the more vigorously do his roots struggle earthward, downward, into the dark, the deep - into evil.
其实人跟树是一样的,越是向往高处的阳光,它的根就越要伸向黑暗的地底。----尼采
每一个不曾起舞的日子,都是对生命的辜负。
But it is the same with man as with the tree. The more he seeks to rise into the height and light, the more vigorously do his roots struggle earthward, downward, into the dark, the deep - into evil.
其实人跟树是一样的,越是向往高处的阳光,它的根就越要伸向黑暗的地底。----尼采
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话