├── .gitignore ├── LICENSE ├── README.md ├── demo ├── gifs │ ├── fruits.gif │ ├── full.gif │ ├── output.gif │ └── thumb.gif └── videos │ ├── fruits.mp4 │ ├── full.mp4 │ ├── output.mp4 │ └── thumb.mp4 ├── docs ├── en │ └── Requirements.pdf └── it │ └── Report.pdf ├── final_challenge.py ├── first_task.py ├── images ├── final_challenge │ ├── C0_000006.png │ ├── C0_000007.png │ ├── C0_000008.png │ ├── C0_000009.png │ ├── C0_000010.png │ ├── C1_000006.png │ ├── C1_000007.png │ ├── C1_000008.png │ ├── C1_000009.png │ └── C1_000010.png ├── first_task │ ├── C0_000001.png │ ├── C0_000002.png │ ├── C0_000003.png │ ├── C1_000001.png │ ├── C1_000002.png │ └── C1_000003.png └── second_task │ ├── C0_000004.png │ ├── C0_000005.png │ ├── C1_000004.png │ ├── C1_000005.png │ └── samples │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── main.py ├── second_task.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 128 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 129 | 130 | # CMake 131 | cmake-build-*/ 132 | 133 | # File-based project format 134 | *.iws 135 | 136 | # IntelliJ 137 | out/ 138 | 139 | # JIRA plugin 140 | atlassian-ide-plugin.xml 141 | 142 | # Crashlytics plugin (for Android Studio and IntelliJ) 143 | com_crashlytics_export_strings.xml 144 | crashlytics.properties 145 | crashlytics-build.properties 146 | fabric.properties 147 | 148 | # .idea folder 149 | .idea/ 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Rossini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fruits Inspector – Computer Vision system for the visual inspection of fruits 2 |

3 | 4 |

5 | 6 | Computer Vision system that is able to detect and locate defects and imperfections on fruits. 7 | 8 | ## Image characteristics 9 | Fruits appearing in the images have been acquired through a NIR (Near Infra-Red) and a color camera with little parallax effect. 10 | 11 | ### First task 12 | 1. Images show three apples with clear external defects. 13 | 14 | ### Second task 15 | 1. Images show two apples with an unwanted reddish-brown area. 16 | 17 | ### Final challenge 18 | 1. Images show five kiwis, one of which with a clear external defect. 19 | 20 | ## Functional specifications 21 | ### First task 22 | For each fruit appearing in each image, the vision system must provide the following information: 23 | 24 | 1. Outline the fruit by generating a binary mask. 25 | 2. Search for the defects on each fruit. 26 | 27 | ### Second task 28 | For each fruit appearing in each image, the vision system must provide the following information: 29 | 30 | 1. Identify the russet or at least some part of it with no false positive areas (if possible), in order to correctly classify the two fruits. 31 | 32 | ### Final challenge 33 | For each fruit appearing in each image, the vision system must provide the following information: 34 | 35 | 1. Segment the fruits and locate the defect in image “000007”. Special care should be taken to remove as “background” the dirt on the conveyor as well as the sticker in image “000006”. 36 | 37 | ## Performances 38 | Performances are calculated as the average observed FPS of 10 000 consecutive software executions on a Intel Core i5 Dual-Core 2,7 GHz processor. 39 | 40 | ### First task 41 | * 36 FPS 42 | 43 | ### Second task 44 | * **Method 1 (K-means clustering)**: 0.4 FPS 45 | 46 | * **Method 2 (Mahalanobis distance)**: 0.7 FPS 47 | 48 | ### Final challenge 49 | * 40 FPS 50 | 51 | ## Full demo 52 | 53 |

54 | 55 |

56 | 57 | ## Requirements 58 | The following Python packages must be installed in order to run the software: 59 | 60 | * numpy 61 | * opencv-python 62 | * scipy 63 | * scikit-learn 64 | 65 | ## Usage 66 | Simply run the "main.py" script from terminal, after making sure it is located in the same directory of the "images" folder: 67 | 68 | ```bash 69 | python main.py 70 | ``` 71 | 72 | or: 73 | 74 | ```bash 75 | python3 main.py 76 | ``` 77 | -------------------------------------------------------------------------------- /demo/gifs/fruits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/gifs/fruits.gif -------------------------------------------------------------------------------- /demo/gifs/full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/gifs/full.gif -------------------------------------------------------------------------------- /demo/gifs/output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/gifs/output.gif -------------------------------------------------------------------------------- /demo/gifs/thumb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/gifs/thumb.gif -------------------------------------------------------------------------------- /demo/videos/fruits.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/videos/fruits.mp4 -------------------------------------------------------------------------------- /demo/videos/full.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/videos/full.mp4 -------------------------------------------------------------------------------- /demo/videos/output.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/videos/output.mp4 -------------------------------------------------------------------------------- /demo/videos/thumb.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/demo/videos/thumb.mp4 -------------------------------------------------------------------------------- /docs/en/Requirements.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/docs/en/Requirements.pdf -------------------------------------------------------------------------------- /docs/it/Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/docs/it/Report.pdf -------------------------------------------------------------------------------- /final_challenge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Final challenge implementation file. 4 | """ 5 | 6 | import cv2 7 | import utils 8 | import numpy as np 9 | 10 | __author__ = "Marco Rossini" 11 | __copyright__ = "Copyright 2020, Marco Rossini" 12 | __date__ = "2020/05" 13 | __license__ = "MIT" 14 | __version__ = "1.0" 15 | 16 | # ---------------------------------------------------------------------------------------------------------------------- 17 | 18 | def run(): 19 | # Read and store all images into an array 20 | path = "./images/final_challenge" 21 | bw_images, bw_file_names, color_images, color_file_names = utils.get_images_as_array(path) 22 | 23 | # Iterate over all images 24 | for i in range(len(bw_images)): 25 | bw_image = bw_images[i] 26 | bw_file_name = bw_file_names[i] 27 | color_image = color_images[i] 28 | color_file_name = color_file_names[i] 29 | 30 | # Show current image and print its name 31 | utils.show_image(color_file_name, color_image) 32 | 33 | # Convert image to grayscale 34 | gray = cv2.cvtColor(bw_image, cv2.COLOR_RGB2GRAY) 35 | 36 | # Equalise histogram to improve contrast 37 | equalised = cv2.equalizeHist(gray) 38 | 39 | # Calculate optimal threshold as 'mode + factor * median / 2' (customizable) 40 | optimal = utils.get_optimal_threshold(equalised, 2.3) 41 | 42 | # Binarize the image to separate foreground and background 43 | threshold, binarized = cv2.threshold(equalised, optimal, 255, cv2.THRESH_BINARY) 44 | 45 | # Get fruit mask (biggest component) 46 | mask = utils.get_biggest_component(binarized) 47 | 48 | # Fill the holes 49 | filled = utils.fill_holes(mask) 50 | 51 | # Separate eventually touching objects by cutting out convexity defects 52 | # specifying a timeout of 1 iteration (customisable) 53 | separated = utils.separate_touching_objects(filled, 1, 1.35) 54 | 55 | # Get fruit mask after separation (biggest component) 56 | mask = utils.get_biggest_component(separated) 57 | 58 | # Apply a median blur to smooth mask 59 | blurred = utils.median_blur(mask, 3, 5) 60 | 61 | # Get grayscale fruit from filled mask 62 | fruit = cv2.bitwise_and(bw_image, bw_image, mask=blurred) 63 | 64 | # Apply a bilateral blur to remove noise but preserving edges 65 | fruit_blurred = cv2.bilateralFilter(fruit, 11, 100, 75) 66 | 67 | # Perform a Canny edge detection 68 | canny = cv2.Canny(fruit_blurred, 10, 95) 69 | 70 | # Get background mask by inverting fruit mask 71 | background = 255 - blurred 72 | 73 | # Dilate background mask to cut out the external edge 74 | kernel = np.ones((5, 5), np.uint8) 75 | background_dilated = cv2.dilate(background, kernel, iterations=3) 76 | 77 | # Remove external fruit contour 78 | defects = cv2.subtract(canny, background_dilated) 79 | 80 | # Apply a closing operation to consolidate detected edges 81 | structuringElement = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (40, 40)) 82 | closed = cv2.morphologyEx(defects, cv2.MORPH_CLOSE, structuringElement) 83 | 84 | # Perform a connected components labeling to detect defects 85 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(closed, 4) 86 | 87 | # Get a copy of the original image for visualisation purposes 88 | display = color_image.copy() 89 | 90 | # Outline the fruit using its binary mask 91 | utils.draw_fruit_outline(display, blurred, 1) 92 | 93 | # Declare a defects counter (for visualisation purposes) 94 | defects_counter = 0 95 | 96 | # Iterate over the detected components to isolate and show defects 97 | for j in range(1, retval): 98 | # Isolate current binarized component 99 | component = utils.get_component(labels, j) 100 | defects_counter = utils.draw_defect(display, component, 2, 1.3, 0, float("inf"), 5) 101 | 102 | # Show processed image 103 | display_bw = closed.copy() 104 | utils.draw_fruit_outline(display_bw, blurred, 1, (255, 255, 255)) 105 | utils.show_image(bw_file_name, display_bw) 106 | 107 | # Print detected defects number 108 | print(color_file_name + ": detected " + str(defects_counter) + " defect(s)") 109 | 110 | # Show original image highlighting defects 111 | utils.show_image(color_file_name, display) 112 | -------------------------------------------------------------------------------- /first_task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | First task implementation file. 4 | """ 5 | 6 | import cv2 7 | import utils 8 | import numpy as np 9 | 10 | __author__ = "Marco Rossini" 11 | __copyright__ = "Copyright 2020, Marco Rossini" 12 | __date__ = "2020/05" 13 | __license__ = "MIT" 14 | __version__ = "1.0" 15 | 16 | # ---------------------------------------------------------------------------------------------------------------------- 17 | 18 | def run(): 19 | # Read and store all images into an array 20 | path = "./images/first_task" 21 | bw_images, bw_file_names, color_images, color_file_names = utils.get_images_as_array(path) 22 | 23 | # Iterate over all images 24 | for i in range(len(bw_images)): 25 | bw_image = bw_images[i] 26 | bw_file_name = bw_file_names[i] 27 | color_image = color_images[i] 28 | color_file_name = color_file_names[i] 29 | 30 | # Show current image and print its name 31 | utils.show_image(color_file_name, color_image) 32 | 33 | # Convert image to grayscale 34 | gray = cv2.cvtColor(bw_image, cv2.COLOR_RGB2GRAY) 35 | 36 | # Calculate optimal threshold as 'mode + factor * median / 2' (customizable) 37 | optimal = utils.get_optimal_threshold(gray, 0.5) 38 | 39 | # Binarize the image to separate foreground and background 40 | threshold, binarized = cv2.threshold(gray, optimal, 255, cv2.THRESH_BINARY) 41 | 42 | # Get fruit mask (biggest component) 43 | mask = utils.get_biggest_component(binarized) 44 | 45 | # Fill the holes 46 | filled = utils.fill_holes(mask) 47 | 48 | # Get grayscale fruit from filled mask 49 | fruit = cv2.bitwise_and(bw_image, bw_image, mask=filled) 50 | 51 | # Apply a bilateral blur to remove noise but preserving edges 52 | fruit_blurred = cv2.bilateralFilter(fruit, 11, 100, 75) 53 | 54 | # Perform a Canny edge detection 55 | canny = cv2.Canny(fruit_blurred, 0, 140) 56 | 57 | # Get background mask by inverting fruit mask 58 | background = 255 - filled 59 | 60 | # Dilate background mask to cut out the external edge 61 | kernel = np.ones((5, 5), np.uint8) 62 | background_dilated = cv2.dilate(background, kernel, iterations=3) 63 | 64 | # Remove external fruit contour 65 | defects = cv2.subtract(canny, background_dilated) 66 | 67 | # Apply a closing operation to consolidate detected edges 68 | structuringElement = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (40, 40)) 69 | closed = cv2.morphologyEx(defects, cv2.MORPH_CLOSE, structuringElement) 70 | 71 | # Perform a connected components labeling to detect defects 72 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(closed, 4) 73 | 74 | # Get a copy of the original image for visualisation purposes 75 | display = color_image.copy() 76 | 77 | # Outline the fruit using the binary mask 78 | utils.draw_fruit_outline(display, filled, 1) 79 | 80 | # Declare a defects counter (for visualisation purposes) 81 | defects_counter = 0 82 | 83 | # Iterate over the detected components to isolate and show defects 84 | for j in range(1, retval): 85 | # Isolate current binarized component 86 | component = utils.get_component(labels, j) 87 | defects_counter += utils.draw_defect(display, component, 2, 2.2, 20, float("inf"), 5) 88 | 89 | # Show processed image 90 | display_bw = closed.copy() 91 | utils.draw_fruit_outline(display_bw, filled, 1, (255, 255, 255)) 92 | utils.show_image(bw_file_name, display_bw) 93 | 94 | # Print detected defects number 95 | print(color_file_name + ": detected " + str(defects_counter) + " defect(s)") 96 | 97 | # Show original image highlighting defects 98 | utils.show_image(color_file_name, display) 99 | -------------------------------------------------------------------------------- /images/final_challenge/C0_000006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C0_000006.png -------------------------------------------------------------------------------- /images/final_challenge/C0_000007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C0_000007.png -------------------------------------------------------------------------------- /images/final_challenge/C0_000008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C0_000008.png -------------------------------------------------------------------------------- /images/final_challenge/C0_000009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C0_000009.png -------------------------------------------------------------------------------- /images/final_challenge/C0_000010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C0_000010.png -------------------------------------------------------------------------------- /images/final_challenge/C1_000006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C1_000006.png -------------------------------------------------------------------------------- /images/final_challenge/C1_000007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C1_000007.png -------------------------------------------------------------------------------- /images/final_challenge/C1_000008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C1_000008.png -------------------------------------------------------------------------------- /images/final_challenge/C1_000009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C1_000009.png -------------------------------------------------------------------------------- /images/final_challenge/C1_000010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/final_challenge/C1_000010.png -------------------------------------------------------------------------------- /images/first_task/C0_000001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C0_000001.png -------------------------------------------------------------------------------- /images/first_task/C0_000002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C0_000002.png -------------------------------------------------------------------------------- /images/first_task/C0_000003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C0_000003.png -------------------------------------------------------------------------------- /images/first_task/C1_000001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C1_000001.png -------------------------------------------------------------------------------- /images/first_task/C1_000002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C1_000002.png -------------------------------------------------------------------------------- /images/first_task/C1_000003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/first_task/C1_000003.png -------------------------------------------------------------------------------- /images/second_task/C0_000004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/C0_000004.png -------------------------------------------------------------------------------- /images/second_task/C0_000005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/C0_000005.png -------------------------------------------------------------------------------- /images/second_task/C1_000004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/C1_000004.png -------------------------------------------------------------------------------- /images/second_task/C1_000005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/C1_000005.png -------------------------------------------------------------------------------- /images/second_task/samples/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/1.png -------------------------------------------------------------------------------- /images/second_task/samples/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/10.png -------------------------------------------------------------------------------- /images/second_task/samples/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/2.png -------------------------------------------------------------------------------- /images/second_task/samples/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/3.png -------------------------------------------------------------------------------- /images/second_task/samples/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/4.png -------------------------------------------------------------------------------- /images/second_task/samples/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/5.png -------------------------------------------------------------------------------- /images/second_task/samples/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/6.png -------------------------------------------------------------------------------- /images/second_task/samples/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/7.png -------------------------------------------------------------------------------- /images/second_task/samples/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/8.png -------------------------------------------------------------------------------- /images/second_task/samples/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProgrammeChef/fruits-inspector/d883cfc2cfcccc3fbd9b6832bd1d359d7aeb069d/images/second_task/samples/9.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Main execution file. 4 | """ 5 | 6 | import first_task 7 | import second_task 8 | import final_challenge 9 | 10 | __author__ = "Marco Rossini" 11 | __copyright__ = "Copyright 2020, Marco Rossini" 12 | __date__ = "2020/05" 13 | __license__ = "MIT" 14 | __version__ = "1.0" 15 | 16 | # ---------------------------------------------------------------------------------------------------------------------- 17 | 18 | # Run first task 19 | print("----- Running first task -----") 20 | first_task.run() 21 | 22 | # Run second task 23 | print("\n----- Running second task -----") 24 | second_task.run_clustering() 25 | second_task.run_samples() 26 | 27 | # Run final challenge 28 | print("\n----- Running final challenge -----") 29 | final_challenge.run() 30 | -------------------------------------------------------------------------------- /second_task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Second task implementation file. 4 | """ 5 | 6 | import cv2 7 | import utils 8 | import numpy as np 9 | from scipy.spatial.distance import cdist 10 | 11 | __author__ = "Marco Rossini" 12 | __copyright__ = "Copyright 2020, Marco Rossini" 13 | __date__ = "2020/05" 14 | __license__ = "MIT" 15 | __version__ = "1.0" 16 | 17 | # ---------------------------------------------------------------------------------------------------------------------- 18 | 19 | def run_clustering(): 20 | print("Method 1: using a clustering algorithm (K-means).") 21 | 22 | # Read and store all images into an array 23 | path = "./images/second_task" 24 | bw_images, bw_file_names, color_images, color_file_names = utils.get_images_as_array(path) 25 | 26 | # Iterate over all images 27 | for i in range(len(bw_images)): 28 | bw_image = bw_images[i] 29 | color_image = color_images[i] 30 | color_file_name = color_file_names[i] 31 | 32 | # Show current image and print its name 33 | utils.show_image(color_file_name, color_image) 34 | 35 | # Convert image to grayscale 36 | gray = cv2.cvtColor(bw_image, cv2.COLOR_RGB2GRAY) 37 | 38 | # Calculate optimal threshold as 'mode + factor * median / 2' (customizable) 39 | optimal = utils.get_optimal_threshold(gray) 40 | 41 | # Binarize the image to separate foreground and background 42 | threshold, binarized = cv2.threshold(gray, optimal, 255, cv2.THRESH_BINARY) 43 | 44 | # Get fruit mask (biggest component) 45 | mask = utils.get_biggest_component(binarized) 46 | 47 | # Fill the holes 48 | filled = utils.fill_holes(mask) 49 | 50 | # Erode one time to remove dark contour 51 | kernel = np.ones((3, 3), np.uint8) 52 | eroded = cv2.erode(filled, kernel, iterations=1) 53 | 54 | # Apply a median blur to remove noise but preserving edges 55 | blurred = utils.median_blur(color_image, 3, 3) 56 | 57 | # Get colored fruit from filled mask 58 | fruit = cv2.bitwise_and(blurred, blurred, mask=eroded) 59 | 60 | # Convert isolated fruit to Lab color space to preserve perceptual meaning 61 | fruit_lab = cv2.cvtColor(fruit, cv2.COLOR_BGR2LAB) 62 | 63 | # Detect dominant colors performing a K-means clustering (with K=3) on 'a' and 'b' channels of the Lab image 64 | # (L is excluded to provide robustness to lighting's variations) 65 | colors, labels = utils.get_dominant_colors(fruit_lab, 3) 66 | 67 | # Discriminate russet color among detected ones measuring distance from 'dark brown' 68 | russet_index = utils.get_russet_index(colors) 69 | 70 | # Show a sample of the detected color 71 | russet_sample = utils.get_clustering_sample(fruit_lab, labels, russet_index) 72 | utils.show_sample_lab(russet_sample, "Detected russet sample", 200, 200) 73 | 74 | # Get isolated russet on fruit as the corresponding cluster of pixels 75 | russet_component = utils.get_component(labels, russet_index) 76 | 77 | # Perform a connected components labeling on russet mask 78 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(russet_component, 4) 79 | 80 | # Get a copy of the original image for visualisation purposes 81 | display = color_image.copy() 82 | 83 | # Outline the fruit using the binary mask 84 | utils.draw_fruit_outline(display, filled, 1) 85 | 86 | # Declare a defects counter (for visualisation purposes) 87 | defects_counter = 0 88 | 89 | # Iterate over the detected components to isolate and show defects 90 | for j in range(1, retval): 91 | # Isolate current binarized component 92 | component = utils.get_component(labels, j) 93 | filled_component = utils.fill_holes(component) 94 | defects_counter += utils.draw_defect(display, filled_component, 2, 1.1, 20, float("inf"), 5) 95 | 96 | # Get colored isolated russet on fruit as the corresponding cluster of pixels (for visualisation purposes) 97 | russet = cv2.bitwise_and(fruit, fruit, mask=russet_component) 98 | 99 | # Show isolated russet 100 | utils.show_image(color_file_name, russet) 101 | 102 | # Print detected defects number 103 | print(color_file_name + ": detected " + str(defects_counter) + " defect(s)") 104 | 105 | # Show original image highlighting defects 106 | utils.show_image(color_file_name, display) 107 | 108 | 109 | def run_samples(): 110 | print("\nMethod 2: using samples and Malahanobis distance.") 111 | 112 | # Read and store all images into an array 113 | path = "./images/second_task" 114 | samples_path = "./images/second_task/samples" 115 | bw_images, bw_file_names, color_images, color_file_names = utils.get_images_as_array(path) 116 | samples, samples_file_names = utils.get_samples_as_array(samples_path) 117 | 118 | # Iterate over all images 119 | for i in range(len(bw_images)): 120 | bw_image = bw_images[i] 121 | color_image = color_images[i] 122 | color_file_name = color_file_names[i] 123 | 124 | # Show current image and print its name 125 | utils.show_image(color_file_name, color_image) 126 | 127 | # Convert image to grayscale 128 | gray = cv2.cvtColor(bw_image, cv2.COLOR_RGB2GRAY) 129 | 130 | # Calculate optimal threshold as 'mode + median / 2' 131 | optimal = utils.get_optimal_threshold(gray) 132 | 133 | # Binarize the image to separate foreground and background 134 | threshold, binarized = cv2.threshold(gray, optimal, 255, cv2.THRESH_BINARY) 135 | 136 | # Get fruit mask (biggest component) 137 | biggest_component = utils.get_biggest_component(binarized) 138 | 139 | # Fill the holes 140 | filled = utils.fill_holes(biggest_component) 141 | 142 | # Erode one time to remove dark contour 143 | kernel = np.ones((3, 3), np.uint8) 144 | eroded = cv2.erode(filled, kernel, iterations=1) 145 | 146 | # Apply a median blur to remove noise but preserving edges 147 | blurred = utils.median_blur(color_image, 3, 3) 148 | 149 | # Get fruit from filled mask 150 | fruit = cv2.bitwise_and(blurred, blurred, mask=eroded) 151 | 152 | # Convert isolated fruit to Lab color space to preserve perceptual meaning 153 | fruit_lab = cv2.cvtColor(fruit, cv2.COLOR_BGR2LAB) 154 | 155 | # Create data structures to store total covariance and mean of samples 156 | covariance_tot = np.zeros((2, 2), dtype="float64") 157 | mean_tot = np.zeros((1, 2), dtype="float64") 158 | 159 | # Iterate over samples to compute the reference color (i.e. mean of samples) and its total covariance 160 | for s in samples: 161 | s_ab = cv2.cvtColor(s, cv2.COLOR_BGR2LAB)[:, :, 1:3] 162 | s_ab_r = s_ab.reshape(s_ab.shape[0] * s_ab.shape[1], 2) 163 | cov, mean = cv2.calcCovarMatrix(s_ab_r, None, cv2.COVAR_NORMAL | cv2.COVAR_ROWS | cv2.COVAR_SCALE) 164 | covariance_tot = np.add(covariance_tot, cov) 165 | mean_tot = np.add(mean_tot, mean) 166 | 167 | # Compute the mean (reference color) as the mean of the means of all the samples 168 | russet_sample = mean_tot / len(samples) 169 | 170 | # Show a sample of the detected color 171 | russet_sample_vis = utils.get_mahalanobis_sample(samples, russet_sample) 172 | utils.show_sample_lab(russet_sample_vis, "Detected russet sample", 200, 200) 173 | 174 | # Compute the inverse of the covariance matrix (needed to measure Mahalanobis distance) 175 | inv_cov = cv2.invert(covariance_tot, cv2.DECOMP_SVD)[1] 176 | 177 | russet_component = np.zeros_like(binarized) 178 | 179 | # Compute pixel-wise Mahalanobis distance between fruit and reference color 180 | for r in range(fruit_lab.shape[0]): 181 | for c in range(fruit_lab.shape[1]): 182 | # Compute the distance only for fruit's pixels (excluding background) 183 | if filled[r][c]: 184 | # Get the pixel as a numpy array (needed for cdist) 185 | p = np.array(fruit_lab[r][c])[1:3].reshape(1, 2) 186 | 187 | # Compute pixel-wise Mahalanobis distance 188 | dist = cdist(p, russet_sample, 'mahalanobis', VI=inv_cov) 189 | 190 | # If distance is small, 'p' is a russet's pixel 191 | if dist < 1.5: 192 | # Store russet's pixel location 193 | russet_component[r][c] = 255 194 | 195 | # Perform a connected components labeling on russet mask 196 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(russet_component, 4) 197 | 198 | # Get a copy of the original image for visualisation purposes 199 | display = color_image.copy() 200 | 201 | # Outline the fruit using the binary mask 202 | utils.draw_fruit_outline(display, filled, 1) 203 | 204 | # Declare a defects counter (for visualisation purposes) 205 | defects_counter = 0 206 | 207 | # Iterate over the detected components to isolate and show defects 208 | for j in range(1, retval): 209 | # Isolate current binarized component 210 | component = utils.get_component(labels, j) 211 | filled_component = utils.fill_holes(component) 212 | defects_counter += utils.draw_defect(display, filled_component, 2, 1.1, 35, float("inf"), 5) 213 | 214 | # Get colored isolated russet on fruit as the corresponding cluster of pixels (for visualisation purposes) 215 | russet = cv2.bitwise_and(fruit, fruit, mask=russet_component) 216 | 217 | # Show isolated russet 218 | utils.show_image(color_file_name, russet) 219 | 220 | # Print detected defects number 221 | print(color_file_name + ": detected " + str(defects_counter) + " defect(s)") 222 | 223 | # Show original image highlighting defects 224 | utils.show_image(color_file_name, display) 225 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Utility functions file. 4 | """ 5 | 6 | import glob 7 | import cv2 8 | import math 9 | import numpy as np 10 | from scipy import stats 11 | from scipy.spatial import distance as dist 12 | from scipy.optimize import linear_sum_assignment 13 | from sklearn.cluster import KMeans 14 | 15 | __author__ = "Marco Rossini" 16 | __copyright__ = "Copyright 2020, Marco Rossini" 17 | __date__ = "2020/05" 18 | __license__ = "MIT" 19 | __version__ = "1.0" 20 | 21 | # ---------------------------------------------------------------------------------------------------------------------- 22 | 23 | # RGB shade of dark brown 24 | dark_brown_rgb = [71, 56, 27] 25 | 26 | 27 | # Processing functions 28 | 29 | def median_blur(image, kernel_size, iterations): 30 | for i in range(iterations): 31 | image = cv2.medianBlur(image, kernel_size) 32 | 33 | return image 34 | 35 | 36 | def separate_component(component, binarized, threshold): 37 | points = get_convexity_points(component) 38 | 39 | filtered_points = filter_points(binarized, points, 9, threshold) 40 | 41 | sorted_points = sort_points_pairwise(filtered_points) 42 | 43 | for p in sorted_points: 44 | # We impose a maximum distance threshold to avoid certainly wrong connections (customisable) 45 | if p[0] <= 12: 46 | cv2.line(binarized, p[1], p[2], (0, 0, 0), 2) 47 | 48 | return binarized 49 | 50 | 51 | def filter_points(binarized, points, radius, tolerance): 52 | result = [] 53 | 54 | for p in points: 55 | window = binarized[p[1] - radius:p[1] + radius, p[0] - radius:p[0] + radius] 56 | white_pixels = cv2.countNonZero(window) 57 | black_pixels = 2 * radius * 2 * radius - white_pixels 58 | 59 | if white_pixels / black_pixels > tolerance: 60 | result.append(p) 61 | 62 | return result 63 | 64 | 65 | def sort_points_pairwise(points): 66 | sorted_points = [] 67 | 68 | if len(points) > 0: 69 | distance_matrix = dist.cdist(np.array(points), np.array(points)) 70 | 71 | for r in range(distance_matrix.shape[0]): 72 | distance_matrix[r][r] = 9999 73 | 74 | # We solve an assignment problem exploiting the Hungarian algorithm (also known as Kuhn-Munkres algorithm) 75 | rows, cols = linear_sum_assignment(distance_matrix) 76 | 77 | rows = rows[:len(rows) // 2] 78 | cols = cols[:len(cols) // 2] 79 | 80 | for i in range(len(rows)): 81 | temp = [distance_matrix[rows[i]][cols[i]], points[rows[i]], points[cols[i]]] 82 | sorted_points.append(temp) 83 | 84 | return sorted_points 85 | 86 | 87 | def separate_touching_objects(binarized, timeout, threshold): 88 | while True: 89 | separated = 0 90 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(binarized, 4) 91 | 92 | for k in range(1, retval): 93 | # Isolate current binarized component 94 | component = get_component(labels, k) 95 | 96 | binarized = separate_component(component, binarized, threshold) 97 | separated += 1 98 | 99 | if separated == 0 or timeout == 0: 100 | break 101 | 102 | timeout -= 1 103 | 104 | return binarized 105 | 106 | 107 | def filter_duplicated_contours(contours, min_parallax): 108 | measured = [] 109 | 110 | for c in contours: 111 | if len(c) >= 5: 112 | centroid, _, _ = cv2.fitEllipse(c) 113 | area = cv2.contourArea(c) 114 | measured.append([c, centroid, area]) 115 | 116 | filtered = [] 117 | 118 | for m in measured: 119 | if is_not_duplicate(m, measured, min_parallax): 120 | filtered.append(m[0]) 121 | 122 | return filtered 123 | 124 | 125 | def rgb_to_lab(rgb): 126 | temp = np.empty((1, 1, 3), np.uint8) 127 | temp[0] = rgb 128 | result = cv2.cvtColor(temp, cv2.COLOR_RGB2LAB)[0][0] 129 | 130 | return result 131 | 132 | 133 | # Boolean functions 134 | 135 | def is_not_duplicate(m, measured, min_parallax): 136 | for c in measured: 137 | if np.array_equal(m[0], c[0]): 138 | continue 139 | 140 | distance = math.sqrt((c[1][0] - m[1][0]) ** 2 + (c[1][1] - m[1][1]) ** 2) 141 | if distance < min_parallax and m[2] <= c[2]: 142 | return False 143 | 144 | return True 145 | 146 | 147 | # Get functions 148 | 149 | 150 | def get_convexity_points(component): 151 | contours, _ = cv2.findContours(component, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 152 | contour = contours[0] 153 | 154 | hull = cv2.convexHull(contour, returnPoints=False) 155 | 156 | points = [] 157 | 158 | defects = cv2.convexityDefects(contour, hull) 159 | 160 | if defects is None: 161 | return points 162 | 163 | for i in range(defects.shape[0]): 164 | _, _, index, _ = defects[i, 0] 165 | point = tuple(contour[index][0]) 166 | points.append(point) 167 | 168 | return points 169 | 170 | 171 | def get_optimal_threshold(image, factor=1): 172 | flattened = image.flatten() 173 | mode = stats.mode(flattened)[0][0] 174 | median = int(np.median(flattened)) 175 | threshold = int((mode + factor * median) / 2) 176 | 177 | return threshold 178 | 179 | 180 | def fill_holes(mask): 181 | holes = np.where(mask == 0) 182 | 183 | if len(holes[0]) == 0: 184 | return np.zeros_like(mask, dtype=np.uint8) 185 | 186 | seed = (holes[0][0], holes[1][0]) 187 | holes_mask_inverted = mask.copy() 188 | h_, w_ = mask.shape 189 | mask_ = np.zeros((h_ + 2, w_ + 2), dtype=np.uint8) 190 | cv2.floodFill(holes_mask_inverted, mask_, seedPoint=seed, newVal=255) 191 | holes_mask = cv2.bitwise_not(holes_mask_inverted) 192 | filled = mask + holes_mask 193 | 194 | return filled 195 | 196 | 197 | # Get functions 198 | 199 | def get_images_as_array(path): 200 | bw_file_names = glob.glob(path + "/*C0*") 201 | bw_file_names.sort() 202 | bw_images = [cv2.imread(img) for img in bw_file_names] 203 | 204 | color_file_names = glob.glob(path + "/*C1*") 205 | color_file_names.sort() 206 | color_images = [cv2.imread(img) for img in color_file_names] 207 | 208 | return bw_images, bw_file_names, color_images, color_file_names 209 | 210 | 211 | def get_samples_as_array(path): 212 | samples_file_names = glob.glob(path + "/*") 213 | samples_file_names.sort() 214 | samples = [cv2.imread(img) for img in samples_file_names] 215 | 216 | return samples, samples_file_names 217 | 218 | 219 | def get_component(labels, label): 220 | component = np.zeros_like(labels, dtype=np.uint8) 221 | component[labels == label] = 255 222 | 223 | return component 224 | 225 | 226 | def get_biggest_component(image): 227 | retval, labels, stats, centroids = cv2.connectedComponentsWithStats(image, 4) 228 | 229 | max_area = float("-inf") 230 | biggest_component = None 231 | 232 | for i in range(1, retval): 233 | component = get_component(labels, i) 234 | component_area = cv2.countNonZero(component) 235 | 236 | if component_area > max_area: 237 | biggest_component = component 238 | max_area = component_area 239 | 240 | return biggest_component 241 | 242 | 243 | def get_dominant_colors(image, clusters): 244 | L, a, b = cv2.split(image) 245 | ab = cv2.merge((a, b)) 246 | 247 | # Reshaping to a list of pixels 248 | converted = ab.reshape((image.shape[0] * image.shape[1], 2)) 249 | 250 | # Using k-means to cluster pixels 251 | kmeans = KMeans(n_clusters=clusters, n_init=100, max_iter=3000) 252 | kmeans.fit(converted) 253 | 254 | # The cluster centers are the dominant colors 255 | colors = kmeans.cluster_centers_.astype(int) 256 | 257 | # Save labels 258 | labels = kmeans.labels_.reshape(image.shape[0], image.shape[1]) 259 | 260 | return colors, labels 261 | 262 | 263 | def get_russet_index(colors): 264 | distances = [] 265 | 266 | dark_brown_lab = rgb_to_lab(dark_brown_rgb)[1:3] 267 | 268 | for c in colors: 269 | d = dist.cityblock(c, dark_brown_lab) 270 | distances.append(d) 271 | 272 | min = [float("inf"), -1] 273 | 274 | for i in range(len(distances)): 275 | cur = distances[i] 276 | if cur <= min[0]: 277 | min[0] = cur 278 | min[1] = i 279 | 280 | russet_index = min[1] 281 | 282 | return russet_index 283 | 284 | 285 | def get_clustering_sample(image, labels, index): 286 | mask = get_component(labels, index) 287 | mean = cv2.mean(image, mask) 288 | sample = [int(mean[0]), int(mean[1]), int(mean[2])] 289 | 290 | return sample 291 | 292 | 293 | def get_mahalanobis_sample(samples, russet_sample): 294 | mean_tot = 0 295 | 296 | for s in samples: 297 | s_lab = cv2.cvtColor(s, cv2.COLOR_BGR2LAB) 298 | mean_tot += np.mean(s_lab, axis=(0, 1))[0] 299 | 300 | mean = mean_tot / len(samples) 301 | result = np.array([mean, russet_sample[0][0], russet_sample[0][1]]) 302 | 303 | return result 304 | 305 | 306 | # Drawing functions 307 | 308 | def draw_fruit_outline(image, mask, thickness, color=(0, 255, 0)): 309 | contours, hierarchy = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) 310 | cv2.drawContours(image, contours, -1, color, thickness) 311 | 312 | 313 | def draw_defect(image, component, thickness, scale, min_area, max_area, min_parallax): 314 | contours, hierarchy = cv2.findContours(component, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 315 | 316 | if len(contours) == 0: 317 | return 318 | 319 | filtered_contours = filter_duplicated_contours(contours, min_parallax) 320 | 321 | drawn = 0 322 | 323 | for c in filtered_contours: 324 | area = cv2.contourArea(c) 325 | if min_area < area and len(c) >= 5: 326 | ellipse = cv2.fitEllipse(c) 327 | scaled_axes = (ellipse[1][0] * scale, ellipse[1][1] * scale) 328 | if scaled_axes[0] * scaled_axes[1] * math.pi < max_area: 329 | scaled_ellipse = ellipse[0], scaled_axes, ellipse[2] 330 | cv2.ellipse(image, scaled_ellipse, (0, 0, 255), thickness) 331 | drawn += 1 332 | 333 | return drawn 334 | 335 | 336 | # Show functions 337 | 338 | def show_image(name, image, x=0, y=0): 339 | cv2.imshow(name, image) 340 | cv2.moveWindow(name, x, y) 341 | cv2.waitKey() 342 | cv2.destroyAllWindows() 343 | 344 | 345 | def show_sample_lab(lab, name, width, height): 346 | temp = np.empty((1, 1, 3), np.uint8) 347 | temp[0] = lab 348 | bgr = cv2.cvtColor(temp, cv2.COLOR_LAB2BGR)[0][0] 349 | 350 | sample = np.empty((width, height, 3), np.uint8) 351 | sample[:, :] = bgr 352 | 353 | cv2.imshow(name, sample) 354 | cv2.moveWindow(name, 255, 0) 355 | --------------------------------------------------------------------------------