MATLAB:车牌识别

MATLAB:车牌识别

% 使用方法:% 将本文件和车牌图片放在同一文件夹中,在 MATLAB 中运行本脚本即可。% 程序会自动扫描 jpg、jpeg、png、bmp、tif、tiff 格式图片。% ========================================================================clear; clc; close all;warning('off', 'all');% 自动切换到脚本所在文件夹,避免 MATLAB 当前路径不同导致找不到图片scriptFullPath = mfilename('fullpath');if ~isempty(scriptFullPath) scriptFolder = fileparts(scriptFullPath); if ~isempty(scriptFolder) cd(scriptFolder); endend%% ============================ 参数配置 ============================% 字符模板统一尺寸,尺寸越大匹配越精细,但运行速度会略慢param.templateHeight = 48;param.templateWidth = 28;% 车牌宽高比范围,普通蓝牌通常约为 3.0 左右,放宽范围可提高鲁棒性param.minPlateAspect = 2.0;param.maxPlateAspect = 6.5;% 候选车牌最小面积比例,用于过滤很小的噪声区域param.minPlateAreaRatio = 0.002;% 处理大图时限制最大边长,避免图像过大导致运行缓慢param.maxImageSide = 1300;% 是否显示中间处理过程,便于调试和观察算法原理param.showProcessFigure = true;% 字符数量范围,普通车牌为 7 位,新能源车牌可能为 8 位param.minCharCount = 5;param.maxCharCount = 8;fprintf('================ MATLAB车牌识别程序启动 ================\n');%% ============================ 扫描图片 ============================% 自动获取当前文件夹中的常见图片文件,保证脚本复制到任意文件夹后都能直接运行imageFiles = findImageFiles();if isempty(imageFiles) error('当前工作文件夹中没有找到车牌图片,请放入 jpg、png、bmp 或 tif 图片后再运行。');endfprintf('共找到 %d 张图片。\n', numel(imageFiles));% 建立字符模板库,后续每张图片重复使用,避免反复渲染模板浪费时间templateLib = buildTemplateLibrary(param.templateHeight, param.templateWidth);allResults = cell(numel(imageFiles), 1);%% ============================ 批量识别 ============================for imgIndex = 1:numel(imageFiles) imgFile = imageFiles{imgIndex}; fprintf('\n----------------------------------------------------------\n'); fprintf('正在处理第 %d/%d 张图片:%s\n', imgIndex, numel(imageFiles), imgFile); result = recognizeOneImage(imgFile, param, templateLib, imgIndex); allResults{imgIndex} = result; fprintf('识别完成:%s\n', result.plateNumber);end%% ============================ 输出汇总 ============================fprintf('\n======================== 识别结果汇总 ========================\n');for i = 1:numel(allResults) fprintf('%s -> %s\n', allResults{i}.fileName, allResults{i}.plateNumber);endfprintf('==============================================================\n');%% ========================================================================% 主识别函数% ========================================================================function result = recognizeOneImage(imgFile, param, templateLib, imgIndex)% 该函数负责完成单张图片的完整车牌识别流程 %% ==================== 第一步:读取图像 ==================== % imread 读取原始彩色图像,后续所有定位结果都会映射到该图像上显示 rawImage = imread(imgFile); rawImage = ensureRgbImage(rawImage); originalImage = rawImage; % 大图缩放可以降低运算量,同时保留车牌的主要结构信息 rawImage, scaleFactor = resizeLargeImage(rawImage, param.maxImageSide); fprintf('原图尺寸:%d × %d,处理缩放比例:%.3f\n', ... size(originalImage, 1), size(originalImage, 2), scaleFactor); %% ==================== 第二步:图像预处理 ==================== % 灰度化将三通道颜色信息压缩为亮度信息,便于后续边缘和二值分析 grayImage = rgb2gray(rawImage); % 自适应直方图均衡化增强局部对比度,可缓解过暗、过亮和光照不均 enhancedImage = enhanceGrayImage(grayImage); % 中值滤波抑制椒盐噪声,高斯滤波进一步平滑细小纹理干扰 denoisedImage = medfilt2(enhancedImage, 3 3); denoisedImage = gaussianSmooth(denoisedImage, 1.0); % Canny 算子对光照变化相对稳定,适合提取车牌矩形边缘和字符边缘 edgeImage = edge(denoisedImage, 'Canny'); %% ==================== 第三步:车牌区域定位 ==================== % 同时使用颜色特征和边缘形态学定位,颜色适合蓝牌、绿牌、黄牌, % 边缘方法作为颜色失效时的补充,提高不同光照和角度下的鲁棒性 plateBox, locateDebug = locatePlate(rawImage, grayImage, edgeImage, param); % 将边界框适当外扩,避免裁剪时切掉车牌边框或边缘字符 plateBox = expandBox(plateBox, size(rawImage), 0.04, 0.08); plateImage = imcrop(rawImage, plateBox); if isempty(plateImage) error('车牌裁剪失败,请检查定位框是否有效。'); end %% ==================== 第四步:车牌校正与归一化 ==================== % 通过霍夫直线估计车牌边缘倾角,轻微旋转后可让字符更接近竖直排列 plateCorrected, rotateAngle = deskewPlate(plateImage);

% 将车牌高度归一化,便于字符分割阈值在不同尺寸图片中保持稳定 targetPlateHeight = 120; plateCorrected = imresize(plateCorrected, targetPlateHeight / size(plateCorrected, 1)); fprintf('定位车牌尺寸:%d × %d,校正角度:%.2f°\n', ... size(plateCorrected, 1), size(plateCorrected, 2), rotateAngle); %% ==================== 第五步:字符分割 ==================== % 字符分割先提取白色或浅色字符区域,再通过投影曲线确定每个字符边界 charImages, charBoxes, charMask, plateForSeg = segmentPlateCharacters(plateCorrected, param); fprintf('分割字符数量:%d\n', numel(charImages)); %% ==================== 第六步:字符识别 ==================== % 第一位按省份简称识别,第二位按英文字母识别,后续按字母和数字识别 plateNumber, charLabels, charScores = recognizeCharacters(charImages, templateLib); %% ==================== 第七步:结果显示 ==================== showRecognitionResult(rawImage, grayImage, enhancedImage, edgeImage, locateDebug, ... plateBox, plateCorrected, plateForSeg, charMask, charImages, charBoxes, ... charLabels, charScores, plateNumber, imgFile, imgIndex, param.showProcessFigure); result.fileName = imgFile; result.plateNumber = plateNumber; result.plateBox = plateBox; result.charLabels = charLabels; result.charScores = charScores;end%% ========================================================================% 图片扫描与基础工具% ========================================================================function imageFiles = findImageFiles()% 扫描当前工作文件夹中的常见图片格式,并排除隐藏文件夹中的图片 exts = {'*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tif', '*.tiff'}; imageFiles = {}; for i = 1:numel(exts) files = dir(exts{i}); for k = 1:numel(files) if ~files(k).isdir imageFiles{end + 1} = files(k).name; end end end imageFiles = unique(imageFiles, 'stable');endfunction rgbImage = ensureRgbImage(inputImage)% 将灰度图或带透明通道的图像统一转换为三通道 RGB 图像 if ndims(inputImage) == 2 rgbImage = repmat(inputImage, 1 1 3); elseif size(inputImage, 3) == 4 rgbImage = inputImage(:, :, 1:3); else rgbImage = inputImage; endendfunction outImage, scaleFactor = resizeLargeImage(inImage, maxSide)% 将过大的图像按比例缩放,缩放比例保存在 scaleFactor 中 h = size(inImage, 1); w = size(inImage, 2); if max(h, w) > maxSide scaleFactor = maxSide / max(h, w); outImage = imresize(inImage, scaleFactor); else scaleFactor = 1.0; outImage = inImage; endendfunction enhanced = enhanceGrayImage(grayImage)% 增强灰度图像对比度,优先使用自适应直方图均衡化 if exist('adapthisteq', 'file') == 2 enhanced = adapthisteq(grayImage, 'ClipLimit', 0.015, 'Distribution', 'rayleigh'); else enhanced = histeq(grayImage); endendfunction smoothed = gaussianSmooth(grayImage, sigmaValue)% 高斯平滑用于削弱噪声和细碎纹理,使边缘检测更加稳定 if exist('imgaussfilt', 'file') == 2 smoothed = imgaussfilt(grayImage, sigmaValue); else kernelSize = max(3, 2 * ceil(3 * sigmaValue) + 1); kernel = fspecial('gaussian', kernelSize kernelSize, sigmaValue); smoothed = imfilter(grayImage, kernel, 'replicate'); endend%% ========================================================================% 车牌定位% ========================================================================function bestBox, debugInfo = locatePlate(rgbImage, grayImage, edgeImage, param)% 通过颜色掩膜和边缘掩膜产生候选区域,再按照车牌几何特征评分 imageSize = size(grayImage); colorMask = buildPlateColorMask(rgbImage); % 闭运算连接车牌底色区域,填洞后形成更完整的矩形候选 colorMaskClean = imclose(colorMask, strel('rectangle', 15 45)); colorMaskClean = imfill(colorMaskClean, 'holes'); colorMaskClean = bwareaopen(colorMaskClean, round(numel(grayImage) * 0.0008)); colorCandidates = collectPlateCandidates(colorMaskClean, imageSize, param, '颜色'); % 边缘方法将水平字符边缘和车牌边框连接成矩形块,适合颜色偏移或曝光异常场景 edgeMask = imclose(edgeImage, strel('rectangle', 6 36)); edgeMask = imdilate(edgeMask, strel('rectangle', 3 12)); edgeMask = imfill(edgeMask, 'holes'); edgeMask = bwareaopen(edgeMask, round(numel(grayImage) * 0.001)); edgeCandidates = collectPlateCandidates(edgeMask, imageSize, param, '边缘'); % 颜色和边缘融合后再生成一组候选,减少单一方法漏检 fusedMask = colorMaskClean | edgeMask; fusedMask = imclose(fusedMask, strel('rectangle', 8 30)); fusedMask = imfill(fusedMask, 'holes'); fusedCandidates = collectPlateCandidates(fusedMask, imageSize, param, '融合'); allCandidates = colorCandidates; edgeCandidates; fusedCandidates; if isempty(allCandidates) error('未能定位到车牌区域,请检查图片中车牌是否清晰可见。'); end scores = allCandidates.score; \~, bestIndex = max(scores); bestBox = allCandidates(bestIndex).box; fprintf('定位方式:%s,候选评分:%.3f\n', allCandidates(bestIndex).source, allCandidates(bestIndex).score); debugInfo.colorMask = colorMaskClean; debugInfo.edgeMask = edgeMask; debugInfo.fusedMask = fusedMask; debugInfo.candidates = allCandidates;endfunction mask = buildPlateColorMask(rgbImage)% 根据 HSV 颜色空间提取可能的车牌底色区域 rgbDouble = im2double(rgbImage); hsvImage = rgb2hsv(rgbDouble); h = hsvImage(:, :, 1); s = hsvImage(:, :, 2); v = hsvImage(:, :, 3); % 蓝牌范围:色相约在蓝色区间,饱和度较高,亮度不能过低 blueMask = (h > 0.52 & h < 0.72) & (s > 0.25) & (v > 0.15); % 绿牌范围:新能源车牌可能偏青绿或黄绿 greenMask = (h > 0.22 & h < 0.48) & (s > 0.20) & (v > 0.20); % 黄牌范围:大型车辆或特殊车辆可能使用黄色车牌 yellowMask = (h > 0.08 & h < 0.18) & (s > 0.25) & (v > 0.25); % 白牌或浅色牌在颜色上不稳定,因此只作为弱候选,后续还要经过几何筛选 whiteMask = (s < 0.20) & (v > 0.65); mask = blueMask | greenMask | yellowMask | whiteMask;endfunction candidates = collectPlateCandidates(binaryMask, imageSize, param, sourceName)% 从二值掩膜中提取矩形候选,并依据面积、宽高比、位置和矩形度进行评分 stats = regionprops(binaryMask, 'BoundingBox', 'Area', 'Extent', 'Solidity'); candidates = struct('box', {}, 'score', {}, 'source', {}); imageArea = imageSize(1) * imageSize(2); for i = 1:numel(stats) box = stats(i).BoundingBox; w = box(3); h = box(4); if h <= 0 || w <= 0 continue; end aspect = w / h; areaRatio = stats(i).Area / imageArea; if aspect < param.minPlateAspect || aspect > param.maxPlateAspect continue; end if areaRatio < param.minPlateAreaRatio continue; end % 宽高比越接近标准车牌,得分越高 aspectScore = exp(-abs(aspect - 3.2) / 2.0); % 矩形度和实心度越高,越像完整车牌区域 shapeScore = 0.55 * stats(i).Extent + 0.45 * stats(i).Solidity; % 面积过小通常是噪声,面积适中更可信 areaScore = min(areaRatio / 0.05, 1.0); % 车牌通常不会出现在图像最上方,给中下区域轻微加权 centerY = box(2) + box(4) / 2; positionScore = 0.7 + 0.3 * min(centerY / imageSize(1), 1); score = aspectScore * 0.40 + shapeScore * 0.30 + areaScore * 0.20 + positionScore * 0.10; candidates(end + 1).box = box; candidates(end).score = score; candidates(end).source = sourceName; endendfunction expandedBox = expandBox(box, imageSize, xRatio, yRatio)% 对定位框进行外扩,防止车牌边缘和字符被裁剪 x = box(1); y = box(2); w = box(3); h = box(4); dx = w * xRatio; dy = h * yRatio; x1 = max(1, x - dx); y1 = max(1, y - dy); x2 = min(imageSize(2), x + w + dx); y2 = min(imageSize(1), y + h + dy); expandedBox = x1, y1, x2 - x1, y2 - y1;end%% ========================================================================% 倾斜校正% ========================================================================function correctedPlate, angleValue = deskewPlate(plateImage)% 使用霍夫直线估计车牌上下边缘的倾斜角,并进行小角度旋转校正 grayPlate = rgb2gray(ensureRgbImage(plateImage)); grayPlate = enhanceGrayImage(grayPlate); edgePlate = edge(grayPlate, 'Canny'); angleValue = 0; correctedPlate = plateImage; try houghMat, thetaValues, rhoValues = hough(edgePlate); peaks = houghpeaks(houghMat, 8, 'Threshold', ceil(0.25 * max(houghMat(:)))); lines = houghlines(edgePlate, thetaValues, rhoValues, peaks, 'FillGap', 20, 'MinLength', size(edgePlate, 2) * 0.25); lineAngles = \[\]; for i = 1:numel(lines) p1 = lines(i).point1; p2 = lines(i).point2; angle = atan2d(double(p2(2) - p1(2)), double(p2(1) - p1(1))); if abs(angle) < 18 lineAngles(end + 1) = angle; end end if ~isempty(lineAngles) angleValue = median(lineAngles); if abs(angleValue) > 1.0 && abs(angleValue) < 15 correctedPlate = imrotate(plateImage, angleValue, 'bilinear', 'crop'); else angleValue = 0; end end catch angleValue = 0; correctedPlate = plateImage; endend%% ========================================================================% 字符分割% ========================================================================function charImages, charBoxes, charMask, plateForSeg = segmentPlateCharacters(plateImage, param)% 对车牌图像进行字符区域提取和单字符切分 plateImage = ensureRgbImage(plateImage); % 裁剪车牌内部区域,弱化边框、铆钉和外部装饰对字符分割的影响 plateForSeg, cropOffset = cropPlateInnerArea(plateImage); % 检测车牌底色类型,决定字符提取极性 % 蓝牌:白字深底 → 字符更亮 % 绿牌/黄牌/白牌:黑字亮底 → 字符更暗 plateType = detectPlateType(plateForSeg); fprintf('车牌底色类型:%s\n', plateType); grayPlate = rgb2gray(plateForSeg); % 两种增强:温和版保留细节,激进版增强对比度 enhancedPlate = enhanceGrayImage(grayPlate); enhancedStrong = adapthisteq(grayPlate, 'ClipLimit', 0.03, 'Distribution', 'rayleigh'); hsvPlate = rgb2hsv(im2double(plateForSeg)); s = hsvPlate(:, :, 2); v = hsvPlate(:, :, 3); grayDouble = im2double(grayPlate); enhancedDouble = im2double(enhancedPlate); brightThresh = graythresh(enhancedDouble); grayThresh = graythresh(grayDouble); % 根据车牌类型选择字符提取策略 if strcmp(plateType, 'blue') % 蓝牌白字:高亮度、低饱和度的像素是字符 colorMask = (v > 0.48 & s < 0.50); levelMask = enhancedDouble > brightThresh; levelMaskRaw = grayDouble > grayThresh; levelMaskStrong = im2double(enhancedStrong) > graythresh(im2double(enhancedStrong)); foregroundPolarity = 'bright'; else % 绿牌/黄牌/白牌黑字:低亮度像素是字符 colorMask = (v < 0.48 & s < 0.55); levelMask = enhancedDouble < brightThresh; levelMaskRaw = grayDouble < grayThresh; levelMaskStrong = im2double(enhancedStrong) < graythresh(im2double(enhancedStrong)); foregroundPolarity = 'dark'; end % 多种自适应二值化:不同灵敏度 + 增强强度 if exist('imbinarize', 'file') == 2 adaptiveMask1 = imbinarize(enhancedPlate, 'adaptive', ... 'ForegroundPolarity', foregroundPolarity, 'Sensitivity', 0.40); adaptiveMask2 = imbinarize(enhancedPlate, 'adaptive', ... 'ForegroundPolarity', foregroundPolarity, 'Sensitivity', 0.55); adaptiveMask3 = imbinarize(enhancedStrong, 'adaptive', ... 'ForegroundPolarity', foregroundPolarity, 'Sensitivity', 0.48); else adaptiveMask1 = im2bw(enhancedPlate, graythresh(enhancedPlate)); adaptiveMask2 = adaptiveMask1; adaptiveMask3 = adaptiveMask1; if strcmp(foregroundPolarity, 'dark') adaptiveMask1 = ~adaptiveMask1; adaptiveMask2 = ~adaptiveMask2; adaptiveMask3 = ~adaptiveMask3; end end % 宽阈值合并:颜色约束下尽可能收集所有字符像素 colorLooseMask = colorMask; if strcmp(plateType, 'blue') colorLooseMask = (v > 0.40) | (s < 0.60 & v > 0.40); else colorLooseMask = (v < 0.55) | (s < 0.60 & v < 0.55); end % 多源合并:用多数投票代替 OR,既保留多源鲁棒性,又显著降低噪声叠加 % 6个阈值源中至少3个为真才认为是前景,避免单个源噪声扩散到结果 levelSources = double(levelMask) + double(levelMaskRaw) ... + double(levelMaskStrong) + double(adaptiveMask1) ... + double(adaptiveMask2) + double(adaptiveMask3); levelVote = levelSources >= 3; % 颜色约束仍保留,但用 OR:只要颜色或多数投票任一为真即保留 charMask = levelVote & colorLooseMask; % 补充:颜色强匹配区域即使投票不足也保留,避免细笔画丢失 charMask = charMask | (colorMask & levelVote); % 自动极性校正:字符填充率应在 15%-50% 之间,过高说明极性反转或边框残留 fillRatio = sum(charMask(:)) / numel(charMask); fprintf('字符掩膜填充率:%.3f\n', fillRatio); if fillRatio > 0.55 || fillRatio < 0.03 charMask = ~charMask; fprintf('极性自动校正:掩膜已反转\n'); end % 去除上下边框区域,保留中间主要字符带 charMask = removeHorizontalBorder(charMask); % 形态学处理:温和闭运算,只修补字符内部细缝,不做大跨度连接 % 避免相邻字符被噪声桥接成一片(导致字符数从7降到5) charMask = bwareaopen(charMask, max(5, round(numel(charMask) * 0.0002))); charMask = imclose(charMask, strel('rectangle', 2 2)); % 小闭运算填2像素细缝 charMask = bwareaopen(charMask, max(8, round(numel(charMask) * 0.0003))); charMask = imopen(charMask, strel('rectangle', 1 1)); % 根据垂直投影寻找字符左右边界 bounds = findCharacterBoundsByProjection(charMask, param); % 如果投影法失败,则使用连通域方法作为备用方案 if size(bounds, 1) < param.minCharCount bounds = findCharacterBoundsByComponents(charMask, param); end % 对边界进行过滤、合并和数量控制,减少分隔点、螺丝和边框被识别为字符 bounds = refineCharacterBounds(bounds, charMask, param); numChars = size(bounds, 1); charImages = cell(numChars, 1); charBoxes = zeros(numChars, 4); for i = 1:numChars x1 = max(1, bounds(i, 1)); x2 = min(size(charMask, 2), bounds(i, 2)); singleMask = charMask(:, x1:x2); singleMask = cropSingleCharacter(singleMask); charImages{i} = normalizeCharacter(singleMask, param.templateHeight, param.templateWidth); charBoxes(i, :) = x1 + cropOffset(1) - 1, cropOffset(2), x2 - x1 + 1, size(plateForSeg, 1); endendfunction innerPlate, offset = cropPlateInnerArea(plateImage)% 裁剪车牌内部区域,尽量去掉外框和铆钉 h = size(plateImage, 1); w = size(plateImage, 2); top = max(1, round(h * 0.12)); bottom = min(h, round(h * 0.88)); left = max(1, round(w * 0.04)); right = min(w, round(w * 0.96)); innerPlate = plateImage(top:bottom, left:right, :); offset = left, top;endfunction plateType = detectPlateType(plateImage)% 根据车牌区域的主导底色判断车牌类型% 返回 'blue'(蓝牌白字)、'green'(绿牌黑字)、'yellow'(黄牌黑字)或 'white' hsvImage = rgb2hsv(im2double(plateImage)); h = hsvImage(:, :, 1); s = hsvImage(:, :, 2); v = hsvImage(:, :, 3); % 统计各底色像素数量 blueCount = sum(sum((h > 0.52 & h < 0.72) & (s > 0.25) & (v > 0.15))); greenCount = sum(sum((h > 0.22 & h < 0.48) & (s > 0.20) & (v > 0.20))); yellowCount = sum(sum((h > 0.08 & h < 0.18) & (s > 0.25) & (v > 0.25))); whiteCount = sum(sum((s < 0.20) & (v > 0.65))); counts = blueCount, greenCount, yellowCount, whiteCount; types = {'blue', 'green', 'yellow', 'white'}; \~, maxIdx = max(counts); plateType = types{maxIdx};endfunction cleanMask = removeHorizontalBorder(mask)% 利用水平投影去除车牌上下边框的连续亮线 h = size(mask, 1); rowProjection = sum(mask, 2); rowProjection = movmean(rowProjection, 5); thresholdValue = max(rowProjection) * 0.12; validRows = find(rowProjection > thresholdValue); cleanMask = false(size(mask)); if isempty(validRows) cleanMask = mask; return; end top = max(1, min(validRows) - round(h * 0.05)); bottom = min(h, max(validRows) + round(h * 0.05)); % 强制只保留中间字符带,进一步减少上、下边框的影响 top = max(top, round(h * 0.05)); bottom = min(bottom, round(h * 0.95)); cleanMask(top:bottom, :) = mask(top:bottom, :);endfunction bounds = findCharacterBoundsByProjection(mask, param)% 通过垂直投影曲线寻找字符块,投影高的位置表示该列存在字符笔画 colProjection = sum(mask, 1); colProjection = movmean(colProjection, 3); if max(colProjection) == 0 bounds = zeros(0, 2); return; end thresholdValue = max(colProjection) * 0.10; minWidth = max(3, round(size(mask, 2) * 0.015)); inChar = false; startCol = 1; bounds = zeros(0, 2); for col = 1:numel(colProjection) if colProjection(col) > thresholdValue && ~inChar startCol = col; inChar = true; elseif colProjection(col) <= thresholdValue && inChar endCol = col - 1; if endCol - startCol + 1 >= minWidth bounds(end + 1, :) = startCol, endCol; end inChar = false; end end if inChar endCol = numel(colProjection); if endCol - startCol + 1 >= minWidth bounds(end + 1, :) = startCol, endCol; end endendfunction bounds = findCharacterBoundsByComponents(mask, param)% 连通域方法作为备用分割方案,适合投影曲线不明显的情况 stats = regionprops(mask, 'BoundingBox', 'Area'); h = size(mask, 1); w = size(mask, 2); bounds = zeros(0, 2); for i = 1:numel(stats) box = stats(i).BoundingBox; bw = box(3); bh = box(4); area = stats(i).Area; if area < numel(mask) * 0.0005 continue; end if bh < h * 0.25 continue; end if bw < w * 0.008 || bw > w * 0.25 continue; end bounds(end + 1, :) = floor(box(1)), ceil(box(1) + box(3)); end if ~isempty(bounds) bounds = sortrows(bounds, 1); endendfunction refined = refineCharacterBounds(bounds, mask, param)% 合并断裂字符,去除过窄分隔符,并控制最终字符数量 if isempty(bounds) refined = bounds; return; end bounds = sortrows(bounds, 1); % 合并间距很小的相邻块,避免一个汉字或断裂数字被切成多个部分 % 间隙阈值取中间值:太小会漏合"8"断裂块,太大会把相邻字符合并 gaps = bounds(2:end, 1) - bounds(1:end-1, 2); if isempty(gaps) mergeGap = round(size(mask, 2) * 0.018); else mergeGap = max(round(size(mask, 2) * 0.012), round(median(gaps) * 0.45)); end merged = bounds(1, :); for i = 2:size(bounds, 1) gap = bounds(i, 1) - merged(end, 2); if gap <= mergeGap merged(end, 2) = max(merged(end, 2), bounds(i, 2)); else merged(end + 1, :) = bounds(i, :); end end bounds = merged; % 去掉紧贴车牌左右边缘的窄条,这类区域通常是边框残留而不是字符 plateWidth = size(mask, 2); widths = bounds(:, 2) - bounds(:, 1) + 1; edgeKeep = true(size(bounds, 1), 1); for i = 1:size(bounds, 1) isLeftEdgeNoise = bounds(i, 1) <= round(plateWidth * 0.015) && widths(i) < plateWidth * 0.035; isRightEdgeNoise = bounds(i, 2) >= round(plateWidth * 0.985) && widths(i) < plateWidth * 0.035; if isLeftEdgeNoise || isRightEdgeNoise edgeKeep(i) = false; end end bounds = bounds(edgeKeep, :); if isempty(bounds) refined = bounds; return; end % 根据每个候选块中的像素高度和宽度过滤螺丝、分隔点和边框残留 keep = true(size(bounds, 1), 1); heights = zeros(size(bounds, 1), 1); widths = bounds(:, 2) - bounds(:, 1) + 1; for i = 1:size(bounds, 1) x1 = max(1, bounds(i, 1)); x2 = min(size(mask, 2), bounds(i, 2)); subMask = mask(:, x1:x2); rows, \~ = find(subMask); if isempty(rows) keep(i) = false; continue; end heights(i) = max(rows) - min(rows) + 1; end medianWidth = median(widths(widths > 0)); % 过滤阈值取中间值,平衡噪声去除与细字符保留 for i = 1:size(bounds, 1) if heights(i) < size(mask, 1) * 0.25 keep(i) = false; end if widths(i) < max(3, medianWidth * 0.30) keep(i) = false; end end bounds = bounds(keep, :); % 如果仍然过多,优先保留高度较大、位置合理的候选字符 if size(bounds, 1) > param.maxCharCount scores = zeros(size(bounds, 1), 1); for i = 1:size(bounds, 1) x1 = max(1, bounds(i, 1)); x2 = min(size(mask, 2), bounds(i, 2)); subMask = mask(:, x1:x2); rows, \~ = find(subMask); if isempty(rows) scores(i) = 0; else charHeight = max(rows) - min(rows) + 1; charArea = sum(subMask(:)); scores(i) = charHeight * 0.7 + charArea * 0.3 / max(1, numel(subMask)); end end \~, order = sort(scores, 'descend'); selected = sort(order(1:param.maxCharCount)); bounds = bounds(selected, :); end refined = sortrows(bounds, 1);endfunction cropped = cropSingleCharacter(charMask)% 裁剪单个字符周围空白,使字符归一化时更居中 rows, cols = find(charMask); if isempty(rows) || isempty(cols) cropped = charMask; return; end top = max(1, min(rows) - 1); bottom = min(size(charMask, 1), max(rows) + 1); left = max(1, min(cols) - 1); right = min(size(charMask, 2), max(cols) + 1); cropped = charMask(top:bottom, left:right);endfunction normalized = normalizeCharacter(charMask, targetHeight, targetWidth)% 将字符等比例缩放并居中到统一画布,保证模板匹配时尺寸一致 charMask = logical(charMask); rows, cols = find(charMask); if isempty(rows) || isempty(cols) normalized = false(targetHeight, targetWidth); return; end charMask = charMask(min(rows):max(rows), min(cols):max(cols)); h = size(charMask, 1); w = size(charMask, 2); scale = min((targetHeight * 0.86) / h, (targetWidth * 0.82) / w); newH = max(1, round(h * scale)); newW = max(1, round(w * scale)); resized = imresize(double(charMask), newH, newW, 'bilinear'); resized = resized > 0.35; normalized = false(targetHeight, targetWidth); rowStart = floor((targetHeight - newH) / 2) + 1; colStart = floor((targetWidth - newW) / 2) + 1; normalized(rowStart:rowStart + newH - 1, colStart:colStart + newW - 1) = resized;end%% ========================================================================% 字符识别% ========================================================================function templateLib = buildTemplateLibrary(templateHeight, templateWidth)% 构建中文省份、英文字母和数字模板库 templateLib.provinces = {'京','津','沪','渝','冀','豫','云','辽','黑','湘', ... '皖','鲁','新','苏','浙','赣','鄂','桂','甘','晋', ... '蒙','陕','吉','闽','贵','粤','青','藏','川','宁','琼'}; templateLib.letters = {'A','B','C','D','E','F','G','H','J','K', ... 'L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z'}; templateLib.digits = {'0','1','2','3','4','5','6','7','8','9'}; fprintf('正在生成字符模板库,请稍候...\n'); templateLib.provinceTemplates = cell(numel(templateLib.provinces), 1); for i = 1:numel(templateLib.provinces) templateLib.provinceTemplates{i} = renderCharacterTemplate(templateLib.provinces{i}, templateHeight, templateWidth, true); end templateLib.letterTemplates = cell(numel(templateLib.letters), 1); for i = 1:numel(templateLib.letters) templateLib.letterTemplates{i} = renderCharacterTemplate(templateLib.letters{i}, templateHeight, templateWidth, false); end templateLib.digitTemplates = cell(numel(templateLib.digits), 1); for i = 1:numel(templateLib.digits) templateLib.digitTemplates{i} = renderCharacterTemplate(templateLib.digits{i}, templateHeight, templateWidth, false); end fprintf('模板库生成完成:省份 %d 个,字母 %d 个,数字 %d 个。\n', ... numel(templateLib.provinces), numel(templateLib.letters), numel(templateLib.digits));endfunction templateImages = renderCharacterTemplate(ch, targetHeight, targetWidth, isChinese)% 通过 MATLAB 图形文字渲染字符模板,再二值化为白字黑底模板% 同一个字符保留多种常见字体模板,可提高对车牌压印字体的适应能力 scale = 5; canvasH = targetHeight * scale; canvasW = targetWidth * scale; if isChinese fontCandidates = {'SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi', 'FangSong'}; fontSize = round(25 * scale); else fontCandidates = {'Arial', 'Arial Narrow', 'Calibri', 'Microsoft YaHei', 'SimHei'}; fontSize = round(31 * scale); end templateImages = {}; for fontIndex = 1:numel(fontCandidates) % 优先使用 Java 在内存中绘制字符模板;如果当前 MATLAB 的 Java % 接口不可用,则自动退回到 MATLAB 图窗绘制,保证程序不中断。 try grayFrame = renderTextByJava(ch, canvasH, canvasW, fontCandidates{fontIndex}, fontSize); catch try grayFrame = renderTextByFigure(ch, canvasH, canvasW, fontCandidates{fontIndex}, fontSize); catch % 当前字体生成失败时跳过,继续尝试下一个字体 continue; end end grayFrame = imresize(grayFrame, targetHeight targetWidth, 'bilinear'); candidateTemplate = grayFrame < 210; candidateTemplate = normalizeCharacter(candidateTemplate, targetHeight, targetWidth); % 如果模板前景像素过少,说明字体渲染失败,继续尝试其他字体 if sum(candidateTemplate(:)) > numel(candidateTemplate) * 0.02 templateImages{end + 1} = candidateTemplate; end end % 如果所有字体都渲染失败,返回空白模板,避免后续程序中断 if isempty(templateImages) templateImages = {false(targetHeight, targetWidth)}; endendfunction grayFrame = renderTextByJava(ch, canvasH, canvasW, fontName, fontSize)% 使用 Java2D 在内存画布上绘制字符,并输出灰度图像 % BufferedImage 类型 1 表示 TYPE_INT_RGB,适合绘制黑字白底图像 bufferedImage = javaObject('java.awt.image.BufferedImage', int32(canvasW), int32(canvasH), int32(1)); graphicsObj = bufferedImage.createGraphics(); % 开启抗锯齿,提升字符模板边缘质量 try rhKey = java.awt.RenderingHints.KEY_ANTIALIASING; rhVal = java.awt.RenderingHints.VALUE_ANTIALIAS_ON; graphicsObj.setRenderingHint(rhKey, rhVal); rhKey2 = java.awt.RenderingHints.KEY_TEXT_ANTIALIASING; rhVal2 = java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON; graphicsObj.setRenderingHint(rhKey2, rhVal2); catch % 部分MATLAB版本Java接口略有差异,抗锯齿设置失败不影响主流程 end % 先填充白色背景,再用黑色绘制字符 whiteColor = javaObject('java.awt.Color', int32(255), int32(255), int32(255)); blackColor = javaObject('java.awt.Color', int32(0), int32(0), int32(0)); graphicsObj.setColor(whiteColor); graphicsObj.fillRect(int32(0), int32(0), int32(canvasW), int32(canvasH)); fontObj = javaObject('java.awt.Font', char(fontName), int32(1), int32(fontSize)); graphicsObj.setFont(fontObj); metrics = graphicsObj.getFontMetrics(fontObj); textString = javaObject('java.lang.String', ch); textWidth = metrics.stringWidth(textString); textHeight = metrics.getAscent() + metrics.getDescent(); x = round((canvasW - textWidth) / 2); y = round((canvasH - textHeight) / 2 + metrics.getAscent()); graphicsObj.setColor(blackColor); graphicsObj.drawString(textString, int32(x), int32(y)); graphicsObj.dispose(); % 批量提取像素,避免逐像素创建Java Color对象导致速度极慢 grayFrame = uint8(255 * ones(canvasH, canvasW)); try % 优先使用批量 getRGB 一次性读取全部像素 numPixels = canvasW * canvasH; rgbArray = int32(zeros(numPixels, 1)); rgbArray = bufferedImage.getRGB(int32(0), int32(0), int32(canvasW), int32(canvasH), ... rgbArray, int32(0), int32(canvasW)); rgbDouble = double(rgbArray); % Java int 为有符号32位,alpha=0xFF 时值为负,需转换为无符号 rgbDouble(rgbDouble < 0) = rgbDouble(rgbDouble < 0) + 4294967296; redValues = mod(floor(rgbDouble / 65536), 256); greenValues = mod(floor(rgbDouble / 256), 256); blueValues = mod(rgbDouble, 256); grayValues = 0.299 * redValues + 0.587 * greenValues + 0.114 * blueValues; % getRGB 按行优先返回,需转置为 MATLAB 的列优先矩阵 grayFrame = uint8(reshape(grayValues, canvasW, canvasH)'); catch % 降级方案:逐像素提取并用位运算解析RGB,不创建Color对象 for row = 1:canvasH for col = 1:canvasW rgbValue = bufferedImage.getRGB(int32(col - 1), int32(row - 1)); if rgbValue < 0 rgbValue = double(rgbValue) + 4294967296; else rgbValue = double(rgbValue); end redValue = mod(floor(rgbValue / 65536), 256); greenValue = mod(floor(rgbValue / 256), 256); blueValue = mod(rgbValue, 256); grayFrame(row, col) = uint8(0.299 * redValue + 0.587 * greenValue + 0.114 * blueValue); end end endendfunction grayFrame = renderTextByFigure(ch, canvasH, canvasW, fontName, fontSize)% 当 Java 绘制失败时,使用 MATLAB 图窗方式生成字符模板% 注意:figure('Visible','off') 在部分 MATLAB 版本中不会渲染 text 对象,% 因此必须使用 Visible=on 并将图窗放到屏幕外,再通过 getframe 截取。 fig = figure('Visible', 'on', ... 'Position', -canvasW - 10, -canvasH - 10, canvasW, canvasH, ... 'MenuBar', 'none', 'ToolBar', 'none', ... 'Color', 'w', 'InvertHardcopy', 'off'); ax = axes('Parent', fig, 'Units', 'normalized', 'Position', 0 0 1 1); set(ax, 'XLim', 0 canvasW, 'YLim', 0 canvasH, 'Visible', 'off'); hold(ax, 'on'); text(ax, canvasW / 2, canvasH / 2, ch, ... 'HorizontalAlignment', 'center', ... 'VerticalAlignment', 'middle', ... 'FontSize', fontSize, ... 'FontWeight', 'bold', ... 'FontName', fontName, ... 'Color', 'k'); drawnow; pause(0.02); frame = getframe(ax); close(fig); if size(frame.cdata, 3) == 3 grayFrame = rgb2gray(frame.cdata); else grayFrame = frame.cdata; endendfunction plateNumber, labels, scores = recognizeCharacters(charImages, templateLib)% 根据字符位置选择候选集并进行模板匹配 numChars = numel(charImages); labels = cell(numChars, 1); scores = zeros(numChars, 1); for i = 1:numChars target = charImages{i}; if i == 1 candidateChars = templateLib.provinces; candidateTemplates = templateLib.provinceTemplates; elseif i == 2 candidateChars = templateLib.letters; candidateTemplates = templateLib.letterTemplates; else candidateChars = templateLib.letters, templateLib.digits; candidateTemplates = templateLib.letterTemplates; templateLib.digitTemplates; end labels{i}, scores(i) = matchCharacter(target, candidateChars, candidateTemplates); % 后处理1:G vs 0 消歧 % s7 闭运算变体会把 G 的右侧开口闭合,导致 G 被误判为 0 % 如果识别为"0"且字符右侧有开口(G/C 特征),则改为"G" if strcmp(labels{i}, '0') && i >= 3 openness = computeRightOpenness(target); if openness > 0.25 labels{i} = 'G'; scores(i) = scores(i) * 0.95; % 略降分数表示后处理修正 fprintf(' 后处理 字符 %d 右侧开口度 %.3f,"0" 修正为 "G"\n', i, openness); end end % 后处理2:粤 vs 青 消歧(省份汉字第1位) % 实测标定:粤 btRatio ≈ 0.88(下部比上部稀疏) % 青 btRatio ≈ 1.0(上下密度接近) % 如果识别为"青"但下/上比 < 0.95,则改为"粤" if i == 1 bottomRatio, topRatio, btRatio = computeDensityRatio(target); if strcmp(labels{i}, '青') fprintf(' 调试 字符 %d 下部密度 %.3f,上部密度 %.3f,下/上比 %.3f\n', ... i, bottomRatio, topRatio, btRatio); if btRatio < 0.95 labels{i} = '粤'; scores(i) = scores(i) * 0.95; fprintf(' 后处理 字符 %d 下/上比 %.3f,"青" 修正为 "粤"\n', i, btRatio); end end end fprintf('字符 %d:%s,相似度 %.3f\n', i, labels{i}, scores(i)); end if isempty(labels) plateNumber = '未识别'; else plateNumber = strjoin(labels, ''); endendfunction bestChar, bestScore = matchCharacter(target, candidateChars, candidateTemplates)% 对目标字符与候选模板逐一比较,选取得分最高的字符 bestScore = -inf; bestChar = '?'; % 预生成 target 的膨胀/腐蚀变体,循环内共用以节省时间 seDisk = strel('disk', 1, 0); seRect = strel('rectangle', 2 1); targetDil = imdilate(target, seDisk); targetEro = imerode(target, seRect); targetInv = ~target; % target:闭运算修补"8"等字符断裂的环,只用于孔洞数检测 targetClosed = imclose(target, strel('disk', 2, 0)); for i = 1:numel(candidateChars) templateGroup = candidateTemplates{i}; if ~iscell(templateGroup) templateGroup = {templateGroup}; end score = -inf; for templateIndex = 1:numel(templateGroup) template = templateGroup{templateIndex}; templateDil = imdilate(template, seDisk); templateEro = imerode(template, seRect); s1 = computeCharacterSimilarity(target, template); s2 = computeCharacterSimilarity(target, templateDil); s3 = computeCharacterSimilarity(targetDil, template); s4 = computeCharacterSimilarity(targetEro, template); s5 = computeCharacterSimilarity(target, templateEro); % 极性安全网:反转目标字符匹配 s6 = computeCharacterSimilarity(targetInv, template); % 孔洞感知匹配:用修补后的 target 比较孔洞数,专治"8"断裂成"5" s7 = computeCharacterSimilarityWithHoles(targetClosed, template);

score = max(score, s1, s2, s3, s4, s5, s6, s7); end if score > bestScore bestScore = score; bestChar = candidateChars{i}; end endendfunction score = computeCharacterSimilarity(a, b)% 综合归一化相关、交并比、前景一致率和孔洞匹配计算二值字符相似度% 权重向 IoU 倾斜,IoU 对"8"(多像素) vs "1"(少像素) 差异更敏感 a = logical(a); b = logical(b); if ~isequal(size(a), size(b)) b = imresize(double(b), size(a), 'bilinear') > 0.35; end va = double(a(:)); vb = double(b(:)); va0 = va - mean(va); vb0 = vb - mean(vb); ncc = (va0' * vb0) / (sqrt(va0' * va0) * sqrt(vb0' * vb0) + eps); intersectionValue = sum(a(:) & b(:)); unionValue = sum(a(:) | b(:)); iou = intersectionValue / (unionValue + eps); % 前景一致率:只看前景像素是否匹配,背景不加分 fgAgree = sum(a(:) & b(:)) / (sum(a(:)) + sum(b(:)) - intersectionValue + eps); % 孔洞数相似度:"8"有2个洞、"0/6/9"各1个、"1/J/A"无洞,强烈区分 holeA = countHoles(a); holeB = countHoles(b); holeSim = 1.0 - abs(holeA - holeB) / max(max(holeA, holeB), 2); score = 0.25 * ncc + 0.45 * iou + 0.20 * fgAgree + 0.10 * holeSim;endfunction score = computeCharacterSimilarityWithHoles(closedTarget, template)% 孔洞感知相似度:针对"8"等带孔洞字符断裂后误判的问题% 输入 closedTarget 是已经过闭运算修补的目标字符% 用修补后的孔洞数计算结构相似度,与正常 IoU/NCC 分开打分,避免互相干扰 a = logical(closedTarget); b = logical(template); if ~isequal(size(a), size(b)) b = imresize(double(b), size(a), 'bilinear') > 0.35; end % 用修补后的 target 重新计算 IoU(断裂环被修补后,"8"的 IoU 会提升) intersectionValue = sum(a(:) & b(:)); unionValue = sum(a(:) | b(:)); iou = intersectionValue / (unionValue + eps); % 孔洞数:修补后的 target 能正确检测到"8"的2个孔洞 holeA = countHoles(a); holeB = countHoles(b); holeSim = 1.0 - abs(holeA - holeB) / max(max(holeA, holeB), 2); % 只在孔洞结构匹配时给奖励,IoU 作为基础分 score = 0.6 * iou + 0.4 * holeSim;endfunction n = countHoles(mask)% 计算二值字符图像中的封闭孔洞数(背景的连通区域中被前景完全包围的数量)% 注意:不在函数内做闭运算修补,避免改变字符形状影响其他特征 try filled = imfill(mask, 'holes'); holePixels = filled & ~mask; if sum(holePixels(:)) == 0 n = 0; return; end stats = regionprops(holePixels, 'Area'); areas = stats.Area; areas(~isfinite(areas)) = \[\]; % 只统计面积足够大的孔洞,过滤噪声造成的微小假孔洞 n = numel(areas(areas > numel(mask) * 0.008)); catch n = 0; endendfunction r = computeRightOpenness(mask)% 计算字符右侧的开口程度,用于区分 G(开口) vs 0(闭合)% G 右侧有竖向开口,0 右侧完全闭合% 返回值 0~1,越高表示右侧越"开" mask = logical(mask); h, w = size(mask); if w < 4 || h < 4 r = 0; return; end % 取右侧 30% 区域 rightStart = max(1, round(w * 0.70)); rightBand = mask(:, rightStart:end); % 统计有多少行在右侧区域没有前景像素(即"开口") rowHasFg = any(rightBand, 2); % 开口度 = 没有前景的行占比 r = 1.0 - mean(rowHasFg);endfunction bottomRatio, topRatio, btRatio = computeDensityRatio(mask)% 计算字符上/下半部分的笔画密度,以及下/上密度比% 粤:下部"米"密集 → btRatio > 1.0% 青:上下密度接近 → btRatio ≈ 1.0 mask = logical(mask); h, w = size(mask); if h < 4 bottomRatio = 0; topRatio = 0; btRatio = 1.0; return; end % 取上半部分(0%~45%) topEnd = round(h * 0.45); topBand = mask(1:topEnd, :); topRatio = sum(topBand(:)) / numel(topBand); % 取下半部分(55%~100%) bottomStart = max(1, round(h * 0.55)); bottomBand = mask(bottomStart:end, :); bottomRatio = sum(bottomBand(:)) / numel(bottomBand); % 下/上比 btRatio = bottomRatio / (topRatio + eps);end%% ========================================================================% 结果显示% ========================================================================function showRecognitionResult(rawImage, grayImage, enhancedImage, edgeImage, locateDebug, ... plateBox, plateCorrected, plateForSeg, charMask, charImages, charBoxes, ... charLabels, charScores, plateNumber, imgFile, imgIndex, showProcessFigure)% 显示预处理、定位、车牌区域、字符分割和最终识别结果 if showProcessFigure figure('Name', '预处理与定位过程 - ' imgFile, ... 'NumberTitle', 'off', 'Position', 80 80 1400 760); subplot(2, 3, 1); imshow(rawImage); title('原始图片'); subplot(2, 3, 2); imshow(grayImage); title('灰度化结果'); subplot(2, 3, 3); imshow(enhancedImage); title('光照增强与对比度提升'); subplot(2, 3, 4); imshow(edgeImage); title('Canny边缘检测'); subplot(2, 3, 5); imshow(locateDebug.colorMask); title('颜色定位掩膜'); subplot(2, 3, 6); imshow(locateDebug.fusedMask); title('颜色与边缘融合掩膜'); end figure('Name', '车牌识别结果 - ' imgFile, ... 'NumberTitle', 'off', 'Position', 100 60 1450 820); subplot(3, 2, 1); imshow(rawImage); hold on; rectangle('Position', plateBox, 'EdgeColor', 'r', 'LineWidth', 3); title('原始图片与车牌定位框:' plateNumber, 'FontSize', 13, 'FontWeight', 'bold'); subplot(3, 2, 2); imshow(plateCorrected); title('定位并校正后的车牌区域'); subplot(3, 2, 3); imshow(plateForSeg); hold on; for i = 1:size(charBoxes, 1) rectangle('Position', charBoxes(i, :), 'EdgeColor', 'g', 'LineWidth', 2); end title('字符分割边界'); subplot(3, 2, 4); imshow(charMask); title('字符二值掩膜'); subplot(3, 2, 5 6); displaySegmentedCharacters(charImages, charLabels, charScores, plateNumber); annotation('textbox', 0.28 0.01 0.45 0.05, ... 'String', '最终识别结果:' plateNumber, ... 'HorizontalAlignment', 'center', ... 'VerticalAlignment', 'middle', ... 'FontSize', 18, ... 'FontWeight', 'bold', ... 'Color', 0.85 0.10 0.10, ... 'EdgeColor', 'none'); fprintf('结果图窗编号:%d,最终识别结果:%s\n', imgIndex, plateNumber);endfunction displaySegmentedCharacters(charImages, charLabels, charScores, plateNumber)% 在同一坐标区域中横向显示所有分割字符及识别标签 cla; axis off; hold on; numChars = numel(charImages); if numChars == 0 text(0.5, 0.5, '未分割到字符', 'HorizontalAlignment', 'center', 'FontSize', 16); return; end gap = 8; charH = size(charImages{1}, 1); charW = size(charImages{1}, 2); canvasH = charH + 42; canvasW = numChars * charW + (numChars - 1) * gap; canvas = ones(canvasH, canvasW); for i = 1:numChars x = (i - 1) * (charW + gap) + 1; canvas(1:charH, x:x + charW - 1) = 1 - double(charImages{i}); end imshow(canvas, \[\]); hold on; for i = 1:numChars xCenter = (i - 1) * (charW + gap) + charW / 2; labelText = sprintf('%s %.2f', charLabels{i}, charScores(i)); text(xCenter, charH + 18, labelText, ... 'HorizontalAlignment', 'center', ... 'FontSize', 12, ... 'FontWeight', 'bold', ... 'Color', 0.10 0.25 0.85); end title('分割字符及模板匹配结果:' plateNumber, 'FontSize', 13, 'FontWeight', 'bold');end

相关推荐
卷无止境1 小时前
用 FastAPI 撑起大文件的上传下载:从流式处理到断点续传的完整实践
后端·python·fastapi
张龙6871 小时前
别再裸调大模型了:用 60 行 Python 给 LLM 调用加上「重试 + 超时 + 降级」
python
菜冻鱼1 小时前
Python-sklearn-评估指标
开发语言·人工智能·python·机器学习·numpy·pandas·sklearn
guyiICtestsocket1 小时前
国内支持定制的手机LPDDR芯片测试座工厂多种结构
人工智能·python·智能手机
for_ever_love__2 小时前
python基础语法学习: 变量, 输入输出, 运算符
网络·python·学习
努力搬砖的咸鱼2 小时前
AI Agent测试全景图:它到底改变了什么
人工智能·python·ai·集成测试·pytest·agent·ai编程
张小殊.2 小时前
LoongForge TAOT 训练方案,解决MoE EP不均衡问题
人工智能·python·深度学习·机器学习·ai
玫幽倩3 小时前
2026黄河流域公安院校-电子物证单项赛(程序逆向分析+服务器取证)
运维·服务器·python·电子取证·逆向·程序分析·服务器取证