-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparticles.py
176 lines (146 loc) · 6.19 KB
/
particles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""
HSI Classifier
Copyright (C) 2021 Josef Brandt, University of Gothenburg <[email protected]>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import cv2
import numpy as np
from typing import *
from collections import Counter
from dataclasses import dataclass
from particledetection.detection import getParticleContours
if TYPE_CHECKING:
from classification.classifiers import BatchClassificationResult
from gui.classUI import ClassInterpretationParams
class ParticleHandler:
__particleID: int = -1
@classmethod
def getNewParticleID(cls) -> int:
"""
Returns a unique particle id.
"""
cls.__particleID += 1
return cls.__particleID
def __init__(self):
self._particles: Dict[int, 'Particle'] = {} # key: unique id, value: Particle object
def getParticlesFromImage(self, binaryImage: np.ndarray) -> None:
"""
Takes a binary image and finds particles.
"""
contours: List[np.ndarray] = getParticleContours(binaryImage)
self._particles = {}
for cnt in contours:
newID: int = ParticleHandler.getNewParticleID()
self._particles[newID] = Particle(newID, cnt)
def getParticles(self) -> List['Particle']:
"""
Returns the current list of particles.
"""
return list(self._particles.values())
def getAssigmentOfParticleOfID(self, id: int, interpretationParams: 'ClassInterpretationParams') -> str:
"""
Returns the assignment of the partice specified by the id.
:param id: The particle's id
:param interpretationParams: The parameters for interpreting the spectra results.
:return: assignment
"""
return self._particles[id].getAssignment(interpretationParams)
def resetParticleResults(self) -> None:
"""
Resets all particle results
"""
for particle in self._particles.values():
particle.resetResult()
def flipParticlesVertically(self, cubeShape: tuple) -> None:
cubeHeight: int = cubeShape[1]
for particle in self._particles.values():
cnt: np.ndarray = particle.getContour()
cnt[:, 0, 1] = cubeHeight - cnt[:, 0, 1]
def flipParticlesHorizontally(self, cubeShape: tuple) -> None:
cubeWidth: int = cubeShape[2]
for particle in self._particles.values():
cnt: np.ndarray = particle.getContour()
cnt[:, 0, 0] = cubeWidth - cnt[:, 0, 0]
@dataclass
class Particle:
__id: int
_contour: np.ndarray
_result: Union[None, 'BatchClassificationResult'] = None
def getID(self) -> int:
return self.__id
def getAssignment(self, params: 'ClassInterpretationParams') -> str:
"""
Returns the assignment string according the currently set threshold.
:param params: Parameters for correct result interpretation.
"""
assignment: str = "unknown"
if self._result is not None:
classNames: np.ndarray = self._result.getResults(cutoff=params.specConfThreshold)
if params.ignoreUnkowns and not np.all(classNames == "unknown"):
classNames = classNames[classNames != "unknown"]
counter: Counter = Counter(classNames)
numTotal: int = sum(counter.values())
numClasses: int = len(counter)
mostFreqClass, highestCount = counter.most_common(numClasses)[0]
if highestCount / numTotal >= params.partConfThreshold:
assignment = mostFreqClass
return assignment
def getContour(self) -> np.ndarray:
"""
Gets the particle's contour.
"""
return self._contour
def getSpectraArray(self, cube: np.ndarray, binning: int = 1) -> np.ndarray:
"""
Takes the spectrum cube and extracts the spectra according the particle's contour.
:param cube: 3D spec cube
:param binning: number of spectra to average. If binning=1, no binning is performed.
:return: MxN array of M specs with N wavelenghts
"""
specs: List[np.ndarray] = []
mask: np.ndarray = np.zeros(cube.shape[1:])
cv2.drawContours(mask, [self._contour], -1, 1, thickness=-1)
indY, indX = np.where(mask == 1)
for y, x in zip(indY, indX):
specs.append(cube[:, y, x])
specs: np.ndarray = np.array(specs)
if binning > 1:
numSpecs: int = specs.shape[0]
if binning >= numSpecs:
binnedSpecs: np.ndarray = np.mean(specs, axis=0)[np.newaxis, :]
else:
binnedSpecs: List[np.ndarray] = []
ind: np.ndarray = np.arange(binning)
for i in range(numSpecs // binning):
if i > 0:
ind += binning
binnedSpecs.append(np.mean(specs[ind, :], axis=0))
lastIndex: int = ind[-1]
if lastIndex < numSpecs - 1: # one or more spectra are left
if lastIndex == numSpecs-2: # exactly one spec is left
binnedSpecs.append(specs[-1, :])
else: # more specs are left
binnedSpecs.append(np.mean(specs[lastIndex:, :], axis=0))
binnedSpecs: np.ndarray = np.array(binnedSpecs)
specs = binnedSpecs
return specs
def setBatchResult(self, batchRes: 'BatchClassificationResult') -> None:
"""
Sets the results counter with the given assignments.
"""
self._result = batchRes
def resetResult(self) -> None:
"""
Resets the result.
"""
self._result = None