内容简介:ArcFace 2.0 API目前支持多种图像格式:在
ArcFace 2.0 API目前支持多种图像格式: BGR24
、 NV21
、 NV12
、 I420
、 YUYV
(Android、IOS只支持其中的部分)。接下来将开始介绍这几种图像格式以及部分转换方式。
一、相关图像颜色空间介绍
1. RGB颜色空间
RGB颜色空间
以Red、Green、Blue三种基本色为基础,进行不同程度的叠加,产生丰富而广泛的颜色,所以俗称三基色模式。
常见的RGB格式有: RGB_565
、 RGB_888
、 ARGB_8888
、 ARGB_4444
等。
2. YUV颜色空间
在
YUV颜色空间
中,Y用来表示亮度,U和V用来表示色度。
常见的YUV格式有以下几大类: planar:
Y、U、V全部连续存储,如 I420
、 YV12
packed:
Y、U、V交叉存储,如 YUYV
semi-planar:
Y连续存储,U、V交叉存储,如 NV21
、 NV12
二、相关图像格式介绍
1. BGR24图像格式
BGR24
图像格式是一种采用24bpp(bit per pixel)的格式。每个颜色通道B、G、R各占8bpp。
排列方式如:
B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R 复制代码
2. NV21图像格式
NV21
图像格式属于YUV颜色空间中的 YUV420SP
格式,每四个Y分量共用一组U分量和V分量,Y连续排序,U与V交叉排序。
排列方式如:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y V U V U V U V U V U V U V U V U 复制代码
3. NV12图像格式
NV12
图像格式属于YUV颜色空间中的 YUV420SP
格式,每四个Y分量共用一组U分量和V分量,Y连续排序,U与V交叉排序( NV12
和 NV21
只是U与V的位置相反)。
排列方式如:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y U V U V U V U V U V U V U V U V 复制代码
4. I420图像格式
I420
图像格式属于YUV颜色空间中的 YUV420P
格式,每四个Y分量共用一组U分量和V分量,Y、U、V各自连续排序。(为了便于说明Y、U、V的共用关系,U和V都未换行)
排列方式如:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y U U U U U U U U V V V V V V V V 复制代码
5. YV12图像格式
YV12
图像格式属于YUV颜色空间中的 YUV420P
格式,每四个Y分量共用一组U分量和V分量,Y、U、V各自连续排序(为了便于说明Y、U、V的共用关系,U和V都未换行)( YV12
和 I420
只是U与V的位置相反)。
排列方式如:
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y V V V V V V V V U U U U U U U U 复制代码
6. YUYV图像格式
YUYV
图像格式属于YUV颜色空间中的 YUV422
格式,每两个Y分量共用一组U分量和V分量,Y、U、V交叉排序。
排列方式如:
Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V Y U Y V 复制代码
三、图像格式转换
由于图像的格式多种多样,转换的方法也不胜枚举,只要了解了 YUV
和 RGB
数据的排列方式,自己编写图像转换代码也花不了多少时间。以下列出部分的图像转换的 Java 代码供参考。
1. 从
Bitmap
中获取 ARGB_8888
图像格式数据(Android平台)
Bitmap
支持多种格式: ALPHA_8,RGB_565,ARGB_4444,ARGB_8888,RGBA_F16,HARDWARE
。我们目前主要选择 ARGB_8888
进行格式转换。
我们可使用
Bitmap
类中的
public void getPixels(@ColorInt int[] pixels, int offset, int stride, int x, int y, int width, int height)
方法获取 int[]
类型的argb数据或
public void copyPixelsToBuffer (Buffer dst)
方法获取 byte[]
类型的 ARGB_8888
数据。
2. ARGB_8888
转换为 BGR_24
举个例子,对于4x2的图片, ARGB_8888
格式内容为:
A1 R1 G1 B1 A2 R2 G2 B2 A3 R3 G3 B3 A4 R4 G4 B4 A5 R5 G5 B5 A6 R6 G6 B6 A7 R7 G7 B7 A8 R8 G8 B8 复制代码
那么若需要转化为 BGR_24
,内容将变成:
B1 G1 R1 B2 G2 R2 B3 G3 R3 B4 G4 R4 B5 G5 R5 B6 G6 R6 B7 G7 R7 B8 G8 R8 复制代码
BGR_24
内容为3个 byte
一组, ARGB_8888
内容为4个 byte
一组。因此,对于第一组 ARGB_8888(A1 R1 G1 B1)
和第一组 BGR_24(B1 G1 R1)
,其对应关系为:
bgr24[0] = argb8888[3]; bgr24[1] = argb8888[2]; bgr24[2] = argb8888[1]; 复制代码
对应的转换代码:
public static byte[] argb8888ToBgr24(byte[] argb8888) { if (argb8888 == null){ throw new IllegalArgumentException("invalid image params!"); } int groupNum = argb8888.length / 4; byte[] bgr24 = new byte[groupNum * 3]; int bgr24Index = 0; int argb8888Index = 0; for (int i = 0; i < groupNum; i++) { bgr24[bgr24Index] = argb8888[argb8888Index + 2]; bgr24[bgr24Index + 1] = argb8888[argb8888Index + 1]; bgr24[bgr24Index + 2] = argb8888[argb8888Index]; bgr24Index += 3; argb8888Index += 4; } return bgr24; } 复制代码
3. ARGB_8888
转换为 NV21
rgb
转 yuv
的算法:
int y = (66 * r + 129 * g + 25 * b + 128 >> 8) + 16; int u = (-38 * r - 74 * g + 112 * b + 128 >> 8) + 128; int v = (112 * r - 94 * g - 18 * b + 128 >> 8) + 128; 复制代码
转换方法:
-
int[]
类型的ARGB_8888
数据转换为NV21
:
private static byte[] argbToNv21(int[] argb, int width, int height) { if (argb == null || argb.length == 0 || width * height != argb.length) { throw new IllegalArgumentException("invalid image params!"); } int yIndex = 0; int uvIndex = width * height; int argbIndex = 0; byte[] nv21 = new byte[width * height * 3 / 2]; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { //对于int型color数据,格式为0xAARRGGBB,可进行与运算后移位取对应A R G B, //但是该YUV转换公式中不需要ALPHA,因此我们只需要取 R G B 即可。 int r = (argb[argbIndex] & 0xFF0000) >> 16; int g = (argb[argbIndex] & 0x00FF00) >> 8; int b = argb[argbIndex] & 0x0000FF; //获取该像素点的R G B,并转换为Y U V,但byte范围是0x00~0xFF,因此在赋值时还需进行判断 int y = (66 * r + 129 * g + 25 * b + 128 >> 8) + 16; nv21[yIndex++] = (byte) (y < 0 ? 0 : (y > 0xFF ? 0xFF : y)); if ((j & 1) == 0 && (argbIndex & 1) == 0 && uvIndex < nv21.length - 2) { int u = (-38 * r - 74 * g + 112 * b + 128 >> 8) + 128; int v = (112 * r - 94 * g - 18 * b + 128 >> 8) + 128; nv21[uvIndex++] = (byte) (v < 0 ? 0 : (v > 0xFF ? 0xFF : v)); nv21[uvIndex++] = (byte) (u < 0 ? 0 : (u > 0xFF ? 0xFF : u)); } ++argbIndex; } } return nv21; } 复制代码
-
byte[]
类型的ARGB_8888
数据转换为NV21
(原理同方法1):
private static byte[] argbToNv21(byte[] argb, int width, int height) { if (argb == null || argb.length == 0 || width * height * 4 != argb.length) { throw new IllegalArgumentException("invalid image params!"); } int yIndex = 0; int uvIndex = width * height; int argbIndex = 0; byte[] nv21 = new byte[width * height * 3 / 2]; for (int j = 0; j < height; ++j) { for (int i = 0; i < width; ++i) { argbIndex++; int r = argb[argbIndex++]; int g = argb[argbIndex++]; int b = argb[argbIndex++]; r &= 0x000000FF; g &= 0x000000FF; b &= 0x000000FF; int y = ((66 * r + 129 * g + 25 * b + 128 >> 8) + 16); nv21[yIndex++] = (byte) (y > 0xFF ? 0xFF : (y < 0 ? 0 : y)); if ((j & 1) == 0 && ((argbIndex >> 2) & 1) == 0 && uvIndex < nv21.length - 2) { int u = ((-38 * r - 74 * g + 112 * b + 128 >> 8) + 128); int v = ((112 * r - 94 * g - 18 * b + 128 >> 8) + 128); nv21[uvIndex++] = (byte) (v > 0xFF ? 0xFF : (v < 0 ? 0 : v)); nv21[uvIndex++] = (byte) (u > 0xFF ? 0xFF : (u < 0 ? 0 : u)); } } } return nv21; } 复制代码
4. NV21
转换为 BGR24
yuv
转 rgb
算法:
int r = (int) ((y & 0xFF) + 1.4075 * ((v & 0xFF) - 128)); int g = (int) ((y & 0xFF) - 0.3455 * ((u & 0xFF) - 128) - 0.7169 * ((v & 0xFF) - 128)); int b = (int) ((y & 0xFF) + 1.779 * ((u & 0xFF) - 128)); 复制代码
转换方法:
private static byte[] nv21ToBgr24(byte[] nv21, int width, int height) { if (nv21 == null || nv21.length == 0 || width * height * 3 / 2 != nv21.length) { throw new IllegalArgumentException("invalid image params!"); } byte[] bgr24 = new byte[width * height * 3]; int bgrLineSize = width * 3; //偶数行的bgr数据下标 int evenLineBgrIndex = 0; //奇数行的bgr数据下标 int oddLineBgrIndex = bgrLineSize; //当前一行y数据最左边的下标 int yLineStart = 0; //uv数据的下标 int uvIndex = width * height; //由于NV21的共用关系,每2行做一次转换 for (int i = 0; i < height; i += 2) { for (int widthOffset = 0; widthOffset < width; widthOffset++) { byte v = nv21[uvIndex]; byte u = nv21[uvIndex + 1]; byte yEven = nv21[yLineStart + widthOffset]; byte yOdd = nv21[yLineStart + width + widthOffset]; //偶数行YUV转RGB int r, g, b; r = (int) ((yEven & 0xFF) + 1.4075 * ((v & 0xFF) - 128)); g = (int) ((yEven & 0xFF) - 0.3455 * ((u & 0xFF) - 128) - 0.7169 * ((v & 0xFF) - 128)); b = (int) ((yEven & 0xFF) + 1.779 * ((u & 0xFF) - 128)); r = r < 0 ? 0 : r > 0xFF ? 0xFF : r; g = g < 0 ? 0 : g > 0xFF ? 0xFF : g; b = b < 0 ? 0 : b > 0xFF ? 0xFF : b; bgr24[evenLineBgrIndex++] = (byte) b; bgr24[evenLineBgrIndex++] = (byte) g; bgr24[evenLineBgrIndex++] = (byte) r; //奇数行YUV转RGB r = (int) ((yOdd & 0xFF) + 1.4075 * ((v & 0xFF) - 128)); g = (int) ((yOdd & 0xFF) - 0.3455 * ((u & 0xFF) - 128) - 0.7169 * ((v & 0xFF) - 128)); b = (int) ((yOdd & 0xFF) + 1.779 * ((u & 0xFF) - 128)); r = r < 0 ? 0 : r > 0xFF ? 0xFF : r; g = g < 0 ? 0 : g > 0xFF ? 0xFF : g; b = b < 0 ? 0 : b > 0xFF ? 0xFF : b; bgr24[oddLineBgrIndex++] = (byte) b; bgr24[oddLineBgrIndex++] = (byte) g; bgr24[oddLineBgrIndex++] = (byte) r; //每两个y将uv下标增1 if ((widthOffset & 1) == 1) { uvIndex += 2; } } //由于在内层循环中已经做过width * 3次自增,所以外层循环中只需要增加一行 evenLineBgrIndex += bgrLineSize; oddLineBgrIndex += bgrLineSize; //y增2行 yLineStart += width * 2; } return bgr24; } 复制代码
5. NV12
和 NV21
的互换
NV21
和 NV12
只是U与V的数据位置不同,因此, NV21
转换为 NV12
的代码同样适用于 NV12
转换为 NV21
。可参考如下代码:
public static byte[] nv21ToNv12(byte[] nv21, int width, int height) { if (nv21 == null || nv21.length == 0 || width * height * 3 / 2 != nv21.length) { throw new IllegalArgumentException("invalid image params!"); } final int ySize = width * height; int totalSize = width * height * 3 / 2; byte[] nv12 = new byte[nv21.length]; //复制Y System.arraycopy(nv21, 0, nv12, 0, ySize); //UV互换 for (int uvIndex = ySize; uvIndex < totalSize; uvIndex += 2) { nv12[uvIndex] = nv21[uvIndex + 1]; nv12[uvIndex + 1] = nv21[uvIndex]; } return nv12; } 复制代码
6. NV21
转 YV12
NV21
转化为 YV12
的过程主要是将其UV数据的交叉 排序 修改为连续排序。可参考如下代码:
public static byte[] nv21ToYv12(byte[] nv21, int width, int height) { if (nv21 == null || nv21.length == 0 || width * height * 3 / 2 != nv21.length) { throw new IllegalArgumentException("invalid image params!"); } final int ySize = width * height; int totalSize = width * height * 3 / 2; byte[] yv12 = new byte[nv21.length]; int yv12UIndex = ySize; int yv12VIndex = ySize * 5 / 4; //复制Y System.arraycopy(nv21, 0, yv12, 0, ySize); //复制UV for (int uvIndex = ySize; uvIndex < totalSize; uvIndex += 2) { yv12[yv12UIndex++] = nv21[uvIndex]; yv12[yv12VIndex++] = nv21[uvIndex + 1]; } return yv12; } 复制代码
7. YUYV
转 NV12
在 YUYV
格式中,两个 Y
共用一组 U
和 V
,而 NV12
是四个 Y
共用一组 U
和 V
,因此,这是一个 YUV422
转 YUV420
的过程,需要舍弃一半的 U
和 V
。可参考如下代码:
public static byte[] yuyvToNv12(byte[] yuyv, int width, int height) { if (yuyv == null || yuyv.length == 0) { throw new IllegalArgumentException("invalid image params!"); } int ySize = yuyv.length / 2; byte[] nv12 = new byte[yuyv.length * 3 / 4]; int nv12YIndex = 0; int nv12UVIndex = ySize; boolean copyUV = false; int lineDataSize = width * 2; for (int i = 0, yuyvIndex = 0; i < height; i++, yuyvIndex += lineDataSize) { if (copyUV) { for (int lineOffset = 0; lineOffset < lineDataSize; lineOffset += 4) { //复制Y nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset]; nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset + 2]; //复制UV nv12[nv12UVIndex++] = yuyv[yuyvIndex + lineOffset + 1]; nv12[nv12UVIndex++] = yuyv[yuyvIndex + lineOffset + 3]; } } else { for (int lineOffset = 0; lineOffset < lineDataSize; lineOffset += 4) { //复制Y nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset]; nv12[nv12YIndex++] = yuyv[yuyvIndex + lineOffset + 2]; } } copyUV = !copyUV; } return nv12; } 复制代码
8. I420
和 YV12
的互换
I420
和 YV12
只是 U
与 V
的数据位置不同,因此, I420
转换为 YV12
的代码同样适用于 YV12
转换为 I420
。可参考如下代码:
public static byte[] i420ToYv12(byte[] i420) { if (i420 == null || i420.length == 0 || i420.length % 6 != 0) { throw new IllegalArgumentException("invalid image params!"); } int ySize = i420.length * 2 / 3; int uvSize = i420.length / 6; byte[] yv12 = new byte[i420.length]; //复制Y System.arraycopy(i420, 0, yv12, 0, ySize); //UV互换 System.arraycopy(i420, ySize, yv12, ySize + uvSize, uvSize); System.arraycopy(i420, ySize + uvSize, yv12, ySize, uvSize); return yv12; } 复制代码
9. I420
转换为 YUYV
I420
和 YUYV
相比, I420
的 U
和 V
只有 YUYV
的一半,这是一个 YUV420
转 YUV422
的过程,缺损的数据只能通过复用 U
和 V
弥补。
public static byte[] i420ToYuyv(byte[] i420, int width, int height) { if (i420 == null || i420.length == 0 || i420.length != width * height * 3 / 2) { throw new IllegalArgumentException("invalid image params!"); } byte[] yuyv = new byte[width * height * 2]; int yuyvLineSize = width * 2; int i420YIndex = 0; int i420UIndex = width * height; int i420VIndex = width * height * 5 / 4; int yuyvLineStart = 0; for (int i = 0; i < height; i += 2) { for (int lineOffset = 0; lineOffset < yuyvLineSize; lineOffset += 4) { byte u = i420[i420UIndex++]; byte v = i420[i420VIndex++]; //偶数行数据赋值 int yuyvOffset = yuyvLineStart + lineOffset; yuyv[yuyvOffset] = i420[i420YIndex]; yuyv[yuyvOffset + 1] = u; yuyv[yuyvOffset + 2] = i420[i420YIndex + 1]; yuyv[yuyvOffset + 3] = v; //奇数行数据赋值 int yuyvNextLineOffset = yuyvLineStart + yuyvLineSize + lineOffset; yuyv[yuyvNextLineOffset] = i420[i420YIndex + width]; yuyv[yuyvNextLineOffset + 1] = u; yuyv[yuyvNextLineOffset + 2] = i420[i420YIndex + width + 1]; yuyv[yuyvNextLineOffset + 3] = v; i420YIndex += 2; } i420YIndex += width; yuyvLineStart += (width << 2); } return yuyv; } 复制代码
四、图像裁剪
与格式转换相同,只要了解的图像的排列方式,图像的裁剪也并不困难。本文提供一种 RGB
颜色空间的图像裁剪和一种 YUV
颜色空间的图像裁剪。
1. 裁剪NV21
public static byte[] i420ToYv12(byte[] i420) { if (i420 == null || i420.length == 0 || i420.length % 6 != 0) { throw new IllegalArgumentException("invalid image params!"); } int ySize = i420.length * 2 / 3; int uvSize = i420.length / 6; byte[] yv12 = new byte[i420.length]; //复制Y System.arraycopy(i420, 0, yv12, 0, ySize); //UV互换 System.arraycopy(i420, ySize, yv12, ySize + uvSize, uvSize); System.arraycopy(i420, ySize + uvSize, yv12, ySize, uvSize); return yv12; } 复制代码
9. I420
转换为 YUYV
I420
和 YUYV
相比, I420
的 U
和 V
只有 YUYV
的一半,这是一个 YUV420
转 YUV422
的过程,缺损的数据只能通过复用 U
和 V
弥补。
public static byte[] i420ToYuyv(byte[] i420, int width, int height) { if (i420 == null || i420.length == 0 || i420.length != width * height * 3 / 2) { throw new IllegalArgumentException("invalid image params!"); } byte[] yuyv = new byte[width * height * 2]; int yuyvLineSize = width * 2; int i420YIndex = 0; int i420UIndex = width * height; int i420VIndex = width * height * 5 / 4; int yuyvLineStart = 0; for (int i = 0; i < height; i += 2) { for (int lineOffset = 0; lineOffset < yuyvLineSize; lineOffset += 4) { byte u = i420[i420UIndex++]; byte v = i420[i420VIndex++]; //偶数行数据赋值 int yuyvOffset = yuyvLineStart + lineOffset; yuyv[yuyvOffset] = i420[i420YIndex]; yuyv[yuyvOffset + 1] = u; yuyv[yuyvOffset + 2] = i420[i420YIndex + 1]; yuyv[yuyvOffset + 3] = v; //奇数行数据赋值 int yuyvNextLineOffset = yuyvLineStart + yuyvLineSize + lineOffset; yuyv[yuyvNextLineOffset] = i420[i420YIndex + width]; yuyv[yuyvNextLineOffset + 1] = u; yuyv[yuyvNextLineOffset + 2] = i420[i420YIndex + width + 1]; yuyv[yuyvNextLineOffset + 3] = v; i420YIndex += 2; } i420YIndex += width; yuyvLineStart += (width << 2); } return yuyv; } 复制代码
四、图像裁剪
与格式转换相同,只要了解的图像的排列方式,图像的裁剪也并不困难。本文提供一种 RGB
颜色空间的图像裁剪和一种 YUV
颜色空间的图像裁剪。
1. 裁剪NV21或NV12
public static byte[] cropYuv420sp(byte[] yuv420sp, int width, int height, int left, int top, int right, int bottom) { if (yuv420sp == null || yuv420sp.length == 0 || width * height * 3 / 2 != yuv420sp.length) { throw new IllegalArgumentException("invalid image params!"); } if (left < 0 || top < 0 || right > width || bottom > height) { throw new IllegalArgumentException("rect out of bounds!"); } if (right < left || bottom < top) { throw new IllegalArgumentException("invalid rect!"); } if (((right - left) & 1) == 1 || ((bottom - top) & 1) == 1) { throw new IllegalArgumentException("yuv420sp width and height must be even!"); } if ((left & 1 )== 1){ throw new IllegalArgumentException("yuv420sp crop left borderIndex and right borderIndex must be even!"); } int cropImageWidth = right - left; int cropImageHeight = bottom - top; byte[] cropYuv420sp = new byte[cropImageWidth * cropImageHeight * 3 / 2]; //复制Y int originalYLineStart = top * width; int targetYIndex = 0; //复制UV int originalUVLineStart = width * height + top * width / 2; int targetUVIndex = cropImageWidth * cropImageHeight; for (int i = top; i < bottom; i++) { System.arraycopy(yuv420sp, originalYLineStart + left, cropYuv420sp, targetYIndex, cropImageWidth); originalYLineStart += width; targetYIndex += cropImageWidth; if ((i & 1) == 0) { System.arraycopy(yuv420sp, originalUVLineStart + left, cropYuv420sp, targetUVIndex, cropImageWidth); originalUVLineStart += width; targetUVIndex += cropImageWidth; } } return cropYuv420sp; } 复制代码
2. 裁剪BGR24
public static byte[] cropBgr24(byte[] bgr24, int width, int height, int left, int top, int right, int bottom) { if (bgr24 == null || bgr24.length == 0 || width * height * 3 != bgr24.length) { throw new IllegalArgumentException("invalid image params!"); } if (left < 0 || top < 0 || right > width || bottom > height) { throw new IllegalArgumentException("rect out of bounds!"); } if (right < left || bottom < top) { throw new IllegalArgumentException("invalid rect!"); } int cropImageWidth = right - left; int cropImageHeight = bottom - top; byte[] cropBgr24 = new byte[cropImageWidth * cropImageHeight * 3]; int originalLineStart = top * width * 3; int targetIndex = 0; for (int i = top; i < bottom; i++) { System.arraycopy(bgr24, originalLineStart + left * 3, cropBgr24, targetIndex, cropImageWidth * 3); originalLineStart += width * 3; targetIndex += cropImageWidth * 3; } return cropBgr24; } 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 他把一块屏摆进数百家便利店 人脸识别挖掘线下流量 已转化会员过万
- js的类型转化三两事儿
- archTIS:将数据安全转化为经济增长
- 如何将JavaScript转化成Swift?(一)
- python3 第十章 - 如何进行进制转化
- 用Golang将图片转化成ASCII码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
How to Design Programs, 2nd Edition
Matthias Felleisen、Robert Bruce Findler、Matthew Flatt、Shriram Krishnamurthi / MIT Press / 2018-5-4 / USD 57.00
A completely revised edition, offering new design recipes for interactive programs and support for images as plain values, testing, event-driven programming, and even distributed programming. This ......一起来看看 《How to Design Programs, 2nd Edition》 这本书的介绍吧!