【机器学习实战高阶】基于深度学习车牌识别 自动识别车牌号码 Automatic License Number Plate Detection and Recognition
深度学习项目 - 自动车牌识别
本项目旨在识别车牌号码。为了检测车牌号码,我们将使用 OpenCV 识别车牌,并利用 Python 的 pytesseract 从车牌中提取字符和数字信息。
自动车牌识别
OpenCV 是一个开源的机器学习库,为计算机视觉提供了通用的基础设施。而 Pytesseract 是一个 Tesseract-OCR 引擎,用于读取图像类型并提取图像中的信息。
安装 OpenCV 和 Pytesseract pip3 Python 包:
pip3 install opencv-python # 安装 OpenCV 库
pip3 install pytesseract # 安装 Pytesseract 库
在本 Python 项目中,为了在输入图像中识别车牌,我们将使用 OpenCV 的以下功能:
- 高斯模糊(Gaussian Blur):我们使用高斯核来平滑图像。这一技术非常有效,能够去除高斯噪声。OpenCV 提供了
cv2.GaussianBlur()
函数来完成此任务。 - Sobel 梯度:我们计算图像的导数。这一功能在许多计算机视觉任务中非常重要。通过导数计算梯度,梯度的大幅变化表示图像中的主要变化。OpenCV 提供了
cv2.Sobel()
函数来计算 Sobel 操作符。 - 形态学变换(Morphological Transformation):这些操作基于图像形状,通常在二值图像上执行。基本的形态学操作包括腐蚀(Erosion)、膨胀(Dilation)、开运算(Opening)和闭运算(Closing)。OpenCV 提供了以下函数:
cv2.erode()
:腐蚀操作。cv2.dilate()
:膨胀操作。cv2.morphologyEx()
:执行开运算和闭运算等复杂形态学操作。
- 轮廓(Contours):轮廓是包含所有连续相同强度点的曲线。它们是对象识别中非常有用的工具。OpenCV 提供了
cv2.findContours()
函数来实现这一功能。
下载项目源代码
在继续之前,请下载源代码:
链接: 基于深度学习车牌识别 自动识别车牌号码
现在,让我们深入到车牌识别代码中。请按照以下步骤操作:
-
导入库:
该项目需要 numpy 和 pillow Python 库,以及 OpenCV 和 pytesseract。
import numpy as np # 导入 numpy 库 import cv2 # 导入 OpenCV 库 from PIL import Image # 导入 PIL 库 import pytesseract as tess # 导入 pytesseract 库
-
定义辅助函数:
现在我们将定义三个函数,用于过滤掉 OpenCV 可能识别到但不太可能是车牌的轮廓。
2.1. 检查面积范围和宽高比:
该函数用于检查轮廓的面积范围和宽高比。如果面积或宽高比不符合要求,则返回 False。
def ratioCheck(area, width, height): # 定义检查面积和宽高比的函数
ratio = float(width) / float(height) # 计算宽高比
if ratio < 1:
ratio = 1 / ratio # 如果宽小于高,则取倒数
if (area < 1063.62 or area > 73862.5) or (ratio < 3 or ratio > 6): # 检查面积和宽高比
return False # 如果不符合要求,返回 False
return True # 如果符合要求,返回 True
2.2. 检查图像矩阵的平均值:
该函数用于检查车牌图像的平均像素值。如果平均值大于等于 115,则认为图像足够亮,返回 True;否则返回 False。
def isMaxWhite(plate): # 定义检查图像矩阵平均值的函数
avg = np.mean(plate) # 计算图像的平均像素值
if(avg>=115): # 如果平均值大于等于 115
return True # 返回 True
else:
return False # 否则返回 False
2.3. 检查轮廓的旋转角度:
该函数用于检查轮廓的旋转角度。如果旋转角度大于 15 度或宽高为 0,则返回 False。否则,检查轮廓的面积和宽高比,如果符合要求则返回 True。
def ratio_and_rotation(rect): # 定义检查轮廓旋转角度的函数
(x, y), (width, height), rect_angle = rect # 获取轮廓的中心点、宽高和旋转角度
if(width>height): # 如果宽度大于高度
angle = -rect_angle # 旋转角度取反
else:
angle = 90 + rect_angle # 否则旋转角度加 90 度
if angle>15: # 如果旋转角度大于 15 度
return False # 返回 False
if height == 0 or width == 0: # 如果高度或宽度为 0
return False # 返回 False
area = height * width # 计算轮廓面积
if not ratioCheck(area, width, height): # 检查面积和宽高比
return False # 如果不符合要求,返回 False
else:
return True # 如果符合要求,返回 True
-
清洗识别到的车牌图像:
该函数用于清洗识别到的车牌图像,以便在使用 pytesseract 之前进行预处理。首先将图像转换为灰度图像,然后进行二值化处理,找到轮廓并返回清理后的图像和轮廓信息。
def clean2_plate(plate): # 定义清洗车牌图像的函数 gray_img = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) # 将图像转换为灰度图像 _, thresh = cv2.threshold(gray_img, 110, 255, cv2.THRESH_BINARY) # 进行二值化处理 if cv2.waitKey(0) & 0xff == ord('q'): # 按下 'q' 键退出 pass num_contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # 找到轮廓 if num_contours: contour_area = [cv2.contourArea(c) for c in num_contours] # 计算每个轮廓的面积 max_cntr_index = np.argmax(contour_area) # 找到面积最大的轮廓 max_cnt = num_contours[max_cntr_index] # 获取面积最大的轮廓 max_cntArea = contour_area[max_cntr_index] # 获取最大轮廓的面积 x, y, w, h = cv2.boundingRect(max_cnt) # 获取最大轮廓的边界矩形 if not ratioCheck(max_cntArea, w, h): # 检查边界矩形的面积和宽高比 return plate, None # 如果不符合要求,返回原图像和 None final_img = thresh[y:y+h, x:x+w] # 提取边界矩形中的图像 return final_img, [x, y, w, h] # 返回清洗后的图像和轮廓信息 else: return plate, None # 如果没有找到轮廓,返回原图像和 None
-
主处理步骤:
在这一步中,我们将读取输入图像,进行高斯模糊、Sobel 梯度和形态学操作。然后在图像中找到轮廓,并遍历每个轮廓来识别车牌。最后,清洗图像轮廓并将其输入到 pytesseract 中以识别数字和字符。
img = cv2.imread("testData/sample15.jpg") # 读取输入图像 print("Number input image...") # 打印输入图像信息 cv2.imshow("input", img) # 显示输入图像 if cv2.waitKey(0) & 0xff == ord('q'): # 按下 'q' 键退出 pass img2 = cv2.GaussianBlur(img, (3, 3), 0) # 应用高斯模糊 img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) # 转换为灰度图像 img2 = cv2.Sobel(img2, cv2.CV_8U, 1, 0, ksize=3) # 计算 Sobel 梯度 _, img2 = cv2.threshold(img2, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 进行二值化处理 element = cv2.getStructuringElement(shape=cv2.MORPH_RECT, ksize=(17, 3)) # 获取结构元素 morph_img_threshold = img2.copy() # 复制二值图像 cv2.morphologyEx(src=img2, op=cv2.MORPH_CLOSE, kernel=element, dst=morph_img_threshold) # 进行闭运算 num_contours, hierarchy = cv2.findContours(morph_img_threshold, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE) # 找到轮廓 cv2.drawContours(img2, num_contours, -1, (0, 255, 0), 1) # 绘制轮廓 for i, cnt in enumerate(num_contours): # 遍历每个轮廓 min_rect = cv2.minAreaRect(cnt) # 获取最小外接矩形 if ratio_and_rotation(min_rect): # 检查轮廓的宽高比和旋转角度 x, y, w, h = cv2.boundingRect(cnt) # 获取轮廓的边界矩形 plate_img = img[y:y+h, x:x+w] # 提取车牌图像 print("Number identified number plate...") # 打印识别到的车牌信息 cv2.imshow("num plate image", plate_img) # 显示车牌图像 if cv2.waitKey(0) & 0xff == ord('q'): # 按下 'q' 键退出 pass if(isMaxWhite(plate_img)): # 检查车牌图像的平均像素值 clean_plate, rect = clean2_plate(plate_img) # 清洗车牌图像 if rect: fg = 0 x1, y1, w1, h1 = rect # 获取清洗后的轮廓信息 x, y, w, h = x + x1, y + y1, w1, h1 # 更新边界矩形 # cv2.imwrite("clena.png", clean_plate) # 保存清洗后的图像 plate_im = Image.fromarray(clean_plate) # 将清洗后的图像转换为 PIL 图像 text = tess.image_to_string(plate_im, lang='eng') # 使用 pytesseract 识别图像中的文本 print("Number Detected Plate Text : ", text) # 打印识别到的车牌文本
项目 GUI 代码
创建一个新文件 gui.py
并粘贴以下代码:
import tkinter as tk
from tkinter import filedialog
from tkinter import *
from PIL import ImageTk, Image
from tkinter import PhotoImage
import numpy as np
import cv2
import pytesseract as tess
代码总结:
这部分代码导入了项目所需的库。tkinter
用于创建 GUI 界面,filedialog
用于文件选择对话框,PIL
用于图像处理,numpy
用于数值计算,cv2
用于图像处理和计算机视觉任务,pytesseract
用于 OCR 识别。
def clean2_plate(plate):
gray_img = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) # 将输入图像转换为灰度图像
_, thresh = cv2.threshold(gray_img, 110, 255, cv2.THRESH_BINARY) # 对灰度图像进行二值化处理
num_contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # 检测图像中的轮廓
if num_contours:
contour_area = [cv2.contourArea(c) for c in num_contours] # 计算每个轮廓的面积
max_cntr_index = np.argmax(contour_area) # 找到面积最大的轮廓索引
max_cnt = num_contours[max_cntr_index] # 获取面积最大的轮廓
max_cntArea = contour_area[max_cntr_index] # 获取面积最大的轮廓的面积
x, y, w, h = cv2.boundingRect(max_cnt) # 计算最大轮廓的边界矩形
if not ratioCheck(max_cntArea, w, h): # 检查边界矩形的宽高比是否符合要求
return plate, None # 如果不符合要求,返回原图像和 None
final_img = thresh[y:y+h, x:x+w] # 裁剪出最大轮廓的区域
return final_img, [x, y, w, h] # 返回处理后的图像和边界矩形
else:
return plate, None # 如果没有检测到轮廓,返回原图像和 None
代码总结:
clean2_plate
函数用于清理和提取车牌图像。它首先将输入图像转换为灰度图像,然后进行二值化处理。接下来,检测图像中的轮廓,并找到面积最大的轮廓。计算该轮廓的边界矩形,并检查其宽高比是否符合要求。如果符合要求,返回裁剪后的图像和边界矩形;否则返回原图像和 None。
def ratioCheck(area, width, height):
ratio = float(width) / float(height) # 计算宽高比
if ratio < 1:
ratio = 1 / ratio # 如果宽高比小于 1,取其倒数
if (area < 1063.62 or area > 73862.5) or (ratio < 3 or ratio > 6): # 检查面积和宽高比是否在合理范围内
return False # 如果不在合理范围内,返回 False
return True # 如果在合理范围内,返回 True
代码总结:
ratioCheck
函数用于检查给定区域的面积和宽高比是否符合车牌的合理范围。它首先计算宽高比,如果宽高比小于 1,取其倒数。然后检查面积是否在 1063.62 和 73862.5 之间,宽高比是否在 3 和 6 之间。如果符合要求,返回 True;否则返回 False。
def isMaxWhite(plate):
avg = np.mean(plate) # 计算图像的平均像素值
if(avg >= 115):
return True # 如果平均像素值大于等于 115,返回 True
else:
return False # 否则返回 False
代码总结:
isMaxWhite
函数用于检查车牌图像是否主要为白色。它计算图像的平均像素值,如果平均像素值大于等于 115,认为图像主要为白色,返回 True;否则返回 False。
def ratio_and_rotation(rect):
(x, y), (width, height), rect_angle = rect # 解析矩形的中心点、宽度、高度和旋转角度
if width > height:
angle = -rect_angle # 如果宽度大于高度,旋转角度为负
else:
angle = 90 + rect_angle # 否则,旋转角度为 90 度加上矩形的旋转角度
if angle > 15:
return False # 如果旋转角度大于 15 度,返回 False
if height == 0 or width == 0:
return False # 如果高度或宽度为 0,返回 False
area = height * width # 计算矩形的面积
if not ratioCheck(area, width, height): # 检查矩形的宽高比是否符合要求
return False # 如果不符合要求,返回 False
else:
return True # 如果符合要求,返回 True
代码总结:
ratio_and_rotation
函数用于检查矩形的宽高比和旋转角度是否符合要求。它首先解析矩形的中心点、宽度、高度和旋转角度,根据宽度和高度的关系调整旋转角度。然后检查旋转角度是否大于 15 度,高度或宽度是否为 0。最后,调用 ratioCheck
函数检查矩形的宽高比是否在合理范围内。如果所有条件都满足,返回 True;否则返回 False。
top = tk.Tk()
top.geometry('900x700') # 设置窗口大小
top.title('Number Plate Recognition') # 设置窗口标题
top.iconphoto(True, PhotoImage(file="/home/shivam/Dataflair/Keras Projects_CIFAR/GUI/logo.png")) # 设置窗口图标
img = ImageTk.PhotoImage(Image.open("logo.png")) # 打开并加载图标图像
top.configure(background='#CDCDCD') # 设置窗口背景颜色
label = Label(top, background='#CDCDCD', font=('arial', 35, 'bold')) # 创建一个标签,用于显示识别结果
sign_image = Label(top, bd=10) # 创建一个标签,用于显示上传的图像
plate_image = Label(top, bd=10) # 创建一个标签,用于显示识别出的车牌图像
代码总结:
这部分代码用于初始化主窗口并设置其属性。创建了一个 Tk
窗口,设置了窗口的大小、标题、图标、背景颜色等。同时还创建了三个标签,分别用于显示识别结果、上传的图像和识别出的车牌图像。
def classify(file_path):
res_text = [0] # 初始化识别结果文本
res_img = [0] # 初始化识别结果图像
img = cv2.imread(file_path) # 读取图像文件
img2 = cv2.GaussianBlur(img, (3, 3), 0) # 对图像进行高斯模糊处理
img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) # 将图像转换为灰度图像
img2 = cv2.Sobel(img2, cv2.CV_8U, 1, 0, ksize=3) # 使用 Sobel 算子进行边缘检测
_, img2 = cv2.threshold(img2, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 对图像进行二值化处理
element = cv2.getStructuringElement(shape=cv2.MORPH_RECT, ksize=(17, 3)) # 获取一个矩形结构元素
morph_img_threshold = img2.copy() # 复制二值化图像
cv2.morphologyEx(src=img2, op=cv2.MORPH_CLOSE, kernel=element, dst=morph_img_threshold) # 进行形态学闭运算
num_contours, hierarchy = cv2.findContours(morph_img_threshold, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE) # 检测图像中的轮廓
cv2.drawContours(img2, num_contours, -1, (0, 255, 0), 1) # 在图像上绘制轮廓
for i, cnt in enumerate(num_contours):
min_rect = cv2.minAreaRect(cnt) # 获取最小面积矩形
if ratio_and_rotation(min_rect): # 检查矩形的宽高比和旋转角度是否符合要求
x, y, w, h = cv2.boundingRect(cnt) # 计算轮廓的边界矩形
plate_img = img[y:y+h, x:x+w] # 裁剪出边界矩形区域
print("Number identified number plate...") # 打印提示信息
res_img[0] = plate_img # 保存裁剪后的图像
cv2.imwrite("result.png", plate_img) # 将裁剪后的图像保存到文件
if isMaxWhite(plate_img): # 检查图像是否主要为白色
clean_plate, rect = clean2_plate(plate_img) # 清理车牌图像
if rect:
fg = 0
x1, y1, w1, h1 = rect # 解析边界矩形
x, y, w, h = x + x1, y + y1, w1, h1 # 调整边界矩形的位置
plate_im = Image.fromarray(clean_plate) # 将清理后的图像转换为 PIL 图像
text = tess.image_to_string(plate_im, lang='eng') # 使用 pytesseract 进行 OCR 识别
res_text[0] = text # 保存识别结果
if text:
break # 如果识别到文本,退出循环
label.configure(foreground='#011638', text=res_text[0]) # 更新标签显示识别结果
uploaded = Image.open("result.png") # 打开保存的车牌图像
im = ImageTk.PhotoImage(uploaded) # 将图像转换为 PhotoImage
plate_image.configure(image=im) # 更新标签显示车牌图像
plate_image.image = im # 保留对图像的引用,避免被垃圾回收
plate_image.pack()
plate_image.place(x=560, y=320) # 将标签放置在窗口中的指定位置
代码总结:
classify
函数用于处理上传的图像并识别车牌。首先读取图像文件,然后进行高斯模糊、灰度转换、Sobel 边缘检测和二值化处理。接下来获取一个矩形结构元素,并对二值化图像进行形态学闭运算。检测图像中的轮廓,并在图像上绘制这些轮廓。对于每个轮廓,获取最小面积矩形并检查其宽高比和旋转角度是否符合要求。如果符合要求,裁剪出轮廓区域并保存为 result.png
。然后检查图像是否主要为白色,如果主要为白色,调用 clean2_plate
函数进行清理,并使用 pytesseract
进行 OCR 识别。最后,更新标签显示识别结果和车牌图像。
def show_classify_button(file_path):
classify_b = Button(top, text="Classify Image", command=lambda: classify(file_path), padx=10, pady=5) # 创建一个“识别图像”按钮
classify_b.configure(background='#364156', foreground='white', font=('arial', 15, 'bold')) # 设置按钮样式
classify_b.place(x=490, y=550) # 将按钮放置在窗口中的指定位置
代码总结:
show_classify_button
函数用于在窗口中显示“识别图像”按钮。该按钮的点击事件会调用 classify
函数处理传入的图像文件路径。
def upload_image():
try:
file_path = filedialog.askopenfilename() # 打开文件选择对话框,获取用户选择的文件路径
uploaded = Image.open(file_path) # 打开用户选择的图像文件
uploaded.thumbnail(((top.winfo_width()/2.25), (top.winfo_height()/2.25))) # 将图像缩放到窗口大小的 1/2.25
im = ImageTk.PhotoImage(uploaded) # 将图像转换为 PhotoImage
sign_image.configure(image=im) # 更新标签显示上传的图像
sign_image.image = im # 保留对图像的引用,避免被垃圾回收
label.configure(text='') # 清空识别结果标签
show_classify_button(file_path) # 显示“识别图像”按钮
except:
pass # 如果发生异常,捕获并忽略
代码总结:
upload_image
函数用于处理用户上传的图像。打开文件选择对话框,获取用户选择的图像文件路径,然后打开并缩放图像。将缩放后的图像转换为 PhotoImage
并更新 sign_image
标签显示该图像。同时清空识别结果标签,并显示“识别图像”按钮。
upload = Button(top, text="Upload an image", command=upload_image, padx=10, pady=5) # 创建一个“上传图像”按钮
upload.configure(background='#364156', foreground='white', font=('arial', 15, 'bold')) # 设置按钮样式
upload.pack() # 将按钮添加到窗口布局
upload.place(x=210, y=550) # 将按钮放置在窗口中的指定位置
sign_image.pack() # 将上传图像标签添加到窗口布局
sign_image.place(x=70, y=200) # 将上传图像标签放置在窗口中的指定位置
label.pack() # 将识别结果标签添加到窗口布局
label.place(x=500, y=220) # 将识别结果标签放置在窗口中的指定位置
heading = Label(top, image=img) # 创建一个标题标签,显示项目图标
heading.configure(background='#CDCDCD', foreground='#364156') # 设置标题标签样式
heading.pack() # 将标题标签添加到窗口布局
top.mainloop() # 进入主事件循环,保持窗口运行
代码总结:
这部分代码用于创建和配置主窗口中的按钮和标签。创建了一个“上传图像”按钮,并设置其点击事件为 upload_image
函数。将上传图像标签、识别结果标签和标题标签添加到窗口布局,并设置它们的位置。最后,进入主事件循环,保持窗口运行。
参考资料
参考资料名称 | 链接 |
---|---|
Tkinter 官方文档 | https://docs.python.org/3/library/tkinter.html |
OpenCV 官方文档 | https://docs.opencv.org/master/ |
PIL 官方文档 | https://pillow.readthedocs.io/en/stable/ |
NumPy 官方文档 | https://numpy.org/doc/stable/ |
PyTesseract 官方文档 | https://pypi.org/project/pytesseract/ |
License Plate Recognition 研究综述 | https://www.researchgate.net/publication/332855701_A_Survey_on_License_Plate_Recognition_Systems |
OpenCV 形态学操作教程 | https://opencv-python-tutroials.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html |
OpenCV 轮廓检测教程 | https://opencv-python-tutroials.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html |
Sobel 边缘检测原理 | https://homepages.inf.ed.ac.uk/rbf/HIPR2/sobel.htm |
Gaussian Blur 高斯模糊原理 | https://towardsdatascience.com/image-processing-with-python-and-opencv-part-2-gaussian-blurring-and-sharpening-bf4cb49228e6 |
OCR 技术综述 | https://www.sciencedirect.com/science/article/pii/S1877050915006783 |
Tesseract OCR 使用教程 | https://www.geeksforgeeks.org/python-ocr-on-pdf-using-pytesseract/ |
OpenCV 二值化处理教程 | https://learnopencv.com/otsu-thresholding-explained/ |
Tkinter GUI 设计教程 | [https://realpython.com/python-gui-tkinter/](https://realpython.com/python-g |
OpenCV 官方文档 | https://docs.opencv.org/4.x/ |
Tesseract-OCR 官方文档 | https://tesseract-ocr.github.io/tessdoc/ |
Python 中的图像处理 | https://www.geeksforgeeks.org/python-digit-and-character-recognition-for-captchas-using-opencv-tesseract-and-pytesseract/ |
车牌识别教程 | https://www.jb51.net/article/192353.htm |
OpenCV 形态学操作 | https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html |
Sobel 梯度算子 | https://www.tutorialspoint.com/opencv/opencv_sobel_operator.htm |
计算机视觉入门 | https://cv-tricks.com/ |
高斯模糊详解 | https://blog.csdn.net/xiaoxiao20180204/article/details/79643369 |
车牌识别研究综述 | https://arxiv.org/abs/1903.02727 |
Pytesseract 使用教程 | https://github.com/madmaze/pytesseract-tutorial |
OpenCV 检测车牌的另一种方法 | https://www.pyimagesearch.com/2015/09/07/blur-detection-with-opencv/ |
形态学变换的原理 | https://www.mathworks.com/help/images/morphology-fundamentals-dilation-and-erosion.html |
车牌检测与识别 | https://www.sciencedirect.com/science/article/pii/S1877050920326946 |
计算机视觉中的轮廓检测 | https://www.tutorialspoint.com/contour-detection-in-computer-vision-using-python |
车牌字符识别方法 | https://ieeexplore.ieee.org/document/8923706 |
数字和字符识别 | https://www.analyticsvidhya.com/blog/2019/06/comprehensive-guide-digital-character-recognition-ocr-python/ |
车牌识别在实际应用中的挑战 | https://www.researchgate.net/publication/335230929_Challenges_and_Solutions_for_License_Plate_Recognition_in_Real-World_Applications |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)