├── SRM_Kernels.npy ├── README.md ├── main.py ├── queues.py ├── YeNet.py ├── generator.py ├── utils.py └── layers.py /SRM_Kernels.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Caenorst/YeNet-Tensorflow/HEAD/SRM_Kernels.npy -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YeNet-Tensorflow 2 | Tensorflow implementation of "Deep Learning Hierarchical Representation for Image Steganalysis" by Jian Ye, Jiangqun Ni and Yang Yi 3 | 4 | ## Dataset 5 | Training and validation contains there own cover and stego / beta maps images directory, stego images must have the same name than there corresponding cover 6 | 7 | ## Publication 8 | [The publication can be found here](http://ieeexplore.ieee.org/document/7937836/) 9 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import numpy as np 3 | import tensorflow as tf 4 | from functools import partial 5 | 6 | from utils import * 7 | from generator import * 8 | from queues import * 9 | from YeNet import YeNet 10 | 11 | parser = argparse.ArgumentParser(description='PyTorch implementation of YeNet') 12 | parser.add_argument('train_cover_dir', type=str, metavar='PATH', 13 | help='path of directory containing all ' + 14 | 'training cover images') 15 | parser.add_argument('train_stego_dir', type=str, metavar='PATH', 16 | help='path of directory containing all ' + 17 | 'training stego images or beta maps') 18 | parser.add_argument('valid_cover_dir', type=str, metavar='PATH', 19 | help='path of directory containing all ' + 20 | 'validation cover images') 21 | parser.add_argument('valid_stego_dir', type=str, metavar='PATH', 22 | help='path of directory containing all ' + 23 | 'validation stego images or beta maps') 24 | parser.add_argument('--batch-size', type=int, default=32, metavar='N', 25 | help='input batch size for training (default: 32)') 26 | parser.add_argument('--test-batch-size', type=int, default=32, metavar='N', 27 | help='input batch size for testing (default: 32)') 28 | parser.add_argument('--epochs', type=int, default=1000, metavar='N', 29 | help='number of epochs to train (default: 1000)') 30 | parser.add_argument('--lr', type=float, default=4e-1, metavar='LR', 31 | help='learning rate (default: 4e-1)') 32 | parser.add_argument('--use-batch-norm', action='store_true', default=False, 33 | help='use batch normalization after each activation,' + 34 | ' also disable pair constraint (default: False)') 35 | parser.add_argument('--embed-otf', action='store_true', default=False, 36 | help='use beta maps and embed on the fly instead' + 37 | ' of use stego images (default: False)') 38 | parser.add_argument('--no-cuda', action='store_true', default=False, 39 | help='disables CUDA training') 40 | parser.add_argument('--gpu', type=int, default=0, 41 | help='index of gpu used (default: 0)') 42 | parser.add_argument('--seed', type=int, default=1, metavar='S', 43 | help='random seed (default: 1)') 44 | parser.add_argument('--log-interval', type=int, default=200, metavar='N', 45 | help='how many batches to wait ' + 46 | 'before logging training status') 47 | parser.add_argument('--log-path', type=str, default='logs/', 48 | metavar='PATH', help='path to generated log file') 49 | args = parser.parse_args() 50 | 51 | import os 52 | os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID' 53 | os.environ['CUDA_VISIBLE_DEVICES'] = '' if args.no_cuda else str(args.gpu) 54 | 55 | tf.set_random_seed(args.seed) 56 | train_ds_size = len(glob(args.train_cover_dir + '/*')) * 2 57 | if args.embed_otf: 58 | train_gen = partial(gen_embedding_otf, args.train_cover_dir, \ 59 | args.train_stego_dir, args.use_batch_norm) 60 | else: 61 | train_gen = partial(gen_flip_and_rot, args.train_cover_dir, \ 62 | args.train_stego_dir, args.use_batch_norm) 63 | 64 | valid_ds_size = len(glob(args.valid_cover_dir + '/*')) * 2 65 | valid_gen = partial(gen_valid, args.valid_cover_dir, \ 66 | args.valid_stego_dir) 67 | 68 | if valid_ds_size % 32 != 0: 69 | raise ValueError("change batch size for validation") 70 | 71 | optimizer = tf.train.AdadeltaOptimizer(args.lr) 72 | 73 | 74 | train(YeNet, train_gen, valid_gen, args.batch_size, \ 75 | args.test_batch_size, valid_ds_size, \ 76 | optimizer, args.log_interval, train_ds_size, \ 77 | args.epochs * train_ds_size, train_ds_size, args.log_path, 8) 78 | -------------------------------------------------------------------------------- /queues.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tensorflow as tf 3 | import threading 4 | import h5py 5 | import functools 6 | 7 | def hdf5baseGen(filepath, thread_idx, n_threads): 8 | with h5py.File(filepath, 'r') as f: 9 | keys = f.keys() 10 | nb_data = f[keys[0]].shape[0] 11 | idx = thread_idx 12 | while True: 13 | yield [np.expand_dims(f[key][idx], 0) for key in keys] 14 | idx = (idx + n_threads) % nb_data 15 | 16 | class GeneratorRunner(): 17 | """ 18 | This class manage a multithreaded queue filled with a generator 19 | """ 20 | def __init__(self, generator, capacity): 21 | """ 22 | inputs: generator feeding the data, must have thread_idx 23 | as parameter (but the parameter may be not used) 24 | """ 25 | self.generator = generator 26 | _input = generator(0,1).next() 27 | if type(_input) is not list: 28 | raise ValueError("generator doesn't return" \ 29 | "a list: %r" % type(_input)) 30 | input_batch_size = _input[0].shape[0] 31 | if not all(_input[i].shape[0] == input_batch_size for i in range(len(_input))): 32 | raise ValueError("all the inputs doesn't have " + \ 33 | "the same batch size," \ 34 | "the batch sizes are: %s" % [_input[i].shape[0] \ 35 | for i in range(len(_input))]) 36 | self.data = [] 37 | self.dtypes = [] 38 | self.shapes = [] 39 | for i in range(len(_input)): 40 | self.shapes.append(_input[i].shape[1:]) 41 | self.dtypes.append(_input[i].dtype) 42 | self.data.append(tf.placeholder(dtype=self.dtypes[i], \ 43 | shape=(input_batch_size,) + self.shapes[i])) 44 | self.queue = tf.FIFOQueue(capacity, shapes=self.shapes, \ 45 | dtypes=self.dtypes) 46 | self.enqueue_op = self.queue.enqueue_many(self.data) 47 | self.close_queue_op = self.queue.close(cancel_pending_enqueues=True) 48 | 49 | def get_batched_inputs(self, batch_size): 50 | """ 51 | Return tensors containing a batch of generated data 52 | """ 53 | batch = self.queue.dequeue_many(batch_size) 54 | return batch 55 | 56 | def thread_main(self, sess, thread_idx=0, n_threads=1): 57 | try: 58 | for data in self.generator(thread_idx, n_threads): 59 | sess.run(self.enqueue_op, feed_dict={i: d \ 60 | for i, d in zip(self.data, data)}) 61 | if self.stop_threads: 62 | return 63 | except RuntimeError: 64 | pass 65 | except tf.errors.CancelledError: 66 | pass 67 | 68 | def start_threads(self, sess, n_threads=1): 69 | self.stop_threads = False 70 | self.threads = [] 71 | for n in range(n_threads): 72 | t = threading.Thread(target=self.thread_main, args=(sess, n, n_threads)) 73 | t.daemon = True 74 | t.start() 75 | self.threads.append(t) 76 | return self.threads 77 | 78 | def stop_runner(self, sess): 79 | self.stop_threads = True 80 | # j = 0 81 | # while np.any([t.is_alive() for t in self.threads]): 82 | # j += 1 83 | # if j % 100 = 0: 84 | # print [t.is_alive() for t in self.threads] 85 | sess.run(self.close_queue_op) 86 | 87 | def queueSelection(runners, sel, batch_size): 88 | selection_queue = tf.FIFOQueue.from_list(sel, [r.queue for r in runners]) 89 | return selection_queue.dequeue_many(batch_size) 90 | 91 | def doubleQueue(runner1, runner2, is_runner1, batch_size1, batch_size2): 92 | return tf.cond(is_runner1, lambda: runner1.queue.dequeue_many(batch_size1), \ 93 | lambda: runner2.queue.dequeue_many(batch_size2)) 94 | 95 | if __name__ == '__main__': 96 | def randomGen(img_size, enqueue_batch_size, thread_idx, n_threads): 97 | while True: 98 | batch_of_1_channel_imgs = np.random.rand(enqueue_batch_size, \ 99 | img_size, img_size, 1) 100 | batch_of_labels = np.random.randint(0,11,enqueue_batch_size) 101 | return [batch_of_1_channel_imgs, batch_of_labels] 102 | 103 | TRAIN_BATCH_SIZE = 64 104 | VALID_BATCH_SIZE = 10 105 | train_runner = GeneratorRunner(functool.partial(randomGen, \ 106 | (128, 10)), TRAIN_BATCH_SIZE * 10) 107 | valid_runner = GeneratorRunner(functool.partial(randomGen, \ 108 | (128, 10)), VALID_BATCH_SIZE * 10) 109 | is_training = tf.Variable(True) 110 | batch_size = tf.Variable(TRAIN_BATCH_SIZE) 111 | enable_training_op = tf.group(tf.assign(is_training, True), \ 112 | tf.assign(batch_size, TRAIN_BATCH_SIZE)) 113 | disable_training_op = tf.group(tf.assign(is_training, False), \ 114 | tf.assign(batch_size, VALID_BATCH_SIZE)) 115 | img_batch, label_batch = queueSelection([valid_runner, train_runner], \ 116 | tf.cast(is_training, tf.int32), \ 117 | batch_size) 118 | # img_batch, label_batch = doubleQueue(train_runner, valid_runner, \ 119 | # is_training, TRAIN_BATCH_SIZE, \ 120 | # VALID_BATCH_SIZE) 121 | -------------------------------------------------------------------------------- /YeNet.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow.contrib import layers 3 | from tensorflow.contrib.framework import add_arg_scope, arg_scope, arg_scoped_arguments 4 | import layers as my_layers 5 | from utils import * 6 | 7 | SRM_Kernels = np.load('SRM_Kernels.npy') 8 | 9 | class YeNet(Model): 10 | def __init__(self, is_training=None, data_format='NCHW', \ 11 | with_bn=False, tlu_threshold=3): 12 | super(YeNet, self).__init__(is_training=is_training, \ 13 | data_format=data_format) 14 | self.with_bn = with_bn 15 | self.tlu_threshold = tlu_threshold 16 | 17 | def _build_model(self, inputs): 18 | self.inputs = inputs 19 | if self.data_format == 'NCHW': 20 | channel_axis = 1 21 | _inputs = tf.cast(tf.transpose(inputs, [0, 3, 1, 2]), tf.float32) 22 | else: 23 | channel_axis = 3 24 | _inputs = tf.cast(inputs, tf.float32) 25 | self.L = [] 26 | with arg_scope([layers.avg_pool2d], \ 27 | padding='VALID', data_format=self.data_format): 28 | with tf.variable_scope('SRM_preprocess'): 29 | W_SRM = tf.get_variable('W', initializer=SRM_Kernels, \ 30 | dtype=tf.float32, \ 31 | regularizer=None) 32 | b = tf.get_variable('b', shape=[30], dtype=tf.float32, \ 33 | initializer=tf.constant_initializer(0.)) 34 | self.L.append(tf.nn.bias_add( \ 35 | tf.nn.conv2d(_inputs, \ 36 | W_SRM, [1,1,1,1], 'VALID', \ 37 | data_format=self.data_format), b, \ 38 | data_format=self.data_format, name='Layer1')) 39 | self.L.append(tf.clip_by_value(self.L[-1], \ 40 | -self.tlu_threshold, self.tlu_threshold, \ 41 | name='TLU')) 42 | with tf.variable_scope('ConvNetwork'): 43 | with arg_scope([my_layers.conv2d], num_outputs=30, \ 44 | kernel_size=3, stride=1, padding='VALID', \ 45 | data_format=self.data_format, \ 46 | activation_fn=tf.nn.relu, \ 47 | weights_initializer=layers.xavier_initializer_conv2d(), \ 48 | weights_regularizer=layers.l2_regularizer(5e-4), \ 49 | biases_initializer=tf.constant_initializer(0.2), \ 50 | biases_regularizer=None), arg_scope([layers.batch_norm], \ 51 | decay=0.9, center=True, scale=True, \ 52 | updates_collections=None, is_training=self.is_training, \ 53 | fused=True, data_format=self.data_format): 54 | if self.with_bn: 55 | self.L.append(layers.batch_norm(self.L[-1], \ 56 | scope='Norm1')) 57 | self.L.append(my_layers.conv2d(self.L[-1], \ 58 | scope='Layer2')) 59 | if self.with_bn: 60 | self.L.append(layers.batch_norm(self.L[-1], \ 61 | scope='Norm2')) 62 | self.L.append(my_layers.conv2d(self.L[-1], \ 63 | scope='Layer3')) 64 | if self.with_bn: 65 | self.L.append(layers.batch_norm(self.L[-1], \ 66 | scope='Norm3')) 67 | self.L.append(my_layers.conv2d(self.L[-1], \ 68 | scope='Layer4')) 69 | if self.with_bn: 70 | self.L.append(layers.batch_norm(self.L[-1], \ 71 | scope='Norm4')) 72 | self.L.append(layers.avg_pool2d(self.L[-1], \ 73 | kernel_size=[2,2], scope='Stride1')) 74 | with arg_scope([my_layers.conv2d], kernel_size=5, \ 75 | num_outputs=32): 76 | self.L.append(my_layers.conv2d(self.L[-1], \ 77 | scope='Layer5')) 78 | if self.with_bn: 79 | self.L.append(layers.batch_norm(self.L[-1], \ 80 | scope='Norm5')) 81 | self.L.append(layers.avg_pool2d(self.L[-1], \ 82 | kernel_size=[3,3], \ 83 | scope='Stride2')) 84 | self.L.append(my_layers.conv2d(self.L[-1], \ 85 | scope='Layer6')) 86 | if self.with_bn: 87 | self.L.append(layers.batch_norm(self.L[-1], \ 88 | scope='Norm6')) 89 | self.L.append(layers.avg_pool2d(self.L[-1], \ 90 | kernel_size=[3,3], \ 91 | scope='Stride3')) 92 | self.L.append(my_layers.conv2d(self.L[-1], \ 93 | scope='Layer7')) 94 | if self.with_bn: 95 | self.L.append(layers.batch_norm(self.L[-1], \ 96 | scope='Norm7')) 97 | self.L.append(layers.avg_pool2d(self.L[-1], \ 98 | kernel_size=[3,3], \ 99 | scope='Stride4')) 100 | self.L.append(my_layers.conv2d(self.L[-1], \ 101 | num_outputs=16, \ 102 | scope='Layer8')) 103 | if self.with_bn: 104 | self.L.append(layers.batch_norm(self.L[-1], \ 105 | scope='Norm8')) 106 | self.L.append(my_layers.conv2d(self.L[-1], \ 107 | num_outputs=16, stride=3, \ 108 | scope='Layer9')) 109 | if self.with_bn: 110 | self.L.append(layers.batch_norm(self.L[-1], \ 111 | scope='Norm9')) 112 | self.L.append(layers.flatten(self.L[-1])) 113 | self.L.append(layers.fully_connected(self.L[-1], num_outputs=2, \ 114 | activation_fn=None, normalizer_fn=None, \ 115 | weights_initializer=tf.random_normal_initializer(mean=0., stddev=0.01), \ 116 | biases_initializer=tf.constant_initializer(0.), scope='ip')) 117 | self.outputs = self.L[-1] 118 | return self.outputs 119 | 120 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import misc, io 3 | from glob import glob 4 | import random 5 | from itertools import izip 6 | from random import random as rand 7 | from random import shuffle 8 | import h5py 9 | 10 | def gen_embedding_otf(cover_dir, beta_dir, shuf_pair, \ 11 | thread_idx=0, n_threads=1): 12 | cover_list = sorted(glob(cover_dir + '/*')) 13 | beta_list = sorted(glob(beta_dir + '/*')) 14 | nb_data = len(cover_list) 15 | assert len(beta_list) != 0, "the beta directory '%s' is empty" % beta_dir 16 | assert nb_data != 0, "the cover directory '%s' is empty" % cover_dir 17 | assert len(beta_list) == nb_data, "the cover directory and " + \ 18 | "the beta directory don't " + \ 19 | "have the same number of files " + \ 20 | "respectively %d and %d" % (nb_data, \ 21 | len(beta_list)) 22 | img = misc.imread(cover_list[0]) 23 | img_shape = img.shape 24 | batch = np.empty((2, img_shape[0], img_shape[1], 1), dtype='uint8') 25 | beta_map = np.empty(img_shape, dtype='