Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
7
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Open sidebar
GEPARD
GEPARD
Commits
11fdf4ed
Commit
11fdf4ed
authored
Aug 27, 2020
by
Josef Brandt
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
new simulated raman
parent
c5a3864a
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
699 additions
and
132 deletions
+699
-132
__main__.py
__main__.py
+12
-1
ramancom/ramancontrol.py
ramancom/ramancontrol.py
+1
-1
ramancom/simulated/imageGenerator.py
ramancom/simulated/imageGenerator.py
+270
-0
ramancom/simulated/simulatedStage.py
ramancom/simulated/simulatedStage.py
+300
-0
ramancom/simulated/simulatedraman.py
ramancom/simulated/simulatedraman.py
+116
-130
No files found.
__main__.py
View file @
11fdf4ed
...
...
@@ -72,6 +72,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
def
closeEvent
(
self
,
event
):
self
.
view
.
closeEvent
(
event
)
closeAll
()
@
QtCore
.
pyqtSlot
(
float
)
def
scalingChanged
(
self
,
scale
):
...
...
@@ -337,6 +338,16 @@ if __name__ == '__main__':
import
sys
from
time
import
localtime
,
strftime
def
closeAll
()
->
None
:
"""
Closes the app and, with that, all windows.
Josef: I implemented this, as with the simulated microscope stage it was difficult to find a proper way to
ONLY close it at the end of running the program. Closing it on disconnect of the ramanctrl is not suitable,
as it should be opened also in disconnected stage (e.g., when another instance is running in optical or raman
scan, but the UI (disconnected) should still update what's going on.
"""
app
.
deleteLater
()
def
excepthook
(
excType
,
excValue
,
tracebackobj
):
"""
Global function to catch unhandled exceptions.
...
...
ramancom/ramancontrol.py
View file @
11fdf4ed
...
...
@@ -43,7 +43,7 @@ except KeyError:
pass
if
interface
==
"SIMULATED_RAMAN_CONTROL"
:
from
.simulatedraman
import
SimulatedRaman
from
.simulated
.simulatedraman
import
SimulatedRaman
RamanControl
=
SimulatedRaman
print
(
"WARNING: using only simulated raman control!"
)
simulatedRaman
=
True
...
...
ramancom/simulated/imageGenerator.py
0 → 100644
View file @
11fdf4ed
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>
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/>.
Image Generator for fake images for testing with simulated instrument interfaces
"""
import
cv2
import
numpy
as
np
import
matplotlib.pyplot
as
plt
import
time
import
noise
from
copy
import
deepcopy
from
typing
import
Tuple
,
List
def
particleIsVisible
(
particle
:
np
.
ndarray
,
full
:
np
.
ndarray
,
pos
:
Tuple
[
int
,
int
])
->
bool
:
"""
:param full: image of the current particle
:param particle: image with all particles
:param pos: x, y of the particle center in pixels
"""
isVisible
:
bool
=
False
height
,
width
=
particle
.
shape
[:
2
]
if
pos
[
0
]
+
width
/
2
>=
0
and
pos
[
0
]
-
width
/
2
<
full
.
shape
[
1
]:
if
pos
[
1
]
+
height
/
2
>=
0
and
pos
[
1
]
-
height
/
2
<
full
.
shape
[
0
]:
isVisible
=
True
return
isVisible
def
addTexture
(
partImage
:
np
.
ndarray
)
->
np
.
ndarray
:
shape
=
partImage
.
shape
startX
=
np
.
random
.
randint
(
0
,
100
)
startY
=
np
.
random
.
randint
(
0
,
100
)
for
i
in
range
(
shape
[
0
]):
for
j
in
range
(
shape
[
1
]):
noiseVal
:
float
=
noise
.
pnoise2
((
startY
+
i
)
/
25
,
(
startX
+
j
)
/
25
,
octaves
=
4
,
persistence
=
0.5
)
noiseVal
=
np
.
clip
(
noiseVal
+
0.5
,
0.0
,
1.0
)
# bring to 0...1 range
partImage
[
i
,
j
,
:]
*=
(
noiseVal
**
0.3
)
return
partImage
class
OverlapRange
:
fullStartX
:
int
=
np
.
nan
fullEndX
:
int
=
np
.
nan
fullStartY
:
int
=
np
.
nan
fullEndY
:
int
=
np
.
nan
subStartX
:
int
=
np
.
nan
subEndX
:
int
=
np
.
nan
subStartY
:
int
=
np
.
nan
subEndY
:
int
=
np
.
nan
def
getParticleOverlapRanges
(
full
:
np
.
ndarray
,
sub
:
np
.
ndarray
,
centerXY
:
Tuple
[
int
,
int
])
->
Tuple
[
bool
,
OverlapRange
]:
ranges
:
OverlapRange
=
OverlapRange
()
ranges
.
fullStartX
=
int
(
round
(
centerXY
[
0
]
-
sub
.
shape
[
1
]
/
2
))
ranges
.
fullEndX
=
ranges
.
fullStartX
+
sub
.
shape
[
1
]
ranges
.
fullStartY
=
int
(
round
(
centerXY
[
1
]
-
sub
.
shape
[
0
]
/
2
))
ranges
.
fullEndY
=
ranges
.
fullStartY
+
sub
.
shape
[
0
]
origRanges
=
deepcopy
(
ranges
)
if
ranges
.
fullEndX
<
0
or
ranges
.
fullEndY
<
0
or
ranges
.
fullStartX
>=
full
.
shape
[
1
]
or
ranges
.
fullStartY
>=
full
.
shape
[
0
]:
success
=
False
else
:
success
=
True
if
origRanges
.
fullStartX
>=
0
:
ranges
.
subStartX
=
0
else
:
ranges
.
subStartX
=
-
origRanges
.
fullStartX
ranges
.
fullStartX
=
0
if
origRanges
.
fullEndX
<=
full
.
shape
[
1
]:
ranges
.
subEndX
=
sub
.
shape
[
1
]
else
:
diff
:
int
=
origRanges
.
fullEndX
-
full
.
shape
[
1
]
ranges
.
subEndX
=
sub
.
shape
[
1
]
-
diff
ranges
.
fullEndX
=
full
.
shape
[
1
]
if
origRanges
.
fullStartY
>=
0
:
ranges
.
subStartY
=
0
else
:
ranges
.
subStartY
=
-
origRanges
.
fullStartY
ranges
.
fullStartY
=
0
if
origRanges
.
fullEndY
<=
full
.
shape
[
0
]:
ranges
.
subEndY
=
sub
.
shape
[
0
]
else
:
diff
:
int
=
origRanges
.
fullEndY
-
full
.
shape
[
0
]
ranges
.
subEndY
=
sub
.
shape
[
0
]
-
diff
ranges
.
fullEndY
=
full
.
shape
[
0
]
return
success
,
ranges
class
FakeParticle
:
""" A fake spherical particle"""
x
:
float
=
np
.
nan
# in µm
y
:
float
=
np
.
nan
# in µm
radius
:
float
=
np
.
nan
# in µm
randImageIndex
:
int
=
np
.
nan
class
FakeCamera
:
# TODO: Implement a small angle to simulate a tilted camera!
def
__init__
(
self
):
self
.
imgDims
:
Tuple
[
int
,
int
]
=
(
500
,
250
)
# width, height of camera imgage
self
.
pixelscale
:
float
=
1.0
# µm/px
self
.
sizeScale
:
float
=
100.0
# smaller values make particles rendered smaller and vice versa
self
.
threshold
:
float
=
0.7
# threshold for determining particles. Larger values -> smaller particles
self
.
numZLevels
:
int
=
7
# number of z-levels cached for faking depth of field
self
.
maxZDiff
:
float
=
100
# max difference in z that is simulated.
self
.
currentImage
:
np
.
ndarray
=
np
.
zeros
((
self
.
imgDims
[
1
],
self
.
imgDims
[
0
],
3
))
self
.
fakeFilter
:
FakeFilter
=
FakeFilter
()
def
updateImageAtPosition
(
self
,
position
:
List
[
float
])
->
None
:
"""
Centers the camera at the given µm coordinates and returns the camera image centered at this position.
:param position: tuple (x in µm, y in µm, z in µm)
"""
particles
:
np
.
ndarray
=
np
.
zeros
((
self
.
imgDims
[
1
],
self
.
imgDims
[
0
],
3
),
dtype
=
np
.
uint8
)
for
particle
in
self
.
fakeFilter
.
particles
:
pixelX
,
pixelY
=
self
.
_getParticlePixelCoords
((
position
[
0
],
position
[
1
]),
particle
)
blurRadius
:
int
=
int
(
round
(
abs
(
position
[
2
]
-
particle
.
radius
)
/
2
))
# we just use radius as height..
if
blurRadius
%
2
==
0
:
blurRadius
+=
1
particleImg
:
np
.
ndarray
=
self
.
fakeFilter
.
getParticleImage
(
particle
,
blurRadius
=
blurRadius
)
particles
=
self
.
_addParticleImg
(
particles
,
particleImg
,
(
pixelX
,
pixelY
))
for
i
in
range
(
3
):
particles
[:,
:,
i
]
=
np
.
flipud
(
particles
[:,
:,
i
])
self
.
currentImage
=
particles
def
_addParticleImg
(
self
,
allParts
:
np
.
ndarray
,
curPart
:
np
.
ndarray
,
position
:
Tuple
[
int
,
int
])
->
np
.
ndarray
:
"""
Adds the current particle image to the image with all particles.
:param allParts: image with all particles
:param curPart: image of the current particle
:param position: x, y of the particle center
"""
def
overlayImages
(
img1
:
np
.
ndarray
,
img2
:
np
.
ndarray
)
->
np
.
ndarray
:
assert
img1
.
shape
==
img2
.
shape
merge
=
np
.
zeros_like
(
img1
)
mask
=
np
.
sum
(
img1
,
axis
=
2
)
>
128
merge
[
mask
]
=
img1
[
mask
]
merge
[
~
mask
]
=
img2
[
~
mask
]
return
np
.
uint8
(
merge
)
particleFits
,
ranges
=
getParticleOverlapRanges
(
allParts
,
curPart
,
position
)
if
particleFits
:
img1
=
allParts
[
ranges
.
fullStartY
:
ranges
.
fullEndY
,
ranges
.
fullStartX
:
ranges
.
fullEndX
,
:]
img2
=
curPart
[
ranges
.
subStartY
:
ranges
.
subEndY
,
ranges
.
subStartX
:
ranges
.
subEndX
,
:]
assert
img1
.
shape
==
img2
.
shape
blend
=
overlayImages
(
img1
,
img2
)
allParts
[
ranges
.
fullStartY
:
ranges
.
fullEndY
,
ranges
.
fullStartX
:
ranges
.
fullEndX
,
:]
=
blend
return
allParts
def
_getParticlePixelCoords
(
self
,
position
:
Tuple
[
float
,
float
],
particle
:
FakeParticle
)
->
Tuple
[
int
,
int
]:
"""Converts the absolute particle x,y coordinates into image pixel coordinates
:param position: x and y coordinates of stage center (in µm)
:param particle: the particle in consideration
:return (x, y): pixel Coordinates of x and y of particle in image
"""
imgHeightMicrons
,
imgWidthMicrons
=
self
.
imgDims
[
1
]
*
self
.
pixelscale
,
self
.
imgDims
[
0
]
*
self
.
pixelscale
upperLeftX
,
upperLeftY
=
position
[
0
]
-
imgWidthMicrons
/
2
,
position
[
1
]
-
imgHeightMicrons
/
2
particlePixelX
:
int
=
int
(
round
((
particle
.
x
-
upperLeftX
)
/
self
.
pixelscale
))
particlePixelY
:
int
=
int
(
round
((
particle
.
y
-
upperLeftY
)
/
self
.
pixelscale
))
return
particlePixelX
,
particlePixelY
class
FakeFilter
:
def
__init__
(
self
):
self
.
numParticles
:
int
=
500
self
.
xRange
:
Tuple
[
float
,
float
]
=
(
-
3000
,
3000
)
# min and max of x Dimensions (in µm)
self
.
yRange
:
Tuple
[
float
,
float
]
=
(
-
3000
,
3000
)
# min and max of x Dimensions (in µm)
self
.
particleSizeRange
:
Tuple
[
float
,
float
]
=
(
5
,
50
)
# min and max of particle radius (in µm)
self
.
numIndParticles
:
int
=
10
# number of individual particles presets to generate
self
.
numBlurSteps
:
int
=
7
# number of blur steps to simulate (additionally to image without any blur)
self
.
maxBlurSize
:
int
=
51
# highest blur radius
self
.
baseImageSize
:
int
=
100
self
.
presetParticles
:
List
[
dict
]
=
self
.
_generagePresetParticles
()
self
.
particles
:
List
[
FakeParticle
]
=
[]
self
.
_generateParticles
()
def
getParticleImage
(
self
,
particle
:
FakeParticle
,
blurRadius
:
int
=
0
)
->
np
.
ndarray
:
def
getSizeCorrectedBlurRadius
(
blurRad
:
int
,
scaling
:
float
)
->
int
:
"""If the images are scaled down later, we want to pick a stronger blurred image at this point"""
newRad
:
int
=
int
(
round
(
blurRad
/
scaling
))
if
newRad
>
0
and
newRad
%
2
==
0
:
newRad
+=
1
return
newRad
blurredImages
:
dict
=
self
.
presetParticles
[
particle
.
randImageIndex
]
scaleFac
:
float
=
particle
.
radius
/
(
self
.
baseImageSize
/
2
)
blurRadius
=
getSizeCorrectedBlurRadius
(
blurRadius
,
scaleFac
)
if
blurRadius
==
0
:
img
:
np
.
ndarray
=
blurredImages
[
0
]
else
:
availableRadii
:
np
.
ndarray
=
np
.
array
([
i
for
i
in
blurredImages
.
keys
()])
closestRadius
=
availableRadii
[
np
.
argmin
(
abs
(
availableRadii
-
blurRadius
))]
img
:
np
.
ndarray
=
blurredImages
[
closestRadius
]
return
cv2
.
resize
(
img
,
None
,
fx
=
scaleFac
,
fy
=
scaleFac
)
def
_generagePresetParticles
(
self
)
->
List
[
dict
]:
"""
The list contains a dict for each particle variation, containing the image in different blur stages.
The key of that dicts is the used blur radius
"""
# TODO: Consider moving that into a separate thread.. it takes quite some seconds at startup..
def
getBlurLevels
()
->
np
.
ndarray
:
levels
:
np
.
ndarray
=
np
.
linspace
(
1
,
self
.
maxBlurSize
,
self
.
numBlurSteps
)
for
j
in
range
(
len
(
levels
)):
levels
[
j
]
=
int
(
round
(
levels
[
j
]))
if
levels
[
j
]
%
2
==
0
:
levels
[
j
]
+=
1
return
levels
baseSize
=
self
.
baseImageSize
presetParticles
:
List
[
dict
]
=
[]
np
.
random
.
seed
(
42
)
for
i
in
range
(
self
.
numIndParticles
):
baseImg
:
np
.
ndarray
=
np
.
zeros
((
baseSize
+
self
.
maxBlurSize
,
baseSize
+
self
.
maxBlurSize
,
3
))
centerX
=
centerY
=
int
(
round
(
baseImg
.
shape
[
0
]
/
2
))
radius
:
int
=
int
(
round
(
baseSize
/
2
))
rgb
:
np
.
ndarray
=
0.7
+
np
.
random
.
rand
(
3
)
*
0.3
# desaturate a bit..
randColor
=
(
int
(
rgb
[
0
]
*
255
),
int
(
rgb
[
1
]
*
255
),
int
(
rgb
[
2
]
*
255
))
cv2
.
circle
(
baseImg
,
(
centerX
,
centerY
),
radius
,
randColor
,
thickness
=-
1
)
baseImg
=
np
.
uint8
(
addTexture
(
baseImg
))
newParticleDict
:
dict
=
{
0
:
baseImg
}
for
radius
in
getBlurLevels
():
radius
=
int
(
radius
)
newParticleDict
[
radius
]
=
cv2
.
GaussianBlur
(
baseImg
,
(
radius
,
radius
),
sigmaX
=
0
)
presetParticles
.
append
(
newParticleDict
)
return
presetParticles
def
_generateParticles
(
self
)
->
None
:
self
.
particles
=
[]
np
.
random
.
seed
(
42
)
for
i
in
range
(
self
.
numParticles
):
rands
:
np
.
ndarray
=
np
.
random
.
rand
(
3
)
newParticle
:
FakeParticle
=
FakeParticle
()
newParticle
.
x
=
self
.
xRange
[
0
]
+
rands
[
0
]
*
(
self
.
xRange
[
1
]
-
self
.
xRange
[
0
])
newParticle
.
y
=
self
.
yRange
[
0
]
+
rands
[
1
]
*
(
self
.
yRange
[
1
]
-
self
.
yRange
[
0
])
newParticle
.
radius
=
self
.
particleSizeRange
[
0
]
+
rands
[
2
]
*
(
self
.
particleSizeRange
[
1
]
-
self
.
particleSizeRange
[
0
])
newParticle
.
randImageIndex
=
np
.
random
.
randint
(
0
,
self
.
numIndParticles
)
self
.
particles
.
append
(
newParticle
)
ramancom/simulated/simulatedStage.py
0 → 100644
View file @
11fdf4ed
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>
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/>.
Simulates a Microscope Stage with Camera.
"""
import
os
import
sys
from
PyQt5
import
QtWidgets
,
QtGui
,
QtCore
import
cv2
import
json
import
numpy
as
np
from
typing
import
List
try
:
from
.imageGenerator
import
FakeCamera
except
ImportError
:
from
imageGenerator
import
FakeCamera
class
SimulatedStage
(
object
):
def
__init__
(
self
,
ui
:
bool
=
True
):
super
(
SimulatedStage
,
self
).
__init__
()
self
.
currentpos
:
List
[
float
]
=
[
0.0
,
0.0
,
0.0
]
# µm pos in x, y and z
self
.
offset
:
List
[
float
]
=
[
0.0
,
0.0
,
0.0
]
# µm offset in x, y and z
self
.
rotMatrix
:
np
.
ndarray
=
np
.
zeros
((
3
,
3
))
self
.
camera
:
FakeCamera
=
FakeCamera
()
self
.
camera
.
updateImageAtPosition
(
self
.
currentpos
)
self
.
filepath
:
str
=
self
.
_getFilePath
()
self
.
uiEnabled
=
ui
if
self
.
uiEnabled
:
self
.
ui
:
SimulatedStageUI
=
SimulatedStageUI
(
self
)
self
.
ui
.
show
()
def
getCurrentCameraImage
(
self
)
->
np
.
ndarray
:
return
self
.
camera
.
currentImage
def
moveToPosition
(
self
,
x
:
float
,
y
:
float
,
z
:
float
):
self
.
currentpos
=
[
x
,
y
,
z
]
self
.
saveConfigToFile
()
self
.
camera
.
updateImageAtPosition
(
self
.
currentpos
)
if
self
.
uiEnabled
:
self
.
ui
.
updateCameraImage
()
self
.
ui
.
updateStageCoords
()
def
connect
(
self
)
->
None
:
self
.
updateConfigFromFile
()
if
self
.
uiEnabled
:
self
.
ui
.
setEnabled
(
True
)
def
disconnect
(
self
)
->
None
:
self
.
saveConfigToFile
()
if
self
.
uiEnabled
:
self
.
ui
.
setEnabled
(
False
)
def
updateConfigFromFile
(
self
)
->
None
:
if
os
.
path
.
exists
(
self
.
filepath
):
with
open
(
self
.
filepath
,
'r'
)
as
fp
:
try
:
configHasChanged
:
bool
=
self
.
_dictToConfig
(
json
.
load
(
fp
))
except
json
.
JSONDecodeError
:
# TODO: get reference to logger and log info that loading config failed..
print
(
'failed updating simulated stage state from file..'
)
return
if
configHasChanged
:
self
.
camera
.
updateImageAtPosition
(
self
.
currentpos
)
if
self
.
uiEnabled
:
self
.
ui
.
updateCameraImage
()
self
.
ui
.
updateStageCoords
()
def
saveConfigToFile
(
self
)
->
None
:
if
self
.
filepath
!=
''
:
with
open
(
self
.
filepath
,
'w'
)
as
fp
:
json
.
dump
(
self
.
_configToDict
(),
fp
)
def
_getFilePath
(
self
)
->
str
:
self
.
app
:
QtWidgets
.
QApplication
=
QtWidgets
.
QApplication
(
sys
.
argv
)
# has to be an instance attribute :/
self
.
app
.
setApplicationName
(
"GEPARD"
)
path
:
str
=
QtCore
.
QStandardPaths
.
writableLocation
(
QtCore
.
QStandardPaths
.
AppLocalDataLocation
)
return
os
.
path
.
join
(
path
,
'simulatedStageConfig.txt'
)
def
_configToDict
(
self
)
->
dict
:
return
{
'currentPos'
:
self
.
currentpos
}
def
_dictToConfig
(
self
,
inputDict
:
dict
)
->
bool
:
configChanged
:
bool
=
False
if
self
.
currentpos
!=
inputDict
[
'currentPos'
]:
self
.
currentpos
=
inputDict
[
'currentPos'
]
configChanged
=
True
return
configChanged
class
SimulatedStageUI
(
QtWidgets
.
QWidget
):
def
__init__
(
self
,
stageParent
:
SimulatedStage
):
super
(
SimulatedStageUI
,
self
).
__init__
()
self
.
stageParent
:
SimulatedStage
=
stageParent
self
.
updateTimer
:
QtCore
.
QTimer
=
QtCore
.
QTimer
()
self
.
updateTimer
.
setSingleShot
(
False
)
self
.
updateTimer
.
timeout
.
connect
(
self
.
stageParent
.
updateConfigFromFile
)
self
.
updateTimer
.
start
(
500
)
self
.
setWindowTitle
(
'Simulated Microscope Stage'
)
coordsGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Stage Coordinates'
)
coordsLayout
:
QtWidgets
.
QHBoxLayout
=
QtWidgets
.
QHBoxLayout
()
coordsGroup
.
setLayout
(
coordsLayout
)
self
.
labelX
:
QtWidgets
.
QLabel
=
QtWidgets
.
QLabel
()
self
.
labelY
:
QtWidgets
.
QLabel
=
QtWidgets
.
QLabel
()
self
.
labelZ
:
QtWidgets
.
QLabel
=
QtWidgets
.
QLabel
()
for
label
in
[
self
.
labelX
,
self
.
labelY
,
self
.
labelZ
]:
coordsLayout
.
addWidget
(
label
)
self
.
camView
:
QtWidgets
.
QGraphicsView
=
self
.
_createCamView
()
self
.
stepSizeSpinbox
:
QtWidgets
.
QSpinBox
=
QtWidgets
.
QSpinBox
()
self
.
stageControls
:
QtWidgets
.
QGroupBox
=
self
.
_createControls
()
layout
:
QtWidgets
.
QHBoxLayout
=
QtWidgets
.
QHBoxLayout
()
self
.
setLayout
(
layout
)
leftColumn
:
QtWidgets
.
QVBoxLayout
=
QtWidgets
.
QVBoxLayout
()
leftColumn
.
addWidget
(
coordsGroup
)
leftColumn
.
addWidget
(
QtWidgets
.
QLabel
(
'Camera Image'
))
leftColumn
.
addWidget
(
self
.
camView
)
leftColumn
.
addWidget
(
self
.
stageControls
)
layout
.
addLayout
(
leftColumn
)
rightColumn
:
QtWidgets
.
QGroupBox
=
self
.
_getExtrasColumn
()
layout
.
addWidget
(
rightColumn
)
self
.
updateCameraImage
()
self
.
updateStageCoords
()
def
_createCamView
(
self
)
->
QtWidgets
.
QGraphicsView
:
camView
:
QtWidgets
.
QGraphicsView
=
QtWidgets
.
QGraphicsView
()
camView
.
item
=
QtWidgets
.
QGraphicsPixmapItem
()
scene
=
QtWidgets
.
QGraphicsScene
(
camView
)
scene
.
addItem
(
camView
.
item
)
camView
.
setScene
(
scene
)
camView
.
scale
(
1.0
,
1.0
)
return
camView
def
_createControls
(
self
)
->
QtWidgets
.
QGroupBox
:
def
makeBtnLambda
(
_btn
:
QtWidgets
.
QPushButton
):
return
lambda
:
self
.
_moveBtnPressed
(
_btn
)
controls
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Stage Controls'
)
layout
:
QtWidgets
.
QGridLayout
=
QtWidgets
.
QGridLayout
()
controls
.
setLayout
(
layout
)
stepSizeLayout
:
QtWidgets
.
QHBoxLayout
=
QtWidgets
.
QHBoxLayout
()
stepSizeLayout
.
addStretch
()
stepSizeLayout
.
addWidget
(
QtWidgets
.
QLabel
(
'Set Step Size (µm): '
))
stepSizeLayout
.
addWidget
(
self
.
stepSizeSpinbox
)
stepSizeLayout
.
addStretch
()
self
.
stepSizeSpinbox
.
setMinimum
(
0
)
self
.
stepSizeSpinbox
.
setValue
(
10
)
self
.
stepSizeSpinbox
.
setMaximum
(
100000
)
self
.
stepSizeSpinbox
.
setSingleStep
(
50
)
self
.
stepSizeSpinbox
.
setMaximumWidth
(
75
)
xyGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'XY Controls'
)
xyLayout
:
QtWidgets
.
QGridLayout
=
QtWidgets
.
QGridLayout
()
xyGroup
.
setLayout
(
xyLayout
)
upBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'UP'
)
downBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'DOWN'
)
leftBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'LEFT'
)
rightBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'RIGHT'
)
for
btn
in
[
upBtn
,
downBtn
,
leftBtn
,
rightBtn
]:
btn
.
clicked
.
connect
(
makeBtnLambda
(
btn
))
xyLayout
.
addWidget
(
upBtn
,
0
,
1
)
xyLayout
.
addWidget
(
leftBtn
,
1
,
0
)
xyLayout
.
addWidget
(
rightBtn
,
1
,
2
)
xyLayout
.
addWidget
(
downBtn
,
2
,
1
)
zGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Z Controls'
)
zLayout
:
QtWidgets
.
QVBoxLayout
=
QtWidgets
.
QVBoxLayout
()
zGroup
.
setLayout
(
zLayout
)
zUpBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'HIGHER'
)
zDownBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'LOWER'
)
for
btn
in
[
zUpBtn
,
zDownBtn
]:
btn
.
clicked
.
connect
(
makeBtnLambda
(
btn
))
zLayout
.
addWidget
(
btn
)
layout
.
addLayout
(
stepSizeLayout
,
0
,
0
,
1
,
2
)
layout
.
addWidget
(
xyGroup
,
1
,
0
)
layout
.
addWidget
(
zGroup
,
1
,
1
)
return
controls
def
_getExtrasColumn
(
self
)
->
QtWidgets
.
QGroupBox
:
def
makePresetBtnLambda
(
pressedBtn
:
QtWidgets
.
QPushButton
):
return
lambda
:
self
.
_moveToPresetPosition
(
pressedBtn
)
extrasGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Extra Operations'
)
extrasLayout
:
QtWidgets
.
QVBoxLayout
=
QtWidgets
.
QVBoxLayout
()
extrasGroup
.
setLayout
(
extrasLayout
)
presetPositionsGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Move to preset positions'
)
presetPositionsLayout
:
QtWidgets
.
QGridLayout
=
QtWidgets
.
QGridLayout
()
presetPositionsGroup
.
setLayout
(
presetPositionsLayout
)
upperLeftBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'UpperLeft'
)
lowerLeftBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'LowerLeft'
)
upperRightBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'UpperRight'
)
lowerRightBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'LowerRight'
)
centerBtn
:
QtWidgets
.
QPushButton
=
QtWidgets
.
QPushButton
(
'Center'
)
for
btn
in
[
upperLeftBtn
,
upperRightBtn
,
lowerLeftBtn
,
lowerRightBtn
,
centerBtn
]:
btn
.
clicked
.
connect
(
makePresetBtnLambda
(
btn
))
presetPositionsLayout
.
addWidget
(
upperLeftBtn
,
0
,
0
)
presetPositionsLayout
.
addWidget
(
upperRightBtn
,
0
,
2
)
presetPositionsLayout
.
addWidget
(
centerBtn
,
1
,
1
)
presetPositionsLayout
.
addWidget
(
lowerLeftBtn
,
2
,
0
)
presetPositionsLayout
.
addWidget
(
lowerRightBtn
,
2
,
2
)
coordGroup
:
QtWidgets
.
QGroupBox
=
QtWidgets
.
QGroupBox
(
'Modify Coordinate System'
)
coordLayout
:
QtWidgets
.
QFormLayout
=
QtWidgets
.
QFormLayout
()
coordGroup
.
setLayout
(
coordLayout
)
extrasLayout
.
addWidget
(
presetPositionsGroup
)
extrasLayout
.
addWidget
(
coordGroup
)
return
extrasGroup
def
_moveBtnPressed
(
self
,
btn
:
QtWidgets
.
QPushButton
)
->
None
:
label
:
str
=
btn
.
text
()
newPos
:
List
[
float
]
=
self
.
stageParent
.
currentpos
if
label
==
'UP'
:
newPos
[
1
]
+=
self
.
stepSizeSpinbox
.
value
()
elif
label
==
'DOWN'
:
newPos
[
1
]
-=
self
.
stepSizeSpinbox
.
value
()
elif
label
==
'LEFT'
:
newPos
[
0
]
-=
self
.
stepSizeSpinbox
.
value
()
elif
label
==
'RIGHT'
:
newPos
[
0
]
+=
self
.
stepSizeSpinbox
.
value
()
elif
label
==
'HIGHER'
:
newPos
[
2
]
+=
self
.
stepSizeSpinbox
.
value
()
elif
label
==
'LOWER'
:
newPos
[
2
]
-=
self
.
stepSizeSpinbox
.
value
()
self
.
stageParent
.
moveToPosition
(
newPos
[
0
],
newPos
[
1
],
newPos
[
2
])
def
_moveToPresetPosition
(
self
,
btn
:
QtWidgets
.
QPushButton
,
stepSize
:
float
=
1000
)
->
None
:
label
:
str
=
btn
.
text
()
newPos
:
List
[
float
]
=
[
0.0
,
0.0
,
0.0
]
if
label
==
'UpperLeft'
:
newPos
[
0
]
-=
stepSize
newPos
[
1
]
+=
stepSize
elif
label
==
'UpperRight'
:
newPos
[
0
]
+=
stepSize
newPos
[
1
]
+=
stepSize
elif
label
==
'LowerLeft'
:
newPos
[
0
]
-=
stepSize
newPos
[
1
]
-=
stepSize
elif
label
==
'LowerRight'
:
newPos
[
0
]
+=
stepSize
newPos
[
1
]
-=
stepSize
elif
label
==
'Center'
:
pass
self
.
stageParent
.
moveToPosition
(
newPos
[
0
],
newPos
[
1
],
newPos
[
2
])