micat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- micat/__init__.py +13 -0
- micat/__main__.py +186 -0
- micat/report_assets/OpenSans-Bold.ttf +0 -0
- micat/report_assets/OpenSans-Regular.ttf +0 -0
- micat/report_assets/OpenSans-SemiBold.ttf +0 -0
- micat/report_assets/emblem_pink.svg +7 -0
- micat/report_assets/main.typ +154 -0
- micat/usafcal.py +670 -0
- micat/usafcal_report.py +131 -0
- micat/utils.py +91 -0
- micat-0.1.0.dist-info/METADATA +80 -0
- micat-0.1.0.dist-info/RECORD +15 -0
- micat-0.1.0.dist-info/WHEEL +4 -0
- micat-0.1.0.dist-info/entry_points.txt +2 -0
- micat-0.1.0.dist-info/licenses/LICENSE +674 -0
micat/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Micat is for Microscopy Calibration and Testing using USAF resolution targets.
|
|
2
|
+
|
|
3
|
+
Micat is planned to become a larger library of calibration tools, but for now the only
|
|
4
|
+
calibration it does is calibrate the size of pixels in a micrograph. The internal API
|
|
5
|
+
for these features are in `usafcal`.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import usafcal
|
|
12
|
+
|
|
13
|
+
__all__ = ["usafcal"]
|
micat/__main__.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""The module used for CLI operation."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import openflexure_metadata
|
|
9
|
+
import piexif
|
|
10
|
+
|
|
11
|
+
import micat
|
|
12
|
+
from micat.usafcal_report import export_results
|
|
13
|
+
|
|
14
|
+
METADATA_UNREAD_VALUE = "unknown"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _create_cli_parsers() -> tuple[argparse.ArgumentParser, argparse.ArgumentParser]:
|
|
18
|
+
"""Create the command line argument parser.
|
|
19
|
+
|
|
20
|
+
:return: The main parser and the subparser for the usafcal command.
|
|
21
|
+
"""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Micat Microscopy Calibration and Testing",
|
|
24
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
25
|
+
)
|
|
26
|
+
subparsers = parser.add_subparsers(
|
|
27
|
+
help="Available commands listed below", metavar="<command>", dest="command"
|
|
28
|
+
)
|
|
29
|
+
parser_usafcal = subparsers.add_parser(
|
|
30
|
+
"usafcal", help="Analyse an image of a USAF target"
|
|
31
|
+
)
|
|
32
|
+
parser_usafcal.add_argument(
|
|
33
|
+
"imagefiles", type=str, nargs="+", help="the image file to analyse"
|
|
34
|
+
)
|
|
35
|
+
parser_usafcal.add_argument("--verbose", "-v", action="count")
|
|
36
|
+
parser_usafcal.add_argument("--outdir", "-O", help="Directory for saved data")
|
|
37
|
+
parser_usafcal.add_argument(
|
|
38
|
+
"--group",
|
|
39
|
+
"-g",
|
|
40
|
+
type=int,
|
|
41
|
+
default=7,
|
|
42
|
+
help="Set smallest group found, see PDF output! (default: 7)",
|
|
43
|
+
)
|
|
44
|
+
parser_usafcal.add_argument(
|
|
45
|
+
"--element",
|
|
46
|
+
"-e",
|
|
47
|
+
type=int,
|
|
48
|
+
default=6,
|
|
49
|
+
help="Set smallest element found, see PDF output! (default: 6)",
|
|
50
|
+
)
|
|
51
|
+
parser_usafcal.add_argument(
|
|
52
|
+
"--largest",
|
|
53
|
+
"-L",
|
|
54
|
+
action="count",
|
|
55
|
+
help="Makes --group and --element to refer to largest not smallest",
|
|
56
|
+
)
|
|
57
|
+
parser_help = subparsers.add_parser(
|
|
58
|
+
"help", help="Run 'help <command>' for detailed help"
|
|
59
|
+
)
|
|
60
|
+
parser_help.add_argument(
|
|
61
|
+
"h_command",
|
|
62
|
+
metavar="<command>",
|
|
63
|
+
nargs="?",
|
|
64
|
+
type=str,
|
|
65
|
+
help="Command to show help for",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return parser, parser_usafcal
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> None:
|
|
72
|
+
"""Run Micat.
|
|
73
|
+
|
|
74
|
+
This is the main function when Micat is run as a CLI tool.
|
|
75
|
+
"""
|
|
76
|
+
parser, parser_usafcal = _create_cli_parsers()
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
if args.command == "usafcal":
|
|
80
|
+
_run_usafcal(args)
|
|
81
|
+
|
|
82
|
+
elif args.command == "help":
|
|
83
|
+
if args.h_command is None:
|
|
84
|
+
parser.print_help()
|
|
85
|
+
elif args.h_command == "usafcal":
|
|
86
|
+
print(
|
|
87
|
+
"\n"
|
|
88
|
+
"Analyse images of USAF 1951 Resolution Targets to calibrate the "
|
|
89
|
+
"field of view and the numper of microns per pixel.\n"
|
|
90
|
+
)
|
|
91
|
+
print(parser_usafcal.format_help())
|
|
92
|
+
else:
|
|
93
|
+
print(f"Invalid micat command {args.h_command}\n\n")
|
|
94
|
+
parser.print_help()
|
|
95
|
+
else:
|
|
96
|
+
print(f"Invalid micat command {args.command}")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_usafcal(args: argparse.Namespace) -> None:
|
|
101
|
+
"""Run the USAF Target calibration based on the specified command line arguments."""
|
|
102
|
+
if not (1 <= args.group <= 7 and 1 <= args.element <= 6):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Group must be between 1 and 7. Element must be between 1 and 6."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
verbose = args.verbose is not None
|
|
108
|
+
|
|
109
|
+
largest = args.largest is not None
|
|
110
|
+
|
|
111
|
+
for filepath in args.imagefiles:
|
|
112
|
+
if verbose:
|
|
113
|
+
print("Analysing %s" % filepath)
|
|
114
|
+
|
|
115
|
+
directory, filename = os.path.split(filepath)
|
|
116
|
+
basefile, ext = os.path.splitext(filename)
|
|
117
|
+
|
|
118
|
+
target = micat.usafcal.analyse_file(
|
|
119
|
+
filepath,
|
|
120
|
+
group=args.group,
|
|
121
|
+
element=args.element,
|
|
122
|
+
smallest=not largest,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if args.outdir is not None:
|
|
126
|
+
directory = args.outdir
|
|
127
|
+
|
|
128
|
+
os.makedirs(directory, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
if verbose:
|
|
131
|
+
print("Exporting results...")
|
|
132
|
+
metadata = read_metadata(filepath)
|
|
133
|
+
export_results(
|
|
134
|
+
target, os.path.join(directory, basefile + "_USAF_calibration"), metadata
|
|
135
|
+
)
|
|
136
|
+
if verbose:
|
|
137
|
+
print("Done!")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def read_metadata(filepath: str) -> dict:
|
|
141
|
+
"""Read selected image metadata from the given filepath."""
|
|
142
|
+
exif = piexif.load(filepath)
|
|
143
|
+
|
|
144
|
+
user_comments = openflexure_metadata.get_exif_usercomment_json(exif)
|
|
145
|
+
capture_time = openflexure_metadata.get_capture_time(exif)
|
|
146
|
+
|
|
147
|
+
# Fully structured default schema
|
|
148
|
+
metadata: dict = {
|
|
149
|
+
"camera": METADATA_UNREAD_VALUE,
|
|
150
|
+
"system": {
|
|
151
|
+
"hostname": METADATA_UNREAD_VALUE,
|
|
152
|
+
"microscope-uuid": METADATA_UNREAD_VALUE,
|
|
153
|
+
"version": METADATA_UNREAD_VALUE,
|
|
154
|
+
"version_source": METADATA_UNREAD_VALUE,
|
|
155
|
+
},
|
|
156
|
+
"capture_time": METADATA_UNREAD_VALUE,
|
|
157
|
+
"capture_time_unix": METADATA_UNREAD_VALUE,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if isinstance(user_comments, dict):
|
|
161
|
+
metadata["camera"] = user_comments.get("camera", METADATA_UNREAD_VALUE)
|
|
162
|
+
|
|
163
|
+
system = user_comments.get("system")
|
|
164
|
+
if isinstance(system, dict):
|
|
165
|
+
metadata["system"]["hostname"] = system.get(
|
|
166
|
+
"hostname", METADATA_UNREAD_VALUE
|
|
167
|
+
)
|
|
168
|
+
metadata["system"]["microscope-uuid"] = system.get(
|
|
169
|
+
"microscope-uuid", METADATA_UNREAD_VALUE
|
|
170
|
+
)
|
|
171
|
+
metadata["system"]["version"] = system.get("version", METADATA_UNREAD_VALUE)
|
|
172
|
+
metadata["system"]["version_source"] = system.get(
|
|
173
|
+
"version_source", METADATA_UNREAD_VALUE
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Capture time
|
|
177
|
+
if capture_time is not None:
|
|
178
|
+
metadata["capture_time"] = datetime.fromtimestamp(capture_time).strftime(
|
|
179
|
+
"%d %B %Y, %H:%M"
|
|
180
|
+
)
|
|
181
|
+
metadata["capture_time_unix"] = capture_time
|
|
182
|
+
return metadata
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg width="100%" height="100%" viewBox="0 0 263 163" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
4
|
+
<g id="path3773" transform="matrix(0.376342,0,0,0.376342,-8.65757,-85.6939)">
|
|
5
|
+
<path d="M212.85,344.652C234.052,276.913 297.347,227.702 372.047,227.702C446.747,227.702 510.042,276.913 531.245,344.652L531.246,344.656C531.789,346.391 532.304,348.138 532.791,349.896C556.452,421.618 655.593,393.868 655.593,393.868L721.09,563.056L472.356,659.347L430.306,550.728L430.129,550.794L406.853,490.142C408.621,489.464 410.361,488.736 412.072,487.962C419.736,484.232 426.404,480.109 432.198,475.703C447.995,463.209 459.586,446.041 465.257,426.69C473.38,395.32 463.384,366.691 463.101,365.891C449.309,328.959 413.54,302.366 372.047,302.366C330.554,302.366 294.785,328.959 280.994,365.891C280.711,366.689 270.714,395.319 278.838,426.69C284.509,446.041 296.099,463.209 311.897,475.703C317.69,480.109 324.359,484.232 332.023,487.962C333.734,488.736 335.474,489.464 337.242,490.142L337.166,490.34L313.965,550.794L313.788,550.728L271.739,659.347L23.004,563.056L88.501,393.868C88.501,393.868 187.642,421.618 211.303,349.896C211.79,348.138 212.306,346.391 212.849,344.656L212.85,344.652Z" style="fill:rgb(197,36,127);fill-rule:nonzero;"/>
|
|
6
|
+
</g>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#let report-title = "Micat Calibration Report"
|
|
2
|
+
#let um = $upright(#sym.mu m)$
|
|
3
|
+
#let um2 = $upright(#sym.mu m^2)$
|
|
4
|
+
#let report-date = datetime.today().display("[day] [month repr:long] [year]")
|
|
5
|
+
|
|
6
|
+
#let calibration_data = json(bytes(sys.inputs.calibration_data))
|
|
7
|
+
|
|
8
|
+
#set document(
|
|
9
|
+
title: report-title,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
#set page(
|
|
13
|
+
paper: "a4",
|
|
14
|
+
margin: (x: 2.5cm, y: 2cm),
|
|
15
|
+
numbering: "1",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
#set text(
|
|
19
|
+
font: "Open Sans",
|
|
20
|
+
size: 12pt,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
#set heading(numbering: "1.")
|
|
24
|
+
#show heading: set block(below: 2em)
|
|
25
|
+
|
|
26
|
+
#set page(
|
|
27
|
+
paper: "a4",
|
|
28
|
+
margin: (x: 2.5cm, y: 2cm),
|
|
29
|
+
footer: context [
|
|
30
|
+
#align(center)[
|
|
31
|
+
#text(size: 9pt, fill: gray)[
|
|
32
|
+
Micat Calibration Report · Micat Version #calibration_data.software_version
|
|
33
|
+
]
|
|
34
|
+
#linebreak()
|
|
35
|
+
#text(size: 9pt, fill: gray)[
|
|
36
|
+
Page #counter(page).display("1")
|
|
37
|
+
of #counter(page).final().at(0)
|
|
38
|
+
· #report-date
|
|
39
|
+
]
|
|
40
|
+
]
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
#grid(
|
|
44
|
+
columns: (auto, auto),
|
|
45
|
+
gutter: 20pt,
|
|
46
|
+
align: horizon+center,
|
|
47
|
+
image("emblem_pink.svg", height: 40pt),
|
|
48
|
+
text(size: 22pt, weight: 700)[#report-title]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
Microscope name: *#calibration_data.metadata.system.hostname*
|
|
52
|
+
|
|
53
|
+
= USAF Resolution Target Analysis Output
|
|
54
|
+
|
|
55
|
+
Effective size of pixel = #calibration_data.um_per_px #sym.plus.minus #calibration_data.um_per_px_uncert #um
|
|
56
|
+
|
|
57
|
+
Field of view = (#calibration_data.fov0 #sym.plus.minus #calibration_data.fov0_uncert #sym.times #calibration_data.fov1 #sym.plus.minus #calibration_data.fov1_uncert) #um2
|
|
58
|
+
|
|
59
|
+
These uncertainties are from the regression only and may be an under estimation.
|
|
60
|
+
|
|
61
|
+
This is based on an image size of #calibration_data.fov_px.at(0) #sym.times #calibration_data.fov_px.at(1) pixels, and will need scaling for other images.
|
|
62
|
+
|
|
63
|
+
Greyscale input image has hashlib md5 hash #calibration_data.md5_hash
|
|
64
|
+
|
|
65
|
+
Report produced on #report-date
|
|
66
|
+
|
|
67
|
+
Micat analysis software version: #calibration_data.software_version
|
|
68
|
+
|
|
69
|
+
#text(blue)[NOTE: These results are only valid if all elements in the following image are correctly labelled.]
|
|
70
|
+
|
|
71
|
+
#pagebreak()
|
|
72
|
+
|
|
73
|
+
= USAF Resolution Labelled Image
|
|
74
|
+
|
|
75
|
+
#text(blue)[Ensure that the elements have the correct group and element number before using these results.]
|
|
76
|
+
|
|
77
|
+
Smallest labelled element is group #calibration_data.smallest_group, element #calibration_data.smallest_element.
|
|
78
|
+
|
|
79
|
+
#figure(
|
|
80
|
+
caption: [Labelled USAF resolution target used for calibration. Red boxes mark a set of 3 lines, each of which will be surrounded by a dark blue box. Light blue highlights the squares, and green highlights other features including numbers.],
|
|
81
|
+
[
|
|
82
|
+
#align(center)[
|
|
83
|
+
#image("file.jpg", width: 80%, fit:"contain")
|
|
84
|
+
]
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
#pagebreak()
|
|
89
|
+
|
|
90
|
+
= USAF Resolution Plots
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
The spacing plot should be an increasing, linear fit. The residuals should be distributed around y = 0 with no overall trends.
|
|
94
|
+
|
|
95
|
+
#image("regression.png", width:100%)
|
|
96
|
+
|
|
97
|
+
= System Information
|
|
98
|
+
|
|
99
|
+
// Produce the rows from given labels and variables, handling either dicts or strings
|
|
100
|
+
#let render-metadata(label, value) = {
|
|
101
|
+
if type(value) == dictionary {
|
|
102
|
+
let rows = value.pairs().map(((k, v)) => (k, str(v)))
|
|
103
|
+
(table.cell(colspan: 2)[*#label*],) + rows.flatten()
|
|
104
|
+
} else {
|
|
105
|
+
(table.cell(colspan: 2)[*#label*], table.cell(colspan: 2)[#str(value)])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#table(
|
|
110
|
+
columns: (auto, 1fr),
|
|
111
|
+
stroke: 0.5pt,
|
|
112
|
+
fill: (_, row) => if calc.odd(row) { luma(245) } else { white },
|
|
113
|
+
table.header(table.cell(colspan: 2, align: center)[*Metadata*]),
|
|
114
|
+
..render-metadata("Camera", calibration_data.metadata.camera),
|
|
115
|
+
..render-metadata("Microscope", calibration_data.metadata.system),
|
|
116
|
+
table.cell(colspan: 2)[*Capture Time*],
|
|
117
|
+
[capture_time], [#calibration_data.metadata.capture_time],
|
|
118
|
+
[capture_time_unix], [#str(calibration_data.metadata.capture_time_unix)],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
#pagebreak()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
= Methodology
|
|
125
|
+
|
|
126
|
+
Calibration was performed using an automated analysis of a USAF 1951 resolution target image.
|
|
127
|
+
|
|
128
|
+
The input image is first converted to greyscale and thresholded to identify contours.
|
|
129
|
+
Detected features are classified based on shape: rectangular features with an aspect
|
|
130
|
+
ratio consistent with USAF bars (approximately 5:1) are retained, while other features
|
|
131
|
+
are discarded or labelled separately.
|
|
132
|
+
|
|
133
|
+
Bars are grouped into sets of three based on their relative spacing and alignment,
|
|
134
|
+
forming individual USAF elements. The spacing between bars is measured in pixels, and
|
|
135
|
+
each element is assigned a group and element number based on the known geometric
|
|
136
|
+
progression of the USAF target.
|
|
137
|
+
|
|
138
|
+
The physical spacing corresponding to each element is calculated using the standard
|
|
139
|
+
relationship for line pairs per millimetre:
|
|
140
|
+
|
|
141
|
+
$ 2^{(g + (e - 1)/6)} $
|
|
142
|
+
|
|
143
|
+
where $g$ is the group number and $e$ is the element number.
|
|
144
|
+
|
|
145
|
+
A linear fit is performed between the measured pixel spacing and the known physical
|
|
146
|
+
spacing. The gradient of this fit is used to determine the calibration factor
|
|
147
|
+
(micrometres per pixel), and the associated uncertainty is estimated from the
|
|
148
|
+
covariance of the fit.
|
|
149
|
+
|
|
150
|
+
The field of view is then calculated from the image dimensions and the calibrated
|
|
151
|
+
pixel size.
|
|
152
|
+
|
|
153
|
+
Further implementation details and source code are available at:
|
|
154
|
+
#link("https://gitlab.com/openflexure/micat")[https://gitlab.com/openflexure/micat]
|