'''
Frontend for running image tests.
'''

import os
import sys
import re
import json
import time
import datetime
import argparse
import subprocess
import shutil
from pathlib import Path
from enum import Enum

import concurrent.futures
import multiprocessing
import signal
import threading
from xml.etree import ElementTree as ET

from core import Environment, helpers, config
from core.environment import find_most_recent_build_config
from core.termcolor import colored

print_mutex = multiprocessing.Lock()

class ProcessController:
    is_exiting = threading.Event()
    all_processes_mutex = multiprocessing.Lock()
    all_processes = {}
    thread_count = 1

    def __init__(self, thread_count):

        self.thread_count = thread_count

        def signal_handler(signum, frame):
            with self.all_processes_mutex:
                self.is_exiting.set()
                for p in self.all_processes:
                    p.kill()

        signal.signal(signal.SIGTERM, signal_handler)

    def is_interrupted(self):
        with self.all_processes_mutex:
            return self.is_exiting.is_set()

    def interrupt_and_exit(self):
        print("Received interrupt (e.g. Ctrl-C), shutting down tests and exiting")
        with self.all_processes_mutex:
            self.is_exiting.set()
            for name, p in self.all_processes.items():
                p.kill()

    def add_process(self, name, p):
        with self.all_processes_mutex:
            if self.is_exiting.is_set():
                p.kill()
                return False
            self.all_processes[name] = p
            return True

def read_header(script_file):
    '''
    Check if script has a IMAGE_TEST dictionary defined at the top and return it's content.
    '''
    with open(script_file) as f:
        script = f.read()
        # Find IMAGE_TEST={} at the top of the script
        m = re.match(r'IMAGE_TEST\s*=\s*({.*})', script, re.DOTALL)
        if m:
            header = None
            # Match curly braces
            depth = 0
            for i, c in enumerate(m.group(1)):
                if c == '{':
                    depth += 1
                if c == '}':
                    depth -= 1
                    if depth == 0:
                        header = m.group(1)[0:i+1]
                        break
            if depth != 0:
                raise Exception(f'Failed to parse script header in {script_file} (curly braces do not match)')
            # Evaluate dictionary
            if header:
                try:
                    return eval(header)
                except Exception as e:
                    raise Exception(f'Failed to parse script header in {script_file} ({e})')

    return {}

class Test:
    '''
    Represents a single image test.
    '''

    class Result(Enum):
        PASSED = 1
        FAILED = 2
        SKIPPED = 3

    COLORED_RESULT_STRING = {
        Result.PASSED: colored('PASSED', 'green'),
        Result.FAILED: colored('FAILED', 'red'),
        Result.SKIPPED: colored('SKIPPED', 'yellow')
    }

    RESULT_STRING = {
        Result.PASSED: 'PASSED',
        Result.FAILED: 'FAILED',
        Result.SKIPPED: 'SKIPPED'
    }

    process_controller = None

    def __init__(self, script_file, root_dir, device_type, header):
        self.script_file = script_file
        self.device_type = device_type
        self.header = header

        # Test name.
        self.name = str(self.script_file.relative_to(root_dir).with_suffix('').as_posix()) + f"_{self.device_type}"

        # Test directory relative to root directory.
        self.test_dir = Path(self.name)

        # Get tags.
        self.tags = self.header.get('tags', ['default'])

        # Get skipped tests.
        self.skip_message = self.header.get('skipped', None)
        self.skipped = self.skip_message != None

        # Get tolerance.
        self.tolerance = self.header.get('tolerance', config.DEFAULT_TOLERANCE)

        # Get timeout.
        self.timeout = self.header.get('timeout', config.DEFAULT_TIMEOUT)

    def __repr__(self):
        return f'Test(name={self.name},script_file={self.script_file})'

    def matches_tags(self, tags):
        '''
        Check if the test's tags matches any of the given tags.
        '''
        for tag in self.tags:
            if tag in tags:
                return True
        return False

    def collect_images(self, image_dir):
        '''
        Collect all reference and result images in a directory.
        Returns paths relative to the given directory.
        '''
        files = image_dir.iterdir()
        files = map(lambda f: f.relative_to(image_dir), files)
        files = filter(lambda f: not str(f).endswith(config.ERROR_IMAGE_SUFFIX), files)
        files = filter(lambda f: f.suffix.lower() in config.IMAGE_EXTENSIONS, files)
        return list(files)

    def generate_images(self, output_dir, mogwai_exe, run_only=False):
        '''
        Run Mogwai to generate a set of images and store them in output_dir.
        Returns a tuple containing the result code and a list of messages.
        '''
        # Bail out if test is skipped.
        if self.skipped:
            return Test.Result.SKIPPED, [self.skip_message] if self.skip_message != '' else [], {}

        # Determine full output directory.
        output_dir = output_dir / self.test_dir
        output_dir.mkdir(parents=True, exist_ok=True)

        # In order to simplify module imports in test scripts, we run Mogwai with
        # a working directory set to the directory the test script resides in.
        # The working directory also specifies the python search path.
        # Here we determine the working directory and a function to resolve paths
        # to be relative to the working directory.
        cwd = self.script_file.parent
        relative_to_cwd = lambda p: os.path.relpath(p, cwd)

        # Write helper script to run test.
        generate_file = output_dir / 'generate.py'
        with open(generate_file, 'w') as f:
            # Hack to pass run only flag to helpers.py
            if run_only:
                f.write("import falcor\n")
                f.write(f'falcor.__dict__["IMAGE_TEST_RUN_ONLY"]=True\n')
            f.write(f'm.frameCapture.outputDir = r"{output_dir}"\n')
            f.write(f'm.script(r"{relative_to_cwd(self.script_file)}")\n')

        # Run Mogwai to generate images.
        args = [
            str(mogwai_exe),
            '--device-type', str(self.device_type),
            '--script', str(relative_to_cwd(generate_file)),
            '--logfile', str(output_dir / 'log.txt'),
            '--headless',
            '--precise'
        ]
        rerun_env = {}
        rerun_env["cwd"] = str(cwd)
        rerun_env["args"] = args[1:]
        p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if not self.process_controller.add_process(self.name + ":run", p):
            return Test.Result.FAILED, ['Process killed due to global exit'], rerun_env
        try:
            outs, errs = p.communicate(timeout=self.timeout)
        except subprocess.TimeoutExpired:
            p.kill()
            return Test.Result.FAILED, ['Process killed due to timeout'], rerun_env

        # Check for success.
        if p.returncode != 0:
            # Generate list of errors from stderr.
            errors = list(map(lambda l: l.rstrip(), errs.decode('utf-8').splitlines()))
            return Test.Result.FAILED, errors + [f'{mogwai_exe} exited with return code {p.returncode}'], rerun_env

        # Bail out if no images have been generated.
        if not run_only and len(self.collect_images(output_dir)) == 0:
            return Test.Result.FAILED, ['Test did not generate any images.'], rerun_env

        return Test.Result.PASSED, [], rerun_env

    def compare_images(self, ref_dir, result_dir, image_compare_exe):
        '''
        Run ImageCompare on a set of images in ref_dir and result_dir.
        Checks if error between reference and result image is within a given tolerance.
        Returns a tuple containing the result code, a list of messages and a list of image reports.
        '''
        # Bail out if test is skipped.
        if self.skipped:
            return Test.Result.SKIPPED, [self.skip_message] if self.skip_message != '' else [], []

        # Determine full directory paths for references and results.
        ref_dir = ref_dir / self.test_dir
        result_dir = result_dir / self.test_dir

        # Make sure directories exist.
        if not ref_dir.exists():
            return Test.Result.FAILED, [f'Reference directory "{ref_dir}" does not exist.'], []
        elif not result_dir.exists():
            return Test.Result.FAILED, [f'Result directory "{result_dir}" does not exist.'], []

        # Collect all reference and result images.
        ref_images = self.collect_images(ref_dir)
        result_images = self.collect_images(result_dir)

        # Bail out if no images have been generated.
        if len(result_images) == 0:
            return Test.Result.FAILED, ['Test did not generate any images.'], []

        result = Test.Result.PASSED
        messages = []
        image_reports = []

        # Compare every result image with the corresponding reference image and report missing references.
        processes = {}
        for image in result_images:
            if not image in ref_images:
                result = Test.Result.FAILED
                messages.append(f'Test has generated image "{image}" with no corresponding reference image.')
                continue

            ref_file = ref_dir / image
            result_file = result_dir / image
            error_file = result_dir / (str(image) + config.ERROR_IMAGE_SUFFIX)

            args = [str(image_compare_exe), '-m', 'mse', '-t', str(self.tolerance), str(ref_file), str(result_file)]
            if error_file:
                args += ['-e', str(error_file)]
            processes[image] = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            if not self.process_controller.add_process(self.name + ":image:" + str(image), processes[image]):
                return Test.Result.FAILED, ['Process killed due to global exit']

        for image, process in processes.items():

            output = process.communicate()[0]
            compare_success =  process.returncode == 0
            compare_error = float(output.strip())

            if not compare_success:
                result = Test.Result.FAILED
                messages.append(f'Test image "{image}" failed with error {compare_error}.')

            image_reports.append({
                'name': str(image),
                'success': compare_success,
                'error': compare_error,
                'tolerance': self.tolerance
            })

        # Report missing result images for existing reference images.
        for image in ref_images:
            if not image in result_images:
                result = Test.Result.FAILED
                messages.append(f'Test has not generated an image for the corresponding reference image "{image}".')

        return result, messages, image_reports

    def run(self, run_only, compare_only, ref_dir, result_dir, mogwai_exe, image_compare_exe):
        '''
        Run the image test.
        First, result images are generated (unless compare_only is True).
        Second, result images are compared against reference images.
        Third, writes a JSON report to the result_dir containing details on the test run.
        Returns a tuple containing the result code and a list of messages.
        '''
        # Setup report.
        report = {
            'name': self.name,
            'ref_dir': str(ref_dir / self.test_dir),
            'images': []
        }

        start_time = time.time()
        result = Test.Result.PASSED
        messages = []
        rerun_env = {}

        # Generate results images.
        if not compare_only:
            result, messages, rerun_env = self.generate_images(result_dir, mogwai_exe, run_only)

        # Compare to references.
        if not run_only and result == Test.Result.PASSED:
            result, messages, report['images'] = self.compare_images(ref_dir, result_dir, image_compare_exe)

        # Finish report.
        report['result'] = Test.RESULT_STRING[result]
        report['messages'] = messages
        report['duration'] = time.time() - start_time
        report['rerun_env'] = rerun_env

        # Write JSON report.
        report_dir = result_dir / self.test_dir
        report_dir.mkdir(parents=True, exist_ok=True)
        report_file = report_dir / 'report.json'
        with open(report_file, 'w') as f:
            json.dump(report, f, indent=4)

        return result, messages

def generate_ref(env, test, ref_dir, process_controller):
    if process_controller.is_interrupted():
        return
    with print_mutex:
        print(f'  {test.name:<60} : STARTED')
    test.process_controller = process_controller
    start_time = time.time()
    result, messages, rerun_env = test.generate_images(ref_dir, env.mogwai_exe)
    elapsed_time = time.time() - start_time
    return {"name": test.name, "elapsed_time": elapsed_time, "result": result, "messages": messages, "rerun_env": rerun_env}

def generate_refs(env, tests, ref_dir, process_controller):
    '''
    Computes references for a set of tests and stores them into ref_dir.
    '''
    print(f'Reference directory: {ref_dir}')
    print(f'Generating references for {len(tests)} tests on {process_controller.thread_count} processes')
    if process_controller.thread_count > 1:
        print(colored('Test timings (both indidivual and total) are unreliable when running tests in parallel.', 'red'))

    # Remove existing references.
    if ref_dir.exists():
        shutil.rmtree(ref_dir, ignore_errors=True)

    success = True
    total_elapsed_time = 0

    try:
        with concurrent.futures.ThreadPoolExecutor(process_controller.thread_count) as executor:
            futures = {executor.submit(generate_ref, env, test, ref_dir, process_controller) for test in tests}
            try:
                for future in concurrent.futures.as_completed(futures):
                    run_result   = future.result()
                    if run_result == None:
                        continue
                    test_name    = run_result["name"]
                    elapsed_time = run_result["elapsed_time"]
                    result       = run_result["result"]
                    messages     = run_result["messages"]

                    if result == Test.Result.FAILED:
                        success = False

                    # Print result and messages.
                    status = Test.COLORED_RESULT_STRING[result]
                    with print_mutex:
                        print(f'  {test_name:<60} : {status} ({elapsed_time:.1f} s)')
                        for message in messages:
                            print(f'    {message}')
            except KeyboardInterrupt:
                process_controller.interrupt_and_exit()
                raise
    except KeyboardInterrupt:
        return False

    status = colored('PASSED', 'green') if success else colored('FAILED', 'red')
    print(f'\nGenerating references {status} ({total_elapsed_time:.1f} s).')
    if process_controller.thread_count > 1:
        print(colored('Test timings (both indidivual and total) are unreliable when running tests in parallel.','red'))

    return success

def run_test(env, test, run_only, compare_only, ref_dir, result_dir, min_tolerance, process_controller):
    if process_controller.is_interrupted():
        return
    with print_mutex:
        print(f'  {test.name:<60} : STARTED')
    test.tolerance = max(test.tolerance, min_tolerance)
    test.process_controller = process_controller
    start_time = time.time()
    result, messages = test.run(run_only, compare_only, ref_dir, result_dir, env.mogwai_exe, env.image_compare_exe)
    elapsed_time = time.time() - start_time
    return {"name": test.name, "elapsed_time": elapsed_time, "result": result, "messages": messages}

def run_tests(env, tests, run_only, compare_only, ref_dir, result_dir, min_tolerance, xml_report, process_controller):
    '''
    Runs a set of tests, stores them into result_dir and compares them to ref_dir.
    '''
    print(f'Result directory: {result_dir}')
    print(f'Reference directory: {ref_dir}')
    print(f'Running {len(tests)} tests on {process_controller.thread_count} processes')
    if process_controller.thread_count > 1:
        print(colored('Test timings (both indidivual and total) are unreliable when running tests in parallel.', 'red'))

    success = True
    run_date = datetime.datetime.now()
    run_start_time = time.time()
    run_results = []
    total_elapsed_time = 0

    try:
        # Run tests on #CPU - 2 (to retain some performance control)
        with concurrent.futures.ThreadPoolExecutor(process_controller.thread_count) as executor:
            futures = {executor.submit(run_test, env, test, run_only, compare_only, ref_dir, result_dir, min_tolerance, process_controller) for test in tests}
            try:
                for future in concurrent.futures.as_completed(futures):
                    run_result   = future.result()
                    if run_result == None:
                        continue

                    run_results.append(run_result)

                    test_name    = run_result["name"]
                    elapsed_time = run_result["elapsed_time"]
                    result       = run_result["result"]
                    messages     = run_result["messages"]

                    if result == Test.Result.FAILED:
                        success = False

                    # Print result and messages.
                    status = Test.COLORED_RESULT_STRING[result]
                    with print_mutex:
                        print(f'  {test_name:<60} : {status} ({elapsed_time:.1f} s)')
                        for message in messages:
                            print(f'    {message}')
            except KeyboardInterrupt:
                process_controller.interrupt_and_exit()
                raise
    except KeyboardInterrupt:
        return False

    total_elapsed_time = time.time() - run_start_time

    status = colored('PASSED', 'green') if success else colored('FAILED', 'red')
    print(f'\nImage tests {status} ({total_elapsed_time:.1f} s).')
    if process_controller.thread_count > 1:
        print(colored('Test timings (both indidivual and total) are unreliable when running tests in parallel.','red'))

    # Setup report.
    report = {
        'date': run_date.isoformat(),
        'result': 'PASSED' if success else 'FAILED',
        'tests': [t.name for t in tests],
        'duration': time.time() - run_start_time
    }

    # Write JSON report.
    report_file = result_dir / 'report.json'
    with open(report_file, 'w') as f:
        json.dump(report, f, indent=4)

    # Write XML report.
    if xml_report:
        testsuites = ET.Element("testsuites")
        suite = ET.SubElement(testsuites, "testsuite", name="Image Tests")
        for run_result in run_results:
            testcase = ET.SubElement(suite, "testcase", name=run_result["name"], time="%.3f" % run_result["elapsed_time"])
            if run_result["result"] == Test.Result.SKIPPED:
                ET.SubElement(testcase, "skipped")
            elif run_result["result"] == Test.Result.FAILED:
                ET.SubElement(testcase, "failure", message="\n".join(run_result["messages"]))

        tree = ET.ElementTree(testsuites)
        tree.write(xml_report)

    return success

def list_tests(tests):
    '''
    Print a list of tests.
    '''
    print(f'Found {len(tests)} tests')
    for test in tests:
        print(f'  {test.name}')

def collect_tests(root_dir, filter_regex, tags):
    '''
    Collect a list of all tests found in root_dir that are matching the filter_regex and tags.
    A test script needs to be named test_*.py to be detected.
    '''
    # Find all script files.
    script_files = list(root_dir.glob('**/test_*.py'))

    # Create tests.
    tests = []
    for script_file in script_files:
        try:
            header = read_header(script_file)
        except Exception as e:
            print(e)
            sys.exit(1)

        # Check if test is enabled for current platform.
        platforms = header.get("platforms", config.DEFAULT_PLATFORMS)
        if not config.PLATFORM in platforms:
            continue

        # For now we default to d3d12 as we bring in image tests on vulkan.
        # We should later switch this default to ["d3d12", "vulkan"].
        device_types = header.get("device_types", ["d3d12"])
        device_types = [d for d in device_types if d in config.SUPPORTED_DEVICE_TYPES]
        for device_type in device_types:
            tests.append(Test(script_file, root_dir, device_type, header))

    # Filter using regex.
    if filter_regex != '':
        regex = re.compile(filter_regex or '')
        tests = list(filter(lambda t: regex.search(t.name) != None, tests))

    # Filter using tags.
    tags = tags.split(',')
    tests = list(filter(lambda t: t.matches_tags(tags), tests))

    return tests

def push_refs(ref_dir, remote_ref_dir):
    '''
    Pushes reference images from ref_dir to remote_ref_dir.
    '''
    print(f'Pushing reference images to {remote_ref_dir} : ', end='', flush=True)
    success, log = helpers.mirror_folders(ref_dir, remote_ref_dir)
    print(colored('OK', 'green') if success else colored('FAILED', 'red'))
    if not success:
        print(log)
    return success

def pull_refs(remote_ref_dir, ref_dir):
    '''
    Pulls reference images from remote_ref_dir to ref_dir.
    '''
    print(f'Pulling reference images from {remote_ref_dir} : ', end='', flush=True)
    success, log = helpers.mirror_folders(remote_ref_dir, ref_dir)
    print(colored('OK', 'green') if success else colored('FAILED', 'red'))
    if not success:
        print(log)
    return success

def main():
    default_config = find_most_recent_build_config()
    default_processes_count = min(config.DEFAULT_PROCESS_COUNT, multiprocessing.cpu_count())

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--environment', type=str, action='store', help=f'Environment', default=None)
    parser.add_argument('--config', type=str, action='store', help=f'Build configuration (default: {default_config})', default=default_config)
    parser.add_argument('--list-configs', action='store_true', help='List available build configurations.')

    parser.add_argument('-l', '--list', action='store_true', help='List available tests')
    parser.add_argument('-t', '--tags', type=str, action='store', help='Comma separated list of tags for filtering tests to run', default='default')
    parser.add_argument('-f', '--filter', type=str, action='store', help='Regular expression for filtering tests to run')
    parser.add_argument('-x', '--xml-report', type=str, action='store', help='XML report output file')
    parser.add_argument('-b', '--ref-branch', help='Reference branch to compare against (defaults to master branch)', default='master')
    parser.add_argument('--run-only', action='store_true', help='Run tests without comparing images')
    parser.add_argument('--compare-only', action='store_true', help='Compare previous results against references without generating new images')
    parser.add_argument('--tolerance', type=float, action='store', help='Override tolerance to be at least this value.', default=config.DEFAULT_TOLERANCE)
    parser.add_argument('--parallel', type=int, action='store', help='Set the number of Mogwai processes to be used in parallel', default=default_processes_count)
    parser.add_argument('--gen-refs', action='store_true', help='Generate reference images instead of running tests')

    additional_group = parser.add_argument_group('extended arguments ', 'Additional options used for testing pipelines on TeamCity.')
    additional_group.add_argument('--pull-refs', action='store_true', help='Pull reference images from remote before running tests')
    additional_group.add_argument('--push-refs', action='store_true', help='Push reference images to remote after generating them')
    additional_group.add_argument('--build-id', action='store', help='TeamCity build ID', default='unknown')

    args = parser.parse_args()

    # Try to load environment.
    env = None
    try:
        env = Environment(args.environment, args.config)
    except Exception as e:
        env_error = e

    # List build configurations.
    if args.list_configs:
        print('Available build configurations:\n' + '\n'.join(config.BUILD_CONFIGS.keys()))
        sys.exit(0)

    # Abort if environment is missing.
    if env == None:
        print(f"\nFailed to load environment: {env_error}")
        sys.exit(1)

    # Collect tests to run.
    tests = collect_tests(env.image_tests_dir, args.filter, args.tags)

    # The number of processes on Windows is 61, which should be enough for anything, so just hard coding it capped here
    args.parallel = min(args.parallel, 61)
    process_controller = ProcessController(args.parallel)

    if args.list:
        # List available tests.
        list_tests(tests)
    elif args.gen_refs:
        # Generate references.
        ref_dir = env.resolve_image_dir(env.image_tests_ref_dir, env.branch, args.build_id)
        if not generate_refs(env, tests, ref_dir, process_controller):
            sys.exit(1)

        # Push references to remote.
        if args.push_refs:
            if not env.image_tests_remote_ref_dir:
                print("Remote reference directory is not configured for this environment.")
                sys.exit(1)
            remote_ref_dir = env.resolve_image_dir(env.image_tests_remote_ref_dir, env.branch, args.build_id)
            if not push_refs(ref_dir, remote_ref_dir):
                sys.exit(1)
    else:
        # Determine result and reference directories.
        result_dir = env.resolve_image_dir(env.image_tests_result_dir, env.branch, args.build_id)
        ref_dir = env.resolve_image_dir(env.image_tests_ref_dir, args.ref_branch, args.build_id)

        # Pull references from remote.
        if args.pull_refs:
            if not env.image_tests_remote_ref_dir:
                print("Remote reference directory is not configured for this environment.")
                sys.exit(1)
            remote_ref_dir = env.resolve_image_dir(env.image_tests_remote_ref_dir, args.ref_branch, args.build_id)
            if not pull_refs(remote_ref_dir, ref_dir):
                sys.exit(1)

        # Give some instructions on how to acquire reference images if not available.
        if not args.run_only and not ref_dir.exists():
            print(ref_dir)
            print(colored(f'\n!!! Reference images for "{args.ref_branch}" branch are not available !!!', 'red'))
            print('')
            print(f'You have the following options:')
            print('')
            print(f'  1. Checkout "{args.ref_branch}" branch and generate reference images using:')
            print('')
            print(f'       run_image_tests --gen-refs')
            print('')
            print(f'  2. Use references from a different branch using:')
            print('')
            print(f'       run_image_tests --ref-branch BRANCH')
            print('')
            sys.exit(1)

        # Run tests.
        if not run_tests(env, tests, args.run_only, args.compare_only, ref_dir, result_dir, args.tolerance, args.xml_report, process_controller):
            sys.exit(1)

    sys.exit(0)

if __name__ == '__main__':
    main()


#!/usr/bin/env python

from PIL import Image
import requests
from io import BytesIO
import pytesseract
from pytesseract import Output
import argparse
import json

def canvases(manifest, sequence=0):
    if 'type' in manifest:
        return manifest['items']
    else:
        return manifest["sequences"][sequence]["canvases"]

def get_service(canvas):
    if 'id' in canvas:
        return canvas['items'][0]['items'][0]['body']['service']
    else:
        return canvas['images'][0]['resource']['service']

def buildIIIFImage(service):
     if '@id' in service:
        return f"{service['@id']}/full/full/0/default.jpg"
     else:
        return f"{service[0]['id']}/full/max/0/default.jpg"

def buildAnno(canvas, ident, x, y, width, height, content):
    if 'id' in canvas:
        return {
            "id": ident,
            "type": "Annotation",
            "motivation": "commenting",
            "body": {
                "type": "TextualBody",
                "format": "text/plain",
                "value": content
            },
            "target": f"{canvas['id']}#xywh={x},{y},{width},{height}"
        }
    else:
        return  {
            "@id": ident,
            "@type": "oa:Annotation",
            "motivation": "commenting",
            "resource": {
                "@type": "cnt:ContentAsText",
                "format": "text/plain",
                "chars": content
            },
            "on": f"{canvas['@id']}#xywh={x},{y},{width},{height}"
        }

def mkannotations(canvas, ident, annotations):
    if 'id' in canvas:
        return  {
            "@context": "http://iiif.io/api/presentation/3/context.json",
            "id": ident,
            "type": "AnnotationPage",
            "items": annotations
        }
    else:
        return {
            "@context": "http://iiif.io/api/presentation/3/context.json",
            "@id": ident,
            "@type": "sc:AnnotationList",
            "resources": annotations
        }
def addAnnotations(canvas, ident):
    if 'id' in canvas:
        canvas["annotations"] = [
            {
                "id": ident,
                "type": "AnnotationPage",
                "label": "Annotations generated by Tesseract"
            }
        ]
    else:
        canvas["otherContent"] = [
            {
                "@id": ident,
                "@type": "sc:AnnotationList",
                "label": "Annotations generated by Tesseract"
            }
        ]
def ocr(anno_uri, outputDir, manifestURI, lang=None, confidence=False):
    # Data is either a string or parsed JSON
    print (f'Loading: {manifestURI}')
    manifest = requests.get(manifestURI).json()

    canvasNo = 0
    for canvas in canvases(manifest):
        service = get_service(canvas)

        url = buildIIIFImage(service)
        print (f'Downloading {url}')
        response = requests.get(url)
        img = Image.open(BytesIO(response.content))

        print ('Running OCR')
        if lang:
            data = pytesseract.image_to_data(img, output_type=Output.DICT, lang=lang)
        else:
            data = pytesseract.image_to_data(img, output_type=Output.DICT)
        annos = []
        annoNo = 0
        for i in range(len(data['text'])):
            if data['conf'][i] >= 0:
                (x, y, width, height) = (data['left'][i], data['top'][i], data['width'][i], data['height'][i])
                ident = f"{anno_uri}/{canvasNo}/annotation/{annoNo}"
                if confidence:
                    content = f"Confidence: {data['conf'][i]}: {data['text'][i]}"
                else:
                    content = f"{data['text'][i]}"

                annos.append(buildAnno(canvas, ident, x, y, width, height, content))

                annoNo += 1

        annotationsID = f"{anno_uri}/{canvasNo}.json"
        annotations = mkannotations(canvas,annotationsID, annos)

        print (f"Saving {outputDir}/{canvasNo}.json")
        with open(f'{outputDir}/{canvasNo}.json', 'w') as f:
            json.dump(annotations, f, indent=4)

        addAnnotations(canvas, annotationsID)
        canvasNo += 1

    with open(f'{outputDir}/manifest.json', 'w') as f:
        json.dump(manifest, f, indent=4)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
                    prog='ocr.py',
                    description='Read a manifest, OCR all the pages then adds the results as annotation lists')

    parser.add_argument('manifest', help="URL to Manifest file")
    parser.add_argument('output', help='Output directory for annotation lists')
    parser.add_argument('--base-output-uri', dest='outputURI', default='http://localhost/annos', help='Output URI for annotations')
    parser.add_argument('--lang', dest='lang', default=None, help='Language to pass to the OCR engine see: https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html')
    parser.add_argument('-c', '--confidence', dest='confidence', action='store_true', default=False, help='Include OCR confidence value in text of the annotation? ')
    args = parser.parse_args()

    ocr(args.outputURI, args.output, args.manifest, args.lang, args.confidence)


# Visualization

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from seaborn import color_palette, diverging_palette
from datetime import datetime
from functools import wraps
import os
from scipy.cluster.hierarchy import dendrogram
from utils.tools import *
import time
from definitions import ROOT_DIR

plt.rcParams['mathtext.fontset'] = 'cm'
plt.rcParams['mathtext.rm'] = 'cm'
style_path = os.path.join(ROOT_DIR, 'src', 'utils', 'visualization', 'BystrickyK.mplstyle')
print(style_path)
plt.style.use({'seaborn', style_path})

def set_save_plot_options(save=False,
                          add_stamp=True, plot=True, format='.svg', dpi=300):

    path = os.path.join('.', 'images')

    file_path = path + os.sep
    if add_stamp:
        now = datetime.now()
        now = now.strftime("%d-%m-%Y_%H-%M")
        file_path = file_path + '_' + now

    if not os.path.exists(file_path):
        print(f"Path {file_path} doesn't exist. Attempting dir creation.")
        try:
            os.mkdir(file_path)
        except OSError:
            print(f"Creation of the directory {file_path} failed")
        else:
            print(f"Successfully created the directory {file_path}")
            path = file_path + os.sep
    else:
        print(f"Folder {file_path} already exists")
        path = file_path + os.sep

    def save_and_plot(save=save, plot=plot, filename='default', stamp=False):

        def decorator(fun):

            @wraps(fun)
            def wrapper(*args, **kwargs):
                fig = fun(*args, **kwargs)
                if save:
                    if stamp:
                        rand_stamp = str(time.time()).replace('.','_')
                        save_path = path + rand_stamp + '_' + filename + format
                    else:
                        save_path = path + filename + format
                    plt.savefig(save_path, dpi=dpi)

                if not plot:
                    plt.close(fig)

                return fig

            return wrapper

        return decorator

    return save_and_plot

####################################################################################
####################################################################################
save_and_plot = set_save_plot_options(save=True, plot=False,
                                      add_stamp=True, format='.svg', dpi=180)
####################################################################################
####################################################################################

@save_and_plot(filename='vector', stamp=True)
def plot_tvector(t, X, var_name='x', title=None):
    dims = X.shape[1]

    colors = color_palette('dark')
    fig, axs = plt.subplots(dims, 1, figsize=(8, 8), tight_layout=True, sharex=True)
    if title == None:
        pass
    else:
        fig.suptitle(title)
    for ii, ax in enumerate(axs):
        ax.plot(t, X[:, ii], color=colors[ii])
        ylabel_str = rf'$ {var_name}_{ii+1} (t) $'
        ax.set_ylabel(ylabel_str)
    ax.set_xlabel(rf'$ Time \  t \  [s] $')
    plt.show()

def plot_ksi(ksi, theta, dx, ax, show_sparse=True, show_sparse_tol=0.1):
    ksi = ksi.T  # fix the ksi output shape
    if show_sparse:
        idx_dim_active = np.apply_along_axis(lambda x: sum(abs(x)) > show_sparse_tol, 1, ksi)
        ksi = ksi[idx_dim_active, :]
        theta = theta.iloc[:, idx_dim_active]

    colormap = color_palette('coolwarm', as_cmap=True)

    thetastr = parse_function_strings(theta.columns)
    dxstr = parse_function_strings(dx.columns)
    dxstr = parse_function_str_add_dots(dxstr)

    # with plt.style.context({'seaborn', './images/BystrickyK.mplstyle'}):
    ax.matshow(ksi, cmap=colormap, vmin=-2, vmax=2)
    # plt.colorbar(p, ax=ax)
    ax.set_yticks([*range(min(theta.shape))])
    ax.set_yticklabels(thetastr)
    ax.yaxis.set_tick_params(rotation=30, labelsize=10)
    ax.set_xticks([*range(min(dx.shape))])
    ax.set_xticklabels(dxstr)
    ax.xaxis.set_tick_params(rotation=45, labelsize=10)
    ax.xaxis.set_tick_params(labelsize=15)
    for (col, row), val in np.ndenumerate(ksi):
        ax.text(row, col, '{:0.2f}'.format(val), ha='center', va='center',
                bbox=dict(boxstyle='round', facecolor='white', edgecolor='0.3'))

@save_and_plot(filename='corr', stamp=True, plot=True)
def plot_corr(corr, regressor_names, labels=True, ticks=True):

    # labelstr = parse_function_strings(regressor_names)
    # labelstr = parse_function_str_add_dots(regressor_names)
    labelstr = regressor_names
    labelstr = np.array(d_to_dot(latexify(labelstr)))

    figsize = tuple(np.array(corr.shape) * 0.15 + 5)
    if figsize[0] > 70:
        figsize = tuple(np.array(figsize) * 0.25)
    print(f"Creating correlation matrix plot\nImage size:\t{figsize}")

    fig, ax = plt.subplots(1, 1,
                           figsize=figsize, tight_layout=True)
    # with plt.style.context({'seaborn', './images/BystrickyK.mplstyle'}):
    im = ax.matshow(corr, cmap='viridis', vmin=-1, vmax=1)
    if ticks:
        ax.set_yticks([*range(min(labelstr.shape))])
        ax.set_yticklabels(labelstr)
        ax.yaxis.set_tick_params(rotation=0, labelsize=12)
        ax.set_xticks([*range(min(labelstr.shape))])
        ax.set_xticklabels(labelstr)
        ax.xaxis.set_tick_params(rotation=90, labelsize=12)
    fig.colorbar(im, ax=ax, orientation='horizontal', fraction=0.046, pad=0.04, shrink=0.5)
    if labels:
        for (col, row), val in np.ndenumerate(corr):
            ax.text(row, col, '{:0.2f}'.format(val), ha='center', va='center',
                    bbox=dict(boxstyle='round', alpha=0.5, facecolor='white', edgecolor='0.3'))

@save_and_plot(filename='activation_dist', stamp=True, plot=True)
def plot_activation_dist_mat(dist_mat, lhs_guess_strings, labels=True):

    # labelstr = parse_function_strings(regressor_names)
    # labelstr = parse_function_str_add_dots(regressor_names)
    labelstr = enumerate(lhs_guess_strings)
    labelstr = ['  |  i:'.join([lhs, str(idx)]) for idx,lhs in labelstr]
    n = len(labelstr)

    figsize = tuple(np.array(dist_mat.shape) * 0.15 + 5)
    if figsize[0] > 70:
        figsize = tuple(np.array(figsize) * 0.15)
        labels = False
    print(f"Creating activation distance plot\nImage size:\t{figsize}")

    # with plt.style.context({'seaborn', './images/BystrickyK.mplstyle'}):
    newcolors = mpl.cm.get_cmap('cividis_r', 4)
    fig, ax = plt.subplots(nrows=1, ncols=1,
                           tight_layout=True,
                           figsize=figsize)
    im = ax.imshow(dist_mat, cmap=newcolors, vmin=3.5, vmax=-0.5)
    fig.colorbar(im, ax=ax, ticks=[*range(0, 4)])
    if labels:
        ax.set_yticks([*range(n)])
        ax.set_yticklabels(labelstr)
        ax.yaxis.set_tick_params(rotation=0, labelsize=7)
        ax.set_xticks([*range(n)])
        ax.set_xticklabels(labelstr)
        ax.xaxis.set_tick_params(rotation=90, labelsize=7)

@save_and_plot(filename='implicit_sols', stamp=True, plot=True)
def plot_implicit_sols(models, theta_labels,
                       show_labels=False, axislabels=True,
                       aic=None):

    sols = np.vstack(models['xi'].values)
    sols = np.array(sols).T

    theta_active = np.vstack(models['active'].values).any(axis=0)
    theta_labels = theta_labels[theta_active]
    theta_labels = d_to_dot(latexify(theta_labels))
    sols = sols[theta_active, :]

    train_rmse = models['train_metric']

    guess_function_str = models['guess_function_string'].values
    guess_function_str = latexify(guess_function_str)
    guess_function_str = d_to_dot(guess_function_str)
    indices = range(len(guess_function_str))

    train_rmse = [str(np.round(m, 2)) for m in train_rmse]
    if aic is None:
        y_labels = [r' | '.join([str(idx), guess, rmse,
                                   ]) for
                      idx, guess, rmse
                      in zip(indices, guess_function_str, train_rmse)]
    else:
        val_aic = models['rmse']
        val_aic = [str(np.round(m, 2)) for m in val_aic]
        y_labels = [r' | '.join([str(idx), guess, rmse, aic,
                                 ]) for
                    idx, guess, rmse, aic
                    in zip(indices, guess_function_str, train_rmse, val_aic)]

    figsize = tuple(np.array(sols.shape) * 0.25 + 4)
    if figsize[1] > 70:
        figsize = np.array(figsize)
        figsize[1] = figsize[1] * 0.5
        figsize = tuple(figsize)
        axislabels = False
    if show_labels:
        figsize = tuple(np.array(figsize)+2)
    print(f"Creating implicit solutions plot\nImage size:\t{figsize}")

    colormap = color_palette('coolwarm', as_cmap=True)

    fig, ax = plt.subplots(1, 1,
                           figsize=figsize, tight_layout=True)
    # with plt.style.context({'seaborn', './images/BystrickyK.mplstyle'}):
    ax.matshow(sols.T, cmap=colormap, vmin=-0.0000001, vmax=0.0000001)
    if axislabels:
        ax.set_yticks([*range(sols.shape[1])])
        ax.set_yticklabels(y_labels)
        ax.yaxis.set_tick_params(rotation=0, labelsize=15)
        ax.set_xticks([*range(len(theta_labels))])
        ax.set_xticklabels(theta_labels)
        ax.xaxis.set_tick_params(rotation=90, labelsize=15)
    if show_labels:
        for (row, col), val in np.ndenumerate(sols):
            ax.text(row, col, '{:0.2f}'.format(val), ha='center', va='center',
                    bbox=dict(boxstyle='round', facecolor='white', edgecolor='0.3',
                              alpha=0.7))

@save_and_plot(filename='implicit_sols', stamp=True, plot=True)
def pareto_front(models, use_train=True, title='title'):

    complexities = [np.sum(active) for active in models['active']]
    val = models['validation_metric']
    train = models['train_metric']

    frontier_error = dict(zip(complexities, val))  # Initialize
    for comp, err in zip(complexities, val):
        if frontier_error[comp] > err:
            frontier_error[comp] = err

    par_x = np.array([*frontier_error.keys()])
    par_y = np.array([*frontier_error.values()])
    idx = np.argsort(par_x)
    par_x = par_x[idx]
    par_y = par_y[idx]

    par_x, par_y = list(par_x), list(par_y)
    len_parx = len(par_x)
    for i in range(len_parx):
        diffs = np.diff(par_y)
        remove_idx = np.argwhere(diffs>0)
        if remove_idx.shape[0]>0:
            remove_idx = remove_idx[0][0] + 1
            par_x.pop(remove_idx)
            par_y.pop(remove_idx)
        else:
            break

    print(f"Creating Pareto front plot")

    fig, ax = plt.subplots(1, 1,
                           figsize=(6,4), tight_layout=True)
    if use_train:
        ax.plot(complexities, train, marker='.', markersize=10, alpha=0.7, linestyle='none')
    else:
        ax.plot(complexities, val, marker='.', markersize=10, alpha=0.7, linestyle='none')
    ax.plot(par_x, par_y, marker='x', alpha=0.7, markersize=10, color='r',
            linestyle='-.', linewidth=2)
    ax.set_xlabel('Model complexity [# of terms]')
    ax.set_ylabel('Normalized validation error [-]')
    if use_train:
        ax.legend(['Train RMSE', 'Pareto frontier'])
    else:
        ax.legend(['Val RMSE', 'Pareto frontier'])
    ax.set_title(title)

    return fig, ax

        # if show_labels:
        #     for (row, col), val in np.ndenumerate(sols):
        #         ax.text(row, col, '{:0.2f}'.format(val), ha='center', va='center',
        #                 bbox=dict(boxstyle='round', facecolor='white', edgecolor='0.3',
        #                           alpha=0.7))

@save_and_plot(filename='ksi', stamp=True, plot=True)
def plot_ksi_fig(ksi, theta, dx, show_sparse=True, title=None):
    fig = plt.figure(tight_layout=True, figsize=(4, 8))
    if title == None:
        pass
    else:
        plt.suptitle(title)
    ax = plt.gca()
    plot_ksi(ksi, theta, dx, ax, show_sparse)
    return fig

@save_and_plot(filename='ksi_comp')
def compare_ksi(ksi1, theta1, ksi2, theta2, dx, show_sparse=True):
    fig, ax = plt.subplots(1, 2, tight_layout=True)
    plot_ksi(ksi1, theta1, dx, ax[0], show_sparse=show_sparse)
    plot_ksi(ksi2, theta2, dx, ax[1], show_sparse=show_sparse)
    return fig

@save_and_plot(filename='ksi_comp', plot=True)
def plot_svd(svd):
    dims = svd['U'].shape[1]
    fig, axs = plt.subplots(1, 2)
    fig.set_size_inches(10, 6)

    p0 = axs[0].matshow(svd['U'], cmap='cividis')
    fig.colorbar(p0, ax=axs[0], fraction=0.046, pad=0.04, shrink=0.5)
    axs[0].set_xticks([*range(0, dims)])
    axs[0].set_xticklabels(['PC' + str(i + 1) for i in range(dims)])
    axs[0].set_yticks([*range(0, dims)])
    axs[0].set_yticklabels(['X' + str(i + 1) for i in range(dims)])
    axs[0].set_title("Left Singular Vectors\nPrincipal Components")

    p1 = axs[1].matshow(svd['Sigma'], cmap='viridis',
                        norm=mpl.colors.LogNorm(vmin=0.1,
                                         vmax=np.max(svd['Sigma'])))
    fig.colorbar(p1, ax=axs[1],
                 fraction=0.046, pad=0.04, shrink=0.5)
    axs[1].set_xticks([*range(0, dims)])
    axs[1].set_xticklabels([str(i + 1) for i in range(dims)])
    axs[1].set_yticks([*range(0, dims)])
    axs[1].set_yticklabels([str(i + 1) for i in range(dims)])
    axs[1].set_title("Singular Values")

    return fig

@save_and_plot(filename='lorentz3d', plot=True)
def plot_lorentz3d(state_data, title=None, **kwargs):
    fig = plt.figure(tight_layout=True, figsize=(9, 8))
    ax = fig.add_subplot(111, projection='3d')
    ax.set_title(title)
    ax.plot3D(state_data[:, 0], state_data[:, 1], state_data[:, 2], **kwargs)
    ax.scatter3D(state_data[[0, -1],0], state_data[[0, -1],1], state_data[[0, -1], 2], s=60, edgecolors='k', linewidths=2)
    ax.set_xlabel(r"$x_1$")
    ax.set_ylabel(r"$x_2$")
    ax.set_zlabel(r"$x_3$")
    return ax

def plot_lorentz3d_ax(state_data, ax, label=None, color='tab:blue', linestyle='-'):
    ax.plot3D(state_data[:, 0],
              state_data[:, 1],
              state_data[:, 2],
              label=label,
              linestyle=linestyle,
              color=color)
    ax.scatter3D(state_data[[0, -1],0],
                 state_data[[0, -1],1],
                 state_data[[0, -1], 2],
                 s=60, edgecolors=color, linewidths=2)
    ax.set_xlabel(r"$\dot{x}_1$")
    ax.set_ylabel(r"$x_2$")
    ax.set_zlabel(r"$x_3$")

# Plot analytic and spectral derivatives
@save_and_plot(filename='derivatives', plot=True)
def plot_dxdt_comparison(sig):
    dims = sig.dims
    t = sig.t
    dxdt_exact = sig.dxdt_exact.values
    dxdt_spectral = sig.dxdt_spectral.values
    if sig.kernel:
        dxdt_spectral_filtered = sig.dxdt_spectral_filtered.values
    dxdt_findiff = sig.dxdt_finitediff.values

    axmax = np.max(dxdt_exact, axis=0)
    axmin = np.min(dxdt_exact, axis=0)
    print(axmax)

    colors = color_palette('deep')
    # with plt.style.context({'seaborn', './images/BystrickyK.mplstyle'}):
    fig, axs = plt.subplots(dims, 1, figsize=(12, 8), sharex='all', tight_layout=True)
    fig.suptitle('')
    plt.xlabel(r'$Time \ t \  [s]$')
    ylabels = [r'$\.{x}_{' + str(i + 1) + '} [s^{-1}]$' for i in range(sig.dims)]
    for ii, ax in enumerate(axs):
        ax.plot(t, dxdt_spectral[:, ii], '-', color=colors[1], alpha=0.2, linewidth=1, label='Spectral Cutoff')
        # ax.plot(t, dxdt_findiff[:, ii], '-', color=colors[2], alpha=0.2, linewidth=1, label='Forward Finite Difference')
        if sig.kernel:
            ax.plot(t, dxdt_spectral_filtered[:, ii], '-', color=colors[3], alpha=0.8, linewidth=1.5,
                    label='Spectral Cutoff Filtered')
        ax.plot(t, dxdt_exact[:, ii], color='black', alpha=1, linewidth=2, label='Exact')
        ax.set_ylabel(ylabels[ii])
        ax.set_xlim(xmin=-0.1, xmax=5)
        ax.set_ylim(ymin=axmin[ii]*1.1, ymax=axmax[ii]*1.1)
    axs[0].legend(loc=1, bbox_to_anchor=(1.28, 1))
    return fig

def plot_dendrogram(model, **kwargs):
    # Create linkage matrix and then plot the dendrogram

    # create the counts of samples under each node
    counts = np.zeros(model.children_.shape[0])
    n_samples = len(model.labels_)
    for i, merge in enumerate(model.children_):
        current_count = 0
        for child_idx in merge:
            if child_idx < n_samples:
                current_count += 1  # leaf node
            else:
                current_count += counts[child_idx - n_samples]
        counts[i] = current_count

    linkage_matrix = np.column_stack([model.children_, model.distances_,
                                      counts]).astype(float)

    # Plot the corresponding dendrogram
    dendrogram(linkage_matrix, **kwargs)

def plot_signals(data1, ylabels, title_str=None, k=0):

    cols = data1.shape[1]

    data1 = np.array(data1)

    fig, axs = plt.subplots(nrows=cols, tight_layout=True, sharex=True, figsize=(12, 5))
    axs[0].set_title(title_str)
    for col in range(cols):
        axs[col].plot(k+np.array(range(len(data1))), data1[:, col],
                      linewidth=2, alpha=0.75, color='tab:blue')
        axs[col].set_ylabel(ylabels[col], fontsize=18)
    axs[cols-1].set_xlabel('Sample index $k$', fontsize=18)

def compare_signals(data1, data2, legend_str, ylabels, title_str=None, k=0):

    cols = data1.shape[1]

    data1 = np.array(data1)
    data2 = np.array(data2)

    fig, axs = plt.subplots(nrows=cols, tight_layout=True, sharex=True, figsize=(12, 8))
    if not hasattr(axs, '__iter__'):
        axs = [axs]
    axs[0].set_title(title_str)
    for col in range(cols):
        axs[col].plot(k+np.array(range(len(data1))), data1[:, col],
                      linewidth=4, alpha=0.75, color='tab:blue')
        axs[col].plot(k+np.array(range(len(data2))), data2[:, col],
                      linewidth=2, alpha=0.8, color='tab:red', linestyle='--')
        axs[col].set_ylabel(ylabels[col], fontsize=20)
        axs[col].legend(legend_str, fontsize=16)
    axs[cols-1].set_xlabel('Sample index $k$', fontsize=20)

def compare_signals3(data1, data2, data3, legend_str, ylabels, title_str=None, k=0):

    cols = data1.shape[1]

    data1 = np.array(data1)
    data2 = np.array(data2)
    data3 = np.array(data3)

    fig, axs = plt.subplots(nrows=cols, tight_layout=True, sharex=True, figsize=(10, 6))
    if not hasattr(axs, '__iter__'):
        axs = [axs]
    axs[0].set_title(title_str)
    for col in range(cols):
        axs[col].plot(k+np.array(range(len(data1))), data1[:, col], linewidth=3, alpha=0.75, color='tab:grey')
        axs[col].plot(k+np.array(range(len(data2))), data2[:, col], '--', linewidth=1.5, alpha=0.75, color='tab:red')
        axs[col].plot(k+np.array(range(len(data3))), data3[:, col], linewidth=1.5, alpha=0.75, color='tab:blue')
        axs[col].set_ylabel(ylabels[col])
        axs[col].legend(legend_str)
    axs[-1].set_xlabel('Sample index $k$')

@save_and_plot(filename='lorentz3d', plot=True)
def plot_lorentz3d(state_data, title=None, **kwargs):
    fig = plt.figure(tight_layout=True, figsize=(9, 8))
    ax = fig.add_subplot(111, projection='3d')
    ax.set_title(title)
    ax.plot3D(state_data[:, 0], state_data[:, 1], state_data[:, 2], **kwargs)
    ax.scatter3D(state_data[[0, -1],0], state_data[[0, -1],1], state_data[[0, -1], 2], s=60, edgecolors='k', linewidths=2)
    ax.set_xlabel(r"$x_1$")
    ax.set_ylabel(r"$x_2$")
    ax.set_zlabel(r"$x_3$")
    return ax

# @save_and_plot(filename='data', plot=True)
# def plot_measurements(state_data, state_derivative_data,


"""Envoy API annotations."""

import re
from functools import partial

# Key-value annotation regex.
ANNOTATION_REGEX = re.compile('\[#([\w-]+?):\s*(.*?)\](\s?)', re.DOTALL)

# Page/section titles with special prefixes in the proto comments
DOC_TITLE_ANNOTATION = 'protodoc-title'

# Specify the type of extensions at extension points
EXTENSION_CATEGORY_ANNOTATION = 'extension-category'

# When documenting an extension, this should be used to specify the qualified
# name that the extension registers as in the static registry, e.g.
# envoy.filters.network.http_connection_manager.
EXTENSION_ANNOTATION = 'extension'

# Not implemented yet annotation on leading comments, leading to hiding of
# field.
NOT_IMPLEMENTED_HIDE_ANNOTATION = 'not-implemented-hide'

# For large protos, place a comment at the top that specifies the next free field number.
NEXT_FREE_FIELD_ANNOTATION = 'next-free-field'

# Comment that allows for easy searching for things that need cleaning up in the next major
# API version.
NEXT_MAJOR_VERSION_ANNOTATION = 'next-major-version'

# Comment. Just used for adding text that will not go into the docs at all.
COMMENT_ANNOTATION = 'comment'

# Annotation on leading comments that permits fields to be declared with the
# fully-qualified-name (FQN) type.
ALLOW_FULLY_QUALIFIED_NAME_ANNOTATION = 'allow-fully-qualified-name'

VALID_ANNOTATIONS = set([
    DOC_TITLE_ANNOTATION,
    EXTENSION_ANNOTATION,
    EXTENSION_CATEGORY_ANNOTATION,
    NOT_IMPLEMENTED_HIDE_ANNOTATION,
    NEXT_FREE_FIELD_ANNOTATION,
    NEXT_MAJOR_VERSION_ANNOTATION,
    COMMENT_ANNOTATION,
    ALLOW_FULLY_QUALIFIED_NAME_ANNOTATION,
])

# These can propagate from file scope to message/enum scope (and be overridden).
INHERITED_ANNOTATIONS = set([
    # Nothing here right now, this used to be PROTO_STATUS_ANNOTATION. Retaining
    # this capability for potential future use.
])

class AnnotationError(Exception):
    """Base error class for the annotations module."""

def extract_annotations(s, inherited_annotations=None):
    """Extract annotations map from a given comment string.

    Args:
        s: string that may contains annotations.
        inherited_annotations: annotation map from file-level inherited annotations
           (or None) if this is a file-level comment.

    Returns:
        Annotation map.
    """
    annotations = {
        k: v
        for k, v in (inherited_annotations or {}).items()
        if k in INHERITED_ANNOTATIONS
    }
    # Extract annotations.
    groups = re.findall(ANNOTATION_REGEX, s)
    for group in groups:
        annotation = group[0]
        if annotation not in VALID_ANNOTATIONS:
            raise AnnotationError('Unknown annotation: %s' % annotation)
        annotations[group[0]] = group[1].lstrip()
    return annotations

def append(s, annotation, content):
    return '%s[#%s: %s]\n' % (s, annotation, content)

def xform_sub(annotation_xforms, present_annotations, match):
    annotation, content, trailing = match.groups()
    present_annotations.add(annotation)
    annotation_xform = annotation_xforms.get(annotation)
    if annotation_xform:
        value = annotation_xform(annotation)
        return '[#%s: %s]%s' % (annotation, value, trailing) if value is not None else ''
    else:
        return match.group(0)

def xform_annotation(s, annotation_xforms):
    """Return transformed string with annotation transformers.

    The annotation will be replaced with the new value returned by the transformer.
    If the transformer returns None, then the annotation will be removed.
    If the annotation presented in transformers doesn't exist in the original string,
    a new annotation will be appended to the end of string.

    Args:
        annotation_xforms: a dict of transformers for annotations.

    Returns:
        transformed string.
    """
    present_annotations = set()

    xformed = ANNOTATION_REGEX.sub(partial(xform_sub, annotation_xforms, present_annotations), s)
    for annotation, xform in sorted(annotation_xforms.items()):
        if annotation not in present_annotations:
            value = xform(None)
            if value is not None:
                xformed = append(xformed, annotation, value)
    return xformed

def without_annotations(s):
    return re.sub(ANNOTATION_REGEX, '', s)


import numpy as np
import io
import matplotlib.pyplot as plt
import tensorflow as tf
import scipy
from os.path import join, isdir
from gammatone import plot
from sklearn.metrics import roc_curve, roc_auc_score, precision_recall_curve
import json
from os import mkdir, makedirs

class NpEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        if isinstance(obj, np.floating):
            # 👇 alternatively use str()
            return float(obj)
        if isinstance(obj, np.ndarray):
            return obj.tolist()
        return json.JSONEncoder.default(self, obj)

class Postprocessor():
    def __init__(self, cfg) -> None:
        self.cfg = cfg

    def plot_loss_hist(self, loss, legend):
        figure = plt.figure(figsize=(4,3))
        plt.hist(loss, bins=30)
        plt.xlabel("MSE")
        plt.ylabel("Number of samples")
        plt.title(f'{legend} loss distribution')
        plt.tight_layout()
        return figure

    def plot_roc_curve(self, predictions, true_labels):
        figure = plt.figure(figsize=(4,3))
        fpr, tpr, _ = roc_curve(true_labels, predictions)
        plt.plot(fpr, tpr, marker='.')
        # axis labels
        plt.xlabel('False Positive Rate')
        plt.ylabel('True Positive Rate')
        # show the legend
        plt.title('ROC Curve')
        plt.grid()
        plt.tight_layout()
        return figure

    def plot_pr_curve(self, predictions, true_labels):
        figure = plt.figure(figsize=(4,3))
        precision, recall, thresholds = precision_recall_curve(true_labels, predictions)
        plt.plot(recall, precision, marker='.')
        # axis labels
        plt.xlabel('Recall')
        plt.ylabel('Prescision')
        # show the legend
        plt.title('Precision-Recall Curve')
        plt.grid()
        plt.tight_layout()
        return figure

    def save_threshold(self,predictions, true_labels, max, min, path_to_save):
        path = path_to_save
        if not isdir(path):
            makedirs(path)

        precision, recall, thresholds = precision_recall_curve(true_labels, predictions)
        threshold = thresholds[np.argmax(precision+recall)]
        # print("MIN",min)
        # print("MAX",max)
        metrics = {
            # 'auc': auc,
            # 'auc_0.1': auc_1,
            # 'fpr': fpr.tolist(),
            # 'tpr': tpr.tolist(),
            'precision': precision.tolist(),
            'recall': recall.tolist(),
            'threshold': threshold,
            'max': max,
            'min': min
        }
        # print(metrics)
        with open(join(path, 'metrics_detail.json'), 'w') as file:
            json.dump(metrics, file, indent=4, cls=NpEncoder)

        print('Done!!')
        return 0

    def plot_to_image(self, figure):
        """
            Converts the matplotlib plot specified by 'figure' to a PNG image and
            returns it. The supplied figure is closed and inaccessible after this call.
        """
        # Save the plot to a PNG in memory.
        buf = io.BytesIO()
        plt.savefig(buf, format='png')
        # Closing the figure prevents it from being displayed directly inside
        # the notebook.
        plt.close(figure)
        buf.seek(0)
        # Convert PNG buffer to TF image
        image = tf.image.decode_png(buf.getvalue(), channels=4)
        # Add the batch dimension
        image = tf.expand_dims(image, 0)
        return image

    def _plot_mel(self, input_feature, idx):
        duration = self.cfg
        aspect_ratio = duration/scipy.constants.golden
        # may need to return Figure type
        figure, ax = plt.subplots(figsize=(4,3))
        formatter = plot.ERBFormatter(f_min, f/2, unit='Hz', places=0)
        ax.yaxis.set_major_formatter(formatter)
        ax.set_title(idx)
        ax.xaxis.label.set_text('Time (s)')
        ax.yaxis.label.set_text('Freq (hz)')
        im = ax.imshow(input_feature, extent=[0,duration,1,0], aspect=aspect_ratio)
        plt.tight_layout()

        return figure

    def _plot_gamma(self):
        pass
    def evaluate(self):
        pass


import sys
import math
from collections import deque
sys.stdin = open('pg기능개발_in.txt', 'r')

# math.ceil(-3.14)
# progresses = [93, 30, 55]
# speeds = [1, 30, 5]

progresses = [95, 90, 99, 99, 80, 99]
speeds = [1, 1, 1, 1, 1, 1]

def solution(progresses, speeds):
    answer = []
    days = []
    completion = 100

    for i in range(len(progresses)):
        remain = completion - progresses[i]
        day = math.ceil(remain/speeds[i])
        days.append(day)

    deq = deque(days)

    while len(deq):
        value = deq.popleft()
        count = 1
        if len(deq) == 0:
            answer.append(count)
            break
        else:
            while value >= deq[0]:
                deq.popleft()
                count += 1
                if len(deq) == 0:
                    break

        answer.append(count)

    return answer

print(solution(progresses, speeds))


# -*- coding: utf-8 -*-
"""
Created on Fri Jul 17 2020

@author: Sam Ehrenstein
"""

import matplotlib
matplotlib.use('Agg')

import torch
import torch.nn as nn
from torch.autograd import Variable

import torch.utils.data as data

import sys
# sys.path.append('../')
import os

from DataSet import BigImageDataset
from UGAN import UGenerator, UDiscriminator
from CORONA.classes.Dataset import Converter
from CORONA.classes.Player import Player

import numpy as np
import matplotlib.pyplot as plt
import time
import datetime
import pickle
from tqdm import tqdm
import yaml

def to_var(x):
    if torch.cuda.is_available():
        x = x.cuda()
    return Variable(x)

if __name__=='__main__':
    """Settings"""
    """========================================================================="""
    #Name and choice of training set
    ProjectName='gan'
    prefix='sim' #invivo,sim_pm,sim
    #Load model

    """Network Settings: Remember to change the parameters when you change model!"""
    gpu=True #if gpu=True, the ResNet will use more parameters
    #Whether to plot predictions during training and frequency
    plot=True
    plotT=1
    if not plot:
        plt.ioff()
    #seed
    seed=1237
    torch.manual_seed(seed)
    #parameters for training
    TrainInstances = 6000 # Size of training dataset
    ValInstances   = 800
    BatchSize      = 40
    ValBatchSize   = 40
    num_epochs     = 30
    m, n, p = (39,39,20)
    frame=10
    #directory of datasets
    d_invivo='/data/Invivo/'
    d_simpm='/data/Sim_PM/'

    # Load settings from config file
    cfg = yaml.load(open('/data/resnet.yaml'))
    d_sim = cfg['datadir']
    loadmodel = cfg['loadmodel']
    if loadmodel=='False':
        loadmodel=False
    lr_list = [cfg['lr']]
    debug = cfg['debug']
    if loadmodel:
        gfile = cfg['gfile']
        dfile = cfg['dfile']
    print(loadmodel)
    """========================================================================="""

    #Dataset, converter and player
    data_dir={'invivo':d_invivo,'sim_pm':d_simpm,'sim':d_sim}[prefix]
    conter=Converter()
    player=Player()
    formshow={'pre':'concat','shape':(m,n,p)}
    formlist=[]
    for i in range(6):
        formlist.append(formshow)
    minloss=np.inf
    #Logs
    log=open('/results/%s_%s_GAN_Log_Tr%s_epoch%s_lr%.2e.txt'\
        %(ProjectName,prefix,TrainInstances,num_epochs,lr_list[0]),'a')

    print('Project Name: %s\n'%ProjectName)
    log.write('Project Name: %s\n\n'%ProjectName)
    #Loading data
    print('Loading phase...')
    print('----------------')
    log.write('Loading phase...\n')
    log.write('----------------\n')
    shape_dset=(m,n,p*2)    # The last dimension is 2*the number of frames (for real and imaginary)
    #training
    train_dataset=BigImageDataset(round(TrainInstances),shape_dset,
                            train=0,data_dir=data_dir)
    train_loader=data.DataLoader(train_dataset,batch_size=BatchSize,shuffle=True)
    #validation
    # val_dataset=BigImageDataset(round(ValInstances),shape_dset,
    #                         train=1,data_dir=data_dir, train_size=TrainInstances)
    # val_loader=data.DataLoader(val_dataset,batch_size=ValBatchSize,shuffle=True)
    # print('Finished loading.\n')
    # log.write('Finished loading.\n\n')

    # Training
    for learning_rate in lr_list:
        #Construct network
        print('Configuring network...')
        log.write('Configuring network...\n')
        if not loadmodel:
            generator = UGenerator(gpu)
            discriminator = UDiscriminator((m, n, p), gpu)
        else:
            if gfile[-3:]=='pkl':
                generator = UGenerator(gpu)
                discriminator = UDiscriminator((m, n, p), gpu)
                g_state_dict=torch.load(gfile, map_location='cuda:0')
                d_state_dict=torch.load(dfile, map_location='cuda:0')
                generator.load_state_dict(g_state_dict)
                discriminator.load_state_dict(d_state_dict)
            else:
                generator = torch.load(gfile)
                discriminator = torch.load(dfile)

        if torch.cuda.is_available():
            generator = generator.cuda()
            discriminator = discriminator.cuda()

        #Loss and optimizer
        adv_loss=nn.BCELoss()
        if torch.cuda.is_available():
            adv_loss = adv_loss.cuda()

        g_optimizer=torch.optim.Adam(generator.parameters(), lr=learning_rate)
        d_optimizer=torch.optim.Adam(discriminator.parameters(), lr=learning_rate)

        #Array for recording data
        outputs_S = to_var(torch.zeros([1,1,m,m,p*2]))
        g_lossmean_vec=np.zeros((num_epochs,))
        g_lossmean_val_vec=np.zeros((num_epochs,))
        d_lossmean_vec=np.zeros((num_epochs,))
        d_lossmean_val_vec=np.zeros((num_epochs,))

        #Training
        print('Training the model over %d samples, with learning rate %.6f\n'\
            %(TrainInstances,learning_rate))
        log.write('Training the model over %d samples, with learning rate %.6f\n\n'\
            %(TrainInstances,learning_rate))
        # Run over each epoch
        for epoch in range(num_epochs):
            #print time
            ts=time.time()
            st=datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
            print('\n'+st)
            log.write('\n'+st+'\n')

            gen_loss_mean = 0
            disc_loss_mean = 0
            # Train
            print('Loading and calculating training batches...')
            log.write('Loading and calculating training batches...\n')
            starttime=time.time()
            # torch.autograd.set_detect_anomaly(True)
            for _,(_,S,D) in tqdm(enumerate(train_loader)):
                valid = to_var(torch.ones(BatchSize, 1))
                valid.requires_grad = False
                fake = to_var(torch.zeros(BatchSize, 1))
                fake.requires_grad = False
                Sg = to_var(S)
                Dg = to_var(D)

                # Train discriminator on real batch
                d_optimizer.zero_grad()
                real_out = discriminator(Sg[None].transpose(0,1))
                real_loss = adv_loss(real_out, valid)

                # Train discriminator on fake batch from generator
                fake_batch = generator(Dg[None].transpose(0,1))
                fake_out = discriminator(fake_batch.detach())
                fake_loss = adv_loss(fake_out, fake)

                # Combine losses and step optimizer
                loss_d = real_loss + fake_loss
                loss_d.backward()
                d_optimizer.step()

                # Now train the generator to fool the discriminator
                g_optimizer.zero_grad()
                gen_out = discriminator(fake_batch)
                gen_loss = adv_loss(gen_out, valid)
                gen_loss.backward()
                g_optimizer.step()

                if debug:
                    print('gen_loss', gen_loss)
                    print('loss_d', loss_d)
                gen_loss_mean += gen_loss.item()
                disc_loss_mean += loss_d.item()

            gen_loss_mean /= (TrainInstances/BatchSize)
            disc_loss_mean /= (TrainInstances/BatchSize)
            endtime=time.time()
            g_lossmean_vec[epoch] = gen_loss_mean
            d_lossmean_vec[epoch] = disc_loss_mean
            print('Training time is %f'%(endtime-starttime))
            log.write('Training time is %f\n'%(endtime-starttime))

            #Save model in each epoch
            print('saved at [epoch%d/%d]'%(epoch+1,num_epochs))
            log.write('saved at [epoch%d/%d]\n'\
                %(epoch+1,num_epochs))
            torch.save(generator.state_dict(),
                    "/results/%s_%s_Gen_Model_Tr%s_epoch%s_lr%.2e.pkl"\
                    %(ProjectName,prefix,TrainInstances,num_epochs,learning_rate))
            torch.save(discriminator.state_dict(),
                    "/results/%s_%s_Dis_Model_Tr%s_epoch%s_lr%.2e.pkl"\
                    %(ProjectName,prefix,TrainInstances,num_epochs,learning_rate))

            # Print loss
            if (epoch + 1)%1==0:    # % 10
                print('Epoch [%d/%d], G lossmean: %.5e, D lossmean: %.5e'\
                    %(epoch+1,num_epochs,gen_loss_mean,disc_loss_mean))

                if gen_loss.item() > 100:
                    print('hitbadrut')
                    log.write('hitbadrut\n')
                    break

            np.savez('/results/%s_%s_GAN_LossData_Tr%s_epoch%s_lr%.2e'\
                %(ProjectName,prefix,TrainInstances,num_epochs,learning_rate),
                g_lossmean_vec, d_lossmean_vec)

        """Save logs, prediction, loss figure, loss data, model and settings """
        #Save settings of training
        params={'ProjectName':ProjectName,
                'prefix':prefix,
                'gfile':gfile if loadmodel else None,
                'dfile':dfile if loadmodel else None,
                'gpu':gpu,
                'data_dir':data_dir,
                'shape':shape_dset,
                'lr_list':lr_list,
                'TrainInstances':TrainInstances,
                'ValInstances':ValInstances,
                'BatchSize':BatchSize,
                'ValBatchSize':ValBatchSize,
                'num_epochs':num_epochs,
                'frame':frame}
        file=open('/results/%s_%s_GAN_Settings_Tr%s_epoch%s_lr%.2e.txt'\
                %(ProjectName,prefix,TrainInstances,num_epochs,learning_rate),'w')
        file.write('Training Settings:\n\t')
        for k,v in params.items():
            file.write(k+'='+str(v)+'\n')
            file.write('\t')
        file.close()


import argparse
import tempfile
from pathlib import Path

import ffmpeg
import librosa
import movis as mv
import numpy as np
import openai
import pandas as pd
import soundfile as sf

def extract(args: argparse.Namespace):
    audio, sr = librosa.load(str(args.input))

    non_silent_slices = librosa.effects.split(
        audio, top_db=args.threshold, frame_length=args.frame_length)
    non_silent_regions = non_silent_slices / sr
    dst_audio = np.concatenate([audio[start:end] for start, end in non_silent_slices])
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir = Path(temp_dir)
        audio_file = temp_dir / 'audio.wav'
        sf.write(str(audio_file), dst_audio, sr)
        ffmpeg.input(str(audio_file)).output(str(args.output), b='128k').run(overwrite_output=True)

    non_silent_frame = pd.DataFrame(non_silent_regions, columns=['start_time', 'end_time'])
    non_silent_frame.to_csv(args.region, sep='\t', index=False)

def transcribe(args: argparse.Namespace):
    client = openai.OpenAI()
    transcription = client.audio.transcriptions.create(
        model="whisper-1", file=open(args.input, 'rb'), response_format='verbose_json')
    rows = []
    for segment in transcription.segments:
        rows.append({
            'start_time': segment['start'],
            'end_time': segment['end'],
            'text': segment['text'],
        })
    frame = pd.DataFrame(rows)
    frame.to_csv(args.output, sep='\t', index=False)

def render(args: argparse.Namespace):
    subtitle = pd.read_csv(args.subtitle, sep='\t')
    region = pd.read_csv(args.region, sep='\t')
    font_name = 'M PLUS 1p'
    font_style = 'Medium'

    video = mv.trim(mv.layer.Video(args.input, audio=True), region['start_time'], region['end_time'])
    scene = mv.layer.Composition(video.size, video.duration)
    scene.add_layer(video)

    bg_layer = mv.layer.LuminanceMatte(
        mask=mv.layer.Gradient(
            size=(video.size[0], 200),
            start_point=(0.0, 0.0), end_point=(0.0, 200.0),
            start_color='#000000', end_color='#ffffff'),
        target=mv.layer.Image.from_color((video.size[0], 200), '#1b4290'))
    bg = scene.add_layer(
        bg_layer,
        position=(video.size[0] / 2, video.size[1]),
        origin_point=mv.Direction.BOTTOM_CENTER,
        opacity=0.75)
    bg.opacity.enable_motion().extend([0.0, 2.0], [0.0, bg.opacity.init_value], ['ease_out'])
    item = scene.add_layer(
        mv.layer.Text.from_timeline(
            subtitle['start_time'], subtitle['end_time'], subtitle['text'],
            font_size=64, font_family=font_name, font_style=font_style, line_spacing=100, contents=[
                mv.layer.StrokeProperty(color='#13557b', width=12),
                mv.layer.FillProperty(color='#ffffff')],
            duration=scene.duration, text_alignment='center'),
        position=(video.size[0] / 2, video.size[1] - 20.0),
        origin_point=mv.Direction.BOTTOM_CENTER)
    item.add_effect(mv.effect.DropShadow(offset=5.0, opacity=0.4, radius=3.0))

    with scene.preview(level=1):
        scene.write_video(args.output, audio=True)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    subparsers = parser.add_subparsers()
    parser_extract = subparsers.add_parser(
        'extract', help='Extract non-silent regions from audio and generate subtitle')
    parser_extract.add_argument('-i', '--input', default='video.mp4', type=Path)
    parser_extract.add_argument('-o', '--output', default='truncated_audio.m4a', type=Path)
    parser_extract.add_argument('--region', default='non_silent_regions.tsv', type=Path)
    parser_extract.add_argument('--threshold', default=30, type=int)
    parser_extract.add_argument('--frame_length', default=512, type=int)
    parser_extract.set_defaults(func=extract)

    parser_transcribe = subparsers.add_parser('transcribe', help='Transcribe audio with OpenAI API')
    parser_transcribe.add_argument('-i', '--input', default='truncated.m4a', type=Path)
    parser_transcribe.add_argument('-o', '--output', default='subtitle.tsv', type=Path)
    parser_transcribe.set_defaults(func=transcribe)

    parser_render = subparsers.add_parser('render', help='Render video with subtitle and non-silent regions')
    parser_render.add_argument('-i', '--input', default='video.mp4', type=Path)
    parser_render.add_argument('--region', default='non_silent_regions.tsv', type=Path)
    parser_render.add_argument('--subtitle', default='subtitle.tsv', type=Path)
    parser_render.add_argument('-o', '--output', default='video_truncated.mp4', type=Path)
    parser_render.set_defaults(func=render)

    args = parser.parse_args()
    if hasattr(args, 'func'):
        args.func(args)
    else:
        parser.print_help()


from bson.objectid import ObjectId

class Trade(object):
    """A class for storing Trade related information"""

    def __init__(self, trade_id=None, user=None, code=None, market=None, timeframe=None,\
                 strategy=None, description=None, open_at=0.0, qty=0, open_date=None, \
                target=0.0, stoploss=0.0, max_loss=0.0, close_at=0.0, qty_close=0, pl=0.0, review=None):
        if trade_id is None:
            self._id = ObjectId()
        else:
            self._id = trade_id
        self.user = user
        self.code = code
        self.market = market
        self.timeframe = timeframe
        self.strategy = strategy
        self.description = description
        self.open_at = open_at
        self.qty = qty
        self.open_date = open_date
        self.target = target
        self.stoploss = stoploss
        self.max_loss = max_loss
        self.close_at = close_at
        self.qty_close = qty_close
        self.pl = pl
        self.review = review

    def to_json(self):
        return json.dumps(self.__dict__)

    @classmethod
    def from_json(cls, json_str):
        json_dict = json.loads(json_str)
        return cls(**json_dict)


import cv2
import imutils
import numpy as np
import os

def cv_show(name):
    cv2.imshow('name', name)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 统一的：mouse callback function
def draw_roi(img):
    # img = imutils.resize(img, width=500) 704 576
    # pts = [(249, 99), (475, 406), (497, 406), (497, 105), (249, 99)]  # 用于存放点

    pts = [(356, 130), (669, 576), (704, 576), (704, 130), (356, 130)]  # 用于存放点
    mask = np.zeros(img.shape, np.uint8)
    points = np.array(pts, np.int32)
    points = points.reshape((-1, 1, 2))
    # 画多边形
    mask = cv2.polylines(mask, [points], True, (255, 255, 255), 2)
    mask2 = cv2.fillPoly(mask.copy(), [points], (255, 255, 255))  # 用于求 ROI
    mask3 = cv2.fillPoly(mask.copy(), [points], (0, 255, 0))  # 用于 显示在桌面的图像

    show_image = cv2.addWeighted(src1=img, alpha=0.8, src2=mask3, beta=0.2, gamma=0)
    cv_show(show_image)

    ROI = cv2.bitwise_and(mask2, img)
    # res = np.hstack(show_image,ROI)
    cv_show(ROI)
    # cv2.imshow("res", res)
    cv2.waitKey(0)

# 创建图像与窗口并将窗口与回调函数绑定

img = cv2.imread("ori/1618310760.0999048.jpg")
draw_roi(img)


import os
import argparse as ap
import cv2
import numpy as np
import subprocess

def create_argparse():
    parser = ap.ArgumentParser(description="Detect abrupt scene change")
    parser.add_argument('frame_folder_path')
    parser.add_argument('output_folder_path')
    return parser.parse_args()

def compute_color_hist_diff(img1_path, img2_path):
    print(img1_path, img2_path)
    img1 = cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE)
    img2 = cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE)
    color_hist1 = np.bincount(img1.ravel(), minlength=256).astype(np.float32)
    color_hist1 /= np.linalg.norm(color_hist1)
    color_hist2 = np.bincount(img2.ravel(), minlength=256).astype(np.float32)
    color_hist2 /= np.linalg.norm(color_hist2)
    dist = np.sum((color_hist2 - color_hist1) ** 2)
    return dist

if __name__ == '__main__':
    args = create_argparse()
    img_paths = [os.path.join(args.frame_folder_path, item) for item in sorted(os.listdir(args.frame_folder_path))]
    num_imgs = len(img_paths)
    for i in range(1, num_imgs, 1):
        dist = compute_color_hist_diff(img_paths[i-1], img_paths[i])
        print(dist)