├── .gitignore ├── Makefile ├── README.md ├── assets ├── bbv_avgdist.png ├── bbv_copymove.png ├── bbv_ela.png ├── bbv_hsv.png ├── bbv_lab_fast.png ├── bbv_lg.png └── eye.ico ├── build ├── .gitignore └── obj │ └── .gitignore ├── debugger.cpp ├── debugger.hpp ├── dev.cpp ├── functions.cpp ├── functions.hpp ├── install_scripts ├── centos.sh └── ubuntu.sh ├── phoenix.cpp ├── resources.rc ├── speedtests.cpp └── structs.h /.gitignore: -------------------------------------------------------------------------------- 1 | i/ 2 | codehelp/ 3 | *.sublime-project 4 | *.sublime-workspace 5 | dist/ -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # COMPILER CONFIG 3 | # 4 | #compiler details 5 | CXX = g++ 6 | CXXFLAGS = -g -std=c++0x -O3 7 | 8 | #project structure 9 | OBJ_DIR = build/obj 10 | BIN_DIR = build 11 | 12 | #source files and corresponding objects 13 | SOURCES = debugger.cpp functions.cpp phoenix.cpp 14 | OBJECTS = $(SOURCES:%.cpp=$(OBJ_DIR)/%.o) 15 | 16 | #header file locations 17 | OCV_INC = C:\opencv_2_4_6\build\include 18 | BOOST_INC = C:\boost_1_54_0 19 | INC_PATHS = -isystem$(OCV_INC) -isystem$(BOOST_INC) 20 | 21 | # 22 | # LINKER CONFIG 23 | # 24 | #used libraries 25 | OCV_LIBS = -lopencv_highgui -lopencv_imgproc -lopencv_core 26 | BOOST_LIBS = -lboost_program_options -lboost_filesystem -lboost_system 27 | WIN_DEPS = -lzlib -llibjpeg -llibtiff -llibpng -lcomctl32 -lgdi32 28 | LINUX_DEPS = `pkg-config opencv --libs` 29 | 30 | #linker options 31 | LDLIBS = $(OCV_LIBS) $(BOOST_LIBS) 32 | LDFLAGS = 33 | 34 | # 35 | # WINDOWS 36 | # 37 | # comment out below for non-windows builds 38 | WIN = 1 39 | 40 | ifdef WIN 41 | #windows settings 42 | EXE_NAME = phoenix.exe 43 | LDLIBS += $(WIN_DEPS) 44 | LDFLAGS = -static 45 | 46 | all: $(OBJECTS) $(OBJ_DIR)/resources.o 47 | $(CXX) $(CXXFLAGS) $(OBJECTS) $(OBJ_DIR)/resources.o $(LDLIBS) $(LDFLAGS) -o $(BIN_DIR)/$(EXE_NAME) 48 | 49 | $(OBJ_DIR)/resources.o: resources.rc assets/eye.ico 50 | windres resources.rc $(OBJ_DIR)/resources.o 51 | else 52 | #linux settings 53 | EXE_NAME = phoenix 54 | LDLIBS += $(LINUX_DEPS) 55 | #set rpath on linux because of shared lib build 56 | # LDFLAGS = -Wl,-rpath,\$$ORIGIN/libs,-z,origin 57 | 58 | all: $(OBJECTS) 59 | $(CXX) $(CXXFLAGS) $(OBJECTS) $(LDLIBS) $(LDFLAGS) -o $(BIN_DIR)/$(EXE_NAME) 60 | endif 61 | 62 | $(OBJ_DIR)/%.o: %.cpp 63 | $(CXX) $(CXXFLAGS) $(INC_PATHS) -c $< -o $@ 64 | 65 | dev: $(OBJ_DIR)/debugger.o 66 | $(CXX) $(CXXFLAGS) $(INC_PATHS) -c dev.cpp -o $(OBJ_DIR)/dev.o 67 | $(CXX) $(CXXFLAGS) $(OBJ_DIR)/dev.o $(OBJ_DIR)/debugger.o $(LDLIBS) $(LDFLAGS) -o $(BIN_DIR)/dev.exe 68 | 69 | .PHONY: clean 70 | clean: 71 | rm -f build/*.* 72 | rm -f build/obj/*.* 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phoenix - Image Forensics 2 | 3 | phoenix is a small image forensics tool that can run some common analyses on images. It was inspired by the image analyses at the [Hackerfactor Blog](http://www.hackerfactor.com/blog/), and implements some of the algorithms used there. It is named phoenix because it has resurrected many times before taking its current form in C++. 4 | 5 | Features: 6 | * Error Level Analysis 7 | * Luminance Gradient 8 | * Average Distance 9 | * HSV and Lab colorspace histograms 10 | * JPEG resave quality estimate ([ImageMagick](http://www.imagemagick.org/script/index.php)-style and [Hackerfactor jpegquality](http://www.hackerfactor.com/src/jpegquality.c) estimates) 11 | * Extract JPEG Quantization Tables 12 | * Copy-Move (Clone Stamp) Detection 13 | 14 | ## Usage 15 | * `-h | -help` display help text. 16 | * `-f | -file ` Required, the path to the source image. 17 | * `-o | -output [path=./]` Save results in files (as PNG) 18 | * `-d | -display` Display results 19 | * `-ela [quality=70]` Error Level Analysis 20 | * `-lg` Luminance Gradient 21 | * `-avgdist` Average Distance 22 | * `-hsv [whitebg=0]` HSV Colorspace Histogram 23 | * `-lab [whitebg=0]` Lab Colorspace Histogram 24 | * `-labfast [whitebg=0]` Lab Colorspace Histogram, faster but less accurate version (256x256 instead of 1024x1024 output) 25 | * `-copymove [retain=4] [qcoeff=1.0]` Copy-Move Detection 26 | * `-a | -autolevels` Flag to enable histogram equalization (auto-levels) on output images 27 | 28 | ## Compiling 29 | phoenix depends on OpenCV (2.4.9) and Boost (1.55.0) Libraries. Exact versions are probably not required. Try `make` to compile. The defaults should work if you didn't do anything fancy while compiling OpenCV or Boost, i.e. change default install path. You can use the shell scripts in `install_scripts` to compile Boost, OpenCV and then phoenix. The scripts are intended for provisioning Vagrant machines, but you can also use it to automatically compile phoenix. Don't clone the repository if you will use the scripts, it will do it for you. 30 | 31 | ## Outputs 32 | Here are some examples of phoenix output with the image used in the legendary [Body By Victoria](http://www.hackerfactor.com/blog/?/archives/322-Body-By-Victoria.html) analysis by Neal Krawetz. 33 | 34 | ### ELA (Error Level Analysis) 35 | ``` 36 | ./phoenix -f bbv.jpg -o -d -ela 37 | ``` 38 | ![Error Level Analysis](assets/bbv_ela.png) 39 | 40 | ### LG (Luminance Gradient) 41 | ``` 42 | ./phoenix -f bbv.jpg -o -d -lg 43 | ``` 44 | ![Luminance Gradient](assets/bbv_lg.png) 45 | 46 | ### AVGDIST (Average-Distance of Neighbor Pixels) 47 | ``` 48 | ./phoenix -f bbv.jpg -o -d -avgdist 49 | ``` 50 | ![Average Distance](assets/bbv_avgdist.png) 51 | 52 | ### Copy-Move Detection 53 | ``` 54 | ./phoenix -f bbv.jpg -o -d -copymove 4 5 55 | ``` 56 | ![Copy-Move Detection](assets/bbv_copymove.png) 57 | 58 | ### HSV Colorspace Histogram 59 | ``` 60 | ./phoenix -f bbv.jpg -o -d -hsv 61 | ``` 62 | ![HSV Histogram](assets/bbv_hsv.png) 63 | 64 | ### Lab Colorspace Histogram 65 | ``` 66 | ./phoenix -f bbv.jpg -o -d -labfast 67 | ``` 68 | ![Lab Histogram](assets/bbv_lab_fast.png) 69 | 70 | ## Resources 71 | Some resources I used while developing many of the algorithms here. 72 | 73 | * [Hackerfactor Blog](http://www.hackerfactor.com/blog/) 74 | * [Quality Time with Your JPEGs](http://blog.apokalyptik.com/2009/09/16/quality-time-with-your-jpegs/) 75 | * [Exploring JPEG](https://www.imperialviolet.org/binary/jpeg/) 76 | * [ELA From Scratch](https://infohost.nmt.edu/~schlake/ela/) 77 | * [elsamuko Image Forensics](https://sites.google.com/site/elsamuko/forensics) -------------------------------------------------------------------------------- /assets/bbv_avgdist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_avgdist.png -------------------------------------------------------------------------------- /assets/bbv_copymove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_copymove.png -------------------------------------------------------------------------------- /assets/bbv_ela.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_ela.png -------------------------------------------------------------------------------- /assets/bbv_hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_hsv.png -------------------------------------------------------------------------------- /assets/bbv_lab_fast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_lab_fast.png -------------------------------------------------------------------------------- /assets/bbv_lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/bbv_lg.png -------------------------------------------------------------------------------- /assets/eye.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ebemunk/phoenix/de7b5839369b54b862ed71870b15f2d7c9a34015/assets/eye.ico -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | * 3 | -------------------------------------------------------------------------------- /build/obj/.gitignore: -------------------------------------------------------------------------------- 1 | *.o -------------------------------------------------------------------------------- /debugger.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "debugger.hpp" 7 | 8 | using namespace std; 9 | 10 | //set to inactive on creation 11 | bool debugger::active = false; 12 | 13 | //lazy static singleton 14 | debugger& debugger::instance() { 15 | static debugger instance; 16 | 17 | return instance; 18 | } 19 | 20 | //start timing & print 21 | void debugger::start(string msg) { 22 | if(!active) return; 23 | 24 | t_start = chrono::high_resolution_clock::now(); 25 | print(msg + " starting"); 26 | } 27 | 28 | //end timing & print 29 | void debugger::end(string msg) { 30 | if(!active) return; 31 | 32 | t_end = chrono::high_resolution_clock::now(); 33 | 34 | int secs = chrono::duration_cast(t_end - t_start).count(); 35 | int millisecs = chrono::duration_cast(t_end - t_start).count(); 36 | 37 | stringstream ss; 38 | ss << msg << " took: " << secs << ":" << millisecs; 39 | string str = ss.str(); 40 | 41 | print(str); 42 | } 43 | 44 | //simple cout wrapper (for now) 45 | void debugger::print(string msg) { 46 | if(!active) return; 47 | 48 | cout << "DEBUG: " << msg << endl; 49 | } -------------------------------------------------------------------------------- /debugger.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace std; 5 | 6 | /* 7 | Singleton debugger for timing stuff and debug messages 8 | */ 9 | class debugger { 10 | private: 11 | chrono::high_resolution_clock::time_point t_start, t_end; 12 | 13 | debugger() {} 14 | 15 | debugger(debugger const&); 16 | void operator=(debugger const&); 17 | 18 | public: 19 | static debugger& instance(); 20 | static bool active; 21 | 22 | void start(string msg); 23 | void end(string msg); 24 | void print(string msg); 25 | }; -------------------------------------------------------------------------------- /dev.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "debugger.hpp" 9 | #include "functions.hpp" 10 | 11 | using namespace std; 12 | using namespace cv; 13 | 14 | int main(int argc, char *argv[]) { 15 | debugger &d = debugger::instance(); 16 | // d.active = true; 17 | 18 | d.start("test"); 19 | d.end("test"); 20 | d.print("hello"); 21 | } -------------------------------------------------------------------------------- /functions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "structs.h" 13 | 14 | using namespace std; 15 | using namespace cv; 16 | using boost::property_tree::ptree; 17 | 18 | /* 19 | HSV Histogram Stretch (Auto-Levels) 20 | converts the image to HSV colorspace and then applies histogram equalization 21 | to the V channel, and converts back to RGB. This is used to make copies that 22 | are better viewable 23 | */ 24 | void hsv_histogram_stretch(Mat &src, Mat &dst) { 25 | //convert to float & HSV colorspace 26 | src.convertTo(dst, CV_32F, 1.0/255.0); 27 | cvtColor(dst, dst, CV_BGR2HSV); 28 | 29 | vector ch; 30 | split(dst, ch); 31 | //convert V channel to 8-bit 32 | ch[2].convertTo(ch[2], CV_8U, 255); 33 | //equalize histogram 34 | equalizeHist(ch[2], ch[2]); 35 | //back to float & normalize to [0,1] 36 | ch[2].convertTo(ch[2], CV_32F, 1.0/255.0); 37 | normalize(ch[2], ch[2], 0, 1, CV_MINMAX); 38 | merge(ch, dst); 39 | 40 | //back to 8-bit rgb 41 | cvtColor(dst, dst, CV_HSV2BGR); 42 | dst.convertTo(dst, CV_8U, 255); 43 | } 44 | 45 | /* 46 | HSV Histogram Analysis 47 | convert image to float and change colorspace to HSV. Count all H,S pairs and 48 | compute frequency + the total value for V for each pair. Divide the total value 49 | by the frequency and create the histogram image 50 | 51 | implementation adapted from Samuel Albrecht's GIMP plugin 52 | https://sites.google.com/site/elsamuko/forensics/hsv-analysis 53 | */ 54 | void hsv_histogram(Mat &src, Mat &dst, bool whitebg = false) { 55 | Vec3f bgcolor = Vec3f(0,0,0); 56 | if(whitebg) { 57 | bgcolor = Vec3f(0,0,1); 58 | } 59 | Mat hsv; 60 | src.convertTo(hsv, CV_32F, 1.0/255.0); 61 | cvtColor(hsv, hsv, CV_BGR2HSV); 62 | //H: (0, 360) S: (0, 1) V: (0, 1) 63 | 64 | //count and calculate average V for each (H,S) 65 | int hbins = 360, sbins = 256; 66 | Mat hist = Mat::zeros(sbins, hbins, CV_32F); 67 | Mat sums = Mat::zeros(sbins, hbins, CV_32F); 68 | for(int i=0; i(i,j); 71 | int H = round(pixel[0]), S = round(pixel[1]*255); 72 | hist.at(S, H)++; 73 | sums.at(S, H) += pixel[2]; 74 | } 75 | } 76 | 77 | divide(sums, hist, hist); 78 | 79 | //draw histogram 80 | Mat hsv_histogram = Mat::zeros(sbins, hbins, CV_32FC3); 81 | for(int s=0; s(s,h); 84 | if(avg > 0) { 85 | hsv_histogram.at(s, h) = Vec3f(h, s/255.0, avg); 86 | } else { 87 | hsv_histogram.at(s, h) = bgcolor; 88 | } 89 | } 90 | } 91 | 92 | //back to 8-bit rgb 93 | cvtColor(hsv_histogram, hsv_histogram, CV_HSV2BGR); 94 | hsv_histogram.convertTo(dst, CV_8U, 255); 95 | } 96 | 97 | /* 98 | Lab Histogram Analysis 99 | convert image to float and change colorspace to Lab. Count all a,b pairs and 100 | compute the frequency + total value for L for each pair. Divide & display the 101 | resulting histogram image 102 | 103 | implementation adapted from Samuel Albrecht's GIMP plugin 104 | https://sites.google.com/site/elsamuko/forensics/lab-analysis 105 | */ 106 | void lab_histogram(Mat &src, Mat &dst, bool whitebg = false) { 107 | Vec3f bgcolor = Vec3f(0,0,0); 108 | if(whitebg) { 109 | bgcolor = Vec3f(100,0,0); 110 | } 111 | //convert to float and scale to [0,1] 112 | Mat lab; 113 | src.convertTo(lab, CV_32F, 1.0/255.0); 114 | cvtColor(lab, lab, CV_BGR2Lab); 115 | //L: (0, 100) a: (-127, 127) b: (-127, 127) 116 | 117 | int abins = 1024, bbins = 1024; 118 | //count frequencies and also sum L values 119 | Mat hist = Mat::zeros(abins, bbins, CV_32F); 120 | Mat sums = Mat::zeros(abins, bbins, CV_32F); 121 | for(int i=0; i(i,j); 124 | int A = round(4*(pixel[1]+128)), B = round(4*(pixel[2]+128)); 125 | hist.at(A, B)++; 126 | sums.at(A, B) += pixel[0]; 127 | } 128 | } 129 | 130 | //get average L value for each bin 131 | divide(sums, hist, hist); 132 | 133 | //construct histogram image 134 | int sub = 512; 135 | Mat lab_histogram = Mat::zeros(abins, bbins, CV_32FC3); 136 | for(int a=0; a(a,b); 139 | if(avg>0) { 140 | lab_histogram.at(b, a) = Vec3f(avg, (a-sub), (b-sub)); 141 | } else { 142 | lab_histogram.at(b, a) = bgcolor; 143 | } 144 | } 145 | } 146 | 147 | //back to 8-bit rgb 148 | cvtColor(lab_histogram, lab_histogram, CV_Lab2BGR); 149 | lab_histogram.convertTo(dst, CV_8U, 255); 150 | } 151 | 152 | /* 153 | Fast version of Lab Histogram, converting to Lab from CV_8U rather than 154 | CV_32F saves a ton of time, but its less accurate. 155 | */ 156 | void lab_histogram_fast(Mat &src, Mat &dst, bool whitebg = false) { 157 | Vec3f bgcolor = Vec3f(0,0,0); 158 | if(whitebg) { 159 | bgcolor = Vec3f(100,0,0); 160 | } 161 | //convert to float and scale to [0,1] 162 | Mat lab; 163 | // src.convertTo(lab, CV_32F, 1.0/255.0); 164 | cvtColor(src, lab, CV_BGR2Lab); 165 | 166 | lab.convertTo(lab, CV_32F); 167 | vector chn; 168 | split(lab, chn); 169 | chn[0] = (chn[0] / 255.0) * 100.0; 170 | chn[1] = chn[1] - 128; 171 | chn[2] = chn[2] - 128; 172 | merge(chn, lab); 173 | //L: (0, 100) a: (-127, 127) b: (-127, 127) 174 | 175 | int abins = 256, bbins = 256; 176 | //count frequencies and also sum L values 177 | Mat hist = Mat::zeros(abins, bbins, CV_32F); 178 | Mat sums = Mat::zeros(abins, bbins, CV_32F); 179 | for(int i=0; i(i,j); 182 | int A = round(1*(pixel[1]+128)), B = round(1*(pixel[2]+128)); 183 | hist.at(A, B)++; 184 | sums.at(A, B) += pixel[0]; 185 | } 186 | } 187 | 188 | //get average L value for each bin 189 | divide(sums, hist, hist); 190 | 191 | //construct histogram image 192 | int sub = 128; 193 | Mat lab_histogram = Mat::zeros(abins, bbins, CV_32FC3); 194 | for(int a=0; a(a,b); 197 | if(avg>0) { 198 | lab_histogram.at(b, a) = Vec3f(avg, (a-sub), (b-sub)); 199 | } else { 200 | lab_histogram.at(b, a) = bgcolor; 201 | } 202 | } 203 | } 204 | 205 | //back to 8-bit rgb 206 | cvtColor(lab_histogram, lab_histogram, CV_Lab2BGR); 207 | lab_histogram.convertTo(dst, CV_8U, 255); 208 | } 209 | 210 | /* 211 | Error Level Analysis 212 | encode a jpeg with a known quality (default 90) and then subtract this image 213 | from the original jpeg. Normalize the resulting image for better viewing 214 | 215 | implemented from Neal Krawetz's algorithm description 216 | http://hackerfactor.com/papers/bh-usa-07-krawetz-wp.pdf 217 | pages 16-20 218 | */ 219 | void error_level_analysis(Mat &src, Mat &dst, int quality = 90) { 220 | vector buffer; 221 | 222 | vector save_params(2); 223 | save_params.push_back(CV_IMWRITE_JPEG_QUALITY); 224 | save_params.push_back(quality); 225 | //encode as jpeg 226 | imencode(".jpg", src, buffer, save_params); 227 | 228 | Mat resaved = imdecode(buffer, CV_LOAD_IMAGE_COLOR); 229 | //normalize the difference for better viewing 230 | normalize(abs(src - resaved), dst, 0, 255, CV_MINMAX); 231 | } 232 | 233 | /* 234 | Luminance Gradient 235 | get image derivatives in X and Y directions using a Sobel filter. afterwards, 236 | colorize the image using the X and Y sobel components as angle in G and R channels 237 | and magnitude of the vectors as the B channel. 238 | 239 | implemented from Neal Krawetz's algorithm description 240 | http://blackhat.com/presentations/bh-dc-08/Krawetz/Presentation/bh-dc-08-krawetz.pdf 241 | pages 60-72 242 | */ 243 | void luminance_gradient(Mat &src, Mat &dst) { 244 | Mat greyscale; 245 | cvtColor(src, greyscale, CV_BGR2GRAY); 246 | 247 | //get sobel in x and y directions 248 | Size size = src.size(); 249 | Mat sobelX; 250 | Mat sobelY; 251 | 252 | Sobel(greyscale, sobelX, CV_32F, 1, 0); 253 | Sobel(greyscale, sobelY, CV_32F, 0, 1); 254 | 255 | dst = Mat::zeros(size, CV_32FC3); 256 | 257 | int rows = dst.rows; 258 | int cols = dst.cols; 259 | if(dst.isContinuous()) { 260 | cols = rows * cols; 261 | rows = 1; 262 | } 263 | 264 | for(int i=0; i(i); 266 | float *sx = sobelX.ptr(i); 267 | float *sy = sobelY.ptr(i); 268 | for(int j=0; j ch; 279 | split(dst, ch); 280 | normalize(ch[0], ch[0], 0, 1, CV_MINMAX); 281 | merge(ch, dst); 282 | 283 | dst.convertTo(dst, CV_8U, 255); 284 | } 285 | 286 | /* 287 | Turn all pixels into the average of the magnitude of its cross-shaped neighbors. 288 | 289 | implemented from https://infohost.nmt.edu/~schlake/ela/src/hfalg.c 290 | */ 291 | void average_distance(Mat &src, Mat &dst) { 292 | //average of cross-shaped neighbors filter 293 | Matx33f filter(0, 0.25, 0, 294 | 0.25, 0, 0.25, 295 | 0, 0.25, 0); 296 | 297 | src.convertTo(dst, CV_32F, 1.0/255.0); 298 | 299 | //apply filter 300 | Mat filtered; 301 | filter2D(dst, filtered, CV_32F, filter); 302 | normalize(abs(dst - filtered), dst, 0, 1, CV_MINMAX); 303 | dst.convertTo(dst, CV_8U, 255); 304 | } 305 | 306 | /* 307 | Extract given marker from jpeg file. 308 | */ 309 | int extract_jpeg_marker(const char* filename, char marker, vector &list) { 310 | //open file and get started 311 | ifstream in(filename, ios::binary); 312 | 313 | // first two bytes must be 0xffd8 for jpeg format 314 | char buffer[2]; 315 | in.read(buffer, 2); 316 | if(buffer[0] != (char)0xFF && buffer[1] != (char)0xD8) { 317 | //not jpeg 318 | return -2; 319 | } 320 | /*cout << "First Two: " << endl; 321 | cout << "\t" << hex << (unsigned short)buffer[0] << endl; 322 | cout << "\t" << hex << (unsigned short)buffer[1] << endl;*/ 323 | 324 | in.read(buffer, 2); 325 | if(buffer[0] != (char)0xFF) { 326 | //jpeg but corrupt? 327 | return -1; 328 | } 329 | /*cout << "Third: "; 330 | cout << "\t" << hex << (unsigned short)buffer[0] << endl;*/ 331 | 332 | /** 333 | * loop until: 334 | * - end of file 335 | * - end of image 0xd9 336 | * - hit image data (no headers after image data starts) 337 | */ 338 | bool compressed = false; 339 | while(buffer[1] != (char)0xD9 && !compressed && in.tellg() != -1) { 340 | /*cout << "Marker:\t" << hex << (unsigned short)buffer[1] << endl;*/ 341 | //check that segment marker is not a restart marker 342 | if(buffer[1] < (char)0xD0 || buffer[1] > (char)0xD7) { 343 | //next two bytes are the size of the segment 344 | char size[2]; 345 | in.read(size, 2); 346 | /*cout << "Size:\t" << hex << (unsigned short)size[0] << endl << "\t" << hex << (unsigned short)size[1] << endl;*/ 347 | //convert to short 348 | unsigned short size_s = size[0]; 349 | size_s <<= 8; 350 | size_s |= size[1] & 0x00FF; //this last bit mask was what was missing all along! why this way? 351 | 352 | /*cout << "+-+-+-+-+-+-+-+-+-+" << endl; 353 | cout << "Size: " << dec << size_s << endl;*/ 354 | 355 | //read segment 356 | //segment size includes the previous two size bytes (i think) 357 | char *segdata = new char[size_s-2]; 358 | in.read(segdata, size_s-2); 359 | 360 | //DQT marker 0xdb 361 | if(buffer[1] == marker) { 362 | list.push_back(segdata); 363 | } 364 | /*cout << "+-+-+-+-+-+-+-+-+-+" << endl;*/ 365 | } 366 | 367 | //if we see start of scan (SOS 0xda) that means its just image data from here on 368 | if(buffer[1] == (char)0xDA) { 369 | compressed = true; 370 | } else { 371 | //read the next two bytes, first one must be 0xff start of segment 372 | in.read(buffer, 2); 373 | if(buffer[0] != (char)0xFF) { //something wrong with this jpeg 374 | return -1; 375 | } 376 | } 377 | } //file reading complete 378 | 379 | return list.size(); 380 | } 381 | 382 | /* 383 | Estimate jpeg quality from extracted QTs (Quantization Tables) 384 | 385 | uses estimation method in Neal Krawetz's jpegquality tool 386 | http://www.hackerfactor.com/src/jpegquality.c 387 | 388 | also uses estimation tables from Imagemagick codebase 389 | http://trac.imagemagick.org/browser/ImageMagick/trunk/coders/jpeg.c 390 | */ 391 | int estimate_jpeg_quality(const char* filename, vector &qtables, vector &quality_estimates) { 392 | vector dqt_tables; 393 | 394 | int num_segments = extract_jpeg_marker(filename, 0xDB, dqt_tables); 395 | if(num_segments < 1) { 396 | return num_segments; 397 | } 398 | 399 | Mat zigzag8 = (Mat_(64, 1) << 0, 1, 5, 6, 14, 15, 27, 28, 2, 4, 7, 13, 16, 26, 29, 42, 3, 8, 12, 17, 25, 30, 41, 43, 9, 11, 18, 24, 31, 40, 44, 53, 10, 19, 23, 32, 39, 45, 52, 54, 20, 22, 33, 38, 46, 51, 55, 60, 21, 34, 37, 47, 50, 56, 59, 61, 35, 36, 48, 49, 57, 58, 62, 63); 400 | 401 | //loop over extracted files and prepare to estimate quality 402 | for(int k=0; k(i, j) = segdata[i*8+j]; //non-zigzag order 414 | dqt.at(i, j) = dqt_tables[k][zigzag8.at(i*8+j)+1]; 415 | } 416 | } 417 | CvScalar sum = cv::sum(dqt); 418 | //Hacker Factor quality estimate for table 419 | double hf_qval = 100 - ((sum.val[0] - dqt.at(0, 0)) / 63.0); 420 | //ImageMagick initial qval 421 | double im_qval; 422 | if(k==1) { 423 | im_qval = dqt_tables[k][2] + dqt_tables[k][53]; 424 | } else { 425 | im_qval = dqt_tables[k][0] + dqt_tables[k][63]; 426 | } 427 | 428 | //push it to vector 429 | qtable table = { 430 | index, precision, dqt, sum.val[0], hf_qval, im_qval 431 | }; 432 | qtables.push_back(table); 433 | } 434 | 435 | double hf_quality, imagick_quality; 436 | int num_qtables = 0; 437 | 438 | //jpeg with only 1 quantization table 439 | if(dqt_tables.size() == 1) { 440 | num_qtables = 1; 441 | //hackerfactor estimate 442 | hf_quality = qtables[0].hf_qval; 443 | 444 | //imagemagick estimation tables for single-dqt jpgs 445 | size_t 446 | hash[101] = { 447 | 510, 505, 422, 380, 355, 338, 326, 318, 311, 305, 448 | 300, 297, 293, 291, 288, 286, 284, 283, 281, 280, 449 | 279, 278, 277, 273, 262, 251, 243, 233, 225, 218, 450 | 211, 205, 198, 193, 186, 181, 177, 172, 168, 164, 451 | 158, 156, 152, 148, 145, 142, 139, 136, 133, 131, 452 | 129, 126, 123, 120, 118, 115, 113, 110, 107, 105, 453 | 102, 100, 97, 94, 92, 89, 87, 83, 81, 79, 454 | 76, 74, 70, 68, 66, 63, 61, 57, 55, 52, 455 | 50, 48, 44, 42, 39, 37, 34, 31, 29, 26, 456 | 24, 21, 18, 16, 13, 11, 8, 6, 3, 2, 457 | 0 458 | }, 459 | sums[101] = { 460 | 16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859, 461 | 12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679, 462 | 9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823, 463 | 6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086, 464 | 4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092, 465 | 3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396, 466 | 3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727, 467 | 2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068, 468 | 1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398, 469 | 1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736, 470 | 667, 592, 518, 441, 369, 292, 221, 151, 86, 471 | 64, 0 472 | }; 473 | 474 | //imagemagick estimate 475 | double sum = qtables[0].sum; 476 | double qvalue = qtables[0].im_qval; 477 | for (int i=0; i < 100; i++) { 478 | if ((qvalue < hash[i]) && (sum < sums[i])) { 479 | continue; 480 | } 481 | if (((qvalue <= hash[i]) && (sum <= sums[i])) || (i >= 50)) { 482 | imagick_quality = i + 1; 483 | } 484 | break; 485 | } 486 | } else { // 2 or 3 quantization tables 487 | if(dqt_tables.size() == 2) { //this means Cr and Cb tables are the same 488 | num_qtables = 2; 489 | qtables.push_back(qtables[1]); 490 | } else { 491 | num_qtables = 3; 492 | } 493 | 494 | //hackerfactor estimate 495 | double q0, q1, q2; 496 | q0 = qtables[0].hf_qval; 497 | q1 = qtables[1].hf_qval; 498 | q2 = qtables[2].hf_qval; 499 | double diff = (abs(q0 - q1) + abs(q0 - q2)) * 0.49; 500 | hf_quality = (q0 + q1 + q2) / 3.0 + diff; 501 | 502 | //imagemagick estimation tables for multi-dqt jpgs 503 | size_t 504 | hash[101] = { 505 | 1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645, 506 | 632, 623, 613, 607, 600, 594, 589, 585, 581, 571, 507 | 555, 542, 529, 514, 494, 474, 457, 439, 424, 410, 508 | 397, 386, 373, 364, 351, 341, 334, 324, 317, 309, 509 | 299, 294, 287, 279, 274, 267, 262, 257, 251, 247, 510 | 243, 237, 232, 227, 222, 217, 213, 207, 202, 198, 511 | 192, 188, 183, 177, 173, 168, 163, 157, 153, 148, 512 | 143, 139, 132, 128, 125, 119, 115, 108, 104, 99, 513 | 94, 90, 84, 79, 74, 70, 64, 59, 55, 49, 514 | 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, 515 | 0 516 | }, 517 | sums[101] = { 518 | 32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, 519 | 27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946, 520 | 23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998, 521 | 16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702, 522 | 12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208, 523 | 9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458, 524 | 8270, 8084, 7896, 7710, 7527, 7347, 7156, 6977, 6788, 525 | 6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128, 526 | 4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698, 3509, 527 | 3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846, 528 | 1666, 1483, 1297, 1109, 927, 735, 554, 375, 201, 529 | 128, 0 530 | }; 531 | 532 | //imagemagick estimate 533 | double sum = qtables[0].sum + qtables[1].sum; 534 | double qvalue = qtables[0].im_qval + qtables[1].im_qval; 535 | for (int i=0; i < 100; i++) { 536 | if ((qvalue < hash[i]) && (sum < sums[i])) { 537 | continue; 538 | } 539 | if (((qvalue <= hash[i]) && (sum <= sums[i])) || (i >= 50)) { 540 | imagick_quality = i + 1; 541 | } 542 | break; 543 | } 544 | } 545 | 546 | quality_estimates.push_back(imagick_quality); 547 | quality_estimates.push_back(hf_quality); 548 | 549 | return num_qtables; 550 | } 551 | 552 | /* 553 | Lexicographically sorts an index for DCT Copy-Move detection 554 | */ 555 | template class sorter { 556 | private: 557 | const vector &values; 558 | 559 | public: 560 | sorter(const vector &v) : values(v) {} 561 | 562 | bool operator()(int a, int b) { 563 | unsigned char *v_a = (unsigned char*)(values[a].data); 564 | unsigned char *v_b = (unsigned char*)(values[b].data); 565 | 566 | int limit = values[a].cols * values[a].rows; 567 | 568 | return lexicographical_compare(v_a, v_a+limit, v_b, v_b+limit); 569 | } 570 | }; 571 | 572 | /* 573 | Copy-Move detection using DCT. 574 | 575 | implementation adapted from "Detection of Copy-Move Forgery in Digital Images" 576 | by Jessica Fridrich, David Soukal, Jan Lukas 577 | http://www.ws.binghamton.edu/fridrich/research/copymove.pdf 578 | 579 | implementation adapted from Samuel Albrecht's GIMP plugin 580 | https://sites.google.com/site/elsamuko/forensics/clone-detection 581 | 582 | This function is different from the above resources: 583 | - Instead of quantizing by the modified JPEG table, this will instead compare 584 | the square submatrix of the DCT values, where the submatrix length is the 585 | "retain" parameter 586 | - The matches with the same shift-vector magnitude get painted in the same (random) color 587 | */ 588 | void copy_move_dct(Mat &src, Mat &dst, int retain = 4, double qcoeff = 1.0) { 589 | Mat grayscale; 590 | cvtColor( src, grayscale, CV_BGR2GRAY ); 591 | grayscale.convertTo(grayscale, CV_32F); 592 | 593 | Mat rectBuffer = src.clone(); 594 | 595 | int subm_limit = retain * retain; 596 | 597 | int blocksize = 16; 598 | int blocks_height = src.rows-blocksize+1; 599 | int blocks_width = src.cols-blocksize+1; 600 | int total_blocks = blocks_height * blocks_width; 601 | 602 | vector< Mat > blocks; 603 | blocks.reserve(total_blocks); 604 | 605 | Mat tmp; 606 | 607 | for(int y=0; y(blocks)); 625 | 626 | for(int i=0; i blocksize ) { 646 | int s_indx = shift.y * (src.cols) + shift.x; 647 | s_count[s_indx]++; 648 | } 649 | } 650 | } 651 | 652 | for(int i=0; i 10 ) { 675 | for(int ii=0; ii(cur.y+ii, cur.x+jj) = color; 678 | rectBuffer.at(next.y+ii, next.x+jj) = color; 679 | } 680 | } 681 | } 682 | } 683 | } 684 | 685 | addWeighted(src, 0.2, rectBuffer, 0.8, 0, dst); 686 | } -------------------------------------------------------------------------------- /functions.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FUNCTIONS_HPP 2 | #define FUNCTIONS_HPP 3 | 4 | #include 5 | 6 | #include "structs.h" 7 | 8 | using namespace cv; 9 | using namespace std; 10 | 11 | /* 12 | Return all colors which have at least one component (R,G,B) set to 255 13 | */ 14 | void rgb_borders(Mat &dst); 15 | 16 | /* 17 | HSV Histogram Stretch (Auto-Levels) 18 | converts the image to HSV colorspace and then applies histogram equalization 19 | to the V channel, and converts back to RGB. This is used to make copies that 20 | are better viewable 21 | */ 22 | void hsv_histogram_stretch(Mat &src, Mat &dst); 23 | 24 | /* 25 | HSV Colorspace Histogram for the image. Count all occurrences of (H,S) and sum the V component, representing the average V in HSV colorspace for each color. 26 | */ 27 | void hsv_histogram(Mat &src, Mat &dst, bool whitebg = false); 28 | 29 | /* 30 | Lab Colorspace Histogram for the image. Count all occurrences of (a,b) and sum the L component, representing the average L in Lab colorspace for each color. 31 | */ 32 | void lab_histogram(Mat &src, Mat &dst, bool whitebg = false); 33 | void lab_histogram_fast(Mat &src, Mat &dst, bool whitebg = false); 34 | 35 | /* 36 | Apply Error Level Analysis to the image. Resave source image at a known quality and subtract the known quality from the source image. 37 | */ 38 | void error_level_analysis(Mat &src, Mat &dst, int quality = 90); 39 | 40 | /* 41 | Colorized X and Y Sobel filters. 42 | */ 43 | void luminance_gradient(Mat &src, Mat &dst); 44 | 45 | /* 46 | Turn every pixel value to the average of the magnitude of its cross-shaped neighbors. 47 | */ 48 | void average_distance(Mat &src, Mat &dst); 49 | 50 | /* 51 | Estimate JPEG quality using Hackerfactor and Imagemagick estimates 52 | */ 53 | int estimate_jpeg_quality(const char *filename, vector &qtables, vector &quality_estimates); 54 | 55 | /* 56 | Copy-Move detection using DCT 57 | */ 58 | void copy_move_dct(Mat &src, Mat &dst, int retain = 4, double qcoeff = 1.0); 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /install_scripts/centos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #provision a clean centos installation 3 | 4 | sudo su 5 | 6 | #update list first 7 | yum -y update 8 | 9 | #install wget 10 | yum -y install wget 11 | 12 | #fetch boost 1.55.0 and compile 13 | wget http://sourceforge.net/projects/boost/files/boost/1.55.0/boost_1_55_0.tar.bz2/download -O boost_1_55_0.tar.bz2 14 | tar --bzip2 -xf boost_1_55_0.tar.bz2 15 | cd boost_1_55_0 16 | ./bootstrap.sh --with-libraries=system,filesystem,program_options 17 | ./b2 install --prefix=/usr 18 | 19 | #install opencv dependencies 20 | yum install -y cmake gtk2-devel python-dev numpy libjpeg-devel libpng-devel libtiff-devel libjasper-devel 21 | 22 | #fetch opencv 2.4.9 and compile 23 | cd .. 24 | wget http://sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.9/opencv-2.4.9.zip/download -O opencv-2.4.9.zip 25 | unzip opencv-2.4.9.zip 26 | cd opencv-2.4.9 27 | mkdir release 28 | cd release 29 | cmake -D CMAKE_BUILD_TYPE=RELEASE \ 30 | -D CMAKE_INSTALL_PREFIX=/usr \ 31 | -D BUILD_opencv_flann=OFF \ 32 | -D BUILD_opencv_features2d=OFF \ 33 | -D BUILD_opencv_calib3d=OFF \ 34 | -D BUILD_opencv_ml=OFF \ 35 | -D BUILD_opencv_video=OFF \ 36 | -D BUILD_opencv_objdetect=OFF \ 37 | -D BUILD_opencv_contrib=OFF \ 38 | -D BUILD_opencv_nonfree=OFF \ 39 | -D BUILD_opencv_gpu=OFF \ 40 | -D BUILD_opencv_legacy=OFF \ 41 | -D BUILD_opencv_photo=OFF \ 42 | -D BUILD_opencv_python=OFF \ 43 | -D BUILD_opencv_stitching=OFF \ 44 | -D BUILD_opencv_ts=OFF \ 45 | -D BUILD_opencv_videostab=OFF \ 46 | -D BUILD_opencv_apps=OFF \ 47 | -D BUILD_TESTS=OFF \ 48 | -D BUILD_PERF_TESTS=OFF \ 49 | .. 50 | make -j2 51 | make install 52 | #update libs 53 | ldconfig 54 | 55 | #good to compile from this point 56 | cd ../.. 57 | git clone https://github.com/ebemunk/phoenix.git 58 | cd phoenix 59 | #comment out the WIN variable in makefile 60 | sed -i '38 s/^/#/' Makefile 61 | export PKG_CONFIG_PATH=/usr/lib/pkgconfig 62 | make -------------------------------------------------------------------------------- /install_scripts/ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #provision a clean ubuntu installation 3 | 4 | sudo su 5 | 6 | #update list first 7 | apt-get update 8 | 9 | #install boost dependencies 10 | apt-get install -y build-essential 11 | 12 | #fetch boost 1.55.0 and compile 13 | wget http://sourceforge.net/projects/boost/files/boost/1.55.0/boost_1_55_0.tar.bz2/download -O boost_1_55_0.tar.bz2 14 | tar --bzip2 -xf boost_1_55_0.tar.bz2 15 | cd boost_1_55_0 16 | ./bootstrap.sh --with-libraries=system,filesystem,program_options 17 | ./b2 install 18 | 19 | #install opencv dependencies 20 | apt-get install -y git cmake libgtk2.0 libgtk2.0-dev pkg-config python-dev python-numpy libjpeg-dev libpng-dev libtiff-dev libjasper-dev unzip 21 | 22 | #fetch opencv 2.4.9 and compile 23 | cd .. 24 | wget http://sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.9/opencv-2.4.9.zip/download -O opencv-2.4.9.zip 25 | unzip opencv-2.4.9.zip 26 | cd opencv-2.4.9 27 | mkdir release 28 | cd release 29 | cmake -D CMAKE_BUILD_TYPE=RELEASE \ 30 | -D CMAKE_INSTALL_PREFIX=/usr/local \ 31 | -D BUILD_opencv_flann=OFF \ 32 | -D BUILD_opencv_features2d=OFF \ 33 | -D BUILD_opencv_calib3d=OFF \ 34 | -D BUILD_opencv_ml=OFF \ 35 | -D BUILD_opencv_video=OFF \ 36 | -D BUILD_opencv_objdetect=OFF \ 37 | -D BUILD_opencv_contrib=OFF \ 38 | -D BUILD_opencv_nonfree=OFF \ 39 | -D BUILD_opencv_gpu=OFF \ 40 | -D BUILD_opencv_legacy=OFF \ 41 | -D BUILD_opencv_photo=OFF \ 42 | -D BUILD_opencv_python=OFF \ 43 | -D BUILD_opencv_stitching=OFF \ 44 | -D BUILD_opencv_ts=OFF \ 45 | -D BUILD_opencv_videostab=OFF \ 46 | -D BUILD_opencv_apps=OFF \ 47 | -D BUILD_TESTS=OFF \ 48 | -D BUILD_PERF_TESTS=OFF \ 49 | .. 50 | make -j2 51 | make install 52 | 53 | #update libs 54 | ldconfig 55 | 56 | #good to compile from this point 57 | cd ../.. 58 | git clone https://github.com/ebemunk/phoenix.git 59 | cd phoenix 60 | #comment out the WIN variable in makefile 61 | sed -i '38 s/^/#/' Makefile 62 | make -------------------------------------------------------------------------------- /phoenix.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include "structs.h" 18 | #include "functions.hpp" 19 | 20 | using namespace std; 21 | using namespace cv; 22 | using namespace boost::program_options; 23 | using namespace boost::filesystem; 24 | using boost::property_tree::ptree; 25 | 26 | //globals for run_analysis function 27 | string output_stem; 28 | ptree root; 29 | bool output, display, autolevels; 30 | 31 | //run_analysis constants 32 | enum analysis_type {A_ELA, A_LG, A_AVGDIST, A_HSV, A_LAB, A_LAB_FAST, A_COPY_MOVE_DCT}; 33 | string analysis_name[] = { 34 | "Error Level Analysis", "Luminance Gradient", "Average Distance", 35 | "HSV Histogram", "Lab Histogram", "Lab Histogram (fast)", "Copy Move Detection (DCT)" 36 | }; 37 | string analysis_abbr[] = {"ela", "lg", "avgdist", "hsv", "lab", "lab_fast", "copymove"}; 38 | 39 | //run analysis on src image 40 | void run_analysis(Mat &src, Mat &dst, analysis_type type, vector params) { 41 | string output_filepath = output_stem + "_" + analysis_abbr[type]; //file name 42 | string title = analysis_name[type]; //display window title 43 | string ptree_element = analysis_abbr[type]; //json tree title 44 | 45 | bool apply_autolevels = autolevels && (type == A_ELA || type == A_LG || type == A_AVGDIST); 46 | if(apply_autolevels) { 47 | output_filepath += "_autolevels.png"; 48 | ptree_element += "_autolevels"; 49 | } else { 50 | output_filepath += ".png"; 51 | } 52 | 53 | switch(type) { 54 | case A_ELA: 55 | error_level_analysis(src, dst, params[0]); 56 | root.put(ptree_element + ".quality", params[0]); 57 | break; 58 | case A_LG: 59 | luminance_gradient(src, dst); 60 | break; 61 | case A_AVGDIST: 62 | average_distance(src, dst); 63 | break; 64 | case A_HSV: 65 | hsv_histogram(src, dst, params[0]); 66 | root.put(ptree_element + ".whitebg", (bool)params[0]); 67 | break; 68 | case A_LAB: 69 | lab_histogram(src, dst, params[0]); 70 | root.put(ptree_element + ".whitebg", (bool)params[0]); 71 | break; 72 | case A_LAB_FAST: 73 | lab_histogram_fast(src, dst, params[0]); 74 | root.put(ptree_element + ".whitebg", (bool)params[0]); 75 | break; 76 | case A_COPY_MOVE_DCT: 77 | copy_move_dct(src, dst, params[0], params[1]); 78 | root.put(ptree_element + ".retain", params[0]); 79 | root.put(ptree_element + ".qcoeff", params[1]); 80 | break; 81 | } 82 | 83 | if(apply_autolevels) { 84 | hsv_histogram_stretch(dst, dst); 85 | } 86 | 87 | if(output) { //output image & add to ptree 88 | bool write_success = imwrite(output_filepath, dst); 89 | if(!write_success) { 90 | root.put(ptree_element + ".filename", "Error! Do you have write permission?"); 91 | } else { 92 | string filepath = canonical(output_filepath).make_preferred().string(); 93 | root.put(ptree_element + ".filename", filepath); 94 | } 95 | } 96 | 97 | if(display) { //display right away, waitKey(0) at the end of program 98 | namedWindow(title); 99 | imshow(title, dst); 100 | } else { //release memory 101 | dst.release(); 102 | } 103 | } 104 | 105 | //override ostream << operator for vector so we can use it as implicit_value 106 | //or as boost puts it, make vector ostream'able 107 | namespace std { 108 | static std::ostream& operator<<(std::ostream& os, const std::vector& v) { 109 | os << '{'; 110 | for(int i=0; i [options]\nAllowed options"); 123 | desc.add_options() 124 | ("help,h", "List all arguments - produce help message") 125 | ("file,f", value()->required(), "Source image file") 126 | 127 | ("ela", value()->implicit_value(70), "Error Level Analysis [quality]") 128 | ("hsv", value()->implicit_value(0), "HSV Colorspace Histogram [whitebg]") 129 | ("lab", value()->implicit_value(0), "Lab Colorspace Histogram [whitebg]") 130 | ("labfast", value()->implicit_value(0), "Lab Colorspace Histogram (Fast Version) [whitebg]") 131 | ("lg", bool_switch()->default_value(false), "Luminance Gradient") 132 | ("avgdist", bool_switch()->default_value(false), "Average Distance") 133 | ("copymove", value>()->multitoken()->implicit_value(vector{4, 1.0}), "Copy-Move Detection (DCT) [retain] [qcoeff]") 134 | 135 | ("autolevels,a", bool_switch()->default_value(false), "Apply histogram stretch to outputs") 136 | ("quality,q", bool_switch()->default_value(true), "Estimate JPEG Quality") 137 | 138 | ("output,o", value()->implicit_value("./"), "Output folder path") 139 | ("display,d", bool_switch()->default_value(false), "Display outputs") 140 | ("verbose,v", bool_switch()->default_value(false), "Verbose (debug) mode") 141 | ("json,j", bool_switch()->default_value(false), "Output JSON") 142 | ; 143 | 144 | variables_map vm; 145 | 146 | try { //try to parse command options 147 | store( 148 | command_line_parser(argc, argv).options(desc) 149 | .style( 150 | command_line_style::allow_short 151 | | command_line_style::short_allow_next 152 | | command_line_style::short_allow_adjacent 153 | | command_line_style::allow_dash_for_short 154 | | command_line_style::allow_long 155 | | command_line_style::long_allow_next 156 | | command_line_style::long_allow_adjacent 157 | | command_line_style::allow_long_disguise 158 | ).run() 159 | , vm); 160 | 161 | if (vm.count("help")) { //print help text before notify() 162 | cout << desc << endl; 163 | return 0; 164 | } 165 | notify(vm); //send commands to variables_map 166 | } catch (const exception &e) { //error with command options 167 | cout << "Error: Cannot parse program commands!" << endl; 168 | cout << e.what() << endl; 169 | cout << "Use -h or -help flag to see available commands." << endl; 170 | return 1; 171 | } catch(...) { 172 | cout << "Error: Fatal error while parsing options." << endl; 173 | return 1; 174 | } 175 | 176 | //some path info 177 | path source_path; 178 | path output_path; 179 | Mat source_image; 180 | 181 | try { //check and try to open source image file (-f) 182 | source_path = vm["file"].as(); 183 | if(!exists(source_path)) { 184 | cout << "Error: File not found!" << endl; 185 | cout << "File path input: " << source_path << endl; 186 | return 1; 187 | } 188 | 189 | //load image to memory 190 | source_image = imread(source_path.string(), CV_LOAD_IMAGE_COLOR); 191 | if(source_image.data == NULL) { 192 | cout << "Error: Cannot read image!" << endl; 193 | cout << "File path input: " << source_path << endl; 194 | return 1; 195 | } 196 | 197 | //validate output path 198 | if(vm.count("output")) { 199 | output_path = vm["output"].as(); 200 | if(!is_directory(output_path)) { 201 | cout << "Error: Output directory does not exist!" << endl; 202 | cout << "Output directory input: " << output_path << endl; 203 | return 1; 204 | } 205 | output_path = canonical(output_path.make_preferred()); 206 | } 207 | } catch(const exception &e) { //cannot load the image for some reason 208 | cout << "Error: Problem while opening the file!" << endl; 209 | cout << e.what() << endl; 210 | return 1; 211 | } 212 | 213 | //assign globals 214 | display = vm["display"].as(); 215 | output = vm.count("output"); 216 | autolevels = vm["autolevels"].as(); 217 | output_stem = output_path.string() + "/" + source_path.stem().string(); 218 | 219 | bool verbose = vm["verbose"].as(); 220 | 221 | if(vm.count("ela")) { 222 | Mat ela; 223 | vector params {(double) vm["ela"].as()}; 224 | 225 | run_analysis(source_image, ela, A_ELA, params); 226 | } 227 | 228 | if(vm["lg"].as()) { 229 | Mat lg; 230 | vector params; 231 | 232 | run_analysis(source_image, lg, A_LG, params); 233 | } 234 | 235 | if(vm["avgdist"].as()) { 236 | Mat avgdist; 237 | vector params; 238 | 239 | run_analysis(source_image, avgdist, A_AVGDIST, params); 240 | } 241 | 242 | if(vm.count("hsv")) { 243 | Mat hsv; 244 | vector params {(double) vm["hsv"].as()}; 245 | 246 | run_analysis(source_image, hsv, A_HSV, params); 247 | } 248 | 249 | if(vm.count("lab")) { 250 | Mat lab; 251 | vector params {(double) vm["lab"].as()}; 252 | 253 | run_analysis(source_image, lab, A_LAB, params); 254 | } 255 | 256 | if(vm.count("labfast")) { 257 | Mat lab; 258 | vector params {(double) vm["labfast"].as()}; 259 | 260 | run_analysis(source_image, lab, A_LAB_FAST, params); 261 | } 262 | 263 | if(vm.count("copymove")) { 264 | Mat copymove; 265 | vector input = vm["copymove"].as>(); 266 | vector params; 267 | if(input.size() == 1) { 268 | if(input[0] > 16) input[0] = 16; 269 | params = {input[0], 1.0}; 270 | } else { 271 | params = input; 272 | } 273 | 274 | run_analysis(source_image, copymove, A_COPY_MOVE_DCT, params); 275 | } 276 | 277 | if(vm["quality"].as()) { 278 | int num_qtables = 0; 279 | vector qtables; 280 | vector quality; 281 | 282 | num_qtables = estimate_jpeg_quality(source_path.string().c_str(), qtables, quality); 283 | 284 | if(num_qtables > 0) { //if we have quantization tables, save them to ptree 285 | root.put("imagick_estimate", quality[0]); 286 | root.put("hf_estimate", quality[1]); 287 | for(int i=0; i(j, k); 292 | if(j*k < 48) { 293 | dqt << ","; 294 | } 295 | } 296 | } 297 | stringstream tableindex; 298 | tableindex << "qtables." << i; 299 | root.put(tableindex.str(), dqt.str()); 300 | } 301 | } 302 | } 303 | 304 | if(vm.count("output") == 0 && vm["display"].defaulted()) { 305 | cout << "Warning: No -output or -display option specified. You might want to use one (or both)." << endl; 306 | } 307 | 308 | if(vm["json"].as() || !vm["quality"].defaulted()) { 309 | write_json(cout, root); 310 | } 311 | 312 | if(display) { 313 | waitKey(0); 314 | } 315 | 316 | return 0; 317 | } -------------------------------------------------------------------------------- /resources.rc: -------------------------------------------------------------------------------- 1 | id ICON "assets/eye.ico" 2 | 1 VERSIONINFO 3 | FILEVERSION 1,0,0,0 4 | BEGIN 5 | BLOCK "StringFileInfo" 6 | BEGIN 7 | BLOCK "080904E4" 8 | BEGIN 9 | VALUE "FileDescription", "Phoenix: Image Forensics" 10 | VALUE "FileVersion", "1.0.0" 11 | VALUE "OriginalFilename", "phoenix.exe" 12 | END 13 | END 14 | 15 | BLOCK "VarFileInfo" 16 | BEGIN 17 | VALUE "Translation", 0x809, 1252 18 | END 19 | END -------------------------------------------------------------------------------- /speedtests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "functions.hpp" 9 | 10 | using namespace std; 11 | using namespace cv; 12 | 13 | int main(int argc, char *argv[]) { 14 | Mat source_image; 15 | source_image = imread("C:\\wamp\\www\\pe\\source_images\\A.jpg", CV_LOAD_IMAGE_COLOR); 16 | 17 | cout << "Starting speed tests..." << endl; 18 | 19 | Mat tmp; 20 | 21 | auto start_time = chrono::high_resolution_clock::now(); 22 | for(int i=0; i<10; i++) { 23 | error_level_analysis(source_image, tmp); 24 | } 25 | auto end_time = chrono::high_resolution_clock::now(); 26 | cout << "ELA: "; 27 | cout << chrono::duration_cast(end_time - start_time).count() << ":"; 28 | cout << chrono::duration_cast(end_time - start_time).count() << ":" << endl; 29 | 30 | start_time = chrono::high_resolution_clock::now(); 31 | for(int i=0; i<10; i++) { 32 | luminance_gradient(source_image, tmp); 33 | } 34 | end_time = chrono::high_resolution_clock::now(); 35 | cout << "LG: "; 36 | cout << chrono::duration_cast(end_time - start_time).count() << ":"; 37 | cout << chrono::duration_cast(end_time - start_time).count() << ":" << endl; 38 | 39 | start_time = chrono::high_resolution_clock::now(); 40 | for(int i=0; i<10; i++) { 41 | average_distance(source_image, tmp); 42 | } 43 | end_time = chrono::high_resolution_clock::now(); 44 | cout << "Avgdist: "; 45 | cout << chrono::duration_cast(end_time - start_time).count() << ":"; 46 | cout << chrono::duration_cast(end_time - start_time).count() << ":" << endl; 47 | 48 | start_time = chrono::high_resolution_clock::now(); 49 | for(int i=0; i<10; i++) { 50 | hsv_histogram(source_image, tmp); 51 | } 52 | end_time = chrono::high_resolution_clock::now(); 53 | cout << "HSV: "; 54 | cout << chrono::duration_cast(end_time - start_time).count() << ":"; 55 | cout << chrono::duration_cast(end_time - start_time).count() << ":" << endl; 56 | 57 | start_time = chrono::high_resolution_clock::now(); 58 | for(int i=0; i<10; i++) { 59 | lab_histogram(source_image, tmp); 60 | } 61 | end_time = chrono::high_resolution_clock::now(); 62 | cout << "Lab: "; 63 | cout << chrono::duration_cast(end_time - start_time).count() << ":"; 64 | cout << chrono::duration_cast(end_time - start_time).count() << ":" << endl; 65 | 66 | return 0; 67 | } -------------------------------------------------------------------------------- /structs.h: -------------------------------------------------------------------------------- 1 | #ifndef STRUCTS_H 2 | #define STRUCTS_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | struct qtable { 9 | int index; 10 | int precision; 11 | cv::Mat table; 12 | double sum; 13 | double hf_qval; 14 | double im_qval; 15 | }; 16 | 17 | #endif --------------------------------------------------------------------------------