diff --git a/ex3.py b/ex3.py index 6fdd678..6929502 100644 --- a/ex3.py +++ b/ex3.py @@ -1,13 +1,14 @@ import os.path from glob import glob +import numpy as np +from PIL import Image, ImageStat + class ImageStandardizer: def __init__(self, input_dir: str) -> None: super().__init__() - # todo scan for .jpeg files - # scan input dir for files files = glob(input_dir + '/**/*.jpg', recursive=True) if len(files) == 0: @@ -19,8 +20,47 @@ class ImageStandardizer: # sort filenames files.sort() - def analyze_images(self) -> None: - pass + self.files = files + self.mean = None + self.std = None - def get_standardized_images(self) -> None: - pass + def analyze_images(self) -> (np.array, np.array): + mymean = np.zeros((3,), dtype=np.float64) + mystd = np.zeros((3,), dtype=np.float64) + + for file in self.files: + img = Image.open(file) + stats = ImageStat.Stat(img) + mymean += stats.mean + mystd += stats.stddev + + del img + + mymean /= len(self.files) + mystd /= len(self.files) + + self.mean = mymean + self.std = mystd + + return mymean, mystd + + def get_standardized_images(self): + if self.mean is None or self.std is None: + raise ValueError + + for file in self.files: + img = Image.open(file) + arr = np.asarray(img.getdata(), dtype=np.float32).reshape( + (img.height, img.width, 3)) # and reshape into 3channel rgb image + # standardize image + arr = (arr - self.mean) / self.std + yield np.array(arr, dtype=np.float32) + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + std = ImageStandardizer(input_dir='unittest/unittest_input_0') + print(std.analyze_images()) + + for i in std.get_standardized_images(): + print(i) diff --git a/ex3_unittest.py b/ex3_unittest.py new file mode 100644 index 0000000..014019a --- /dev/null +++ b/ex3_unittest.py @@ -0,0 +1,204 @@ +""" +Author -- Michael Widrich, Andreas Schörgenhumer +Contact -- schoergenhumer@ml.jku.at +Date -- 02.03.2022 + +############################################################################### + +The following copyright statement applies to all code within this file. + +Copyright statement: +This material, no matter whether in printed or electronic form, +may be used for personal and non-commercial educational use only. +Any reproduction of this manuscript, no matter whether as a whole or in parts, +no matter whether in printed or in electronic form, requires explicit prior +acceptance of the authors. + +############################################################################### + +Images taken from: https://pixabay.com/ +""" + +import gzip +import os +import signal +import sys +from glob import glob +from types import GeneratorType + +import dill as pkl +import numpy as np + + +def print_outs(outs, line_token="-"): + print(line_token * 40) + print(outs, end="" if isinstance(outs, str) and outs.endswith("\n") else "\n") + print(line_token * 40) + + +time_given = int(15) + +check_for_timeout = hasattr(signal, "SIGALRM") + +if check_for_timeout: + def handler(signum, frame): + raise TimeoutError(f"Timeout after {time_given}sec") + + + signal.signal(signal.SIGALRM, handler) + +ex_file = "ex3.py" +full_points = 15 +points = full_points +python = sys.executable + +inputs = sorted(glob(os.path.join("unittest", "unittest_input_*"), recursive=True)) + +if not len(inputs): + raise FileNotFoundError("Could not find unittest_input_* files") + +for test_i, input_folder in enumerate(inputs): + comment = "" + fcall = "" + + with open(os.devnull, "w") as null: + # sys.stdout = null + try: + if check_for_timeout: + signal.alarm(time_given) + from ex3 import ImageStandardizer + + signal.alarm(0) + else: + from ex3 import ImageStandardizer + proper_import = True + except Exception as e: + errs = e + points -= full_points / len(inputs) + proper_import = False + finally: + sys.stdout.flush() + sys.stdout = sys.__stdout__ + + if proper_import: + with open(os.devnull, "w") as null: + # sys.stdout = null + try: + if check_for_timeout: + signal.alarm(time_given) + # check constructor + instance = ImageStandardizer(input_dir=input_folder) + fcall = f"ImageStandardizer(input_dir='{input_folder}')" + signal.alarm(0) + else: + # check constructor + instance = ImageStandardizer(input_dir=input_folder) + fcall = f"ImageStandardizer(input_dir='{input_folder}')" + errs = "" + + # check correct file names + sorting + input_basename = os.path.basename(input_folder) + with open(os.path.join("unittest", "solutions", input_basename, f"filenames.txt"), "r") as f: + # must replace the separator that was used when creating the solution files + files_sol = f.read().replace("\\", os.path.sep).splitlines() + # for simplicity's sake, only compare relative paths here + common = os.path.commonprefix(instance.files) + rel_instance_files = [os.path.join(input_folder, f[len(common):]) for f in instance.files] + if not hasattr(instance, "files"): + points -= full_points / len(inputs) / 3 + comment += f"Attributes 'files' missing.\n" + elif rel_instance_files != files_sol: + points -= full_points / len(inputs) / 3 + comment += f"Attribute 'files' should be {files_sol} but is {instance.files} (see directory 'solutions').\n" + elif len(instance.files) != len(files_sol): + points -= full_points / len(inputs) / 3 + comment += f"Number of files should be {len(files_sol)} but is {len(instance.files)} (see directory 'solutions').\n" + + # check if class has method analyze_images + method = "analyze_images" + if not hasattr(instance, method): + comment += f"Method '{method}' missing.\n" + points -= full_points / len(inputs) / 3 + else: + # check for correct data types + stats = instance.analyze_images() + if (type(stats) is not tuple) or (len(stats) != 2): + points -= full_points / len(inputs) / 3 + comment += f"Incorrect return value of method '{method}' (should be tuple of length 2).\n" + else: + with open(os.path.join("unittest", "solutions", input_basename, f"mean_and_std.pkl"), + "rb") as fh: + data = pkl.load(fh) + m = data["mean"] + s = data["std"] + if not (isinstance(stats[0], np.ndarray) and isinstance(stats[1], np.ndarray) and + stats[0].dtype == np.float64 and stats[1].dtype == np.float64 and + stats[0].shape == (3,) and stats[1].shape == (3,)): + points -= full_points / len(inputs) / 3 + comment += f"Incorrect return data type of method '{method}' (tuple entries should be np.ndarray of dtype np.float64 and shape (3,)).\n" + else: + if not np.isclose(stats[0], m, atol=0).all(): + points -= full_points / len(inputs) / 6 + comment += f"Mean should be {m} but is {stats[0]} (see directory 'solutions').\n" + if not np.isclose(stats[1], s, atol=0).all(): + points -= full_points / len(inputs) / 6 + comment += f"Std should be {s} but is {stats[1]} (see directory 'solutions').\n" + + # check if class has method get_standardized_images + method = "get_standardized_images" + if not hasattr(instance, method): + comment += f"Method '{method}' missing.\n" + points -= full_points / len(inputs) / 3 + # check for correct data types + elif not isinstance(instance.get_standardized_images(), GeneratorType): + points -= full_points / len(inputs) / 3 + comment += f"'{method}' is not a generator.\n" + else: + # Read correct image solutions + with gzip.open(os.path.join("unittest", "solutions", input_basename, "images.pkl"), "rb") as fh: + ims_sol = pkl.load(file=fh) + + # Get image submissions + ims_sub = list(instance.get_standardized_images()) + + if not len(ims_sub) == len(ims_sol): + points -= full_points / len(inputs) / 3 + comment += f"{len(ims_sol)} image arrays should have been returned but got {len(ims_sub)}.\n" + elif any([im_sub.dtype.num != np.dtype(np.float32).num for im_sub in ims_sub]): + points -= full_points / len(inputs) / 3 + comment += f"Returned image arrays should have datatype np.float32 but at least one array isn't.\n" + else: + equal = [np.all(np.isclose(im_sub, im_sol, atol=0)) for im_sub, im_sol in zip(ims_sub, ims_sol)] + if not all(equal): + points -= full_points / len(inputs) / 3 + comment += f"Returned images {list(np.where(np.logical_not(equal))[0])} do not match solution (see images.pkl files for solution).\n" + except Exception as e: + errs = e + points -= full_points / len(inputs) + finally: + sys.stdout.flush() + sys.stdout = sys.__stdout__ + + print() + print_outs(f"Test {test_i}", line_token="#") + print("Function call:") + print_outs(fcall) + + if errs: + print(f"Some unexpected errors occurred:") + print_outs(f"{type(errs).__name__}: {errs}") + else: + print("Notes:") + print_outs("No issues found" if comment == "" else comment) + + # due to floating point calculations it could happen that we get -0 here + if points < 0: + assert abs(points) < 1e-7, f"points were {points} < 0: error when subtracting points?" + points = abs(points) + print(f"Current points: {points:.2f}") + +print(f"\nEstimated points upon submission: {points:.2f} (out of {full_points:.2f})") +print(f"This is only an estimate, see 'Instructions for submitting homework' in Moodle " + f"for common mistakes that can still lead to 0 points.") +if not check_for_timeout: + print("\n!!Warning: Had to switch to Windows compatibility version and did not check for timeouts!!") diff --git a/unittest/solutions/unittest_input_0/filenames.txt b/unittest/solutions/unittest_input_0/filenames.txt new file mode 100644 index 0000000..cb3340b --- /dev/null +++ b/unittest/solutions/unittest_input_0/filenames.txt @@ -0,0 +1,7 @@ +unittest\unittest_input_0\00.jpg +unittest\unittest_input_0\01.jpg +unittest\unittest_input_0\02.jpg +unittest\unittest_input_0\04.jpg +unittest\unittest_input_0\05.jpg +unittest\unittest_input_0\subfolder\06.jpg +unittest\unittest_input_0\subfolder\07.jpg diff --git a/unittest/solutions/unittest_input_0/images.pkl b/unittest/solutions/unittest_input_0/images.pkl new file mode 100644 index 0000000..d262fc7 Binary files /dev/null and b/unittest/solutions/unittest_input_0/images.pkl differ diff --git a/unittest/solutions/unittest_input_0/mean_and_std.pkl b/unittest/solutions/unittest_input_0/mean_and_std.pkl new file mode 100644 index 0000000..22074a5 Binary files /dev/null and b/unittest/solutions/unittest_input_0/mean_and_std.pkl differ diff --git a/unittest/solutions/unittest_input_1/filenames.txt b/unittest/solutions/unittest_input_1/filenames.txt new file mode 100644 index 0000000..4d7b863 --- /dev/null +++ b/unittest/solutions/unittest_input_1/filenames.txt @@ -0,0 +1,9 @@ +unittest\unittest_input_1\08.jpg +unittest\unittest_input_1\09.jpg +unittest\unittest_input_1\11.jpg +unittest\unittest_input_1\12.jpg +unittest\unittest_input_1\13.jpg +unittest\unittest_input_1\14.jpg +unittest\unittest_input_1\subfolder\15.jpg +unittest\unittest_input_1\subfolder\subsubfolder\16.jpg +unittest\unittest_input_1\subfolder\subsubfolder\17.jpg diff --git a/unittest/solutions/unittest_input_1/images.pkl b/unittest/solutions/unittest_input_1/images.pkl new file mode 100644 index 0000000..1b25e22 Binary files /dev/null and b/unittest/solutions/unittest_input_1/images.pkl differ diff --git a/unittest/solutions/unittest_input_1/mean_and_std.pkl b/unittest/solutions/unittest_input_1/mean_and_std.pkl new file mode 100644 index 0000000..0478da1 Binary files /dev/null and b/unittest/solutions/unittest_input_1/mean_and_std.pkl differ diff --git a/unittest/unittest_input_0/00.jpg b/unittest/unittest_input_0/00.jpg new file mode 100644 index 0000000..82ef1fb Binary files /dev/null and b/unittest/unittest_input_0/00.jpg differ diff --git a/unittest/unittest_input_0/01.jpg b/unittest/unittest_input_0/01.jpg new file mode 100644 index 0000000..2593d15 Binary files /dev/null and b/unittest/unittest_input_0/01.jpg differ diff --git a/unittest/unittest_input_0/02.jpg b/unittest/unittest_input_0/02.jpg new file mode 100644 index 0000000..b00ede3 Binary files /dev/null and b/unittest/unittest_input_0/02.jpg differ diff --git a/unittest/unittest_input_0/04.jpg b/unittest/unittest_input_0/04.jpg new file mode 100644 index 0000000..2ae36d6 Binary files /dev/null and b/unittest/unittest_input_0/04.jpg differ diff --git a/unittest/unittest_input_0/05.jpg b/unittest/unittest_input_0/05.jpg new file mode 100644 index 0000000..61d43fd Binary files /dev/null and b/unittest/unittest_input_0/05.jpg differ diff --git a/unittest/unittest_input_0/subfolder/06.jpg b/unittest/unittest_input_0/subfolder/06.jpg new file mode 100644 index 0000000..3d232e2 Binary files /dev/null and b/unittest/unittest_input_0/subfolder/06.jpg differ diff --git a/unittest/unittest_input_0/subfolder/07.jpg b/unittest/unittest_input_0/subfolder/07.jpg new file mode 100644 index 0000000..bd36bf3 Binary files /dev/null and b/unittest/unittest_input_0/subfolder/07.jpg differ diff --git a/unittest/unittest_input_1/08.jpg b/unittest/unittest_input_1/08.jpg new file mode 100644 index 0000000..c5e4f76 Binary files /dev/null and b/unittest/unittest_input_1/08.jpg differ diff --git a/unittest/unittest_input_1/09.jpg b/unittest/unittest_input_1/09.jpg new file mode 100644 index 0000000..df6e7bc Binary files /dev/null and b/unittest/unittest_input_1/09.jpg differ diff --git a/unittest/unittest_input_1/10.webp b/unittest/unittest_input_1/10.webp new file mode 100644 index 0000000..d33c36d Binary files /dev/null and b/unittest/unittest_input_1/10.webp differ diff --git a/unittest/unittest_input_1/11.jpg b/unittest/unittest_input_1/11.jpg new file mode 100644 index 0000000..d4f7edd Binary files /dev/null and b/unittest/unittest_input_1/11.jpg differ diff --git a/unittest/unittest_input_1/12.jpg b/unittest/unittest_input_1/12.jpg new file mode 100644 index 0000000..fc7fce9 Binary files /dev/null and b/unittest/unittest_input_1/12.jpg differ diff --git a/unittest/unittest_input_1/13.jpg b/unittest/unittest_input_1/13.jpg new file mode 100644 index 0000000..fc7fce9 Binary files /dev/null and b/unittest/unittest_input_1/13.jpg differ diff --git a/unittest/unittest_input_1/14.jpg b/unittest/unittest_input_1/14.jpg new file mode 100644 index 0000000..6e39801 Binary files /dev/null and b/unittest/unittest_input_1/14.jpg differ diff --git a/unittest/unittest_input_1/subfolder/15.jpg b/unittest/unittest_input_1/subfolder/15.jpg new file mode 100644 index 0000000..1fd4201 Binary files /dev/null and b/unittest/unittest_input_1/subfolder/15.jpg differ diff --git a/unittest/unittest_input_1/subfolder/subsubfolder/03.webp b/unittest/unittest_input_1/subfolder/subsubfolder/03.webp new file mode 100644 index 0000000..417f27f Binary files /dev/null and b/unittest/unittest_input_1/subfolder/subsubfolder/03.webp differ diff --git a/unittest/unittest_input_1/subfolder/subsubfolder/16.jpg b/unittest/unittest_input_1/subfolder/subsubfolder/16.jpg new file mode 100644 index 0000000..d5beaab Binary files /dev/null and b/unittest/unittest_input_1/subfolder/subsubfolder/16.jpg differ diff --git a/unittest/unittest_input_1/subfolder/subsubfolder/17.jpg b/unittest/unittest_input_1/subfolder/subsubfolder/17.jpg new file mode 100644 index 0000000..a235075 Binary files /dev/null and b/unittest/unittest_input_1/subfolder/subsubfolder/17.jpg differ