libilibi项目优化(2)视频文件分块上传

第一版

文件分片上传过程总结

整个文件分片上传过程分为三个主要步骤:预上传、分片上传和获取已上传分块信息。以下是每个步骤的详细描述:

1. 预上传(preUploadVideo
  • 功能:生成唯一的上传 ID,并将文件信息存储到 Redis 中,为后续的分片上传做准备。
  • 流程
    1. 从前端接收文件名(fileName)和分片总数(chunks)。
    2. 从 Redis 中获取当前用户的用户信息(TokenUserInfoDto)。
    3. 调用 redisComponent.savePreVideoFileInfo 方法:
      • 生成唯一的上传 ID(uploadId)。
      • 创建 UploadingFileDto 对象,存储文件的基本信息(文件名、分片总数、初始分片索引等)。
      • 根据当前日期和用户 ID 生成存储路径,并确保路径存在。
      • 将文件信息存储到 Redis 中,并设置过期时间。
    4. 返回生成的上传 ID 给前端,用于后续的分片上传。
//预上传文件@RequestMapping("/preUploadVideo")public ResponseVO preUploadVideo(@NotEmpty String fileName, @NotNull Integer chunks) {//从redis中获取用户信息TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();//将预上传的文件信息存入redis,并放回相对应的上传idString uploadId = redisComponent.savePreVideoFileInfo(tokenUserInfoDto.getUserId(), fileName, chunks);//将id放回给前端,为之后的正式上传做准备return getSuccessResponseVO(uploadId);}
2. 分片上传(uploadVideo
  • 功能:接收分片文件,将其存储到指定路径,并更新 Redis 中的上传进度。

  • 流程

    1. 从前端接收分片文件(chunkFile)、分片索引(chunkIndex)和上传 ID(uploadId)。
    2. 从 Redis 中获取当前用户的用户信息(TokenUserInfoDto)。
    3. 根据上传 ID 获取对应的文件信息(UploadingFileDto):
      • 如果文件信息不存在,抛出异常提示前端重新上传。
    4. 检查文件大小是否超过系统设置的最大文件大小限制:
      • 如果超过限制,抛出异常。
    5. 构造存储路径,并将分片文件存储到对应路径:
      • 路径格式为:项目根目录/文件夹/临时文件夹/日期/用户ID上传ID/分片索引
    6. 更新 Redis 中的文件信息:
      • 增加已上传的分片索引。
      • 更新已上传的文件大小。
    7. 返回成功响应给前端。
    //上传文件@RequestMapping("/uploadVideo")public ResponseVO uploadVideo(@NotNull MultipartFile chunkFile, @NotNull Integer chunkIndex, @NotEmpty String uploadId) throws IOException {//获得当前用户信息TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();//获得对应的上传文件的实体UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(tokenUserInfoDto.getUserId(), uploadId);if (fileDto == null) {throw new BusinessException("文件不存在请重新上传");}SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();if (fileDto.getFileSize() > sysSettingDto.getVideoSize() * Constants.MB_SIZE) {throw new BusinessException("文件超过最大文件限制");}//获得路径String folder = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_TEMP+ fileDto.getFilePath();//创建文件File targetFile = new File(folder + "/" + chunkIndex);chunkFile.transferTo(targetFile);//记录文件上传的分片数fileDto.setChunkIndex(chunkIndex);//设置当前已上传文件的大小fileDto.setFileSize(fileDto.getFileSize() + chunkFile.getSize());//更新文件实体信息redisComponent.updateVideoFileInfo(tokenUserInfoDto.getUserId(), fileDto);return getSuccessResponseVO(null);}
    
3. 获取已上传分块信息(getUploadedChunks
  • 功能:返回已上传的分片信息,用于前端判断哪些分片已经上传成功。

  • 流程

    1. 从前端接收上传 ID(uploadId)。
    2. 从 Redis 中获取当前用户的用户信息(TokenUserInfoDto)。
    3. 根据上传 ID 获取对应的文件信息(UploadingFileDto):
      • 如果文件信息不存在,抛出异常提示前端重新上传。
    4. 返回文件信息给前端,包括已上传的分片索引和文件大小。
    // 获取已上传分块信息@RequestMapping("/getUploadedChunks")public ResponseVO getUploadedChunks(@NotEmpty String uploadId) {TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(tokenUserInfoDto.getUserId(), uploadId);if (fileDto == null) {throw new BusinessException("文件不存在请重新上传");}return getSuccessResponseVO(fileDto);}
    

Redis 相关操作

  • savePreVideoFileInfo 方法

    • 生成唯一的上传 ID。
    • 创建 UploadingFileDto 对象,存储文件的基本信息。
    • 根据当前日期和用户 ID 生成存储路径,并确保路径存在。
    • 将文件信息存储到 Redis 中,并设置过期时间。
  • updateVideoFileInfo 方法

    • 更新 Redis 中的文件信息,包括已上传的分片数和文件大小。
  • getUploadingVideoFile 方法

    • 根据用户 ID 和上传 ID 从 Redis 中获取文件信息。
public String savePreVideoFileInfo(String userId, String fileName, Integer chunks) {//生成上传idString uploadId = StringTools.getRandomString(Constants.LENGTH_15);//生成将要上传的文件对应的实体UploadingFileDto fileDto = new UploadingFileDto();//设置分片大小fileDto.setChunks(chunks);//设置文件名fileDto.setFileName(fileName);//设置上传idfileDto.setUploadId(uploadId);//设置初始分片索引fileDto.setChunkIndex(0);//根据天数新建目录,根据用户id和上传id生成文件路径String day = DateUtil.format(new Date(), DateTimePatternEnum.YYYYMMDD.getPattern());String filePath = day + "/" + userId + uploadId;String folder = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_TEMP+ filePath;File folderFile = new File(folder);if (!folderFile.exists()) {folderFile.mkdirs();}//设置对应文件真实路径fileDto.setFilePath(filePath);//设置预上传的预留时间redisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId, fileDto, Constants.REDIS_KEY_EXPIRES_DAY);return uploadId;}public void updateVideoFileInfo(String userId, UploadingFileDto fileDto) {redisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + fileDto.getUploadId(), fileDto, Constants.REDIS_KEY_EXPIRES_DAY);}public UploadingFileDto getUploadingVideoFile(String userId, String uploadId) {//通过userId和uploadId获得当前上传文件的实体return (UploadingFileDto) redisUtils.get(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId);}

UploadingFileDto

  • 用于存储上传文件的相关信息,包括:
    • 上传 ID(uploadId
    • 文件名(fileName
    • 已上传的分片索引(chunkIndex
    • 总分片数(chunks
    • 文件大小(fileSize
    • 文件路径(filePath
public class UploadingFileDto implements Serializable {private String uploadId;private String fileName;private Integer chunkIndex;private Integer chunks;private Long fileSize = 0L;private String filePath;}

总结

整个文件分片上传过程通过预上传生成唯一的上传 ID 和文件信息,分片上传将每个分片存储到指定路径并更新上传进度,最后通过获取已上传分块信息接口返回前端已上传的分片信息。整个过程利用 Redis 存储文件信息,确保上传过程的高效和可靠。

第二版

使用Redis中的Set结构验证每个分块是否缺失
以下是使用Redis Set集合优化后的代码示例:

1. 预上传文件(preUploadVideo 方法)

@RequestMapping("/preUploadVideo")
public ResponseVO preUploadVideo(@NotEmpty String fileName, @NotNull Integer chunks) {// 从redis中获取用户信息TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();// 生成上传IDString uploadId = redisComponent.savePreVideoFileInfo(tokenUserInfoDto.getUserId(), fileName, chunks);// 返回上传ID给前端return getSuccessResponseVO(uploadId);
}// 在RedisComponent中
public String savePreVideoFileInfo(String userId, String fileName, Integer chunks) {// 生成唯一的上传IDString uploadId = StringTools.getRandomString(Constants.LENGTH_15);// 创建UploadingFileDto对象,存储文件基本信息UploadingFileDto fileDto = new UploadingFileDto();fileDto.setUploadId(uploadId);fileDto.setFileName(fileName);fileDto.setChunks(chunks);fileDto.setChunkIndex(0);fileDto.setFileSize(0L);// 设置存储路径String day = DateUtil.format(new Date(), DateTimePatternEnum.YYYYMMDD.getPattern());String filePath = day + "/" + userId + uploadId;fileDto.setFilePath(filePath);// 创建存储目录String folderPath = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_TEMP+ filePath;File folder = new File(folderPath);if (!folder.exists()) {folder.mkdirs();}// 将文件信息存储到RedisredisUtils.setex(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId, fileDto, Constants.REDIS_KEY_EXPIRES_DAY);return uploadId;
}

2. 分片上传(uploadVideo 方法)

@RequestMapping("/uploadVideo")
public ResponseVO uploadVideo(@NotNull MultipartFile chunkFile, @NotNull Integer chunkIndex, @NotEmpty String uploadId) throws IOException {// 获取当前用户信息TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();String userId = tokenUserInfoDto.getUserId();// 获取上传文件信息UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(userId, uploadId);if (fileDto == null) {throw new BusinessException("文件不存在请重新上传");}// 检查文件大小是否超过限制SysSettingDto sysSettingDto = redisComponent.getSysSettingDto();if (fileDto.getFileSize() + chunkFile.getSize() > sysSettingDto.getVideoSize() * Constants.MB_SIZE) {throw new BusinessException("文件超过最大文件限制");}// 构造存储路径并保存分片文件String folderPath = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_TEMP+ fileDto.getFilePath();File targetFile = new File(folderPath + "/" + chunkIndex);chunkFile.transferTo(targetFile);// 更新文件信息fileDto.setChunkIndex(chunkIndex);fileDto.setFileSize(fileDto.getFileSize() + chunkFile.getSize());redisComponent.updateVideoFileInfo(userId, fileDto);// 将分片索引加入Redis Set集合redisUtils.sadd(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId, chunkIndex);return getSuccessResponseVO(null);
}

3. 合并文件(mergeVideo 方法)

@RequestMapping("/mergeVideo")
public ResponseVO mergeVideo(@NotEmpty String uploadId) {TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();String userId = tokenUserInfoDto.getUserId();// 获取上传文件信息UploadingFileDto fileDto = redisComponent.getUploadingVideoFile(userId, uploadId);if (fileDto == null) {throw new BusinessException("文件不存在请重新上传");}// 获取已上传的分片索引Set<Integer> uploadedChunks = redisUtils.smembers(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);// 检查分片是否完整if (uploadedChunks.size() != fileDto.getChunks()) {// 找出缺失的分片索引Set<Integer> missingChunks = new HashSet<>();for (int i = 0; i < fileDto.getChunks(); i++) {if (!uploadedChunks.contains(i)) {missingChunks.add(i);}}throw new BusinessException("存在未上传的分片:" + missingChunks);}// 检查分片索引的连续性List<Integer> sortedChunks = new ArrayList<>(uploadedChunks);Collections.sort(sortedChunks);for (int i = 0; i < sortedChunks.size(); i++) {if (sortedChunks.get(i) != i) {throw new BusinessException("分片索引不连续,存在缺失的分片");}}// 构造存储路径String tempFolderPath = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_TEMP+ fileDto.getFilePath();String finalFilePath = appConfig.getProjectFolder()+ Constants.FILE_FOLDER+ Constants.FILE_FOLDER_VIDEO+ fileDto.getFilePath();// 合并分片文件File finalFile = new File(finalFilePath);try (FileOutputStream out = new FileOutputStream(finalFile)) {for (int i = 0; i < fileDto.getChunks(); i++) {File chunkFile = new File(tempFolderPath + "/" + i);if (chunkFile.exists()) {try (FileInputStream in = new FileInputStream(chunkFile)) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesRead);}}}}} catch (IOException e) {throw new BusinessException("文件合并失败", e);}// 检查合并后的文件大小是否与预上传时的总文件大小一致long mergedFileSize = finalFile.length();if (mergedFileSize != fileDto.getFileSize()) {throw new BusinessException("合并后的文件大小不匹配,存在缺失的分片");}// 清理临时文件和Redis中的记录File tempFolder = new File(tempFolderPath);if (tempFolder.exists()) {deleteDirectory(tempFolder);}redisUtils.del(Constants.REDIS_KEY_UPLOADING_FILE + userId + uploadId);redisUtils.del(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);return getSuccessResponseVO(finalFilePath);
}

4. 获取已上传分块信息(getUploadedChunks 方法)

@RequestMapping("/getUploadedChunks")
public ResponseVO getUploadedChunks(@NotEmpty String uploadId) {Set<Integer> uploadedChunks = redisUtils.smembers(Constants.REDIS_KEY_UPLOADED_CHUNKS + uploadId);return getSuccessResponseVO(uploadedChunks);
}

5. Redis 工具类方法

// 添加元素到Set集合
public void sadd(String key, Integer value) {redisTemplate.opsForSet().add(key, value);
}// 获取Set集合中的所有元素
public Set<Integer> smembers(String key) {return redisTemplate.opsForSet().members(key);
}// 删除键值对
public void del(String key) {redisTemplate.delete(key);
}

通过以上代码,利用Redis的Set集合来记录已上传的分片索引,可以在合并文件时快速检查分片的完整性和连续性,并支持断点续传功能。

优化后的代码的优点和不足之处

优点
  1. 高效记录与检查已上传分片

    • 使用Redis的Set数据结构来记录已上传的分片索引,可以快速进行插入、删除和查找操作,时间复杂度接近O(1),提高了效率。
  2. 支持断点续传

    • 前端可以获取已上传的分片信息,只上传未完成的分片,提升了用户体验,特别是在网络不稳定或上传中断的情况下。
  3. 完整性校验

    • 在合并文件时,通过比较Redis Set集合的大小和预期的分片总数,以及检查分片索引的连续性,确保文件的完整性。
  4. 文件大小校验

    • 合并文件后,检查合并后的文件大小是否与预上传时的总文件大小一致,进一步确保文件的完整性。
  5. 资源清理

    • 在文件合并成功后,清理临时存储的分片文件和Redis中的相关记录,避免资源浪费。
  6. 扩展性

    • 使用Redis进行数据存储和管理,便于扩展和维护,可以轻松地与其他微服务或分布式系统集成。
不足之处
  1. Redis性能问题

    • 当分片数量非常大时,Redis的Set数据结构可能会占用较多内存,影响性能。虽然Redis本身是内存数据库,性能较高,但大规模数据操作仍可能导致延迟增加。
  2. 数据一致性问题

    • 如果在上传过程中服务器宕机或出现其他异常情况,可能会导致Redis中的数据与实际上传的文件不一致。需要额外的机制来确保数据的一致性。
  3. 并发上传问题

    • 如果多个用户同时上传大量文件,可能会导致Redis和服务器的存储压力增大,需要考虑并发控制和资源限制。
  4. 错误处理不够完善

    • 当前代码在某些异常情况下的处理不够完善,例如网络中断、存储路径错误等,需要进一步增强错误处理和恢复机制。
  5. 哈希校验缺失

    • 虽然代码中没有实现,但为了进一步确保文件的完整性,可以考虑在分片上传时使用哈希校验,确保每个分片的内容未被篡改。
  6. 文件合并的效率问题

    • 当分片数量非常多时,合并文件的过程可能会比较耗时,特别是在磁盘I/O性能较低的情况下。

总结

优化后的代码在功能上较为完善,支持断点续传、完整性校验和资源清理等功能,但在处理大规模数据、并发上传和异常情况时仍存在一些不足。在实际应用中,可以根据具体需求和场景进一步优化和改进。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/73737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TCP简单链接的编程实现

TCP简单链接的编程实现 本文主要介绍TCP应用层的编码实现。 TCP是一种面向连接的、可靠的、基于字节流的传输层协议&#xff0c;它是互联网协议套件&#xff08;TCP/IP&#xff09;中的核心协议之一&#xff0c;广泛应用于需要可靠数据传输的场景&#xff0c;如&#xff1a;网…

使用Multiprocessing模块创建子进程,需要放到__main__中

1 场景说明 在Python中&#xff0c;使用multiprocessing模块创建子进程时&#xff0c;将创建子进程的代码放在if __name__ __main__: 块之外&#xff0c;如下面代码&#xff1a; import multiprocessing import timedef test_func(name):print(f"子进程 {name} 开始运行…

描述<canvas>标签的主要用途,如何在其上绘制简单图形?

大白话描述标签的主要用途&#xff0c;如何在其上绘制简单图形&#xff1f; <canvas> 标签的主要用途 <canvas> 标签是 HTML5 中新增的一个标签&#xff0c;它就像是一块“画布”&#xff0c;你可以在网页上用它来绘制各种图形、动画、制作游戏等。简单来说&…

【RHCE实验】搭建主从DNS、WEB等服务器

目录 需求 环境搭建 配置nfs服务器 配置web服务器 配置主从dns服务器 主dns服务器 从dns服务器 配置客户端 客户端测试 需求 客户端通过访问 www.nihao.com 后&#xff0c;能够通过 dns 域名解析&#xff0c;访问到 nginx 服务中由 nfs 共享的首页文件&#xff0c;内容…

Shell条件判断

一、使用if选择结构 if单分支的语法组成&#xff1a; if 条件测试;then 命令序列 fi if双分支的语法组成&#xff1a; if 条件测试;then 命令序列1 else 命令序列2 fi if多分支的语法组成&#xff1a; if 条…

理解langchain langgraph 官方文档示例代码中的MemorySaver

以下是langchain v0.3官方示例代码 from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import START, MessagesState, StateGraph# 可以理解为&#xff1a;定义一个流程&#xff0c;这个流程中用到的数据类型是Messages。 <---定义一个有向图&…

【HarmonyOS Next之旅】DevEco Studio使用指南(三)

目录 1 -> 一体化工程迁移 1.1 -> 自动迁移 1.2 -> 手动迁移 1.2.1 -> API 10及以上历史工程迁移 1.2.2 -> API 9历史工程迁移 1 -> 一体化工程迁移 DevEco Studio从 NEXT Developer Beta1版本开始&#xff0c;提供开箱即用的开发体验&#xff0c;将SD…

vuex持久化存储,手动保存到localStorage

vuex持久化存储&#xff0c;手动保存到localStorage 一、vue21. 手动存储到localStoragestore/index.js 2. 使用持久化存储插件store/index.jsstore/modules/otherData.js保存到localStorage 二、vue31. index.ts2. store/modules/globalData.ts3. 在组件中使用App.vue 一、vue…

nodejs使用 mysql2 模块获取 mysql 中的 json字段,而不是 mysql

mysql 模块获取的 json 字段&#xff0c;是字符串mysql2 模块获取的 json 字段&#xff0c;是符合预期的 json 对象 mysql mysql2 最后编辑于&#xff1a;2025-02-24 22:16:53 © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点赞、收藏、关注哦&#xff01;…

鸿蒙(OpenHarmony)开发实现 息屏/亮屏 详情

官网参考链接 实现点击关闭屏幕&#xff0c;定时5秒后唤醒屏幕 权限 {"name": "ohos.permission.POWER_OPTIMIZATION"}代码实现 import power from ohos.power;Entry Component struct Page3 {private timeoutID: number | null null; // 初始化 timeout…

【网工第6版】第1章 计算机网络概论

目录 1计算机网络形成和发展 ■计算机网络 ■我国互联网发展 ■计算机网路分类 ■计算机网络应用 2 OSI和TCP/IP参考模型 ■网络分层的意义 ■OSI参考模型 ■TCP/IP参考模型 ■TCP/IP参考模型协议 3 数据封装与解封过程 ■封装 ■解封 1计算机网络形成和发展 ■计…

理解我们单片机拥有的资源

目录 为什么要查询单片机拥有的资源 所以&#xff0c;去哪些地方可以找数据手册 一个例子&#xff1a;STM32F103C8T6 前言 本文章隶属于项目&#xff1a; Charliechen114514/BetterATK: This is a repo that helps rewrite STM32 Common Repositorieshttps://github.com/C…

《我的Python觉醒之路》之转型Python(十五)——控制流

[今天是2025年3月17日&#xff0c;继续复习第一章节、第二章节的内容 ] 《我的Python觉醒之路》之转型Python&#xff08;十四&#xff09;——控制流

AndroidStudio+Android8.0下的Launcher3 导入,编译,烧录,调试

文章目录 编译完成搜索输出文件Android.mk配置gradle编译环境报错一报错二报错三输出文件下载INSTALL_FAILED_TEST_ONLY查找系统签名查找签名工具开始签名查看签名签名问题重新生成秘钥解决方案生成成功挽救错误:重新刷机更换testkey秘钥keystore生成keystoreINSTALL_FAILED_S…

Linux--gdb/cgdb

ok&#xff0c;我们今天学习gdb的安装和使用 调试器-gdb/cgdb使用 VS、VScode编写的代码一般都是release格式的&#xff0c;gdb 的格式一般是debug 换成debug模式命令 :-g gdb会记录最新的一条命令&#xff0c;直接回车就是默认执行该命令 一个调试周期下&#xff0c;断点…

Oracle GoldenGate 全面解析

Oracle GoldenGate 全面解析 Oracle GoldenGate 是一种实时数据集成和复制解决方案,广泛应用于数据同步、数据库迁移、高可用性和灾难恢复等场景。以下将详细解答您提出的关于 Oracle GoldenGate 的一系列问题。 1. Oracle GoldenGate 的架构组成及其核心组件的作用 架构组成…

ModBus TCP/RTU互转(主)(从)|| Modbus主动轮询下发的工业应用 || 基于智能网关的串口服务器进行Modbus数据收发的工业应用

目录 前言 一、ModBus TCP/RTU互转&#xff08;从&#xff09;及应用|| 1.1 举栗子 二、ModBus TCP/RTU互转&#xff08;主&#xff09; 2.1 举栗子 三、ModBus 主动轮询 3.1 Modbus主动轮询原理 3.2 Modbus格式上传与下发 3.2.1.设置Modbus主动轮询指令 3.2.2 设…

场景题:一个存储IP地址的100G 的文件, 找出现次数最多的 IP ?

和大文件中存id&#xff0c;然后要求排序问题一样的处理思路 使用MapReduce的思想解决&#xff0c;加上哈希分割&#xff0c;先将大文件中的IP地址按照哈希函数进行分割&#xff0c;存到多个文件上&#xff0c;接着每个分片单独处理&#xff0c;用Hashmap统计IP出现频次&#…

【操作系统安全】任务2:用户与用户组

目录 一、用户与用户组介绍 1.1 用户 1.2 用户组 1.3 用户与用户组的关系 二、用户与用户组管理 2.1 用户管理 2.1.1 创建用户 2.1.2 设置用户密码 2.1.3 删除用户 2.2 用户组管理 2.2.1 创建用户组 2.2.2 删除用户组 2.2.3 将用户添加到用户组 三、影子账户创建…

OpenCV计算摄影学(20)非真实感渲染之增强图像的细节函数detailEnhance()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 此滤波器增强特定图像的细节。 cv::detailEnhance用于增强图像的细节&#xff0c;通过结合空间域和频率域的处理&#xff0c;提升图像中特定细节…