着色器

正如在Hello Triangle一章中提到的,着色器是基于GPU的小程序。这些程序针对图形管道的每个特定部分运行。在基本意义上,着色器只不过是把输入转换成输出的程序。着色器也是非常孤立的程序,因为它们不允许彼此通信;他们唯一的交流是通过他们的输入和输出。

在前一章中,我们简要地接触了着色器的表面以及如何正确地使用它们。现在我们将以更一般的方式解释着色器,特别是OpenGL着色语言。

GLSL

着色器是用类c语言GLSL编写的。GLSL是为与图形一起使用而定制的,包含了专门针对向量和矩阵操作的有用特性。

着色器总是以版本声明开始,然后是输入和输出变量的列表,Uniforms和它的主要功能。每个着色器的入口点都在它的主要函数处,在这里我们处理任何输入变量并在输出变量中输出结果。如果你不知道什么是Uniforms,别担心,我们很快就会知道。

一个着色器通常有以下结构:

#version version_number
in type in_variable_name;
in type in_variable_name;out type out_variable_name;uniform type uniform_name;void main()
{// process input(s) and do some weird graphics stuff...// output processed stuff to output variableout_variable_name = weird_stuff_we_processed;
}

当我们特别谈论顶点着色器时,每个输入变量也被称为顶点属性。我们可以声明的顶点属性的最大数量受硬件的限制。OpenGL保证至少有16个4组件的顶点属性可用,但一些硬件可能允许更多,你可以通过查询GL_MAX_VERTEX_ATTRIBS检索:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

这通常返回16的最小值,在大多数情况下,这已经足够了。

类型

和其他编程语言一样,GLSL也有数据类型来指定我们想要使用的变量类型。GLSL拥有我们从C语言中知道的大多数默认基本类型:int、float、double、uint和bool。GLSL还提供了两种容器类型,即向量和矩阵,我们将大量使用它们。我们将在后面的章节中讨论矩阵。

向量

GLSL中的向量是刚才提到的任何基本类型的1、2、3或4组件容器。可以采用以下形式(n表示组件的数量):

  • vecn: n个浮点数的默认向量。
  • bvecn: n个布尔值的向量。
  • ivecn:一个n个整数的向量.
  • uvecn: 一个n个无符号整数的向量.
  • dvecn: a vector of n double components.

大多数时候,我们将使用基本的vecn,因为float对于我们的大多数目的来说已经足够了。

向量的分量可以通过向量-x访问,其中x是向量的第一个分量。您可以使用.x、.y、.z和.w分别访问它们的第一个、第二个、第三个和第四个组件。GLSL还允许您使用rgba用于颜色或stpq用于纹理坐标,访问相同的组件。

向量数据类型允许一些有趣而灵活的组件选择,称为swizzling。Swizzling允许我们使用这样的语法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用多达4个字母的任何组合来创建一个新的矢量(相同类型),只要原始矢量有这些组件;例如,不允许访问vec2的.z组件。我们也可以将向量作为参数传递给不同的向量构造函数调用,减少所需参数的数量:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

因此,向量是一种灵活的数据类型,可以用于各种输入和输出。在整本书中,你会看到很多关于我们如何创造性地管理向量的例子。

进进出出(Ins and outs)

着色器本身是很好的小程序,但它们是整体的一部分,因此我们想要在每个着色器上有输入和输出,这样我们就可以移动东西。为此,GLSL专门定义了in和out关键字。每个着色器都可以使用这些关键字指定输入和输出,当输出变量与下一个着色器阶段的输入变量匹配时,它们就会被传递。顶点和片段着色器有一点不同。

顶点着色器应该接受某种形式的输入,否则它将是相当无效的。顶点着色器的输入不同,因为它直接从顶点数据接收输入。为了定义顶点数据的组织方式,我们用位置元数据指定输入变量,这样我们就可以在CPU上配置顶点属性。我们在前面的章节中已经看到了布局(location = 0),因此顶点着色器需要一个额外的布局说明,以便我们可以将它的输入与顶点数据链接起来。

也可以省略布局(location = 0)说明符,并通过glGetAttribLocation查询OpenGL代码中的属性位置,但我更喜欢在顶点着色器中设置它们。它更容易理解,并节省了您(和OpenGL)的一些工作。

另一个例外是片段着色器需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终的输出颜色。如果你没有在你的片段着色器中指定一个输出颜色,那些片段的颜色缓冲输出将是未定义的(这通常意味着OpenGL将渲染它们要么是黑色要么是白色)。

如果我们想从一个着色器发送数据到另一个着色器,我们必须在发送着色器中声明一个输出,在接收着色器中声明一个类似的输入。当类型和名字在两边相等时,OpenGL将这些变量链接在一起,然后就有可能在着色器之间发送数据(这是在链接程序对象时完成的)。为了向你展示这是如何在实践中工作的,我们将改变前一章的着色器,让顶点着色器决定片段着色器的颜色。

Vertex shader


#version 330 core
layout (location = 0) in vec3 aPos; // the position variable has attribute position 0out vec4 vertexColor; // specify a color output to the fragment shadervoid main()
{gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructorvertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color
}

Fragment shader


#version 330 core
out vec4 FragColor;in vec4 vertexColor; // the input variable from the vertex shader (same name and same type)  void main()
{FragColor = vertexColor;
} 

你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,我们在片段着色器中声明了一个类似的vertexColor输入。因为它们都有相同的类型和名称,在片段着色器的vertexColor链接到顶点着色器的vertexColor。因为我们在顶点着色器中将颜色设置为暗红色,所以产生的片段也应该是暗红色的。输出如下图所示:

现在!我们只是设法从顶点着色器发送一个值到片段着色器。让我们添加一点香料,看看我们是否可以从我们的应用程序发送一个颜色到片段着色器!

Uniforms

Uniforms是另一种方式,从我们的应用程序上的CPU到GPU的着色器传递数据。然而,Uniforms与顶点属性稍有不同。首先,制服是全局性的。全局的,意思是一个统一的变量对每个着色程序对象是唯一的,并且可以在着色程序的任何阶段从任何着色程序访问。其次,无论您将Uniforms值设置为什么,Uniforms将保持它们的值,直到它们被重置或更新。

要在GLSL中声明一个统一,我们只需在一个具有类型和名称的着色器中添加统一关键字。从那时起,我们可以在着色器中使用新声明的Uniforms。让我们看看这次是否可以通过Uniforms来设置三角形的颜色:


#version 330 core
out vec4 FragColor;uniform vec4 ourColor; // we set this variable in the OpenGL code.void main()
{FragColor = ourColor;
}   

我们在片段着色器中声明了一个统一的vec4 ourColor,并将片段的输出颜色设置为这个统一值的内容。因为Uniforms是全局变量,我们可以在任何我们想要的着色器阶段定义它们,所以不需要再次通过顶点着色器来获得一些东西到片段着色器。我们没有在顶点着色器中使用这个Uniforms,所以没有必要在那里定义它。

如果你声明了一个在你的GLSL代码中没有使用的Uniforms,编译器会悄悄地从编译的版本中删除变量,这是一些令人沮丧的错误的原因;记住这一点!

Uniforms目前是空的;我们还没有给Uniforms添加任何数据,让我们试一下。我们首先需要找到着色器中统一属性的索引/位置。一旦我们有了统一的索引/位置,我们就可以更新它的值。而不是传递一个单一的颜色到碎片着色器,让我们随着时间逐渐改变颜色:


float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

首先,我们通过glfwGetTime()检索以秒为单位的运行时间。然后我们使用sin函数在0.0 - 1.0范围内改变颜色,并将结果存储为greenValue。

然后使用glGetUniformLocation查询ourColor统一的位置。我们向查询函数提供着色程序和uniform的名称(我们想要从中检索位置)。如果glGetUniformLocation返回-1,它将无法找到该位置。最后,我们可以使用glUniform4f函数设置uniform 值。注意,找到uniform 的位置不需要你使用着色器程序,但更新一个uniform 确实需要你首先使用程序(通过调用glUseProgram),因为它设置了当前活动的着色器程序的uniform。

因为OpenGL是在它的核心C库中,它不支持函数重载,所以只要一个函数可以被不同类型调用,OpenGL就会为每种类型定义新的函数;glUniform就是一个很好的例子。对于你想要设置的制服类型,这个函数需要一个特定的后缀。

函数需要一个浮点数作为它的值。

  • f: the function expects a float as its value.
  • i: the function expects an int as its value.
  • ui: the function expects an unsigned int as its value.
  • 3f: the function expects 3 floats as its value.
  • fv: the function expects a float vector/array as its value.

每当您想配置OpenGL的选项时,只需选择与您的类型对应的重载函数。在我们的例子中,我们希望分别设置4个统一浮动,这样我们就可以通过glUniform4f传递数据(注意,我们也可以使用fv版本)。

现在我们知道了如何设置Uniform变量的值,我们可以使用它们来进行渲染。如果我们想让颜色逐渐变化,我们需要在每一帧中更新这个Uniform的颜色,否则如果我们只设置一次,三角形就会保持单一的纯色。因此,我们计算greenValue,更新统一的渲染迭代:


while(!glfwWindowShouldClose(window))
{// inputprocessInput(window);// render// clear the colorbufferglClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// be sure to activate the shaderglUseProgram(shaderProgram);// update the uniform colorfloat timeValue = glfwGetTime();float greenValue = sin(timeValue) / 2.0f + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);// now render the triangleglBindVertexArray(VAO);glDrawArrays(GL_TRIANGLES, 0, 3);// swap buffers and poll IO eventsglfwSwapBuffers(window);glfwPollEvents();
}

该代码是对前面代码的相对简单的修改。这一次,我们在绘制三角形之前在每一帧中更新一个Uniform的值。如果你正确地更新了制服,你应该会看到你的三角形的颜色逐渐从绿色变成黑色,然后再变成绿色。

如果您卡住了,请在这里here 查看源代码。

正如你所看到的,Uniform是一个有用的工具来设置属性,可以改变每一帧,或者在你的应用程序和着色器之间交换数据,但是如果我们想为每个顶点设置颜色呢?在那种情况下,我们必须声明和顶点一样多的Uniform。一个更好的解决方案是在顶点属性中加入更多的数据,这就是我们现在要做的。

More attributes!

在前一章中我们看到了如何填充VBO,配置顶点属性指针并将其存储在VAO中。这一次,我们还要向顶点数据添加颜色数据。我们将添加颜色数据作为3个浮点数到顶点数组。我们分别为三角形的每个角分配红、绿、蓝三种颜色:


float vertices[] = {// positions         // colors0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // bottom right-0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // bottom left0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // top 
};    

因为我们现在有更多的数据要发送到顶点着色器,所以有必要调整顶点着色器来接收作为顶点属性输入的颜色值。注意,我们使用布局说明符将aColor属性的位置设置为1:


#version 330 core
layout (location = 0) in vec3 aPos;   // the position variable has attribute position 0
layout (location = 1) in vec3 aColor; // the color variable has attribute position 1out vec3 ourColor; // output a color to the fragment shadervoid main()
{gl_Position = vec4(aPos, 1.0);ourColor = aColor; // set ourColor to the input color we got from the vertex data
}       

Since we no longer use a uniform for the fragment's color, but now use the ourColor output variable we'll have to change the fragment shader as well:

因为我们不再为片段的颜色使用一个uniform,但现在使用我们的颜色输出变量,我们同样将不得不改变片段着色器:


#version 330 core
out vec4 FragColor;  
in vec3 ourColor;void main()
{FragColor = vec4(ourColor, 1.0);
}

因为我们添加了另一个顶点属性并更新了VBO的内存,我们必须重新配置顶点属性指针。更新后的数据在VBO的内存现在看起来有点像这样:

知道当前的布局,我们可以更新顶点格式通过glVertexAttribPointer:


// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

glVertexAttribPointer的前几个参数相对简单。这次我们在属性位置1上配置顶点属性。颜色值的大小为3个浮点数,我们没有对这些值进行规范化。

因为我们现在有两个顶点属性,我们必须重新计算步幅值。为了得到数据数组中的下一个属性值(例如位置向量的下一个x分量),我们必须向右移动6个浮点数,3个位置值,3个颜色值。这为我们提供了以字节为单位的6倍浮点数大小的跨步值(= 24字节)。

另外,这次我们必须指定一个偏移量。对于每个顶点,位置顶点属性是第一个,因此我们声明一个偏移量为0。color属性在位置数据之后开始,因此偏移量为3 * sizeof(浮动)字节(= 12字节)。

运行该应用程序应该会出现以下图像:

如果您卡住了,请在这里here查看源代码。

图像可能不是你所期望的那样,因为我们只提供了3种颜色,而不是我们现在看到的巨大的调色板。这都是片段着色器中所谓的片段插值的结果。当渲染一个三角形时,光栅化阶段通常会导致比最初指定的顶点更多的片段。然后,光栅化程序根据片段在三角形上的位置确定每个片段的位置。

基于这些位置,它插值所有片段着色器的输入变量。例如,我们有一条线,上面的点是绿色的,下面的点是蓝色的。如果片段着色器运行在一个片段上,驻留在70%的线的位置,它的结果颜色输入属性将是绿色和蓝色的线性组合;更准确地说,是30%的蓝色和70%的绿色。

这就是在三角形中发生的事情。我们有3个顶点,因此3种颜色,从三角形的像素判断,它可能包含大约50000个片段,片段着色器在这些像素中插值颜色。如果你仔细看看这些颜色,你会发现这一切都是有道理的:红色到蓝色,首先是紫色,然后是蓝色。片段插值应用于所有片段着色器的输入属性。

Our own shader class

编写、编译和管理着色器是相当麻烦的。作为最后的触摸着色主体,我们将使我们的生活更容易一点,通过建立一个着色类,读取着色器从磁盘,编译和链接,检查错误,很容易使用。这也让您对如何将我们目前所学的一些知识封装成有用的抽象对象有了一些概念。

我们将完全在头文件中创建着色器类,主要是为了学习目的和可移植性。让我们从添加必需的include和定义类结构开始:


#ifndef SHADER_H
#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader
{
public:// the program IDunsigned int ID;// constructor reads and builds the shaderShader(const char* vertexPath, const char* fragmentPath);// use/activate the shadervoid use();// utility uniform functionsvoid setBool(const std::string &name, bool value) const;  void setInt(const std::string &name, int value) const;   void setFloat(const std::string &name, float value) const;
};#endif

我们在头文件的顶部使用了几个预处理器指令。使用这些代码行通知编译器只包括和编译这个头文件,如果它还没有被包括,即使多个文件包括着色器头。这防止了链接冲突。

着色器类持有着色器程序的ID。它的构造函数需要顶点着色器和片段着色器的源代码的路径,我们可以将它们作为简单的文本文件存储在磁盘上。为了增加一点额外的,我们也增加了一些实用功能,以减轻我们的生活一点:使用激活着色程序,和所有设置…函数查询统一位置并设置其值。

Reading from file

我们使用c++ filestreams将文件中的内容读入几个字符串对象中:


Shader(const char* vertexPath, const char* fragmentPath)
{// 1. retrieve the vertex/fragment source code from filePathstd::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// ensure ifstream objects can throw exceptions:vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);try {// open filesvShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// read file's buffer contents into streamsvShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();		// close file handlersvShaderFile.close();fShaderFile.close();// convert stream into stringvertexCode   = vShaderStream.str();fragmentCode = fShaderStream.str();		}catch(std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char* fShaderCode = fragmentCode.c_str();[...]

接下来我们需要编译和链接着色器。注意,我们还会检查编译/链接是否失败,如果失败,则打印编译时错误。这在调试时非常有用(你最终会需要那些错误日志):


// 2. compile shaders
unsigned int vertex, fragment;
int success;
char infoLog[512];// vertex Shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// print compile errors if any
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{glGetShaderInfoLog(vertex, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};// similiar for Fragment Shader
[...]// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// print linking errors if any
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{glGetProgramInfoLog(ID, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);

使用功能简单明了:


void use() 
{ glUseProgram(ID);
}  

类似地,对于任何统一的setter函数:


void setBool(const std::string &name, bool value) const
{         glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
} 

我们有了它,一个完整的着色器类shader class.。使用shader类是相当容易的;我们创建了一个着色对象,从那一点上简单地开始使用:


Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
[...]
while(...)
{ourShader.use();ourShader.setFloat("someUniform", 1.0f);DrawStuff();
}

这里我们存储了顶点和片段着色器的源代码在两个文件称为shader.vs和shader.fs。你可以任意命名你的着色器文件;我个人觉得扩展.vs和.fs相当直观。

你可以在这里 here使用我们新创建的着色器类shader class.找到源代码。注意,你可以点击着色器文件路径来找到着色器的源代码。

Exercises

  1. Adjust the vertex shader so that the triangle is upside down: solution.
  2. Specify a horizontal offset via a uniform and move the triangle to the right side of the screen in the vertex shader using this offset value: solution.
  3. Output the vertex position to the fragment shader using the out keyword and set the fragment's color equal to this vertex position (see how even the vertex position values are interpolated across the triangle). Once you managed to do this; try to answer the following question: why is the bottom-left side of our triangle black?: solution

 

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

相关文章

  1. 轻量型神经网络,请自行排序

    ...

    2024/4/28 4:35:58
  2. mysql无法本地、外网同时访问 Host is not allowed to connect t

    mysql无法外网连接 Host * is not allowed to connect to this MySQL server在本机登入mysql后,更改"mysql"数据库里的"user"表里的"host"项,从"localhost"改为%。代码如下mysql>mysql>use mysql;mysql>select host from u…...

    2024/4/9 0:55:36
  3. Python 告诉你疫情扩散有多可怕!Python模拟疫情扩散!

    今年(2020年)是注定要铭记史册的一年,从年初开始新冠疫情,席卷了全球,中国人民众志成城,为战胜疫情做出了巨大牺牲。最近北京疫情形式又变得严峻,面对疫情我们不能掉以轻心。今天我们模拟一下病毒的扩散过程,增强对疫情的认识同时,还可以了解下 Python 模拟技术,开干…...

    2024/4/23 17:10:11
  4. JS超时处理最简单的方式

    当我们请求后端接口时间过长时,我们需要给用户反馈错误信息,以告知用户目前的状态是什么,那么最简单的超时处理方式是: Promise.race() promise.race()是接口竞技,就是哪个接口先返回数据就处理哪个数据,适用于多个接口并发执行时。 let p1 = new Promise((resolve, reje…...

    2024/4/27 10:30:10
  5. 学生成绩管理系统-C语言链表版

    学生成绩管理系统绝对是我们课设的经典选题之一,那么今天就给大家带来一个链表版本的学生成绩管理系统,希望对大家的学习有所帮助,这个项目采用链表这一数据结构最为底层的存储框架,然后是用文件对数据进行一个永久存储,达到了数据持久化的目的以上是代码的运行截图,大家…...

    2024/4/28 9:11:11
  6. ArrayList使用基本类型需要使用其对应的包装类

    ArrayList对象不能存储基本类型,只能存储引用类型的数据。类似<int>不能写,但是存储基本数据类型对应的包装类是可以的。所以想要存储基本数据类型,<>中的数据类型必须转换成包装类对应的引用类型。基本类型 基本类型包装类 byte Byte short …...

    2024/4/9 0:55:32
  7. webrtc视频QOS方法汇总

    https://blog.csdn.net/crystalshaw/category_9281395.html...

    2024/4/28 10:33:56
  8. generator原理及用法解析——Python的生成器

    前言 生成器generator生成器的本质是一个迭代器(iterator)要理解生成器,就要在理解一下迭代,可迭代对象,迭代器,这三个概念Python生成器generator简介iteration, iterable, iterator迭代(iteration):在python中迭代通常是通过for...in...来实现的.而且只要是可迭代对象iterable…...

    2024/4/22 16:42:43
  9. 安装Android studio的详细步骤

    程序员日常中用到Android studio开发时,总免不了安装AS软件,但是本人作为一个新手,在安装中不免出现了很多问题,导致代码报错,运行不了等等问题,经过仔细研究后我决定写下这篇文章,希望能够帮助更多人跳过安装的坑! 本文分为两个部分一、基本的安装教程 二、安装好后测…...

    2024/4/9 0:55:30
  10. Java虚拟机简介(一)

    Java虚拟机的主要作用 Java虚拟机简称JVM,它的总体结构是这样的:Java的语法是由Java Language Specification来规定的,Java语言规范和Java虚拟机规范是两大规范构成了Java的支柱,Java语言规范规定的是表面上的语法,Java虚拟机规范规定了Java内部的细节和原理。 Java之所以被…...

    2024/4/8 22:32:00
  11. 有趣的Python OpenCV教程学习(下)

    接着上一篇:https://blog.csdn.net/qq_34717531/article/details/1074025457.模板匹配import cv2 import numpy as npimport cv2 import numpy as npimg_rgb = cv2.imread(timg.jpeg)#读取原图 img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)#转灰度图template = cv2.…...

    2024/4/28 16:04:34
  12. linux使用useradd创建的用户没有目录的解决办法

    linux使用useradd创建的用户没有目录的解决办法 我使用 sudo useradd newuser 后 /home里并没有 newuser的目录。查询百度得知,可以用 useradd -m newuser ,但是-m这个命令只有在你创建用户的时候才有用。 如果已经创建了用户且没有目录的话,useradd -m newuser是不会为用户…...

    2024/4/23 21:28:54
  13. LeetCode MySQL 1468. 计算税后工资

    文章目录1. 题目2. 解题 1. 题目 Salaries 表: +---------------+---------+ | Column Name | Type | +---------------+---------+ | company_id | int | | employee_id | int | | employee_name | varchar | | salary | int | +--------------…...

    2024/4/28 20:46:12
  14. js实现图片上传前预览

    利用js实现了图片上传前的预览 <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Image Preview…...

    2024/4/15 8:42:58
  15. 大数据可视化搭建存在哪些难题

    数据可视化可以增强数据的呈现效果,方便用户以更直观的方式观察数据,进而发现数据中隐藏的信息。可视化有着广泛的应用,包括网络数据可视化、交通数据可视化、文本数据可视化、数据挖掘可视化、生物医学可视化、社会可视化等。虽然数据大屏幕可视化技术日趋成熟。然而,数据…...

    2024/4/27 20:54:02
  16. 二叉树算法--数据结构课程设计

    题目要求: 二叉树算法设计 TXT文件内容(Btree.txt): 1 2 4 8 0 0 9 0 0 5 0 0 3 6 0 0 7 0 0 一定注意,这里文件按照前序遍历的数字形式,空树按照0进行表示,每个数字间空格,这样能区分十位数还是个位数,并且只能是纯数字 代码展示: #include<iostream> #includ…...

    2024/4/26 13:27:07
  17. 手写springboot starter组件示例

    Spring Boot 特性: 1.能够快速创建基于Spring的应用程序; 2.能供直接使用java main 方法启动内置的Tomcat或者Jetty服务器运行Spring Boot程序; 3.提供约定的starter POM来简化Maven的配置,让Maven的配置变得更简单; 4.根据项目的Maven依赖配置,Spring Boot自动配置Sprin…...

    2024/4/9 0:55:25
  18. C#建立网络通信详解

    C#建立网络通信可以使用Socket类或者TcpListener类,本文详细讲解使用Socket类建立网络通信。 Socket简单介绍Socket接口是TCP/IP网络的应用程序接口(API)。程序员可以用它们来开发TCP/IP网络应用程序。Socket可以看成是网络通信上的一个端点,也就是说,网络通信包括两台主机…...

    2024/4/9 0:55:24
  19. 【Jupyter】打开Jupyter notebook时,网页打不开

    这个只需要在Jupyter Notebook文件中设置一下默认浏览器即可,步骤如下: 1.打开anaconda prompt 2.运行jupyter notebook --generate-config 结果:3.复制上述文件路径,使用记事本打开 4.找到#c.NotebookApp.browser = ‘’ 在下方加入 import webbrowserwebbrowser.register…...

    2024/4/27 12:06:51
  20. Makefile使用总结

    文章目录Makefile用法文件命名规则:用途:工作原理:Makefile变量分类:使用Makefile函数函数写法:使用: Makefile用法 文件命名规则: Makefile 或者 makefile 用途: 用于项目的代码编译管理,一次可以编译多个文件,且再次编译时不会对还没有修改过的文件进行重新编译就节…...

    2024/4/9 0:55:24

最新文章

  1. <计算机网络自顶向下> Internet Protocol(未完成)

    互联网中的网络层 IP数据报格式 ver: 四个比特的版本号&#xff08;IPV4 0100, IPV6 0110&#xff09; headlen&#xff1a;head的长度&#xff08;头部长度字段&#xff08;IHL&#xff09;指定了头部的长度&#xff0c;以32位字&#xff08;4字节&#xff09;为单位计算。这…...

    2024/4/29 2:34:07
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. windows更新驱动导致Linux虚拟机网卡找不到

    windows更新驱动导致Linux虚拟机网卡找不到 1、现象2、解决过程3、参考 1、现象 原先虚拟机配置了静态IP&#xff0c;更新windows驱动后xshell连接不上这台虚拟机&#xff08;其他几台也是&#xff09;。 2、解决过程 service network restart出现一下报错&#xff1a; Rest…...

    2024/4/25 19:19:14
  4. Jmeter02-1:参数化组件CVS

    目录 1、Jmeter组件&#xff1a;参数化概述 1.1 是什么&#xff1f; 1.2 为什么&#xff1f; 1.3 怎么用&#xff1f; 2、Jmeter组件&#xff1a;参数化实现之CSV Data Set Config(重点中重点) 2.1 是什么&#xff1f; 2.2 为什么&#xff1f; 2.3 怎么用&#xff1f; …...

    2024/4/25 11:52:18
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/4/28 4:04:40
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/4/28 12:01:04
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/4/28 16:34:55
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/4/28 18:31:47
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/4/28 12:01:03
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/4/28 12:01:03
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/4/28 12:01:03
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/4/28 16:07:14
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/4/28 23:42:05
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/4/28 9:00:42
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/4/27 18:40:35
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/4/28 4:14:21
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/4/27 13:52:15
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/4/27 13:38:13
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/4/28 12:00:58
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/4/28 12:00:58
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/4/27 22:51:49
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/4/28 7:31:46
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/4/28 8:32:05
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/4/27 20:28:35
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57