Hanjie's Blog

一只有理想的羊驼

我们希望对输入图片根据Distort程序进行畸变处理:

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
void Distort(const float &x_src,
const float &y_src,
const cv::Mat &control_points,
const float &r2,
const cv::Mat &W,
float &x_dst,
float &y_dst) {

int pts_num = control_points.rows;

x_dst = 0;
y_dst = 0;
for (int i = 0; i < pts_num; i++) {
float x_diff = x_src - control_points.at<cv::Vec2f>(i)[0];
float y_diff = y_src - control_points.at<cv::Vec2f>(i)[1];

float kernel = 1.f / sqrt(x_diff * x_diff + y_diff * y_diff + r2);

x_dst += kernel * W.at<float>(i, 0);
y_dst += kernel * W.at<float>(i, 1);
}

x_dst += (W.at<float>(pts_num, 0) + W.at<float>(pts_num + 1, 0) * x_src + W.at<float>(pts_num + 2, 0) * y_src);
y_dst += (W.at<float>(pts_num, 1) + W.at<float>(pts_num + 1, 1) * x_src + W.at<float>(pts_num + 2, 1) * y_src);
}
输入图像 处理后图像
chessboard_input chessboard_warp

可以根据Distort程序,生成OpenCV中cv::remap函数所需的map1map2映射矩阵,然后再使用cv::remap对输入图片进行处理。为了加速这个图像处理速度,尝试使用OpenGL,通过显卡实现类似于cv::remap函数的功能:

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
/*
* @Author: Hanjie Luo luohanjie@gmail.com
* @Date: 2023-01-11 16:55:10
* @LastEditors: Hanjie Luo luohanjie@gmail.com
* @LastEditTime: 2023-01-29 19:24:09
* @FilePath: /my_slam/tests/OpenGL/test_distorted_image.cpp
* @Description:
*
* Copyright (c) 2023 by CVTE, All Rights Reserved.
*/

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <thread>

// Include GLEW
#include <GL/glew.h>

// Include GLFW
#include <GLFW/glfw3.h>

#include <opencv2/opencv.hpp>

// shader
// -------------------------------------------------------------------------------------------
// The vertex shader is a program on the graphics card that processes each vertex and its attributes as they appear in the vertex array.
// Its duty is to output the final vertex position in device coordinates(Device X and Y coordinates are mapped to the screen between -1 and 1. y axis is positive above the center. x axis is positive in the right the center.) and to output any data the fragment shader requires.
// That's why the 3D transformation should take place here. The fragment shader depends on attributes like the color and texture coordinates, which will usually be passed from input to output without any calculations.

// Apart from the regular C types, GLSL has built-in vector and matrix types identified by vec* and mat* identifiers. The type of the values within these constructs is always a float.
// The number after vec specifies the number of components (x, y, z, w) and the number after mat specifies the number of rows /columns. Since the position attribute consists of only an X and Y coordinate, vec2 is perfect.
// The final position of the vertex is assigned to the special gl_Position variable, because the position is needed for primitive assembly and many other built-in processes. For these to function correctly, the last value w needs to have a value of 1.0f.

// Each shader can specify inputs and outputs using in and out keywords and wherever an output variable matches with an input variable of the next shader stage they're passed along. The vertex and fragment shader differ a bit though.
// The vertex shader receives its input straight from the vertex data.

const char* vertex_shader_source =
"#version 330 core\n"
"layout(location=0) in vec2 Position;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(Position, 0.0, 1.0);\n"
"}\0";

// The output from the vertex shader is interpolated over all the pixels on the screen covered by a primitive. These pixels are called fragments and this is what the fragment shader operates on. Just like the vertex shader it has one mandatory output, the final color of a fragment.
// You'll immediately notice that we're not using some built-in variable for outputting the color, say gl_FragColor. This is because a fragment shader can in fact output multiple colors.
// The outColor variable uses the type vec4, because each color consists of a red, green, blue and alpha component. Colors in OpenGL are generally represented as floating point numbers between 0.0 and 1.0 instead of the common 0 and 255.

// uniforms are essentially global variables, having the same value for all vertices and/or fragments.
// Changing the value of a uniform is just like setting vertex attributes, you first have to grab the location:
// GLint uni_color = glGetUniformLocation(shader_program, "TriangleColor");
// The values of uniforms are changed with any of the glUniformXY functions, where X is the number of components and Y is the type. Common types are f (float), d (double) and i (integer).
// glUniform3f(uni_color, 1.0f, 1.0f, 0.0f);

// The fragment shader should also have access to the texture object, but how do we pass the texture object to the fragment shader?
// GLSL has a built-in data-type for texture objects called a sampler that takes as a postfix the texture type we want e.g. sampler1D, sampler3D or in our case sampler2D.
// We can then add a texture to the fragment shader by simply declaring a uniform sampler2D that we later assign our texture to.
// To sample the color of a texture we use GLSL's built-in texture function that takes as its first argument a texture sampler and as its second argument the corresponding texture coordinates.
// The texture function then samples the corresponding color value using the texture parameters we set earlier. The output of this fragment shader is then the (filtered) color of the texture at the (interpolated) texture coordinate.

// texelFetch is quite different from texture.
// texture is your usual texture access function which handles filtering and normalized ([0,1]) texture coordinates.
// texelFetch directly accesses a texel in the texture (no filtering) using unnormalized coordinates (e.g. (64,64) in the middle-ish texel in a 128x128 texture vs (.5,.5) in normalized coordinates).

const char* fragment_shader_source =
"#version 330 core\n"
"out vec4 FragColor;\n"
"uniform sampler2D FragTexture;\n"
"uniform sampler2D LutTexture;\n"
"uniform float Width;\n"
"uniform float Height;\n"
"void main()\n"
"{\n"
" vec2 uv = texture(LutTexture, vec2(gl_FragCoord.x/Width, gl_FragCoord.y/Height)).rg;\n"
" FragColor = texture(FragTexture, uv);\n"
"}\0";
// -------------------------------------------------------------------------------------------


cv::Mat UpdateImage(const cv::Mat &img, const std::string &str) {
cv::Mat img_tmp = img.clone();
cv::putText(img_tmp, "Frame: " + str, cv::Point(20, img_tmp.rows - 50), cv::FONT_HERSHEY_TRIPLEX, 2, cv::Scalar(0, 255, 0), 2, 8, 0);
cv::flip(img_tmp, img_tmp, 0);
return img_tmp;
}

void SaveImage(GLFWwindow *window, const std::string &file) {
int width, height;
glfwGetFramebufferSize(window, &width, &height);

cv::Mat img = cv::Mat(height, width, CV_8UC3);

// ===================================
// Color Image
// opengl 默认要求内存存储的宽度被4整除
// img.step: 640, img.elemSize(): 1
glPixelStorei(GL_PACK_ALIGNMENT, (img.step & 3) ? 1 : 4);
glPixelStorei(GL_PACK_ROW_LENGTH,
(GLint)(img.step / img.elemSize()));
glReadPixels(0, 0, img.cols, img.rows, GL_BGR,
GL_UNSIGNED_BYTE, img.data);
// ===================================

cv::flip(img, img, 0);
cv::imwrite(file, img);

std::cout<<"frame buffer width: "<<width<<" height: "<<height<<std::endl;

}

void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
glfwSetWindowShouldClose(window, true);
} else if (key == GLFW_KEY_S && action == GLFW_PRESS) {
SaveImage(window, "/Users/luohanjie/Downloads/img_screen_right_warp_gl.png");
}
}

void Distort(const float &x_src,
const float &y_src,
const cv::Mat &control_points,
const float &r2,
const cv::Mat &W,
float &x_dst,
float &y_dst) {

int pts_num = control_points.rows;

x_dst = 0;
y_dst = 0;
for (int i = 0; i < pts_num; i++) {
float x_diff = x_src - control_points.at<cv::Vec2f>(i)[0];
float y_diff = y_src - control_points.at<cv::Vec2f>(i)[1];

float kernel = 1.f / sqrt(x_diff * x_diff + y_diff * y_diff + r2);

x_dst += kernel * W.at<float>(i, 0);
y_dst += kernel * W.at<float>(i, 1);
}

x_dst += (W.at<float>(pts_num, 0) + W.at<float>(pts_num + 1, 0) * x_src + W.at<float>(pts_num + 2, 0) * y_src);
y_dst += (W.at<float>(pts_num, 1) + W.at<float>(pts_num + 1, 1) * x_src + W.at<float>(pts_num + 2, 1) * y_src);
}


int main(void) {
// Read Image
// -------------------------------------------------------------------------------------------
std::string img_file = "/Users/luohanjie/Workspace/Vision/virtual2real_alignment_calibration/data/chessboard_35_63.tiff";
cv::Mat img = cv::imread(img_file, 1);

// As mentioned before, OpenGL expects the first pixel to be located in the bottom-left corner, which means that textures will be flipped when loaded with directly.
// To counteract that, the code in the tutorial will use flipped Y coordinates for texture coordinates from now on.
// That means that 0, 0 will be assumed to be the top-left corner instead of the bottom-left. This practice might make texture coordinates more intuitive as a side-effect.
cv::flip(img, img, 0);

int img_width = img.cols;
int img_height = img.rows;

std::cout<<"Read image with size width: "<<img_width<<" height: "<<img_height<<std::endl;
// -------------------------------------------------------------------------------------------

// Read Screen Calibration Data
// -------------------------------------------------------------------------------------------
std::string screen_calibration_file = "/Users/luohanjie/Workspace/Vision/virtual2real_alignment_calibration/data/V3_0001_v2/screen/screen_calibration.xml";
cv::FileStorage fs;
fs.open(screen_calibration_file, cv::FileStorage::READ);
if (!fs.isOpened()) {
std::cout<<"Error! Can not open " <<screen_calibration_file<<std::endl;
return 0;
}

cv::Mat control_points, weights;
float r2;
// fs["control_points_left"] >> control_points;
// fs["weights_left"] >> weights;
// fs["r2_left"] >> r2;

fs["control_points_right"] >> control_points;
fs["weights_right"] >> weights;
fs["r2_right"] >> r2;
// -------------------------------------------------------------------------------------------

// Generate Texcoords LUT
// -------------------------------------------------------------------------------------------
// By default, gl_FragCoord assumes a lower-left origin for window coordinates and assumes pixel centers are located at half-pixel coordinates. For example, the (x, y) location (0.5, 0.5) is returned for the lower-left-most pixel in a window"
cv::Mat lut = cv::Mat::zeros(img_height, img_width, CV_32FC2);
float x_distort, y_distort, x_cv, y_cv, x_tex, y_tex;
// float x_frag, y_frag;
for(int y = 0; y < img_height; y++) {
// y_frag = y + 0.5f;
// y_cv = float(img_height) - y_frag - 0.5f;
y_cv = float(img_height) - y - 1;
for(int x = 0; x < img_width; x++) {
// x_frag = x + 0.5f;
// x_cv = x_frag - 0.5f;
x_cv = x;

Distort(x_cv, y_cv, control_points, r2, weights, x_distort, y_distort);
// std::cout<<"["<<x<<", "<<y<<"] -> ["<<x_distort<<", "<<y_distort<<"]"<<std::endl;

x_tex = (x_distort + 0.5f) / float(img_width);
y_tex = 1.f - (y_distort + 0.5f) / float(img_height);

lut.at<cv::Vec2f>(y, x)[0] = x_tex;
lut.at<cv::Vec2f>(y, x)[1] = y_tex;

}

}
// -------------------------------------------------------------------------------------------


// GLFW
// -------------------------------------------------------------------------------------------
glfwInit();

// The glfwWindowHint function is used to specify additional requirements for a window.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// The GLFW_OPENGL_PROFILE option specifies that we want a context that only supports the new core functionality.
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

// While the size of a window is measured in screen coordinates, OpenGL works with pixels. On some machines screen coordinates and pixels are the same, but on others they will not be. There is a second set of functions to retrieve the size, in pixels, of the framebuffer of a window.
// On Mac OS X, GLFW reports screen sizes and framebuffer sizes properly if upscaling is enabled in the OS. E.g. on a "retina" display at 2x upscaling, creating a window at 900x600 screen size will result in a 1800x1200 frame buffer size.
glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GL_FALSE);
#endif
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

// The first two parameters of glfwCreateWindow specify the width and height of the drawing surface and the third parameter specifies the window title.
// The fourth parameter should be set to NULL for windowed mode and glfwGetPrimaryMonitor() for fullscreen mode. The last parameter allows you to specify an existing OpenGL context to share resources like textures with.
GLFWwindow* window = glfwCreateWindow(img_width, img_height, "test_distorted_image", NULL, NULL); // Windowed
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}

// After creating the window, the OpenGL context has to be made active
glfwMakeContextCurrent(window);

// Define the viewport dimensions
// int buffer_width, buffer_height;
// glfwGetFramebufferSize(window, &buffer_width, &buffer_height);
// std::cout<<"glfwGetFramebufferSize width: "<<buffer_width<<" height: "<<buffer_height<<std::endl;
// glViewport(0, 0, buffer_width, buffer_height);
// -------------------------------------------------------------------------------------------

// GLEW
// -------------------------------------------------------------------------------------------
// The glewExperimental line is necessary to force GLEW to use a modern OpenGL method for checking if a function is available.
glewExperimental = GL_TRUE;
// Initialize GLEW
if (glewInit() != GLEW_OK) {
std::cout << "Failed to initialize GLEW" << std::endl;
glfwTerminate();
return -1;
}
// -------------------------------------------------------------------------------------------

// Shaders
// -------------------------------------------------------------------------------------------
// The graphics pipeline: {vertices} -> vertex shader -> shape assembly -> geometry shader -> rasterization -> fragment shader -> tests and blending
// Shaders are written in a C-style language called GLSL (OpenGL Shading Language). OpenGL will compile your program from source at runtime and copy it to the graphics card.
// creating a shader object and loading data into it.
// Unlike VBOs, you can simply pass a reference to shader functions instead of making it active or anything like that.
GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
// This function copies the source code in the string specified by parameter string and associates it with the shader object identified by parameter shader, which is the identifier returned by glCreateShader.
// The count parameter specifies how many strings are present in the string parameter, in our case this is 1 since vertex_shader is one long string.
// The last parameter can contain an array of source code string lengths, passing NULL simply makes it stop at the null terminator.
glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
// compiling the shader into code that can be executed by the graphics card now:
glCompileShader(vertex_shader);
// check for shader compile errors
GLint success;
char info_log[512];
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
if (success != GL_TRUE) {
glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< info_log << std::endl;
}

GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
glCompileShader(fragment_shader);
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if (success != GL_TRUE) {
glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"
<< info_log << std::endl;
}

// Up until now the vertex and fragment shaders have been two separate objects. While they've been programmed to work together, they aren't actually connected yet. This connection is made by creating a program out of these two shaders.
GLuint shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader);
glAttachShader(shader_program, fragment_shader);

// Since a fragment shader is allowed to write to multiple framebuffers, you need to explicitly specify which output is written to which framebuffer. This needs to happen before linking the program. However, since this is 0 by default and there's only one output right now, the following line of code is not necessary
// glBindFragDataLocation(shader_program, 0, "FragColor");

// After attaching both the fragment and vertex shaders, the connection is made by linking the program. It is allowed to make changes to the shaders after they've been added to a program (or multiple programs!),
// but the actual result will not change until a program has been linked again. It is also possible to attach multiple shaders for the same stage (e.g. fragment) if they're parts forming the whole shader together.
glLinkProgram(shader_program);
// check for linking errors
glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shader_program, 512, NULL, info_log);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
<< info_log << std::endl;
}

// A shader object can be deleted with glDeleteShader, but it will not actually be removed before it has been detached from all programs with glDetachShader.
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);

// To actually start using the shaders in the program, you just have to call glUseProgram. Just like a vertex buffer, only one program can be active at a time.
// Every shader and rendering call after glUseProgram will now use this program object (and thus the shaders).
glUseProgram(shader_program);
// -------------------------------------------------------------------------------------------


// Vertex Array Objects(VAO)
// -------------------------------------------------------------------------------------------
// You can imagine that real graphics programs use many different shaders and vertex layouts to take care of a wide variety of needs and special effects. Changing the active shader program is easy enough with a call to glUseProgram,
// but it would be quite inconvenient if you had to set up all of the attributes again every time. Luckily, OpenGL solves that problem with Vertex Array Objects (VAO). VAOs store all of the links between the attributes and your VBOs with raw vertex data.
// A Vertex Array Object (or VAO) is an object that describes how the vertex attributes are stored in a Vertex Buffer Object (or VBO). This means that the VAO is not the actual object storing the vertex data, but the descriptor of the vertex data.
// Vertex attributes can be described by the glVertexAttribPointer function and its two sister functions glVertexAttribIPointer and glVertexAttribLPointer.
// A vertex array object stores the following:
// Calls to glEnableVertexAttribArray or glDisableVertexAttribArray.
// Vertex attribute configurations via glVertexAttribPointer.
// Vertex buffer objects associated with vertex attributes by calls to glVertexAttribPointer.
// From that point on we should bind/configure the corresponding VBO(s) and attribute pointer(s) and then unbind the VAO for later use. As soon as we want to draw an object, we simply bind the VAO with the preferred settings before drawing the object and that is it.
// Usually when you have multiple objects you want to draw, you first generate/configure all the VAOs (and thus the required VBO and attribute pointers) and store those for later use. The moment we want to draw one of our objects, we take the corresponding VAO, bind it, then draw the object and unbind the VAO again.
GLuint vao;
glGenVertexArrays(1, &vao);
// To use a VAO all you have to do is bind the VAO using glBindVertexArray.
glBindVertexArray(vao);
// Since only calls after binding a VAO stick to it, make sure that you've created and bound the VAO at the start of your program. Any vertex buffers and element buffers bound before it will be ignored.
// -------------------------------------------------------------------------------------------

// Vertex Buffer Object(VBO)
// -------------------------------------------------------------------------------------------
// Device X and Y coordinates are mapped to the screen between -1 and 1. y axis is positive above the center. x axis is positive in the right the center. The order in which the attributes appear doesn't matter, as long as it's the same for each vertex.
// https://learnopengl.com/img/getting-started/ndc.png
// The pixels in the texture will be addressed using texture coordinates during drawing operations. These coordinates range from 0.0 to 1.0 where (0,0) is conventionally the bottom-left corner and (1,1) is the top-right corner of the texture image.
// https://learnopengl.com/img/getting-started/tex_coords.png
// X, Y
float vertices[] = {
// Position
-1.0f, 1.0f, // Top-left
1.0f, 1.0f, // Top-right
1.0f, -1.0f, // Bottom-right
-1.0f, -1.0f // Bottom-left
};

// The next step is to upload this vertex data to the graphics card.
// This is done by creating a Vertex Buffer Object (VBO):
GLuint vbo; // GLuint is simply a cross-platform substitute for unsigned int, just like GLint is one for int.
// Its first parameter, n, is the number of buffers requested. Once n buffers have been generated, their identifiers (also referred to as "names" in the OpenGL documentation) are stored in the array buffers(vbo), the function's second parameter. buffers(vbo) must be a GLuint array of n elements.
// In our case, we request one buffer to be generated, and its identifier stored in vbo. The generated buffers are of an undefined type until they are bound to a specific target.
glGenBuffers(1, &vbo); // Generate 1 buffer

// To upload the actual data to it you first have to make it the active object by calling glBindBuffer:
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Now that it's active we can copy the vertex data to it.
// Notice that this function doesn't refer to the id of our VBO, but instead to the active array buffer.
// The final parameter is very important and its value depends on the usage of the vertex data. I'll outline the ones related to drawing here:
// GL_STATIC_DRAW: The vertex data will be uploaded once and drawn many times (e.g. the world).
// GL_DYNAMIC_DRAW: The vertex data will be created once, changed from time to time, but drawn many times more than that.
// GL_STREAM_DRAW: The vertex data will be uploaded once and drawn once.
// This usage value will determine in what kind of memory the data is stored on your graphics card for the highest efficiency.
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// The vertices with their attributes have been copied to the graphics card now, but they're not quite ready to be used yet.
// you have to explain to the graphics card how to handle these attributes.

// Although we have our vertex data, OpenGL still doesn't know how the attributes are formatted and ordered. You first need to retrieve a reference to the position input in the vertex shader.
// The location is a number depending on the order of the input definitions. The first and only input position in this example will always have location 0.
// With the reference to the input, you can specify how the data for that input is retrieved from the array.
// The first parameter specifies which vertex attribute we want to configure. Remember that we specified the location of the position vertex attribute in the vertex shader with layout (location = 0). This sets the location of the vertex attribute to 0 and since we want to pass data to this vertex attribute, we pass in 0.
// if we changed the index parameter to 18, we must also update the GLSL shader's layout qualifier's location argument to layout(location=18).
// he second parameter specifies the number of values for that input, which is the same as the number of components of the vec.
// The third parameter specifies the type of each component and the fourth parameter specifies whether the input values should be normalized between -1.0 and 1.0 (or 0.0 and 1.0 depending on the format) if they aren't floating point numbers.
// The last two parameters are arguably the most important here as they specify how the attribute is laid out in the vertex array. The first number specifies the stride, or how many bytes are between each position attribute in the array. The value 0 means that there is no data in between.
// The last parameter specifies the offset, or how many bytes from the start of the array the attribute occurs.
// It is important to know that this function will store not only the stride and the offset, but also the VBO that is currently bound to GL_ARRAY_BUFFER. That means that you don't have to explicitly bind the correct VBO when the actual drawing functions are called.
// This also implies that you can use a different VBO for each attribute.
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), 0);
// Last, but not least, the vertex attribute array needs to be enabled.
glEnableVertexAttribArray(0);

// It is also possible to omit the glGetAttribLocation and query for the attribute locations via layout (location = 0) in vertex_shader_source specifier.
// GLint pos_attrib = glGetAttribLocation(shader_program, "Position");
// glVertexAttribPointer(posAttrpos_attribib, 2, GL_FLOAT, GL_FALSE, 0, 0);
// glEnableVertexAttribArray(posAttrpos_attribib);

// For textures
// glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(2*sizeof(float)));
// glEnableVertexAttribArray(1);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// -------------------------------------------------------------------------------------------

// Element Buffer Objects(EBO)
// -------------------------------------------------------------------------------------------
// An EBO is a buffer, just like a vertex buffer object, that stores indices that OpenGL uses to decide what vertices to draw.
GLuint elements[] = {
0, 1, 2,
2, 3, 0
};

GLuint ebo;
glGenBuffers(1, &ebo);
// Note that we're now giving GL_ELEMENT_ARRAY_BUFFER as the buffer target.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(elements), elements, GL_STATIC_DRAW);
// remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// -------------------------------------------------------------------------------------------


// Textures
// -------------------------------------------------------------------------------------------
GLuint tex0;
glGenTextures(1, &tex0);

// The default texture unit for a texture is 0 which is the default active texture unit so we didn't need to assign a location in the previous section;
// note that not all graphics drivers assign a default texture unit so the previous section may not have rendered for you.
// OpenGL should have a at least a minimum of 16 texture units for you to use which you can activate using GL_TEXTURE0 to GL_TEXTURE15. They are defined in order so we could also get GL_TEXTURE8 via GL_TEXTURE0 + 8 for example, which is useful when we'd have to loop over several texture units.
// Texture unit GL_TEXTURE0 is always by default activated
glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture

// After activating a texture unit, a subsequent glBindTexture call will bind that texture to the currently active texture unit.
glBindTexture(GL_TEXTURE_2D, tex0);

// The pixels in the texture will be addressed using texture coordinates during drawing operations.
// These coordinates range from 0.0 to 1.0 where (0,0) is conventionally the bottom-left corner and (1,1) is the top-right corner of the texture image.
// see: https://learnopengl.com/img/getting-started/tex_coords.png
// The operation that uses these texture coordinates to retrieve color information from the pixels is called sampling.

// The first thing you'll have to consider is how the texture should be sampled when a coordinate outside the range of 0 to 1 is given. OpenGL offers 4 ways of handling this:
// GL_REPEAT: The integer part of the coordinate will be ignored and a repeating pattern is formed.
// GL_MIRRORED_REPEAT: The texture will also be repeated, but it will be mirrored when the integer part of the coordinate is odd.
// GL_CLAMP_TO_EDGE: The coordinate will simply be clamped between 0 and 1.
// GL_CLAMP_TO_BORDER: The coordinates that fall outside the range will be given a specified border color.
// https://open.gl/media/img/c3_clamping.png
// The wrapping can be set per coordinate, where the equivalent of (x,y,z) in texture coordinates is called (s,t,r). Texture parameter are changed with the glTexParameter* functions as demonstrated here.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
// As before, the i here indicates the type of the value you want to specify. If you use GL_CLAMP_TO_BORDER and you want to change the border color, you need to change the value of GL_TEXTURE_BORDER_COLOR by passing an RGBA float array:
float border_color[] = { 0.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);


// Since texture coordinates are resolution independent, they won't always match a pixel exactly. This happens when a texture image is stretched beyond its original size or when it's sized down. OpenGL offers various methods to decide on the sampled color when this happens.
// This process is called filtering and the following methods are available:
// GL_NEAREST: Returns the pixel that is closest to the coordinates.
// GL_LINEAR: Returns the weighted average of the 4 pixels surrounding the given coordinates.
// GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR: Sample from mipmaps instead.
// You can specify which kind of interpolation should be used for two separate cases: scaling the image down and scaling the image up.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// As you've seen, there is another way to filter textures: mipmaps. Mipmaps are smaller copies of your texture that have been sized down and filtered in advance. It is recommended that you use them because they result in both a higher quality and higher performance.
// To use mipmaps, select one of the four mipmap filtering methods.
// GL_NEAREST_MIPMAP_NEAREST: Uses the mipmap that most closely matches the size of the pixel being textured and samples with nearest neighbour interpolation.
// GL_LINEAR_MIPMAP_NEAREST: Samples the closest mipmap with linear interpolation.
// GL_NEAREST_MIPMAP_LINEAR: Uses the two mipmaps that most closely match the size of the pixel being textured and samples with nearest neighbour interpolation.
// GL_LINEAR_MIPMAP_LINEAR: Samples closest two mipmaps with linear interpolation.
// Note that you do have to load the texture image itself before mipmaps can be generated from it.
// glGenerateMipmap(GL_TEXTURE_2D);

// Loading texture images
// ----------------------------------
// The function begins loading the image at coordinate (0,0), so pay attention to this.
glTexImage2D(GL_TEXTURE_2D, // Type of texture
0, // Pyramid level (for mip-mapping) - 0 is the top level
GL_RGB, // Internal colour format to convert to
img_width, // Image width i.e. 640 for Kinect in standard mode
img_height, // Image height i.e. 480 for Kinect in standard mode
0, // Border width in pixels (can either be 1 or 0)
GL_BGR, // Input image format (i.e. GL_RGB, GL_RGBA, GL_BGR etc.)
GL_UNSIGNED_BYTE, // Image data type
img.ptr()); // The actual image data itself

// Using glUniform1i we can actually assign a location value to the texture sampler so we can set multiple textures at once in a fragment shader. This location of a texture is more commonly known as a texture unit.
// The default texture unit for a texture is 0 which is the default active texture unit so we didn't need to assign a location in the previous section;
glUniform1i(glGetUniformLocation(shader_program, "FragTexture"), 0);

GLuint tex1;
glGenTextures(1, &tex1);
glActiveTexture(GL_TEXTURE1); // activate the texture unit first before binding texture
glBindTexture(GL_TEXTURE_2D, tex1);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
// float border_value[] = { 0.0f, 0.0f, 0.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

glTexImage2D(GL_TEXTURE_2D, // Type of texture
0, // Pyramid level (for mip-mapping) - 0 is the top level
GL_RG32F, // Internal colour format to convert to
img_width, // Image width i.e. 640 for Kinect in standard mode
img_height, // Image height i.e. 480 for Kinect in standard mode
0, // Border width in pixels (can either be 1 or 0)
GL_RG, // Input image format (i.e. GL_RGB, GL_RGBA, GL_BGR etc.)
GL_FLOAT, // Image data type
lut.ptr()); // The actual image data itself

glUniform1i(glGetUniformLocation(shader_program, "LutTexture"), 1);
// ----------------------------------

glUniform1f(glGetUniformLocation(shader_program, "Width"), float(img_width));
glUniform1f(glGetUniformLocation(shader_program, "Height"), float(img_height));

// -------------------------------------------------------------------------------------------

// Vertex Array Objects(VAO)
// -------------------------------------------------------------------------------------------
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// -------------------------------------------------------------------------------------------


glfwSetKeyCallback(window, KeyCallback);

// int k = 0;
while (!glfwWindowShouldClose(window)) {
// Clear the screen to black
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// Clear color and depth buffers
glClear(GL_COLOR_BUFFER_BIT);

// cv::Mat img_show = UpdateImage(img, std::to_string(k++));

// Update Texture
// glTexImage2D creates the storage for the texture, defining the size/format and removing all previous pixel data. glTexSubImage2D only modifies pixel data within the texture. It can be used to update all the texels, or simply a portion of them.
// https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexSubImage2D.xhtml
// glTexSubImage2D(GL_TEXTURE_2D, // Type of texture
// 0, // Pyramid level (for mip-mapping) - 0 is the top level
// 0, // Specifies a texel offset in the x direction within the texture array.
// 0, // Specifies a texel offset in the y direction within the texture array.
// img_width, // Image width
// img_height, // Image height
// GL_BGR, // Input image format (i.e. GL_RGB, GL_RGBA, GL_BGR etc.)
// GL_UNSIGNED_BYTE, // Image data type
// img_show.ptr()); // The actual image data itself

// The first argument specifies the mode we want to draw in, similar to glDrawArrays. The second argument is the count or number of elements we'd like to draw. We specified 6 indices so we want to draw 6 vertices in total.
// The third argument is the type of the indices which is of type GL_UNSIGNED_INT. The last argument allows us to specify an offset in the EBO (or pass in an index array, but that is when you're not using element buffer objects), but we're just going to leave this at 0.
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // use ebo

// glBindVertexArray(0); // no need to unbind it every time

// When rendering a frame, the results will be stored in an offscreen buffer known as the back buffer to make sure the user only sees the final result.
// The glfwSwapBuffers() call will copy the result from the back buffer to the visible window buffer, the front buffer.
glfwSwapBuffers(window);
glfwPollEvents();

std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
glDeleteTextures(1, &tex0);
glDeleteTextures(1, &tex1);

glDeleteProgram(shader_program);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);

glfwDestroyWindow(window);
glfwTerminate();
return 0;
}

罗汉杰. 磁力计数据的处理方法和装置[P]. 中国专利: CN115327452A, 2022.11.11.

背景介绍

磁力计也叫地磁、磁感器,它拥有三个正交方向的霍尔传感器,能够测量出三个方向的磁场强度。由于地球的磁场像一个条形磁体一样由磁南极指向磁北极,通过合成三个方向的霍尔传感器磁场强度读数,可以计算出出设备的航向和姿态。磁力计被广泛地应用于手机,飞行器,机器人等设备中。

然而在一般情况下,地球的磁场十分微弱。如果磁场计周围存在磁性物质或者可以影响局部磁场强度的物质存在,会对磁场造成干扰,使得地磁读取数据产生偏差。再加上受制造安装工艺、敏感轴电气性不一致及零点偏移等因素影响,磁力仪本身存在三轴非正交、敏感轴灵敏度不一致及零偏误差等性能缺陷,使得磁测的准确性受到较大影响。

magnetometer1

而各种传感器都有各自的坐标系。在实际应用中,往往需要将多种传感器的坐标对齐到同一个坐标系下。例如,我们一般将磁力计的坐标系对齐到加速度传感器的坐标系下。但由于安装误差的存在,磁力计坐标系与加速度坐标系没有相互重合,存在一个旋转误差。

magnetometer2

因此,我们在使用磁力计前,一方面需要对其进行标定工作,获得真实的地磁方向数据;另一方面将地磁计的坐标系与其他坐标系进行对齐,以方便后续使用。

磁力计误差模型

磁力计误差一般包括硬铁误差,软铁误差,尺度误差,三轴非正交误差和零偏误差。

硬铁误差:永磁铁或者磁化金属会对磁场产生额外的磁场干扰,其干扰磁场的大小和相对于载体的方向一般保持不变。我们称这种干扰导致的误差为硬铁误差\(\textbf{b}_{n}\)。硬铁误差\(\textbf{b}_{n}\)相当于在磁场上添加一个偏移向量,我们定义为一个3×1向量(\(𝑇\)符号表示矩阵转置):

\[\begin{equation} \label{eq:mag1} \textbf{b}_{n}=[b_{n,x},b_{n,y},b_{n,z}]^{T} \end{equation}\]

软铁误差:软铁磁场是由铁磁材料如铁、镍、PCB 板等对受地磁场或电磁场的磁化而产生的。软铁磁场会随时间和载体航向的变化而变化。我们定义软铁磁场造成的误差\(\textbf{A}_{s}\)为一个3×3矩阵:

\[\begin{equation} \label{eq:mag2} \textbf{A}_{s}=\left[{\begin{array}{c c c}{a_{00}}&{a_{01}}&{a_{02}} \newline {a_{10}}&{a_{11}}&{a_{12}} \newline {a_{20}}&{a_{21}}&{a_{22}}\end{array}}\right] \end{equation}\]

尺度误差\(\textbf{S}\)为一个3×3矩阵:

\[\begin{equation} \label{eq:mag3} \textbf{S}=\left[{\begin{array}{c c c}{s_{x}}&{0}&{0} \newline {0}&{s_{y}}&{0} \newline {0}&{0}&{s_{z}}\end{array}}\right] \end{equation}\]

三轴非正交误差\(\textbf{N}\)为一个3×3矩阵:

\[\begin{equation} \label{eq:mag4} \textbf{N}=\left[{\begin{array}{c c c}{n_{x,x}}&{n_{y,x}}&{n_{z,x}} \newline {n_{x,y}}&{n_{y,y}}&{n_{z,y}} \newline {n_{x,z}}&{n_{y,z}}&{n_{z,z}}\end{array}}\right] \end{equation}\]

零偏误差\(\textbf{b}_{m}\)为一个3×1向量:

\[\begin{equation} \label{eq:mag5} \textbf{b}_{m}=[b_{m,x},b_{m,y},b_{m,z}]^{T} \end{equation}\]

综上所述,磁力计的误差模型为:

\[\begin{equation} \label{eq:mag6} \textbf{h}=\textbf{S}\textbf{N}(\textbf{A}_{s}\textbf{h}' + \textbf{b}_{n}) + \textbf{b}_{m} \end{equation}\]

其中,\(\textbf{h}=[h_{x},h_{y},h_{z}]^{T}\)为磁力计未经标定的直接读取值,\(\textbf{h}'=[h_{x}',h_{y}',h_{z}']^{T}\)为磁力计经过标定后的地磁真实值,简化公式\(\eqref{eq:mag6}\)

\[\begin{equation} \label{eq:mag7} \textbf{h}'=\textbf{A}(\textbf{h} - \textbf{b}) \end{equation}\]

其中,\(\textbf{A}=(\textbf{S}\textbf{N}\textbf{A}_{s})^{-1}\)\(\textbf{b}=\textbf{S}\textbf{N}\textbf{b}_{n}+\textbf{b}_{m}\)为未知的误差模型参数。通过对磁力计进行标定,我们可以得到参数\(\textbf{A}\)\(\textbf{b}\)的值,然后我们可以使用公式\(\eqref{eq:mag7}\),将读取的,带误差的测量值\(\textbf{h}\)恢复为真实值\(\textbf{h}'\)

椭圆体拟合

假设空间中有\(𝑛\)个三维点集\(\lbrace \textbf{X}_{i} = [X_{i}, Y_{i}, Z_{i}]^{T} \mid 0 \le i < n \rbrace\),我们希望能够找到一个最佳椭圆体去拟合这些三维点。描述椭圆体的一般方程为:

\[\begin{equation} \label{eq:mag8} 𝑎𝑋^{2} +𝑏𝑌^{2} +𝑐𝑍^{2} + 2𝑑𝑋𝑌 + 2𝑒𝑋𝑍 + 2𝑓𝑌𝑍 + 2𝑔𝑋 + 2h𝑌 + 2𝑖𝑍 + 𝑗 = 0 \end{equation}\]

上式可以写为矩阵形式:

\[\begin{equation} \label{eq:mag9} [X \quad Y \quad Z] \left[{\begin{array}{c c c}{a}&{d}&{e} \newline {d}&{b}&{f} \newline {e}&{f}&{c}\end{array}}\right] \left[{\begin{array}{c}{X} \newline {Y} \newline {Z}\end{array}}\right] + [X \quad Y \quad Z] \left[{\begin{array}{c}{2g} \newline {2h} \newline {2i}\end{array}}\right] + j = 0 \end{equation}\]

其中,\(𝑎, 𝑏, 𝑐, 𝑑, 𝑒, 𝑓, 𝑔, h, 𝑖, 𝑗\)为描述椭圆体的参数,表示了椭圆体的中心位置,轴方向,和旋转等信息。椭圆体拟合,就是给定一个三维点集,求取满足公式\(\eqref{eq:mag9}\)的椭圆体的参数\(𝑎, 𝑏, 𝑐, 𝑑, 𝑒, 𝑓, 𝑔, h, 𝑖, 𝑗\)。椭圆体的拟合方法有很多,这里我们使用经典的最小二乘法。

设定未知的参数向量\(\textbf{v} = [𝑎, 𝑏, 𝑐, 𝑑, 𝑒, 𝑓, 𝑔, h, 𝑖, 𝑗]^{T}\),对于每一个点\(\textbf{X}_{i} = [X_{i}, Y_{i}, Z_{i}]^{T}\),定义一个临时向量\(\textbf{P}_{i}\)

\[\begin{equation} \label{eq:mag10} \textbf{P}_{i} = (X_{i}^{2}, Y_{i}^{2}, Z_{i}^{2}, 2Y_{i}Z_{i}, 2X_{i}Z_{i}, 2X_{i}Y_{i}, 2X_{i}, 2Y_{i}, 2Z_{i}, 1)^{T} \end{equation}\]

定义10×n矩阵\(\textbf{D} = (\textbf{P}_{0}, \textbf{P}_{1}, \cdots, \textbf{P}_{n-1})\),它包含了所有的三维点集的信息。

定义6×6大的临时矩阵\(\textbf{C}_{1}\)

\[\begin{equation} \label{eq:mag11} \textbf{C}_{1} = \left[{\begin{array}{c c c c c c}{-1}&{1}&{1}&{0}&{0}&{0} \newline {1}&{-1}&{1}&{0}&{0}&{0} \newline {1}&{1}&{-1}&{0}&{0}&{0} \newline {0}&{0}&{0}&{-4}&{0}&{0} \newline {0}&{0}&{0}&{0}&{-4}&{0} \newline {0}&{0}&{0}&{0}&{0}&{-4}\end{array}}\right] \end{equation}\]

定义10×10大的约束矩阵\(\textbf{C}\)

\[\begin{equation} \label{eq:mag12} \textbf{C} = \left[{\begin{array}{c c}{\textbf{C}_{1}}&{\textbf{0}_{6×4}} \newline {\textbf{0}_{4×6}}&{\textbf{0}_{4×4}}\end{array}}\right] \end{equation}\]

根据椭圆体的几何特性,拟合的椭圆体,在满足公式\(\eqref{eq:mag9}\)的前提下,还需满足约束公式\(\textbf{v}^{T}\textbf{C}\textbf{v}=1\)。将椭圆体拟合问题变成最小二乘优化问题,即求取\(\textbf{v}\),使得:

\[\begin{equation} \label{eq:mag13} \min_{\textbf{v}} \lVert \textbf{D} \textbf{v} \rVert ^{2}, 并且\textbf{v}^{T}\textbf{C}\textbf{v}=1 \end{equation}\]

使用增广拉格朗日乘子法,从公式\(\eqref{eq:mag13}\)能够得到:

\[\begin{align} \textbf{D}\textbf{D}^{T}\textbf{v} &= \lambda \textbf{C}\textbf{v} \label{eq:mag14} \newline \textbf{v}^{T}\textbf{C}\textbf{v} &= 1 \label{eq:mag15} \end{align}\]

其中,\(\lambda\)为拉格朗日乘数。\(\textbf{D}\textbf{D}^{T}\)为一个10×10大的矩阵,\(\textbf{v}\)为一个10×1的向量。分解矩阵\(\textbf{D}\textbf{D}^{T}\)和向量\(\textbf{v}\)为:

\[\begin{equation} \label{eq:mag16} \textbf{D}\textbf{D}^{T} = \left[{\begin{array}{c c}{\textbf{S}_{11}}&{\textbf{S}_{12}} \newline {\textbf{S}_{12}}&{\textbf{S}_{22}}\end{array}}\right] \end{equation}\]

\[\begin{equation} \label{eq:mag17} \textbf{v} = \left[{\begin{array}{c}{\textbf{v}_{1}} \newline {\textbf{v}_{2}}\end{array}}\right] \end{equation}\]

其中\(\textbf{S}_{11}\)\(\textbf{S}_{12}\)\(\textbf{S}_{22}\)大小为6×6,6×4和4×4,向量\(\textbf{v}_{1}\)\(\textbf{v}_{2}\)大小为6和4。公式\(\eqref{eq:mag14}\)可以改写为:

\[\begin{align} \textbf{C}_{1}^{-1}(\textbf{S}_{11}-\textbf{S}_{12}\textbf{S}_{22}^{-1}\textbf{S}_{12}^{T})\textbf{v}_{1} &= \lambda \textbf{v}_{1} \label{eq:mag18} \newline \textbf{v}_{2} &= - \textbf{S}_{22}^{-1}\textbf{S}_{12}^{T}\textbf{v}_{1} \label{eq:mag19} \end{align}\]

对公式\(\eqref{eq:mag18}\)中的矩阵\(\textbf{C}_{1}^{-1}(\textbf{S}_{11}-\textbf{S}_{12}\textbf{S}_{22}^{-1}\textbf{S}_{12}^{T})\textbf{v}_{1}\)进行奇异值分解,求取特征值和特征向量,则\(\textbf{v}_{1}\)的值为最大特征值所对应的特征向量。然后将\(\textbf{v}_{1}\)代入公式\(\eqref{eq:mag19}\)求取\(\textbf{v}_{2}\),最终椭圆体的参数向量\(v\)可得。

磁力计标定

本磁力计标定方法包括两个部分,一方面是求取磁力计的误差模型,用来获取真实的地磁方向数据;另外一个方面将磁力计坐标系对齐到加速度坐标系下。

我们先采集数据。假定载体上有已经经过标定的加速度计和未经标定的磁力计。旋转载体,然后静置,采集在该静置姿态下的,已标定的加速度计读数\(\textbf{a}\)(由于是在静置姿态下,所以该读数即为重力加速度)和未经标定的磁力计读数\(\textbf{h}\)。重复以上操作\(𝑛\)次,获取载体在各种姿态下(尽量覆盖所有的方向)的重力加速度数据集\(\lbrace \textbf{a}_{i} = [a_{i,x}, a_{i,y}, a_{i,z}]^{T} \mid 0\le i < n \rbrace\)和地磁数据集\(\lbrace \textbf{h}_{i} = [h_{i,x}, h_{i,y}, h_{i,z}]^{T} \mid 0\le i < n \rbrace\)\(𝑛\)为数据集的大小。

磁力计误差标定

公式\(\eqref{eq:mag7}\)为磁力计的误差模型,其中\(\textbf{h}\)为未经标定的磁力计值,\(\textbf{h}'\)为标定后的真实值。由于在实际应用中,一般只需要磁力计的矢量方向用来计算设备的航向角,而不关心磁场的大小,因此会对\(\textbf{h}'\)进行归一化处理,即有\(\textbf{h}'^{T} \textbf{h}'= {\lVert \textbf{h}' \rVert }^2\)。代入公式\(\eqref{eq:mag7}\)并展开,有:

\[\begin{equation} \label{eq:mag20} \textbf{h}^{T} \textbf{Q} \textbf{h} + \textbf{h}^{T} \textbf{n} + d = 0 \end{equation}\]

其中\(\textbf{Q}=\textbf{A}^{T}\textbf{A}\),\(\textbf{n}=-2\textbf{Q}\textbf{b}\),\(d=\textbf{b}^{T}\textbf{Q}\textbf{b} - 1\)

对比公式\(\eqref{eq:mag20}\)和椭圆体公式\(\eqref{eq:mag9}\),我们发现磁力计误差模型\(\eqref{eq:mag20}\)相当于一个椭圆体公式,即磁力计的读值\(\textbf{h}\)会落在了一个椭圆体上(见下图):

mag_calibration1

去掉硬铁误差,软铁误差,尺度误差,零点误差等误差而得到的磁力计真实值\(\textbf{h}'\),应该落在一个圆心在原点的球体上。将未经标定的磁力计数据集\(\lbrace \textbf{h}_{i} \mid 0\le i < n \rbrace\)当作椭圆体上的三维点集\(\lbrace \textbf{X}_{i} \mid 0\le i < n \rbrace\),使用上述的椭圆体拟合法求取参数向量\(\textbf{v} = [𝑎, 𝑏, 𝑐, 𝑑, 𝑒, 𝑓, 𝑔, h, 𝑖, 𝑗]^{T}\)。根据公式\(\eqref{eq:mag9}\)和公式\(\eqref{eq:mag20}\),我们能够获得磁力计的误差模型参数\(\textbf{Q}\)\(\textbf{n}\)\(d\)

\[\begin{equation} \label{eq:mag21} \textbf{Q} = \left[{\begin{array}{c c c}{a}&{d}&{e} \newline {d}&{b}&{f} \newline {e}&{f}&{c}\end{array}}\right], \quad \textbf{n} = \left[{\begin{array}{c}{2g} \newline {2h} \newline {2i}\end{array}}\right], \quad d = j \end{equation}\]

实际上通过公式\(\eqref{eq:mag20}\)计算获得的误差模型参数\(\textbf{Q}\)\(\textbf{n}\)\(d\)并非是真实值,它们与真实值\(\overline{\textbf{Q}}\)\(\overline{\textbf{n}}\)\(\overline{d}\)之间存在一个矢量\(\alpha\)值,即\(\overline{\textbf{Q}} = \alpha \textbf{Q}\)\(\overline{\textbf{n}} = \alpha \textbf{n}\)\(\overline{d} = \alpha d\)\(\textbf{Q}\)\(\textbf{n}\)\(d\)乘以一个任意矢量\(\alpha\)值都会满足公式\(\eqref{eq:mag20}\),所以我们通过公式\(\eqref{eq:mag20}\)获 得的\(\textbf{Q}\)\(\textbf{n}\)\(d\)并非是唯一值)。我们将\(\alpha\)设定为尺度变量,根据定义有\(\overline{\textbf{n}}=-2\overline{\textbf{Q}}\textbf{b}\),\(\overline{d} = \textbf{b}^{T}\overline{\textbf{Q}} \textbf{b} - 1\),进一步得出:

\[\begin{equation} \nonumber \begin{aligned} 1 &= \textbf{b}^{T}\overline{\textbf{Q}} - \overline{d} \newline &= (-0.5\overline{\textbf{n}}^{T}\overline{\textbf{Q}}^{-1}) \overline{\textbf{Q}} (-0.5\overline{\textbf{n}}^{T}\overline{\textbf{Q}}^{-1}) - \overline{d} \newline &= 0.25 \overline{\textbf{n}}^{T}\overline{\textbf{Q}}^{-1} \overline{\textbf{n}} - \overline{d} \newline &= \alpha(0.25 \textbf{n}^{T}\textbf{Q}^{-1}\textbf{n}-d) \end{aligned} \end{equation}\]

\[\begin{equation} \label{eq:mag22} \longrightarrow \quad \alpha = \frac{4}{\textbf{n}^{T}\textbf{Q}^{-1}\textbf{n}-d} \end{equation}\]

根据\(\overline{\textbf{n}}= − 2\overline{\textbf{Q}}\textbf{b}\)\(\overline{\textbf{Q}} = \alpha\textbf{Q}\)\(\overline{\textbf{n}} = \alpha\textbf{n}\),有:

\[\begin{align} b &= -\frac{1}{2} \overline{\textbf{Q}}^{-1}\overline{\textbf{n}} \nonumber \newline &= -\frac{1}{2} \textbf{Q}^{-1} \textbf{n} \label{eq:mag23} \end{align}\]

根据\(\overline{\textbf{Q}}=\overline{\textbf{A}}^{T}\overline{\textbf{A}}\)\(\overline{\textbf{Q}}=\alpha\textbf{Q}\),有:

\[\begin{equation} \label{eq:mag24} \textbf{A} = (\alpha \textbf{Q})^{\frac{1}{2}} \end{equation}\]

则标定后的磁力计值\(\textbf{h}'=\textbf{A}(\textbf{h} - \textbf{b})\),为一个半径为1的球体上的点,如下图:

mag_calibration2

坐标系对齐

设定有3×3旋转矩阵\(\textbf{R}\),能够将磁力计坐标系下的,经过标定的地磁向量\(\textbf{h}'\)对齐到加速度坐标系下,即:

\[\begin{equation} \label{eq:mag25} \textbf{h}^{a} = \textbf{R}\textbf{h}' \end{equation}\]

在局部地理环境下,地磁向量\(\textbf{h}'\)与重力加速度\(\textbf{a}\)有固定的夹角\(\theta\),根据几何关系有:

\[\begin{equation} \label{eq:mag26} \cos(\theta) - \frac{\textbf{a}^{T} \textbf{R} \textbf{h}'}{ {\lVert \textbf{a} \rVert}{\lVert \textbf{h}' \rVert} } = 0 \end{equation}\]

mag_calibration3

我们使用高斯-牛顿优化方法求取旋转矩阵\(\textbf{R}\)。为方便后续计算,我们使用李代数\(\boldsymbol{ \phi } = [{\phi}_{1}, {\phi}_{2}, {\phi}_{3}]\)表示旋转矩阵\(\textbf{R}\),李代数\(\boldsymbol{ \phi }\)与旋转矩阵\(\textbf{R}\)有以下关系\(\textbf{R}(\boldsymbol{ \phi })={e}^{\boldsymbol{ \phi }^{\wedge}}\)\(e\)为自然常数,\(\wedge\)符号表示以下操作:

\[\begin{equation} \label{eq:mag27} \boldsymbol{ \phi }^{\wedge} = ([{\phi}_{1}, {\phi}_{2}, {\phi}_{3}])^{\wedge} = \begin{bmatrix} {0}&{-\phi_{3}}&{\phi_{2}} \newline {\phi_{3}}&{0}&{-\phi_{1}} \newline {-\phi_{2}}&{\phi_{1}}&{0} \end{bmatrix} \end{equation}\]

根据公式\(\eqref{eq:mag26}\),对重力加速度数据集\(\lbrace \textbf{a}_{i} \mid 0\le i < n \rbrace\)和标定后的地磁数据集\(\lbrace \textbf{h}_{i}' = \textbf{A} (\textbf{h}_{i} - \textbf{b}) \mid 0\le i < n \rbrace\)建立代价函数\(L(\textbf{x})\),有:

\[\begin{equation} \label{eq:mag28} L(\textbf{x}) = L([\boldsymbol{ \phi },k]^{T})=\frac{1}{2}\sum_{i=0}^{n-1}(k-\overline{\textbf{a}}_{i}^{T}\textbf{R}(\boldsymbol{ \phi })\overline{\textbf{h}}_{i})^2 \end{equation}\]

其中\(\textbf{x}=[\boldsymbol{ \phi },k]^{T}=[{\phi}_{1}, {\phi}_{2}, {\phi}_{3}, k]^{T}\)\(k=\cos(\theta)\)\({\overline{\textbf{a}}_{i}} = {\textbf{a}_{i}} / {\lVert \textbf{a}_{i} \rVert}\)\({\overline{\textbf{h}}_{i}} = {\textbf{h}_{i}'} / {\lVert \textbf{h}_{i}' \rVert}\)。我们希望通过最小化代价函数,即\(\min_{L(\textbf{x})}\),计算得到\(\textbf{x}\),来求取未知的旋转矩阵\(\textbf{R}(\boldsymbol{ \phi })\)和夹角变量\(k\)

对于数据集\(\lbrace \textbf{a}_{i} \mid 0\le i < n \rbrace\)\(\lbrace \textbf{h}_{i}' \mid 0\le i < n \rbrace\),有n×4雅可比矩阵\(J(\textbf{x})\)

\[\begin{equation} \label{eq:mag29} J(\textbf{x}) = \left[{\begin{array}{c c } {-(\textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{0})^{T}\overline{\textbf{a}}_{0}^{\wedge}}&{1} \newline {-(\textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{1})^{T}\overline{\textbf{a}}_{1}^{\wedge}}&{1} \newline {\vdots}&{\vdots} \newline {-(\textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{n-1})^{T}\overline{\textbf{a}}_{n-1}^{\wedge}}&{1} \end{array}}\right] \end{equation}\]

和n×1残差矩阵\(F(\textbf{x})\)

\[\begin{equation} \label{eq:mag30} F(\textbf{x}) = \left[{\begin{array}{c} {k - \overline{\textbf{a}}_{0}^{T} \textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{0}} \newline {k - \overline{\textbf{a}}_{1}^{T} \textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{1}} \newline {\vdots} \newline {k - \overline{\textbf{a}}_{n-1}^{T} \textbf{R}(\boldsymbol{ \phi }) \overline{\textbf{h}}_{n-1}} \end{array}}\right] \end{equation}\]

高斯-牛顿优化方法的算法流程如下:

  1. 输入重力加速度数据集\(\lbrace \textbf{a}_{i} \mid 0\le i < n \rbrace\)和标定后的地磁数据集\(\lbrace \textbf{h}_{i}' \mid 0\le i < n \rbrace\);初始化待求\(x=[0, 0, 0, 0]^{T}\)
  2. 根据公式\(\eqref{eq:mag29}\)\(\eqref{eq:mag30}\),计算雅可比矩阵\(J(\textbf{x})\)和残差矩阵\(F(\textbf{x})\)
  3. 计算增量\(\Delta \textbf{x} = -(J(\textbf{x})^{T}J(\textbf{x}))^{-1}J(\textbf{x})^{T}F(\textbf{x})\)
  4. 更新\(\textbf{x} = \textbf{x} + \Delta \textbf{x}\)
  5. 如果\(\lVert \Delta \textbf{x} \rVert > 1e^{-8}\),则返回步骤c;如果否,则说明算法收敛,\(\textbf{x}\)为结果值,进入到下一个步骤。
  6. 因为\(\textbf{x}=[\boldsymbol{ \phi },k]^{T}=[{\phi}_{1}, {\phi}_{2}, {\phi}_{3}, k]^{T}\),根据\(\textbf{R}(\boldsymbol{ \phi })={e}^{\boldsymbol{ \phi }^{\wedge}}\)得到旋转矩阵\(\textbf{R}\)

最终标定结果

通过步骤1,我们获得磁力计的误差模型参数\(\textbf{A}\)\(\textbf{b}\)。根据\(\eqref{eq:mag7}\),可以将带误差的磁力计原始测量值\(\textbf{h}\)转换为真实的地磁值\(\textbf{h}'\)

通过步骤2,我们获得地磁计坐标系与加速度计坐标系之间的旋转关系\(\textbf{R}\), 并且根据公式\(\eqref{eq:mag25}\),将地磁向量\(\textbf{h}'\),对齐到加速度坐标系下,\(\textbf{h}^{a}\)为最终标定的结果。


开源代码:https://github.com/HanjieLuo/drone/tree/master/Client/magnetometer_calibration

罗汉杰. 磁力计数据的处理方法和装置[P]. 中国专利: CN115327452A, 2022.11.11.

安装

安装Node.js

1
brew install node
1
2
node -v         
v18.11.0

安装Hexo 1

1
npm install -g hexo-cli
1
2
npm info hexo-cli version
4.3.0

Blog文件夹下初始化Hexo:

1
2
3
cd Blog
hexo init
npm install

升级Hexo 2

进入带有package.json文件的Hexo文件夹

1
npm update

安装/升级NexT主题 3 4

Blog文件夹下:

1
npm install hexo-theme-next@latest

修改Blog/_config.yml文件:

1
theme: next

测试:

1
2
hexo clean
hexo s --debug

浏览器打开http://localhost:4000

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
hexo -v

INFO Validating config
INFO ==================================
███╗ ██╗███████╗██╗ ██╗████████╗
████╗ ██║██╔════╝╚██╗██╔╝╚══██╔══╝
██╔██╗ ██║█████╗ ╚███╔╝ ██║
██║╚██╗██║██╔══╝ ██╔██╗ ██║
██║ ╚████║███████╗██╔╝ ██╗ ██║
╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝
========================================
NexT version 8.13.1
Documentation: https://theme-next.js.org
========================================
hexo: 6.3.0
hexo-cli: 4.3.0
os: darwin 21.6.0 12.6

node: 18.11.0
v8: 10.2.154.15-node.12
uv: 1.44.2
zlib: 1.2.11
brotli: 1.0.9
ares: 1.18.1
modules: 108
nghttp2: 1.50.0
napi: 8
llhttp: 6.0.10
openssl: 1.1.1q
cldr: 41.0
icu: 71.1
tz: 2022a
unicode: 14.0

配置

在Hexo中有两份主要的配置文件,其名称都是_config.yml。 其中,一份位于站点根目录下,主要包含Hexo本身的配置;另一份位于主题目录下/node_modules/hexo-theme-next/_config.yml,这份配置由主题作者提供,主要用于配置主题相关的选项。为了描述方便,在以下说明中,将前者称为站点配置文件, 后者称为主题配置文件

主题配置文件

主题配置文件文件/node_modules/hexo-theme-next/_config.yml中修改:

Schemes

1
scheme: Pisces

language

1
language: en
1
2
3
4
5
6
7
8
9
10
11
12
13
# Usage: `Key: /link/ || icon`
# Key is the name of menu item. If the translation for this item is available, the translated text will be loaded, otherwise the Key name will be used. Key is case-sensitive.
# Value before `||` delimiter is the target link, value after `||` delimiter is the name of Font Awesome icon.
# External url should start with http:// or https://
menu:
home: / || fa fa-home
about: /about/ || fa fa-user
tags: /tags/ || fa fa-tags
categories: /categories/ || fa fa-th
archives: /archives/ || fa fa-archive
#schedule: /schedule/ || fa fa-calendar
#sitemap: /sitemap.xml || fa fa-sitemap
#commonweal: /404/ || fa fa-heartbeat

手动创建页面tags:

1
hexo new page "tags"

修改Blog/source/tags/index.md

1
2
3
4
5
6
---
title: tags
date: 2016-01-28 18:44:44
type: "tags"
comments: false
---
1
hexo new page "categories"

修改Blog/source/categories/index.md

1
2
3
4
5
6
---
title: categories
date: 2016-01-28 19:18:13
type: "categories"
comments: false
---

手动创建页面categories:

1
hexo new page "categories"

修改Blog/source/categories/index.md

1
2
3
4
5
6
---
title: categories
date: 2016-01-28 19:18:13
type: "categories"
comments: false
---

手动创建页面about:

1
hexo new page "about"

修改Blog/source/about/index.md

1
2
3
4
5
---
title: about
date: 2016-01-28 19:43:39
comments: false
---
1
2
3
sidebar:
position: left
display: always

头像

放置在source/images/目录下,配置为:avatar: /images/avatar.png

1
2
3
4
5
6
# Sidebar Avatar
avatar:
# Replace the default image and set the url here.
url: /images/avatar.png
# If true, the avatar will be displayed in circle.
rounded: true

Google Analytics

1
2
3
4
5
6
7
# Google Analytics
# See: https://analytics.google.com
google_analytics:
tracking_id: G-FKR2744S26
# By default, NexT will load an external gtag.js script on your site.
# If you only need the pageview feature, set the following option to true to get a better performance.
only_pageview: false

社交链接

1
2
3
4
5
social:
GitHub: https://github.com/HanjieLuo || fab fa-github
Linkedin: https://www.linkedin.com/in/hanjie-luo-89602197 || fa-brands fa-linkedin-in
YouTube: https://www.youtube.com/luohanjie || fab fa-youtube
Bilibili: https://space.bilibili.com/319752 || fa-brands fa-bilibili

disqus

1
2
3
4
disqus:
enable: true
shortname: luohanjie
count: true

代码高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
codeblock:
# Code Highlight theme
# All available themes: https://theme-next.js.org/highlight/
theme:
light: stackoverflow-light
dark: stackoverflow-dark
prism:
light: prism
dark: prism-dark
# Add copy button on codeblock
copy_button:
enable: false
# Available values: default | flat | mac
style:

访客统计、访问次数统计、文章阅读次数统计

1
2
3
4
5
6
7
8
9
10
# Show Views / Visitors of the website / page with busuanzi.
# For more information: http://ibruce.info/2015/04/04/busuanzi/
busuanzi_count:
enable: true
total_visitors: true
total_visitors_icon: fa fa-user
total_views: true
total_views_icon: fa fa-eye
post_views: true
post_views_icon: far fa-eye
在Hexo的NexT主题里不蒜子统计问题修复[^8]

修改hexo-theme-next/layout/_third-party/statistics/busuanzi-counter.njk文件为如下内容:

1
2
3
{%- if theme.busuanzi_count.enable %}
<script defer src="https://vercount.one/js"></script>
{%- endif %}

本地显示的数字是随机的,部署到服务器上面就好了。

站点建立时间

1
2
3
footer:
# Specify the year when the site was setup. If not defined, current year will be used.
since: 2016
1
npm install hexo-generator-searchdb

Blog\_config.yml添加:

1
2
3
4
5
search:
path: search.xml
field: post
content: true
format: html

修改/node_modules/hexo-theme-next/_config.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Local search
# Dependencies: https://github.com/next-theme/hexo-generator-searchdb
local_search:
enable: true
# If auto, trigger search by changing input.
# If manual, trigger search by pressing enter key or search button.
trigger: auto
# Show top n results per article, show all results by setting to -1
top_n_per_article: 1
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false

mathjax 5

1
2
3
4
brew install pandoc

npm uninstall hexo-renderer-marked
npm install hexo-renderer-pandoc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Math Formulas Render Support
# Warning: Please install / uninstall the relevant renderer according to the documentation.
# See: https://theme-next.js.org/docs/third-party-services/math-equations
# Server-side plugin: https://github.com/next-theme/hexo-filter-mathjax
math:
# Default (false) will load mathjax / katex script on demand.
# That is it only render those page which has `mathjax: true` in front-matter.
# If you set it to true, it will load mathjax / katex script EVERY PAGE.
every_page: true

mathjax:
enable: true
# Available values: none | ams | all
tags: ams

katex:
enable: false
# See: https://github.com/KaTeX/KaTeX/tree/master/contrib/copy-tex
copy_tex: false

CDN

1
2
3
4
5
vendors:
internal: custom
plugins: custom
# Custom CDN URL
custom_cdn_url: https://lib.baomitu.com/${cdnjs_name}/${version}/${cdnjs_file}

Favicon

准备一张260x260以上大的图片:

favicon

用软件转换成svg格式,改名为logo.svg。在realfavicongenerator或者websiteplanet(It allows to create favicons from pictures that are up to 5 MB. thanks to Virgy)上生成favicon_package包,包括favicon-16x16-next.png(由favicon-16x16.png改名),favicon-32x32-next.png(由favicon-32x32.png改名),apple-touch-icon-next.png(由apple-touch-icon.png改名)。将以上4张图片复制到Blog/node_modules/hexo-theme-next/source/images/进行替换。

站点配置文件

站点配置文件文件_config.yml中修改:

网站

1
2
3
4
5
6
7
title: Hanjie's Blog
subtitle: 一只有理想的羊驼
description: ''
keywords:
author: Hanjie Luo
language: en
timezone: ''

网址

1
2
3
4
5
6
7
8
# URL
## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url: http://luohanjie.com
permalink: :year-:month-:day/:urlname.html
permalink_defaults:
pretty_urls:
trailing_index: true # Set to false to remove trailing 'index.html' from permalinks
trailing_html: true # Set to false to remove trailing '.html' from permalinks

文章的Formatter:

1
2
3
4
5
6
7
title: Mac上安装和配置Hexo博客
urlname: install-and-create-a-blog-with-hexo-on-mac
categories: [Tech, Web]
date: 2022-10-25 16:08:00
tags: [Web, Mac, Hexo, Next]
published: true
---

文章

1
2
3
4
5
6
7
8
# Writing
new_post_name: :title.md # File name of new posts
filename_case: 1
highlight:
enable: true
line_number: false
auto_detect: true
tab_replace:

分页

1
2
3
4
index_generator:
path: ''
per_page: 3
order_by: -date

hexo-renderer-markdown-it

支持footnote

可以通过将markdown渲染器替换为hexo-renderer-markdown-it,使得支持footnote功能6。必须要先卸载原先的渲染器,然后安装:

1
2
npm un hexo-renderer-marked --save
npm i hexo-renderer-markdown-it --save

然后在网站\_config.yml文件中添加:

1
2
3
markdown:
plugins:
- markdown-it-footnote

然后发现每一条footnote间都有一行间隔。想要去掉的话,可以在/Blogs//Users/luohanjie/Workspace/Web/Blog/node_modules/hexo-theme-next/source/css/_common/components/post/post-footer.styl中添加:

1
2
3
.footnote-item p {
margin-bottom: 0
}

hexo-renderer-markdown-it还支持很多其他的功能,具体可以到官网了解。

html

The html setting defines whether or not HTML content inside the document should be escaped or passed to the final result.

1
2
3
markdown:
render:
html: true # Doesn't escape HTML content

支持插入pdf

1
npm install --save hexo-pdf

主题配置文件文件/node_modules/hexo-theme-next/_config.yml中修改:

1
2
3
4
pdf:
enable: true
# Default height
height: 500px

使用:

1
2
3
4
5
6
7
8
# Normal PDF
{% pdf http://7xov2f.com1.z0.glb.clouddn.com/bash_freshman.pdf %}

#Google drive
{% pdf https://drive.google.com/file/d/0B6qSwdwPxPRdTEliX0dhQ2JfUEU/preview %}

#Slideshare
{% pdf http://www.slideshare.net/slideshow/embed_code/key/8Jl0hUt2OKUOOE %}

自定义字体

修改/node_modules/hexo-theme-next/_config.yml:

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
font:
enable: true

# Uri of fonts host, e.g. https://fonts.googleapis.com (Default).
host: https://cdnjs.com

# Font options:
# `external: true` will load this font family from `host` above.
# `family: Times New Roman`. Without any quotes.
# `size: x.x`. Use `em` as unit. Default: 1 (16px)

# Global font settings used for all elements inside <body>.
global:
external: true
family: Noto Serif SC
size:

# Font settings for site title (.site-title).
title:
external: true
family: Noto Serif SC
size:

# Font settings for headlines (<h1> to <h6>).
headings:
external: true
family: Noto Serif SC
size:

# Font settings for posts (.post-body).
posts:
external: true
family: Noto Serif SC

# Font settings for <code> and code blocks.
codes:
external: true
family: Source Code Pro

编辑主题的Blog/node_modules/hexo-theme-next/source/css/_variables/base.styl文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Font size
$font-size-base = (hexo-config('font.enable') and hexo-config('font.global.size') is a 'unit') ? unit(hexo-config('font.global.size'), em) : 1em;
$font-size-smallest = .55em;
$font-size-smaller = .6125em;
$font-size-small = .675em;
$font-size-medium = 0.8em;
$font-size-large = 0.925em;
$font-size-larger = 1.15em;
$font-size-largest = 1.7em;


// Headings font size
$font-size-headings-step = .125em;
$font-size-headings-base = (hexo-config('font.enable') and hexo-config('font.headings.size') is a 'unit') ? unit(hexo-config('font.headings.size'), em) : 1.425em;

sitemap

1
npm install hexo-generator-sitemap --save

服务器部署 7

使用Git Hook自动部署到vps上。

创建用户

服务器上,创建git用户并且赋予权限:

1
2
3
adduser git
chmod 740 /etc/sudoers
nano /etc/sudoers
1
2
3
# User privilege specification
root ALL=(ALL:ALL) ALL
git ALL=(ALL:ALL) ALL
1
chmod 440 /etc/sudoers

建立密钥

本地Mac上,查看密钥(没有的话需要创建一个):

1
ls -al ~/.ssh

创建钥匙密钥:

1
2
3
4
cd ~/.ssh 
ssh-keygen -t rsa -C "你的邮箱" // 执行这个命令会提示输入用于保存的密钥名和口令之类的,都不填

touch ~/.ssh/config
1
2
3
4
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_rsa
1
ssh-add -K ~/.ssh/id_rsa

复制公匙:

1
pbcopy < ~/.ssh/id_rsa.pub

服务器上:

1
2
3
4
cd /home/git                   //切换到git用户目录
mkdir .ssh //创建.ssh目录
cd .ssh //进入.ssh目录
nano authorized_keys //将本地的公钥复制到authorized_keys文件里
1
2
3
chmod 700 /home/git
chmod 700 /home/git/.ssh #只有拥有者有读、写、执行权限
chmod 600 /home/git/.ssh/authorized_keys #只有拥有者有读写权限
1
nano /etc/ssh/sshd_config
1
2
3
4
// 取消这些注释
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
1
sudo /etc/init.d/ssh restart

关闭git用户的shell权限,设置后git用户可以通过ssh正常使用git服务,但无法登录shell:

1
nano /etc/passwd
1
git:x:1001:1001:,,,:/home/git:/bin/bash 改成 git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

Git仓库

服务器上:

1
2
3
4
5
6
apt-get install git

cd /home/git //切换到git用户目录
mkdir blog.git //创建仓库目录,以blog.git为例
cd blog.git //进入仓库目录
git init --bare //使用--bare参数初始化为裸仓库,这样创建的仓库不包含工作区

修改/home/git/blog.git目录的用户组权限为git:git

1
2
3
sudo chown git:git -R /home/git/.ssh
sudo chown git:git -R /home/git/.ssh/authorized_keys
sudo chown git:git -R /home/git/blog.git

本地Mac上测试:

1
2
3
ssh -v git@ip

git clone ssh://git@ip:port/home/git/blog.git

Git Hooks自动部署

在本地编辑Markdown文章,然后使用Git推送到VPS的Git仓库。Git Hooks实际上就是当Git仓库收到最新的push时,将Git仓库接受到的内容复制到VPS上的网站目录内。相当于完成了手动将public文件夹复制到VPS的网站根目录里的操作。

建立网站根目录:

1
2
3
cd /home/git
mkdir blog
sudo chown git:git -R /home/git/blog

创建post-receive文件:

1
2
cd /home/git/blog.git/hooks     //切换到hooks目录下
nano post-receive //创建post-receive文件并编辑
1
2
3
4
5
6
7
8
#!/bin/bash
GIT_REPO=/home/git/blog.git
TMP_GIT_CLONE=/tmp/blog
PUBLIC_WWW=/home/git/blog
rm -rf ${TMP_GIT_CLONE}
git clone $GIT_REPO $TMP_GIT_CLONE
rm -rf ${PUBLIC_WWW}/*
cp -rf ${TMP_GIT_CLONE}/* ${PUBLIC_WWW}
1
chmod +x post-receive

部署Nginx

服务器上:

1
2
3
sudo apt-get install nginx

sudo nano /etc/nginx/conf.d/blog.conf

写入:

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
server {
listen 80;
server_name example.com; # 自己的域名

root /home/git/blog; # 刚才说的路径
access_log /var/log/nginx/blog_access.log;
error_log /var/log/nginx/blog_error.log;

# 这里是针对静态资源文件做个缓存
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
expires 1d;
add_header Pragma public;
add_header Cache-Control "public";
}

# 如果使用了hexo-pdf插件,并且将文件放到服务器上,则还需添加
location ~* \.(pdf)$ {
add_header "Access-Control-Allow-Origin" "http://nagland.github.io";
expires 1d;
}

# 这里就是把请求转给我们的静态文件了
location / {
root /home/git/blog;
if (-f $request_filename) {
rewrite ^/(.*)$ /$1 break;
}
}
}

由于nginx的运行用户没有权限访问网站所在的目录,检查:

1
/etc/nginx/nginx.conf

可以将user的值改为git

重启:

1
service nginx restart

打开服务器端口

1
apt install ufw

允许80端口:

1
2
3
ufw allow 80

ufw status verbose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Rule added
Rule added (v6)
root@NY:/var/log/nginx# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80 ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
80 (v6) ALLOW IN Anywhere (v6)

Hexo远程部署

本地Blog文件夹下:

1
npm install hexo-deployer-git --save

站点配置文件文件_config.yml中修改:

1
2
3
4
deploy:
type: git
repo: git@ip:blog.git
branch: master
1
git clone ssh://git@ip:port/home/git/blog.git
1
2
3
hexo clean
hexo generate
hexo deploy

  1. https://hexo.io/zh-cn/docs/↩︎

  2. https://dandyxu.me/Hexo/How-to-update-Hexo-and-Hexo-theme-properly/↩︎

  3. http://theme-next.iissnan.com/getting-started.html↩︎

  4. https://israynotarray.com/hexo/20201101/60919/↩︎

  5. https://github.com/theme-next/hexo-theme-next/blob/master/docs/zh-CN/MATH.md↩︎

  6. https://github.com/hexojs/hexo-renderer-markdown-it↩︎

  7. https://www.eula.club/搭建Hexo静态博客并使用Git部署到VPS.html↩︎

0%