CS50-Week4

文章发布时间:

最后更新时间:

文章总字数:
4.4k

预计阅读时间:
19 分钟

中断了 3 个月的更新,忙完了 CS50 的 C 语言部分,简单看了看后面的内容,然后投入到 CS106x 的学习中。

PSet

Volome

目标:修改音频文件的音量
看着作业文档稀里糊涂的就复制粘贴完了,关于文件读写操作还是很懵,应该不是目前的重点,先聚焦于内存吧
仔细看一眼
关于文件读写
首先,定义一个文件变量,大概是指针的形式FILE *input = fopen("[filename]","[mode]").
常用的文件读取模式有‘r’只读 ‘w’写入 ‘a’ 追加
进行文件读取操作后应紧接验证文件是否正常打开,失败应抛出错误”NULL”
文件调用结束后应及时使用fclose()关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
FILE *fileOpen = fopen("filename.txt","r");

if (fileOpen == NULL)
{
printf("file open fail");
return 1;
}

fclose(fileOpen);
return 0;
}

其他的文件操作有fgetc()``fread()``fwrite()
这里仅介绍fread()
fread 接受 4 个参数:存储数据的内存指针,数据项大小,数据项数量,被读取文件。
以本题为例,给出待读取文件指针为 input;设置暂存区变量 buffer;定义一种新变量类型 int16_t,仅读取 1 项。
读取语句为 fread(&buffer,sizeof(int16_t),1,input);返回值为 1,当读取至末尾时返回 0

Filter more

Background

简单看一下想干嘛

Bitmaps

位图可以通过增加深度承载不同的颜色,有 8 位,24 位等,在 RGB 颜色模式中,通过 Red,Green,Blue 的数值进行颜色表示
在 BMP 图像格式中,首先出现的是头文件BITMAPFILEHEADER,长度为 14 Bytes,接着是BITMAPINFOHEADER,位图信息头文件,长度为 40 Bytes。接着是特定图像的信息。
通过二维数组可以记录位图信息

Image Filtering

过滤器,可以理解为滤镜,通过滤镜可以让原始图像显示需要的信息

Grayscale

“灰度”是使用广泛的过滤器,可以使图像转为黑白,彩色信息会转换为灰色的亮度信息
方法:~~ 该干嘛啊?~~

如果红色、绿色和蓝色值都设置为 0x00(0 为十六进制),则像素为黑色。如果所有值都设置为 0xff(255 为十六进制),则像素为白色。只要红色、绿色和蓝色值都相等,结果将是沿黑白光谱的不同灰色深浅,较高的值表示较浅的阴影(更接近白色),较低的值表示较深的阴影(更接近黑色)。

确保新图像的每个像素仍然具有与旧图像相同的一般亮度或暗度,我们可以取红色、绿色和蓝色值的平均值来确定新像素的灰色阴影

Reflection

“反射”,将图像对称或旋转等其他像素的位置操作

Blur

模糊,将中心像素颜色设置为周围 3x3 范围内颜色的平均值
让像素的颜色信息与周围像素更接近

Edges

Vocabularykernel 核心,内核 vernal 春天的,和煦的 eternal 永恒的,不朽的
corresponding 相似的,无冲突的
cap
锐化,在 AI 图像处理领域,常用 Sobel operator 进行操作
与模糊相反的操作,但要复杂的多
to be continue…
image.png
锐化算法会使用 Sobel 的两个卷积核 Gx,Gy。卷积核中的数值是预先定义的,经过精心设计与实验验证。
对于中心像素,垂直检测边缘使用 Gx 卷积核,将每个卷积核值与对应的像素值相乘,然后将所有的结果的平方和的根。 ${Gx^2 + Gy^2}$的根就是新的像素值。

Understanding

Vocabularyaliases 别名。primitive 原始的;简陋的。encapsulate 总结;囊括;装入胶囊。triple 三倍的;三方的
retrieve 恢复;检索。prototype 原型,蓝本 prototypical 典型的,模范的。confined 限制,约束。
whereas 但是,而

本项目的作业文件中包含多个文件,先看 bmp.h
定义了一堆东西,方便我们将数据从内存中取出

filter.c
代码已经完成,但是有个要点需要注意
line 10 定义可被程序读取的字符串为”begr”,分别对应 blur edge grayscale reflection
接着程序会打开给定的图像文件并读取全部的像素信息存储到image变量中
跳到 line 101,switch 语句会判断用户需要的过滤器并调用,我们需要完成具体的过滤器函数
其余语句用于输出处理后图像

helper.h
一个很短的程序,仅提供了 filter.c 中出现的过滤器函数声明
helper.c
具体的函数功能消失啦~

This part is up to you.

Makefile
最后,看看这个没有后缀的文件是啥
wow~ ⊙o⊙ 竟然是咱们在命令行的老熟人 make 命令
里面只有两行命令,用来调用 clang 进行编译 c 程序
使用命令make filter来告知 make 命令要编译谁,并通过./filter -g image/yard.bmp out.bmp来对 image 文件夹内的 yard.bmp 图像执行灰度操作,输出图像为 out.bmp

Walkthrough

灰度

两层循环,对 RGB 颜色求平均值即可,esay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Convert image to grayscale
void grayscale(int height, int width, RGBTRIPLE image[height][width])
{
int average = 0;
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width;j++)
{
average = (image[i][j].rgbtBlue + image[i][j].rgbtGreen + image[i][j].rgbtRed) / 3;
image[i][j].rgbtBlue = average;
image[i][j].rgbtRed = average;
image[i][j].rgbtGreen = average;
}
}
return;
}

注:本题需四舍五入,RGBTRPLE 为整数型,所以计算平均值时须进行浮点计算,并引入数学计算库math.h
average = round((float)(image[i][j].rgbtBlue + image[i][j].rgbtGreen + image[i][j].rgbtRed) / 3);

反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Reflect image horizontally
void reflect(int height, int width, RGBTRIPLE image[height][width])
{
RGBTRIPLE temp;
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width / 2;j++)
{
image[i][j].rgbtBlue = temp.rgbtBlue;
image[i][j].rgbtRed = temp.rgbtRed;
image[i][j].rgbtGreen = temp.rgbtGreen;

image[i][j].rgbtBlue = image[i][width - j].rgbtBlue;
image[i][j].rgbtRed = image[i][width - j].rgbtRed;
image[i][j].rgbtGreen = image[i][width - j].rgbtGreen;

image[i][width - j].rgbtBlue = temp.rgbtBlue;
image[i][width - j].rgbtRed = temp.rgbtRed;
image[i][width - j].rgbtGreen = temp.rgbtGreen;
}
}
return;
}

将每列的第一个值的 RGB 信息与末位对调,定义一个 RGB 同款变量暂存一下信息 eas…wait!怎么反转了右半边的图像,左半边的黑掉了!?
问问鸭子

ddb
It seems like you’re trying to reflect an image horizontally. However, it looks like you’re not correctly swapping the pixels. You’re assigning the values of image[i][j] to temp after you’ve already changed image[i][j]. This means temp isn’t storing the original values.
Try this: before you change image[i][j], first store its original values in temp. Then, you can safely modify image[i][j] and use temp to change image[i][width - j].
Remember, when you’re reflecting the image, you’re swapping pixels from opposite ends of the row. So, image[i][width - j - 1] might be the correct index for the pixel on the other end, not image[i][width - j].

谢谢 dd 鸭,temp 没能正确存储信息,图像的索引也漏掉了-1操作.
temp 是未初始化的空值,将 temp 赋给 image[i][j]明显是错误的
调整一下 temp
Reflection 根本没有对 RGB 值进行操作,直接将图像左右对调就好了
简化后代码是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Reflect image horizontally
void reflect(int height, int width, RGBTRIPLE image[height][width])
{
RGBTRIPLE temp;
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width / 2;j++)
{
temp = image[i][j];
image[i][j] = image[i][width - j - 1];
image[i][width - j - 1] = temp;
}
}
return;
}

模糊

将中心像素颜色设置为周围 3x3 范围内颜色的平均值
定义一个与像素同类型的变量表示平均值
嵌套循环不解释,先解决周围一圈都有邻居的
用 if 把它们全挑出来,按 i-1,i,i+1 顺序来计算,同时也可以作为模板运用到边角求平均,同时考虑边缘,利用 if 进行判断

1
2
3
4
5
6
7
8
9
10
11
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width;j++)
{
if (i == 0)
else if (i == height - 1)
else if (j == 0)
else if (j == width -1)
else
}
}

3x3 网格计算式如下:

1
2
3
4
5
6
else
{
average.rgbtRed = (image[i - 1][j - 1].rgbtRed + image[i - 1][j].rgbtRed + image[i - 1][j + 1].rgbtRed + image[i][j - 1].rgbtRed + image[i][j].rgbtRed + image[i][j + 1].rgbtRed + image[i + 1][j - 1].rgbtRed + image[i + 1][j].rgbtRed + image[i + 1][j + 1].rgbtRed) / 9;
average.rgbtGreen = (image[i - 1][j - 1].rgbtGreen + image[i - 1][j].rgbtGreen + image[i - 1][j + 1].rgbtGreen + image[i][j - 1].rgbtGreen + image[i][j].rgbtGreen + image[i][j + 1].rgbtGreen + image[i + 1][j - 1].rgbtGreen + image[i + 1][j].rgbtGreen + image[i + 1][j + 1].rgbtGreen) / 9;
average.rgbtBlue = (image[i - 1][j - 1].rgbtBlue + image[i - 1][j].rgbtBlue + image[i - 1][j + 1].rgbtBlue + image[i][j - 1].rgbtBlue + image[i][j].rgbtBlue + image[i][j + 1].rgbtBlue + image[i + 1][j - 1].rgbtBlue + image[i + 1][j].rgbtBlue + image[i + 1][j + 1].rgbtBlue) / 9;
}

计算完把均值赋给像素点,函数返回值,结束
debug
取平均操作会去到周围的值,此处应用原始图像 RGB 值计算,而上述代码在每个循环内都改变了当前像素的值,导致下方的计算全部出错。
计算均值过程不能改变原值,计算完成后统一赋值。
设置临时变量temp[i][j]存储位置象素平均数,循环结束后再进行一次赋值循环。
完成
code backup(i j 错了好多,写的时候)

【以编辑】
我决定保留以下这段代码。

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
// Blur image
void blur(int height, int width, RGBTRIPLE image[height][width])
{
RGBTRIPLE average;
RGBTRIPLE temp[height][width];
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width;j++)
{
//corner
if (i == 0 && j == 0)
{
average.rgbtRed = round((float)(image[i][j].rgbtRed + image[i + 1][j].rgbtRed + image[i][j + 1].rgbtRed + image[i + 1][j + 1].rgbtRed) / 4);
average.rgbtGreen = round((float)(image[i][j].rgbtGreen + image [i + 1][j].rgbtGreen + image[i][j + 1].rgbtGreen + image[i + 1][j + 1].rgbtGreen) / 4);
average.rgbtBlue = round((float)(image[i][j].rgbtBlue + image[i + 1][j].rgbtBlue + image[i][j + 1].rgbtBlue + image[i + 1][j + 1].rgbtBlue) / 4);
}
else if (i == height - 1 && j == 0)
{
average.rgbtRed = round((float)(image[i][j].rgbtRed + image[i - 1][j].rgbtRed + image[i][j + 1].rgbtRed + image[i - 1][j + 1].rgbtRed) / 4);
average.rgbtGreen = round((float)(image[i][j].rgbtGreen + image [i - 1][j].rgbtGreen + image[i][j + 1].rgbtGreen + image[i - 1][j + 1].rgbtGreen) / 4);
average.rgbtBlue = round((float)(image[i][j].rgbtBlue + image[i - 1][j].rgbtBlue + image[i][j + 1].rgbtBlue + image[i - 1][j + 1].rgbtBlue) / 4);
}
else if (i == 0 && j == width - 1)
{
average.rgbtRed = round((float)(image[i][j].rgbtRed + image[i + 1][j].rgbtRed + image[i][j - 1].rgbtRed + image[i + 1][j - 1].rgbtRed) / 4);
average.rgbtGreen = round((float)(image[i][j].rgbtGreen + image [i + 1][j].rgbtGreen + image[i][j - 1].rgbtGreen + image[i + 1][j - 1].rgbtGreen) / 4);
average.rgbtBlue = round((float)(image[i][j].rgbtBlue + image[i + 1][j].rgbtBlue + image[i][j - 1].rgbtBlue + image[i + 1][j - 1].rgbtBlue) / 4);
}
else if (i == height - 1 && j == width - 1)
{
average.rgbtRed = round((float)(image[i][j].rgbtRed + image[i - 1][j].rgbtRed + image[i][j - 1].rgbtRed + image[i - 1][j - 1].rgbtRed) / 4);
average.rgbtGreen = round((float)(image[i][j].rgbtGreen + image [i - 1][j].rgbtGreen + image[i][j - 1].rgbtGreen + image[i - 1][j - 1].rgbtGreen) / 4);
average.rgbtBlue = round((float)(image[i][j].rgbtBlue + image[i - 1][j].rgbtBlue + image[i][j - 1].rgbtBlue + image[i - 1][j - 1].rgbtBlue) / 4);
}
//edge
else if (i == 0 && j != 0 && j != width - 1)
{
average.rgbtRed = round((float)(image[i][j - 1].rgbtRed + image[i][j].rgbtRed + image[i][j + 1].rgbtRed + image[i + 1][j - 1].rgbtRed + image[i + 1][j].rgbtRed + image[i + 1][j + 1].rgbtRed) / 6);
average.rgbtGreen = round((float)(image[i][j - 1].rgbtGreen + image[i][j].rgbtGreen + image[i][j + 1].rgbtGreen + image[i + 1][j - 1].rgbtGreen + image[i + 1][j].rgbtGreen + image[i + 1][j + 1].rgbtGreen) / 6);
average.rgbtBlue = round((float)(image[i][j - 1].rgbtBlue + image[i][j].rgbtBlue + image[i][j + 1].rgbtBlue + image[i + 1][j - 1].rgbtBlue + image[i + 1][j].rgbtBlue + image[i + 1][j + 1].rgbtBlue) / 6);
}
//other 3 blocks like this...

//center
else
{
average.rgbtRed = round((float)(image[i - 1][j].rgbtRed + image[i][j].rgbtRed + image[i + 1][j].rgbtRed + image[i + 1][j - 1].rgbtRed + image[i][j - 1].rgbtRed + image[i - 1][j - 1].rgbtRed + image[i - 1][j + 1].rgbtRed + image[i][j + 1].rgbtRed + image[i + 1][j + 1].rgbtRed) / 9);
average.rgbtGreen = round((float)(image[i - 1][j].rgbtGreen + image[i][j].rgbtGreen + image[i + 1][j].rgbtGreen + image[i + 1][j - 1].rgbtGreen + image[i][j - 1].rgbtGreen + image[i - 1][j - 1].rgbtGreen + image[i - 1][j + 1].rgbtGreen + image[i][j + 1].rgbtGreen + image[i + 1][j + 1].rgbtGreen) / 9);
average.rgbtBlue = round((float)(image[i - 1][j].rgbtBlue + image[i][j].rgbtBlue + image[i + 1][j].rgbtBlue + image[i + 1][j - 1].rgbtBlue + image[i][j - 1].rgbtBlue + image[i - 1][j - 1].rgbtBlue + image[i - 1][j + 1].rgbtBlue + image[i][j + 1].rgbtBlue + image[i + 1][j + 1].rgbtBlue) / 9);
}
temp[i][j] = average;
}
}
for (int i = 0;i < height;i++)
{
for (int j = 0;j < width;j++)
{
image[i][j] = temp[i][j];
}
}
return;
}

边缘检测

先设置两个卷积核,如何遍历每个像素,进行计算
对于边缘像素:

我们将要求您将图像视为图像边缘周围有一个 1 像素的黑色实心边框:因此,尝试访问图像边缘以上的像素应被视为纯黑色像素(每个红色的值为 0, 绿色和蓝色)。这将有效地忽略我们计算中的这些像素 Gx 以及 Gy.

就是在外面再套上一层黑框呗,怎么搞?

  • 黑框的值都为 0,即使参与计算也不会对结果造成影响,干脆不对黑框计算了,试了,循环逻辑被搞的很复杂
  • 新建一个临时图片,即原图片加上一层黑框:两层循环将数值计入 temp 变量,原图信息从[1][1]位置开始写入

没忍住问了一下 GPT,然后发现给出了极为简洁的代码,我在 Blur 那里写的实在是太烂了。
使用嵌套循环,在对每个像素操作时在套两层循环,用来读取 Gx 和 Gy 数组循环计数从-1 开始,到 1 结束。太妙了!

关于数值限制
temp[i][j].rgbtRed = (temp[i][j].rgbtRed > 255) ? 255 : temp[i][j].rgbtRed;这行代码是什么意思
这行代码使用了条件运算符(也称为三元运算符),其语法如下:

1
condition ? expression_if_true : expression_if_false;

在你提到的代码中,这行代码的作用是检查 temp[i][j].rgbtRed 是否大于 255。如果条件成立(即 temp[i][j].rgbtRed > 255 为真),则表达式的结果为 255;如果条件不成立,则表达式的结果为 temp[i][j].rgbtRed 的当前值。

具体说,这行代码的含义是:如果红色通道的值 temp[i][j].rgbtRed 大于 255,将其设为 255;否则,保持其原始值。这样做的目的是确保图像的红色通道值在合理的范围内,不超过 255。同样的逻辑也适用于绿色和蓝色通道的处理。

将原始图像添加一层黑框的处理中,临时存储图像的变量 temp 大小为 weight + 2 ,最终存储新图像信息的临时变量 temp1 大小与原始图像一至。

Recover

Reverse(颠倒)(2024 版 CS50 删去该题)
目标:从内存中恢复 JPEG 图像

Background

如何将 JPEG 图片辨认出来?
JPEG 文件的前三个字节为 0xff 0xd8 0xff,第四个字节的前四位是 1110
对任意一张图片,相机都会将其按“块”进行存储,每个块大小为 512B,则一张 1MB 的图片会将占用 2048 个块,因为松弛空间的存在,0.99MB 大小的图片同样占据 2048 个块
松弛空间均被 0 填充,且无需考虑
本题总共应能恢复 50 张图像

Understanding

本题没有预先给出功能性代码
对于文件操作,使用<stdlib.h>库

Specification

文件接受一行命令行输入
不正确的操作即时停止并返回值 1
恢复的图片以###.jpg 格式命名,从 000 开始
如果使用 malloc,不要泄露内存

Walkthrough

首先打开和关闭 raw 文件,若错误返回 1检测命令行传入参数数量是否唯一,否则弹出错误提示,然后尝试打开文件
设置缓冲区 buffer,每次读取 512 个大小,每当 JPEG 文件签名验证通过时,创建新的图像文件并写入
关于函数 sprintf
This function prints a “formatted string” to a location in memory. It expects as input the address of a buffer (which must be large enough to fit the string, including its \0), a “format string” that specifies what to print, and zero or more subsequent arguments. The format string can optionally contain “conversion specifications,” placeholders that begin with % that specify how to format the function’s subsequent arguments, if any. For instance, if buffer is an array of (at least) 13 bytes and i is 50, this function could format a string as follows:
此函数将“格式化字符串”打印到内存中的某个位置。它期望缓冲区的地址(必须足够大以适合字符串,包括其 \0)、指定要打印的内容的“格式字符串”以及零个或多个后续参数作为输入。格式字符串可以选择包含“转换规范”,即以 %指定如何设置函数后续参数的格式(如果有)。例如,如果 buffer 是一个包含(至少)13 个字节且 i 是 50,此函数可以按如下方式格式化字符串:

1
sprintf(buffer, "hello, %s\n", i);

何处使用?
批量创建文件时,为可创建文件的 fopen 函数提供文件名

ddb
在 C 语言中,你可以使用一个循环结构来批量创建文件。在每次循环中,你可以使用 sprintf 函数来生成文件名,然后使用 fopen 函数来创建文件。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
char filename[8];
for (int i = 1; i <= 100; i++)
{
sprintf(filename, "%03d", i);
FILE* file = fopen(filename, "w");
// Remember to check if the file was successfully opened
if (file == NULL)
{
// Handle the error
}
// Don't forget to close the file when you're done
fclose(file);
}

这段代码会创建名为 001,002,003,…,100 的文件。注意,%03d 在 sprintf 中表示一个三位数,如果数值不足三位,则在前面补 0。

Source Code

【已编辑】
详见课程文档Walkthrough部分。

vocabulary

demarcation initialize implications iteration trailing
划定的界限 初始化 暗示;蕴含 反复 牵引的;拖尾的