├── hvf_extraction_script ├── __init__.py ├── hvf_data │ ├── __init__.py │ ├── perc_icons │ │ ├── __init__.py │ │ ├── perc_1.JPG │ │ ├── perc_2.JPG │ │ ├── perc_5.JPG │ │ ├── perc_half.JPG │ │ └── perc_normal.JPG │ ├── other_icons │ │ ├── __init__.py │ │ ├── icon_triangle_v1.PNG │ │ └── icon_triangle_v2.PNG │ ├── value_icons │ │ ├── __init__.py │ │ ├── v0 │ │ │ ├── __init__.py │ │ │ ├── value_0.PNG │ │ │ ├── value_1.PNG │ │ │ ├── value_2.PNG │ │ │ ├── value_3.PNG │ │ │ ├── value_4.PNG │ │ │ ├── value_5.PNG │ │ │ ├── value_6.PNG │ │ │ ├── value_7.PNG │ │ │ ├── value_8.PNG │ │ │ ├── value_9.PNG │ │ │ ├── value_minus.PNG │ │ │ └── value_less_than.PNG │ │ ├── v1 │ │ │ ├── __init__.py │ │ │ ├── value_0.PNG │ │ │ ├── value_1.PNG │ │ │ ├── value_2.PNG │ │ │ ├── value_3.PNG │ │ │ ├── value_4.PNG │ │ │ ├── value_5.PNG │ │ │ ├── value_6.PNG │ │ │ ├── value_7.PNG │ │ │ ├── value_8.PNG │ │ │ ├── value_9.PNG │ │ │ ├── value_minus.PNG │ │ │ └── value_less_than.PNG │ │ └── v2 │ │ │ ├── __init__.py │ │ │ ├── value_0.PNG │ │ │ ├── value_1.PNG │ │ │ ├── value_2.PNG │ │ │ ├── value_3.PNG │ │ │ ├── value_4.PNG │ │ │ ├── value_5.PNG │ │ │ ├── value_6.PNG │ │ │ ├── value_7.PNG │ │ │ ├── value_8.PNG │ │ │ ├── value_9.PNG │ │ │ ├── value_minus.PNG │ │ │ └── value_less_than.PNG │ ├── hvf_perc_icon.py │ └── hvf_value.py ├── utilities │ ├── __init__.py │ ├── ocr_utils.py │ ├── logger.py │ ├── file_utils.py │ ├── image_utils.py │ └── regex_utils.py └── hvf_manager │ ├── __init__.py │ ├── hvf_editor.py │ ├── hvf_patient_container.py │ ├── hvf_metric_calculator.py │ └── hvf_export.py ├── .gitignore ├── setup.py ├── hvf_object_tester.py ├── hvf_bulk_processing.py ├── README.md └── LICENSE.txt /hvf_extraction_script/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_manager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/other_icons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | hvf_test_cases/* 3 | .DS_Store 4 | build/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/perc_1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/perc_icons/perc_1.JPG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/perc_2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/perc_icons/perc_2.JPG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/perc_5.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/perc_icons/perc_5.JPG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/perc_half.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/perc_icons/perc_half.JPG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/perc_icons/perc_normal.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/perc_icons/perc_normal.JPG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_0.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_0.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_1.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_2.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_3.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_4.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_5.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_6.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_6.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_7.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_8.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_8.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_9.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_9.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_0.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_0.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_1.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_2.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_3.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_4.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_5.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_6.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_6.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_7.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_8.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_8.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_9.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_9.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_0.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_0.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_1.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_2.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_3.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_4.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_5.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_6.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_6.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_7.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_8.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_8.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_9.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_9.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_minus.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_minus.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_minus.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_minus.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_minus.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_minus.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/other_icons/icon_triangle_v1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/other_icons/icon_triangle_v1.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/other_icons/icon_triangle_v2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/other_icons/icon_triangle_v2.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v0/value_less_than.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v0/value_less_than.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v1/value_less_than.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v1/value_less_than.PNG -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/value_icons/v2/value_less_than.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msaifee786/hvf_extraction_script/HEAD/hvf_extraction_script/hvf_data/value_icons/v2/value_less_than.PNG -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="hvf_extraction_script", 8 | version="0.1.0", 9 | author="Murtaza Saifee", 10 | author_email="saifeeapps@gmail.com", 11 | description="Python extraction script for HVF report images using AWS rekognition as an option as well", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/msaifee786/hvf_extraction_script", 15 | packages=setuptools.find_packages(), 16 | package_data={ 17 | "hvf_extraction_script": [ 18 | "hvf_data/other_icons/*.PNG", 19 | "hvf_data/perc_icons/*.JPG", 20 | "hvf_data/value_icons/v0/*.PNG", 21 | "hvf_data/value_icons/v1/*.PNG", 22 | "hvf_data/value_icons/v2/*.PNG", 23 | ] 24 | }, 25 | install_requires=[ 26 | "regex", 27 | "pydicom", 28 | "pillow", 29 | "opencv-python", 30 | "fuzzywuzzy", 31 | "fuzzysearch", 32 | "typed-argument-parser", 33 | "python-levenshtein", 34 | ], 35 | extras_require={ 36 | "tesserOCR": ["tesserOCR"], 37 | }, 38 | classifiers=[ 39 | "Programming Language :: Python :: 3", 40 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 41 | "Operating System :: OS Independent", 42 | ], 43 | python_requires=">=3.6", 44 | ) 45 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/ocr_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use AWS rekognition detect_text engine 3 | """ 4 | import io 5 | from cmath import isclose 6 | from collections import defaultdict 7 | from operator import attrgetter 8 | 9 | import boto3 10 | from PIL import Image 11 | 12 | from hvf_extraction_script.utilities.image_utils import Image_Utils 13 | from hvf_extraction_script.utilities.regex_utils import Regex_Utils 14 | 15 | 16 | class RekognitionText: 17 | def __init__(self, text_data): 18 | self.text = text_data.get("DetectedText") 19 | self.kind = text_data.get("Type") 20 | self.id = text_data.get("Id") 21 | self.parent_id = text_data.get("ParentId") 22 | self.confidence = text_data.get("Confidence") 23 | self.geometry = text_data.get("Geometry") 24 | 25 | def __repr__(self) -> str: 26 | return f"{self.id}:{self.parent_id}:{self.kind}:{self.text}" 27 | 28 | 29 | def columnise(texts, rel_tol=0.02) -> str: 30 | tops = [x.geometry["BoundingBox"]["Top"] for x in texts] 31 | ntops = tops[:] 32 | for n, top in enumerate(tops[:-1]): 33 | if isclose(top, tops[n + 1], rel_tol=rel_tol): 34 | ntops.remove(top) 35 | dic_lines = defaultdict(list) 36 | for n, top in enumerate(ntops): 37 | for block in texts: 38 | ltop = block.geometry["BoundingBox"]["Top"] 39 | if isclose(top, ltop, rel_tol=rel_tol): 40 | dic_lines[n].append(block) 41 | elif ltop > top: 42 | break 43 | 44 | # sort by Left within line 45 | res = [] 46 | for oo in [sorted(x, key=lambda y: y.geometry["BoundingBox"]["Left"]) for x in dic_lines.values()]: # type: ignore 47 | res.append(" ".join(list(map(attrgetter("text"), oo)))) 48 | 49 | return "\n".join(res) 50 | 51 | 52 | class Ocr_Utils: 53 | @staticmethod 54 | def perform_ocr( 55 | img_arr, proc_img: bool = False, column: bool = True, debug_dir: str = "", rekognition=False 56 | ) -> str: 57 | if rekognition: 58 | return Ocr_Utils.do_rekognition(img_arr, column, debug_dir) 59 | else: 60 | return Ocr_Utils.do_tesserocr(proc_img, img_arr, column, debug_dir) 61 | 62 | @staticmethod 63 | def do_rekognition(img_arr, column, debug_dir): 64 | img = Image.fromarray(img_arr) 65 | client = boto3.client("rekognition") 66 | buf = io.BytesIO() 67 | img.save(buf, format="JPEG") 68 | data = buf.getvalue() 69 | res = client.detect_text(Image={"Bytes": data}) 70 | texts = [x for x in [RekognitionText(text) for text in res["TextDetections"]] if x.kind == "LINE"] 71 | if column: 72 | text = columnise(texts) 73 | else: 74 | text = " ".join( 75 | [x.text for x in [RekognitionText(text) for text in res["TextDetections"]] if x.kind == "LINE"] 76 | ) 77 | 78 | if debug_dir: 79 | out = Regex_Utils.temp_out(debug_dir=debug_dir) 80 | img.save(f"{out}.jpg") 81 | with open(f"{out}.txt", "w") as f: 82 | f.writelines(text) 83 | 84 | # Return extracted text: 85 | return text 86 | 87 | @staticmethod 88 | def do_tesserocr(proc_img, img_arr, column, debug_dir): 89 | from tesserocr import PSM, PyTessBaseAPI 90 | 91 | if proc_img: 92 | # First, preprocessor the image: 93 | img_arr = Image_Utils.preprocess_image(img_arr, debug_dir=debug_dir) 94 | 95 | # Next, convert image to python PIL (because pytesseract using PIL): 96 | img_pil = Image.fromarray(img_arr) 97 | 98 | if not Ocr_Utils.OCR_API_HANDLE: 99 | Ocr_Utils.OCR_API_HANDLE = PyTessBaseAPI(psm=PSM.SINGLE_COLUMN) 100 | # Ocr_Utils.OCR_API_HANDLE = PyTessBaseAPI(psm=PSM.SINGLE_BLOCK) 101 | 102 | Ocr_Utils.OCR_API_HANDLE.SetImage(img_pil) 103 | Ocr_Utils.OCR_API_HANDLE.SetSourceResolution(200) 104 | text: str = Ocr_Utils.OCR_API_HANDLE.GetUTF8Text() 105 | 106 | if debug_dir: 107 | out = Regex_Utils.temp_out(debug_dir=debug_dir) 108 | img_pil.save(f"{out}.jpg") 109 | with open(f"{out}.txt", "w") as f: 110 | f.writelines(text) 111 | 112 | # Return extracted text: 113 | return text 114 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_manager/hvf_editor.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_editor.py 3 | # 4 | # Description: 5 | # Functions for editing HVF objects 6 | # 7 | ############################################################################### 8 | 9 | # Import necessary packages 10 | import copy 11 | 12 | import numpy as np 13 | 14 | # Import the HVF_Object class and helper classes 15 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 16 | from hvf_extraction_script.hvf_data.hvf_perc_icon import Hvf_Perc_Icon 17 | from hvf_extraction_script.hvf_data.hvf_plot_array import Hvf_Plot_Array 18 | from hvf_extraction_script.hvf_data.hvf_value import Hvf_Value 19 | 20 | # Import logger class to handle any messages: 21 | 22 | 23 | class Hvf_Editor: 24 | 25 | ############################################################################### 26 | # VARIABLE/CONSTANT DECLARATIONS: ############################################# 27 | ############################################################################### 28 | 29 | ############################################################################### 30 | # STATIC EDITING FUNCTIONS #################################################### 31 | ############################################################################### 32 | 33 | ############################################################################### 34 | # Converts HVF object from 30-2 to 24-2. If it is not 30-2, does nothing. 35 | @staticmethod 36 | def convert_hvf_302_to_242(hvf_obj): 37 | if hvf_obj.metadata.get(Hvf_Object.KEYLABEL_FIELD_SIZE) == Hvf_Object.HVF_30_2: 38 | 39 | is_right = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) == Hvf_Object.HVF_OD 40 | 41 | hvf_obj.raw_value_array = Hvf_Editor.mask_302_to_242(hvf_obj.raw_value_array, is_right) 42 | hvf_obj.abs_dev_value_array = Hvf_Editor.mask_302_to_242(self.abs_dev_value_array, is_right) 43 | hvf_obj.pat_dev_value_array = Hvf_Editor.mask_302_to_242(self.pat_dev_value_array, is_right) 44 | hvf_obj.abs_dev_percentile_array = Hvf_Editor.mask_302_to_242(self.abs_dev_percentile_array, is_right) 45 | hvf_obj.pat_dev_percentile_array = Hvf_Editor.mask_302_to_242(self.pat_dev_percentile_array, is_right) 46 | 47 | return 48 | 49 | ############################################################################### 50 | # GENERAL PURPOSE ARRAY METHODS ############################################### 51 | ############################################################################### 52 | 53 | ############################################################################### 54 | # For transposing left -> right eye arrays 55 | def transpose_array(array): 56 | 57 | new_array = array.copy() 58 | 59 | for i in range(0, np.size(array, 1)): 60 | 61 | size = np.size(array, 0) 62 | 63 | for j in range(0, size): 64 | 65 | new_array[j, i] = array[size - 1 - j, i] 66 | 67 | return new_array 68 | 69 | ############################################################################### 70 | # For converting 30-2 fields to 24-2 71 | # also takes in flag for is_right indicating if right eye (false if left) - 72 | # needed for transposition 73 | def mask_302_to_242(plot_obj, is_right): 74 | 75 | new_plot_obj = copy.deepcopy(plot_obj) 76 | 77 | # Transpose the array if needed: 78 | if not is_right: 79 | new_plot_obj.plot_array = Hvf_Editor.transpose_array(new_plot_obj.plot_array) 80 | 81 | # Iterate through the array: 82 | for i in range(0, np.size(new_plot_obj.plot_array, 1)): 83 | size = np.size(new_plot_obj.plot_array, 0) 84 | for j in range(0, size): 85 | 86 | if not (Hvf_Plot_Array.BOOLEAN_MASK_24_2[j][i]): 87 | if new_plot_obj.icon_type == Hvf_Plot_Array.PLOT_PERC: 88 | new_plot_obj.plot_array[j, i] = Hvf_Perc_Icon.get_perc_icon_from_char( 89 | Hvf_Perc_Icon.PERC_NO_VALUE_CHAR 90 | ) 91 | elif new_plot_obj.icon_type == Hvf_Plot_Array.PLOT_VALUE: 92 | new_plot_obj.plot_array[j, i] = Hvf_Value.get_value_from_display_string( 93 | Hvf_Value.VALUE_NO_VALUE_CHAR 94 | ) 95 | 96 | # Transpose the array back if needed: 97 | if not is_right: 98 | new_plot_obj.plot_array = Hvf_Editor.transpose_array(new_plot_obj.plot_array) 99 | 100 | return new_plot_obj 101 | -------------------------------------------------------------------------------- /hvf_object_tester.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_object_tester.py 3 | # 4 | # Description: 5 | # Tests the HVF_Object class. There are 3 different things this class does: 6 | # 7 | # - Demos result from a specific HVF file. Usage: 8 | # python hvf_object_tester -i 9 | # 10 | # - Runs unit tests of the specified collection. Specify 2 arguments: 11 | # - Test name 12 | # - Test type (image_vs_serialization, image_vs_dicom, etc -- see Hvf_Test) 13 | # Usage: 14 | # python hvf_object_tester -t 15 | # 16 | # - Adds a unit test to the specified collection/test type. Takes in 4 arguments, 17 | # and copies files into the hvf_test_cases folder 18 | # Usage: 19 | # python hvf_object_tester -a 20 | # 21 | ############################################################################### 22 | 23 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 24 | from hvf_extraction_script.hvf_manager.hvf_test import Hvf_Test 25 | from hvf_extraction_script.utilities.file_utils import File_Utils 26 | from hvf_extraction_script.utilities.logger import Logger 27 | from tap import Tap 28 | 29 | # Default directory for unit tests: 30 | default_unit_test_dir = "default" 31 | 32 | 33 | class MyArgParser(Tap): 34 | 35 | image: str # path to input HVF image file to test 36 | dicom: str # path to input DICOM file to test 37 | test: str 38 | add_test_case: str # adds input hvf image to test cases 39 | rekognition: bool = False # use AWS Rekognition rather than tesserOCR 40 | 41 | def configure(self) -> None: 42 | self.add_argument("-i", "--image", required=False) 43 | self.add_argument("-d", "--dicom", required=False) 44 | self.add_argument("-t", "--test", nargs=2, required=False) 45 | self.add_argument("-a", "--add_test_case", nargs=4, required=False) 46 | self.add_argument("-r", "--rekognition") 47 | 48 | 49 | args = MyArgParser().parse_args() 50 | 51 | 52 | # Set up the logger module: 53 | # debug_level = Logger.DEBUG_FLAG_INFO; 54 | debug_level = Logger.DEBUG_FLAG_WARNING 55 | # debug_level = Logger.DEBUG_FLAG_DEBUG; 56 | msg_logger = Logger.get_logger().set_logger_level(debug_level) 57 | 58 | 59 | ############################################################################### 60 | # SINGLE IMAGE TESTING ######################################################## 61 | ############################################################################### 62 | 63 | # If we are passed in an image, read it and show results 64 | if args.image: 65 | 66 | hvf_image = File_Utils.read_image_from_file(args.image) 67 | Hvf_Test.test_single_image(hvf_image, args.rekognition) 68 | 69 | 70 | ############################################################################### 71 | # DICOM FILE TESTING ########################################################## 72 | ############################################################################### 73 | if args.dicom: 74 | 75 | hvf_dicom = File_Utils.read_dicom_from_file(args.dicom) 76 | hvf_obj = Hvf_Object.get_hvf_object_from_dicom(hvf_dicom) 77 | print(hvf_obj.get_pretty_string()) 78 | 79 | 80 | ############################################################################### 81 | # ADD NEW UNIT TESTS ########################################################## 82 | ############################################################################### 83 | 84 | 85 | # If given a new file, add to the unit test to the specified collection. This 86 | # will use the current version of Hvf_Object to generate the expected result 87 | elif args.add_test_case: 88 | 89 | if not (len(args.add_test_case) == 4): 90 | Logger.get_logger().log_msg( 91 | Logger.DEBUG_FLAG_ERROR, 92 | "Incorrect number of arguments, needs 4 (test_name, test_type, ref_data, test_data)", 93 | ) 94 | else: 95 | 96 | test_name = args.add_test_case[0] 97 | test_type = args.add_test_case[1] 98 | ref_data_path = args.add_test_case[2] 99 | test_data_path = args.add_test_case[3] 100 | 101 | Hvf_Test.add_unit_test(test_name, test_type, ref_data_path, test_data_path) 102 | 103 | 104 | ############################################################################### 105 | # BULK UNIT TESTING ########################################################### 106 | ############################################################################### 107 | 108 | # If flag, then do unit tests: 109 | elif args.test: 110 | 111 | # Assume argument is the testing directory. Make sure its in format of directory 112 | if not (len(args.test) == 2): 113 | Logger.get_logger().log_msg( 114 | Logger.DEBUG_FLAG_ERROR, "Incorrect number of arguments, needs 2 (test_name, test_type)" 115 | ) 116 | else: 117 | 118 | dir = args.test[0] 119 | test_type = args.test[1] 120 | 121 | Hvf_Test.test_unit_tests(dir, test_type, args.rekognition) 122 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/logger.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # logger.py 3 | # 4 | # Description: 5 | # Class definition for logger object. Logs error/info/timing messages. To use 6 | # the logger, just call on the class method get_logger and use that object. 7 | # Do not instantiate your own. 8 | # 9 | # To Do: 10 | # 11 | ############################################################################### 12 | 13 | # Import some helper packages: 14 | import math 15 | import time 16 | 17 | 18 | class Logger: 19 | 20 | # Need a class variable to hold the logger instance 21 | # We want logger to be available globally, so we hold onto logger as a class var and 22 | # access it using a class method 23 | global_logger = None 24 | 25 | # Debug flag variables 26 | DEBUG_FLAG_NONE = -1 27 | DEBUG_FLAG_DEBUG = 0 28 | DEBUG_FLAG_TIME = 1 29 | DEBUG_FLAG_INFO = 2 30 | DEBUG_FLAG_BROADCAST = 3 31 | DEBUG_FLAG_WARNING = 4 32 | DEBUG_FLAG_ERROR = 5 33 | DEBUG_FLAG_SYSTEM = 99 34 | 35 | # Debug flag message prefixes: 36 | debug_flag_levels = { 37 | DEBUG_FLAG_DEBUG: "[DEBUG]", 38 | DEBUG_FLAG_TIME: "[TIME]", 39 | DEBUG_FLAG_INFO: "[INFO]", 40 | DEBUG_FLAG_BROADCAST: "[BROADCAST]", 41 | DEBUG_FLAG_WARNING: "[WARNING]", 42 | DEBUG_FLAG_ERROR: "[ERROR]", 43 | DEBUG_FLAG_SYSTEM: "[SYSTEM]", 44 | } 45 | 46 | # Default flag: 47 | DEFAULT_FLAG_LEVEL = DEBUG_FLAG_ERROR 48 | 49 | # Flags for use in time benchmarking 50 | TIME_START = 0 51 | TIME_END = 1 52 | 53 | ############################################################################### 54 | # Initializer method: 55 | def __init__(self, flag_level): 56 | 57 | # Set up a dictionary to hold onto any benchmarking times: 58 | self.myTimes = {} 59 | 60 | # Set up flag level: 61 | self.myFlagLevel = flag_level 62 | 63 | ############################################################################### 64 | # Function returns boolean - if flag level is higher/equal than set internal level 65 | def should_log(self, flag_level): 66 | return flag_level >= self.myFlagLevel 67 | 68 | ############################################################################### 69 | # Function to log any message: 70 | def log_msg(self, flag_level, msg): 71 | 72 | # Do we need to report this? 73 | if self.should_log(flag_level): 74 | 75 | # Grab prefix string: 76 | prefix_string = Logger.debug_flag_levels[flag_level] 77 | 78 | # Construct our display string: 79 | display_string = prefix_string + " " + msg 80 | 81 | # And print it: 82 | print(display_string) 83 | 84 | ############################################################################### 85 | # Function to call any debugging/logging function based on a log level 86 | # Assumes function func takes no arguments -- pass in func as a lambda with 87 | # args preloaded. Will only call func if flag_level is high priority than 88 | # baseline log level 89 | def log_function(self, flag_level, func): 90 | 91 | # Do we need to call the function? 92 | if self.should_log(flag_level): 93 | 94 | # Yes - call function 95 | func() 96 | 97 | ############################################################################### 98 | # Method for logging time elapsed for a specified event. Passing TIME_START flag 99 | # starts timing; passing TIME_END flag stops timing, logs a time event, and 100 | # returns time elapsed 101 | # Usage: 102 | # Logger.get_logger().log_time("Event_string", Logger.TIME_START) 103 | # < Code to time > 104 | # time_elapsed = Logger.get_logger().log_time("Event_string", Logger.TIME_END) 105 | # 106 | def log_time(self, event_string, time_flag): 107 | 108 | # If we're starting to time an event, just log the time and finish: 109 | if time_flag == Logger.TIME_START: 110 | self.myTimes[event_string] = time.time() 111 | 112 | # If we're ending a time of an event, pull the old time and log the difference 113 | # If no start time was logged, it defaults to now: 114 | if time_flag == Logger.TIME_END: 115 | # Grab start time. Default value current time (if key is missing) 116 | start_time = self.myTimes.get(event_string, time.time()) 117 | 118 | # Get end time (now): 119 | end_time = time.time() 120 | 121 | # Time elapsed in ms: 122 | time_elapsed = math.trunc(1000 * (end_time - start_time)) 123 | 124 | # Log the time: 125 | self.log_msg(Logger.DEBUG_FLAG_TIME, event_string + f" done in {time_elapsed} ms") 126 | 127 | return time_elapsed 128 | 129 | ############################################################################### 130 | @classmethod 131 | def get_logger(cls): 132 | # Class method to access the logger object. All modules using the logger should 133 | # only get loggers through this method -- this will allow all modules to use the 134 | # same logger. 135 | 136 | # If we don't have one yet, make one: 137 | if Logger.global_logger is None: 138 | Logger.global_logger = Logger(Logger.DEFAULT_FLAG_LEVEL) 139 | 140 | return Logger.global_logger 141 | 142 | ############################################################################### 143 | @classmethod 144 | def set_logger_level(cls, level): 145 | Logger.get_logger().myFlagLevel = level 146 | 147 | @classmethod 148 | def get_logger_level(cls): 149 | return Logger.get_logger().myFlagLevel 150 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_manager/hvf_patient_container.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_patient_container.py 3 | # 4 | # Description: 5 | # Class description for container to hold HVFs, grouped by patient/laterality 6 | # Organizes HVFs by: 7 | # Patient 8 | # Laterality 9 | # HVF (ordered by date) 10 | ############################################################################### 11 | 12 | # Import necessary packages 13 | 14 | # Import logger class to handle any messages: 15 | 16 | # Import the HVF_Object class: 17 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 18 | 19 | 20 | class Hvf_Patient_Container: 21 | 22 | ############################################################################### 23 | # VARIABLE/CONSTANT DECLARATIONS: ############################################# 24 | ############################################################################### 25 | 26 | ############################################################################### 27 | # INIT/OBJECT FUNCTIONS ####################################################### 28 | ############################################################################### 29 | 30 | ############################################################################### 31 | # Initialization function: 32 | def __init__(self): 33 | self.hvf_obj_dict = {} 34 | 35 | ############################################################################### 36 | # Add new patient to list: 37 | def add_hvf(self, hvf_obj): 38 | 39 | # Construct a few parameters of hvf_obj to help organize/sorting: 40 | hvf_obj_patient_id = ( 41 | hvf_obj.metadata.get(Hvf_Object.KEYLABEL_NAME).lower() 42 | + " | " 43 | + hvf_obj.metadata.get(Hvf_Object.KEYLABEL_ID) 44 | ) 45 | 46 | hvf_obj_laterality = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) 47 | 48 | hvf_obj_date_key = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_TEST_DATE) 49 | 50 | # First, is patient present in this container? If its not, add patient in 51 | if hvf_obj_patient_id not in self.hvf_obj_dict: 52 | self.hvf_obj_dict[hvf_obj_patient_id] = {} 53 | 54 | # Now, we know patient is present. Is laterality present? If not, add it in 55 | if hvf_obj_laterality not in self.hvf_obj_dict[hvf_obj_patient_id]: 56 | self.hvf_obj_dict[hvf_obj_patient_id][hvf_obj_laterality] = {} 57 | 58 | # Lastly, add in HVF object: 59 | 60 | self.hvf_obj_dict[hvf_obj_patient_id][hvf_obj_laterality][hvf_obj_date_key] = hvf_obj 61 | 62 | return 63 | 64 | ############################################################################### 65 | # Get list of patients in this container: 66 | def get_patient_list(self): 67 | return self.hvf_obj_dict.keys() 68 | 69 | ############################################################################### 70 | # Get list of lateralities for a particular patient: 71 | def get_laterality_list(self, patient_id): 72 | return self.hvf_obj_dict.get(patient_id, {}).keys() 73 | 74 | ############################################################################### 75 | # Get dictionary of test dates -> hvf_objs for a particular patient/laterality: 76 | def get_hvf_obj_dict(self, patient_id, laterality): 77 | return self.hvf_obj_dict.get(patient_id, {}).get(laterality, {}) 78 | 79 | ############################################################################### 80 | # Remove an HVF from the container. Must pass in the object to remove 81 | # Assumes hvf_obj is in container 82 | def remove_hvf(self, hvf_obj): 83 | 84 | # Construct a few parameters of hvf_obj to help organize/sorting: 85 | hvf_obj_patient_id = ( 86 | hvf_obj.metadata.get(Hvf_Object.KEYLABEL_NAME).lower() 87 | + " | " 88 | + hvf_obj.metadata.get(Hvf_Object.KEYLABEL_ID) 89 | ) 90 | 91 | hvf_obj_laterality = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) 92 | 93 | hvf_obj_date_key = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_TEST_DATE) 94 | 95 | self.remove_hvf_by_parameter(hvf_obj_patient_id, hvf_obj_laterality, hvf_obj_date_key) 96 | 97 | return 98 | 99 | ############################################################################### 100 | # Remove an HVF from the container using parameters. 101 | # Assumes hvf_obj is in container 102 | def remove_hvf_by_parameter(self, id, laterality, date_key): 103 | 104 | # First, pop/remove the hvf 105 | self.hvf_obj_dict.get(id, {}).get(laterality, {}).pop(date_key, None) 106 | 107 | # Then clean up laterality/id if there are no other elements: 108 | if not (self.hvf_obj_dict.get(id, {}).get(laterality, {})): 109 | self.hvf_obj_dict.get(id, {}).pop(laterality, None) 110 | 111 | if not (self.hvf_obj_dict.get(id, {})): 112 | self.hvf_obj_dict.pop(id, None) 113 | 114 | return 115 | 116 | ############################################################################### 117 | # NON-EDITING HELPER FUNCTIONS ################################################ 118 | ############################################################################### 119 | 120 | @staticmethod 121 | def is_same_patient(hvf_obj1, hvf_obj2): 122 | 123 | name_bool = ( 124 | hvf_obj1.metadata.get(HVF_Object.KEYLABEL_NAME).lower() 125 | == hvf_obj2.metadata.get(HVF_Object.KEYLABEL_NAME).lower() 126 | ) 127 | 128 | id_bool = hvf_obj1.metadata.get(HVF_Object.KEYLABEL_ID) == hvf_obj2.metadata.get(HVF_Object.KEYLABEL_ID) 129 | 130 | dob_bool = hvf_obj1.metadata.get(HVF_Object.KEYLABEL_DOB) == hvf_obj2.metadata.get(HVF_Object.KEYLABEL_DOB) 131 | 132 | return name_bool and id_bool and dob_bool 133 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/file_utils.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # file_utils.py 3 | # 4 | # Description: 5 | # Class definition for commonly used, general use file handling functions 6 | # 7 | ############################################################################### 8 | 9 | # Import necessary packages 10 | import os 11 | 12 | import cv2 13 | import pydicom 14 | 15 | 16 | class File_Utils: 17 | ############################################################################### 18 | # CONSTANTS AND STATIC VARIABLES ############################################## 19 | ############################################################################### 20 | 21 | ############################################################################### 22 | # FILE I/O METHODS ############################################################ 23 | ############################################################################### 24 | 25 | ############################################################################### 26 | # Given directory path and list of extensions, gets all files within directory 27 | # with that extensions and returns in list. 28 | # Returns full paths 29 | # Does not recursively walk subdirectories 30 | @staticmethod 31 | def get_files_within_dir(dir_path, list_of_exts): 32 | 33 | file_list = [] 34 | 35 | # For each file in the test folder: 36 | for file_name in os.listdir(dir_path): 37 | 38 | full_file_name_path = os.path.join(dir_path, file_name) 39 | if os.path.isfile(full_file_name_path): 40 | 41 | # Skip hidden files: 42 | if file_name.startswith("."): 43 | continue 44 | 45 | # Then, find corresponding serialization text file 46 | filename_root, ext = os.path.splitext(file_name) 47 | 48 | # If file has appropriate extension, add to return list 49 | if ext.lower() in list_of_exts: 50 | file_list.append(full_file_name_path) 51 | 52 | return file_list 53 | 54 | ############################################################################### 55 | # Given directory path, gets all immediate subdirectories and returns as list 56 | # Returns full paths 57 | # Does not recursively walk subdirectories 58 | @staticmethod 59 | def get_dirs_within_dir(dir_path): 60 | 61 | dir_list = [] 62 | 63 | # For dir in the test folder: 64 | for dir_name in os.listdir(dir_path): 65 | 66 | # If indeed directory, add to list 67 | full_dir_name_path = os.path.join(dir_path, dir_name) 68 | if os.path.isdir(full_dir_name_path): 69 | 70 | dir_list.append(dir_name) 71 | 72 | return dir_list 73 | 74 | ############################################################################### 75 | # Given file path, reads cv2 image from file 76 | @staticmethod 77 | def read_image_from_file(file_path): 78 | 79 | return cv2.imread(file_path) 80 | 81 | ############################################################################### 82 | # Given file path, reads DICOM object from file 83 | @staticmethod 84 | def read_dicom_from_file(file_path): 85 | 86 | return pydicom.dcmread(file_path) 87 | 88 | ############################################################################### 89 | # Given directory path, reads cv2 images from all files within the directory 90 | # Does not recursively search for files within any subdirectories 91 | # Returns a dictionary with file_name root -> image 92 | @staticmethod 93 | def read_images_from_directory(dir_path): 94 | 95 | list_of_image_file_extensions = [".bmp", ".jpg", ".jpeg", ".png"] 96 | 97 | files_to_read = File_Utils.get_files_within_dir(dir_path, list_of_image_file_extensions) 98 | 99 | return_dict = {} 100 | for file_path in files_to_read: 101 | 102 | head, file_name = os.path.split(file_path) 103 | 104 | return_dict[file_name] = File_Utils.read_image_from_file(file_path) 105 | 106 | return return_dict 107 | 108 | ############################################################################### 109 | # Given file path, reads in content as string and returns it 110 | @staticmethod 111 | def read_text_from_file(file_path): 112 | f = open(file_path) 113 | text_string = f.read() 114 | f.close() 115 | return text_string 116 | 117 | ############################################################################### 118 | # Given directory path, reads content from all files within the directory 119 | # Does not recursively search for files within any subdirectories 120 | # Returns a dictionary with file_name root -> text string 121 | @staticmethod 122 | def read_texts_from_directory(dir_path): 123 | 124 | list_of_text_file_extensions = [".txt"] 125 | 126 | files_to_read = File_Utils.get_files_within_dir(dir_path, list_of_text_file_extensions) 127 | 128 | return_dict = {} 129 | for file_path in files_to_read: 130 | 131 | head, file_name = os.path.split(file_path) 132 | 133 | return_dict[file_name] = File_Utils.read_text_from_file(file_path) 134 | 135 | return return_dict 136 | 137 | ############################################################################### 138 | # Given string and path, write string to path filename 139 | @staticmethod 140 | def write_string_to_file(content_string, file_path): 141 | 142 | f = open(file_path, "w+") 143 | f.write(content_string) 144 | f.close() 145 | return "" 146 | 147 | ############################################################################### 148 | # Given dictionary of file_name->strings and directory path, writes each string 149 | # to a separate file 150 | @staticmethod 151 | def write_strings_to_directory_files(dict_of_strings, dir_path): 152 | 153 | # Write each string individually 154 | for file_name in dict_of_strings: 155 | 156 | # Construct the file name: 157 | file_path = os.path.join(dir_path, str(file_name) + ".txt") 158 | 159 | # Write file: 160 | File_Utils.write_string_to_file(dict_of_strings[file_name], file_path) 161 | 162 | return "" 163 | 164 | ############################################################################### 165 | # FILE HANDLER METHODS ######################################################## 166 | ############################################################################### 167 | 168 | ############################################################################### 169 | # Gets a file handle (for easy file writing) 170 | @staticmethod 171 | def get_writing_fh(file_path): 172 | fh = open(file_path, "w+") 173 | 174 | ############################################################################### 175 | # Writes a line to the file handler 176 | @staticmethod 177 | def write_fh_line(fh, string_content): 178 | fh.write(string_content) 179 | 180 | ############################################################################### 181 | # Closes a file handler 182 | @staticmethod 183 | def close_fh(fh): 184 | fh.close() 185 | -------------------------------------------------------------------------------- /hvf_bulk_processing.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_bulk_processing.py 3 | # 4 | # Description: 5 | # Given a directory of HVF files, outputs them in a CSV file format. See 6 | # code for ordering/column info 7 | # 8 | # Usage: 9 | # python hvf_bulk_processing -i 10 | # Outputs a spreadsheet TSV file "output_spreadsheet.tsv" from images 11 | # 12 | # python hvf_bulk_processing -t 13 | # Outputs a spreadsheet TSV file "output_spreadsheet.tsv" from JSON 14 | # text files 15 | # 16 | # python hvf_bulk_processing -s 17 | # Outputs a directory of JSON text files into directory 18 | # "serialized_hvf"; makes directory if does not exist 19 | # 20 | # python hvf_bulk_processing -d 21 | # Outputs a directory of JSON text files into directory 22 | # "serialized_hvf"; makes directory if does not exist 23 | # 24 | # python hvf_bulk_processing -f 25 | # Outputs a directory of JSON text files into directory 26 | # "serialized_hvf"; makes directory if does not exist 27 | # 28 | ############################################################################### 29 | 30 | # Import necessary packages 31 | import argparse 32 | import os 33 | 34 | # import dlib 35 | # Import the HVF_Object class 36 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 37 | 38 | # Import tester class: 39 | from hvf_extraction_script.hvf_manager.hvf_export import Hvf_Export 40 | 41 | # Import file utilities 42 | from hvf_extraction_script.utilities.file_utils import File_Utils 43 | 44 | # Import logger class to handle any messages: 45 | from hvf_extraction_script.utilities.logger import Logger 46 | 47 | # Construct the argument parse and parse the arguments 48 | ap = argparse.ArgumentParser() 49 | ap.add_argument("-i", "--image_directory", required=False, help="path to directory of images to convert to spreadsheet") 50 | ap.add_argument( 51 | "-t", "--text_directory", required=False, help="path to directory of text files to convert to spreadsheet" 52 | ) 53 | ap.add_argument( 54 | "-s", "--save_images", required=False, help="path to directory of image files to read and save as text documents" 55 | ) 56 | ap.add_argument("-f", "--import_file", required=False, help="path to TSV file to import and save as text documents") 57 | ap.add_argument( 58 | "-d", "--dicom_file", required=False, help="path to directory of DICOM files to convert to text documents" 59 | ) 60 | args = vars(ap.parse_args()) 61 | 62 | 63 | ############################################################################### 64 | # HELPER METHODS ############################################################## 65 | ############################################################################### 66 | 67 | ############################################################################### 68 | # From a directory of images, returns a dictionary of HVF objects: 69 | 70 | 71 | def get_dict_of_hvf_objs_from_imgs(directory): 72 | 73 | # Read in images from directory: 74 | list_of_image_file_extensions = [".bmp", ".jpg", ".jpeg", ".png"] 75 | list_of_img_paths = File_Utils.get_files_within_dir(directory, list_of_image_file_extensions) 76 | 77 | dict_of_hvf_objs = {} 78 | 79 | for hvf_img_path in list_of_img_paths: 80 | 81 | path, filename = os.path.split(hvf_img_path) 82 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Reading HVF image " + filename) 83 | hvf_img = File_Utils.read_image_from_file(hvf_img_path) 84 | 85 | hvf_obj = Hvf_Object.get_hvf_object_from_image(hvf_img) 86 | hvf_obj.release_saved_image() 87 | dict_of_hvf_objs[filename] = hvf_obj 88 | 89 | return dict_of_hvf_objs 90 | 91 | 92 | ############################################################################### 93 | # From a directory of text files, returns a dictionary of HVF objects: 94 | 95 | 96 | def get_dict_of_hvf_objs_from_text(directory): 97 | 98 | # Read in text files from directory: 99 | list_of_file_extensions = [".txt"] 100 | list_of_txt_paths = File_Utils.get_files_within_dir(directory, list_of_file_extensions) 101 | 102 | dict_of_hvf_objs = {} 103 | 104 | for hvf_txt_path in list_of_txt_paths: 105 | 106 | path, filename = os.path.split(hvf_txt_path) 107 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Reading HVF text file " + filename) 108 | 109 | hvf_txt = File_Utils.read_text_from_file(hvf_txt_path) 110 | 111 | hvf_obj = Hvf_Object.get_hvf_object_from_text(hvf_txt) 112 | 113 | dict_of_hvf_objs[filename] = hvf_obj 114 | 115 | return dict_of_hvf_objs 116 | 117 | 118 | ############################################################################### 119 | # BULK PROCESSING ############################################################# 120 | ############################################################################### 121 | 122 | Logger.set_logger_level(Logger.DEBUG_FLAG_SYSTEM) 123 | 124 | # If flag, then do unit tests: 125 | if args["image_directory"]: 126 | 127 | # Grab the argument directory for readability 128 | directory = args["image_directory"] 129 | 130 | dict_of_hvf_objs = get_dict_of_hvf_objs_from_imgs(directory) 131 | 132 | return_string = Hvf_Export.export_hvf_list_to_spreadsheet(dict_of_hvf_objs) 133 | 134 | File_Utils.write_string_to_file(return_string, "output_spreadsheet.tsv") 135 | 136 | elif args["text_directory"]: 137 | 138 | # Grab the argument directory for readability 139 | directory = args["text_directory"] 140 | 141 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "========== START READING ALL TEXT FILES ==========") 142 | dict_of_hvf_objs = get_dict_of_hvf_objs_from_text(directory) 143 | 144 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "========== FINISHED READING ALL TEXT FILES ==========") 145 | 146 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "========== START EXPORT ==========") 147 | 148 | return_string = Hvf_Export.export_hvf_list_to_spreadsheet(dict_of_hvf_objs) 149 | 150 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "========== WRITING EXPORT SPREADSHEET ==========") 151 | 152 | File_Utils.write_string_to_file(return_string, "output_spreadsheet.tsv") 153 | 154 | elif args["save_images"]: 155 | 156 | directory = args["save_images"] 157 | 158 | save_dir = "serialized_hvfs" 159 | 160 | if not os.path.isdir(save_dir): 161 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Making new save directory: " + save_dir) 162 | 163 | os.mkdir(save_dir) 164 | 165 | list_of_image_file_extensions = [".bmp", ".jpg", ".jpeg", ".png"] 166 | list_of_img_paths = File_Utils.get_files_within_dir(directory, list_of_image_file_extensions) 167 | 168 | for hvf_img_path in list_of_img_paths: 169 | 170 | path, filename = os.path.split(hvf_img_path) 171 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Reading HVF image " + filename) 172 | hvf_img = File_Utils.read_image_from_file(hvf_img_path) 173 | 174 | try: 175 | hvf_obj = Hvf_Object.get_hvf_object_from_image(hvf_img) 176 | 177 | file_path = os.path.join(save_dir, str(filename) + ".txt") 178 | 179 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Writing text serialization file " + filename) 180 | File_Utils.write_string_to_file(hvf_obj.serialize_to_json(), file_path) 181 | 182 | except: 183 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "============= FAILURE on serializing " + filename) 184 | 185 | elif args["import_file"]: 186 | 187 | path_to_tsv_file = args["import_file"] 188 | 189 | save_dir = "serialized_hvfs" 190 | 191 | if not os.path.isdir(save_dir): 192 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Making new save directory: " + save_dir) 193 | 194 | os.mkdir(save_dir) 195 | 196 | tsv_file_string = File_Utils.read_text_from_file(path_to_tsv_file) 197 | dict_of_hvf_objs = Hvf_Export.import_hvf_list_from_spreadsheet(tsv_file_string) 198 | 199 | for filename in dict_of_hvf_objs.keys(): 200 | hvf_obj = dict_of_hvf_objs.get(filename) 201 | hvf_serialized = hvf_obj.serialize_to_json() 202 | 203 | try: 204 | filename_root, ext = os.path.splitext(filename) 205 | 206 | if not (ext == "txt"): 207 | filename = filename + ".txt" 208 | 209 | file_path = os.path.join(save_dir, str(filename)) 210 | 211 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Writing text serialization file " + filename) 212 | File_Utils.write_string_to_file(hvf_serialized, file_path) 213 | 214 | except: 215 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "============= FAILURE on serializing " + filename) 216 | 217 | elif args["dicom_file"]: 218 | 219 | directory = args["dicom_file"] 220 | 221 | save_dir = "serialized_hvfs" 222 | 223 | if not os.path.isdir(save_dir): 224 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Making new save directory: " + save_dir) 225 | 226 | os.mkdir(save_dir) 227 | 228 | list_of_file_extensions = [".dcm"] 229 | list_of_paths = File_Utils.get_files_within_dir(directory, list_of_file_extensions) 230 | 231 | for hvf_dcm_path in list_of_paths: 232 | 233 | path, filename = os.path.split(hvf_dcm_path) 234 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Reading HVF DICOM " + filename) 235 | hvf_dicom_ds = File_Utils.read_dicom_from_file(hvf_dcm_path) 236 | 237 | try: 238 | hvf_obj = Hvf_Object.get_hvf_object_from_dicom(hvf_dicom_ds) 239 | 240 | file_path = os.path.join(save_dir, str(filename) + ".txt") 241 | 242 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "Writing text serialization file " + filename) 243 | 244 | File_Utils.write_string_to_file(hvf_obj.serialize_to_json(), file_path) 245 | 246 | except: 247 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, "============= FAILURE on serializing " + filename) 248 | 249 | 250 | else: 251 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_ERROR, "No input directory given") 252 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/image_utils.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # image_utils.py 3 | # 4 | # Description: 5 | # Class definition for commonly used, general use image functions 6 | # 7 | ############################################################################### 8 | 9 | from functools import reduce 10 | 11 | # Import necessary packages 12 | import cv2 13 | 14 | # Import some helper packages: 15 | import numpy as np 16 | from PIL import Image 17 | 18 | # For error/debug logging: 19 | from hvf_extraction_script.utilities.logger import Logger 20 | from hvf_extraction_script.utilities.regex_utils import Regex_Utils 21 | 22 | 23 | class Image_Utils: 24 | 25 | ############################################################################### 26 | # CONSTANTS AND STATIC VARIABLES ############################################## 27 | ############################################################################### 28 | 29 | ############################################################################### 30 | # IMAGE PROCESSING METHODS #################################################### 31 | ############################################################################### 32 | 33 | ############################################################################### 34 | # Preprocessing image to enhance image quality 35 | @staticmethod 36 | def preprocess_image(image, debug_dir=""): 37 | 38 | # Look up how to optimize/preprocess images for tesseract - this is a common issue 39 | 40 | # Median Blurring: 41 | # image = cv2.medianBlur(image, 7) 42 | 43 | # Gaussian Blurring: 44 | # image = cv2.GaussianBlur(image,(3,3),0) 45 | 46 | # Thresholding: 47 | # image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)[1]; 48 | 49 | # This works out to best preprocess - esp for photo images that have variable 50 | # lighting 51 | image = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) 52 | 53 | if debug_dir: 54 | out = Regex_Utils.temp_out(debug_dir=debug_dir) 55 | img_pil = Image.fromarray(image) 56 | img_pil.save(f"{out}.jpg") 57 | 58 | return image 59 | 60 | ############################################################################### 61 | # Given starting coordinates and corresponding slice sizes (all in fractions of 62 | # the total image size), slices the input image. This uses Numpy slicing 63 | @staticmethod 64 | def slice_image(image, y_ratio, y_size, x_ratio, x_size, debug_dir=""): 65 | 66 | # Calculate height/width for slicing later: 67 | height = np.size(image, 0) 68 | width = np.size(image, 1) 69 | 70 | # Calculate the starting and ending indices of y and x: 71 | y1 = int(height * y_ratio) 72 | y2 = int(height * (y_ratio + y_size)) 73 | 74 | x1 = int(width * x_ratio) 75 | x2 = int(width * (x_ratio + x_size)) 76 | 77 | image_slice = image[y1:y2, x1:x2] 78 | 79 | if debug_dir: 80 | img = Image.fromarray(image_slice) 81 | out = Regex_Utils.temp_out(debug_dir=debug_dir) 82 | img.save(f"{out}.jpg") 83 | 84 | return image_slice 85 | 86 | ############################################################################### 87 | # Given an image, returns the coordinates cropping the image (ie, eliminates white 88 | # border). Returns bounding x's and y's 89 | @staticmethod 90 | def crop_white_border(image): 91 | 92 | # Crop out the borders so we just have the central values - this allows us 93 | # to standardize size 94 | # First, crop the white border out of the element to get just the core icon: 95 | x0, y0 = 0, 0 96 | x1, y1 = np.size(image, 1) - 1, np.size(image, 0) - 1 97 | 98 | element_mask = image > 0 99 | 100 | # Find bounding ys: 101 | for row_index in range(0, np.size(image, 0)): 102 | if reduce((lambda x, y: x and y), element_mask[row_index, :]): # All white row 103 | y0 = row_index 104 | continue 105 | else: # We have at least 1 black pixel - stop 106 | break 107 | 108 | for row_index in range(np.size(image, 0) - 1, 0, -1): 109 | if reduce((lambda x, y: x and y), element_mask[row_index, :]): # All white row 110 | y1 = row_index 111 | continue 112 | else: # We have at least 1 black pixel - stop 113 | break 114 | 115 | # Find bounding xs: 116 | for col_index in range(0, np.size(image, 1)): 117 | if reduce((lambda x, y: x and y), element_mask[:, col_index]): # All white row 118 | x0 = col_index 119 | continue 120 | else: # We have at least 1 black pixel - stop 121 | break 122 | 123 | for col_index in range(np.size(image, 1) - 1, 0, -1): 124 | if reduce((lambda x, y: x and y), element_mask[:, col_index]): # All white row 125 | x1 = col_index 126 | continue 127 | else: # We have at least 1 black pixel - stop 128 | break 129 | 130 | return x0, x1, y0, y1 131 | 132 | ############################################################################### 133 | # Helper function for bounding box area of a contour 134 | def contour_bound_box_area(x): 135 | x, y, w, h = cv2.boundingRect(x) 136 | return w * h 137 | 138 | ############################################################################### 139 | # Given a image, masks out contours of a certain size or smaller (based 140 | # on fraction of total plot element or relative to largest contour) 141 | @staticmethod 142 | def delete_stray_marks(image, global_threshold, relative_threshold): 143 | 144 | # Threshold by area when to remove a contour: 145 | plot_area = np.size(image, 0) * np.size(image, 1) 146 | 147 | image_temp = cv2.bitwise_not(image.copy()) 148 | cnts, hierarchy = cv2.findContours(image_temp, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 149 | mask = np.ones(image_temp.shape[:2], dtype="uint8") * 255 150 | 151 | # We want to eliminate small contours. Define relative to entire plot area and/or 152 | # relative to largest contour 153 | cnts = sorted(cnts, key=Image_Utils.contour_bound_box_area, reverse=True) 154 | 155 | largest_contour_area = 0 156 | 157 | if len(cnts) > 0: 158 | largest_contour_area = Image_Utils.contour_bound_box_area(cnts[0]) 159 | 160 | contours_to_mask = [] 161 | 162 | # Loop over the contours 163 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Looping through contours, length " + str(len(cnts))) 164 | for c in cnts: 165 | 166 | # Grab size of contour: 167 | # Can also consider using cv2.contourArea(cnt); 168 | contour_area = Image_Utils.contour_bound_box_area(c) 169 | contour_plot_size_fraction = contour_area / plot_area 170 | contour_relative_size_fraction = contour_area / largest_contour_area 171 | 172 | Logger.get_logger().log_msg( 173 | Logger.DEBUG_FLAG_DEBUG, 174 | "Contour plot size fraction: " 175 | + str(contour_plot_size_fraction) 176 | + "; contour relative size fraction: " 177 | + str(contour_relative_size_fraction), 178 | ) 179 | 180 | # if the contour is too small, draw it on the mask 181 | if contour_plot_size_fraction < global_threshold or contour_relative_size_fraction < relative_threshold: 182 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Found a small contour, masking out") 183 | contours_to_mask.append(c) 184 | 185 | cv2.drawContours(mask, contours_to_mask, -1, 0, -1) 186 | 187 | # remove the contours from the image 188 | image = cv2.bitwise_not(cv2.bitwise_and(image_temp, image_temp, mask=mask)) 189 | 190 | return image 191 | 192 | ############################################################################### 193 | # Given a image, determines width of axis (assume cross hairs plot centred in image) 194 | # Assumes image is black and white binarized 195 | @staticmethod 196 | def measure_plot_axis_width(image, is_x_axis): 197 | 198 | # For readability, grab our height/width: 199 | plot_width = np.size(image, 1) 200 | plot_height = np.size(image, 0) 201 | 202 | if is_x_axis: 203 | lower_point = int(plot_width / 3) 204 | 205 | else: 206 | lower_point = int(plot_height / 3) 207 | 208 | upper_point = lower_point * 2 209 | 210 | lower_axis_width = 0 211 | upper_axis_width = 0 212 | 213 | # Lower side First 214 | while True: 215 | 216 | lower_point_low = 255 217 | upper_point_low = 255 218 | 219 | if is_x_axis: 220 | lower_point_low = image[int(plot_height / 2) - lower_axis_width, lower_point] 221 | upper_point_low = image[int(plot_height / 2) - lower_axis_width, upper_point] 222 | 223 | else: 224 | lower_point_low = image[lower_point, int(plot_width / 2) - lower_axis_width] 225 | upper_point_low = image[upper_point, int(plot_width / 2) - lower_axis_width] 226 | 227 | if (lower_point_low == 0) and (upper_point_low == 0): 228 | lower_axis_width = lower_axis_width + 1 229 | else: 230 | break 231 | 232 | # Then upper side: 233 | 234 | while True: 235 | 236 | lower_point_high = 255 237 | upper_point_high = 255 238 | 239 | if is_x_axis: 240 | lower_point_high = image[int(plot_height / 2) + upper_axis_width, lower_point] 241 | upper_point_high = image[int(plot_height / 2) + upper_axis_width, upper_point] 242 | 243 | else: 244 | lower_point_high = image[lower_point, int(plot_width / 2) + upper_axis_width] 245 | upper_point_high = image[upper_point, int(plot_width / 2) + upper_axis_width] 246 | 247 | if (lower_point_high == 0) and (upper_point_high == 0): 248 | upper_axis_width = upper_axis_width + 1 249 | else: 250 | break 251 | 252 | return lower_axis_width, upper_axis_width 253 | -------------------------------------------------------------------------------- /hvf_extraction_script/utilities/regex_utils.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # regex_utils.py 3 | # 4 | # Description: 5 | # Class definition for regex utility functions 6 | # 7 | ############################################################################### 8 | 9 | # Import necessary packages 10 | # Import regular expression packages 11 | import os 12 | import re 13 | from glob import glob 14 | 15 | import regex 16 | from fuzzysearch import find_near_matches 17 | from fuzzywuzzy import fuzz, process 18 | 19 | 20 | class Regex_Utils: 21 | ############################################################################### 22 | # CONSTANTS AND STATIC VARIABLES ############################################## 23 | ############################################################################### 24 | 25 | REGEX_FAILURE = "Extraction Failure" 26 | 27 | ############################################################################### 28 | # REGEX METHODS ############################################################### 29 | ############################################################################### 30 | 31 | ############################################################################### 32 | # Given a label and a list of strings to search within, fuzzy matches the label into 33 | # the best-matched string and returns the field following the label. 34 | # Example: 35 | # Label = "Name: " 36 | # String = ["Na'me: Smith, John", "ID:55555555", "Fovea: 35?B"] 37 | # Returns: "Smith, John" 38 | @staticmethod 39 | def fuzzy_regex(label, string_list): 40 | 41 | ret = "" 42 | 43 | # Only remove the best-matched string if we have a good score. If the score is poor, 44 | # possible that its a wrong match and deleting it will cause that field to have 45 | # a wrong match 46 | threshold_to_remove = 85 47 | 48 | filtered_string_list = list(filter(lambda x: len(x) >= len(label), string_list)) 49 | 50 | if len(filtered_string_list) == 0: 51 | 52 | ret = Regex_Utils.REGEX_FAILURE 53 | 54 | else: 55 | 56 | # First, search and extract the highest match: 57 | string_match = process.extractOne(label, filtered_string_list, scorer=fuzz.partial_ratio) 58 | string = string_match[0] 59 | score = string_match[1] 60 | 61 | # If its a sufficiently high match, then remove it from the list to help next searches 62 | if score >= threshold_to_remove: 63 | string_list.remove(string) 64 | 65 | # Fuzzysearch for best match of label within the string: 66 | match = find_near_matches(label, string, max_l_dist=2) 67 | 68 | # Need to sort and pull out best match: 69 | 70 | if len(match) == 0: 71 | ret = Regex_Utils.REGEX_FAILURE 72 | 73 | else: 74 | 75 | best_match = sorted(match, key=lambda i: i.dist)[0] 76 | 77 | # Convert the best match into actual string slice 78 | best_match_string = string[best_match.start : best_match.end] 79 | 80 | # Construct regex based on this slice 81 | regex = best_match_string + r"\s*(.*)" 82 | 83 | try: 84 | # Perform the regex search to find the text of interest 85 | output = re.search(regex, string) 86 | 87 | ret = output.group(1) 88 | 89 | except Exception: 90 | print(label + " Failed searches") 91 | ret = Regex_Utils.REGEX_FAILURE 92 | 93 | return ret, string_list 94 | 95 | @staticmethod 96 | def strict_regex(label, string_list): 97 | 98 | ret = "" 99 | 100 | # Only remove the best-matched string if we have a good score. If the score is poor, 101 | # possible that its a wrong match and deleting it will cause that field to have 102 | # a wrong match 103 | threshold_to_remove = 85 104 | 105 | filtered_string_list = list(filter(lambda x: len(x) >= len(label), string_list)) 106 | 107 | if len(filtered_string_list) == 0: 108 | 109 | ret = Regex_Utils.REGEX_FAILURE 110 | 111 | else: 112 | 113 | # First, search and extract the highest match: 114 | string_match = process.extractOne(label, filtered_string_list, scorer=fuzz.partial_ratio) 115 | string = string_match[0] 116 | score = string_match[1] 117 | 118 | # If its a sufficiently high match, then remove it from the list to help next searches 119 | if score >= threshold_to_remove: 120 | string_list.remove(string) 121 | 122 | # Fuzzysearch for best match of label within the string: 123 | match = find_near_matches(label, string, max_l_dist=1) 124 | 125 | # Need to sort and pull out best match: 126 | 127 | if len(match) == 0: 128 | ret = Regex_Utils.REGEX_FAILURE 129 | 130 | else: 131 | 132 | best_match = sorted(match, key=lambda i: i.dist)[0] 133 | 134 | # Convert the best match into actual string slice 135 | best_match_string = string[best_match.start : best_match.end] 136 | 137 | # Construct regex based on this slice 138 | regex = best_match_string + r"\s*(.*)" 139 | 140 | try: 141 | # Perform the regex search to find the text of interest 142 | output = re.search(regex, string) 143 | 144 | ret = output.group(1) 145 | 146 | except Exception: 147 | print(label + " Failed searches") 148 | ret = Regex_Utils.REGEX_FAILURE 149 | 150 | return ret, string_list 151 | 152 | ############################################################################### 153 | # Given a label, a regular expression, and a list of strings to search within, fuzzy 154 | # matches the label into the best-matched string within the list, then applies the 155 | # regex onto that string to extract the string of interest 156 | # Example: 157 | # Label = Central Threshold" 158 | # regex_str = "Central (.*) Threshold{e<=2}" 159 | # String = ["Na'me: Smith, John", "ID:55555555", "Fovea: 35?B", Centra'l 24-2 Threshold] 160 | # Returns: "24-2" 161 | 162 | @staticmethod 163 | def fuzzy_regex_middle_field(label, regex_str, string_list): 164 | 165 | ret = "" 166 | 167 | filtered_string_list = list(filter(lambda x: len(x) >= len(label), string_list)) 168 | 169 | # Only remove the best-matched string if we have a good score. If the score is poor, 170 | # possible that its a wrong match and deleting it will cause that field to have 171 | # a wrong match 172 | threshold_to_remove = 85 173 | 174 | if len(filtered_string_list) == 0: 175 | ret = Regex_Utils.REGEX_FAILURE 176 | else: 177 | 178 | # First, search and extract the highest match: 179 | string_match = process.extractOne(label, filtered_string_list, scorer=fuzz.partial_token_sort_ratio) 180 | 181 | if string_match is None: 182 | ret = Regex_Utils.REGEX_FAILURE 183 | 184 | else: 185 | string = string_match[0] 186 | score = string_match[1] 187 | 188 | # If its a sufficiently high match, then remove it from the list to help next searches 189 | if score >= threshold_to_remove: 190 | string_list.remove(string) 191 | 192 | # Perform the regex search to find the text of interest 193 | output = regex.search(regex_str, string) 194 | 195 | try: 196 | ret = output.group(1) 197 | 198 | except Exception: 199 | ret = Regex_Utils.REGEX_FAILURE 200 | 201 | return ret, string_list 202 | 203 | ############################################################################### 204 | # Removes any spaces from the string. Pass through if extraction failure. 205 | def remove_spaces(string): 206 | 207 | if string == Regex_Utils.REGEX_FAILURE: 208 | return string 209 | 210 | else: 211 | return string.replace(" ", "") 212 | 213 | ############################################################################### 214 | # Cleans up erroneous punctuation. Pass through if extraction failure. 215 | def clean_punctuation_to_period(string): 216 | if string == Regex_Utils.REGEX_FAILURE: 217 | return string 218 | 219 | else: 220 | string = string.replace(",", ".") 221 | string = string.replace(";", ".") 222 | string = ".".join(list(filter(None, string.split(".")))) 223 | 224 | return string 225 | 226 | ############################################################################### 227 | # Removes non-numeric characters from the string. Keeps number characters, '.' and '-' 228 | def remove_non_numeric(string, safe_char_list): 229 | if string == Regex_Utils.REGEX_FAILURE: 230 | return string 231 | 232 | else: 233 | 234 | regex_str = "^0-9" 235 | 236 | for char in safe_char_list: 237 | regex_str = regex_str + "^\\" + char 238 | 239 | string = re.sub(f"[{regex_str}]", "", string) 240 | 241 | return string 242 | 243 | ############################################################################### 244 | # Adds a decimal point if none is detect. Assumes that number should have 2 spaces 245 | # after decimal point 246 | def add_decimal_if_absent(string): 247 | if string == Regex_Utils.REGEX_FAILURE: 248 | return string 249 | elif not (re.search(r"(\.)", string) and len(string) > 2): 250 | 251 | i = len(string) - 2 252 | 253 | string = string[:i] + "." + string[i:] 254 | 255 | return string 256 | 257 | ############################################################################### 258 | # Clean minus sign. Condense any similar-looking prefixes into a single minus sign 259 | @staticmethod 260 | def clean_minus_sign(string): 261 | if string == Regex_Utils.REGEX_FAILURE: 262 | return string 263 | else: 264 | string = re.sub(r"(\=)+", "-", string) 265 | string = re.sub(r"^(\-)+", "-", string) 266 | 267 | return string 268 | 269 | ############################################################################### 270 | # Clear out non ASCII unicode characters 271 | @staticmethod 272 | def clean_nonascii(string: str) -> str: 273 | if string == Regex_Utils.REGEX_FAILURE: 274 | return string 275 | else: 276 | string = string.encode("ascii", "ignore").decode("unicode_escape") 277 | 278 | return string 279 | 280 | @staticmethod 281 | def temp_out(debug_dir: str = ".", string: str = "debug_pass_") -> str: 282 | max_val = 0 283 | string2 = os.path.join(debug_dir, string) 284 | gg = [re.search(r"_(\d+)\.", x) for x in glob(f"{string2}*.*")] 285 | if gg: 286 | max_val = max(map(int, [x.group(1) for x in gg])) # type: ignore 287 | return f"{string2}{max_val+1}" 288 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_manager/hvf_metric_calculator.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_metric_calculator.py 3 | # 4 | # Description: 5 | # Functions for calculating global and regional metrics of an HVF_Object. 6 | # Meant to be used as static methods (no need to instantiate calculator obj) 7 | # 8 | # These functions assume a 24-2 field size. Any 30-2 HVF objects will be 9 | # converted (masked) into 24-2. This function will produce erroneous metrics 10 | # for 10-2. 11 | # 12 | ############################################################################### 13 | 14 | # Import necessary packages 15 | 16 | import numpy as np 17 | 18 | # Import the HVF_Object class and helper classes 19 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 20 | from hvf_extraction_script.hvf_data.hvf_perc_icon import Hvf_Perc_Icon 21 | from hvf_extraction_script.hvf_data.hvf_plot_array import Hvf_Plot_Array 22 | from hvf_extraction_script.hvf_data.hvf_value import Hvf_Value 23 | 24 | # Import HVF data managers: 25 | from hvf_extraction_script.hvf_manager.hvf_editor import Hvf_Editor 26 | 27 | # Import logger class to handle any messages: 28 | 29 | 30 | class Hvf_Metric_Calculator: 31 | 32 | ############################################################################### 33 | # VARIABLE/CONSTANT DECLARATIONS: ############################################# 34 | ############################################################################### 35 | 36 | ############################################################################### 37 | # 2D array specifying individual regions within a 24-2 field. This mask is for 38 | # a RIGHT eye (must be transposed for left). 39 | # 40 | # Pattern follows: 41 | # 4 4 | 6 6 42 | # 4 4 4 | 4 6 6 43 | # 8 8 2 2 | 2 2 x x 44 | # 8 8 8 0 0 | 0 x x 45 | # --------------------- 46 | # 9 9 9 1 1 | 1 x x 47 | # 9 9 3 3 | 3 3 x x 48 | # 5 5 5 | 5 7 7 49 | # 5 5 | 7 7 50 | # 51 | # Labels (superior/inferior): 52 | # 0, 1 - Central 53 | # 2, 3 - Paracentral 54 | # 4, 5 - Arcuate 1 55 | # 6, 7 - Arcuate 2 56 | # 8, 9 - Nasal 57 | REGION_NUMERICAL_MASK = [ 58 | ["x", "x", "x", "x", "x", "x", "x", "x", "x", "x"], 59 | ["x", "x", "x", 4, 4, 6, 6, "x", "x", "x"], 60 | ["x", "x", 4, 4, 4, 4, 6, 6, "x", "x"], 61 | ["x", 8, 8, 2, 2, 2, 2, "x", "x", "x"], 62 | [8, 8, 8, 0, 0, 0, "x", "x", "x", "x"], 63 | [9, 9, 9, 1, 1, 1, "x", "x", "x", "x"], 64 | ["x", 9, 9, 3, 3, 3, 3, "x", "x", "x"], 65 | ["x", "x", 5, 5, 5, 5, 7, 7, "x", "x"], 66 | ["x", "x", "x", 5, 5, 7, 7, "x", "x", "x"], 67 | ["x", "x", "x", "x", "x", "x", "x", "x", "x", "x"], 68 | ] 69 | 70 | NUMBER_REGIONS = 10 71 | 72 | ############################################################################### 73 | # CIGTS SCORING CONSTANTS: 74 | 75 | CIGTS_ICON_SCORE = { 76 | Hvf_Perc_Icon.PERC_NORMAL: 0, 77 | Hvf_Perc_Icon.PERC_5_PERCENTILE: 1, 78 | Hvf_Perc_Icon.PERC_2_PERCENTILE: 2, 79 | Hvf_Perc_Icon.PERC_1_PERCENTILE: 3, 80 | Hvf_Perc_Icon.PERC_HALF_PERCENTILE: 4, 81 | } 82 | 83 | ############################################################################### 84 | # SIMPLE METRIC FUNCTIONS ##################################################### 85 | ############################################################################### 86 | 87 | ############################################################################### 88 | # Calculates VFI score 89 | def get_vfi_score(hvf_obj): 90 | 91 | return 0 92 | 93 | ############################################################################### 94 | # Calculates regional total deviation score 95 | def get_regional_total_deviation(hvf_obj): 96 | 97 | return 0 98 | 99 | ############################################################################### 100 | # Calculates regional pattern deviation score 101 | def get_regional_pattern_deviation(hvf_obj): 102 | 103 | return 0 104 | 105 | ############################################################################### 106 | # CIGTS METRIC FUNCTIONS ###################################################### 107 | ############################################################################### 108 | 109 | # Based on CIGTS trial. Calculation algorithm based on paper: 110 | # The Collaborative Initial Glaucoma Treatment Study: Baseline Visual Field 111 | # and Test-Retest Variability 112 | 113 | ############################################################################### 114 | # Calculates global CIGTS TDP score 115 | def get_global_cigts_tdp_score(hvf_obj): 116 | 117 | # Grab the plot_object, and a few pertinent metadata 118 | plot_obj = hvf_obj.abs_dev_percentile_array 119 | is_right = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) == Hvf_Object.HVF_OD 120 | 121 | # Mask to 24-2 if necessary: 122 | if hvf_obj.metadata.get(Hvf_Object.KEYLABEL_FIELD_SIZE) == Hvf_Object.HVF_30_2: 123 | Hvf_Editor.mask_302_to_242(plot_obj, is_right) 124 | 125 | # Now take the perc_array: 126 | perc_array = plot_obj.plot_array 127 | 128 | # If left, need to transpose: 129 | if not is_right: 130 | perc_array = Hvf_Editor.transpose_array(perc_array) 131 | 132 | cigts_score_array = Hvf_Metric_Calculator.calculate_cigts_score_array(perc_array) 133 | 134 | return int(np.sum(cigts_score_array) / 10.4) 135 | 136 | ############################################################################### 137 | # Calculates global CIGTS PDP score 138 | def get_global_cigts_pdp_score(hvf_obj): 139 | 140 | # Grab the plot_object, and a few pertinent metadata 141 | plot_obj = hvf_obj.pat_dev_percentile_array 142 | is_right = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) == Hvf_Object.HVF_OD 143 | 144 | # Mask to 24-2 if necessary: 145 | if hvf_obj.metadata.get(Hvf_Object.KEYLABEL_FIELD_SIZE) == Hvf_Object.HVF_30_2: 146 | Hvf_Editor.mask_302_to_242(plot_obj, is_right) 147 | 148 | # Now take the perc_array: 149 | perc_array = plot_obj.plot_array 150 | 151 | # If left, need to transpose: 152 | if not is_right: 153 | perc_array = Hvf_Editor.transpose_array(perc_array) 154 | 155 | cigts_score_array = Hvf_Metric_Calculator.calculate_cigts_score_array(perc_array) 156 | 157 | return int(np.sum(cigts_score_array) / 10.4) 158 | 159 | ############################################################################### 160 | # Calculates regional CIGTS TDP score 161 | def get_regional_cigts_tdp_score(hvf_obj): 162 | 163 | return 0 164 | 165 | ############################################################################### 166 | # Calculates regional CIGTS PDP score 167 | def get_regional_cigts_pdp_score(hvf_obj): 168 | 169 | return 0 170 | 171 | ############################################################################### 172 | # Helper function - calculates CIGTS score for input percentile array 173 | # NOTE: calculates raw scores in array layout (10x10), to be summed by 174 | # calling function 175 | def calculate_cigts_score_array(perc_array): 176 | 177 | cigts_score = 0 178 | cigts_score_array = np.zeros(np.shape(perc_array)) 179 | 180 | # Iterate through entire array: 181 | for y in range(0, np.size(perc_array, 0)): 182 | for x in range(0, np.size(perc_array, 1)): 183 | 184 | # Grab the icon: 185 | element = perc_array[x, y].get_enum() 186 | 187 | # Move on if element is not a true icon, or if its normal 188 | if (element == Hvf_Perc_Icon.PERC_NORMAL) or element not in Hvf_Metric_Calculator.CIGTS_ICON_SCORE.keys(): 189 | continue 190 | 191 | # Initialize counts of surrounding icons: 192 | icon_counter_dict = { 193 | Hvf_Perc_Icon.PERC_NORMAL: 0, 194 | Hvf_Perc_Icon.PERC_5_PERCENTILE: 0, 195 | Hvf_Perc_Icon.PERC_2_PERCENTILE: 0, 196 | Hvf_Perc_Icon.PERC_1_PERCENTILE: 0, 197 | Hvf_Perc_Icon.PERC_HALF_PERCENTILE: 0, 198 | } 199 | 200 | # Iterate through all surrounding icons 201 | # We'll also iterate through icon itself, as its easier (delete it later) 202 | for jj in range(y - 1, y + 2): 203 | for ii in range(x - 1, x + 2): 204 | 205 | # Move on if out of bounds: 206 | if (jj < 0 or jj >= np.size(perc_array, 0)) or (ii < 0 or ii >= np.size(perc_array, 1)): 207 | continue 208 | 209 | # Get adjacent element enum: 210 | adj_element_enum = perc_array[ii, jj].get_enum() 211 | 212 | # Move on if adjacent icon is not a true icon: 213 | if adj_element_enum not in Hvf_Metric_Calculator.CIGTS_ICON_SCORE.keys(): 214 | continue 215 | 216 | # Skip if not in same vertical hemisphere: 217 | if ((y == 4) and (jj == 5)) or ((y == 5) and (jj == 4)): 218 | continue 219 | 220 | # Add icon count: 221 | icon_counter_dict[adj_element_enum] = icon_counter_dict[adj_element_enum] + 1 222 | 223 | # Lastly, loop above counted cell of interest, so need to remove: 224 | icon_counter_dict[element] = icon_counter_dict[element] - 1 225 | 226 | # Determine max adjacent depth (with count 2+): 227 | max_adjacent_depth = Hvf_Perc_Icon.PERC_NORMAL 228 | 229 | for icon in icon_counter_dict.keys(): 230 | if (icon_counter_dict[icon] >= 2) and ( 231 | Hvf_Metric_Calculator.CIGTS_ICON_SCORE[icon] 232 | > Hvf_Metric_Calculator.CIGTS_ICON_SCORE[max_adjacent_depth] 233 | ): 234 | max_adjacent_depth = icon 235 | 236 | # Now, taken the minimum between the adjacent icon score and the element score: 237 | element_cigts_score = min( 238 | Hvf_Metric_Calculator.CIGTS_ICON_SCORE[element], 239 | Hvf_Metric_Calculator.CIGTS_ICON_SCORE[max_adjacent_depth], 240 | ) 241 | cigts_score = cigts_score + element_cigts_score 242 | cigts_score_array[x, y] = element_cigts_score 243 | 244 | # Now, normalize the score: 245 | cigts_score = int(cigts_score / 10.4) 246 | 247 | # for y in range(0, np.size(cigts_score_array, 0)): 248 | # line_string = ""; 249 | # for x in range(0, np.size(cigts_score_array, 1)): 250 | # 251 | # element = int(cigts_score_array[x, y]) 252 | # 253 | # if (element == 0): 254 | # element = " "; 255 | # elif (element == -1): 256 | # element = str(element); 257 | # else: 258 | # element = " "+str(element) 259 | # 260 | # line_string = line_string + element + "|" 261 | # 262 | # print(line_string); 263 | 264 | # Return array (calling function will sum up as needed) 265 | return cigts_score_array 266 | 267 | ############################################################################### 268 | # AGIS METRIC FUNCTIONS ####################################################### 269 | ############################################################################### 270 | 271 | ############################################################################### 272 | # Calculates global AGIS score 273 | def get_global_agis_score(hvf_obj): 274 | 275 | return 0 276 | 277 | ############################################################################### 278 | # Calculates regional AGIS score 279 | 280 | def get_regional_agis_score(hvf_obj): 281 | 282 | return 0 283 | 284 | ############################################################################### 285 | # HELPER METRIC FUNCTIONS ##################################################### 286 | ############################################################################### 287 | 288 | ############################################################################### 289 | # Mask for particular region (and deletes all else), based on input argument: 290 | # Assumes right eye format 291 | # Does no error checking on region_val 292 | def mask_field_region(perc_array, icon_type, region_val): 293 | 294 | ret_perc_array = perc_array.copy() 295 | 296 | # Iterate through entire array: 297 | for y in range(0, np.size(perc_array, 0)): 298 | for x in range(0, np.size(perc_array, 1)): 299 | 300 | if not (Hvf_Metric_Calculator.REGION_NUMERICAL_MASK[x, y] == region_val): 301 | if icon_type == Hvf_Plot_Array.PLOT_VALUE: 302 | ret_perc_array[x, y] = Hvf_Value.get_value_from_display_string("") 303 | if icon_type == Hvf_Plot_Array.PLOT_PERC: 304 | ret_perc_array[x, y] = Hvf_Perc_Icon.get_perc_icon_from_char(" ") 305 | 306 | return ret_perc_array 307 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_manager/hvf_export.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_export.py 3 | # 4 | # Description: 5 | # Code for exporting HVF objects to CSV/other excel-compatible format 6 | # See code for ordering/column info 7 | # 8 | ############################################################################### 9 | 10 | # Import necessary packages 11 | import numpy as np 12 | 13 | # Import the HVF_Object class 14 | from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 15 | from hvf_extraction_script.hvf_data.hvf_perc_icon import Hvf_Perc_Icon 16 | from hvf_extraction_script.hvf_data.hvf_plot_array import Hvf_Plot_Array 17 | from hvf_extraction_script.hvf_data.hvf_value import Hvf_Value 18 | 19 | # General purpose file functions: 20 | # Import logger class to handle any messages: 21 | from hvf_extraction_script.utilities.logger import Logger 22 | 23 | # Include editing modules: 24 | 25 | 26 | class Hvf_Export: 27 | 28 | ############################################################################### 29 | # VARIABLE/CONSTANT DECLARATIONS: ############################################# 30 | ############################################################################### 31 | CELL_DELIMITER = "\t" 32 | 33 | ############################################################################### 34 | # HELPER FUNCTIONS ############################################################ 35 | ############################################################################### 36 | 37 | ############################################################################### 38 | # For converting plots into strings 39 | def convert_plot_to_delimited_string(plot_obj, laterality, delimiter): 40 | 41 | # Total deviation Percentile array: 42 | plot_array = plot_obj.get_plot_array() 43 | 44 | # Transpose if left: 45 | # if (laterality == "Left"): 46 | # plot_array = Hvf_Editor.transpose_array(plot_array); 47 | 48 | plot_array_string = Hvf_Plot_Array.get_array_string(plot_array, plot_obj.get_icon_type(), delimiter) 49 | plot_array_string = plot_array_string.replace("\n\n", delimiter) 50 | 51 | # Lastly, we know that there's usually a trailing newline in the plot 52 | # array string, which will become a delimiter character. Remove it. 53 | plot_array_string = plot_array_string.rstrip(delimiter) 54 | 55 | # TODO: Remove spaces (values put spaces to print pretty, so need to remove) 56 | 57 | return plot_array_string 58 | 59 | ############################################################################### 60 | # For converting hvf objects to delimited strings 61 | # Always constructs string the same way: 62 | # 1. Metadata (per order in argument list of keys) 63 | # 2. Raw value plot 64 | # 3. Total deviation value plot 65 | # 4. Total deviation percentile plot 66 | # 5. Pattern deviation value plot 67 | # 6. Pattern deviation percentile plot 68 | 69 | def convert_hvf_obj_to_delimited_string(hvf_obj, metadata_key_list, delimiter): 70 | 71 | # Declare our data list to store the info 72 | data_list = [] 73 | 74 | plot_size = 100 75 | 76 | # First, grab the metadata fields 77 | for i in range(len(metadata_key_list)): 78 | 79 | # Append the field to the list 80 | data_list.append(hvf_obj.metadata.get(metadata_key_list[i])) 81 | 82 | # Next, grab the plots: 83 | # (Transpose as necessary) 84 | laterality = hvf_obj.metadata.get(Hvf_Object.KEYLABEL_LATERALITY) 85 | 86 | # Raw plot: 87 | raw_vals = Hvf_Export.convert_plot_to_delimited_string(hvf_obj.raw_value_array, laterality, delimiter) 88 | 89 | # Total deviation value array: 90 | tdv_vals = Hvf_Export.convert_plot_to_delimited_string(hvf_obj.abs_dev_value_array, laterality, delimiter) 91 | 92 | # Total deviation Percentile array: 93 | tdp_vals = Hvf_Export.convert_plot_to_delimited_string(hvf_obj.abs_dev_percentile_array, laterality, delimiter) 94 | 95 | # Need to handle pattern plots separately, because it is possible they are not 96 | # generated 97 | 98 | pdv_vals = "" 99 | pdp_vals = "" 100 | 101 | if hvf_obj.pat_dev_value_array.is_pattern_not_generated(): 102 | # Not generated, just fill with blanks: 103 | pdv_vals = delimiter.join([""] * plot_size) 104 | pdp_vals = delimiter.join([""] * plot_size) 105 | 106 | else: 107 | # Pattern is generated, so can convert to string as usual 108 | pdv_vals = Hvf_Export.convert_plot_to_delimited_string(hvf_obj.pat_dev_value_array, laterality, delimiter) 109 | pdp_vals = Hvf_Export.convert_plot_to_delimited_string( 110 | hvf_obj.pat_dev_percentile_array, laterality, delimiter 111 | ) 112 | 113 | # Add our plot strings (remember, order matters here - correlate with headers above) 114 | data_list.append(raw_vals) 115 | data_list.append(tdv_vals) 116 | data_list.append(tdp_vals) 117 | data_list.append(pdv_vals) 118 | data_list.append(pdp_vals) 119 | 120 | # Lastly, generate our return line by interlacing it with delimiter: 121 | return_line = delimiter.join(data_list) 122 | 123 | return return_line 124 | 125 | ############################################################################### 126 | # BULK HVF OBJECT EXPORTING ################################################### 127 | ############################################################################### 128 | 129 | ############################################################################### 130 | # Given a dict of file_name->hvf objects, creates a delimited string containing 131 | # all the data (for export to a spreadsheet file). Delimiter specified in the 132 | # class code. 133 | 134 | def export_hvf_list_to_spreadsheet(dict_of_hvf): 135 | 136 | # First, generate headers. Major categories of data: 137 | # 1. Filename source 138 | # 2. Metadata 139 | # 3. Raw plot 140 | # 4. Absolute plots 141 | # 5. Pattern plots 142 | # Use keylabel list from Hvf_Object to maintain order/completeness 143 | metadata_header_list = Hvf_Object.METADATA_KEY_LIST.copy() 144 | 145 | # HEADERS FOR PLOTS. Easiest way is to hard code it. Not very elegant, though. 146 | 147 | plot_size = 100 148 | raw_val_list = [None] * plot_size 149 | tdv_list = [None] * plot_size 150 | tdp_list = [None] * plot_size 151 | pdv_list = [None] * plot_size 152 | pdp_list = [None] * plot_size 153 | 154 | for i in range(0, plot_size): 155 | raw_val_list[i] = "raw" + str(i) 156 | tdv_list[i] = "tdv" + str(i) 157 | tdp_list[i] = "tdp" + str(i) 158 | pdv_list[i] = "pdv" + str(i) 159 | pdp_list[i] = "pdp" + str(i) 160 | 161 | # Construct our header list 162 | headers_list = ["file_name"] + metadata_header_list + raw_val_list + tdv_list + tdp_list + pdv_list + pdp_list 163 | 164 | # And construct our return array: 165 | string_list = [] 166 | string_list.append(Hvf_Export.CELL_DELIMITER.join(headers_list)) 167 | 168 | # Now, iterate for each HVF object and pull the data: 169 | for file_name in dict_of_hvf: 170 | 171 | # Grab hvf_obj: 172 | hvf_obj = dict_of_hvf[file_name] 173 | 174 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, f"Converting File {file_name}") 175 | 176 | # Convert to string: 177 | hvf_obj_line = ( 178 | file_name 179 | + Hvf_Export.CELL_DELIMITER 180 | + Hvf_Export.convert_hvf_obj_to_delimited_string( 181 | hvf_obj, metadata_header_list, Hvf_Export.CELL_DELIMITER 182 | ) 183 | ) 184 | 185 | # hvf_obj_line = Regex_Utils.clean_nonascii(hvf_obj_line) 186 | 187 | # And add line to the running list: 188 | string_list.append(hvf_obj_line) 189 | 190 | # Finally, return joined string: 191 | return "\n".join(string_list) 192 | 193 | ############################################################################### 194 | # SPREADSHEET IMPORTING TO HVF OBJECT DICTIONARY ############################## 195 | ############################################################################### 196 | 197 | ############################################################################ 198 | # Given a data string, slurp in file into a list of rows (which are dicts 199 | # themselves) 200 | def slurp_string_to_dict_list(data_string): 201 | 202 | return_list = [] 203 | 204 | string_list = data_string.strip("\n").split("\n") 205 | 206 | header_list = string_list.pop(0).split(Hvf_Export.CELL_DELIMITER) 207 | 208 | for line in string_list: 209 | 210 | line_data = line.split(Hvf_Export.CELL_DELIMITER) 211 | 212 | line_dict = dict(zip(header_list, line_data)) 213 | 214 | return_list.append(line_dict) 215 | 216 | return return_list 217 | 218 | ############################################################################### 219 | # Given a line, generate the HVF object from the data 220 | 221 | def get_hvf_object_from_line(line): 222 | 223 | line, raw_plot = Hvf_Export.get_hvf_plot_from_line(line, Hvf_Plot_Array.PLOT_RAW, Hvf_Plot_Array.PLOT_VALUE) 224 | line, tdv_plot = Hvf_Export.get_hvf_plot_from_line( 225 | line, Hvf_Plot_Array.PLOT_TOTAL_DEV, Hvf_Plot_Array.PLOT_VALUE 226 | ) 227 | line, tdp_plot = Hvf_Export.get_hvf_plot_from_line( 228 | line, Hvf_Plot_Array.PLOT_TOTAL_DEV, Hvf_Plot_Array.PLOT_PERC 229 | ) 230 | line, pdv_plot = Hvf_Export.get_hvf_plot_from_line( 231 | line, Hvf_Plot_Array.PLOT_PATTERN_DEV, Hvf_Plot_Array.PLOT_VALUE 232 | ) 233 | line, pdp_plot = Hvf_Export.get_hvf_plot_from_line( 234 | line, Hvf_Plot_Array.PLOT_PATTERN_DEV, Hvf_Plot_Array.PLOT_PERC 235 | ) 236 | 237 | # Clean up metadata: 238 | for key in line.keys(): 239 | line[key] = line.get(key).replace('"', "").strip() 240 | 241 | hvf_obj = Hvf_Object(line, raw_plot, tdv_plot, pdv_plot, tdp_plot, pdp_plot, None) 242 | 243 | return hvf_obj 244 | 245 | def get_hvf_plot_from_line(line, plot_type, icon_type): 246 | 247 | plot_name = "" 248 | plot_values_array = 0 249 | 250 | if plot_type == Hvf_Plot_Array.PLOT_RAW: 251 | plot_name = "raw" 252 | plot_array = np.zeros((10, 10), dtype=Hvf_Value) 253 | 254 | if plot_type == Hvf_Plot_Array.PLOT_TOTAL_DEV: 255 | if icon_type == Hvf_Plot_Array.PLOT_VALUE: 256 | plot_name = "tdv" 257 | plot_array = np.zeros((10, 10), dtype=Hvf_Value) 258 | 259 | if icon_type == Hvf_Plot_Array.PLOT_PERC: 260 | plot_name = "tdp" 261 | plot_array = np.zeros((10, 10), dtype=Hvf_Perc_Icon) 262 | 263 | if plot_type == Hvf_Plot_Array.PLOT_PATTERN_DEV: 264 | if icon_type == Hvf_Plot_Array.PLOT_VALUE: 265 | plot_name = "pdv" 266 | plot_array = np.zeros((10, 10), dtype=Hvf_Value) 267 | 268 | if icon_type == Hvf_Plot_Array.PLOT_PERC: 269 | plot_name = "pdp" 270 | plot_array = np.zeros((10, 10), dtype=Hvf_Perc_Icon) 271 | 272 | is_blank = True 273 | for i in range(0, 100): 274 | header_name = plot_name + str(i) 275 | 276 | data_point = line.pop(header_name).replace(" ", "") 277 | 278 | if data_point == "": 279 | # Empty strings cause issues - replace with space 280 | data_point = " " 281 | else: 282 | is_blank = False 283 | 284 | if icon_type == Hvf_Plot_Array.PLOT_VALUE: 285 | cell_object = Hvf_Value.get_value_from_display_string(data_point) 286 | 287 | if icon_type == Hvf_Plot_Array.PLOT_PERC: 288 | cell_object = Hvf_Perc_Icon.get_perc_icon_from_char(data_point) 289 | 290 | x = int(i % 10) 291 | y = int(i / 10) 292 | 293 | plot_array[x, y] = cell_object 294 | 295 | if is_blank and plot_type == Hvf_Plot_Array.PLOT_PATTERN_DEV: 296 | plot_array = Hvf_Plot_Array.NO_PATTERN_DETECT 297 | 298 | plot_array_obj = Hvf_Plot_Array(plot_type, icon_type, plot_array, None) 299 | 300 | return line, plot_array_obj 301 | 302 | ############################################################################### 303 | # Given a dict of file_name->hvf objects, creates a delimited string containing 304 | # all the data (for export to a spreadsheet file). Delimiter specified in the 305 | # class code. 306 | 307 | # Given an exported spreadsheet (delimited string), creates a dict of 308 | # file_name->hvf objects. Inverse function of export. 309 | 310 | def import_hvf_list_from_spreadsheet(delimited_string): 311 | 312 | return_dict = {} 313 | 314 | # We will assume the spreadsheet is of correct format - no error checking 315 | # This means: columns are of correct names (corresponding to metadata, etc) 316 | 317 | # We assume first line is column header, followed by lines of data 318 | # First, slurp in entire string into a list of dictionaries to we can 319 | # process each one by one 320 | 321 | list_of_lines = Hvf_Export.slurp_string_to_dict_list(delimited_string) 322 | 323 | # Now, process each one by one: 324 | 325 | for line in list_of_lines: 326 | 327 | file_name = line.pop("file_name") 328 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_SYSTEM, f"Reading data for {file_name}") 329 | 330 | hvf_obj = Hvf_Export.get_hvf_object_from_line(line) 331 | 332 | return_dict[file_name] = hvf_obj 333 | 334 | return return_dict 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HVF Extraction Script 2 | 3 | Python module for Humphrey Visual Field (HVF) report data extraction. The package can taken in HVF single field analysis report images (from HFA2 or HFA3), and using OCR (tesseract) and image processing techniques (openCV), extracts data into an object oriented format for further processing. 4 | 5 | ## Getting Started 6 | 7 | ### Requirements 8 | - Python 3.6.7 or higher 9 | - TesserOCR 10 | - Regex 11 | - PyDicom 12 | - Pillow 13 | - OpenCV 4.2.0 14 | - FuzzyWuzzy 15 | - Fuzzysearch 16 | 17 | ### Installation 18 | 19 | Note: This software package was developed and tested on an Intel Mac OSX system; while it should work on any platform, its execution is best understood on such systems. 20 | 21 | To use the system, first download and install [Anaconda](https://www.anaconda.com/) (or Miniconda) for Python, a distribution and package manager for Python specifically geared towards data science. This will help download many of the dependencies for the system. 22 | 23 | Within Anaconda, create a dedicated environment for development and use with HVF Extraction Script: 24 | 25 | ```shell 26 | (base) $ conda create --name hvf_env # Replace 'hvf_env' with desired environment name 27 | ``` 28 | 29 | and switch to that environment: 30 | 31 | ```shell 32 | (base) $ conda activate hvf_env # or the environment name you chose 33 | ``` 34 | 35 | Within your environment, download a few required dependencies with Anaconda, namely PIP (to manage PyPI Package repository) and tesseract: 36 | 37 | ```shell 38 | (hvf_env) $ conda install pip 39 | ... 40 | (hvf_env) $ conda install -c conda-forge tesseract 41 | ... 42 | ``` 43 | 44 | Lastly, use PIP to install hvf-extraction-script, to download the package and all other required dependencies: 45 | 46 | ```shell 47 | (hvf_env) $ pip install hvf-extraction-script 48 | ``` 49 | 50 | Occasionally, installation of hvf-extraction-script has trouble locating some dependencies (specifically tesseract) and fails installation; this may be due to some internal links not refreshing. Try restarting your terminal program and trying again. 51 | 52 | ## Usage 53 | 54 | ### Overview 55 | 56 | HVF data can be stored in a variety of formats that can be imported into the hvf_extraction_script platform. The platform can import data from 1) HVF single field analysis report images from HFA2 or HFA3 (PNG, JPG, etc - any file format that openCV can read), 2) HVF DICOM files, and 3) serialized JSON files (produced by the script platform). See below for examples on how to import data from these different sources. 57 | 58 | Once imported, data is managed primarily through the Hvf_Object class, which contains the report metadata (name/ID, test date, field size and strategy, etc), and the 5 data plots (raw sensitivity, total deviation value/percentile plots, and pattern deviation value/percentile plots). Plot data is stored as Hvf_Plot_Array objects (internally as 10x10 Numpy arrays), and individual plot data elements are stored as either Hvf_Value or Hvf_Perc_Icon objects. See below for the basic structure of Hvf_Object (and helper classes). 59 | 60 | Data modules (which are Hvf_Object, Hvf_Plot_Array, Hvf_Value, Hvf_Perc_Icon) are contained in the subpackage hvf_data. hvf_extraction_script also includes two other subpackages, hvf_manager and utilities, that contain modules to assist in data processing. 61 | 62 | Subpackage hvf_manager contains functions to 'manage' or process HVF data. This includes a module for running unit tests for image extraction (hvf_test) and a module for exporting Hvf_Objects to human-readable spreadsheet for further processing (hvf_export). There is also a module for calculating HVF metrics (hvf_metric_calculator), but this module is still under development. 63 | 64 | Subpackage utilities contains general purpose utility modules not specific to HVF data. This includes a module for file I/O (file_utils), image processing (image_utils), OCR functions (ocr_utils - essentially a wrapper for TesserOCR), and regex functions (regex_utils). 65 | 66 | ### Importing and exporting data 67 | 68 | Importing/extracting data from an image: 69 | 70 | ```shell 71 | >>> from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 72 | >>> from hvf_extraction_script.utilities.file_utils import File_Utils 73 | 74 | >>> hvf_img_path = "path/to/hvf/image/file/to/read"; 75 | >>> hvf_img = File_Utils.read_image_from_file(hvf_img_path); 76 | >>> hvf_obj = Hvf_Object.get_hvf_object_from_image(hvf_img); 77 | ``` 78 | 79 | Importing data from a DICOM file: 80 | 81 | ```shell 82 | >>> from hvf_extraction_script.hvf_data.hvf_object import Hvf_Object 83 | >>> from hvf_extraction_script.utilities.file_utils import File_Utils 84 | 85 | >>> hvf_dicom_path = "path/to/hvf/dicom/file/to/read"; 86 | >>> hvf_dicom = File_Utils.read_dicom_from_file(hvf_dicom_path); 87 | >>> hvf_obj = Hvf_Object.get_hvf_object_from_dicom(hvf_dicom); 88 | ``` 89 | 90 | Saving as a text file: 91 | ```shell 92 | >>> serialized_string = hvf_obj.serialize_to_json(); 93 | >>> txt_file_path = “path/to/target/file/to/write”; 94 | >>> File_Utils.write_string_to_file(serialized_string, target_file_path) 95 | ``` 96 | 97 | Reinstantiating Hvf_Object from text file 98 | ```shell 99 | >>> hvf_txt = File_Utils.read_text_from_file(txt_file_path); 100 | >>> hvf_obj = Hvf_Object.get_hvf_object_from_text(hvf_txt); 101 | ``` 102 | 103 | Export to spreadsheet (tab-separated values): 104 | ```shell 105 | # Takes in a dictionary of filename_string -> hvf_obj 106 | >>> from hvf_extraction_script.hvf_manager.hvf_export import Hvf_Export; 107 | 108 | >>> dict_of_hvf_objs = {“file1.PNG”: hvf_obj1, “file2.PNG”: hvf_obj2, “file3.PNG”: hvf_obj3 }; 109 | >>> spreadsheet_string = Hvf_Export.export_hvf_list_to_spreadsheet(dict_of_hvf_objs) 110 | >>> File_Utils.write_string_to_file(return_string, "output_spreadsheet.tsv") 111 | # Saves data in a spreadsheet, with first column as filename 112 | ``` 113 | 114 | Import Hvf_Objects from outputted spreadsheet (tab-separated values): 115 | ```shell 116 | >>> tsv_file_string = File_Utils.read_text_from_file("output_spreadsheet.tsv"); 117 | >>> dict_of_hvf_objs = Hvf_Export.import_hvf_list_from_spreadsheet(tsv_file_string); 118 | # Returns dictionary of filename_string -> hvf_obj 119 | ``` 120 | 121 | ### Structure of Hvf_Object and helper classes 122 | 123 | Hvf_Object contains data from the source HVF study within instance variables. Metadata (including name, ID, field size, reliability indices, etc) strings are stored within a instance variable dictionary; data is accessible using keys stored within Hvf_Object as constants: 124 | 125 | - KEYLABEL_LAYOUT # Internal data corresponding to layout of source HVF image 126 | - KEYLABEL_NAME 127 | - KEYLABEL_DOB 128 | - KEYLABEL_ID 129 | - KEYLABEL_TEST_DATE 130 | - KEYLABEL_LATERALITY 131 | - KEYLABEL_FOVEA 132 | - KEYLABEL_FIXATION_LOSS 133 | - KEYLABEL_FALSE_POS 134 | - KEYLABEL_FALSE_NEG 135 | - KEYLABEL_TEST_DURATION 136 | - KEYLABEL_FIELD_SIZE 137 | - KEYLABEL_STRATEGY 138 | - KEYLABEL_PUPIL_DIAMETER 139 | - KEYLABEL_RX 140 | - KEYLABEL_MD 141 | - KEYLABEL_PSD 142 | - KEYLABEL_VF 143 | 144 | Metadata can be accessed from the object as such: 145 | 146 | ```shell 147 | # As example, accessing name: 148 | >>> hvf_obj.metadata[Hvf_Object.KEYLABEL_NAME]; 149 | 'SMITH, JOHN' 150 | ``` 151 | 152 | Additionally, there are 5 plots in every HVF object, represented by Hvf_Plot_Array objects. These can be accessed by: 153 | 154 | ```shell 155 | # Raw sensitivity array: 156 | hvf_obj.raw_value_array 157 | 158 | # Total deviation value array: 159 | hvf_obj.abs_dev_value_array 160 | 161 | # Pattern deviation value array: 162 | hvf_obj.pat_dev_value_array 163 | 164 | # Total deviation percentile array: 165 | hvf_obj.abs_dev_percentile_array 166 | 167 | # Pattern deviation percentile array: 168 | hvf_obj.pat_dev_percentile_array 169 | ``` 170 | 171 | The main data in the Hvf_Plot_Array object are: 172 | 173 | ```shell 174 | array_obj.plot_type 175 | # Possible values are Hvf_Plot_Array.PLOT_RAW, Hvf_Plot_Array.PLOT_TOTAL_DEV or Hvf_Plot_Array.PLOT_PATTERN_DEV 176 | 177 | array_obj.icon_type 178 | # Possible values are Hvf_Plot_Array.PLOT_VALUE or, Hvf_Plot_Array.PLOT_PERC 179 | 180 | array_obj.plot_array 181 | # 10x10 Numpy array containing either Hvf_Value or Hvf_Perc_Icon (depending on icon_type) representing the value of the plot in that position 182 | ``` 183 | 184 | Hvf_Value is essentially a wrapper class for a numerical value in a value plot (only relevant datum in this object is Hvf_Value.value, the number to wrap). There are some special non-numerical values that this object can take in specific circumstances, including: 185 | 186 | - Hvf_Value.VALUE_NO_VALUE (ie, a blank value - for areas in the plot that are empty) - ' ' 187 | - Hvf_Value.VALUE_FAILURE (ie, the program was unable to determine was the value was - in other words, an program error) '?' 188 | - Hvf_Value.VALUE_BELOW_THRESHOLD (the value '<0') - '<0 ' 189 | 190 | Values from Hvf_Value can be queried by calling the method get_value() (ie, hvf_value_obj.get_value()) to get the raw value wrapped, or get_display_string(), which will convert the above cases to a display character/string version for easy reading. 191 | 192 | Hvf_Perc_Icon is a similar wrapper class for a percentile icon in a percentile plot (again, only relevant datum in this object is Hvf_Perc_Icon.perc_enum, an enum value corresponding to the icon it represents). The possible values are: 193 | 194 | - Hvf_Perc_Icon.PERC_NO_VALUE (ie, a blank value - for areas in the plot that are empty) - ' ' 195 | - Hvf_Perc_Icon.PERC_NORMAL (a 'normal' sensitivity - single dot icon) - '.' 196 | - Hvf_Perc_Icon.PERC_5_PERCENTILE (lower than 5th percentile) - '5' 197 | - Hvf_Perc_Icon.PERC_2_PERCENTILE (lower than 2nd percentile) - '2' 198 | - Hvf_Perc_Icon.PERC_1_PERCENTILE (lower than 1st percentile) - '1' 199 | - Hvf_Perc_Icon.PERC_HALF_PERCENTILE (lower than 0.5th percentile - full black box) - 'x' 200 | - Hvf_Perc_Icon.PERC_FAILURE (the program was unable to determine was the value was - in other words, an program error) - '?' 201 | 202 | Values from Hvf_Perc_Icon can be queried by calling the method get_enum() to get the enum value, or get_display_string(), which will get a character representing the icon. 203 | 204 | For example, to query for a specific value: 205 | ```shell 206 | # As example, accessing name: 207 | >>> hvf_obj.metadata[Hvf_Object.KEYLABEL_NAME] 208 | 'SMITH, JOHN' 209 | >>> hvf_obj.metadata[Hvf_Object.KEYLABEL_MD] 210 | '-5.54' 211 | >>> hvf_obj.metadata[Hvf_Object.KEYLABEL_VFI] 212 | '87%' 213 | >>> print(hvf_obj.get_display_raw_val_plot_string()) 214 | Raw Value Plot: 215 | 216 | 217 | 23 24 28 25 218 | 219 | 29 29 28 29 29 28 220 | 221 | 27 27 29 29 29 29 27 25 222 | 223 | 29 27 30 31 27 27 27 27 23 224 | 225 | 29 0 27 32 30 28 15 11 <0 226 | 227 | 26 27 25 30 31 28 0 <0 228 | 229 | 29 27 27 15 2 <0 230 | 231 | 26 26 23 0 232 | 233 | >>> hvf_obj.raw_value_array.plot_array[7,7].get_value() 234 | -97 # In above plot, refers to <0 - value is Hvf_Value.VALUE_BELOW_THRESHOLD 235 | >>> hvf_obj.raw_value_array.plot_array[7,7].get_display_string() 236 | '<0' 237 | >>> hvf_obj.pat_dev_percentile_array.plot_array[4,4].get_enum() 238 | 1 # Enum value for Hvf_Perc_Icon.PERC_NORMAL 239 | >>> hvf_obj.pat_dev_percentile_array.plot_array[4,4].get_display_string() 240 | '.' # Character representation of Hvf_Perc_Icon.PERC_NORMAL 241 | ``` 242 | 243 | ### Running Unit Tests 244 | 245 | Single Image Testing: 246 | 247 | Running a single image test performs an extraction of an image report, shows its extraction data in pretty-print, and tests serialization/deserialization procedures 248 | 249 | ```shell 250 | >>> from hvf_extraction_script.hvf_manager.hvf_test import Hvf_Test 251 | >>> from hvf_extraction_script.utilities.file_utils import File_Utils 252 | 253 | >>> image_path = “path/to/image/file.PNG”; 254 | >>> hvf_image = File_Utils.read_image_from_file(image_path); 255 | >>> Hvf_Test.test_single_image(hvf_image); 256 | ... 257 | ``` 258 | 259 | Unit Testing: 260 | 261 | This package comes with the ability to run unit tests, but with no pre-loaded unit tests to run. Unit testing code is under Hvf_Test, with some example code in hvf_object_testers.py (uploaded in GitHub source code). In general, unit testing can perform testing comparison between: 262 | - Image extraction vs serialized reference 263 | - Image extraction vs DICOM file reference 264 | - Serialized text file vs DICOM file reference 265 | - Serialized text file vs serialized reference 266 | 267 | The image file and reference test files are stored under hvf_test_cases with corresponding names. 268 | 269 | Adding unit tests: 270 | 271 | ```shell 272 | >>> unit_test_name = “unit_test_name” 273 | >>> test_type = Hvf_Test.UNIT_TEST_IMAGE_VS_DICOM; 274 | >>> ref_data_path = "path/to/dicom/file.dcm" 275 | >>> test_data_path = “path/to/image/file.PNG”; 276 | >>> Hvf_Test.add_unit_test(test_name, test_type, ref_data_path, test_data_path); 277 | 278 | ``` 279 | 280 | Running unit tests: 281 | ```shell 282 | >>> Hvf_Test.test_unit_tests(unit_test_nam, test_type) 283 | ... 284 | [SYSTEM] ================================================================================ 285 | [SYSTEM] Starting test: v2_26 286 | [SYSTEM] Test v2_26: FAILED ============================== 287 | [SYSTEM] - Metadata MISMATCH COUNT: 1 288 | [SYSTEM] --> Key: pupil_diameter - expected: 4.1, actual: 4.7 289 | [SYSTEM] - Raw Value Plot: FULL MATCH 290 | [SYSTEM] - Total Deviation Value Plot: FULL MATCH 291 | [SYSTEM] - Pattern Deviation Value Plot: FULL MATCH 292 | [SYSTEM] - Total Deviation Percentile Plot: FULL MATCH 293 | [SYSTEM] - Pattern Deviation Percentile Plot: FULL MATCH 294 | [SYSTEM] END Test v2_26 FAILURE REPORT ===================== 295 | [SYSTEM] ================================================================================ 296 | [SYSTEM] Starting test: v2_27 297 | [SYSTEM] Test v2_27: PASSED 298 | [SYSTEM] ================================================================================ 299 | [SYSTEM] Starting test: v2_28 300 | [SYSTEM] Test v2_28: PASSED 301 | [SYSTEM] ================================================================================ 302 | ... 303 | [SYSTEM] ================================================================================ 304 | [SYSTEM] UNIT TEST AGGREGATE METRICS: 305 | [SYSTEM] Total number of tests: 30 306 | [SYSTEM] Average extraction time per report: 4741ms 307 | [SYSTEM] 308 | [SYSTEM] Total number of metadata fields: 510 309 | [SYSTEM] Total number of metadata field errors: 7 310 | [SYSTEM] Metadata field error rate: 0.014 311 | [SYSTEM] 312 | [SYSTEM] Total number of value data points: 3817 313 | [SYSTEM] Total number of value data point errors: 2 314 | [SYSTEM] Value data point error rate: 0.001 315 | [SYSTEM] 316 | [SYSTEM] Total number of percentile data points: 3309 317 | [SYSTEM] Total number of percentile data point errors: 0 318 | [SYSTEM] Percentile data point error rate: 0.0 319 | ``` 320 | 321 | ## Authors 322 | - Murtaza Saifee, MD - Ophthalmology resident, UCSF. Email: saifeeapps@gmail.com 323 | 324 | ## Validation 325 | In progress 326 | 327 | ## License 328 | GPL License 329 | 330 | ## Using/Contributing 331 | This project was developed in the spirit of facilitating vision research. To that end, we encourage all to download, use, critique and improve upon the project. Fork requests are encouraged. Research collaboration requests are also welcomed. 332 | 333 | ## Acknowledgements 334 | - PyImageSearch for excellent tutorials on image processing 335 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/hvf_perc_icon.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_perc_icon.py 3 | # 4 | # Description: 5 | # Class definition for a HVF percentile icon detector HVF object. Meant to be 6 | # used in the hvf_object class as a helper class. 7 | # 8 | # Uses OpenCV image detection/template matching to match percentile icons from 9 | # a list of known icons. 10 | # 11 | # Main usage: 12 | # Call init method to initialize all reference icons 13 | # 14 | # Call factory with image (or enum) corresponding to cell to identify. Performs 2 15 | # major tasks: 16 | # 1. Icon detection/cropping 17 | # 2. Icon recognition 18 | # 19 | # To Do: 20 | # - Get display string 21 | # 22 | # 23 | ############################################################################### 24 | 25 | # Import necessary packages 26 | import os 27 | import pkgutil 28 | 29 | import cv2 30 | 31 | # Import some helper packages: 32 | import numpy as np 33 | 34 | from hvf_extraction_script.utilities.file_utils import File_Utils 35 | from hvf_extraction_script.utilities.image_utils import Image_Utils 36 | 37 | # Import some of our own written modules: 38 | from hvf_extraction_script.utilities.logger import Logger 39 | 40 | 41 | class Hvf_Perc_Icon: 42 | 43 | ############################################################################### 44 | # CONSTANTS AND STATIC VARIABLES ############################################## 45 | ############################################################################### 46 | 47 | ############################################################################### 48 | # Percentile icons and enums 49 | 50 | # Important constants - declare the percentile icons enum values 51 | PERC_NO_VALUE = 0 52 | PERC_NORMAL = 1 53 | PERC_5_PERCENTILE = 2 54 | PERC_2_PERCENTILE = 3 55 | PERC_1_PERCENTILE = 4 56 | PERC_HALF_PERCENTILE = 5 57 | PERC_FAILURE = 6 58 | 59 | enum_perc_list = [PERC_5_PERCENTILE, PERC_2_PERCENTILE, PERC_1_PERCENTILE, PERC_HALF_PERCENTILE] 60 | 61 | # Display characters for each percentile icon 62 | PERC_NO_VALUE_CHAR = " " 63 | PERC_NORMAL_CHAR = "." 64 | PERC_5_PERCENTILE_CHAR = "5" 65 | PERC_2_PERCENTILE_CHAR = "2" 66 | PERC_1_PERCENTILE_CHAR = "1" 67 | PERC_HALF_PERCENTILE_CHAR = "x" 68 | PERC_FAILURE_CHAR = "?" 69 | 70 | # Display character dictionary: 71 | perc_disp_char_dict = { 72 | PERC_NO_VALUE: PERC_NO_VALUE_CHAR, 73 | PERC_NORMAL: PERC_NORMAL_CHAR, 74 | PERC_5_PERCENTILE: PERC_5_PERCENTILE_CHAR, 75 | PERC_2_PERCENTILE: PERC_2_PERCENTILE_CHAR, 76 | PERC_1_PERCENTILE: PERC_1_PERCENTILE_CHAR, 77 | PERC_HALF_PERCENTILE: PERC_HALF_PERCENTILE_CHAR, 78 | PERC_FAILURE: PERC_FAILURE_CHAR, 79 | } 80 | 81 | # Class variables: 82 | # Need icon templates for the percentile icons to match against 83 | # These need to be initialized at runtime 84 | perc_5_template = None 85 | perc_2_template = None 86 | perc_1_template = None 87 | perc_half_template = None 88 | 89 | template_perc_list = None 90 | 91 | # Initialization flag 92 | is_initialized = False 93 | 94 | ############################################################################### 95 | # CONSTRUCTOR AND FACTORY METHODS ############################################# 96 | ############################################################################### 97 | 98 | ############################################################################### 99 | # Initializer method 100 | # Not to be used publicly - use factory methods instead 101 | # Takes in pertinent data (enum corresponding to percentile icon, and image 102 | # slice) 103 | def __init__(self, perc_enum, image_slice): 104 | 105 | self.perc_enum = perc_enum 106 | 107 | self.raw_image = image_slice 108 | 109 | ############################################################################### 110 | # Factory method - given an image slice, returns an enum corresponding to the 111 | # icon 112 | @staticmethod 113 | def get_perc_icon_from_image(slice): 114 | 115 | perc_enum = Hvf_Perc_Icon.get_perc_plot_element(slice) 116 | 117 | return Hvf_Perc_Icon(perc_enum, slice) 118 | 119 | ############################################################################### 120 | # Factory method - given an char, returns an enum corresponding to the 121 | # icon (used for deserialization) 122 | @staticmethod 123 | def get_perc_icon_from_char(icon_char): 124 | 125 | reverse_disp_char_dict = {v: k for k, v in Hvf_Perc_Icon.perc_disp_char_dict.items()} 126 | 127 | try: 128 | perc_enum = reverse_disp_char_dict[icon_char] 129 | except: 130 | perc_enum = Hvf_Perc_Icon.PERC_FAILURE 131 | 132 | return Hvf_Perc_Icon(perc_enum, None) 133 | 134 | ############################################################################### 135 | # INITIALIZATION METHODS ###################################################### 136 | ############################################################################### 137 | 138 | ############################################################################### 139 | # Variable Initialization method - does some preprocessing for variables for 140 | # ease of calculation 141 | @classmethod 142 | def initialize_class_vars(cls): 143 | 144 | # Load the perc icons from a sub-directory -- assumes they are present 145 | resource_module_dir, _ = os.path.split( 146 | pkgutil.get_loader("hvf_extraction_script.hvf_data.perc_icons").get_filename() 147 | ) 148 | 149 | perc_5_template_path = os.path.join(resource_module_dir, "perc_5.JPG") 150 | perc_2_template_path = os.path.join(resource_module_dir, "perc_2.JPG") 151 | perc_1_template_path = os.path.join(resource_module_dir, "perc_1.JPG") 152 | perc_half_template_path = os.path.join(resource_module_dir, "perc_half.JPG") 153 | 154 | cls.perc_5_template = cv2.cvtColor(File_Utils.read_image_from_file(perc_5_template_path), cv2.COLOR_BGR2GRAY) 155 | cls.perc_2_template = cv2.cvtColor(File_Utils.read_image_from_file(perc_2_template_path), cv2.COLOR_BGR2GRAY) 156 | cls.perc_1_template = cv2.cvtColor(File_Utils.read_image_from_file(perc_1_template_path), cv2.COLOR_BGR2GRAY) 157 | cls.perc_half_template = cv2.cvtColor( 158 | File_Utils.read_image_from_file(perc_half_template_path), cv2.COLOR_BGR2GRAY 159 | ) 160 | 161 | # cls.perc_5_template = cv2.cvtColor(File_Utils.read_image_from_file("hvf_extraction_script/hvf_data/perc_icons/perc_5.JPG"), cv2.COLOR_BGR2GRAY); 162 | # cls.perc_2_template = cv2.cvtColor(File_Utils.read_image_from_file("hvf_extraction_script/hvf_data/perc_icons/perc_2.JPG"), cv2.COLOR_BGR2GRAY); 163 | # cls.perc_1_template = cv2.cvtColor(File_Utils.read_image_from_file("hvf_extraction_script/hvf_data/perc_icons/perc_1.JPG"), cv2.COLOR_BGR2GRAY); 164 | # cls.perc_half_template = cv2.cvtColor(File_Utils.read_image_from_file("hvf_extraction_script/hvf_data/perc_icons/perc_half.JPG"), cv2.COLOR_BGR2GRAY); 165 | 166 | # Load them into lists for ease of use: 167 | cls.template_perc_list = [cls.perc_5_template, cls.perc_2_template, cls.perc_1_template, cls.perc_half_template] 168 | 169 | # Lastly, flip the flag to indicate initialization has been done 170 | cls.is_initialized = True 171 | 172 | return None 173 | 174 | ############################################################################### 175 | # OBJECT METHODS ############################################################## 176 | ############################################################################### 177 | 178 | ############################################################################### 179 | # Simple accessor for enum 180 | def get_enum(self): 181 | return self.perc_enum 182 | 183 | ############################################################################### 184 | # Simple accessor for image (NOTE: may be None) 185 | def get_source_image(self): 186 | return self.raw_image 187 | 188 | ############################################################################### 189 | # Simple accessor/toString method 190 | def get_display_string(self): 191 | return Hvf_Perc_Icon.perc_disp_char_dict[self.perc_enum] 192 | 193 | ############################################################################### 194 | # Simple equals method 195 | def is_equal(self, other): 196 | return isinstance(other, Hvf_Perc_Icon) and (self.get_enum() == other.get_enum()) 197 | 198 | ############################################################################### 199 | # Releases saved images (to help save memory) 200 | def release_saved_image(self): 201 | 202 | self.raw_image = None 203 | 204 | return 205 | 206 | ############################################################################### 207 | # HELPER METHODS ############################################################## 208 | ############################################################################### 209 | 210 | ############################################################################### 211 | # Helper function for doing template matching: 212 | @staticmethod 213 | def do_template_matching(plot_element, w, h, perc_icon): 214 | 215 | # Determine which one is larger 216 | if w < np.size(perc_icon, 1): 217 | 218 | # Plot element is smaller 219 | scale_factor = min((np.size(perc_icon, 0)) / h, (np.size(perc_icon, 1)) / w) 220 | 221 | plot_element = cv2.resize(plot_element, (0, 0), fx=scale_factor, fy=scale_factor) 222 | 223 | else: 224 | # perc icon is smaller 225 | scale_factor = min(h / (np.size(perc_icon, 0)), w / (np.size(perc_icon, 1))) 226 | 227 | perc_icon = cv2.resize(perc_icon, (0, 0), fx=scale_factor, fy=scale_factor) 228 | 229 | # Apply template matching: 230 | temp_matching = cv2.matchTemplate(plot_element, perc_icon, cv2.TM_SQDIFF) 231 | 232 | # Grab our result 233 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(temp_matching) 234 | 235 | return min_val, max_val, min_loc, max_loc 236 | 237 | ############################################################################### 238 | # Get the corresponding percentile element from the image cell: 239 | @staticmethod 240 | def get_perc_plot_element(plot_element): 241 | 242 | # Declare our return value: 243 | ret_val = Hvf_Perc_Icon.PERC_NO_VALUE 244 | 245 | # What is the total plot size? 246 | plot_area = np.size(plot_element, 0) * np.size(plot_element, 1) 247 | 248 | # Delete stray marks; filters out specks based on size compared to global element 249 | # and relative to largest contour 250 | plot_threshold = 0.005 251 | relative_threshold = 0.005 252 | plot_element = Image_Utils.delete_stray_marks(plot_element, plot_threshold, relative_threshold) 253 | 254 | # First, crop the white border out of the element to get just the core icon: 255 | x0, x1, y0, y1 = Image_Utils.crop_white_border(plot_element) 256 | 257 | # Calculate height and width: 258 | h = y1 - y0 259 | w = x1 - x0 260 | 261 | # If our bounding indices don't catch any borders (ie, x0 > x1) then its must be an 262 | # empty element: 263 | if w < 0: 264 | ret_val = Hvf_Perc_Icon.PERC_NO_VALUE 265 | 266 | # Finding 'normal' elements is tricky because the icon is small (scaling doesn't 267 | # work as easily for it) and it tends to get false matching with other icons 268 | # However, its size is very different compared to the other icons, so just detect 269 | # it separately 270 | # If the cropped bounding box is less than 20% of the overall box, highly likely 271 | # normal icon 272 | elif (h / np.size(plot_element, 0)) < 0.20: 273 | 274 | ret_val = Hvf_Perc_Icon.PERC_NORMAL 275 | 276 | else: 277 | 278 | # Grab our element icon: 279 | element_cropped = plot_element[y0 : 1 + y1, x0 : 1 + x1] 280 | 281 | # Now, we template match against all icons and look for best fit: 282 | best_match = None 283 | best_perc = None 284 | 285 | for ii in range(len(Hvf_Perc_Icon.template_perc_list)): 286 | 287 | # Scale up the plot element or perc icon, whichever is smaller 288 | # (meaning, scale up so they're equal, don't scale down - keep as much 289 | # data as we can) 290 | 291 | # Grab our perc icon: 292 | perc_icon = Hvf_Perc_Icon.template_perc_list[ii] 293 | 294 | min_val, max_val, min_loc, max_loc = Hvf_Perc_Icon.do_template_matching(plot_element, w, h, perc_icon) 295 | 296 | # Check to see if this is our best fit yet: 297 | if best_match is None or min_val < best_match: 298 | # This is best fit - record the value and the icon type 299 | best_match = min_val 300 | best_perc = Hvf_Perc_Icon.enum_perc_list[ii] 301 | 302 | # Debug strings for matching the enum: 303 | debug_string = "Matching enum " + str(Hvf_Perc_Icon.enum_perc_list[ii]) + "; match : " + str(min_val) 304 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, debug_string) 305 | 306 | ret_val = best_perc 307 | 308 | # Now we need to ensure that all declared 5-percentile icons are true, because 309 | # this program often mixes up between 5-percentile and half-percentile 310 | 311 | if ret_val == Hvf_Perc_Icon.PERC_5_PERCENTILE: 312 | 313 | # Check for contours here - we know that the 5 percentile has multiple small contours 314 | plot_element = cv2.bitwise_not(plot_element) 315 | 316 | # Find contours. Note we are using RETR_EXTERNAL, meaning no children contours (ie 317 | # contours within contours) 318 | contours, hierarchy = cv2.findContours(plot_element, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 319 | 320 | # Now add up all the contour area 321 | total_cnt_area = 0 322 | for cnt in contours: 323 | total_cnt_area = total_cnt_area + cv2.contourArea(cnt) 324 | 325 | # Now compare to our cropped area 326 | # In optimal scenario, 5-percentile takes up 25% of area; half-percentile essentially 100% 327 | # Delineate on 50% 328 | AREA_PERCENTAGE_CUTOFF = 0.5 329 | area_percentage = total_cnt_area / (w * h) 330 | 331 | Logger.get_logger().log_msg( 332 | Logger.DEBUG_FLAG_DEBUG, "Recheck matching betwen 5-percentile and half-percentile" 333 | ) 334 | Logger.get_logger().log_msg( 335 | Logger.DEBUG_FLAG_DEBUG, "Total contour area percentage: " + str(area_percentage) 336 | ) 337 | 338 | # Check to see which is better. Because we are inverting, check max value 339 | if area_percentage > AREA_PERCENTAGE_CUTOFF: 340 | 341 | # Half percentile is a better fit - switch our match 342 | ret_val = Hvf_Perc_Icon.PERC_HALF_PERCENTILE 343 | 344 | # Declare as such: 345 | debug_string = "Correction: switching from 5-percentile to half-percentile" 346 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, debug_string) 347 | 348 | # Debug strings for bounding box: 349 | debug_bound_box_string = "Bounding box: " + str(x0) + "," + str(y0) + " ; " + str(x1) + "," + str(y1) 350 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, debug_bound_box_string) 351 | debug_bound_box_dim_string = "Bounding box dimensions: " + str(w) + " , " + str(h) 352 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, debug_bound_box_dim_string) 353 | 354 | # And debug function for showing the cropped element: 355 | show_cropped_element_func = lambda: cv2.imshow("cropped " + str(Hvf_Perc_Icon.i), element_cropped) 356 | Logger.get_logger().log_function(Logger.DEBUG_FLAG_DEBUG, show_cropped_element_func) 357 | Hvf_Perc_Icon.i = Hvf_Perc_Icon.i + 1 358 | 359 | return ret_val 360 | 361 | i = 0 362 | j = 0 363 | -------------------------------------------------------------------------------- /hvf_extraction_script/hvf_data/hvf_value.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # hvf_value.py - OCR VERSION 3 | # 4 | # Description: 5 | # Class definition for a HVF value icon/number detector HVF object. Meant to 6 | # beused in the hvf_object class as a helper class. 7 | # 8 | # Uses OpenCV image detection/template matching to match value number icons 9 | # from a list of known icons. 10 | # 11 | # Main usage: 12 | # Call init method to initialize all reference icons 13 | # 14 | # Call factory with image corresponding to cell (or enum) to identify. Performs 15 | # 2 major tasks: 16 | # 1. Icon detection/cropping 17 | # 2. Icon recognition 18 | # 19 | # To Do: 20 | # - Get display string 21 | # 22 | # 23 | ############################################################################### 24 | 25 | # Import necessary packages 26 | import os 27 | import pkgutil 28 | 29 | import cv2 30 | 31 | # Import some helper packages: 32 | import numpy as np 33 | 34 | # For reading files: 35 | from hvf_extraction_script.utilities.file_utils import File_Utils 36 | 37 | # General purpose image functions: 38 | from hvf_extraction_script.utilities.image_utils import Image_Utils 39 | 40 | # Import some of our own written modules: 41 | # For error/debug logging: 42 | from hvf_extraction_script.utilities.logger import Logger 43 | 44 | 45 | class Hvf_Value: 46 | 47 | ############################################################################### 48 | # CONSTANTS AND STATIC VARIABLES ############################################## 49 | ############################################################################### 50 | 51 | ############################################################################### 52 | # Value/Percentile icons and enums 53 | 54 | # Value enum corresponding to blank: 55 | VALUE_NO_VALUE = -99 56 | VALUE_FAILURE = -98 57 | VALUE_BELOW_THRESHOLD = -97 58 | 59 | VALUE_NO_VALUE_CHAR = " " 60 | VALUE_FAILURE_CHAR = "?" 61 | VALUE_BELOW_THRESHOLD_CHAR = "<0" 62 | 63 | VALUE_MAX_VALUE = 50 64 | VALUE_MIN_VALUE_RAW = 0 65 | VALUE_MIN_VALUE_DEV = -50 66 | 67 | # Class variables: 68 | # Need icon templates for value digits to match against: 69 | value_0_template = None 70 | value_1_template = None 71 | value_2_template = None 72 | value_3_template = None 73 | value_4_template = None 74 | value_5_template = None 75 | value_6_template = None 76 | value_7_template = None 77 | value_8_template = None 78 | value_9_template = None 79 | 80 | value_minus_template = None 81 | 82 | value_less_than_template = None 83 | 84 | template_value_list = None 85 | 86 | # Initialization flag 87 | is_initialized = False 88 | 89 | ############################################################################### 90 | # CONSTRUCTOR AND FACTORY METHODS ############################################# 91 | ############################################################################### 92 | 93 | ############################################################################### 94 | # Initializer method 95 | # Not to be used publicly - use factory methods instead 96 | # Takes in pertinent data (enum corresponding to percentile icon, and image 97 | # slice) 98 | def __init__(self, value, image_slice): 99 | 100 | self.value = value 101 | self.raw_image = image_slice 102 | 103 | ############################################################################### 104 | # Factory method - given an image slice, returns a value corresponding to the 105 | # image 106 | @staticmethod 107 | def get_value_from_image(slice, slice_backup, plot_type): 108 | 109 | value = Hvf_Value.get_value_plot_element(slice, slice_backup, plot_type) 110 | 111 | exception_list = [Hvf_Value.VALUE_FAILURE, Hvf_Value.VALUE_NO_VALUE, Hvf_Value.VALUE_BELOW_THRESHOLD] 112 | 113 | # In case value generated is incorrect, bring within normal limits: 114 | if value not in exception_list: 115 | if value > Hvf_Value.VALUE_MAX_VALUE: 116 | value = Hvf_Value.VALUE_MAX_VALUE 117 | 118 | elif (plot_type == "raw") and (value < Hvf_Value.VALUE_MIN_VALUE_RAW): 119 | 120 | value = Hvf_Value.VALUE_MIN_VALUE_RAW 121 | 122 | elif not (plot_type == "raw") and (value < Hvf_Value.VALUE_MIN_VALUE_DEV): 123 | 124 | value = Hvf_Value.VALUE_MIN_VALUE_DEV 125 | 126 | return Hvf_Value(value, slice) 127 | 128 | ############################################################################### 129 | # Factory method - given an number, returns a value corresponding to the 130 | # cell (used for deserialization) 131 | @staticmethod 132 | def get_value_from_display_string(num): 133 | 134 | if num == "" or num == " ": 135 | num = Hvf_Value.VALUE_NO_VALUE 136 | elif num == Hvf_Value.VALUE_FAILURE_CHAR: 137 | num = Hvf_Value.VALUE_FAILURE 138 | elif num == Hvf_Value.VALUE_BELOW_THRESHOLD_CHAR: 139 | num = Hvf_Value.VALUE_BELOW_THRESHOLD 140 | else: 141 | try: 142 | num = int(num) 143 | except: 144 | num = Hvf_Value.VALUE_FAILURE 145 | 146 | return Hvf_Value(num, None) 147 | 148 | ############################################################################### 149 | # INITIALIZATION METHODS ###################################################### 150 | ############################################################################### 151 | 152 | ############################################################################### 153 | # Variable Initialization method - does some preprocessing for variables for 154 | # ease of calculation 155 | @classmethod 156 | def initialize_class_vars(cls): 157 | 158 | # Load all the icon images for matching: 159 | 160 | # Declare our template dictionaries: 161 | cls.value_icon_templates = {} 162 | cls.minus_icon_templates = {} 163 | cls.less_than_icon_templates = {} 164 | 165 | # Iterate through the icon folders: 166 | 167 | module_list = [ 168 | "hvf_extraction_script.hvf_data.value_icons.v0", 169 | "hvf_extraction_script.hvf_data.value_icons.v1", 170 | "hvf_extraction_script.hvf_data.value_icons.v2", 171 | ] 172 | 173 | for module in module_list: 174 | 175 | module_dir, _ = os.path.split(pkgutil.get_loader(module).get_filename()) 176 | 177 | head, dir = os.path.split(module_dir) 178 | 179 | # Assume that names are standardized within the directory: 180 | 181 | # Add number value icons (construct file names): 182 | 183 | for ii in range(10): 184 | # Construct filename: 185 | value_icon_file_name = "value_" + str(ii) + ".PNG" 186 | 187 | # Construct full path: 188 | value_icon_full_path = os.path.join(module_dir, value_icon_file_name) 189 | icon_template = cv2.cvtColor(File_Utils.read_image_from_file(value_icon_full_path), cv2.COLOR_BGR2GRAY) 190 | 191 | # Add to value icon template dictionary: 192 | if ii not in cls.value_icon_templates: 193 | cls.value_icon_templates[ii] = {} 194 | 195 | cls.value_icon_templates[ii][dir] = icon_template 196 | 197 | # Add minus template: 198 | minus_icon_full_path = os.path.join(module_dir, "value_minus.PNG") 199 | minus_template = cv2.cvtColor(File_Utils.read_image_from_file(minus_icon_full_path), cv2.COLOR_BGR2GRAY) 200 | 201 | cls.minus_icon_templates[dir] = minus_template 202 | 203 | # Add less than template: 204 | less_than_full_path = os.path.join(module_dir, "value_less_than.PNG") 205 | less_than_template = cv2.cvtColor(File_Utils.read_image_from_file(less_than_full_path), cv2.COLOR_BGR2GRAY) 206 | 207 | cls.less_than_icon_templates[dir] = less_than_template 208 | 209 | # Lastly, flip the flag to indicate initialization has been done 210 | cls.is_initialized = True 211 | 212 | return None 213 | 214 | ############################################################################### 215 | # OBJECT METHODS ############################################################## 216 | ############################################################################### 217 | 218 | ############################################################################### 219 | # Simple accessor for value (NOTE: May be other enum - NO_VALUE or FAILURE) 220 | def get_value(self): 221 | return self.value 222 | 223 | ############################################################################### 224 | # Simple accessor for image (NOTE: may be None) 225 | def get_source_image(self): 226 | return self.raw_image 227 | 228 | ############################################################################### 229 | # Simple accessor/toString method 230 | def get_display_string(self): 231 | return Hvf_Value.get_string_from_value(self.value) 232 | 233 | ############################################################################### 234 | # Simple accessor/toString method 235 | def get_standard_size_display_string(self): 236 | 237 | x = self.get_display_string() 238 | 239 | # Otherwise return standardized size: 240 | if len(x) == 1: 241 | x = " " + x 242 | elif len(x) == 2: 243 | x = " " + x 244 | elif len(x) == 3: 245 | x = x 246 | 247 | return x 248 | 249 | ############################################################################### 250 | # Simple equals method 251 | def is_equal(self, other): 252 | return isinstance(other, Hvf_Value) and (self.get_value() == other.get_value()) 253 | 254 | ############################################################################### 255 | # Releases saved images (to help save memory) 256 | def release_saved_image(self): 257 | 258 | self.raw_image = None 259 | 260 | return 261 | 262 | ############################################################################### 263 | # HELPER METHODS ############################################################## 264 | ############################################################################### 265 | 266 | ############################################################################### 267 | # Helper method for value -> display string 268 | def get_string_from_value(value): 269 | # Determine if we have any special chars; 270 | if value == Hvf_Value.VALUE_NO_VALUE: 271 | x = Hvf_Value.VALUE_NO_VALUE_CHAR 272 | elif value == Hvf_Value.VALUE_FAILURE: 273 | x = Hvf_Value.VALUE_FAILURE_CHAR 274 | elif value == Hvf_Value.VALUE_BELOW_THRESHOLD: 275 | x = Hvf_Value.VALUE_BELOW_THRESHOLD_CHAR 276 | else: 277 | x = str(value) 278 | 279 | return x 280 | 281 | def contour_bound_box_area(x): 282 | x, y, w, h = cv2.boundingRect(x) 283 | return w * h 284 | 285 | def contour_x_dim(x): 286 | x, y, w, h = cv2.boundingRect(x) 287 | return x 288 | 289 | def contour_width(c): 290 | x, y, w, h = cv2.boundingRect(c) 291 | return w 292 | 293 | ############################################################################### 294 | # Given a plot element, finds number of contours 295 | @staticmethod 296 | def find_num_contours(plot_element): 297 | 298 | plot_element_temp = cv2.bitwise_not(plot_element.copy()) 299 | cnts, hierarchy = cv2.findContours(plot_element_temp, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 300 | 301 | return len(cnts) 302 | 303 | ############################################################################### 304 | # Given an image slice, cleans it up (preparing for digit value detection): 305 | @staticmethod 306 | def clean_slice(slice): 307 | 308 | # Clean up borders - sometimes straggler pixels come along 309 | slice_w = np.size(slice, 1) 310 | 311 | slice = slice[:, 1 : slice_w - 1] 312 | slice = cv2.copyMakeBorder(slice, 0, 0, 1, 1, cv2.BORDER_REPLICATE) 313 | 314 | # Crop the characters to remove excess white: 315 | x0, x1, y0, y1 = Image_Utils.crop_white_border(slice) 316 | 317 | # Possible we have a fully blank slice, so only crop if there is something 318 | if x0 < x1 and y0 < y1: 319 | slice = slice[y0:y1, x0:x1] 320 | 321 | return slice 322 | 323 | ############################################################################### 324 | # Given an image slice, chops number and returns a list of separate digits slices 325 | def chop_into_char_list(slice): 326 | 327 | # W/H Ratio for character 328 | MAX_W_H_RATIO = 0.7 329 | 330 | ret_list = [] 331 | 332 | slice_h = np.size(slice, 0) 333 | 334 | # Get contours: 335 | slice_temp = cv2.bitwise_not(slice) 336 | cnts, hierarchy = cv2.findContours(slice_temp, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 337 | 338 | # Sort contours from left to right 339 | cnts = sorted(cnts, key=Hvf_Value.contour_x_dim) 340 | 341 | # Iterate through the contours: 342 | for ii in range(len(cnts)): 343 | 344 | # Get bounding box: 345 | x, y, w, h = cv2.boundingRect(cnts[ii]) 346 | 347 | # The contour may contain multiple digits, so need to detect it: 348 | if w / slice_h > MAX_W_H_RATIO: 349 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Multiple Digits") 350 | 351 | DIGIT_WH_RATIO = 0.575 352 | 353 | # Multiple digits 354 | expected_num_chars = max(round(((w / slice_h) / DIGIT_WH_RATIO) + 0.15), 1) 355 | 356 | for ii in range(expected_num_chars): 357 | x_coor = x + (int(w * ii / expected_num_chars)) 358 | x_size = int(w / expected_num_chars) 359 | 360 | # Slice them: 361 | char_slice = slice[:, x_coor : x_coor + x_size] 362 | 363 | # And append slice to list: 364 | ret_list.append(char_slice) 365 | 366 | else: 367 | char_slice = slice[:, x : x + w] 368 | 369 | ret_list.append(char_slice) 370 | 371 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Showing Element " + str(Hvf_Value.i)) 372 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Number of elements: " + str(len(ret_list))) 373 | for ii in range(len(ret_list)): 374 | show_element_func = lambda: cv2.imshow("Element " + str(Hvf_Value.i) + "." + str(ii), ret_list[ii]) 375 | Logger.get_logger().log_function(Logger.DEBUG_FLAG_DEBUG, show_element_func) 376 | return ret_list 377 | 378 | ############################################################################### 379 | # Given an image and a icon template, scales the icon to the height of the image 380 | # and copyBorders the image to match the sizes. Then, performs template matching 381 | # and returns the result 382 | @staticmethod 383 | def resize_and_template_match(image, icon): 384 | 385 | h = np.size(image, 0) 386 | 387 | # Scale the value icon: 388 | scale_factor = h / np.size(icon, 0) 389 | 390 | icon = cv2.resize(icon, (0, 0), fx=scale_factor, fy=scale_factor) 391 | 392 | # In case the original is too small by width compared to icon, need to widen; 393 | # do so by copymakeborder replicate 394 | image = image.copy() 395 | 396 | if np.size(image, 1) < np.size(icon, 1): 397 | border = np.size(icon, 1) - np.size(image, 1) 398 | image = cv2.copyMakeBorder(image, 0, 0, 0, border, cv2.BORDER_REPLICATE) 399 | 400 | # Apply template matching: 401 | temp_matching = cv2.matchTemplate(image, icon, cv2.TM_CCOEFF_NORMED) 402 | 403 | # Grab our result 404 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(temp_matching) 405 | 406 | return max_val 407 | 408 | ############################################################################### 409 | # Helper function: Given a image, determines if it is a "<" sign - returns 410 | # boolean 411 | # Uses template matching 412 | @staticmethod 413 | def is_less_than(plot_element): 414 | 415 | LESS_THAN_DETECTION_THRESHOLD = 0.4 416 | 417 | best_match_val = 0 418 | 419 | for key in Hvf_Value.less_than_icon_templates: 420 | 421 | match_val = Hvf_Value.resize_and_template_match(plot_element, Hvf_Value.less_than_icon_templates[key]) 422 | 423 | best_match_val = max(match_val, best_match_val) 424 | 425 | # Return if either match value worked: 426 | return best_match_val > LESS_THAN_DETECTION_THRESHOLD 427 | 428 | ############################################################################### 429 | # Helper function: Given a image, determines if it is a "-" sign - returns 430 | # boolean 431 | # Uses template matching 432 | @staticmethod 433 | def is_minus(plot_element): 434 | 435 | THRESHOLD_MATCH_MINUS = 0.55 436 | 437 | best_match_val = 0 438 | 439 | for key in Hvf_Value.minus_icon_templates: 440 | 441 | match_val = Hvf_Value.resize_and_template_match(plot_element, Hvf_Value.minus_icon_templates[key]) 442 | 443 | best_match_val = max(match_val, best_match_val) 444 | 445 | # Return if either match value worked: 446 | return best_match_val > THRESHOLD_MATCH_MINUS 447 | 448 | ############################################################################### 449 | # Helper function: Given an image of a single digit, returns the best match 450 | # Uses template matching 451 | @staticmethod 452 | def identify_digit(plot_element, allow_search_zero): 453 | 454 | # We template match against all icons and look for best fit: 455 | best_match = None 456 | best_val = None 457 | best_loc = None 458 | best_scale_factor = None 459 | best_dir = None 460 | 461 | height = np.size(plot_element, 0) 462 | width = np.size(plot_element, 1) 463 | 464 | # Can skip 0 if flag tells us to. This can help maximize accuracy in low-res cases 465 | # Do this when we know something about the digit (it is a leading digit, etc) 466 | start_index = 0 467 | if not allow_search_zero: 468 | start_index = 1 469 | 470 | for ii in range(start_index, len(Hvf_Value.value_icon_templates.keys())): 471 | 472 | for dir in Hvf_Value.value_icon_templates[ii]: 473 | 474 | # First, scale our template value: 475 | val_icon = Hvf_Value.value_icon_templates[ii][dir] 476 | 477 | plot_element_temp = plot_element.copy() 478 | 479 | scale_factor = 1 480 | # Use the smaller factor to make sure we fit into the element icon 481 | if height < np.size(val_icon, 0): 482 | # Need to upscale plot_element 483 | scale_factor = np.size(val_icon, 0) / height 484 | plot_element_temp = cv2.resize(plot_element_temp, (0, 0), fx=scale_factor, fy=scale_factor) 485 | 486 | else: 487 | # Need to upscale val_icon 488 | scale_factor = height / (np.size(val_icon, 0)) 489 | val_icon = cv2.resize(val_icon, (0, 0), fx=scale_factor, fy=scale_factor) 490 | 491 | # In case the original is too small by width compared to value_icon, need 492 | # to widen - do so by copymakeborder replicate 493 | 494 | if np.size(plot_element_temp, 1) < np.size(val_icon, 1): 495 | border = np.size(val_icon, 1) - np.size(plot_element_temp, 1) 496 | # plot_element_temp = cv2.copyMakeBorder(plot_element_temp,0,0,0,border,cv2.BORDER_CONSTANT,0); 497 | 498 | # Apply template matching: 499 | temp_matching = cv2.matchTemplate(plot_element_temp, val_icon, cv2.TM_CCOEFF_NORMED) 500 | 501 | # Grab our result 502 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(temp_matching) 503 | 504 | Logger.get_logger().log_msg( 505 | Logger.DEBUG_FLAG_DEBUG, "Matching against " + str(ii) + ": " + str(max_val) 506 | ) 507 | 508 | # Check to see if this is our best fit yet: 509 | if best_match is None or max_val > best_match: 510 | # This is best fit - record the match value and the actual value 511 | best_match = max_val 512 | best_val = ii 513 | best_loc = max_loc 514 | best_scale_factor = scale_factor 515 | best_dir = dir 516 | # TODO: refine specific cases that tend to be misclassified 517 | 518 | # 1 vs 4 519 | if best_val == 4 or best_val == 1: 520 | 521 | if best_dir == "v0": 522 | 523 | # Cut number in half and take bottom half -> find contours 524 | # If width of contour is most of element --> 4 525 | # otherwise, 1 526 | 527 | bottom_half = Image_Utils.slice_image(plot_element, 0.50, 0.25, 0, 1) 528 | 529 | cnts, hierarchy = cv2.findContours( 530 | cv2.bitwise_not(bottom_half.copy()), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE 531 | ) 532 | 533 | # Sort contours by width 534 | sorted_contours = sorted(cnts, key=Hvf_Value.contour_width, reverse=True) 535 | # largest_contour = sorted(cnts, key = Hvf_Value.contour_width, reverse = True)[0]; 536 | 537 | if len(sorted_contours) > 0: 538 | 539 | if Hvf_Value.contour_width(sorted_contours[0]) > width * 0.8: 540 | best_val = 4 541 | else: 542 | best_val = 1 543 | 544 | if (best_dir == "v1") or (best_dir == "v2"): 545 | # Cut number in half and take bottom half -> find contours 546 | # If width of contour is most of element --> 4 547 | # otherwise, 1 548 | 549 | bottom_half = Image_Utils.slice_image(plot_element, 0.50, 0.50, 0, 1) 550 | 551 | cnts, hierarchy = cv2.findContours( 552 | cv2.bitwise_not(bottom_half.copy()), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE 553 | ) 554 | 555 | # Sort contours by width 556 | sorted_contours = sorted(cnts, key=Hvf_Value.contour_width, reverse=True) 557 | # largest_contour = sorted(cnts, key = Hvf_Value.contour_width, reverse = True)[0]; 558 | 559 | if len(sorted_contours) > 0: 560 | 561 | if Hvf_Value.contour_width(sorted_contours[0]) > width * 0.8: 562 | best_val = 4 563 | else: 564 | best_val = 1 565 | 566 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, f"Best match {best_val}, best dir {best_dir}") 567 | 568 | return best_val, best_loc, best_scale_factor, best_match 569 | 570 | ############################################################################### 571 | # RAW VALUE IDENTIFICATION VERSION: ########################################### 572 | ############################################################################### 573 | 574 | ############################################################################### 575 | # Get the corresponding value element/number from the plot element: 576 | @staticmethod 577 | def get_value_plot_element(plot_element, plot_element_backup, plot_type): 578 | # Declare return value 579 | return_val = 0 580 | 581 | # CV2 just slices images and returns the native image. We mess with the pixels so 582 | # for cleanliness, just copy it over: 583 | plot_element = plot_element.copy() 584 | 585 | # First, clean up any small noisy pixels by eliminating small contours 586 | # Tolerance for stray marks is different depending on plot type 587 | 588 | # Relative to largest contour: 589 | plot_threshold = 0 590 | relative_threshold = 0 591 | 592 | if plot_type == "raw": 593 | plot_threshold = 0.005 594 | relative_threshold = 0.1 595 | else: 596 | plot_threshold = 0.005 597 | relative_threshold = 0.01 598 | 599 | plot_element = Image_Utils.delete_stray_marks(plot_element, plot_threshold, relative_threshold) 600 | plot_element_backup = Image_Utils.delete_stray_marks(plot_element_backup, plot_threshold, relative_threshold) 601 | 602 | # Now, crop out the borders so we just have the central values - this allows us 603 | # to standardize size 604 | x0, x1, y0, y1 = Image_Utils.crop_white_border(plot_element) 605 | 606 | # Now we have bounding x/y coordinates 607 | # Calculate height and width: 608 | h = y1 - y0 609 | w = x1 - x0 610 | 611 | # Sometimes in low quality images, empty cells may have noise - also need to filter 612 | # based on area of element 613 | # THRESHOLD_AREA_FRACTION = 0.03; 614 | # fraction_element_area = (w*h)/(np.size(plot_element, 0)*np.size(plot_element, 1)); 615 | 616 | # If this was an empty plot, (or element area is below threshold) we have no value 617 | # if ((w <= 0) or (h <= 0) or fraction_element_area < THRESHOLD_AREA_FRACTION): 618 | if (w <= 0) or (h <= 0): 619 | Logger.get_logger().log_msg( 620 | Logger.DEBUG_FLAG_DEBUG, "Declaring no value because cell is empty/below threshold marks" 621 | ) 622 | 623 | return_val = Hvf_Value.VALUE_NO_VALUE 624 | 625 | Hvf_Value.i = Hvf_Value.i + 1 626 | else: 627 | 628 | # First, split the slice into a character list: 629 | 630 | list_of_chars = Hvf_Value.chop_into_char_list(plot_element[y0 : 1 + y1, x0 : 1 + x1]) 631 | list_of_chars_backup = Hvf_Value.chop_into_char_list(plot_element[y0 : 1 + y1, x0 : 1 + x1]) 632 | 633 | # Check for special cases (ie, non-numeric characters) 634 | 635 | # Check if <0 value 636 | # Can optimize detection accuracy by limiting check to only raw plot values with 2 chars: 637 | if ( 638 | plot_type == "raw" 639 | and len(list_of_chars) == 2 640 | and (Hvf_Value.is_less_than(list_of_chars[0]) or Hvf_Value.is_less_than(list_of_chars_backup[0])) 641 | ): 642 | 643 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Detected less-than sign") 644 | return_val = Hvf_Value.VALUE_BELOW_THRESHOLD 645 | 646 | # Check if the above detection worked: 647 | if return_val == 0: 648 | 649 | # No, so continue detection for number 650 | 651 | # Determine if we have a minus sign 652 | is_minus = 1 653 | 654 | # First, look for minus sign - if we have 2 or 3 characters 655 | 656 | # Negative numbers are not present in raw plot 657 | if not (plot_type == "raw"): 658 | 659 | if len(list_of_chars) == 2 and Hvf_Value.is_minus(list_of_chars[0]): 660 | 661 | # Detected minus sign: 662 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Detected minus sign") 663 | 664 | # Set our multiplier factor (makes later numeric correction easier) 665 | is_minus = -1 666 | 667 | # Remove the character from the list 668 | list_of_chars.pop(0) 669 | list_of_chars_backup.pop(0) 670 | 671 | elif len(list_of_chars) == 3: 672 | # We know there must be a minus sign, so just raise flag 673 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Assuming minus sign") 674 | 675 | is_minus = -1 676 | # Remove the character from the list 677 | list_of_chars.pop(0) 678 | list_of_chars_backup.pop(0) 679 | 680 | # Now, look for digits, and calculate running value 681 | 682 | running_value = 0 683 | 684 | for jj in range(len(list_of_chars)): 685 | 686 | # Pull out our digit to detect, and clean it 687 | digit = Hvf_Value.clean_slice(list_of_chars[jj]) 688 | 689 | show_element_func = lambda: cv2.imshow("Sub element " + str(Hvf_Value.i) + "_" + str(jj), digit) 690 | Logger.get_logger().log_function(Logger.DEBUG_FLAG_DEBUG, show_element_func) 691 | 692 | Hvf_Value.j = Hvf_Value.j + 1 693 | 694 | # Search for 0 if it is the trailing 0 of a multi-digit number, or if lone digit and not a minus 695 | allow_search_zero = ((jj == len(list_of_chars) - 1) and (len(list_of_chars) > 1)) or ( 696 | (len(list_of_chars) == 1) and (is_minus == 1) 697 | ) 698 | 699 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "Allow 0 search: " + str(allow_search_zero)) 700 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_DEBUG, "jj: " + str(jj)) 701 | Logger.get_logger().log_msg( 702 | Logger.DEBUG_FLAG_DEBUG, "list_of_chars length: " + str(len(list_of_chars)) 703 | ) 704 | 705 | best_value, best_loc, best_scale_factor, best_match = Hvf_Value.identify_digit( 706 | digit, allow_search_zero 707 | ) 708 | 709 | # If not a good match, recheck with alternatively processed image -> may increase yield 710 | threshold_match_digit = 0.5 711 | 712 | if best_match > 0 and best_match < threshold_match_digit: 713 | 714 | digit_backup = Hvf_Value.clean_slice(list_of_chars_backup[jj]) 715 | best_value, best_loc, best_scale_factor, best_match = Hvf_Value.identify_digit( 716 | digit_backup, allow_search_zero 717 | ) 718 | 719 | running_value = (10 * running_value) + best_value 720 | 721 | Hvf_Value.i = Hvf_Value.i + 1 722 | Hvf_Value.j = 0 723 | 724 | return_val = running_value * is_minus 725 | 726 | # Debug info string for the best matched value: 727 | debug_best_match_string = "Best matched value: " + Hvf_Value.get_string_from_value(return_val) 728 | Logger.get_logger().log_msg(Logger.DEBUG_FLAG_INFO, debug_best_match_string) 729 | 730 | return return_val 731 | 732 | i = 0 733 | j = 0 734 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------