技术研究之大疆智图

标签: 无 分类: 未分类 创建时间:2025-01-09 03:49:38 更新时间:2025-04-14 11:36:54

1.前言

最近要进行农业行业的应用,还需要使用到三维立体的功能,于是就去尝试了大疆智图。大疆智图API 在2024年1月25日发布了第一个版本,公测一年之内免费,后面收费再说,等我使用的时候,已经2025年01月09日了,都快过期了。

大疆智图是一款提供自主航线规划、飞行航拍、二维正射影像与三维模型重建的 PC 应用软件。一站式解决方案帮助行业用户全面提升航测内外业效率,将真实场景转化为数字资产。

参考文章:
【1】.Rainbow Cloud航线规划 这个网站可以购买建模的一些功能,好像比大疆智图要便宜很多。

2.正射影像

正射影像或者倾斜摄影进行航线规划时需要注意的点:

  • 尽量选择晴朗、能见度好的天气条件进行航摄;
  • 数据采集后,及时检查影像成像质量,如亮度、清晰度等;
  • 航测过程,为了确保精灵Phantom 4 RTK解算稳定,应避免电磁干扰、遮挡干扰比较严重的区域,同时确保遥控器与飞行器连接良好;
  • 确保足够的航向及旁向重叠率,推荐航向重叠率80%,旁向重叠度70%。根据地形起伏条件,可适当调整重叠率。
  • 推荐设置的航向重叠率为 80%,旁向重叠率为 70%,适用于大部分场景。对于地形起伏变化较大的区域,地形最低点和最高点重叠度相差过大的情况下,为了保证最高点处的重叠度,可以适当增加重叠率。地势平坦的区域,整体重叠度接近,可以适当减少重叠率,以增加航测效率,减少数据处理量,但需确保航向重叠率不低于 65%,旁向重叠率不低于 60%。
参考文章:
【1】.大疆智图常见问题 这里是官方的一些问题回答,好像能解决大部分的问题。

3.API

大疆智图API 申请相关的api。

注意
这里会遇到一个问题,因为token有效期只有10分钟,当文件数量较多的时候,一个有效期内还不一定能上传完成。

参考文章:
【1】.大疆智图api(java实现) 这里有具体的代码,可以进行参考。
【2】.大疆智图 API 重磅上线!1 年免费,赋能多个行业应用 这是一个手机版的网站,不太美观
【3】.大疆智图api web端中接入使用 这里是用的 python 脚本的方式
【4】.Java开发者的大疆制图API探索之旅:从入门到实战 1.申请账号与准备素材;2.Java 环境配置与依赖引入;3.请求封装与签名生成;4.获取 Token 与上传素材;5.Java 整合大疆制图 API 实战。
【5】.大疆智图 API 云端服务 api接口说明,这个更详细一点。

4.激光点云

激光点云处理,激光雷达(LiDAR)获取的点云数据包含大量的噪声和无效点(如地面点、植被点、建筑物点等),这些噪声和无效点会影响后续的数据分析和应用。滤波处理的目的是去除噪声、分离地面点和非地面点,从而提取出有用的信息。

  • PDAL
    PDAL(Point Data Abstraction Library)是一个开源的点云数据处理库,旨在为开发者提供高效、灵活的点云数据处理能力。PDAL支持多种点云数据格式,并提供了丰富的处理功能,包括数据读取、转换、过滤、分类等。无论是在地理信息系统(GIS)、遥感、测绘还是其他需要处理点云数据的领域,PDAL都能提供强大的支持。

  • PCL
    点云数据的处理可以采用获得广泛应用的Point Cloud Library (点云库,PCL库)。PCL库是一个最初发布于2013年的开源C++库。它实现了大量点云相关的通用算法和高效的数据管理。

参考文章:
【1】.稠密的无人机激光雷达点云数据处理与分析方法与工具科普系列 这个没啥有用的东西
【2】.PDAL:点云数据处理的强大工具 显著特点:高性能、跨平台支持、丰富的数据格式支持、灵活的插件机制、完善的文档和社区支持
【3】.PCL(Point Cloud Library)学习指南&资料推荐(2025版)
【4】.PCL滤波介绍(1) 在获取点云数据时 ,由于设备精度,操作者经验环境因素带来的影响,以及电磁波的衍射特性,被测物体表面性质变化和数据拼接配准操作过程的影响,点云数据中讲不可避免的出现一些噪声。在点云处理流程中滤波处理作为预处理的第一步,对后续的影响比较大,只有在滤波预处理中将噪声点 ,离群点,孔洞,数据压缩等按照后续处理定制,才能够更好的进行配准,特征提取,曲面重建,可视化等后续应用处理,PCL中点云滤波模块提供了很多灵活实用的滤波处理算法,例如:双边滤波,高斯滤波,条件滤波,直通滤波,基于随机采样一致性滤波,

5.Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
public class DjiMapUtil {

// 大疆智图API的APP Key
private static final String DJI_APP_KEY = "HNAIUd6FhsnGECI59rHVj";
// 大疆智图API的Secret Key
private static final String DJI_SECRET_KEY = "V2NbU9nwpZets9axVXhMo6umWFni4nMp";
// 大疆智图API 接口请求地址
private static final String HOST = "https://openapi-cn.dji.com";
// 获取Token的请求路径
public static final String URI_TOKEN = "/terra-rescon-be/v2/store/obtain_token"; // 获取token
public static final String RESOURCE_BITCH="/terra-rescon-be/v2/store/upload_callback"; // 关联资源


// 大疆智图API的外网访问网址,默认 https://oss-cn-hangzhou.aliyuncs.com
public static final String ENDPOINT = "https://oss-cn-hangzhou.aliyuncs.com";

/*
* URL 大疆智图API 接口请求地址
* payload 请求参数,如果不需要请求参数,请为空字符串
* lowerMethod 请求方法,小写字母 get/post/put/delete
*/
public static JSONObject AccessDJ(String URI, String payload, String lowerMethod) {
try {
String url = HOST + URI;
String date = getFormattedDate();
String digest = getDigest(payload);
String requestSignature = calculateSignature(date, lowerMethod, URI, digest);
HttpRequest request = null;
if (lowerMethod.equals("post")) {
request = HttpRequest.post(url);
} else if (lowerMethod.equals("get")) {
request = HttpRequest.get(url);
}
request.header("Date", date)
.header("Digest", "SHA-256=" + digest)
.header("Authorization", "hmac username=\"" + DJI_APP_KEY + "\", algorithm=\"hmac-sha256\", headers=\"date @request-target digest\", signature=\"" + requestSignature + "\"")
.header("Content-Type", "application/json;charset=UTF-8");
if (!payload.isEmpty()) {
request.body(payload);
}
HttpResponse response = request.execute();
if (response.isOk()) {
log.info("response.body() = " + response.body());
return JSONUtil.parseObj(response.body());
} else {
log.error("response = " + response);
return new JSONObject();
}
} catch (Exception e) {
log.error(e.getMessage());
return new JSONObject();
}
}

/**
* 时间格式化
* @return
*/
private static String getFormattedDate() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).withZone(ZoneId.of("GMT"));
return formatter.format(ZonedDateTime.now(ZoneId.of("GMT")));
}

/**
* 日期加密
* @param data
* @return
* @throws NoSuchAlgorithmException
*/
private static String getDigest(String data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}

/**
* 计算签名
* @param date
* @param method
* @param uri
* @param digest
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static String calculateSignature(String date, String method, String uri, String digest) throws NoSuchAlgorithmException, InvalidKeyException {
String content = "date: " + date + "\n@request-target: " + method + " " + uri + "\ndigest: SHA-256=" + digest;
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
hmacSha256.init(new SecretKeySpec(DJI_SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] signatureBytes = hmacSha256.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signatureBytes);
}

/**
* 获取请求 Token
* @return
*/
public static JSONObject getAccessToken() {
JSONObject data = new JSONObject();
JSONObject resultData= AccessDJ(URI_TOKEN, "", "post");
JSONObject result=resultData.getJSONObject("result");
if(result!=null){
Integer code=result.getInt("code");
if(code==0){
data=resultData.getJSONObject("data");
}
}
return data;
}

/**
* 上传文件
* @param ACCESS_KEY_ID
* @param SECRET_ACCESS_KEY
* @param BUCKET_NAME
* @param STORE_PATH
* @return
* @throws IOException
*/
public static JSONObject uploadFile(String ACCESS_KEY_ID, String SECRET_ACCESS_KEY,String SESSION_TOKEN,String BUCKET_NAME, String STORE_PATH,String FILE_PATH) throws IOException {
JSONObject resultJSON = new JSONObject();
try {
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY,SESSION_TOKEN);
// 构建上传文件的路径,包含STORE_PATH和文件名
String objectName = STORE_PATH + new File(FILE_PATH).getName();
// 上传文件
PutObjectResult result = ossClient.putObject(BUCKET_NAME, objectName, new File(FILE_PATH));
resultJSON.putOnce("name", objectName);
String etag = result.getETag();
resultJSON.putOnce("etag", etag);
resultJSON.putOnce("checksum", etag);
// 关闭OSSClient
ossClient.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
return resultJSON;
}

/**
* 绑定资源
* @param resourceUuid
* @param callbackParam
* @param files
* @return
*/
public static boolean bindBatchFiles(String resourceUuid, String callbackParam, JSONArray files){
JSONObject payload = new JSONObject();
payload.put("resourceUUID", resourceUuid);
payload.putOnce("callbackParam",callbackParam);
payload.putOnce("files",files);
String payloadStr=JSONUtil.toJsonStr(payload);
JSONObject resultData=AccessDJ(RESOURCE_BITCH,payloadStr,"post");
JSONObject result=resultData.getJSONObject("result");
if(result!=null){
Integer code=result.getInt("code");
if(code==0){
return true;
}
}
return false;
}
}

问题

(1)上传文件总是关联不上
开发第一个,遇到的问题就是上传文件之后,虽然上传到了大疆的阿里云,但是关联文件的时候,虽然返回的是关联成功,但是查询文件个数,还是为空。

【尝试方案】
(1) 开始的时候,我是一个文件一个文件上传的,后来改成了一个 ossclient 上传多个文件。
(2) 我尝试将文件夹里面的文件减少,结果还真是可以的,就是只上传几个文件,然后就关联,结果能关联上。
(3) 我以为是 token 十分钟过期的问题,于是我还专门弄了一个 十分钟过期,然后重新申请 token 的方法,结果不行。
(5) 最后我只能是多方面的检查,还是不行,浪费了很多时间。
(6) 尝试增大这个文件数量,结果出现了新的问题: Access denied by authorizer’s policy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public static boolean uploadFile(String resourceUuid,String FILE_PATH) throws IOException {
try {
if(redisUtil == null){
redisUtil= SpringContextUtils.getApplicationContext().getBean(RedisUtil.class);
}
JSONArray uploadedFiles = new JSONArray();
OSS ossClient = null;
/**
* 判断上传 token 是否在有效期内,
* 如果即将失效,则需要先执行文件关联任务,然后再进行文件上传
* 否则直接进行上传文件
* 除此之外,还有一个每次只能关联50个文件的限制,需要做进一步的处理
*/
File localFolder = new File(FILE_PATH); //获取其file对象
File[] fs = localFolder.listFiles(); //遍历path下的文件和目录,放在File数组中
for(File f:fs){ //遍历File[]数组
if(!f.isDirectory()){
// long expire = redisUtil.getExpire("dji_map_access_token");
// if(expire<=500&&expire>0){ // 文件关联
// JSONObject accessToken = DjiMapUtil.getAccessToken();
// String callbackParam=accessToken.getStr("callbackParam");
// JSONArray uploadList=new JSONArray();
// int a=0;
// for (int i = 0; i < uploadedFiles.size(); i++) {
// JSONObject upload=uploadedFiles.getJSONObject(i);
// uploadList.add(upload);
// a++;
// if(a>=9){
// DjiMapUtil.bindBatchFiles(resourceUuid,callbackParam,uploadList);
// uploadList.clear();
// a=0;
// }
// }
// // 关联剩余的文件
// DjiMapUtil.bindBatchFiles(resourceUuid,callbackParam,uploadList);
//
// // 清空文件列表
// uploadedFiles.clear();
// redisUtil.del("dji_map_access_token");
// ossClient=null;
// }

// 重新获取 token
JSONObject accessToken = DjiMapUtil.getAccessToken();
String ACCESS_KEY_ID = accessToken.getStr("accessKeyID");
String SECRET_ACCESS_KEY = accessToken.getStr("secretAccessKey");
String BUCKET_NAME = accessToken.getStr("cloudBucketName");
String SESSION_TOKEN = accessToken.getStr("sessionToken");
String STORE_PATH = accessToken.getStr("storePath");
STORE_PATH=STORE_PATH.substring(0,STORE_PATH.indexOf("{fileName}"));
String callbackParam=accessToken.getStr("callbackParam");
if(ossClient==null){
ossClient=new OSSClientBuilder().build(ENDPOINT,ACCESS_KEY_ID,SECRET_ACCESS_KEY,SESSION_TOKEN);
}

// 上传文件
Path path = Paths.get(f.getPath());
String relativePath = localFolder.toURI().relativize(path.toUri()).getPath();
String ossFilePath = STORE_PATH + relativePath.replace("\\", "/");
try {
PutObjectResult putResult = ossClient.putObject(BUCKET_NAME, ossFilePath, path.toFile());
String etag = putResult.getETag();
System.out.println("Uploaded: " + path + " -> " + ossFilePath + ", etag: " + etag);

JSONObject fileInfo = new JSONObject();
fileInfo.put("name", relativePath.replace("\\", "/"));
fileInfo.put("etag", etag);
fileInfo.put("checksum", etag);
uploadedFiles.add(fileInfo);

} catch (Exception e) {
log.error(e.getMessage());
}

// 关联文件
if(uploadedFiles.size()>=50){
DjiMapUtil.bindBatchFiles(resourceUuid,callbackParam,uploadedFiles);
uploadedFiles.clear();
ossClient.shutdown();
ossClient=null;
// 重新进行token生成
redisUtil.del("dji_map_access_token");
}
}
}
// 绑定剩余的文件
if(!uploadedFiles.isEmpty()){
JSONObject accessToken = DjiMapUtil.getAccessToken();
String callbackParam=accessToken.getStr("callbackParam");
DjiMapUtil.bindBatchFiles(resourceUuid,callbackParam,uploadedFiles);
uploadedFiles.clear();
ossClient.shutdown();
// 重新进行token生成
redisUtil.del("dji_map_access_token");
}
if(ossClient!=null){
ossClient.shutdown();
}
return true;
}catch (Exception e){
log.error("uploadFile",e);
}
return false;
}

【解决方案】
解决方案就是每次绑定文件之后,重新获取新的上传的token信息,这样可以保证绑定和上传的 token 和 callbackParam 等参数一致。

参考文章:
【1】.大疆智图api出现1008错误
【2】. DjZhiTu.java 这里用了递归遍历的方法进行文件上传的。
【3】.Java Files.walk方法具有什么功能呢?
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
bibichuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。