├── .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 |
--------------------------------------------------------------------------------