ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

02FusionGAN:跑起来

2021-09-14 09:02:24  阅读:257  来源: 互联网

标签:loss 起来 None ir batch 02FusionGAN train tf


运行demo之后续补充

运行CPU版,看各部分工作原理

致命错误:

为了使占用内存不至于过高,使用了batch_size的融合方法
这是与源代码不同的地方
但是,这也造成了生成图像成了小方块

即:test的输入只能是完整图像

这个理解错了,具体原因在XMind中分析
在运行…
merge函数中

img[120X5,120X7]是总画布
img[0:120,0:120,:] 放image[120,120,1]

配置更改:

epochs=2
train_ir等的图片设置为1张
训练结束,想要生成图像,要更改main函数中:is_train->False,操作如下

srcnn.train(FLAGS)
FLAGS.is_train = False
srcnn.train(FLAGS)

代码更改:

util.py第203行添加

padding_h,padding_w = int(padding_h),int(padding_w)

test部分,代码更改如下:

print("Testing...")
if not self.load(config.checkpoint_dir): return            
result = np.zeros(shape=train_label_ir.shape)  #用于存储生成的图像的白板

len_data = len(train_data_ir)

batch_idxs = len_data // config.batch_size
for idx in range(0, batch_idxs):
    idx_start = idx * config.batch_size
    idx_end = min( (idx + 1) * config.batch_size, len_data )

    batch_images_ir = train_data_ir[idx_start:idx_end]
    batch_labels_ir = train_label_ir[idx_start:idx_end]
    batch_images_vi = train_data_vi[idx_start:idx_end]
    batch_labels_vi = train_label_vi[idx_start:idx_end]

    temp_result = self.fusion_image.eval(
        feed_dict={self.images_ir: batch_images_ir, self.labels_ir: batch_labels_ir,
                   self.images_vi: batch_images_vi,self.labels_vi: batch_labels_vi})
    result[idx_start:idx_end,:,:,:] = temp_result

# result = result * 127.5 + 127.5
result = merge(result, [nx_ir, ny_ir])

原来的代码是:

result = self.fusion_image.eval(
                feed_dict={self.images_ir: train_data_ir, self.labels_ir: train_label_ir,
                           self.images_vi: train_data_vi, self.labels_vi: train_label_vi})

load函数
了解:取得path最后的文件名os.path.basename()方法

加载模型使用了方法一

try:
    self.sess.run(tf.global_variables_initializer())
    self.saver.restore(self.sess, os.sep.join([checkpoint_dir,'other_model','CGAN.model-3']))

    print("Model restored !")
    return True
except Exception as e:
    print("Restore Error :",e)
    return False

解释:

  1. 1张输入的裁剪:

batchsize=32
1张输入图片:768X576
每个ep: 1472个图像需要训练,总批数:batch_idxs=46
总批数计算方法:
total_num =int((576-132)/14+1)*int((768-132)/14+1)
batch_idxs = total_num // batchsize

  1. 生成图片的保存位置:

sample/test_image.png

  1. 生成模型的保存(每个ep保存一次)

checkpoint/CGAN_120/...

步骤:

input_setup存为h5数据,再由train函数读出
取得每个批的图片(详细见下面)
训练2次d,1次g
每10批输出一次训练情况

Training...
Epoch: [ 1], step: [10], time: [2607.9601], loss_d: [0.92256868],loss_g:[15.22666168]

####解除bug的封印



总览概括

捋一捋思路

main函数中,先预定义了超参数,然后建立了模型CGAN(),紧接着进行了训练train()
build_model分为好几步:

对Ir,Vi设立占位符,各包含image(原料)和label(对抗)
输入之前,对Ir和Vi各自的image进行了拼接,这样channel变成2
然后生成融合图像fusion
d_loss部分,根据label得到pos损失,根据fusion得到neg损失
g_loss部分,分别得到g_loss_1欺骗损失和g_loss_2辐射相似性及梯度相似性

train部分需要一些处理:

  • 准备数据:
  1. 训练模式下:
    将dataset(Train_ir,Train_vi)文件夹下的bmp,tif找出并排序
    读入照片,将图像平移为0中心,并归一化
    按14步长取输入图像patch,image:3333,label:2121
    将它们作为准备好的输入数据
    使用np.asarray() 转换成 dtype 数据
    制作h5数据,放在./checkpoint_20/Train_ir/train.h5
  2. 测试模式下:
    将dataset(Test_ir,Test_vi)文件夹下的bmp,tif找出并排序
    填充测试图像t1: 左padding,右填充直到33; 下padding,上填充直到33
    将填充后的图像划分为若干个33*33的输入图像i2,并以1个图像作为一个输入单元shape=(33,33,1)
    制作h5数据(./checkpoint_20/Test_ir/test.h5)
    返回测试图像t1的真实尺寸,后面会以此为根据复原融合的图
  • 读出h5数据,作为输入

  • 找出变量组,包括Generator和discriminator的(由前面由
    with tf.variable_scope('discriminator'): ...语句生成)

  • 设置优化操作,tf.train.AdamOptimizer().minimize(self.g_loss_total,var_list=self.g_vars).降低损失是将要训练的目的

  • 开始训练:

  1. 共 batch_idxs 个批次,每个批次batch_size个输入,每个输入有images_ir,labels_ir,images_vi,labels_vi
  2. 判别器2次:每次一个batch_size喂入,计算[discriminator变量组,d_loss]
    生成器1次:每次一个batch_size喂入,计算[Generator变量组,g_loss_total,]
  3. 每10次输出一次比较
  4. 保存模型:
    训练时在路径’CGAN_21/CGAN.model’,保存检查点文件
  • 开始测试
  1. result = self.fusion_image.eval(feed_dict={})
  2. 将结果拼接起来
    保存图片

对代码的改动和注解

廓清概念

image_size: Iir 和 Ivi 的尺寸
label_size: generator生成的尺寸
label图像,有vi的和ir的

作用之一:在判别器中与If对抗
作用之二:计算g_loss中Lcontent一项,它包括If和Iir的相似性,以及If梯度和Ivi梯度的相似性

utils中代码的注解:

  1. scope.reuse_variables()
    验证集通常是取训练集中的一小部分数据来检测训练的准确率,要共享之前训练好的相关参数的值。这种共享方式,需要在 变量命名空间 tf.variable_scope 下进行,而且采用 tf.get_variable 的共享变量定义方式,在共享之前,必须声明以下的参数是需要共享的,否则会重新定义。
with tf.variable_scope("inference") as scope:
    train_y_conv =7 inference(train_images)   #先训练
    scope.reuse_variables()   #说明以下验证集的参数是要共享训练集训练好的参数,而不是重新定义 
    test_y_conv = inference(test_images)  #计算验证集的值

reuse的另一种用法:直接放入scope参数

with tf.variable_scope('discriminator', reuse=reuse):
  1. 对W进行的操作
    指定维度求和
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGrB1hXK-1628232138446)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\tf_reduce_sum.jpg)]

L2范数:平方-求和-取开方
参考博文
范数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zk9b3ozk-1628232138448)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\l.jpg)].
L2范数: 可以用作正则项(防止过拟合)或损失函数(最小二乘误差least squares error, LSE)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7JfeFo7-1628232138449)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\l2.jpg)]
本文代码待解读:像是缩放(看论文)

add_to_collection(名称,值)

将值存储在具有给定名称的集合中。
请注意,集合不是set(去重),因此可以多次向集合添加值。
参数:
名称:集合的键。 GraphKeys 类包含许多集合的标准名称。
value:要添加到集合中的值。
为Graph的一个方法,可以简单地认为Graph下维护了一个字典,key为name,value为list,而add_to_collection就是把变量添加到对应key下的list中

操作的含义待解
3. lrelu:把x<0的区域抬起来一些 比如0.2时,抬起80%

tensorflow中:  
tf.nn.relu(features, name=None)
tf.nn.leaky_relu(features, alpha=0.2, name=None)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SSi9yrOI-1628232138450)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\relu.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vDBbOg7-1628232138451)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\lrelu.png)]

  1. input_setup: np.lib.pad
arr = np.array([[4, 3],[4, 2]])
arr_pad = np.lib.pad(arr,((3,2),(2,1)),'constant',constant_values=0)

表示:在上面填充了3行,下面填充了2行,前面填充了2列,后面填充了1列。填充的都是 常量,0

若是图片填充,参考



model模块中的注解:

  1. tf.Variable()和tf.get_variable()的区别:
    Variable是定义变量,而get_variable是获取变量(只不过如果获取不到就重新定义一个变量),如果按照这种逻辑,已经基本上可以理解两者的差异了

tf.Variable 每次都会创建新的变量,在变量名重复的时候不会报错,而是会自动创建新的变量,只是后缀加上 _1, _2 类似的用以区别。通常 lr 或 global step 之类的辅助变量会使用它来创建。tf.get_variable() 则主要用于网络的权值设置,它可以实现权值共享,在多 GPU 的并行计算的时候使用较多,其实通过 get 的前缀就可以很好看出它们的区别,它一定是和tf.variable_scope()共同使用的,不然二者就没有太大的区别了。

def get_variable(name,shape=None,dtype=None,initializer=None,...):
     
常见的initializer有:常量初始化器tf.constant_initializer、正太分布初始化器tf.random_normal_initializer、截断正态分布初始化器tf.truncated_normal_initializer、均匀分布初始化器tf.random_uniform_initializer。
  1. tf.name_scope(‘IR_input’) 给变量"包"一层名字,方便变量管理
    代码运行见test.py

  2. tf.concat与tf.stack都是在某个维度上对矩阵(向量)进行拼接,不同点在于前者拼接后的矩阵维度不变,后者则会增加一个维度。
    拼接:第二个接在第一个后面

a = tf.constant([[1,2,3],[4,5,6]])
b = tf.constant([[7,8,9],[10,11,12]])
ab1 = tf.concat([a,b],axis=0)
print(sess.run(ab1).shape)  # (4, 3)
ab2 = tf.stack([a,b], axis=0)
print(sess.run(ab2).shape)  # (2, 2, 3)

4.Batch Normalization :为了加速训练对数据进行预处理,最常用的是零均值和PCA(白化).随着网络层次加深,导致每层间以及不同迭代的相同层的输入分布发生改变,导致网络需要重新适应新的分布,迫使我们降低学习率降低影响。有些人首先提出在每层增加PCA(先对数据进行去相关然后再进行归一化),但计算量大.BN是近似白化预处理。
概括:减均值->除标准差->加个性化参数γ、β

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HwLP9ydl-1628232138452)(file:///D:\PythonFiles\paper02_FusionGAN_master\Others\BN.jpg)]
还可参考

两个参数γ、β。这样在训练过程中就可以学习这两个参数,采用适合自己网络的BN公式

tf.contrib.layers.batch_norm(
    inputs,decay=0.999,center=True,scale=False,epsilon=0.001,activation_fn=None,updates_collections=tf.GraphKeys.UPDATE_OPS,...
)

inputs: 输入
decay :衰减系数。合适的衰减系数值接近1.0,特别是含多个9的值:0.999,0.99,0.9。如果训练集表现很好而验证/测试集表现得不好,选择小的系数(推荐使用0.9)。如果想要提高稳定性,zero_debias_moving_mean设为True
center:是否有偏移?如果为True,有b偏移量;如果为False,无b偏移量
scale:input是否有缩放?如果为True,则乘以gamma。如果为False,gamma则不使用。当下一层是线性的时(例如nn.relu),由于缩放可以由下一层完成,所以可以禁用该层。
epsilon:避免被零除
activation_fn:用于激活,默认为线性激活函数
updates_collections :Collections来收集计算的更新操作。updates_ops需要使用train_op来执行。如果为None,则会添加控件依赖项以确保更新已计算到位。

tf.GraphKeys 包含所有graph collection中的标准集合名

graph collection 即tf.Graph,包含两类相关信息:
图结构 ,图集合(tf.add_to_collection函数允许您将对象列表与一个键相关联(其中tf.GraphKeys定义了部分标准键),tf.get_collection则允许您查询与键关联的所有对象。
TensorFlow库的许多组成部分会使用它:例如,当您创建tf.Variable时,系统会默认将其添加到表示“全局变量(tf.global_variables)”和“可训练变量tf.trainable_variables)”的集合中。当您后续创建tf.train.Saver或tf.train.Optimizer时,这些集合中的变量将用作默认参数。

这部分collection的名字被称为tf.GraphKeys,可以用来获取不同类型的op
常见的GraphKeys

  1. tf.nn.conv2d
tf.nn.conv2d (input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

input : 输入的要做卷积的图片,要求为一个张量,shape为 [ batch, in_height, in_width, in_channel ],其中batch为图片的数量,in_height 为图片高度,in_width 为图片宽度,in_channel 为图片的通道数,灰度图该值为1,彩色图为3。(也可以用其它值,但是具体含义不是很理解)

filter: 卷积核,要求也是一个张量,shape为 [ filter_height, filter_width, in_channel, out_channels ],其中 filter_height 为卷积核高度,filter_width 为卷积核宽度,in_channel 是图像通道数 ,和 input 的 in_channel 要保持一致,out_channel 是卷积核数量。

strides: 卷积时在图像每一维的步长,这是一个一维的向量,[ 1, strides, strides, 1],第一位和最后一位固定必须是1

padding: string类型,值为“SAME” 和 “VALID”,表示的是卷积的形式,是否考虑边界。"SAME"是考虑边界,不足的时候用0去填充周围,"VALID"则不考虑
use_cudnn_on_gpu: bool类型,是否使用cudnn加速,默认为true
博文链接

  1. 卷积输出大小计算计算公式

out_size = (w - k + 2p)/s + 1
w:Width ,
k: Kernel_size,
p:Padding_len , 因为一般两边都填充,所以2*p
s: stride_size

  1. d_loss = pos_loss + neg_loss
Ld = 1/N (生成样本bs个:'每个样本If预测值' - '软_标签值0~0.3' 的平方求和)   + 1/N (可见光样本bs个:'每个样本Ivi预测值' - '软_标签值0.7~1.2' 的平方求和)
pos_loss = tf.reduce_mean(
                tf.square(pos - tf.random_uniform(shape=[self.batch_size, 1], minval=0.7, maxval=1.2)))  
neg_loss = tf.reduce_mean(
                tf.square(neg - tf.random_uniform(shape=[self.batch_size, 1], minval=0, maxval=0.3, dtype=tf.float32)))                

tf.reducemean()

tf.reduce_mean(input_tensor, axis=None, keep_dims=False, name=None, reduction_indices=None)
根据给出的axis在input_tensor上求平均值。除非keep_dims为真,axis中的每个的张量秩会减少1。如果keep_dims为真,求平均值的维度的长度都会保持为1.如果不设置axis,所有维度上的元素都会被求平均值,并且只会返回一个只有一个元素的张量。

见test.py代码 函数名:reduce_mean

对axis的理解: 2维中,有行和列,先行后列,行为第1维,因为从axis从0起编号,因此也最小(axis=0).列为第二维,可以理解为次要维.当收缩时,axis=1即收缩次要维.  

当很多维的时候,也可以看数组,从最外层开始,往深层次走,理解为重要性依次减弱.那么,如果axis=-1(最后一个),那么便是收缩最次要维,也就是最里面一个.

tf.summary.scalar()的用法:
这个方法是添加变量到直方图中,但是不配合其他的方法,根本就显示不出来它的意义!
基本使用步骤:
定义变量;
让变量添加到summary.scalar;
将summary保存到磁盘以便tensorboard显示;
把步骤都记录下;
cmd命令,切换到相应的文件夹下,启动tensorborder;
然后再页面上输入localhost:6006
使用样例

自动管理模式merge_all()参考和手动管理模式

summary_writer = tf.summary.FileWriter('E://result/', flush_secs=60)
summary_writer.add_graph(sess.graph)#添加graph图

tf.summary.scalar('loss', loss)
tf.summary.scalar('accuracy', accuracy) ##生成准确率标量图  

sum_ops = tf.summary.merge_all()  #自动管理

metall = sess.run(sum_ops, feed_dict={X: batch_x_test, Y: batch_y_test, keep_prob: 1.})

summary_writer.add_summary(metall, global_step=step) # 写入文件
  1. g_loss = g_loss_1 + g_loss_2
g_loss_1 = 1/N (生成样本bs个:'每个样本If预测值' - '软_标签值0.7~1.2' 的平方求和)   
代表预测值和Generator想要Discriminator认为的值之间的差距.  
即:融合图像和真实图像间的差距
g_loss_2 = (融合图像If-红外图像Ir)的元素的平方的和的平均 + 5 * (If梯度-Ivi梯度)的元素的平方的和的平均

tf.placeholder构筑静态graph -> 启动一个session ->运行模型时feed_dict={…}喂数据
这样做的好处在于:tensorflow帮你优化整个系统的代码

tf.placeholder(
    dtype,     
    shape=None,
    name=None
)
shape:数据形状。默认是None,就是一维值,也可以是多维(比如[2,3], [None, 3]表示列是3,行不定)

示例

import tensorflow as tf
import numpy as np
 
x = tf.placeholder(tf.float32, shape=(1024, 1024))
y = tf.matmul(x, x)
 
with tf.Session() as sess:
    #print(sess.run(y))  # ERROR:此处x还没有赋值
    rand_array = np.random.rand(1024, 1024)
    print(sess.run(y, feed_dict={x: rand_array})) 

Frobenius范数: 矩阵元素的平方和再开方
f范数实际上就是衡量这个矩阵和对应的零矩阵的距离
-文中用于比较真实矩阵和估计矩阵之间的相似性

max_to_keep 参数表示要保留的最近检查点文件的最大数量
参考这里

保存:
tf.train.Saver(max_to_keep=50)  
saver.save(sess,  '路径 + 模型文件名')

载入:
saver = tf.train.Saver()
ckpt = tf.train.get_checkpoint_state(model_save_path)
    # 载入模型,不需要提供模型的名字,会通过 checkpoint 文件定位到最新保存的模型
if ckpt and ckpt.model_checkpoint_path:
    saver.restore(sess, ckpt.model_checkpoint_path)

  1. train 函数
    glob模块用来查找文件目录和文件,返回列表,如下是用来查找e盘中所有的exe文件
glob.glob(r'e:\*.exe')

os.sep: 根据所处的平台,自动采用相应的分隔符号

data_dir = os.sep.join(['hello', 'world'])
得到linux: 'hello/world'或者win: 'hello\world'

图像插值函数原型

sub_input = cv2.resize(sub_input, (config.image_size / 4, config.image_size / 4),interpolation=cv2.INTER_CUBIC)

cv2.resize(InputArray src, OutputArray dst, Size, fx, fy, interpolation)
INTER_CUBIC: 4x4像素邻域的双三次插值


h5数据制作与读取

with h5py.File(savepath, 'w') as hf:
     hf.create_dataset(name , value)
with h5py.File(savepath, 'r') as hf:
     hf.get(name)

#示例
with h5py.File(savepath, 'w') as hf:
    hf.create_dataset('data', data=data)
    hf.create_dataset('label', data=label)
with h5py.File(path, 'r') as hf:
    data = np.array(hf.get('data'))
    label = np.array(hf.get('label'))
    return data, label

tf.train.AdamOptimizer

self.train_fusion_op = tf.train.AdamOptimizer(config.learning_rate).minimize(self.g_loss_total,var_list=self.g_vars)

函数原型为,参考

__init__(
    learning_rate=0.001,...
)
可调用的方法为:
.minimize(
    loss,   
    global_step=None,
    var_list=None,  #会通过更新var_list添加操作以最大限度地最小化 loss
    gate_gradients=GATE_OP,
    aggregation_method=None,
    colocate_gradients_with_ops=False,
    name=None,
    grad_loss=None
)


main中的注解:

1. 在tensorflow中,tf.app.flags.FLAGS已经转移到tf.flags.FLAGS路径下

FLAGS = tf.app.flags.FLAGS
FLAGS = tf.flags.FLAGS

2. tf.app.flags.FLAGS:  在用命令行执行程序时,需要传些参数

参数为key、默认值、和参数描述
# tf.app.flags.DEFINE_string("param_name", "default_val", "description")
tf.flags.DEFINE_string("key1","hello","这是一个字符串")
tf.flags.DEFINE_xxx()就是添加命令行的optional argument(可选参数),而tf.flags.FLAGS可以从对应的命令行参数取出参数。

3. 更改缩进:停留光标在~~~上,会有选项

额外补充

Tensorflow的运行机制包括: 定义计算图和运行计算图两个部分,计算图即模型的构建
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndd861Fc-1628232138453)(file:///D:/PythonFiles/paper02_FusionGAN_master/Others/tf_def.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L58o78gk-1628232138453)(file:///D:/PythonFiles/paper02_FusionGAN_master/Others/tf_process.jpg)]
先定义placeholder、Variable和OP等构成一张完成计算图Graph;
接下来通过新建Session实例启动模型运行,Session实例会分布式执行Graph;
输入数据,根据优化算法更新Variable,然后返回执行结果即Tensor实例。\

一个使用样例:

import tensorflow as tf # 引入tensorflow相关包
constant_a = tf.constant('Hello World!') # 定义常量
with tf.Session() as session:
  print(session.run(constant_a)) # 运行图,并获取constant_a的执行结果

注入机制: 用户通过placeholder定义占位符并构建完整Graph后,利用Session实例.run将训练/测试数据注入到图中,驱动任务的实际运行

import tensorflow as tf # 引入tensorflow相关包
placeholder_a = tf.placeholder(tf.float32) # 定义placeholder实例
placeholder_b = tf.placeholder(tf.float32)
add_result = tf.add(placeholder_a, placeholder_b) # OP使两值相加
multiply_result = tf.multiply(placeholder_a, placeholder_b) # OP使两值相加
with tf.Session() as session:
  # 运行图,获取执行结果
  print(session.run(add_result, feed_dict = {placeholder_a: 1.0, placeholder_b: 2.0})) # 获取单个值
  print(session.run([add_result, multiply_result], feed_dict = {placeholder_a: 3.0, placeholder_b: 4.0})) # 获取多个值

session的参数有:target(指定硬件设备),graph(适用于多计算图的场景,指定运行的计算图),config(定义session运行配置,用tf.ComfigProto配置)

sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True))

###首先,填的一些小坑:导入包和版本代码修改

pip3 install tensorflow-gpu==1.15
pip install opencv-python==4.1.1.26
pip install scipy==1.2.1

修改了:xrange 为 range

FusionGAN
FusionGAN 的代码,一种用于红外和可见光图像融合的 GAN 模型
请参阅我们的以下论文以了解算法详细信息:
gpu1.15
pip install opencv-python
4.1.1.26
pip install scipy==1.2.1

修改了:xrange 为 range


  
参考论文,以了解算法详细信息:
马嘉仪、魏宇、彭伟梁、常丽和江俊军,《“FusionGAN:用于红外和可见光图像融合的生成对抗网络”》,信息融合,48,第 11-26 页,2019 年 8 月。

标签:loss,起来,None,ir,batch,02FusionGAN,train,tf
来源: https://blog.csdn.net/weixin_42433809/article/details/119454725

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有