多译 · 更新日志页面开发(三) 初识TypeScript

前几天看完React的视频,我觉得我懂了,信心满满的要去进行开发了。但是当我把项目clone下来以后,发现色老师的项目我根本难以读懂。

或者说,代码的大多数语法我都不能够理解。

因为色老师的项目使用的是TypeScript 和 TSX。

无奈,我只能继续学习新的知识,看来距离能够上手开发还是有一段距离,或许还是很遥远的距离

好在,至少我从高一就开始学C++。学习一门新的语言对于我来说应该难度不算很大。。。

1、为什么要有TypeScript

还是同样的学习哲学,我们首先要思考ts存在的意义。

顾名思义,有类型的script,正如文档首页的自我描述:

TypeScript 是 JavaScript 类型的超集,它可以编译成纯JavaScript

我们可以将它理解为有类型的js。

如果说弱类型带来的灵活性是js的优点,那么弱类型带来的潜在的不可控则是js的缺点

ts就是为了解决这一问题,通过对js的变量进行类型限制,取舍一定的灵活性,换来更好的稳定性,至少是可控性。

2、基础类型

  • boolean
  • number 所有数字都是浮点数
  • string
  • array number[] or Array<number>
  • Tuple
  • enum
  • any 不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。
  • void 没有类型
  • null
  • undefined
  • never 表示的是那些永不存在的值的类型.比如那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;
  • Object 非原始类型

类型断言

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

  • 尖括号语法
    1
    2
    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
  • as 语法
    1
    2
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;

    在TypeScript里使用JSX时,只有 as语法断言是被允许的。

3、变量声明

文档在这一部分中重申了let、const以及var的区别,其中一个例子值得注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}

第一个输出全是10,后面两组输出是正常的0-9
具有一定js基础的话,对于前两组的结果是很容易理解的。而对于第三组,文档中的解释让我对于作用域有了进一步理解

直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

当let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout例子里我们仅使用let声明就可以了。

本章节文档的第二部分主要讲了解构与展开,这部分和es6的标准是一样的。

4、接口

ts的核心原则之一就是对值所具有的结构进行类型检查。ts的接口就是为了让我们能够对js中灵活的变量定义类型。

4.1 冒号定义类型

1
2
3
4
5
6
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

如上图,函数 printLabel 的参数 labelledObj 后面用 :{label: string} 表示其类型是一个具有 label 属性的对象。
label 后面用 :string 来表示 label 属性是一个 string 类型。

上面的例子可以通过用 interface 关键字来定义接口名称来重写

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

这个例子中,我们为 labelledObj 的类型命名为 LabelledValue

4.2 可选属性 option bags

1
2
3
4
interface SquareConfig {
color?: string;
width?: number;
}

4.3 只读属性 readonly

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

ts还具有ReadonlyArray,可以将数组的所有可变方法都屏蔽掉

1
2
3
4
5
6
7
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
a = ro as number[];

4.4 对象字面量的额外属性检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}

let mySquare = createSquare({ colour: "red", width: 100 });

// 下面的写法则不会报错
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

对比上面的例子和4.1中的例子,可以看到当我们有可选属性的时候,ts回对对象的字面量进行额外的属性检查。

4.5 函数类型

1
2
3
4
5
6
7
8
9
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

定义参数列表和返回值类型。参数列表里的每个参数都需要名字和类型。其中函数返回值的类型写在函数定义中 括号的后面
函数的参数名不需要与接口里定义的名字相匹配

4.6 可索引的类型

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致。

4.7 混合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

上述例子中 Counter 定义了一个函数对象的接口,第一行定义了其作为函数的类型,后面两行定义了其作为对象的类型。

function getCounter():Counter 表示该函数的返回值的类型是 Counter

在函数体内可以看到其按照接口逐步完成了Counter对象的构造

5、类与泛型

读完 TypeScript 的类的文档以后,我惊奇的发现它与Java多么的相近。之前听qc科普js的原型链的时候,我其实是对于js的面向对象的理念不太理解的。但是ts这种类java语法的面向对象,我很容易就能够理解了。

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
private greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter("world");

上面一个简单的例子。其中关于 private protected public 关键字的用法和Java也是类似的。

并且ts也有 static 关键字, 静态成员这一部分和java也很类似。

ts中的泛型和java也是很类似的,这里着重记录一下ts的泛型类型

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn {
<T>(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn = identity;
1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

下面的这种写法可以让接口中的其他属性也获取到泛型变量T

其实这些用法都可以用java来类比

小结

由于这次看ts的目的主要是为了开发miniproject,没有太多的时间去深入了解ts以及其高级的语言特性。结合色老师的代码,阅读完文档的前几章节的基础部分之后,我觉得我已经能够看懂多译的主页项目了,因此关于ts文档的阅读就先告一段落,以后有机会的时候可以仔细研究一下ts的高级特性。

web开发技术链条

昨天是加入ina技术部以后的第二次会,任总来分享了GPU的薅羊毛方法。(但我目前可能不太会炼丹)

色老师分享了产品设计和web开发种的工具链条。我才发现原来开发也是一件讲究的事情。

设计

前端

1
2
“任何可以使用JavaScript来编写的应用,最终会由JavaScript编写。”
——Atwood定律

又一次听到了这句话。不过js最近确实使用人数越来越多。

  • 【web】umi 开发框架
  • 【app】react native
  • 【小程序】taro开发框架
  • 【桌面端】Electron

umi确实很好用,最近在做miniproject,这个东西有点重。

色老师说他最近开了新坑,查老师挂掉了,他们要搞个新的查老师。用的就是Taro框架,以后我们应该也要参与到开发中了。

后端

  • 【框架】Egg.js
  • postman
  • mongoDB Compass
  • Alinode

色老师演示了 postman的使用方法,这个真的是神器。

mongoDB最近写miniproject的时候也在看,这个数据库比MySQL好用一万倍。
把数据库当作对象处理,很符合js的理念比sql语句要好用的多。

运维

  • linux的宝塔面板;
  • docker portainer

docker portainer我前段时间刚刚用过。docker的定义上学期开开好像例会上有提过。
我前段时间刚好还用过这个玩意。不过是不务正业,用docker搭了mc服务器和瑞同石大佬一起玩。

日志管理

  • Sentry
  • ELK

数据追踪

  • Metabase
  • Google Analy
  • Mixpanel

这些东西从来没有接触过,感觉很工业,离我挺遥远的。

##阳阳的私货分享————BPM 音乐

  • BPM:
    • bpm 软件计算版: MixMeister BMP Analyzer
    • 自己估算:每一个拍,数一个数字。1-10数两组,用1200/秒数
    • 懒人不算
  • BPM 100-110
    • 有氧运动:台阶 慢速跳绳。
    • 无氧运动:Focus

DIP-HW4

实验四

一、实验目的和要求

  • 目的
    • 了解图像变换的种类
    • 掌握常见几何变换(平移、旋转、对称、投影、放缩)的方法。
    • 了解插值的原因、目的及原理。
    • 掌握线性插值、双线性插值的方法,并比较其效果。
  • 要求
    • 自己写c语言程序,实现所有操作,不得调用库。

二、实验内容和原理

  • 内容: 对图像进行几何变换。

    • 读入24位彩图
    • 依次对图像进行平移、旋转、对称、投影和放缩变换
    • 输出变换后的图像
  • 原理:

    ​ 对图像进行几何变换操作的原理是,对于即将生成的新图像中每个像素点$P’=(x’,y’)$ ,我们利用图像几何变换的逆变换求得其在原始图像中对应的点 $P=(x,y)=T(x’,y’)$ 。则$R(x’,y’)=R(x,y);G(x’,y’)=G(x,y);B(x’,y’)=B(x,y);$

    ​ 很多情况下我们得到的 $(x,y)$ 并不是整数,这个时候我们需要选择一个方法来得到$(x,y) $应当对应的$RGB$值。这个时候就用到插值法。即利用$(x,y)$ 附近的一些像素点的$RGB$值来计算得到$(x’,y’)$的$RGB$值。

    • 平移 $(dx,dy)$

      $$ \left{ \begin{aligned} x = x’-dx\y = y’-dy \end{aligned} \right. $$

    • 逆时针旋转 $\theta$

      一些常量的定义:

      $pw,ph (原图的宽度和高度)$

      $nw,nh (新图的宽度和高度)$

      $cx=pw/2 (原图中心点的横坐标)$

      $cy=ph/2 (原图中心点的横坐标)$

      $dx=(nw-pw)/2$

      $dy=(nh-ph)/2$

      定义两个中间变量:

      $a=x’-dx-cx+cxcos\theta-cysin\theta$

      $b=y’-dy-cy+cycos\theta+cxsin\theta$

      $$ \left{ \begin{aligned} x = bsin\theta +acos\theta \y = bcos\theta-asin\theta \end{aligned} \right. $$

    • **投影 $\theta$ **

      x轴方向 $$ \begin{cases} x = x’ -y’*tan\theta \y =y’ \end{cases} $$

      y轴方向 $$ \begin{cases} x = x’ \y =y’ -x’*tan\theta \end{cases} $$

    • 对称

      x轴方向 $$ \begin{cases} x = pw -x’-1 \y =y’ \end{cases} $$

      y轴方向 $$ \begin{cases} x = x’ \y =ph-y’-1 \end{cases} $$

    • 放缩,比例为$(a,b)$

      $$ \begin{cases} x = \frac{x’}{a} \y =\frac{y’}{b} \end{cases} $$

    • 插值法

      一些常量的定义:

      $x,y \rightarrow (根据变换公式计算得到的坐标,可能为小数)$

      $x_1= \lfloor x \rfloor $

      $y_1= \lfloor y \rfloor $

      $x_2= \lceil x \rceil $

      $y_2= \lceil y \rceil $

      $radio_{11}=x-x_1$

      $radio_{12}=x_2-x$

      $radio_{21}=y-y1$

      $radio_{22}=y2-y$

      (理论上 $radio_{11}=\frac{x-x_1}{x_2-x_1}$ ,由于$x_2-x_1=1$ 。其他$radio$同理)

      $C\in {R,G,B} \rightarrow (原始图像的RGB值)$

      $C’\in {R’,G’,B’} \rightarrow (处理后图像的RGB值)$

      则:

      $$C’(x’,y’)=radio_{21}*(radio_{11}*C(x_1,y_1)+radio_{12}C(x_1,y_2))\\qquad\qquad+ radio_{22}(radio_{11}*C(x_2,y_1)+radio_{12}*C(x_2,y_2))$$

      上述公式即为双线性插值法。

      其本质在于,利用像素点变换前的实际位置周围四个像素点的 $RGB$ 值的加权平均来计算新像素点的 $RGB$ 值。并且如果周围的像素点距离实际位置越远,其的值对实际的 $RGB$ 值影响越小。原理如下图所示。

      image-20191116193730438

三、源代码和分析

  • 源代码

    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
    /*
    本代码的坐标系统:
    左上角对应坐标原点,水平向右为x轴正向,垂直向下为y轴正向。
    根据bmp文件的扫描顺序,这样的坐标系统刚好符合常规的坐标系。
    */
    #include<stdio.h>
    #include<stdlib.h>
    #include<math.h>
    #include<string.h>
    #define WORD unsigned short
    #define DWORD unsigned int
    #define BYTE unsigned char
    #define INF 9999999
    const double pi=3.14;
    int ph,pw,psize,lsize,cx,cy;
    typedef struct BMP_FILE_HEADER
    {
    WORD bfType;
    DWORD bfSize;
    WORD bfReserved1;
    WORD bfReserved2;
    DWORD bfOffBits;
    }BMPFILEHEADER;
    /* 位图信息头 */
    typedef struct BMP_INFO
    {
    DWORD biSize; // 信息头的大小
    DWORD biWidth; // 图像的宽度
    DWORD biHeight; // 图像的高度
    WORD biPlanes; // 图像的位面数
    WORD biBitCount; // 每个像素的位数
    DWORD biCompression; // 压缩类型
    DWORD biSizeImage; // 图像的大小,以字节为单位
    DWORD biXPelsPerMeter; // 水平分辨率
    DWORD biYPelsPerMeter; // 垂直分辨率
    DWORD biClrUsed; // 使用的色彩数
    DWORD biClrImportant; // 重要的颜色数
    } BMPINF; // 40 字节
    BMPFILEHEADER thisBmpFileHeader; // 定义一个 BMP 文件头的结构体
    BMPINF thisBmpInfo; // 定义一个 BMP 文件信息结构体
    void readFileHead(FILE *fp); //fp指向的文件的文件头读入到thisBMPFileHeader中
    void writeFileHead(FILE *fp); //thisBMPFileHeader写入到fp指向的文件的文件头中
    void Translation(BYTE *p,int dx,int dy,int flag);//进行平移操作,dx,dy为坐标移动的增量。flag=0时,画布增大,flag=1时,画布不增大。
    void Rotation(BYTE *p,double theta);//对图像旋转theta角度
    void Rotation2(BYTE *p,double theta);//对图像旋转theta角度
    void Shear(BYTE *p,double theta,int flag);//对图像投影theta角度
    void Scale(BYTE *p,float a,float b);//对图像放缩变换,a,b分别表示横纵坐标方向的比例。
    void Mirror(BYTE *p,int flag);//对图像进行对称变换,flag=0时x轴,flag=1时y轴
    double GetRolX(int x,int y,double theta);
    double GetRolY(int x,int y,double theta);
    int GetRolHW(int *x,int *y,double theta);
    int Interpolation(BYTE *p,double x,double y,int _);
    void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);
    int main()
    {
    FILE *fp; // 定义一个文件指针
    if((fp=fopen("in.bmp","rb"))==NULL)
    {
    printf("Can't open the file\n");
    return 0;
    }
    printf("open the file\n");
    readFileHead(fp);
    ph=thisBmpInfo.biHeight;//高度
    pw=thisBmpInfo.biWidth;//宽度
    psize=pw*ph*3;//图片信息字节总数
    lsize=pw*3;//每行信息字节总数
    BYTE* p=(BYTE*)malloc(psize);
    fread(p,psize,1,fp);
    fclose(fp);
    Translation(p,-200,-100,0);
    Translation(p,-200,-100,1);
    Rotation(p,pi/3.0);
    Rotation2(p,pi/3.0);
    Scale(p,1.2,0.8);
    Shear(p,pi/6,0);
    Shear(p,pi/6,1);
    Mirror(p,0);
    Mirror(p,1);
    return 0;
    }
    void readFileHead(FILE *fp)
    {
    fread(&thisBmpFileHeader.bfType,sizeof(thisBmpFileHeader.bfType),1,fp);
    fread(&thisBmpFileHeader.bfSize,sizeof(thisBmpFileHeader.bfSize),1,fp);
    fread(&thisBmpFileHeader.bfReserved1,sizeof(thisBmpFileHeader.bfReserved1),1,fp);
    fread(&thisBmpFileHeader.bfReserved2,sizeof(thisBmpFileHeader.bfReserved2),1,fp);
    fread(&thisBmpFileHeader.bfOffBits,sizeof(thisBmpFileHeader.bfOffBits),1,fp);
    fread(&thisBmpInfo.biSize, sizeof(thisBmpInfo.biSize), 1, fp);
    fread(&thisBmpInfo.biWidth, sizeof(thisBmpInfo.biWidth), 1, fp);
    fread(&thisBmpInfo.biHeight, sizeof(thisBmpInfo.biHeight), 1, fp);
    fread(&thisBmpInfo.biPlanes, sizeof(thisBmpInfo.biPlanes), 1, fp);
    fread(&thisBmpInfo.biBitCount, sizeof(thisBmpInfo.biBitCount), 1, fp);
    fread(&thisBmpInfo.biCompression, sizeof(thisBmpInfo.biCompression), 1, fp);
    fread(&thisBmpInfo.biSizeImage, sizeof(thisBmpInfo.biSizeImage), 1, fp);
    fread(&thisBmpInfo.biXPelsPerMeter, sizeof(thisBmpInfo.biXPelsPerMeter), 1, fp);
    fread(&thisBmpInfo.biYPelsPerMeter, sizeof(thisBmpInfo.biYPelsPerMeter), 1, fp);
    fread(&thisBmpInfo.biClrUsed, sizeof(thisBmpInfo.biClrUsed), 1, fp);
    fread(&thisBmpInfo.biClrImportant, sizeof(thisBmpInfo.biClrImportant), 1, fp);
    }
    void writeFileHead(FILE *fp)
    {
    // 写入位图文件头
    fwrite(&thisBmpFileHeader.bfType,sizeof(thisBmpFileHeader.bfType),1,fp);
    fwrite(&thisBmpFileHeader.bfSize,sizeof(thisBmpFileHeader.bfSize),1,fp);
    fwrite(&thisBmpFileHeader.bfReserved1,sizeof(thisBmpFileHeader.bfReserved1),1,fp);
    fwrite(&thisBmpFileHeader.bfReserved2,sizeof(thisBmpFileHeader.bfReserved2),1,fp);
    fwrite(&thisBmpFileHeader.bfOffBits,sizeof(thisBmpFileHeader.bfOffBits),1,fp);
    // 写入位图信息头
    fwrite(&thisBmpInfo.biSize, sizeof(thisBmpInfo.biSize), 1, fp);
    fwrite(&thisBmpInfo.biWidth, sizeof(thisBmpInfo.biWidth), 1, fp);
    fwrite(&thisBmpInfo.biHeight, sizeof(thisBmpInfo.biHeight), 1, fp);
    fwrite(&thisBmpInfo.biPlanes, sizeof(thisBmpInfo.biPlanes), 1, fp);
    fwrite(&thisBmpInfo.biBitCount, sizeof(thisBmpInfo.biBitCount), 1, fp);
    fwrite(&thisBmpInfo.biCompression, sizeof(thisBmpInfo.biCompression), 1, fp);
    fwrite(&thisBmpInfo.biSizeImage, sizeof(thisBmpInfo.biSizeImage), 1, fp);
    fwrite(&thisBmpInfo.biXPelsPerMeter, sizeof(thisBmpInfo.biXPelsPerMeter), 1, fp);
    fwrite(&thisBmpInfo.biYPelsPerMeter, sizeof(thisBmpInfo.biYPelsPerMeter), 1, fp);
    fwrite(&thisBmpInfo.biClrUsed, sizeof(thisBmpInfo.biClrUsed), 1, fp);
    fwrite(&thisBmpInfo.biClrImportant, sizeof(thisBmpInfo.biClrImportant), 1, fp);
    }
    void Translation(BYTE *p,int dx,int dy,int flag)
    {
    if(flag!=1&&flag!=0)
    {
    printf("Translation Error! Please input a right value of flag!\n");
    return;
    }
    if(dx%4) dx=(dx/4+1)*4;//保证x轴方向的偏移量为4的倍数
    int i,j,tmp,n;
    if(flag==1)
    {
    BYTE *pOUT=(BYTE*)malloc(psize);
    for(i=0;i<ph;i++)
    for(j=0;j<pw;j++)
    {
    tmp=lsize*i+j*3;
    int tmpi=i-dy,tmpj=j-dx;
    if(tmpi<0) tmpi+=ph;
    if(tmpi>=ph) tmpi-=ph;
    if(tmpj<0) tmpj+=pw;
    if(tmpj>=pw) tmpj-=pw;
    int Ntmp = tmpi*lsize+tmpj*3;
    pOUT[tmp]=p[Ntmp];
    pOUT[tmp+1]=p[Ntmp+1];
    pOUT[tmp+2]=p[Ntmp+2];
    }
    OutPutImg(pOUT,pw,ph,"Translation1");
    }
    else
    {
    int nh = ph+abs(dy),nw=pw+abs(dx),nsize=nw*nh*3;
    BYTE *pOUT=(BYTE*)malloc(nw*nh*3);
    int oi[2],oj[2];
    if(dx>=0)
    {
    oj[0]=dx;
    oj[1]=nw-1;
    }
    if(dx<0)
    {
    oj[0]=0;
    oj[1]=ph-1;
    }
    if(dy>=0)
    {
    oi[0]=dy;
    oi[1]=nh-1;
    }
    if(dy<0)
    {
    oi[0]=0;
    oi[1]=pw-1;
    }
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    int Ntmp = i*nw*3+j*3;
    if(i>=oi[0]&&i<=oi[1]&&j>=oj[0]&&j<=oj[1])
    {

    tmp=lsize*(i-oi[0])+(j-oj[0])*3;
    pOUT[Ntmp]=p[tmp];
    pOUT[Ntmp+1]=p[tmp+1];
    pOUT[Ntmp+2]=p[tmp+2];
    }
    else
    {
    pOUT[Ntmp]=pOUT[Ntmp+1]=pOUT[Ntmp+2]=0;
    }
    }
    OutPutImg(pOUT,nw,nh,"Translation0");
    }
    }
    void Rotation(BYTE *p,double theta)//对图像旋转theta角度
    {
    int nh,nw;
    GetRolHW(&nw,&nh,theta);
    BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
    int dX=(nw-pw)/2,dY=(nh-ph)/2; // 由于图像放大,计算坐标后需要移动一定距离
    int i,j,tmp;
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    int _;
    tmp=i*nw*3+j*3;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=0;
    }

    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    tmp=nw*i*3+j*3;
    double a,b;
    a=j-dX-cx+cx*cos(theta)-cy*sin(theta);
    b=i-dY-cy+cy*cos(theta)+cx*sin(theta);
    double x,y;
    x=b*sin(theta)+a*cos(theta);
    y=b*cos(theta)-a*sin(theta);
    int _;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=Interpolation(p,x,y,_);
    }
    int x=theta*180/pi;
    char s1[100]="Rotation",s[20];
    sprintf(s,"_%d_degree",x);
    strcat(s1,s);
    OutPutImg(pOUT,nw,nh,s1);
    }
    void Shear(BYTE *p,double theta,int flag)//对图像投影theta角度,flag==0 -> x轴投影,1->y轴
    {
    BYTE* pOUT;
    char s1[100]="Shear",s[20];
    int x=theta*180/pi;
    if(flag!=1&&flag!=0)
    {
    printf("Shear Error! Please input a right value of flag!\n");
    return;
    }
    int nw,nh,tmp,i,j,_;
    if(flag==0)
    {
    sprintf(s,"_X_%d_degree",x);
    nh=ph;
    nw=pw+ph*tan(theta);
    if(nw%4) nw=(nw/4+1)*4;
    pOUT=(BYTE*)malloc(nw*nh*3);
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    for(_=0;_<3;_++)
    pOUT[i*nw*3+j*3+_]=0;
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    double x=(double)j-tan(theta)*i,
    y=(double)i;
    tmp=i*nw*3+j*3;
    int _;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=Interpolation(p,x,y,_);
    }
    }
    else
    {
    sprintf(s,"_Y_%d_degree",x);
    nh=ph+pw*tan(theta);
    nw=pw;
    pOUT=(BYTE*)malloc(nw*nh*3);
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    for(_=0;_<3;_++)
    pOUT[i*nw*3+j*3+_]=0;
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    double y=(double)i-tan(theta)*j,
    x=(double)j;
    tmp=i*nw*3+j*3;
    int _;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=Interpolation(p,x,y,_);
    }
    }
    strcat(s1,s);
    OutPutImg(pOUT,nw,nh,s1);
    }
    void Scale(BYTE *p,float a,float b)//对图像放缩变换,a,b分别表示横纵坐标方向的比例。
    {
    int nh=(int)(ph*b),
    nw=(int)(pw*a);
    if(nw%4) nw=(nw/4+1)*4;
    BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
    int *flag=(int *)malloc(sizeof(int)*nw*nh);
    memset(pOUT,0,nh*nw*3);
    memset(flag,0,sizeof(int)*nh*nw);
    int i,j,tmp;
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    tmp=i*nw*3+j*3;
    double x=floor((j*1.0)/a),
    y=floor((i*1.0)/b);
    int _;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=Interpolation(p,x,y,_);
    }
    OutPutImg(pOUT,nw,nh,"Scale");
    }
    void Mirror(BYTE *p,int flag)//对图像进行对称变换,flag=0时x轴,flag=1时y轴
    {
    BYTE *pOUT=(BYTE*)malloc(psize);
    int i,j,tmp;
    if(flag==0)
    {
    for(i=0;i<ph;i++)
    for(j=0;j<pw;j++)
    {
    tmp=i*lsize+j*3;
    pOUT[tmp]=p[i*lsize+(pw-j-1)*3];
    pOUT[tmp+1]=p[i*lsize+(pw-j-1)*3+1];
    pOUT[tmp+2]=p[i*lsize+(pw-j-1)*3+2];
    }
    FILE *fp=fopen("MirrorX.bmp","wb");
    writeFileHead(fp);
    fwrite(pOUT,psize,1,fp);
    fclose(fp);
    printf("MirrorX finised!\n");
    }
    if(flag==1)
    {
    for(j=0;j<pw;j++)
    for(i=0;i<ph;i++)
    {
    tmp=i*lsize+j*3;
    pOUT[tmp]=p[(ph-i-1)*lsize+j*3];
    pOUT[tmp+1]=p[(ph-i-1)*lsize+j*3+1];
    pOUT[tmp+2]=p[(ph-i-1)*lsize+j*3+2];
    }
    FILE *fp=fopen("MirrorY.bmp","wb");
    writeFileHead(fp);
    fwrite(pOUT,psize,1,fp);
    fclose(fp);
    printf("MirrorY finised!\n");
    }
    }
    double GetRolX(int x,int y,double theta)
    {
    cx=pw/2;
    cy=ph/2;
    x-=cx;
    y-=cy;
    double RolX=x*cos(theta)-y*sin(theta);
    return RolX=RolX+cx;
    }
    double GetRolY(int x,int y,double theta)
    {
    cx=pw/2;
    cy=ph/2;
    x-=cx;
    y-=cy;
    double RolY=x*sin(theta)+y*cos(theta);
    return RolY=RolY+cy;
    }
    int GetRolHW(int *x,int *y,double theta)
    {
    double tmpx[4],tmpy[4],Maxx=-1,Minx=INF,Maxy=-1,Miny=INF;
    int fx[4]={0},fy[4]={0};
    fx[2]=fx[1]=pw;
    fy[2]=fy[3]=ph;
    int i;
    for(i=0;i<4;i++)
    {
    tmpx[i]=GetRolX(fx[i],fy[i],theta);
    tmpy[i]=GetRolY(fx[i],fy[i],theta);
    if(tmpx[i]>Maxx) Maxx=tmpx[i];
    if(tmpx[i]<Minx) Minx=tmpx[i];
    if(tmpy[i]>Maxy) Maxy=tmpy[i];
    if(tmpy[i]<Miny) Miny=tmpy[i];
    }
    *x=(int)(Maxx-Minx)+2;
    if((*x)%4) *x=(*x/4+1)*4;
    *y=(int)(Maxy-Miny)+2;
    }
    int Interpolation(BYTE *p,double x,double y,int _)
    {
    int x1,x2,y1,y2;
    x1=floor(x);y1=floor(y);
    x2=x1+1;y2=y1+1;
    if(x1>=0&&x1<pw-1&&y1>=0&&y1<ph-1)
    {
    float radio11=x-x1,radio12=x2-x,
    radio21=y-y1,radio22=y2-y;
    int tmp1,tmp2,tmp3,tmp4;
    tmp1=y1*lsize+x1*3;
    tmp2=y1*lsize+x2*3;
    tmp3=y2*lsize+x1*3;
    tmp4=y2*lsize+x2*3;
    int X1,X2;
    X1=p[tmp1+_]*radio11+p[tmp2+_]*radio12;
    X2=p[tmp3+_]*radio11+p[tmp4+_]*radio12;
    return X1*radio21+X2*radio22;
    }
    else
    return 0;
    }
    void OutPutImg(BYTE *pOUT,int nw,int nh,char *name)
    {
    char postfix[10]=".bmp",tmp[100];
    strcpy(tmp,name);
    strcat(tmp,postfix);
    FILE *fp=fopen(tmp,"wb");
    thisBmpFileHeader.bfSize=thisBmpFileHeader.bfSize-psize+nh*nw*3;
    thisBmpInfo.biWidth=nw;
    thisBmpInfo.biHeight=nh;
    thisBmpInfo.biSizeImage=nw*nh*3;
    writeFileHead(fp);
    fwrite(pOUT,nw*nh*3,1,fp);
    fclose(fp);
    printf("%s",name);
    printf(" finised!\n");
    thisBmpInfo.biWidth=pw;
    thisBmpInfo.biHeight=ph;
    thisBmpInfo.biSizeImage=pw*ph*3;
    }
    void Rotation2(BYTE *p,double theta)//对图像旋转theta角度
    {
    int nh,nw;
    GetRolHW(&nw,&nh,theta);
    int *fMinX=(int *)malloc(nh*sizeof(int)),
    *fMaxX=(int *)malloc(nh*sizeof(int)),//记录旋转后图像每一行最大、小的横坐标
    *fMinY=(int *)malloc(nw*sizeof(int)),
    *fMaxY=(int *)malloc(nw*sizeof(int));//记录旋转后图像每一列最大、小的纵坐标
    int *flag=(int *)malloc(nw*nh*sizeof(int));
    BYTE *pOUT=(BYTE*)malloc(nh*nw*3);
    int dX=(nw-pw)/2,dY=(nh-ph)/2; // 由于图像放大,计算坐标后需要移动一定距离
    int i,j,tmp;
    for(i=0;i<nh;i++)
    {
    fMinX[i]=INF;
    fMaxX[i]=-1;
    }
    for(i=0;i<nw;i++)
    {
    fMinY[i]=INF;
    fMaxY[i]=-1;
    }
    for(i=0;i<nh;i++)
    for(j=0;j<nw;j++)
    {
    flag[i*nw+j]=0;
    int _;
    tmp=i*nw*3+j*3;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=0;
    }

    for(i=0;i<ph;i++)
    for(j=0;j<pw;j++)
    {

    tmp=lsize*i+j*3;
    int tx=(int)GetRolX(j,i,theta)+dX,
    ty=(int)GetRolY(j,i,theta)+dY,
    Ntmp=ty*nw*3+tx*3;
    pOUT[Ntmp]=p[tmp];
    pOUT[Ntmp+1]=p[tmp+1];
    pOUT[Ntmp+2]=p[tmp+2];
    if(tx<fMinX[ty]) fMinX[ty]=tx;
    if(tx>fMaxX[ty]) fMaxX[ty]=tx;
    if(ty<fMinY[tx]) fMinY[tx]=ty;
    if(ty>fMaxY[tx]) fMaxY[tx]=ty;

    flag[ty*nw+tx]=1;
    }
    for(i=0;i<nh;i++)
    {
    int lastj=-1;
    for(j=0;j<nw;j++)
    {
    tmp=i*nw*3+j*3;

    if(j<fMinX[i]||j>fMaxX[i])
    {
    int _=0;
    for(_=0;_<3;_++)
    pOUT[tmp+_]=0;
    continue;
    }
    if(flag[i*nw+j]==1)
    {
    lastj=j;
    continue;
    }
    else
    {
    int nextj=j;
    while(nextj<nw&&flag[i*nw+nextj]==0) nextj++;
    int _,tmpl=i*nw*3+lastj*3,tmpn=i*nw*3+nextj*3;
    if(nextj>=nw)
    {
    if(lastj==-1) break;
    else
    for(_=0;_<3;_++)
    pOUT[tmp+_]=pOUT[tmpl+_];

    }
    else
    {
    if(lastj==-1)
    for(_=0;_<3;_++)
    pOUT[tmp+_]=pOUT[tmpn+_];
    else
    {
    float radio1=(j-lastj)*1.0/(nextj-lastj),
    radio2=(nextj-j)*1.0/(nextj-lastj);
    for(_=0;_<3;_++)
    pOUT[tmp+_]=radio2*pOUT[tmpn+_]+radio1*pOUT[tmpl+_];
    }
    }
    }
    }
    }
    int x=theta*180/pi;
    char s1[100]="Rotation2",s[20];
    sprintf(s,"_%d_degree",x);
    strcat(s1,s);
    OutPutImg(pOUT,nw,nh,s1);
    }

  • 分析

    • 坐标系统和存储系统

      • 代码中像素点的颜色信息存储在 $P$ 数组中。$P$ 是个一维数组,我们可以把它排列成二维数组,通过计算可以获取下标。$P[tmp]\rightarrow P[i][j] ,(tmp=iwh+j*3)$

      • 代码中的坐标系统是把下标为$P[0][0]$ 的像素点作为坐标原点,每一行下标增长的方向作为 $X$ 轴的正方向。每一列下标增长的方向作为 $Y$ 轴的正方向。

        即 坐标为 $(i,j)$ 的像素点对应的下标为 $P[j][i]$

      • bmp图像的数据扫描顺序是,每一行从左向右扫描,而行则是从下往上扫描。即数组中的第一行是图片中最后一行的数据。这样我们代码中选取的坐标系统与图片中常用的坐标是相符的

    • 全局变量的含义

      1
      2
      3
      4
      5
      6
      7
      const double pi=3.14; //定义常量pi
      int ph,pw, //原始图像的高度和宽度
      psize, //原始图像的字节数
      lsize, //原始图像每行的字节数
      cx,cy; //原始图像的中心点的坐标。
      BMPFILEHEADER thisBmpFileHeader; // 定义一个 BMP 文件头的结构体
      BMPINF thisBmpInfo; // 定义一个 BMP 文件信息结构体

      主函数中读入in.bmp,将文件头信息存储到 thisBmpFileHeaderthisBmpInfo 中。从文件头信息中读取 ph,pw 的值。

      根据公式 $\begin{cases}psize = pwph3\size=pw*3\cx=pw/2\cy=ph/2\end{cases}$ 计算出各个变量的值,作为全局变量供所有函数使用。

  • 函数的作用及参数表

    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
    //fp指向的文件的文件头读入到thisBmpFileHeader和thisBmpInfo中。
    void readFileHead(FILE *fp);
    //thisBmpFileHeader和thisBmpInfo中的信息写入到fp指向的文件中。
    void writeFileHead(FILE *fp);
    //进行平移操作,dx,dy为坐标移动的增量。flag=0时,画布增大,flag=1时,画布不增大。
    void Translation(BYTE *p,int dx,int dy,int flag);
    //对图像逆时针旋转theta角度(双线性插值法)
    void Rotation(BYTE *p,double theta);
    //对图像逆时针旋转theta角度(先旋转,后补全空缺的像素点)
    void Rotation2(BYTE *p,double theta);
    //对图像投影theta角度,flag=0 -> X轴方向投影,flag=0 -> y轴方向投影。
    void Shear(BYTE *p,double theta,int flag);
    //对图像放缩变换,a,b分别表示横纵坐标方向的比例。
    void Scale(BYTE *p,float a,float b);
    //对图像进行对称变换,flag=0时x轴,flag=1时y轴
    void Mirror(BYTE *p,int flag);
    //获取原图中(x,y)像素点绕图片中心逆时针旋转theta后的坐标。
    double GetRolX(int x,int y,double theta);
    double GetRolY(int x,int y,double theta);
    //获取原图经旋转后得到的新的宽度和高度,分别存储在x,y指针中
    int GetRolHW(int *x,int *y,double theta);
    //(x,y)在原图中双线性插值后的结果。 _ 变量表示RGB三个分量之一
    int Interpolation(BYTE *p,double x,double y,int _);
    //输出图像,图片名称为 name
    void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);
  • 文件读入输出。

    1
    2
    3
    void readFileHead(FILE *fp);
    void writeFileHead(FILE *fp);
    void OutPutImg(BYTE *pOUT,int nw,int nh,char *name);

    这三个函数的功能与之前实验中功能相同并且实现起来很简单,不做赘述。

    • 双线性插值算法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      int Interpolation(BYTE *p,double x,double y,int _)
      {
      int x1,x2,y1,y2;
      x1=floor(x);y1=floor(y);
      x2=x1+1;y2=y1+1;
      if(x1>=0&&x1<pw-1&&y1>=0&&y1<ph-1)
      {
      float radio11=x-x1,radio12=x2-x,
      radio21=y-y1,radio22=y2-y;
      int tmp1,tmp2,tmp3,tmp4;
      tmp1=y1*lsize+x1*3;
      tmp2=y1*lsize+x2*3;
      tmp3=y2*lsize+x1*3;
      tmp4=y2*lsize+x2*3;
      int X1,X2;
      X1=p[tmp1+_]*radio11+p[tmp2+_]*radio12;
      X2=p[tmp3+_]*radio11+p[tmp4+_]*radio12;
      return X1*radio21+X2*radio22;
      }
      else
      return 0;
      }

      利用公式 $$C’(x’,y’)=radio_{21}*(radio_{11}*C(x_1,y_1)+radio_{12}C(x_1,y_2))\\qquad\qquad+ radio_{22}(radio_{11}*C(x_2,y_1)+radio_{12}*C(x_2,y_2))$$
      进行模拟即可

    • 平移 Translation

      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
      Translation(BYTE *p,int dx,int dy,int flag)
      if(dx%4) dx=(dx/4+1)*4;//保证x轴方向的偏移量为4的倍数
      int i,j,tmp,n;
      if(flag==1)
      for each pixel (i,j) in new img
      x:=j-dx
      y:=i-dy
      if(x>=pw) x:=x-pw;
      if(x<0) x:=x+pw;
      if(y>=ph) y:=y-ph;
      if(y<0) y:=y+ph;
      pOUT(i,j) := p(y,x)
      end
      end
      else
      Get the width nw and height nh of new img.
      Get the coordidates of the pixels which are border.
      Assume (x0,y0) is the right-top border
      for each pixel (i,j) in new img
      x:=j-x0-dx
      y:=i-y0-dy
      if (x,y)in the old img
      pOUT(i,j) := p(y,x)
      end
      end

      flag的值是1时,我们做的平移操作不改变图像的尺寸。按照公式$$ \left{ \begin{aligned} x = x’-dx\y = y’-dy \end{aligned} \right. $$ 计算得到 $(x,y)$ 的值,如果 $(x,y)$ 超出了图片的范围,我们把它移动到图片另一侧。

      flag的值是0时,我们做的平移操作会改变图像的尺寸。首先获取新的图像的尺寸。其次,根据不同的平移情况,获取有图案的部分位于画布的位置,也就是在画布上的四个边界顶点的坐标。设左上点的坐标是$ (x_0,y_0)$ 。则坐标公式为$$ \left{ \begin{aligned} x = j-x_0-dx\y = i-y_0-dy \end{aligned} \right. $$ 。最后进行对应的赋值。

    • 旋转(插值法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Rotation(BYTE *p,double theta)
    GetRolHW(&nw,&nh,theta);
    dX:=(nw-pw)/2,dY:=(nh-ph)/2;
    for each pixel (i,j) in new img
    a:=j-dX-cx+cx*cos(theta)-cy*sin(theta);
    b:=i-dY-cy+cy*cos(theta)+cx*sin(theta);
    x:=b*sin(theta)+a*cos(theta);
    y:=b*cos(theta)-a*sin(theta);
    pOUT=Interpolation(p,x,y,_);
    end
    end
      $\begin{cases}a=x'-dx-cx+cx*cos\theta-cy*sin\theta\\b=y'-dy-cy+cy*cos\theta+cx*sin\theta\end{cases}$   $$ \left\{ \begin{aligned}  x = b*sin\theta +a*cos\theta \\y = b*cos\theta-a*sin\theta \end{aligned} \right. $$ 
    

    首先利用函数 GetRolHW 获取新图像的宽度和高度。

    然后根据上面的公式计算得到每个像素点对应的初始坐标(x,y)。

    然后用函数 Interpolation 做双线性插值。

  • 旋转(补空法)

      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void Rotation2(BYTE *p,double theta)//对图像旋转theta角度
    GetRolHW(&nw,&nh,theta);
    dX=(nw-pw)/2,dY=(nh-ph)/2
    Get the value of fMinX、fMaxX、fMinY、fMaxY //图像的边界
    for each pixel (i,j) in origin img
    get (x,y) rotate (i,j)
    Mark (i,j)
    end
    for each pixel (i,j) in new img
    if (i,j) is not marked
    if (i,j) is the last marked pixel in the row
    (i,j) = the pixel before it which is marked
    if (i,j) is the first marked pixel in the row
    (i,j) = the pixel after it which is marked
    if (i,j) is in the middle
    (i,j) = radio1*pafter + radio2*pbefore
    end
    end
    end
    首先利用函数 ***GetRolHW*** 获取新图像的宽度和高度。 遍历原始图像中的各个像素点,然后进行旋转操作得到其对应的新图像的坐标,赋值后标记这些点。 遍历新图像中的各个像素点,如果其在图像边界之内,并且没有被标记,则用新图像中其周围的像素点的信息来计算像素点的值。 如果这个像素点之前的所有点均未被标记,则这个像素点的值为其后第一个被标记的点的像素点。 如果这个像素点之后的所有点均未被标记,则这个像素点的值为其前第一个被标记的点的像素点。 如果这个像素点前后均有点被标记,则这个像素点的值为其前第一个被标记的点的像素点和其后第一个被标记的点的像素点的加权平均。

    ```


  • 放缩、对称和投影

    这三个函数的操作非常简单,利用原理部分的公式,计算得到(x,y)后,进行插值算法。

    其代码和旋转操作代码十分相似,只有坐标计算公式不同。

  • 结果展示

    平移

    Translation0

    平移

    Translation1

    X轴方向对称

    MirrorX

    Y轴方向对称

    MirrorY

    逆时针旋转60度

    Rotation_60_degree

    放缩

    Scale

    水平投影30度

    Shear_X_30_degree

    垂直投影30度

    Shear_Y_30_degree

  • 两种不同算法的旋转操作的结果比较

    对原图和两种算法得到的旋转图像分别作出直方图如下

    image-20191117141147551

    image-20191117141200983

    image-20191117141211497

    根据直方图我们可以得出结论两种算法得到的旋转图像均保留了图像的样貌。

四、心得体会

  • 对图像变换的理解

    通过这次实验,我明白了图像的几何变换就是对像素点的坐标进行几何变换,可以借助矩阵运算来更加方便的计算坐标。

  • 对插值法的理解
    我们插值的目的在于,通过几何变换计算坐标时,并不总是能够得到整数的值。当我们计算得到的值是小数的时候,就需要利用这个点周围的像素点RGB值来求其对应的值。很明显距离越远的的点对它造成的影响应当越少。自然而然可以想到按照距离进行加权平均,这也正是双线性插值的公式

  • 写代码过程中的问题

    • 坐标到数组下标的转化

      在根据坐标计算其在数组中对应的下标时,由于新老图片尺寸等问题,在计算时犯了很多低级错误,导致出现奇怪的bug。思考之后有两个改进方案。其一是改进存储方法,不使用一个数组来存储整张图片信息,而是用一个结构体数组来存储,每个结构体含有R,G,B三个成员。这样在计算坐标的时候,就不会出现问题。第一个方法则是把这个计算关系写成一个函数。

    • 最开始写的时候没有对程序有好的架构。导致不同的几何变换中出现了大量重复的操作,代码显得十分冗长。后来进行了一次重构,将重复的操作提炼成函数,这样使代码更加简洁明了。

多译 · 更新日志页面开发(二):初识Redux

最近课程压力不算特别重,多花一些时间去做miniproject。

前几天看完《React16.4 开发简书项目 从零基础入门到实践》系列中的React基础部分,感悟颇深。React的这种设计理念,让我觉得写web是一件十分优雅的事情。这种数据驱动的理念让曾经一些复杂交互的实现变得十分简单。

然鹅React虽然很优雅,但是不够强大。

之前的实践中,我们的应用逻辑很简单,可能只有一个页面和两三个组件。但是尽管如此,在本系列的上一篇文章 初识 React 中的最后一节中,子组件向父组件传递数据已经是一件比较复杂的事情了。

设想倘若一个项目中有10个页面和数十个组件,这些组件批次之间必然会互相使用并且修改一些相同的数据。这个时候我们的React对于这些数据的传递就会十分复杂,从而让我们抓狂。

幸运的是我们有Redux

1、为什么要有Redux

如果你不知道是否需要Redux, 那就是不需要它

只有遇到React实在解决不了的问题,你才需要Redux

这两句话也是对我们上面进行的场景思考的印证。

反过来想,Redux解决的就是比较复杂的情形

阮一峰的博客中有给出具体的例子

如果UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。比如:

  • 用户的使用方式非常简单
  • 用户之间没有协作
  • 不需要与服务器大量交互,也没有使用 WebSocket
  • 视图层(View)只从单一来源获取数据

而如下情况则需要使用 Redux:多交互、多数据源。

  • 用户的使用方式复杂
  • 不同身份的用户有不同的使用方式(比如普通用户和管理员)
  • 多个用户之间可以协作
  • 与服务器大量交互,或者使用了WebSocket
  • View要从多个来源获取数据

从组件角度看,如果有以下场景,可以考虑使用 Redux。

  • 某个组件的状态,需要共享
  • 某个状态需要在任何地方都可以拿到
  • 一个组件需要改变全局状态
  • 一个组件需要改变另一个组件的状态

2、设计思想

Redux 的设计思想就是进行状态管理

  1. Web 应用是一个状态机,状态与视图一一对应
  2. 所有的状态都保存在一个对象里面

3、Redux的工作流程

Redux Flow

Store

Store保存了所有的state

整个应用只能有一个Store

Redux 提供了 createStore 方法,接收另一个函数生成store对象

Action

State和View是一一对应的,View可以很容易的获取State,但是View如何操控State呢,这时候就需要Action的定义

action表示 View 发出的通知,通知 Store来改变State

action是一个对象,type属性是必须的,一个action的规范

1
2
3
4
const action = {
type: 'save',
payload:{/*some data*/}
}

一个好的习惯是 将type的字符串以同名变量保存,这样有助于维护与debug,以免因为输入错误而导致action失效。要知道这种输入错误往往是很难找到的。
例如

1
2
3
4
5
6
7
8

const ACTION = {
SAVE: 'save'
}
const action = {
type: ACTION.SAVE,
payload:{/*some data*/}
}

如果Action很多的话可以定义一个函数来生成Action,这个函数就是 Action Creator

store.dispatch() 是View发出Action的唯一方式,它接收一个action并且发送

Reducer

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

1
2
3
4
5
6
7
8
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};

store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法

这个reducer的命名很容易让我们联想到reduce方法,其实仔细思考一下这是有道理的。reducer本质上也是对 preState 和 data 生成一个nextState

纯函数

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。

纯函数是函数式编程的概念,必须遵守以下一些约束。

  • 不得改写参数
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

这和我们之前的文章中,不允许直接改变state中的list,而是要拷贝一份list然后返回新的数组给state 是一致的

4、个人感悟

经过最近为时一周的React+Redux学习经历,我对于React的理念有了基本的概念。

React的本质就是对于组件的组织与维护,其组织体现在通过布局等方式将这些组件排列,展现在用户面前。而维护的本质则在于组件的状态管理,不论是组件内部的状态管理还是通过Redux对于状态进行管理。

我们通过状态管理来操控页面的变化。

参考资料: Redux 入门教程(一):基本用法 作者: 阮一峰

多译 · 更新日志页面开发(一):初识react

如愿以偿加入了INA技术部的infinite studio,先要经历一段时间的实习期,在此期间要完成mentor分配的miniproject。我的任务是在色老师的产品多译网站上添加一个更新日志的功能。

色老师给我们发了一个React的入门视频,也就是通过这个视频,我第一次认真学习了React,也认真学习了前端开发。

这个视频系列的名字叫做 《React16.4 开发简书项目 从零基础入门到实践》 非常感谢这个作者。

1 回看往事

回看之前的前端开发,大一的时候在鸡腿从0到1,学习了HTML+CSS+JavaScript全家桶,做一个页面往往都是东抄西抄。后来了解到了一些UI框架诸如BootStrap、ElementUI,自己也从来没有去深入了解过其文档,拿着最简单的Demo实现鸡腿要求的功能以后就仅此而已了。后来一段时间学业繁忙,在鸡腿成为部长以后忙于部门各项工作,也不用自己亲历亲为地参与到开发之中。
这门“手艺”彷佛已经丢失。

或许是因为在我内心里,是对前端不以为意的,或者说嗤之以鼻的。

当然人的想法总是会变的。

2 重新审视前端的契机

加入INA以后,色老师分享了一些自己的产品和技术链条,这让我备受震撼。

而简单观看过React的视频及阅读其文档之后,我更是不得不重新审视前端。

因为我发现,我之前干的事情仅仅是一个入门一礼拜前端就应当会干的事情。

3 初识React

这个视频的作者通过一个简书的项目,在实践中教授React的使用方式。看完这些视频之后我对于这个框架有了基本的概念。下面写一下我对于React的认知。

React由Facebook提出,2013年开源,用函数式编程的理念。是目前使用人数最多的前端框架,并且其文档十分建全,并且拥有完善的社区

React Fiber 指React 16以上的版本

结合React官方文档中,React哲学一节

3.1 组件化

React的设计原则就是将一个界面划分为一个个组件,这些组件可能是嵌套的。

你可以将组件当作一种函数或者是对象来考虑,根据单一功能原则来判定组件的范围。

一个组件就是一个类,其render函数返回一个dom标签,用来进行渲染。

组件名首字母必须大写

3.2 props 和 state

props由父组件传入,用以父组件向子组件传输数据

state由组件内部自己维护

props 和 state更新以后,组件都会重新渲染。这也是React的MVVM的体现,或者说React是数据驱动的。
我们想要更新View只需要更新相应的数据(即 props 和 state).比如一个点击事件,我们不用在点击事件中直接操作dom,而是操作dom绑定的数据

在constructor中设定state,作为this的一个属性

render函数中return里面用{}包围的内容解析为代码,其他的解析为标签或者字符串

state的设定不能用赋值语句,要用setState(),state是只读的。

对于数组state的操作,如下例子中,实现在list state中插入了一个”123“字符串,其中三个点是es6语法的展开运算符。

1
2
3
this.setState({
list: [...this.state.list, "123"];
})

数组的map方法可以用来渲染列表,map方法参数是一个函数,该函数可以有三个参数分别为 item index array 其中后两个是可选的。map方法返回一个数组,即参数函数返回值够成的数组。

1
2
3
list.map((item)=>{
return <li>{item}</li>
})

数组的元素删除应当先拷贝一份state,然后操作副本。(注意这个拷贝要用展开表达式,而不能简单用个等于号)

3.3 自底向上的传递方式

从上述props和state的组合方式中可以看出数据是从上到下传递的,并且是单向。
但是我们不得不需要自底向上的数据传递方式。

子组件修改父组件的state: 父组件向子组件传递方法

注意

1
2
3
4
5
6
//in child
handleFatherState("123")

//in father
<Child handleFatherState = {this.props.handleState.bind(this)}/>