纯前端实现视频转音频:FileReader、decodeAudioData与OfflineAudioContext实战
1. 浏览器里把视频拆成音频到底难在哪很多人第一次听到前端视频转音频这个需求第一反应是这不就是调个接口的事吗实际上浏览器原生并没有提供一个叫video to audio的按钮。你要做的事情本质上是把视频容器里的音频轨道抽出来再重新编码成一个独立的音频文件。这个过程涉及三个核心环节读取文件、解码音频数据、重新渲染并导出。我最早接触这个需求是帮一个做播客的朋友处理一批采访素材。他手里有一堆手机拍的视频想把里面的声音单独提取出来做音频节目。当时第一想法是用服务端处理但素材涉及隐私上传到服务器不太合适。于是就有了这个纯前端方案用户选一个视频文件浏览器本地完成全部转换数据不出设备。这个方案适合谁如果你在做在线教育工具、播客剪辑辅助、语音笔记整理或者任何需要视频里扒音频的场景这套思路都能直接复用。技术栈不复杂核心就是三个APIFileReader、decodeAudioData、OfflineAudioContext。但真正跑通并且稳定输出中间有不少细节值得聊。提示本文所有代码和思路均基于浏览器原生能力不依赖任何第三方云服务数据全程在本地内存中处理。2. 三个核心API各自扮演什么角色2.1 FileReader把用户选的文件变成可操作的二进制数据用户通过input typefile选中的文件在JavaScript里是一个File对象。这个对象本身不能直接丢给音频解码器你需要先把它读成ArrayBuffer。FileReader就是干这个的。function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload () resolve(reader.result); reader.onerror () reject(reader.error); reader.readAsArrayBuffer(file); }); }这里有个容易忽略的点readAsArrayBuffer是异步的而且对于大文件比如超过500MB的视频读取过程会占用不少内存。我实测过一个800MB的录屏文件在16GB内存的机器上读取没问题但在一些低配设备上会明显卡顿。所以如果你的场景里文件普遍很大建议加一个文件大小提示或者考虑分片读取。另外FileReader的onload回调里拿到的result就是完整的ArrayBuffer。这个ArrayBuffer是后续所有操作的起点它代表视频文件的原始字节流。2.2 decodeAudioData从视频容器里捞出音频轨道拿到ArrayBuffer之后下一步是解码。这里用的是AudioContext的decodeAudioData方法。它的作用是接收一段音频文件的二进制数据返回一个AudioBuffer对象里面包含了PCM格式的音频采样数据。async function decodeAudio(arrayBuffer) { const audioContext new AudioContext(); const audioBuffer await audioContext.decodeAudioData(arrayBuffer); return audioBuffer; }关键问题来了decodeAudioData能直接处理视频文件吗答案是——取决于浏览器和视频编码格式。在Chrome和Edge上如果视频容器是MP4音频轨道是AAC编码decodeAudioData通常能直接解码出音频。但如果是WebM容器里的Opus音频或者某些特殊编码就可能报错。我踩过的坑有一次处理一批MOV格式的视频decodeAudioData直接抛了一个EncodingError。后来发现是音频轨道用了PCM编码浏览器不支持。解决办法是先让用户用工具转成MP4或者在前端加一个格式检测提示。注意decodeAudioData是异步的而且会消耗CPU。对于长视频解码过程可能需要几秒到几十秒建议加一个加载状态提示。2.3 OfflineAudioContext不发声的渲染工厂拿到AudioBuffer之后你已经有音频数据了。但AudioBuffer是内存里的PCM数据不能直接下载成文件。你需要把它渲染成一个标准的音频文件格式比如WAV。这时候OfflineAudioContext就派上用场了。它和普通的AudioContext不同不会连接扬声器而是在后台快速渲染音频。你可以把它理解成一个离线渲染工厂输入音频数据输出渲染后的结果。async function renderToWav(audioBuffer) { const offlineContext new OfflineAudioContext( audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate ); const source offlineContext.createBufferSource(); source.buffer audioBuffer; source.connect(offlineContext.destination); source.start(0); const renderedBuffer await offlineContext.startRendering(); return renderedBuffer; }这里有个细节OfflineAudioContext的构造函数需要三个参数——声道数、采样帧数、采样率。这三个参数必须和原始AudioBuffer一致否则渲染出来的音频会变调或者时长不对。渲染完成后你拿到的是另一个AudioBuffer。接下来需要把它编码成WAV格式的二进制数据才能触发下载。3. 从AudioBuffer到可下载的WAV文件3.1 WAV格式的头部结构WAV文件本质上是一个RIFF容器里面包含一段PCM数据。它的头部有固定的44个字节记录了采样率、声道数、位深等信息。你需要手动构造这个头部然后把PCM数据拼接在后面。function audioBufferToWav(audioBuffer) { const numberOfChannels audioBuffer.numberOfChannels; const sampleRate audioBuffer.sampleRate; const format 1; // PCM const bitDepth 16; let result; if (numberOfChannels 2) { result interleave(audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)); } else { result audioBuffer.getChannelData(0); } const dataLength result.length * (bitDepth / 8); const buffer new ArrayBuffer(44 dataLength); const view new DataView(buffer); writeString(view, 0, RIFF); view.setUint32(4, 36 dataLength, true); writeString(view, 8, WAVE); writeString(view, 12, fmt ); view.setUint32(16, 16, true); view.setUint16(20, format, true); view.setUint16(22, numberOfChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numberOfChannels * (bitDepth / 8), true); view.setUint16(32, numberOfChannels * (bitDepth / 8), true); view.setUint16(34, bitDepth, true); writeString(view, 36, data); view.setUint32(40, dataLength, true); floatTo16BitPCM(view, 44, result); return buffer; }这段代码看起来有点长但逻辑很直接先写44字节的头部再把浮点采样数据转成16位整数最后拼接在一起。3.2 浮点转16位整数的细节AudioBuffer里的采样数据是32位浮点数范围在-1到1之间。WAV的16位PCM需要把它映射到-32768到32767的整数范围。function floatTo16BitPCM(output, offset, input) { for (let i 0; i input.length; i, offset 2) { const s Math.max(-1, Math.min(1, input[i])); output.setInt16(offset, s 0 ? s * 0x8000 : s * 0x7FFF, true); } }这里有个小技巧先做Math.max(-1, Math.min(1, ...))的钳位操作防止某些异常采样值超出范围导致爆音。然后负数乘以0x8000正数乘以0x7FFF这样能保证映射的对称性。3.3 触发下载的两种方式生成WAV的ArrayBuffer之后你需要把它变成一个Blob然后创建一个临时的a标签触发下载。function downloadWav(wavBuffer, filename) { const blob new Blob([wavBuffer], { type: audio/wav }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename || audio.wav; a.click(); URL.revokeObjectURL(url); }另一种方式是使用navigator.share或者navigator.clipboard但兼容性不如直接下载。我一般用下载方式简单可靠。提示URL.revokeObjectURL一定要在下载触发后调用否则会造成内存泄漏。虽然现代浏览器会在页面关闭时自动清理但手动释放是好习惯。4. 完整流程串起来从选文件到下载音频4.1 主流程代码把前面的步骤串起来整个转换流程大概是这样的async function convertVideoToAudio(file) { // 1. 读取文件 const arrayBuffer await readFileAsArrayBuffer(file); // 2. 解码音频 const audioContext new AudioContext(); const audioBuffer await audioContext.decodeAudioData(arrayBuffer); // 3. 离线渲染 const offlineContext new OfflineAudioContext( audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate ); const source offlineContext.createBufferSource(); source.buffer audioBuffer; source.connect(offlineContext.destination); source.start(0); const renderedBuffer await offlineContext.startRendering(); // 4. 编码WAV const wavBuffer audioBufferToWav(renderedBuffer); // 5. 下载 downloadWav(wavBuffer, file.name.replace(/\.[^.]$/, .wav)); }这段代码在Chrome上跑通没问题但有几个地方需要根据实际情况调整。4.2 处理大文件的内存问题如果视频文件很大decodeAudioData解码出来的AudioBuffer会占用大量内存。一个10分钟、44.1kHz、立体声的音频PCM数据大约是100MB左右。如果视频更长内存占用会线性增长。我的做法是在解码前先检查文件大小超过一定阈值比如500MB就提示用户文件较大转换可能需要较长时间。另外转换完成后及时释放AudioContext和OfflineAudioContext调用close()方法。await audioContext.close();4.3 兼容性处理不同浏览器对decodeAudioData的支持程度不一样。Chrome和Edge支持得最好Firefox次之Safari在某些编码上会有限制。我一般会在页面加载时检测AudioContext是否存在如果不存在就提示用户升级浏览器。if (!window.AudioContext !window.webkitAudioContext) { alert(当前浏览器不支持音频处理请使用最新版Chrome或Edge); }另外OfflineAudioContext在Safari上需要加webkit前缀虽然现在大部分现代浏览器都支持标准写法但为了保险可以做一个兼容处理。5. 实测中遇到的几个典型问题5.1 decodeAudioData报EncodingError这是最常见的问题。原因通常是视频的音频编码格式浏览器不支持。比如某些MOV文件用PCM音频或者某些MP4用AC-3编码。解决办法有两个一是提示用户先转成标准MP4H.264 AAC二是在前端尝试用MediaElementSource播放视频然后通过MediaRecorder录制音频流。第二种方案更复杂但兼容性更好。思路是创建一个隐藏的video元素把视频文件通过URL.createObjectURL加载进去然后播放视频用MediaRecorder录制captureStream()出来的音频轨道。不过这个方案需要视频实际播放一遍耗时较长而且有些浏览器对captureStream的支持也不完善。我一般优先用decodeAudioData方案失败后再降级到录制方案。5.2 转换后的音频时长不对这个问题通常是因为OfflineAudioContext的采样率和原始AudioBuffer不一致。比如原始音频是48kHz但OfflineAudioContext默认用了44.1kHz渲染出来的音频就会变调或者时长变化。解决办法很简单构造OfflineAudioContext时三个参数都从audioBuffer里取。const offlineContext new OfflineAudioContext( audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate );5.3 大文件转换时页面卡死decodeAudioData和startRendering都是计算密集型操作在主线程上执行会阻塞UI。对于长视频页面可能会卡住几秒甚至十几秒。我的做法是在转换开始前显示一个加载动画然后用setTimeout把转换逻辑放到下一个事件循环里让浏览器有机会先渲染加载状态。showLoading(); setTimeout(async () { try { await convertVideoToAudio(file); } finally { hideLoading(); } }, 100);如果对性能要求更高可以考虑用Web Worker把解码和渲染放到后台线程。不过AudioContext和OfflineAudioContext在Worker里的支持还不完善需要额外处理。6. 几个能直接抄的优化技巧6.1 用OfflineAudioContext做音频裁剪如果你只需要视频里某一段的音频可以在OfflineAudioContext里控制source.start和source.stop的时间。source.start(0, startOffset, duration);这样渲染出来的音频就只包含指定时间段的内容不用先转完整音频再裁剪。6.2 批量转换时的队列管理如果需要处理多个文件建议用一个简单的队列逐个转换避免同时解码多个大文件导致内存爆掉。const queue []; let isProcessing false; async function enqueue(file) { queue.push(file); if (!isProcessing) { processQueue(); } } async function processQueue() { isProcessing true; while (queue.length 0) { const file queue.shift(); await convertVideoToAudio(file); } isProcessing false; }6.3 输出格式的选择WAV是无损格式但文件体积大。一个10分钟的立体声WAV大约100MB。如果对体积有要求可以考虑用MediaRecorder输出WebM或MP3。不过MediaRecorder需要实时录制不能像OfflineAudioContext那样离线快速渲染。我的建议是如果用户只是临时提取音频WAV够用如果要长期存储或分享可以在前端用lamejs这类库把WAV转成MP3体积能缩小到十分之一左右。注意lamejs是一个纯JavaScript的MP3编码库引入后会增加页面体积但不需要服务端参与适合对隐私要求高的场景。7. 这套方案适合什么场景不适合什么场景7.1 适合的场景用户需要快速从视频里提取音频且不想上传到服务器视频文件不大几百MB以内浏览器内存足够视频音频编码是浏览器支持的标准格式AAC、MP3、Opus等对音频质量要求不高WAV或MP3都能接受7.2 不适合的场景视频文件超过1GB浏览器内存容易爆视频音频编码特殊decodeAudioData无法解码需要批量处理大量文件前端性能瓶颈明显需要输出专业级音频格式如FLAC、AAC浏览器原生不支持如果遇到不适合的场景可以考虑混合方案前端做初步处理复杂编码交给服务端。但那就超出纯前端的范畴了。7.3 一个实际项目的取舍我之前做过一个在线课程工具老师上传录屏视频系统自动提取音频生成文字稿。最初想用纯前端方案但后来发现老师的录屏文件普遍在1GB以上而且有些用专业软件录制音频编码不标准。最后改成了前端只做文件预览和格式检测实际转换交给服务端。前端这套FileReader decodeAudioData OfflineAudioContext的方案保留作为小文件快速处理的备选路径。所以技术选型没有绝对的好坏关键看你的实际场景。如果文件小、格式标准、隐私要求高纯前端方案很香如果文件大、格式杂、需要批量处理服务端更稳妥。8. 关于音频采样和位深的一点补充8.1 采样率决定了什么采样率是指每秒采集多少个音频样本。常见的44.1kHz意味着每秒采集44100个点。采样率越高能保留的高频信息越多但文件也越大。人耳的听觉范围大约在20Hz到20kHz之间根据奈奎斯特采样定理采样率至少要是最高频率的两倍所以44.1kHz足够覆盖人耳范围。在OfflineAudioContext里采样率必须和原始AudioBuffer一致否则会出现变调。如果你确实需要改变采样率比如从48kHz降到44.1kHz需要在渲染后做重采样处理这比较复杂一般不建议在前端做。8.2 位深对音质的影响位深决定了每个采样点的精度。16位位深意味着每个采样点用16个二进制位表示动态范围大约是96dB。24位位深的动态范围大约是144dB适合专业录音场景。在audioBufferToWav函数里我用了16位位深因为这是WAV最通用的格式兼容性最好。如果你需要更高精度可以把bitDepth改成24或32但对应的floatTo16BitPCM函数也要调整而且文件体积会翻倍。8.3 声道数的处理AudioBuffer的numberOfChannels属性告诉你音频是单声道还是立体声。在编码WAV时需要根据声道数决定如何交错采样数据。立体声需要把左右声道的采样交替排列单声道直接顺序排列即可。function interleave(left, right) { const length left.length right.length; const result new Float32Array(length); let index 0; for (let i 0; i left.length; i) { result[index] left[i]; result[index] right[i]; } return result; }这段代码把左右两个声道的数据交错合并成一个数组供WAV编码使用。9. 最后聊几句实际使用中的体会这套方案我在几个小工具里都用过整体稳定性不错但有几个点值得反复提醒自己。第一永远不要假设用户的视频格式是标准的一定要加错误处理和降级方案。第二大文件的内存管理很重要及时释放AudioContext和Blob URL能避免很多莫名其妙的问题。第三加载状态一定要有哪怕转换只需要两秒用户也需要知道系统在工作。还有一个容易被忽略的点decodeAudioData在不同浏览器上的行为差异。Chrome允许直接解码MP4里的AAC音频但Firefox在某些版本上会要求先提取音频轨道。如果你的用户群体浏览器分布比较广建议在页面里加一个简单的格式检测提前告诉用户哪些格式支持得最好。另外如果你打算把这个功能集成到现有产品里建议把转换逻辑封装成一个独立的模块输入File对象输出Blob。这样无论是用在前端页面还是Electron应用里都能直接复用。我现在的做法是把它写成一个单独的video-to-audio.js文件通过export暴露一个convert函数调用方只需要关心输入和输出不用管内部实现。这套东西不复杂但细节不少。希望这些经验能帮你少踩几个坑。

相关新闻

知道却做不到?从道德认知到行动实践的自我训练指南

知道却做不到?从道德认知到行动实践的自我训练指南

我们总在某个深夜突然想明白一件"对的事":该向某个人道歉,该拒绝某个无理的要求,该承认自己错了,该为一件不关己的事站出来。但天亮之后,我们往往什么都没做。问题很少出在"不知道什么是对的"&…

2026/10/17 3:32:53 阅读更多 →
YOLOv8光伏EL检测实战:从PVEL-AD数据集到模型部署

YOLOv8光伏EL检测实战:从PVEL-AD数据集到模型部署

光伏EL(电致发光)检测,是组件出厂前绕不开的一道硬门槛。通电后的电池片会在近红外波段发出特定波长的光,缺陷区域和正常区域在亮度、纹理上有明显差异,但因为整片电池面积大、缺陷形态杂、背景干扰多,纯人…

2026/10/17 3:32:53 阅读更多 →
Java后端音频流代理转发实战:POST拉流与流式响应

Java后端音频流代理转发实战:POST拉流与流式响应

1. 音频流代理转发的整体架构设计音频流从第三方服务到前端浏览器,中间隔着服务端这一层。很多刚接触这个需求的开发者第一反应是“让前端直接去请求那个音频地址不就行了”,但实际项目中往往行不通——第三方接口需要鉴权、跨域限制、签名校验、或者干脆…

2026/10/17 3:32:53 阅读更多 →

最新新闻

世界模型创建流程全解析:从任务定义到下游部署

世界模型创建流程全解析:从任务定义到下游部署

我第一次把一个完整的world_model跑通,是在一个带随机障碍物的二维导航任务里。训练的损失曲线说实话并不惊艳,重建出来的画面也有点糊。但真正震撼我的时刻,是闭掉真实观测之后,模型自己在潜空间里预测出来的那几十步轨迹&#x…

2026/10/17 5:44:29 阅读更多 →
XML自动生成工具:从手写拼接转向序列化库与配置驱动

XML自动生成工具:从手写拼接转向序列化库与配置驱动

简介:这是一款面向Java后端开发与数据集成人员的XML文件自动生成工具,核心解决从数据库快速导出结构化XML文档、避免手工编写易出错的问题。工具基于JDBC连接数据源,通过加载驱动、建立连接、执行SQL查询并遍历结果集,将每行数据映…

2026/10/17 5:44:29 阅读更多 →
Allegro DRC报错类型详解:PCB设计工程师的排查指南

Allegro DRC报错类型详解:PCB设计工程师的排查指南

PCB设计评审时,Allegro的DRC报错经常是整块板子上最扎眼的东西。看到满屏的红、白、绿色标记,很多人第一反应是“完了,设计有问题”,但老工程师不会急着改图,而是先看报错类型:哪个规则被触发了、对象是什么…

2026/10/17 5:44:29 阅读更多 →
结构光三维重建:散斑、多频与相移格雷码选型全解析

结构光三维重建:散斑、多频与相移格雷码选型全解析

做三维重建、机器人视觉定位或者工业检测的人,大概率都绕不开“结构光”这个词。散斑结构光、多频结构光、相移加格雷码,这三套方案几乎是市面上结构光相机和3D扫描仪的三条主流技术路线。我经常被问到同一个问题:这三种到底选哪个&#xff1…

2026/10/17 5:44:29 阅读更多 →
Docker部署ELK日志系统全流程实战

Docker部署ELK日志系统全流程实战

最近业务日志量上来了,接触ELK的朋友越来越多。我见过不少人折腾这套东西,最有意思的是:他们往往不是被Elasticsearch的查询语法难倒的,而是卡在最开始——怎么把三个组件干干净净地跑起来。本机装个单机版ES倒是简单,…

2026/10/17 5:44:29 阅读更多 →
基于MP2642GR-Z与TM4C129LNCZAD的双串电池主动均衡方案设计

基于MP2642GR-Z与TM4C129LNCZAD的双串电池主动均衡方案设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 5:43:29 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →