#requires pip install python-opencv-headless

import argparse
import cv2 as cv
import numpy as np
import os

parser = argparse.ArgumentParser(description=
"""
Compares to images by pixels, outputing two different images for the differences. 

output file {status}
output file {name}.diff0.png
    The exact abs(A - B) of each pixel
output file {name}.diff1.png
    A mask which has 1 for difference and 0 for no difference at a given pixel
""")

parser.add_argument("-n", "--name", type=str, required=True, help="name used for output images")
parser.add_argument("-c", "--candidate", type=str, required=True, help="candidate for image diffing")
parser.add_argument("-g", "--golden", type=str, required=True, help="golden to compare against")
parser.add_argument("-s", "--status", type=str, required=True, help="output stats file name and location")
parser.add_argument("-o", "--outdir", type=str, help="output directory to store image diffs, if not provided then no output image is saved")
parser.add_argument("-v", "--verbose", action='store_true', help="enable verbose logging")
parser.add_argument("-l", "--log", action='store_true', help="redirect all verbose logging to a file with --name")
parser.add_argument("-H", "--histogram", action='store_true', help="compare images using histograms as an additional method for ruling out 'same' images. This should prevent subtle differences in the image that should not be visible from preventing a passing result")

args = parser.parse_args()

def verbose_log(val):
    if not args.verbose:
        return
    if args.log:
        with open(f"tmp/{args.name}.log", "a") as log_file:
            log_file.write(val+"\n")
    else:
        print(val)

def main():
    if args.log:
        os.makedirs("tmp", exist_ok=True)

    size_match = False
    failed = False
    try:
        verbose_log(f"loading {args.candidate}")
        candidate = cv.imread(args.candidate)
        verbose_log(f"loading {args.golden}")
        golden = cv.imread(args.golden)

        if candidate is not None and golden is not None:
            size_match = candidate.shape == golden.shape
            # get absolute difference between golden and candidate
            diff = cv.absdiff(golden, candidate)
            #convert diff to grey scale to make calculations easier
            grey_diff = cv.cvtColor(diff, cv.COLOR_BGR2GRAY)
            #convert to rgb for output png
            rgb_diff = cv.cvtColor(diff, cv.COLOR_BGR2RGB)
            # convert the diff to just white where a difference is
            _, mask = cv.threshold(grey_diff, 0, 255, cv.THRESH_BINARY)
            # get the total number of different pixels
            total_diff_count = cv.countNonZero(grey_diff)
            # get the max channel independent difference
            max_diff = int(diff.max())
            # get average pixel diff, this result is slitely different then imageDiff but its very close.
            # the only difference is that we first convert the diff to grey scale rather than adding the max of every channel
            avg = grey_diff.sum() / (grey_diff.shape[0]*grey_diff.shape[1]*255)

            if args.histogram:
                # convert to HSV
                hsv_candidate = cv.cvtColor(candidate, cv.COLOR_BGR2HSV)
                hsv_golden = cv.cvtColor(golden, cv.COLOR_BGR2HSV)

                # down sample to 32x32 for the histogram
                h_bins = 32
                s_bins = 32
                histSize = [h_bins, s_bins]

                # available range for hue and saturation
                h_ranges = [0, 180]
                s_ranges = [0, 256]
                ranges = h_ranges + s_ranges

                # get our histograms
                hist_candidate = cv.calcHist(hsv_candidate, [0,1], None, histSize, ranges, accumulate=False)
                hist_golden = cv.calcHist(hsv_golden, [0,1], None, histSize, ranges, accumulate=False)

                # compare using CORREL histogram algorithm. the different options are detailed here 
                # https://docs.opencv.org/3.4/d6/dc7/group__imgproc__hist.html#ga994f53817d621e2e4228fc646342d386
                # a hist_result of 1.0 means identical with this method. any variance results in a number less then 1.0
                hist_result = cv.compareHist(hist_candidate, hist_golden, cv.HISTCMP_CORREL)
                
    except Exception as E:
        print(f"Failed to load and process images {E}")
        failed = True

    # make path to stats file if necessary
    if os.path.dirname(args.status):
        verbose_log(f"making status file path {os.path.dirname(args.status)}")
        os.makedirs(os.path.dirname(args.status), exist_ok=True)
    verbose_log(f"making status file {args.status}")
    with open(args.status, "a") as status:
        status.write(args.name + "\t");
        if candidate is None:
            status.write("missing_candidate\n")
            verbose_log("missing golden for " + args.name)
            return
        if golden is None:
            status.write("missing_golden\n")
            verbose_log("missing golden for " + args.name)
            return
        if failed:
            status.write("failed\n")
            verbose_log("failed to load golden or candidate")
            return
        if total_diff_count == 0:
            status.write("identical\n")
            verbose_log("files are identical")
            return
        if not size_match:
            status.write("sizemismatch\n")
            verbose_log("files are not the same size")
            return
        status.write(str(max_diff)+"\t")
        # prevent python from writing out in scientific notation
        status.write(f"{float(avg):.5f}\t")
        status.write(str(total_diff_count)+"\t")
        status.write(str(diff.shape[0]*diff.shape[1]))
        # add our histogram result as the last value of the status file or write a new line to say we are finished with this status
        if args.histogram:
            status.write(f"\t{float(hist_result):.5f}\n")
        else:
            status.write("\n")

    verbose_log("status file finished")
    # save the output file if location provided
    if args.outdir:
        # color diff file location
        c_diff_path = os.path.join(args.outdir, args.name + ".diff0.png")
        # mask diff file location 
        c_mask_path = os.path.join(args.outdir, args.name + ".diff1.png")
        # create output dir if needed
        verbose_log(f"making output directory {args.outdir}")
        os.makedirs(args.outdir, exist_ok=True)
        verbose_log(f"writing images {c_diff_path} and {c_mask_path}")
        cv.imwrite(c_diff_path, rgb_diff)
        cv.imwrite(c_mask_path, mask)
        verbose_log("finished writing output images")

if __name__ == '__main__':
    main()
