├── .gitignore ├── Image Forgery Detection.pdf ├── README.md ├── images └── comp30.jpg └── src ├── blocks.py ├── container.py ├── copy_move_cfa.py ├── copy_move_detection.py ├── detect.py ├── double_jpeg_compression.py ├── ela_extractor.py ├── image_object.py └── noise_variance.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /Image Forgery Detection.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewlevandoski/Image-Forgery-Detection/8010e1f21d0e07c38d3d73570ee582608bca1f53/Image Forgery Detection.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Forgery Detection Tool 2 | The forgery detection tool contained in this repository currently features forensic methods to detect the following: 3 | 4 | - Double JPEG compression 5 | - Copy-move forgeries 6 | - CFA artifacts 7 | - Noise variance inconsitencies 8 | 9 | Please read our paper for a detailed explanation of our motivation and research when developing this tool. 10 | 11 | ## To Run: 12 | Place any images that you wish to analyze into the **images** directory. 13 | 14 | Navigate to the **src** directory: 15 | ``` 16 | $ cd src 17 | ``` 18 | 19 | Next, run the **detect.py** script, providing the image you wish to evaluate: 20 | ``` 21 | $ python detect.py image.jpg 22 | ``` 23 | 24 | Once finished, details on the image will be reported in the terminal. Supplemental images generated during copy-move forgery detection can be found in the output directory. 25 | -------------------------------------------------------------------------------- /images/comp30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewlevandoski/Image-Forgery-Detection/8010e1f21d0e07c38d3d73570ee582608bca1f53/images/comp30.jpg -------------------------------------------------------------------------------- /src/blocks.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.decomposition import PCA 3 | 4 | class blocks(object): 5 | def __init__(self, grayscaleImageBlock, rgbImageBlock, x, y, blockDimension): 6 | self.imageGrayscale = grayscaleImageBlock # block of grayscale image 7 | self.imageGrayscalePixels = self.imageGrayscale.load() 8 | 9 | if rgbImageBlock is not None: 10 | self.imageRGB = rgbImageBlock 11 | self.imageRGBPixels = self.imageRGB.load() 12 | self.isImageRGB = True 13 | else: 14 | self.isImageRGB = False 15 | 16 | self.coor = (x, y) 17 | self.blockDimension = blockDimension 18 | 19 | def computeBlock(self): 20 | blockDataList = [] 21 | blockDataList.append(self.coor) 22 | blockDataList.append(self.computeCharaFeatures(4)) 23 | blockDataList.append(self.computePCA(6)) 24 | return blockDataList 25 | 26 | def computePCA(self, precision): 27 | PCAModule = PCA(n_components=1) 28 | if self.isImageRGB: 29 | imageArray = np.array(self.imageRGB) 30 | r = imageArray[:, :, 0] 31 | g = imageArray[:, :, 1] 32 | b = imageArray[:, :, 2] 33 | 34 | concatenatedArray = np.concatenate((r, np.concatenate((g, b), axis=0)), axis=0) 35 | PCAModule.fit_transform(concatenatedArray) 36 | principalComponents = PCAModule.components_ 37 | preciseResult = [round(element, precision) for element in list(principalComponents.flatten())] 38 | return preciseResult 39 | else: 40 | imageArray = np.array(self.imageGrayscale) 41 | PCAModule.fit_transform(imageArray) 42 | principalComponents = PCAModule.components_ 43 | preciseResult = [round(element, precision) for element in list(principalComponents.flatten())] 44 | return preciseResult 45 | 46 | def computeCharaFeatures(self, precision): 47 | characteristicFeaturesList = [] 48 | 49 | c4_part1 = 0 50 | c4_part2 = 0 51 | c5_part1 = 0 52 | c5_part2 = 0 53 | c6_part1 = 0 54 | c6_part2 = 0 55 | c7_part1 = 0 56 | c7_part2 = 0 57 | 58 | if self.isImageRGB: 59 | sumOfRedPixelValue = 0 60 | sumOfGreenPixelValue = 0 61 | sumOfBluePixelValue = 0 62 | for yCoordinate in range(0, self.blockDimension): # compute sum of the pixel value 63 | for xCoordinate in range(0, self.blockDimension): 64 | tmpR, tmpG, tmpB = self.imageRGBPixels[xCoordinate, yCoordinate] 65 | sumOfRedPixelValue += tmpR 66 | sumOfGreenPixelValue += tmpG 67 | sumOfBluePixelValue += tmpB 68 | 69 | sumOfPixels = self.blockDimension * self.blockDimension 70 | sumOfRedPixelValue = sumOfRedPixelValue / (sumOfPixels) # mean from each of the colorspaces 71 | sumOfGreenPixelValue = sumOfGreenPixelValue / (sumOfPixels) 72 | sumOfBluePixelValue = sumOfBluePixelValue / (sumOfPixels) 73 | 74 | characteristicFeaturesList.append(sumOfRedPixelValue) 75 | characteristicFeaturesList.append(sumOfGreenPixelValue) 76 | characteristicFeaturesList.append(sumOfBluePixelValue) 77 | 78 | else: 79 | characteristicFeaturesList.append(0) 80 | characteristicFeaturesList.append(0) 81 | characteristicFeaturesList.append(0) 82 | 83 | for yCoordinate in range(0, self.blockDimension): 84 | for xCoordinate in range(0, self.blockDimension): 85 | if yCoordinate <= self.blockDimension / 2: 86 | c4_part1 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 87 | else: 88 | c4_part2 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 89 | if xCoordinate <= self.blockDimension / 2: 90 | c5_part1 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 91 | else: 92 | c5_part2 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 93 | if xCoordinate - yCoordinate >= 0: 94 | c6_part1 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 95 | else: 96 | c6_part2 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 97 | if xCoordinate + yCoordinate <= self.blockDimension: 98 | c7_part1 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 99 | else: 100 | c7_part2 += self.imageGrayscalePixels[xCoordinate, yCoordinate] 101 | 102 | characteristicFeaturesList.append(float(c4_part1) / float(c4_part1 + c4_part2)) 103 | characteristicFeaturesList.append(float(c5_part1) / float(c5_part1 + c5_part2)) 104 | characteristicFeaturesList.append(float(c6_part1) / float(c6_part1 + c6_part2)) 105 | characteristicFeaturesList.append(float(c7_part1) / float(c7_part1 + c7_part2)) 106 | 107 | preciseResult = [round(element, precision) for element in characteristicFeaturesList] 108 | return preciseResult 109 | -------------------------------------------------------------------------------- /src/container.py: -------------------------------------------------------------------------------- 1 | class container(object): 2 | def __init__(self): 3 | self.container = [] 4 | return 5 | 6 | def getLength(self): 7 | return self.container.__len__() 8 | 9 | def addBlock(self, newData): 10 | self.container.append(newData) 11 | return 12 | 13 | def sortFeatures(self): 14 | self.container = sorted(self.container, key=lambda x:(x[1], x[2])) 15 | return 16 | 17 | def printAllContainer(self): 18 | for index in range(0, self.container.__len__()): 19 | print('\t',self.container[index]) 20 | return 21 | 22 | def printContainer(self, count): 23 | print("\tElement's index:", self.container.__len__()) 24 | if count > self.container.__len__(): 25 | self.printAllContainer() 26 | else: 27 | for index in range(0, count): 28 | print('\t', self.container[index]) 29 | return 30 | -------------------------------------------------------------------------------- /src/copy_move_cfa.py: -------------------------------------------------------------------------------- 1 | # Implementation derived from vasiliauskas.agnius@gmail.com 2 | 3 | import sys 4 | from PIL import Image, ImageFilter, ImageDraw 5 | import operator as op 6 | from optparse import OptionParser 7 | 8 | def Dist(p1,p2): 9 | x1, y1 = p1 10 | x2, y2 = p2 11 | return (((x1-x2)*(x1-x2)) + ((y1-y2)*(y1-y2)))**0.5 12 | 13 | def intersectarea(p1,p2,size): 14 | x1, y1 = p1 15 | x2, y2 = p2 16 | ix1, iy1 = max(x1,x2), max(y1,y2) 17 | ix2, iy2 = min(x1+size,x2+size), min(y1+size,y2+size) 18 | iarea = abs(ix2-ix1)*abs(iy2-iy1) 19 | if iy2 < iy1 or ix2 < ix1: iarea = 0 20 | return iarea 21 | 22 | def Hausdorff_distance(clust1, clust2, forward, dir): 23 | if forward == None: 24 | return max(Hausdorff_distance(clust1,clust2,True,dir),Hausdorff_distance(clust1,clust2,False,dir)) 25 | else: 26 | clstart, clend = (clust1,clust2) if forward else (clust2,clust1) 27 | dx, dy = dir if forward else (-dir[0],-dir[1]) 28 | return sum([min([Dist((p1[0]+dx,p1[1]+dy),p2) for p2 in clend]) for p1 in clstart])/len(clstart) 29 | 30 | def hassimilarcluster(ind, clusters, opt): 31 | item = op.itemgetter 32 | found = False 33 | tx = min(clusters[ind],key=item(0))[0] 34 | ty = min(clusters[ind],key=item(1))[1] 35 | for i, cl in enumerate(clusters): 36 | if i != ind: 37 | cx = min(cl,key=item(0))[0] 38 | cy = min(cl,key=item(1))[1] 39 | dx, dy = cx - tx, cy - ty 40 | specdist = Hausdorff_distance(clusters[ind],cl,None,(dx,dy)) 41 | if specdist <= int(opt.rgsim): 42 | found = True 43 | break 44 | return found 45 | 46 | def blockpoints(pix, coords, size): 47 | xs, ys = coords 48 | for x in range(xs,xs+size): 49 | for y in range(ys,ys+size): 50 | yield pix[x,y] 51 | 52 | def colortopalette(color, palette): 53 | for a,b in palette: 54 | if color >= a and color < b: 55 | return b 56 | 57 | def imagetopalette(image, palcolors): 58 | assert image.mode == 'L', "Only grayscale images supported !" 59 | pal = [(palcolors[i],palcolors[i+1]) for i in range(len(palcolors)-1)] 60 | image.putdata([colortopalette(c,pal) for c in list(image.getdata())]) 61 | 62 | def getparts(image, block_len, opt): 63 | img = image.convert('L') if image.mode != 'L' else image 64 | w, h = img.size 65 | parts = [] 66 | # Bluring image for abandoning image details and noise. 67 | for n in range(int(opt.imblev)): 68 | img = img.filter(ImageFilter.SMOOTH_MORE) 69 | # Converting image to custom palette 70 | imagetopalette(img, [x for x in range(256) if x%int(opt.impalred) == 0]) 71 | pix = img.load() 72 | 73 | for x in range(w-block_len): 74 | for y in range(h-block_len): 75 | data = list(blockpoints(pix, (x,y), block_len)) + [(x,y)] 76 | parts.append(data) 77 | parts = sorted(parts) 78 | return parts 79 | 80 | def similarparts(imagparts, opt): 81 | dupl = [] 82 | l = len(imagparts[0])-1 83 | 84 | for i in range(len(imagparts)-1): 85 | difs = sum(abs(x-y) for x,y in zip(imagparts[i][:l],imagparts[i+1][:l])) 86 | mean = float(sum(imagparts[i][:l])) / l 87 | dev = float(sum(abs(mean-val) for val in imagparts[i][:l])) / l 88 | if mean == 0: mean = .000000000001 89 | if dev/mean >= float(opt.blcoldev): 90 | if difs <= int(opt.blsim): 91 | if imagparts[i] not in dupl: 92 | dupl.append(imagparts[i]) 93 | if imagparts[i+1] not in dupl: 94 | dupl.append(imagparts[i+1]) 95 | 96 | return dupl 97 | 98 | def clusterparts(parts, block_len, opt): 99 | parts = sorted(parts, key=op.itemgetter(-1)) 100 | clusters = [[parts[0][-1]]] 101 | 102 | # assign all parts to clusters 103 | for i in range(1,len(parts)): 104 | x, y = parts[i][-1] 105 | 106 | # detect box already in cluster 107 | fc = [] 108 | for k,cl in enumerate(clusters): 109 | for xc,yc in cl: 110 | ar = intersectarea((xc,yc),(x,y),block_len) 111 | intrat = float(ar)/(block_len*block_len) 112 | if intrat > float(opt.blint): 113 | if not fc: clusters[k].append((x,y)) 114 | fc.append(k) 115 | break 116 | 117 | # if this is new cluster 118 | if not fc: 119 | clusters.append([(x,y)]) 120 | else: 121 | # re-clustering boxes if in several clusters at once 122 | while len(fc) > 1: 123 | clusters[fc[0]] += clusters[fc[-1]] 124 | del clusters[fc[-1]] 125 | del fc[-1] 126 | 127 | item = op.itemgetter 128 | # filter out small clusters 129 | clusters = [clust for clust in clusters if Dist((min(clust,key=item(0))[0],min(clust,key=item(1))[1]), (max(clust,key=item(0))[0],max(clust,key=item(1))[1]))/(block_len*1.4) >= float(opt.rgsize)] 130 | 131 | # filter out clusters, which doesn`t have identical twin cluster 132 | clusters = [clust for x,clust in enumerate(clusters) if hassimilarcluster(x, clusters, opt)] 133 | 134 | return clusters 135 | 136 | def marksimilar(image, clust, size, opt): 137 | blocks = [] 138 | if clust: 139 | draw = ImageDraw.Draw(image) 140 | mask = Image.new('RGB', (size,size), 'cyan') 141 | for cl in clust: 142 | for x,y in cl: 143 | im = image.crop((x,y,x+size,y+size)) 144 | im = Image.blend(im,mask,0.5) 145 | blocks.append((x,y,im)) 146 | for bl in blocks: 147 | x,y,im = bl 148 | image.paste(im,(x,y,x+size,y+size)) 149 | if int(opt.imauto): 150 | for cl in clust: 151 | cx1 = min([cx for cx,cy in cl]) 152 | cy1 = min([cy for cx,cy in cl]) 153 | cx2 = max([cx for cx,cy in cl]) + block_len 154 | cy2 = max([cy for cx,cy in cl]) + block_len 155 | draw.rectangle([cx1,cy1,cx2,cy2],outline="magenta") 156 | return image 157 | 158 | def detect(input, opt, args): 159 | block_len = 15 160 | im = Image.open(input) 161 | lparts = getparts(im, block_len, opt) 162 | dparts = similarparts(lparts, opt) 163 | cparts = clusterparts(dparts, block_len, opt) if int(opt.imauto) else [[elem[-1] for elem in dparts]] 164 | im = marksimilar(im, cparts, block_len, opt) 165 | out = input.split('.')[0] + '_analyzed.jpg' 166 | im.save(out) 167 | identical_regions = len(cparts) if int(opt.imauto) else 0 168 | print('\tCopy-move output is saved in file -', out) 169 | return(identical_regions) 170 | -------------------------------------------------------------------------------- /src/copy_move_detection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import image_object 4 | 5 | def detect_dir(sourceDirectory, outputDirectory, blockSize=32): 6 | timeStamp = time.strftime("%Y%m%d_%H%M%S") # get current timestamp 7 | os.makedirs(outputDirectory + timeStamp) # create a folder named as the current timestamp 8 | 9 | if not os.path.exists(sourceDirectory): 10 | print("\tError: Source Directory did not exist.") 11 | return 12 | elif not os.path.exists(outputDirectory): 13 | print("\tError: Output Directory did not exist.") 14 | return 15 | 16 | for fileName in os.listdir(sourceDirectory): 17 | anImage = image_object.image_object(sourceDirectory, fileName, blockSize, outputDirectory + timeStamp + '/') 18 | anImage.run() 19 | 20 | print("\tDone.") 21 | 22 | 23 | def detect(sourceDirectory, fileName, outputDirectory, blockSize=32): 24 | if not os.path.exists(sourceDirectory): 25 | print("\tError: Source Directory did not exist.") 26 | return 27 | elif not os.path.exists(sourceDirectory + fileName): 28 | print("\tError: Image file did not exist.") 29 | return 30 | elif not os.path.exists(outputDirectory): 31 | print("\tError: Output Directory did not exist.") 32 | return 33 | 34 | singleImage = image_object.image_object(sourceDirectory, fileName, blockSize, outputDirectory) 35 | imageResultPath = singleImage.run() 36 | 37 | return imageResultPath 38 | -------------------------------------------------------------------------------- /src/detect.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import sys 3 | 4 | import double_jpeg_compression 5 | import copy_move_cfa 6 | import copy_move_detection 7 | import noise_variance 8 | 9 | from optparse import OptionParser 10 | 11 | if __name__ == '__main__': 12 | # copy-move parameters 13 | cmd = OptionParser("usage: %prog image_file [options]") 14 | cmd.add_option('', '--imauto', help='Automatically search identical regions. (default: %default)', default=1) 15 | cmd.add_option('', '--imblev',help='Blur level for degrading image details. (default: %default)', default=8) 16 | cmd.add_option('', '--impalred',help='Image palette reduction factor. (default: %default)', default=15) 17 | cmd.add_option('', '--rgsim', help='Region similarity threshold. (default: %default)', default=5) 18 | cmd.add_option('', '--rgsize',help='Region size threshold. (default: %default)', default=1.5) 19 | cmd.add_option('', '--blsim', help='Block similarity threshold. (default: %default)',default=200) 20 | cmd.add_option('', '--blcoldev', help='Block color deviation threshold. (default: %default)', default=0.2) 21 | cmd.add_option('', '--blint', help='Block intersection threshold. (default: %default)', default=0.2) 22 | opt, args = cmd.parse_args() 23 | if not args: 24 | cmd.print_help() 25 | sys.exit() 26 | im_str = args[0] 27 | 28 | print('\nRunning double jpeg compression detection...') 29 | double_compressed = double_jpeg_compression.detect('..//images//' + im_str) 30 | 31 | if(double_compressed): print('\nDouble compression detected') 32 | else: print('\nSingle compressed') 33 | 34 | print('\nRunning CFA artifact detection...\n') 35 | identical_regions_cfa = copy_move_cfa.detect('..//images//' + im_str, opt, args) 36 | print('\n' + identical_regions_cfa, 'CFA artifacts detected') 37 | 38 | print('\nRunning noise variance inconsistency detection...') 39 | noise_forgery = noise_variance.detect('..//images//' + im_str) 40 | 41 | if(noise_forgery): print('\nNoise variance inconsistency detected') 42 | else: print('\nNo noise variance inconsistency detected') 43 | 44 | print('\nRunning copy-move detection...\n') 45 | copy_move_detection.detect('../images/', im_str, '../output/', blockSize=32) 46 | print(identical_regions_cfa, 'identical regions detected') 47 | 48 | if ((not double_compressed) and (identical_regions_cfa == 0) and (not noise_forgery)): 49 | print('\nNo forgeries were detected - this image has probably not been tampered with.') 50 | else: 51 | print('\nSome forgeries were detected - this image may have been tampered with.') 52 | -------------------------------------------------------------------------------- /src/double_jpeg_compression.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import cv2 4 | import argparse 5 | import csv 6 | import sys 7 | 8 | from scipy import fftpack as fftp 9 | from matplotlib import pyplot as plt 10 | 11 | def detect(input): 12 | firstq = 30 13 | secondq = 40 14 | thres = 0.5 15 | 16 | dct_rows = 0; 17 | dct_cols = 0; 18 | 19 | image = cv2.imread(input) 20 | shape = image.shape; 21 | 22 | if shape[0]%8 != 0: dct_rows = shape[0]+8-shape[0]%8 23 | else: dct_rows = shape[0] 24 | 25 | if shape[1]%8 != 0: dct_cols = shape[1]+8-shape[1]%8 26 | else: dct_cols = shape[1] 27 | 28 | dct_image = np.zeros((dct_rows,dct_cols,3),np.uint8) 29 | dct_image[0:shape[0], 0:shape[1]] = image 30 | 31 | y = cv2.cvtColor(dct_image, cv2.COLOR_BGR2YCR_CB)[:,:,0] 32 | 33 | w = y.shape[1] 34 | h = y.shape[0] 35 | n = w*h/64 36 | 37 | Y = y.reshape(h//8,8,-1,8).swapaxes(1,2).reshape(-1, 8, 8) 38 | 39 | qDCT =[] 40 | 41 | for i in range(0,Y.shape[0]): qDCT.append(cv2.dct(np.float32(Y[i]))) 42 | 43 | qDCT = np.asarray(qDCT, dtype=np.float32) 44 | qDCT = np.rint(qDCT - np.mean(qDCT, axis = 0)).astype(np.int32) 45 | f,a1 = plt.subplots(8,8) 46 | a1 = a1.ravel() 47 | 48 | k=0; 49 | flag = True 50 | for idx,ax in enumerate(a1): 51 | k+=1; 52 | data = qDCT[:,int(idx/8),int(idx%8)] 53 | val,key = np.histogram(data, bins=np.arange(data.min(), data.max()+1),normed = True) 54 | z = np.absolute(fftp.fft(val)) 55 | z = np.reshape(z,(len(z),1)) 56 | rotz = np.roll(z,int(len(z)/2)) 57 | 58 | slope = rotz[1:] - rotz[:-1] 59 | indices = [i+1 for i in range(len(slope)-1) if slope[i] > 0 and slope[i+1] < 0] 60 | 61 | peak_count = 0 62 | 63 | for j in indices: 64 | if rotz[j][0]>thres: peak_count+=1 65 | 66 | if(k==3): 67 | if peak_count>=20: return True 68 | else: return False 69 | flag = False 70 | -------------------------------------------------------------------------------- /src/ela_extractor.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import os 3 | import numpy as np 4 | import math 5 | 6 | def getImageDifference(image1, image2): 7 | height, width = image1.shape[:2] 8 | outputMap = np.zeros((3, width, height)) 9 | 10 | for i in range(0, width): 11 | for j in range (0, height): 12 | tmpColor1 = image1[i][j] 13 | tmpColor2 = image2[i][j] 14 | red_diff = int(tmpColor1[0]) - int(tmpColor2[0]) 15 | green_diff = int(tmpColor1[1]) - int(tmpColor2[1]) 16 | blue_diff = int(tmpColor1[2]) - int(tmpColor2[2]) 17 | outputMap[0][i][j] = float(red_diff * red_diff) 18 | outputMap[1][i][j] = float(green_diff * green_diff) 19 | outputMap[2][i][j] = float(blue_diff * blue_diff) 20 | 21 | return outputMap 22 | 23 | def getELA(filename): 24 | sc_width = 600 25 | sc_height = 600 26 | 27 | quality = 75; 28 | displayMultiplier = 20; 29 | 30 | origImage = cv2.imread(filename) 31 | outpath = "recompressed_" + filename 32 | 33 | cv2.imwrite(outpath, origImage, [int(cv2.IMWRITE_JPEG_QUALITY), quality]) 34 | recompressedImage = cv2.imread(outpath) 35 | imageDifference = getImageDifference(origImage, recompressedImage) 36 | 37 | elaMin = np.amax(imageDifference) 38 | elaMax = np.amin(imageDifference) 39 | intDifference = np.zeros(imageDifference.shape) 40 | 41 | for i in range(0, imageDifference.shape[0]): 42 | for j in range(0, imageDifference.shape[1]): 43 | for k in range(0, imageDifference.shape[2]): 44 | intDifference[i][j][k] = int(math.sqrt(imageDifference[i][j][k]) * displayMultiplier) 45 | if (intDifference[i][j][k] > 255): 46 | intDifference[i][j][k] = 255 47 | 48 | displaySurface_temp = intDifference 49 | 50 | ds_height = displaySurface_temp.shape[1] 51 | ds_width = displaySurface_temp.shape[2] 52 | 53 | if (ds_height > ds_width): 54 | if (ds_height > sc_height): 55 | sc_width = (sc_height * ds_width) / ds_height 56 | displaySurface = cv2.resize(displaySurface_temp, (sc_width, sc_height)) 57 | else: 58 | displaySurface = displaySurface_temp 59 | else: 60 | if (ds_width > sc_width): 61 | sc_height = (sc_width * ds_height) / ds_width 62 | displaySurface = cv2.resize(displaySurface_temp, (sc_width, sc_height)) 63 | else: 64 | displaySurface = displaySurface_temp 65 | 66 | cv2.imwrite("diff_" + filename, displaySurface) 67 | 68 | return origImage 69 | 70 | if __name__ == "__main__": 71 | getELA("comp30.jpg") 72 | -------------------------------------------------------------------------------- /src/image_object.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from math import pow 3 | from tqdm import tqdm, trange 4 | 5 | import scipy.misc 6 | import numpy as np 7 | import builtins 8 | import time 9 | 10 | import container 11 | import blocks 12 | 13 | class image_object(object): 14 | def __init__(self, imageDirectory, imageName, blockDimension, outputDirectory): 15 | print("\tStep 1 of 4: Object and variable initialization") 16 | 17 | # image parameter 18 | self.imageOutputDirectory = outputDirectory 19 | self.imagePath = imageName 20 | self.imageData = Image.open(imageDirectory + imageName) 21 | self.imageWidth, self.imageHeight = self.imageData.size # height = vertikal, width = horizontal 22 | 23 | if self.imageData.mode != 'L': # L means grayscale 24 | self.isThisRGBImage = True 25 | self.imageData = self.imageData.convert('RGB') 26 | RGBImagePixels = self.imageData.load() 27 | self.imageGrayscale = self.imageData.convert( 28 | 'L') # creates a grayscale version of current image to be used later 29 | GrayscaleImagePixels = self.imageGrayscale.load() 30 | 31 | for yCoordinate in range(0, self.imageHeight): 32 | for xCoordinate in range(0, self.imageWidth): 33 | redPixelValue, greenPixelValue, bluePixelValue = RGBImagePixels[xCoordinate, yCoordinate] 34 | GrayscaleImagePixels[xCoordinate, yCoordinate] = int(0.299 * redPixelValue) + int( 35 | 0.587 * greenPixelValue) + int(0.114 * bluePixelValue) 36 | else: 37 | self.isThisRGBImage = False 38 | self.imageData = self.imageData.convert('L') 39 | 40 | # algorithm's parameters from the first paper 41 | self.N = self.imageWidth * self.imageHeight 42 | self.blockDimension = blockDimension 43 | self.b = self.blockDimension * self.blockDimension 44 | self.Nb = (self.imageWidth - self.blockDimension + 1) * (self.imageHeight - self.blockDimension + 1) 45 | self.Nn = 2 # amount of neighboring block to be evaluated 46 | self.Nf = 188 # minimum treshold of the offset's frequency 47 | self.Nd = 50 # minimum treshold of the offset's magnitude 48 | 49 | # algorithm's parameters from the second paper 50 | self.P = (1.80, 1.80, 1.80, 0.0125, 0.0125, 0.0125, 0.0125) 51 | self.t1 = 2.80 52 | self.t2 = 0.02 53 | 54 | print('\t', self.Nb, self.isThisRGBImage) 55 | 56 | # container initialization to later contains several data 57 | self.featurescontainer = container.container() 58 | self.blockPaircontainer = container.container() 59 | self.offsetDictionary = {} 60 | 61 | def run(self): 62 | # time logging (optional, for evaluation purpose) 63 | self.compute() 64 | startTimestamp = time.time() 65 | timestampAfterComputing = time.time() 66 | self.sort() 67 | timestampAfterSorting = time.time() 68 | self.analyze() 69 | timestampAfterAnalyze = time.time() 70 | imageResultPath = self.reconstruct() 71 | timestampAfterImageCreation = time.time() 72 | 73 | print("\tComputing time :", timestampAfterComputing - startTimestamp, "seconds") 74 | print("\tSorting time :", timestampAfterSorting - timestampAfterComputing, "seconds") 75 | print("\tAnalyzing time :", timestampAfterAnalyze - timestampAfterSorting, "seconds") 76 | print("\tImage creation :", timestampAfterImageCreation - timestampAfterAnalyze, "seconds") 77 | 78 | totalRunningTimeInSecond = timestampAfterImageCreation - startTimestamp 79 | totalMinute, totalSecond = divmod(totalRunningTimeInSecond, 60) 80 | totalHour, totalMinute = divmod(totalMinute, 60) 81 | print("\tTotal time : %d:%02d:%02d second" % (totalHour, totalMinute, totalSecond), '\n') 82 | return imageResultPath 83 | 84 | def compute(self): 85 | print("\tStep 2 of 4: Computing characteristic features") 86 | 87 | imageWidthOverlap = self.imageWidth - self.blockDimension 88 | imageHeightOverlap = self.imageHeight - self.blockDimension 89 | 90 | if self.isThisRGBImage: 91 | for i in tqdm(range(0, imageWidthOverlap + 1, 1)): 92 | for j in range(0, imageHeightOverlap + 1, 1): 93 | imageBlockRGB = self.imageData.crop((i, j, i + self.blockDimension, j + self.blockDimension)) 94 | imageBlockGrayscale = self.imageGrayscale.crop( 95 | (i, j, i + self.blockDimension, j + self.blockDimension)) 96 | imageBlock = blocks.blocks(imageBlockGrayscale, imageBlockRGB, i, j, self.blockDimension) 97 | self.featurescontainer.addBlock(imageBlock.computeBlock()) 98 | else: 99 | for i in range(imageWidthOverlap + 1): 100 | for j in range(imageHeightOverlap + 1): 101 | imageBlockGrayscale = self.imageData.crop((i, j, i + self.blockDimension, j + self.blockDimension)) 102 | imageBlock = blocks.blocks(imageBlockGrayscale, None, i, j, self.blockDimension) 103 | self.featurescontainer.addBlock(imageBlock.computeBlock()) 104 | 105 | def sort(self): 106 | self.featurescontainer.sortFeatures() 107 | 108 | def analyze(self): 109 | print("\tStep 3 of 4:Pairing image blocks") 110 | z = 0 111 | time.sleep(0.1) 112 | featurecontainerLength = self.featurescontainer.getLength() 113 | for i in tqdm(range(featurecontainerLength)): 114 | for j in range(i + 1, featurecontainerLength): 115 | result = self.isValid(i, j) 116 | if result[0]: 117 | self.addDict(self.featurescontainer.container[i][0], self.featurescontainer.container[j][0], result[1]) 118 | z += 1 119 | else: 120 | break 121 | 122 | def isValid(self, firstBlock, secondBlock): 123 | if abs(firstBlock - secondBlock) < self.Nn: 124 | iFeature = self.featurescontainer.container[firstBlock][1] 125 | jFeature = self.featurescontainer.container[secondBlock][1] 126 | 127 | # check the validity of characteristic features according to the second paper 128 | if abs(iFeature[0] - jFeature[0]) < self.P[0]: 129 | if abs(iFeature[1] - jFeature[1]) < self.P[1]: 130 | if abs(iFeature[2] - jFeature[2]) < self.P[2]: 131 | if abs(iFeature[3] - jFeature[3]) < self.P[3]: 132 | if abs(iFeature[4] - jFeature[4]) < self.P[4]: 133 | if abs(iFeature[5] - jFeature[5]) < self.P[5]: 134 | if abs(iFeature[6] - jFeature[6]) < self.P[6]: 135 | if abs(iFeature[0] - jFeature[0]) + abs(iFeature[1] - jFeature[1]) + abs( 136 | iFeature[2] - jFeature[2]) < self.t1: 137 | if abs(iFeature[3] - jFeature[3]) + abs(iFeature[4] - jFeature[4]) + abs( 138 | iFeature[5] - jFeature[5]) + abs( 139 | iFeature[6] - jFeature[6]) < self.t2: 140 | 141 | # compute the pair's offset 142 | iCoordinate = self.featurescontainer.container[firstBlock][0] 143 | jCoordinate = self.featurescontainer.container[secondBlock][0] 144 | 145 | # Non Absolute Robust Detection Method 146 | offset = ( 147 | iCoordinate[0] - jCoordinate[0], iCoordinate[1] - jCoordinate[1]) 148 | 149 | # compute the pair's magnitude 150 | magnitude = np.sqrt(pow(offset[0], 2) + pow(offset[1], 2)) 151 | if magnitude >= self.Nd: 152 | return 1, offset 153 | return 0, 154 | 155 | def addDict(self, firstCoordinate, secondCoordinate, pairOffset): 156 | if pairOffset in self.offsetDictionary: 157 | self.offsetDictionary[pairOffset].append(firstCoordinate) 158 | self.offsetDictionary[pairOffset].append(secondCoordinate) 159 | else: 160 | self.offsetDictionary[pairOffset] = [firstCoordinate, secondCoordinate] 161 | 162 | def reconstruct(self): 163 | print("\tStep 4 of 4: Image reconstruction") 164 | 165 | # create an array as the canvas of the final image 166 | groundtruthImage = np.zeros((self.imageHeight, self.imageWidth)) 167 | linedImage = np.array(self.imageData.convert('RGB')) 168 | 169 | for key in sorted(self.offsetDictionary, key=lambda key: builtins.len(self.offsetDictionary[key]), 170 | reverse=True): 171 | if self.offsetDictionary[key].__len__() < self.Nf * 2: 172 | break 173 | print('\t', key, self.offsetDictionary[key].__len__()) 174 | for i in range(self.offsetDictionary[key].__len__()): 175 | # The original image (grayscale) 176 | for j in range(self.offsetDictionary[key][i][1], 177 | self.offsetDictionary[key][i][1] + self.blockDimension): 178 | for k in range(self.offsetDictionary[key][i][0], 179 | self.offsetDictionary[key][i][0] + self.blockDimension): 180 | groundtruthImage[j][k] = 255 181 | 182 | # creating a line edge from the original image (for the visual purpose) 183 | for xCoordinate in range(2, self.imageHeight - 2): 184 | for yCordinate in range(2, self.imageWidth - 2): 185 | if groundtruthImage[xCoordinate, yCordinate] == 255 and \ 186 | (groundtruthImage[xCoordinate + 1, yCordinate] == 0 or groundtruthImage[ 187 | xCoordinate - 1, yCordinate] == 0 or 188 | groundtruthImage[xCoordinate, yCordinate + 1] == 0 or groundtruthImage[ 189 | xCoordinate, yCordinate - 1] == 0 or 190 | groundtruthImage[xCoordinate - 1, yCordinate + 1] == 0 or groundtruthImage[ 191 | xCoordinate + 1, yCordinate + 1] == 0 or 192 | groundtruthImage[xCoordinate - 1, yCordinate - 1] == 0 or groundtruthImage[ 193 | xCoordinate + 1, yCordinate - 1] == 0): 194 | 195 | # creating the edge line, respectively left-upper, right-upper, left-down, right-down 196 | if groundtruthImage[xCoordinate - 1, yCordinate] == 0 and \ 197 | groundtruthImage[xCoordinate, yCordinate - 1] == 0 and \ 198 | groundtruthImage[xCoordinate - 1, yCordinate - 1] == 0: 199 | linedImage[xCoordinate - 2:xCoordinate, yCordinate, 1] = 255 200 | linedImage[xCoordinate, yCordinate - 2:yCordinate, 1] = 255 201 | linedImage[xCoordinate - 2:xCoordinate, yCordinate - 2:yCordinate, 1] = 255 202 | elif groundtruthImage[xCoordinate + 1, yCordinate] == 0 and \ 203 | groundtruthImage[xCoordinate, yCordinate - 1] == 0 and \ 204 | groundtruthImage[xCoordinate + 1, yCordinate - 1] == 0: 205 | linedImage[xCoordinate + 1:xCoordinate + 3, yCordinate, 1] = 255 206 | linedImage[xCoordinate, yCordinate - 2:yCordinate, 1] = 255 207 | linedImage[xCoordinate + 1:xCoordinate + 3, yCordinate - 2:yCordinate, 1] = 255 208 | elif groundtruthImage[xCoordinate - 1, yCordinate] == 0 and \ 209 | groundtruthImage[xCoordinate, yCordinate + 1] == 0 and \ 210 | groundtruthImage[xCoordinate - 1, yCordinate + 1] == 0: 211 | linedImage[xCoordinate - 2:xCoordinate, yCordinate, 1] = 255 212 | linedImage[xCoordinate, yCordinate + 1:yCordinate + 3, 1] = 255 213 | linedImage[xCoordinate - 2:xCoordinate, yCordinate + 1:yCordinate + 3, 1] = 255 214 | elif groundtruthImage[xCoordinate + 1, yCordinate] == 0 and \ 215 | groundtruthImage[xCoordinate, yCordinate + 1] == 0 and \ 216 | groundtruthImage[xCoordinate + 1, yCordinate + 1] == 0: 217 | linedImage[xCoordinate + 1:xCoordinate + 3, yCordinate, 1] = 255 218 | linedImage[xCoordinate, yCordinate + 1:yCordinate + 3, 1] = 255 219 | linedImage[xCoordinate + 1:xCoordinate + 3, yCordinate + 1:yCordinate + 3, 1] = 255 220 | 221 | # creating the straigh line, respectively upper, down, left, right line 222 | elif groundtruthImage[xCoordinate, yCordinate + 1] == 0: 223 | linedImage[xCoordinate, yCordinate + 1:yCordinate + 3, 1] = 255 224 | elif groundtruthImage[xCoordinate, yCordinate - 1] == 0: 225 | linedImage[xCoordinate, yCordinate - 2:yCordinate, 1] = 255 226 | elif groundtruthImage[xCoordinate - 1, yCordinate] == 0: 227 | linedImage[xCoordinate - 2:xCoordinate, yCordinate, 1] = 255 228 | elif groundtruthImage[xCoordinate + 1, yCordinate] == 0: 229 | linedImage[xCoordinate + 1:xCoordinate + 3, yCordinate, 1] = 255 230 | 231 | timeStamp = time.strftime("%Y%m%d_%H%M%S") 232 | scipy.misc.imsave(self.imageOutputDirectory + timeStamp + "_" + self.imagePath, groundtruthImage) 233 | scipy.misc.imsave(self.imageOutputDirectory + timeStamp + "_lined_" + self.imagePath, linedImage) 234 | 235 | return self.imageOutputDirectory + timeStamp + "_lined_" + self.imagePath 236 | -------------------------------------------------------------------------------- /src/noise_variance.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math 3 | import numpy as np 4 | 5 | from PIL import Image 6 | from scipy import signal 7 | from sklearn.cluster import KMeans 8 | 9 | def estimate_noise(I): 10 | H, W = I.shape 11 | 12 | M = [[1, -2, 1], [-2, 4, -2], [1, -2, 1]] 13 | 14 | sigma = np.sum(np.sum(np.absolute(signal.convolve2d(I, M)))) 15 | sigma = sigma * math.sqrt(0.5 * math.pi) / (6 * (W-2) * (H-2)) 16 | 17 | return sigma 18 | 19 | def detect(input, blockSize=32): 20 | im = Image.open(input) 21 | im = im.convert('1') 22 | 23 | blocks = [] 24 | 25 | imgwidth, imgheight = im.size 26 | 27 | # break up image into NxN blocks, N = blockSize 28 | for i in range(0,imgheight,blockSize): 29 | for j in range(0,imgwidth,blockSize): 30 | box = (j, i, j+blockSize, i+blockSize) 31 | b = im.crop(box) 32 | a = np.asarray(b).astype(int) 33 | blocks.append(a) 34 | 35 | variances = [] 36 | for block in blocks: 37 | variances.append([estimate_noise(block)]) 38 | 39 | kmeans = KMeans(n_clusters=2, random_state=0).fit(variances) 40 | center1, center2 = kmeans.cluster_centers_ 41 | 42 | if abs(center1 - center2) > .4: return True 43 | else: return False 44 | --------------------------------------------------------------------------------