当前位置: 首页 > news >正文

vue分片上传视频并转换为m3u8文件并播放

开发环境:

基于若依开源框架的前后端分离版本的实践,后端java的springboot,前端若依的vue2,做一个分片上传视频并分段播放的功能,因为是小项目,并没有专门准备文件服务器和CDN服务,后端也是套用的若依的上传功能

实现思路:

  • 前端根据视频文件计算出文件md5值
  • 前端按照指定大小截取视频,执行分片上传(可优化,先使用文件MD5检查文件是否已上传)
  • 后端实现接收分片的接口,当已上传分片数等于总分片数时执行合并分片,得到原视频文件
  • 后端使用ffmpeg按照时间进行视频分割,切割时间根据视频清晰度不同而不同,得到m3u8文件和ts文件列表
  • 后端保存视频信息和文件实际保存地址,并提供查询接口
  • 前端使用流播放器播放视频文件

代码实现

1. vue的分片上传

前端分片上传功能按照以下步骤实现:

1.1,先要写一个上传组件,这里使用elementUI的上传组件

:auto-upload 设置的视频直接不解释上传,即选择好本地文件就上传
:before-upload 中需要计算好文件的md5值,然后去后端查看文件是否已被上传
:http-request 中实现具体的分片上传逻辑
:action 虽然设置了上传地址,但是任然是以http-request设置的方法为准,只是不设置会报错

<el-form-item label="视频文件" prop="file" v-if="form.id==null"><el-upload ref="upload":action="uploadUrl":on-error="onError":before-upload="beforeUpload":before-remove="beforeRemove":auto-upload="true":limit="1":http-request="chunkedUpload":on-progress="onProgress"><div style="border: 1px dashed #c0ccda;padding: 1rem;"><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div></div><div class="el-upload__tip" slot="tip">只能上传mp4文件,且不超过500M</div><el-progress :percentage="uploadPercentage" status="success"></el-progress></el-upload>
</el-form-item>

1.2,上传方法的js

我使用了两个后端接口,
一个是 testUploadVideo 判断文件是否存在,是若依分装的请求
一个是 process.env.VUE_APP_BASE_API + ‘/manage/video/upload’,单独用axios执行上传分片

在这里插入图片描述

<script>
import { addVideo, getVideo, testUploadVideo, updateVideo } from '@/api/manage/video'
import SparkMD5 from 'spark-md5'
import axios from 'axios'export default {name: 'videoWin',data() {return {uploadUrl: process.env.VUE_APP_BASE_API + '/manage/video/upload', //文件上传的路径uploadPromises: [], // 记录并发上传分片的线程uploadPercentage:0 //上传进度}},,methods: {beforeUpload: async function(file) {// 在上传之前获取视频的宽高和分辨率const video = document.createElement('video')video.src = URL.createObjectURL(file)video.preload = 'metadata'const loadedMetadata = new Promise(resolve => {video.onloadedmetadata = () => {window.URL.revokeObjectURL(video.src)const width = video.videoWidthconst height = video.videoHeightconsole.log('视频宽高:', width, height)this.form.width = widththis.form.height = heightresolve();}});// 等待视频的宽高和分辨率获取完成await loadedMetadata;// 计算文件的md5值const reader = new FileReader()const md5Promise = new Promise(resolve => {reader.onload = () => {const spark = new SparkMD5.ArrayBuffer()spark.append(reader.result)const md5 = spark.end(false)this.form.identifier = md5 // 将MD5值存储到form中resolve(md5);}});reader.readAsArrayBuffer(file); // 读取文件内容并计算MD5值const md5 = await md5Promise;// 检查文件是否已被上传const response = await testUploadVideo(md5);console.log("判断文件是否存在", response)if (response.msg === "文件已存在,秒传成功") {console.log("文件已存在")// 取消上传this.$refs.upload.abort(file);return false;} else {return true;}},chunkedUpload({ file }) {const totalSize = file.sizeconst chunkCount = Math.ceil(totalSize / (5 * 1024 * 1024)) // 每个分片5MB// 创建分片上传请求数组// 上传分片for (let i = 0; i < chunkCount; i++) {const start = i * (5 * 1024 * 1024)const end = Math.min((i + 1) * (5 * 1024 * 1024), totalSize)const chunk = file.slice(start, end)const formData = new FormData()formData.append('file', chunk)formData.append('filename', file.name)formData.append('totalChunks', chunkCount)formData.append('chunkNumber', i)formData.append('identifier', this.form.identifier) // 添加文件的MD5值作为参数// 发送分片上传请求const source = axios.CancelToken.source() // 创建cancelTokenconst uploadPromise = this.uploadChunk(formData, source.token, (progressEvent) => {console.log('更新进度', progressEvent)this.uploadPercentage = Math.round((progressEvent.loaded / progressEvent.total) * 100) // 更新进度条的值;}).catch(error => {console.error('分片上传失败', error)// 弹出告警消息this.$message({type: 'error',message: '视频上传失败!'})})this.uploadPromises.push({ promise: uploadPromise, source }) // 保存cancelToken}// 等待所有分片上传完成return Promise.all(this.uploadPromises).then(responses => {console.log('分片上传完成', responses)}).catch(error => {console.error('分片上传失败', error)})},/**更新进度*/onProgress(event, file) {this.uploadPercentage = Math.floor((event.loaded / event.total) * 100);},/**上传分片*/uploadChunk(formData, onProgress) {return axios.post(process.env.VUE_APP_BASE_API + '/manage/video/upload', formData, {onUploadProgress: onProgress // 添加进度回调}).then(response => {console.log('分片上传成功', response.data)})},/**上传分片失败*/onError(error, file, fileList) {console.error('上传失败', error)},// 取消上传请求beforeRemove(file, fileList) {this.form.identifier = nullreturn true}}
}
</script>

2. 后端接口实现

2.1 控制层代码

@RestController
@RequestMapping("/manage/video")
@CrossOrigin // 允许跨域
public class ManageVideoController extends BaseController {@Autowiredprivate IManageVideoService manageVideoService;/*** 上传分片前校验文件是否存在** @return*/@GetMapping("/preUpload")public AjaxResult preUpload(@RequestParam("fileMd5") String fileMd5) {return manageVideoService.checkExists(fileMd5);}/*** 上传分片** @return*/@PostMapping("/upload")public AjaxResult fragmentation(@ModelAttribute UploadPO uploadPO) {return manageVideoService.uploadChunk(uploadPO);}
}

2.1 服务层代码

接收到分片上传文件后经历以下步骤:

  1. 再次校验是否文件已存在,不存在就保存临时分片文件;
  2. 校验已上传分片数是否等于总分篇数,如果是则合并;
  3. 将临时文件合并和源mp4文件;
  4. 获取视频的时长和大小,因为ffmpeg不支持按照大小拆分,如果只是按照固定时长拆分,20s可能是2M也可能是34M,无法达到拆分视频以缩短预览视频等待时间的目的;
  5. 执行视频拆分,生成playlist.m3u8和一系列ts文件
  6. 重写m3u8文件的ts地址,1是因为若依开发环境和线上环境的指定前缀不一致,2是因为本地开发没开nginx转发静态资源,线上也没开文件服务
@Overridepublic AjaxResult checkExists(String fileMd5) {String fileUploadDir = RuoYiConfig.getProfile() + "/video";//判断文件是否已被上传String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";File file = new File(videoFile);if (file.exists()) {return AjaxResult.success("文件已存在,秒传成功");}return AjaxResult.success();}@Overridepublic AjaxResult uploadChunk(UploadPO uploadPO) {String fileUploadTempDir = RuoYiConfig.getProfile() + "/videotmp";String fileUploadDir = RuoYiConfig.getProfile() + "/video";// 获得文件分片数据MultipartFile fileData = uploadPO.getFile();// 分片第几片int index = uploadPO.getChunkNumber();//总分片数int totalChunk = uploadPO.getTotalChunks();// 文件md5标识String fileMd5 = uploadPO.getIdentifier();//判断文件是否已被上传String videoFile = fileUploadDir + "/" + fileMd5 + ".mp4";File file = new File(videoFile);if (file.exists()) {return AjaxResult.success("文件已存在,秒传成功");}String newName = fileMd5 + index + ".tem";File uploadFile = new File(fileUploadTempDir + "/" + fileMd5, newName);if (!uploadFile.getParentFile().exists()) {uploadFile.getParentFile().mkdirs();}try {fileData.transferTo(uploadFile);// 判断总分片数是否等于当前目录下的分片文件数量int currentChunkCount = getChunkCount(fileUploadTempDir + "/" + fileMd5);if (totalChunk == currentChunkCount) {// 调用合并方法merge(fileMd5, fileUploadTempDir, fileUploadDir);//根据运行环境分别调用ffmpegString os = System.getProperty("os.name").toLowerCase();String m3u8Dir = fileUploadDir + "/" + fileMd5;File m3u8FileDir = new File(m3u8Dir);if (!m3u8FileDir.exists()) {m3u8FileDir.mkdirs();}//计算视频总时长和视频大小,确定视频的分段时长String mp4File = fileUploadDir + "/" + fileMd5 + ".mp4";//每个2M分片的毫秒数long duration = getTsDuration(mp4File);// 异步执行视频拆分if (os.contains("win")) {mp4ToM3u8ForWindow(fileMd5, mp4File, m3u8Dir, duration);} else {mp4ToM3u8ForLinux(fileMd5, mp4File, m3u8Dir, duration);}}//执行成功返回 urlreturn AjaxResult.success();} catch (IOException | InterruptedException e) {log.error("上传视频失败:{}", e.toString());FileUtil.del(fileUploadTempDir + "/" + fileMd5); //删除临时文件FileUtil.del(videoFile); //删除视频源文件FileUtil.del(fileUploadDir + "/" + fileMd5); //删除分段ts视频return AjaxResult.error(502, "上传视频失败");} catch (EncoderException e) {log.error("视频切割时计算分段时长失败:{}", e.toString());FileUtil.del(fileUploadTempDir + "/" + fileMd5); //删除临时文件FileUtil.del(videoFile); //删除视频源文件FileUtil.del(fileUploadDir + "/" + fileMd5); //删除分段ts视频return AjaxResult.error(502, "上传视频失败");}}/*** 获取当前目录下的分片文件数量** @param directoryPath* @return*/private int getChunkCount(String directoryPath) {File directory = new File(directoryPath);if (!directory.exists() || !directory.isDirectory()) {return 0;}File[] files = directory.listFiles((dir, name) -> name.endsWith(".tem"));return files != null ? files.length : 0;}/*** 合并分片** @param uuid* @return*/public void merge(String uuid, String fileUploadTempDir, String fileUploadDir) throws IOException {File dirFile = new File(fileUploadTempDir + "/" + uuid);//分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)String[] fileNames = dirFile.list();Arrays.sort(fileNames, (o1, o2) -> {int i1 = Integer.parseInt(o1.substring(o1.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);int i2 = Integer.parseInt(o2.substring(o2.indexOf(uuid) + uuid.length()).split("\\.tem")[0]);return i1 - i2;});//创建空的合并文件,以未见md5为文件名File targetFile = new File(fileUploadDir, uuid + ".mp4");if (!targetFile.getParentFile().exists()) {targetFile.getParentFile().mkdirs();}RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");long position = 0;for (String fileName : fileNames) {System.out.println(fileName);File sourceFile = new File(fileUploadTempDir + "/" + uuid, fileName);RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");int chunksize = 1024 * 3;byte[] buf = new byte[chunksize];writeFile.seek(position);int byteCount;while ((byteCount = readFile.read(buf)) != -1) {if (byteCount != chunksize) {byte[] tempBytes = new byte[byteCount];System.arraycopy(buf, 0, tempBytes, 0, byteCount);buf = tempBytes;}writeFile.write(buf);position = position + byteCount;}readFile.close();}writeFile.close();cn.hutool.core.io.FileUtil.del(dirFile);}/*** 视频拆分** @param inputFilePath   D:/home/dxhh/uploadPath/video/md5.mp4* @param outputDirectory D:/home/dxhh/uploadPath/video/md5*/@Asyncpublic void mp4ToM3u8ForWindow(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException {File uploadFile = new File(outputDirectory);if (!uploadFile.exists()) {uploadFile.mkdirs();}Path outputDirPath = Paths.get(outputDirectory);//我的ffmpeg.exe放在 项目的/resources/script目录下Path resourcePath = Paths.get("./script/ffmpeg.exe");FFmpeg.atPath(resourcePath.getParent()).addInput(UrlInput.fromPath(Paths.get(inputFilePath))).addOutput(UrlOutput.toPath(outputDirPath.resolve("output_%03d.ts"))).addArguments("-f", "segment").addArguments("-segment_time", ms + "ms") // 分片时长为30s.addArguments("-segment_list", outputDirPath.resolve("playlist.m3u8").toString()).addArguments("-c:v", "copy") // 优化视频编码参数.addArguments("-c:a", "copy") // 优化音频编码参数.execute();// 修改生成的m3u8文件,将ts链接替换为完整URLupdateM3u8File(fileMd5, outputDirectory);}/*** 视频拆分** @param fileMd5         adw1dwdadadwdadasd* @param inputFilePath   /home/dxhh/uploadPath/video/md5.mp4* @param outputDirectory /home/dxhh/uploadPath/video/md5* @throws IOException* @throws InterruptedException*/public void mp4ToM3u8ForLinux(String fileMd5, String inputFilePath, String outputDirectory, long ms) throws IOException, InterruptedException {String command = "ffmpeg -i " + inputFilePath + " -c copy -map 0 -f segment -segment_time " + ms + "ms -segment_list " + outputDirectory + "/playlist.m3u8 " + outputDirectory + "/output_%03d.ts";//ffmpeg -i /home/dxhh/uploadPath/video/md5.mp4 -c copy -map 0 -f segment -segment_time 1236ms -segment_list /home/dxhh/uploadPath/video/md5/playlist.m3u8 /home/dxhh/uploadPath/video/md5/output_%03d.tslog.info("视频分割脚本:{}", command);ProcessBuilder builder = new ProcessBuilder(command.split(" "));builder.redirectErrorStream(true);Process process = builder.start();BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line;while ((line = reader.readLine()) != null) {System.out.println(line);}int exitCode = process.waitFor();if (exitCode == 0) {System.out.println("FFmpeg command executed successfully");updateM3u8File(fileMd5, outputDirectory);} else {System.out.println("FFmpeg command failed with exit code " + exitCode);}}private void updateM3u8File(String fileMd5, String outputDirectory) throws IOException {String m3u8FilePath = outputDirectory + "/playlist.m3u8";List<String> lines = Files.readAllLines(Paths.get(m3u8FilePath));List<String> newLines = new ArrayList<>();for (String line : lines) {if (line.endsWith(".ts")) {if ("dev".equals(active)) {newLines.add("/dev-api/profile/video/" + fileMd5 + "/" + line);} else {newLines.add("/stage-api/profile/video/" + fileMd5 + "/" + line);}} else {newLines.add(line);}}Files.write(Paths.get(m3u8FilePath), newLines);}public long getTsDuration(String filePath) throws EncoderException {int targetSize = 2 * 1024 * 1024; // 2MBFile videoFile = new File(filePath);long fileSize = videoFile.length();Encoder encoder = new Encoder();MultimediaInfo multimediaInfo = encoder.getInfo(videoFile);long duration = multimediaInfo.getDuration();System.out.println("Duration: " + duration + " ms");System.out.println("File size: " + fileSize + " bytes");// Calculate target duration for a 2MB videolong targetDuration = (duration * targetSize) / fileSize;System.out.println("Target duration for a 2MB video: " + targetDuration + " ms");return targetDuration;}

获取视频时长需要用到jave工具包,想上传资源的提示已存在,应该可以在csdn搜到;
还需要ffmpeg软件,如果是windows环境运行,只需要调用本地的ffmpeg.exe就好,如果是在linux运行,需要安装ffmpeg;

   <!--视频切割--><dependency><groupId>com.github.kokorin.jaffree</groupId><artifactId>jaffree</artifactId><version>2023.09.10</version></dependency><dependency><groupId>it.sauronsoftware.jave</groupId><artifactId>jave2</artifactId><version>1.0.2</version><scope>system</scope><systemPath>${project.basedir}/lib/jave-1.0.2.jar</systemPath></dependency>

2.3 linux中安装ffmpeg

  1. 下载 ffmpeg 工具包并解压
wget http://www.ffmpeg.org/releases/ffmpeg-4.2.tar.gz
tar -zxvf ffmpeg-4.2.tar.gz
  1. 进入工具包文件夹并进行安装,将 ffmpeg 安装至 / usr/local/ffmpeg 下
 cd ffmpeg-4.2./configure --prefix=/usr/local/ffmpeg
./configure --prefix=/usr/local/ffmpeg --enable-openssl --disable-x86asm
make && make install

注意:若出现以下报错,请跳至第五步,待第五步安装成功后再返回第二步。
在这里插入图片描述

  1. 配置环境变量,使其 ffmpeg 命令生效
 #利用vi编辑环境变量
vi /etc/profile#在最后位置处添加环境变量,点击i进入编辑模式,esc键可退出编辑模式
export PATH=$PATH:/usr/local/ffmpeg/bin#退出编辑模式后,:wq 保存退出
#刷新资源,使其生效
source /etc/profile
  1. 查看 ffmpeg 版本,验证是否安装成功
ffmpeg -version

若出现以下内容,则安装成功。

在这里插入图片描述

  1. 若第二步出现图片中的错误信息,则需要安装 yasm

记得退出 ffmpeg 工具包文件夹,cd … 返回上一层

 #下载yasm工具包
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz#解压
tar -zxvf yasm-1.3.0.tar.gz#进入工具包文件夹并开始安装
cd yasm-1.3.0
./configure
make && make install

安装完成后直接返回第二步即可,此时命令就不会报错了。

2.4 视频资源地址

因为是基于若依框架开发的,其实只要上传的的时候是往 RuoYiConfig.getProfile() 这个指定配置目录保存文件,都是能直接访问不需要额外开发,这里就简单过一下
若依的自定义参数配置类从yml文件读取用户配置

@Component
@ConfigurationProperties(prefix = "xxx")
public class RuoYiConfig {/*** 上传路径 /home/user/xxxx/upload*/private static String profile;
}

在通用配置定义一个静态资源路由前缀

/*** 通用常量定义** @author li.dh*/
public class CommonConstant {/*** 资源映射路径 前缀*/public static final String RESOURCE_PREFIX = "/profile";
}

在mvc配置中添加静态资源的转发映射,将/profile前缀的请求转发到RuoYiConfig.getProfile()路径下

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {/** 本地文件上传路径 */registry.addResourceHandler(CommonConstant.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");/** swagger配置 */registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/").setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());}
}

3. vue播放流视频

我的需求是在列表上点击视频弹出播放弹窗

<!-- 播放视频 --><el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @close="open=false"><video-player class="video-player vjs-custom-skin"ref="videoPlayer":playsinline="true":options="playerOptions"></video-player></el-dialog>
import 'video.js/dist/video-js.css'data(){return {// 弹出层标题title: '',m3u8Url: '',// 是否显示弹出层open: false,playerOptions: {playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度autoplay: true, // 如果为true,浏览器准备好时开始回放。muted: false, // 默认情况下将会消除任何音频。loop: false, // 是否视频一结束就重新开始。preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)language: 'zh-CN',aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。sources: [{type: 'application/x-mpegURL', // 类型src: this.m3u8Url}],poster: '', // 封面地址notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。controlBar: {timeDivider: true, // 当前时间和持续时间的分隔符durationDisplay: true, // 显示持续时间remainingTimeDisplay: false, // 是否显示剩余时间功能fullscreenToggle: true // 是否显示全屏按钮}}}
},
methods: {openVideo(picurl, url, title) {this.title = titlelet videourl = process.env.VUE_APP_BASE_API + urllet imgurl = process.env.VUE_APP_BASE_API + picurl// console.log("视频地址:" , videourl)this.m3u8Url = videourlthis.playerOptions.sources[0].src = videourl // 重新加载视频this.playerOptions.poster = imgurl // 封面// this.$refs.videoPlayer.play() // 播放视频this.open = true}
}

4. 实现效果

在这里插入图片描述

相关文章:

vue分片上传视频并转换为m3u8文件并播放

开发环境&#xff1a; 基于若依开源框架的前后端分离版本的实践&#xff0c;后端java的springboot&#xff0c;前端若依的vue2&#xff0c;做一个分片上传视频并分段播放的功能&#xff0c;因为是小项目&#xff0c;并没有专门准备文件服务器和CDN服务&#xff0c;后端也是套用…...

【MySQL】对表结构进行增删查改的操作

表的操作 前言正式开始建表查看表show tables;desc xxx;show create table xxx; 修改表修改表名 rename to对表结构进行修改新增一个列 add 对指定列的属性做修改 modify修改列名 change 删除某列 drop 删除表 drop 前言 前一篇讲了库相关的操作&#xff0c;如果你不太懂&…...

Hadoop原理,HDFS架构,MapReduce原理

Hadoop原理&#xff0c;HDFS架构&#xff0c;MapReduce原理 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c…...

【Spring Boot】035-Spring Boot 整合 MyBatis Plus

【Spring Boot】035-Spring Boot 整合 MyBatis Plus 【Spring Boot】010-Spring Boot整合Mybatis https://blog.csdn.net/qq_29689343/article/details/108621835 文章目录 【Spring Boot】035-Spring Boot 整合 MyBatis Plus一、MyBatis Plus 概述1、简介2、特性3、结构图4、相…...

Hafnium之强制性的接口

安全之安全(security)博客目录导读 目录 一、FFA_VERSION 二、FFA_FEATURES 三、FFA_RXTX_MAP/FFA_RXTX_UNMAP 四、FFA_PARTITION_INFO_GET 五、FFA_PARTITION_INFO_GET_REGS...

计算机视觉:使用opencv实现银行卡号识别

1 概述 1.1 opencv介绍 OpenCV是Open Source Computer Vision Library&#xff08;开源计算机视觉库&#xff09;的简称&#xff0c;由Intel公司在1999年提出建立&#xff0c;现在由Willow Garage提供运行支持&#xff0c;它是一个高度开源发行的计算机视觉库&#xff0c;可以…...

【Proteus仿真】【Arduino单片机】简易计算器设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器&#xff0c;使用PCF8574、LCD1602液晶、4*4矩阵键盘等。 主要功能&#xff1a; 系统运行后&#xff0c;操作矩阵按键可实现简单四则运算。 二、软件设计 /* …...

pychon/PIL/opencv/json学习过程中遇到的问题

1. 使用PIL.Image读取图片 注意&#xff1a;pytorch中对图像预处理是transforms的输入必须是PIL格式的文件&#xff0c;使用cv2读取的图片就按照第二条的代码处理&#xff08;3通道合并、归一化处理&#xff09; from PIL import Image img Image.open("test1.jpg"…...

YOLO目标检测——番茄数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;番茄检测数据集说明&#xff1a;番茄目标检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富标签说明&#xff1a;使用lableimg标注软件标注&#xff0c;标注框质量高&#xff0c;含voc(xml)、coco(json)和yolo(txt)三种格式标签…...

(JAVA)线程

线程的创建 方式一&#xff1a;Thread public class dome {public static void main(String[] args) {MyThread myThread new MyThread();myThread.start();for(int i1;i<5;i){System.out.println("主线程"i);}} }public class MyThread extends Thread{Overri…...

【深度学习环境】windows安装 NVIDIA Docker

摘要 不要安装 Docker Desktop&#xff01;我们将在 Ubuntu 中自行安装 Docker。 请安装 Windows 10 Insider Build 或 Windows 11 &#xff08;Beta也行&#xff09;。&#xff08;稳定发行版无法在 WSL 2 中使用 GPU&#xff09; 请安装 WSL 2 w/Ubuntu 20.04 或同等版本。…...

【微信小程序】自定义组件(三)

自定义组件 插槽1、什么是插槽2、单个插槽3、定义多个插槽 父子组件之间的通信1、父子组件之间的通信的3种方式2、事件绑定3、behaviors 插槽 1、什么是插槽 在自定义组件的wxml结构中&#xff0c;可以提供一个<solot> 节点&#xff08;插槽&#xff09;&#xff0c;用…...

Python语言:经典案例分析讲解2

例题1&#xff1a;文件的操作 例题2&#xff1a;调用函数求偶数之和 例题3&#xff1a;调用函数并使用递归的方法求斐波那契数前N项之和 题1: 以只写的模式打开文件test.txt&#xff0c;写入"Python"&#xff0c;关闭文件。 代码如下&#xff1a; f open("E:/…...

dbeaver连接别人的数据库没有表

1.概念 非缺省的数据库&#xff1a; 通常是指在一个数据库管理系统&#xff08;DBMS&#xff09;中&#xff0c;除了系统默认创建的数据库之外的其他用户创建或自定义的数据库。许多数据库系统在安装后会创建一个默认数据库&#xff0c;例如MySQL中的mysql数据库&#xff0c;…...

EXIT(1)

EXTI介绍 EXTI是片上外设 NVIC是cpu内的外设 回忆起之前的GPIO和AFIO 我们是如何检测按键按下的 我们是一直用while循环读取IDR寄存器的对应位置的值 一直检测判断按键是否被按下 那么是否有第二种方式检测按键是否被按下了呢&#xff1f; 通过EXTI 当EXTI检测到按键的电平发生…...

Qt信号量用于对共享资源进行同步

定义信号量与缓冲区&#xff1a; const int BufferSize 8; int buffer1[BufferSize]; int buffer2[BufferSize]; int curBuf1; //当前正在写入的Bufferint bufNo0; //采集的缓冲区序号quint8 counter0;//数据生成器QSemaphore emptyBufs(2);//信号量&#xff1a;空的缓冲区…...

在报错中学python something

这里写目录标题 动手学深度学习pandas完整代码数据处理TypeError: can only concatenate str (not "int") to str&#xff08;fillna填补缺失值&#xff09; 创建文件夹学习这个数据分组get_dummies实现one hot encode 动手学深度学习pandas完整代码 import osimpor…...

如何调用 DBMS_DISKGROUP 对 ASM 文件进行随机读取

目录 一、概述 二、实现思路与注意点 三、Java Demo 1、直接调用 2、读写异步 一、概述 对于 Oracle Rac 环境下,数据文件大多默认存放在 ASM 共享存储上,当我们需要读取 ASM 上存储的数据文件时可以使用 Oracle 提供的一些方法,比如 ASMCMD CP。但是,对于一些备份场景…...

UART学习

uart.c #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_uart.h" // UART4_TX : PG11 AF6 // UART4_RX : PB2 AF8 void __uart_init() {// GPIOB2 设置为复用功能GPIOB->MODER & (~(0x3 << 4));GPIOB->MODER | (0x2 << 4);G…...

洗地机哪个牌子最好用?洗地机品牌排行榜

近年来&#xff0c;洗地机相当热门&#xff0c;洗地机结合了扫地拖地吸地为一体的多功能清洁工具&#xff0c;让我们告别了传统方式打扫卫生&#xff0c;让我们清洁不再费劲&#xff0c;可是市面上的洗地机五花八门&#xff0c;怎么挑选到一个洗地机也是一个问题&#xff0c;下…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略

本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装&#xff1b;只需暴露 19530&#xff08;gRPC&#xff09;与 9091&#xff08;HTTP/WebUI&#xff09;两个端口&#xff0c;即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

【项目实战】通过多模态+LangGraph实现PPT生成助手

PPT自动生成系统 基于LangGraph的PPT自动生成系统&#xff0c;可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析&#xff1a;自动解析Markdown文档结构PPT模板分析&#xff1a;分析PPT模板的布局和风格智能布局决策&#xff1a;匹配内容与合适的PPT布局自动…...

Cinnamon修改面板小工具图标

Cinnamon开始菜单-CSDN博客 设置模块都是做好的&#xff0c;比GNOME简单得多&#xff01; 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Java线上CPU飙高问题排查全指南

一、引言 在Java应用的线上运行环境中&#xff0c;CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时&#xff0c;通常会导致应用响应缓慢&#xff0c;甚至服务不可用&#xff0c;严重影响用户体验和业务运行。因此&#xff0c;掌握一套科学有效的CPU飙高问题排查方法&…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...