news 2026/10/26 2:09:57

Swift实况图制作:JPEG+MOV元数据配对与相册写入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Swift实况图制作:JPEG+MOV元数据配对与相册写入实战

简介:这份 Swift 实况图制作 demo 面向已有一定 iOS 基础、希望掌握 Live Photo 生成与展示的开发者,围绕静态 JPEG 与短视频 MOV 的合成流程,提供可运行的工程样例,用于解决实况图素材匹配、PHLivePhoto 创建及界面预览等实践问题。压缩包共 21 个文件,约 4.37MB,以 5 个 Swift 源码为核心,配合 storyboard 界面、plist 配置、json 数据及 Xcode 工程文件,并附带 jpeg 与 mp4 示例素材,目录按应用入口、工具封装、资源素材与视图控制分层,整体结构清晰,便于直接编译、运行与对照研究。目前已有 1333 人学习下载。工程把素材匹配、合成请求参数、UIImageView 与 PHLivePhotoView 展示、PHAssetChangeObserver 进度监听等环节组织在同一示例中,读者可据此理解 Photos 框架下实况图的结构组成、元数据封装、尺寸与时长约束、播放暂停控制与制作进度反馈,也能为集成动态壁纸、增强照片交互等场景提供可复用的实现参考,适合用作功能验证与二次开发的基础样例。

1. Swift 实况图 Demo 拆解:一张 JPEG 加一段 MOV 为什么能“活”起来

这份 iOS Swift 实况图制作 Demo 把 Live Photo 拆成一条可复现的生成链路:一张带配对标识的 JPEG、一段带 still-image-time 的 MOV,再加一次相册资源写入。它解决的是手里有图片和短视频、却做不出系统相册能识别实况图的问题,适合正在做相机工具、内容导出、相册管理或社交分享功能的开发者。Demo 把配对、元数据写入、保存和预览四段拆开,新手能按步骤跑通,熟手能直接改参数和封装。

2. Live Photo 配对原理:asset identifier、still-image-time 与 3 秒窗口

实况图在文件层面不是单一格式,而是“静态图 + 配对视频”的组合。系统相册识别它,靠的不是文件名,也不是把两个文件放进同一个目录,而是资源类型、配对标识和视频里的静态帧时间点同时成立。很多 Demo 翻车,问题都出在把 Live Photo 当成普通图片保存,或者只写了一半元数据。

这一章先把配对原理讲透,再落到可执行的读取和保存代码。理解PHAsset.mediaSubtypes、asset identifier、still-image-time这三个点,后面第 3 章的生成链路才不会变成盲抄。

2.1 为什么不能简单把图片和视频塞进同一个相册

系统相册不是文件管理器。用普通图片保存接口写一张图,再用视频保存接口写一段 MOV,得到的是两个独立资源:一个PHAsset是图片,另一个PHAsset是视频。它们可以出现在同一个时间线附近,但不会自动变成实况图。实况图的判定依赖PHAsset的mediaSubtypes是否包含.photoLive,而这个子类型来自资源写入时的配对关系。

我在排查时最常用的第一段代码不是生成,而是读回:

import Photos func inspectAsset(_ asset: PHAsset) { // mediaType 只能区分图片/视频,不能区分实况图 let isImage = asset.mediaType == .image // .photoLive 是实况图的关键子类型标记 let isLivePhoto = asset.mediaSubtypes.contains(.photoLive) print("isImage=\(isImage), isLivePhoto=\(isLivePhoto), subtypes=\(asset.mediaSubtypes.rawValue)") }

逻辑说明:mediaType只告诉你这是图片还是视频,实况图仍然属于图片资源,所以不能用mediaType == .image判断它是不是实况图。mediaSubtypes是位掩码,.photoLive存在才说明系统把它当实况图处理。读回时如果isLivePhoto为 false,就不用再怀疑预览控件,先回到保存链路查配对。

参数说明:asset可以来自PHAsset.fetchAssets(withLocalIdentifiers:options:),也可以来自相册变更回调。调试阶段建议把localIdentifier一起打印,方便和保存时拿到的占位标识对齐。不要用文件名判断类型,系统可能重命名资源,文件名不可靠。

常见误用是“先保存图片,再保存视频,最后期望相册合并”。系统没有义务把两个独立资源合并成实况图,尤其当两个资源的创建时间、配对标识不一致时,更不会自动识别。正确路径是让一次资源创建请求同时携带.photo和.pairedVideo,由照片库在写入阶段建立配对关系。

2.2 配对元数据的三处落点

实况图的配对标识要同时写进静态图和视频。静态图这边常见落点是 JPEG 的 MakerNote 字典,键17存一个 UUID 字符串;视频这边落在 QuickTime metadata 的com.apple.quicktime.content.identifier,值必须是同一个 UUID。视频还需要一个com.apple.quicktime.still-image-time,表示长按时静态画面出现在视频的哪个时间点。

落点容器键/字段作用Demo 取值
静态图JPEG MakerNote17配对标识每次生成新的 UUID
配对视频QuickTime metadatacom.apple.quicktime.content.identifier配对标识与 JPEG 完全一致
配对视频QuickTime metadatacom.apple.quicktime.still-image-time静态帧时间点0 或时长中间点

这三处不是“可选装饰”。少了 JPEG 的配对标识,系统可能只认成普通照片;少了 MOV 的content.identifier,视频无法和图片对齐;少了still-image-time,长按播放的起点可能不对,表现成“能动一下但很快停住”或者“预览正常、保存后不动”。实际排查时,先把这三处读出来,比反复改 UI 控件有效。

读 JPEG 元数据可以用 ImageIO:

import ImageIO func readJPEGMakerNote(_ url: URL) { guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else { print("JPEG 读取失败") return } // MakerNote 里常见键 "17" 存放实况图配对标识 let makerNote = properties[kCGImagePropertyMakerAppleDictionary] print("JPEG MakerNote: \(String(describing: makerNote))") }

逻辑说明:CGImageSourceCreateWithURL打开静态图,CGImageSourceCopyPropertiesAtIndex读取第 0 帧属性。kCGImagePropertyMakerAppleDictionary对应 MakerNote 字典,里面取17就是配对标识。这个读取动作不修改文件,适合在保存前做自检。

参数说明:url必须是本地文件 URL,远程 URL 要先下载到临时目录。读取时不要用UIImage(contentsOfFile:)再重新编码,那样会丢掉 MakerNote。Demo 里建议把读出的字典打印成字符串,和视频元数据并排比较。

视频元数据可以用 AVFoundation 读:

import AVFoundation func readMOVMetadata(_ url: URL) { let asset = AVURLAsset(url: url) for item in asset.metadata { let key = item.key as? String ?? "nil" let value = item.value as? String ?? String(describing: item.value) print("key=\(key), value=\(value)") } }

逻辑说明:AVURLAsset加载视频容器,asset.metadata返回全局元数据项。重点看com.apple.quicktime.content.identifier和com.apple.quicktime.still-image-time是否存在。前者应该是 UUID 字符串,后者应该是数值。

参数说明:如果视频元数据为空,先确认导出时是否真的把metadata赋给了导出会话,而不是只创建了AVMutableMetadataItem却没有挂到导出器上。另一个常见问题是导出预设不支持元数据透传,可以换成 MOV 输出并检查导出后的文件。

2.3 用 PHAssetCreationRequest 做资源写入的选型理由

生成实况图有两条路:一条是用PHLivePhoto.request(withResourceFileURLs:...)拼出预览对象,另一条是用PHAssetCreationRequest把资源写进相册。前者负责“显示”,后者负责“落库”,两者不能互相替代。只调PHLivePhoto.request能拿到可预览的PHLivePhoto,但不会在相册里创建持久化实况图资源;只保存两个文件又缺少配对关系。

import Photos func saveLivePhoto(jpegURL: URL, movURL: URL, completion: @escaping (Result<String, Error>) -> Void) { PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in guard status == .authorized || status == .limited else { completion(.failure(NSError(domain: "PhotoAuth", code: -1))) return } var placeholderID: String? PHPhotoLibrary.shared().performChanges({ let request = PHAssetCreationRequest.forAsset() // 静态图资源 request.addResource(with: .photo, fileURL: jpegURL, options: nil) // 配对视频资源,类型必须是 .pairedVideo request.addResource(with: .pairedVideo, fileURL: movURL, options: nil) placeholderID = request.placeholderForCreatedAsset?.localIdentifier }) { success, error in if let error = error { completion(.failure(error)) } else if success, let id = placeholderID { completion(.success(id)) } else { completion(.failure(NSError(domain: "PhotoSave", code: -2))) } } } }

逻辑说明:PHAssetCreationRequest.forAsset()创建一次资源写入事务,addResource(with: .photo, ...)添加静态图,addResource(with: .pairedVideo, ...)添加配对视频。照片库在同一个事务里看到这两种资源,才会建立实况图关系。placeholderForCreatedAsset?.localIdentifier可以在事务内拿到未来资源的标识,方便保存后跳转或校验。

参数说明:.photo和.pairedVideo的顺序建议静态图在前、配对视频在后,减少读回时的不确定性。options传nil表示使用默认行为;如果临时文件需要保留,不要设置移动语义,保存成功后再自己清理。权限用.addOnly足够保存;如果保存后还要 fetch 读回,需要.readWrite并在工程配置里写好相册读取说明。

注意:保存前先确认 JPEG 和 MOV 的配对标识一致,再调用保存。标识不一致时,保存可能成功,但读回不是实况图,排查成本会更高。

3. 从素材到可播放实况图:Demo 的完整生成链路

这一章按“素材检查 → 裁剪 → 写 JPEG 元数据 → 写 MOV 元数据 → 保存 → 预览”的顺序走。每一步都能单独跑,也都能单独验证。实际项目里最容易省掉的是裁剪和读回校验,但这两步恰恰决定了长按播放是否稳定。

先约定一个最小输入:一张 JPEG 静态图,一段带视频轨的 MOV。静态图内容要和视频第一帧尽量接近,否则长按播放时会出现明显跳变。视频时长控制在 1 到 3 秒,Demo 默认按 3 秒窗口处理。

3.1 素材检查与时长裁剪

素材检查不要只看文件是否存在。视频要能读出时长和视频轨,静态图要能被 ImageIO 打开。时长太短会导致长按没有内容,太长会让文件体积变大,常见做法是截到 3 秒以内,保留第一段。

import AVFoundation struct LivePhotoSource { let imageURL: URL let videoURL: URL let duration: CMTime } enum LivePhotoError: Error { case missingFile case invalidVideo case tooShort } func validateSource(imageURL: URL, videoURL: URL) throws -> LivePhotoSource { guard FileManager.default.fileExists(atPath: imageURL.path), FileManager.default.fileExists(atPath: videoURL.path) else { throw LivePhotoError.missingFile } let asset = AVURLAsset(url: videoURL) let duration = asset.duration guard duration.isValid, duration.seconds > 0 else { throw LivePhotoError.invalidVideo } // Demo 默认 3 秒窗口,短于 1 秒的素材直接拒绝 let target = CMTime(seconds: 3.0, preferredTimescale: 600) let finalDuration = CMTimeMinimum(duration, target) guard finalDuration.seconds >= 1.0 else { throw LivePhotoError.tooShort } return LivePhotoSource(imageURL: imageURL, videoURL: videoURL, duration: finalDuration) }

逻辑说明:先做文件存在性检查,再用AVURLAsset读时长。CMTimeMinimum取“原时长”和“3 秒”中较短的一个,避免把短视频强行拉长。preferredTimescale: 600是为了让时间换算更细,后续裁剪和帧对齐更稳。

参数说明:target可以按产品需求改成 2 秒或 3 秒,但不要低于 1 秒。duration.seconds是浮点值,判断时留一点余量。如果视频没有视频轨,validateSource还应该加asset.tracks(withMediaType: .video).isEmpty检查,避免后面插入轨道时崩。

裁剪可以用组合轨道加透传导出:

func trimVideo(sourceURL: URL, to duration: CMTime, outputURL: URL, completion: @escaping (Result<URL, Error>) -> Void) { let asset = AVURLAsset(url: sourceURL) let composition = AVMutableComposition() guard let videoTrack = asset.tracks(withMediaType: .video).first, let compositionTrack = composition.addMutableTrack( withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid ) else { completion(.failure(LivePhotoError.invalidVideo)) return } do { try compositionTrack.insertTimeRange( CMTimeRange(start: .zero, duration: duration), of: videoTrack, at: .zero ) // 保留原视频方向,否则保存后可能旋转或黑边 compositionTrack.preferredTransform = videoTrack.preferredTransform } catch { completion(.failure(error)) return } guard let exporter = AVAssetExportSession( asset: composition, presetName: AVAssetExportPresetPassthrough ) else { completion(.failure(LivePhotoError.invalidVideo)) return } exporter.outputURL = outputURL exporter.outputFileType = .mov exporter.exportAsynchronously { if let error = exporter.error { completion(.failure(error)) } else { completion(.success(outputURL)) } } }

逻辑说明:AVMutableComposition只插入视频轨的前duration范围,AVAssetExportPresetPassthrough尽量不重编码,减少画质损失和处理时间。preferredTransform必须从原轨道复制到组合轨道,否则竖屏素材导出后方向可能丢失。

参数说明:outputURL必须是尚未存在的本地路径,扩展名建议.mov。duration来自上一步的finalDuration。如果原视频有音频轨,实况图通常不需要声音,Demo 可以只保留视频轨;如果产品要求保留环境音,再额外插入音频轨并检查时长对齐。

3.2 生成配对标识并写入 JPEG 元数据

配对标识建议每次生成新的 UUID,不要用固定字符串。固定字符串会让连续生成的多张实况图在排查时互相干扰,严重时旧资源和新资源看起来“串号”。JPEG 写入要用 ImageIO 复制原图并附加属性,不要用UIImage.jpegData重新编码,否则 MakerNote 很容易丢。

import ImageIO import UniformTypeIdentifiers func writeJPEGMetadata(sourceURL: URL, outputURL: URL, assetIdentifier: String) throws { guard let source = CGImageSourceCreateWithURL(sourceURL as CFURL, nil), let image = CGImageSourceCreateImageAtIndex(source, 0, nil), let destination = CGImageDestinationCreateWithURL( outputURL as CFURL, UTType.jpeg.identifier as CFString, 1, nil ) else { throw LivePhotoError.invalidVideo } var properties = (CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]) ?? [:] var makerNote = (properties[kCGImagePropertyMakerAppleDictionary] as? [String: Any]) ?? [:] // 键 "17" 写入本次生成的 UUID makerNote["17"] = assetIdentifier properties[kCGImagePropertyMakerAppleDictionary] = makerNote CGImageDestinationAddImage(destination, image, properties as CFDictionary) guard CGImageDestinationFinalize(destination) else { throw LivePhotoError.invalidVideo } }

逻辑说明:先从源 JPEG 复制属性字典,再把 MakerNote 里的17替换成新的 UUID,最后用CGImageDestination输出新 JPEG。这样做保留原图的 EXIF、色彩和尺寸信息,只改配对标识。

参数说明:assetIdentifier建议用UUID().uuidString,同一张实况图的 JPEG 和 MOV 必须使用同一个值。outputURL不要和sourceURL相同,避免边读边写导致文件损坏。UTType.jpeg.identifier需要引入UniformTypeIdentifiers,输出格式固定为 JPEG,方便后续写入和调试。

提示:如果读回 JPEG 时发现 MakerNote 为空,先检查是不是用了UIImage重编码。重编码路径会丢掉大量元数据,生成实况图时不要走这条路。

3.3 给 MOV 打 still-image-time 与 content identifier

视频这边要写两个关键元数据:com.apple.quicktime.content.identifier和com.apple.quicktime.still-image-time。前者和 JPEG 的17对齐,后者决定长按播放时静态画面出现的时间点。Demo 里把stillTime设为 0,表示靠近视频第一帧;也可以设成视频时长的一半,让静态画面出现在中段。

import AVFoundation func writeMOVMetadata(sourceURL: URL, outputURL: URL, assetIdentifier: String, stillTime: Double) throws { let asset = AVURLAsset(url: sourceURL) let composition = AVMutableComposition() guard let videoTrack = asset.tracks(withMediaType: .video).first, let compositionTrack = composition.addMutableTrack( withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid ) else { throw LivePhotoError.invalidVideo } try compositionTrack.insertTimeRange( CMTimeRange(start: .zero, duration: asset.duration), of: videoTrack, at: .zero ) compositionTrack.preferredTransform = videoTrack.preferredTransform let contentItem = AVMutableMetadataItem() contentItem.key = "com.apple.quicktime.content.identifier" as NSString contentItem.keySpace = .quickTimeMetadata contentItem.value = assetIdentifier as NSString let stillItem = AVMutableMetadataItem() stillItem.key = "com.apple.quicktime.still-image-time" as NSString stillItem.keySpace = .quickTimeMetadata stillItem.value = NSNumber(value: stillTime) guard let exporter = AVAssetExportSession( asset: composition, presetName: AVAssetExportPresetPassthrough ) else { throw LivePhotoError.invalidVideo } exporter.outputURL = outputURL exporter.outputFileType = .mov exporter.metadata = [contentItem, stillItem] let semaphore = DispatchSemaphore(value: 0) exporter.exportAsynchronously { semaphore.signal() } semaphore.wait() if let error = exporter.error { throw error } }

逻辑说明:用AVMutableComposition重建视频轨,设置preferredTransform保留方向,再把两个AVMutableMetadataItem挂到导出器的metadata上。content.identifier用字符串,still-image-time用数值,键空间都设成.quickTimeMetadata。

参数说明:stillTime建议取 0 或duration.seconds / 2,不要超过视频时长。assetIdentifier必须和 JPEG 的17完全一致,包括大小写。AVAssetExportPresetPassthrough适合已经编码好的 MOV;如果导出后读不到元数据,可以改成AVAssetWriter重写视频并显式添加元数据项,这是更稳但代码更多的路径。

同步等待只适合 Demo 或后台队列,不要在主线程阻塞。实际项目里建议把导出封装成async方法,导出完成后再进入保存步骤。

3.4 保存到相册并做 PHLivePhotoView 预览

保存和预览要分开验证。保存成功后,用PHAsset.fetchAssets读回,确认mediaSubtypes包含.photoLive;预览则用PHLivePhoto.request从本地文件 URL 拼出PHLivePhoto,再交给PHLivePhotoView。

import Photos import PhotosUI final class LivePhotoPreviewController: UIViewController { private let livePhotoView = PHLivePhotoView() private let jpegURL: URL private let movURL: URL init(jpegURL: URL, movURL: URL) { self.jpegURL = jpegURL self.movURL = movURL super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func viewDidLoad() { super.viewDidLoad() livePhotoView.frame = view.bounds livePhotoView.contentMode = .scaleAspectFit view.addSubview(livePhotoView) PHLivePhoto.request( withResourceFileURLs: [jpegURL, movURL], placeholderImage: UIImage(contentsOfFile: jpegURL.path), targetSize: CGSize(width: 1200, height: 1200), contentMode: .aspectFit ) { [weak self] livePhoto, info in guard let self, let livePhoto else { return } self.livePhotoView.livePhoto = livePhoto // .hint 只提示,.full 完整播放 self.livePhotoView.startPlayback(with: .hint) } } }

逻辑说明:PHLivePhoto.request用资源文件 URL 组装预览对象,placeholderImage先在解码完成前占位。拿到livePhoto后赋给PHLivePhotoView,再调用startPlayback提示用户长按。保存到相册则回到第 2.3 节的PHAssetCreationRequest。

参数说明:targetSize控制预览解码尺寸,太小会糊,太大会增加内存压力,1200 左右适合大多数列表和详情页。contentMode用.aspectFit避免裁切。startPlayback(with: .hint)适合首次展示,.full适合用户主动触发。预览正常不代表相册识别正常,两个验证都要做。

4. 避坑与排查:实况图在相册里“不动”的五个常见原因

实况图的问题表现很集中:要么没有角标,要么有角标但长按不播放,要么预览正常保存后方向错,要么连续生成后配对串号。下面几条按“现象 → 原因 → 解决”拆开,基本覆盖 Demo 落地时最常见的翻车点。

4.1 现象:相册里出现一张照片和一段视频,但没有实况图角标

原因通常是把静态图和视频分别保存成了两个独立资源,或者保存视频时用了.video而不是.pairedVideo。照片库看到的是两个普通资源,自然不会合成实况图。另一个隐蔽原因是 JPEG 的 MakerNote 没有写入,系统只把静态图当普通照片。

解决方式是回到PHAssetCreationRequest,在同一个事务里同时添加.photo和.pairedVideo,不要先保存再合并。保存后 fetch 读回mediaSubtypes,确认.photoLive存在。如果不存在,先读 JPEG 的kCGImagePropertyMakerAppleDictionary里有没有17,再读 MOV 的com.apple.quicktime.content.identifier是否一致。

4.2 现象:有实况图角标,长按只动一下就停

原因多半是视频时长太短,或者still-image-time落在有效范围外。视频只有零点几秒时,长按播放几乎没有内容;stillTime超过视频时长时,静态帧定位失败,表现成播放立即结束。还有一种情况是视频第一帧和 JPEG 内容差异太大,播放时看起来像“跳一下”。

解决方式是把视频裁到 1 到 3 秒,stillTime取 0 或时长中间点,确保不超过视频时长。静态图尽量选视频第一帧附近的画面。读回 MOV 元数据,确认still-image-time是数值而不是字符串,且content.identifier与 JPEG 的17对齐。

4.3 现象:预览正常,保存后方向旋转或出现黑边

原因是AVMutableComposition插入视频轨后没有继承原视频的preferredTransform。预览阶段可能直接读原文件,保存阶段读的是重新导出的 MOV,方向信息丢失后就会旋转。另一个原因是导出尺寸和静态图比例不一致,预览用.aspectFit看不出来,相册里就出现黑边。

解决方式是在插入轨道后立即设置compositionTrack.preferredTransform = videoTrack.preferredTransform,导出后再读一次naturalSize和preferredTransform。静态图比例和视频比例尽量一致,至少保持同一方向。如果产品允许裁切,在生成前统一做一次居中裁剪,别把问题留到相册里。

4.4 现象:连续生成多张后,配对串号或旧图覆盖新图

原因是assetIdentifier用了固定字符串,或者临时文件路径固定,后一次生成覆盖了前一次的文件。异步导出没有做串行化时,两个任务可能同时写同一个输出路径,最后保存的资源和元数据对不上。表现是打开 A 图却播放 B 视频,或者旧实况图突然失效。

解决方式是每次生成新的UUID().uuidString,JPEG、MOV 和临时目录都带同一个 UUID 后缀。导出任务串行执行,保存成功后再清理临时文件。调试时把assetIdentifier打进日志,保存后读回比对,出现串号能第一时间定位。

4.5 排查清单:从相册资源到元数据逐层确认

排查不要一上来就改 UI。先确认相册资源类型,再确认 JPEG 配对标识,再确认 MOV 两个元数据,最后才看预览控件。下面这段代码可以把关键信息一次打印出来:

import AVFoundation import ImageIO func debugLivePhotoPair(jpegURL: URL, movURL: URL) { // 1. JPEG MakerNote 里的 "17" if let source = CGImageSourceCreateWithURL(jpegURL as CFURL, nil), let props = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] { print("JPEG MakerNote:", props[kCGImagePropertyMakerAppleDictionary] ?? "nil") } // 2. MOV 的全局元数据 let asset = AVURLAsset(url: movURL) for item in asset.metadata { print("MOV meta:", item.keySpace?.rawValue ?? "nil", item.key ?? "nil", item.value ?? "nil") } // 3. 视频轨与时长 print("duration:", asset.duration.seconds) print("videoTracks:", asset.tracks(with <p> <a href="https://download.csdn.net/download/lyh1083908486/87521908" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 2:08:59

Windows下Qt Creator+MSVC+CUDA环境配置与编译避坑

简介&#xff1a;面向Windows平台希望在Qt Creator中启用CUDA开发的初学者与中级开发者&#xff0c;这份示例工程围绕“qtcreator中添加cuda”的实际配置场景&#xff0c;提供可对照的工程组织方式&#xff0c;帮助理解如何在Qt项目中纳入.cu源文件并衔接nvcc编译流程。rar压缩…

作者头像 李华
网站建设 2026/10/26 2:08:43

DirectX 9.0 SDK 免安装版配置指南:VS2017~VS2022 通用实战

简介&#xff1a;本资源为DirectX 9.0 SDK免安装精简版&#xff0c;面向Windows平台C游戏开发初学者与图形编程学习者&#xff0c;解决传统SDK安装繁琐、环境配置复杂的问题&#xff0c;支持快速接入Direct3D 9、DirectSound、DirectInput等核心组件开发。压缩包为RAR格式&…

作者头像 李华
网站建设 2026/10/26 2:07:42

前方交会测试程序:点投影系数法实战指南

简介&#xff1a;这是一套面向测绘工程、GIS专业学生及工程测量技术人员的前方交会定位算法实践资源&#xff0c;聚焦点投影系数法这一经典三角测量解算模型&#xff0c;解决无直接通视条件下的目标点坐标精确定位问题&#xff0c;适用于无人机航拍控制点布设、复杂地形勘测等实…

作者头像 李华
网站建设 2026/10/26 2:07:07

人行道边界感知网络:面向轻量部署的双分支检测方案

简介&#xff1a;本资源是一份面向计算机视觉初学者与深度学习实践者的完整人行道检测项目实战包&#xff0c;聚焦智能交通与自动驾驶场景下的语义分割与目标检测任务&#xff0c;以Python为核心实现技术栈。压缩包共32个文件&#xff0c;涵盖6个核心Python脚本&#xff08;含模…

作者头像 李华
网站建设 2026/10/26 2:07:06

Utgard对接OPC DA实战:Java工业数据采集落地指南

简介&#xff1a;本资源是面向工业自动化、物联网及MES系统开发者的Java OPC数据采集实践方案&#xff0c;聚焦UTGard开源库在Windows平台对接OPC DA服务器的核心能力。资源解决Java应用中稳定接入OPC服务、应对网络抖动导致连接中断等典型工程难题&#xff0c;特别适用于需高可…

作者头像 李华
网站建设 2026/10/26 2:07:06

C# WPF图像控件与OpenCV ROI交互实现

简介&#xff1a;面向WPF机器视觉开发者的C#图像控件与ROI管理示例&#xff0c;聚焦仿Halcon HSmartWindowControl的交互体验&#xff0c;解决图像加载显示、感兴趣区域绘制与增删改管理等常见需求&#xff0c;适合具备一定C#与WPF基础、希望理解视觉界面控件设计的开发者参考。…

作者头像 李华