Merge branch 'master' into imjoy-support

This commit is contained in:
Richard 2021-05-29 12:10:01 +01:00
commit dac1c9a748
21 changed files with 994 additions and 2122 deletions

View file

@ -1,3 +1,36 @@
# [v2.10.0b0](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.9.3...v2.10.0b0) (2021-05-18)
## New features
* Improved auto gain, white balance, and lens shading table correction (!119). Fixes #191.
The old auto calibration code has been replaced by a slower, more manual method that is more reliable.
Individual parts of the calibration can be run independently (auto gain/exposure, auto white balance, lens shading).
This should also improve the experience when using e.g. fluorescence imaging modes.
## Developer changes
* Build instructions were updated (!115)
* We've switched from `poetry` to `pipenv` to solve dependency management issues on the Raspberry Pi (!124). See the updated
repository README for a full breakdown of what configuration information goes where. Closes #218.
* Improved handling of stream settings and origin override (!125, !116, !117)
This fixes a longstanding irritation when developing on localhost using `npm run serve`, where it was necessary to
enter the API origin address every time the page was refreshed, and then manually re-enable the web stream. Both
these issues are now fixed.
* The API origin override field in the "dev tools" pane now remembers its value
* The API origin can be overridden using the URL
* Stream settings are now remembered in local storage
## Minor bug fixes and improvements
* Better error handling for picamerax (!86)
* Add coverage report to mypy (!110)
* `MissingStage` can now be explicitly selected in microscope configuration (!112)
* The first-run "tour" placement is improved and shouldn't block the interface any more (!114)
* Fix placement of tour messages for icons ([afbb716](https://gitlab.com/openflexure/openflexure-microscope-server/commit/afbb716))
* This closes #199 and helps with #193
* The IHI interface now explicitly states scan style should be raster (!113)
* Absolute moves are now fixed (!126), closing #220, #221, and #222.
* Fixed a typing error with the camera stage mapping matrix and `numpy` 1.20 (!127).
* Scans now have a minimum dimension of 1 in each axis, which avoids dividing by zero (!123)
# [v2.9.3](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.9.2...v2.9.3) (2020-12-14)
- Fix #194 ([0592d31](https://gitlab.com/openflexure/openflexure-microscope-server/commit/0592d31)), closes [#194](https://gitlab.com/openflexure/openflexure-microscope-server/issues/194)

645
Pipfile.lock generated
View file

@ -26,11 +26,11 @@
"yaml"
],
"hashes": [
"sha256:5ec0fe72f1422a1198973fcbb48d0eb5c7390f4b0fbe55474fce999ad6826a9b",
"sha256:83e4d30edce987f4791f950c847d2b8cbe91e55af3d1ac7e3a35011a5c6fdbdd"
"sha256:aaded4ae451752c58b70e579923d8db7daf0b71f62def8c8fe7a2a52bd7c06a2",
"sha256:f49c49e0dee3fa545462d4536856458aae61b972727b3c32eabc7d6db8c074f8"
],
"markers": "python_version >= '3.6'",
"version": "==4.3.0"
"version": "==4.4.1"
},
"apispec-webframeworks": {
"hashes": [
@ -40,21 +40,13 @@
"markers": "python_version >= '3.6'",
"version": "==0.5.2"
},
"atomicwrites": {
"hashes": [
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
"sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
],
"markers": "sys_platform == 'win32'",
"version": "==1.4.0"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"camera-stage-mapping": {
"hashes": [
@ -73,14 +65,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.4"
},
"colorzero": {
"hashes": [
"sha256:0e60d743a6b8071498a56465f7719c96a5e92928f858bab1be2a0d606c9aa0f8",
@ -155,11 +139,11 @@
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
"sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196",
"sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.1.2"
"version": "==1.1.4"
},
"flask-cors": {
"hashes": [
@ -185,11 +169,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a",
"sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe"
"sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581",
"sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"
],
"markers": "python_version < '3.8'",
"version": "==3.10.0"
"version": "==4.0.1"
},
"iniconfig": {
"hashes": [
@ -225,103 +209,83 @@
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:19536834abffb3fa155017053c607cb835b2ecc6a3a2554a88043d991dffb736",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:3d61f15e39611aacd91b7e71d903787da86d9e80896e683c0103fced9add7834",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:7952deddf24b85c88dab48f6ec366ac6e39d2761b5280f2f9594911e03fcd064",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:060b790af48b3d1cb6776a1e8956fb41106c6befaca717181c41a1728147dc73",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"marshmallow": {
"hashes": [
"sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd",
"sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b"
"sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040",
"sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"
],
"markers": "python_version >= '3.5'",
"version": "==3.11.1"
"version": "==3.12.1"
},
"numpy": {
"hashes": [
"sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
"sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
"sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
"sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
"sha256:4b5a485d5aeed9b403d475dd3db750ae52f085f56f15c19a56b00ddf99597de5",
"sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
"sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
"sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
"sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
"sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
"sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
"sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
"sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
"sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
"sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
"sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
"sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
"sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
"sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
"sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
"sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
"sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
"sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
"sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
"sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
"sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010",
"sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd",
"sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43",
"sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9",
"sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df",
"sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400",
"sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2",
"sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4",
"sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a",
"sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6",
"sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8",
"sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b",
"sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8",
"sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb",
"sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2",
"sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f",
"sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4",
"sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a",
"sha256:d840d80623c96696fab0d15489f49425bec1c0f47f3bf0b3362ce0c75dbad993",
"sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16",
"sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f",
"sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69",
"sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65",
"sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17",
"sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48"
],
"markers": "python_version >= '3.7'",
"version": "==1.20.2"
"version": "==1.20.3"
},
"opencv-python-headless": {
"hashes": [
@ -470,11 +434,11 @@
},
"pytest": {
"hashes": [
"sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
"sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
],
"markers": "python_version >= '3.6'",
"version": "==6.2.2"
"version": "==6.2.4"
},
"pytest-cov": {
"hashes": [
@ -510,6 +474,7 @@
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:8fc87fd1a1d30991d9198a4317e90dfaf3d2963c013608a019e8311568fa08b2",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
@ -526,6 +491,16 @@
],
"version": "==5.4.1"
},
"rpi.gpio": {
"hashes": [
"sha256:5cd2f3c88d0b1e2f4780c905702c0474768c2bc113fb793121c2e117b401ab16",
"sha256:6a4791f41cafc2ee6e4cb70e5bd31fadc66a0cfab29b38df8723a98f6f73ad5a",
"sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f",
"sha256:7da5235aeba20da39ad4fb97f3ba3a2c6b2d60bba8958b00a4df42f608c6d42e"
],
"markers": "platform_machine == 'armv7l'",
"version": "==0.7.0"
},
"sangaboard": {
"hashes": [
"sha256:3791159d57a749571f89acdce70aa9c1de82afef71f5186d5e2845c7be44698c",
@ -535,37 +510,37 @@
},
"scipy": {
"hashes": [
"sha256:03f1fd3574d544456325dae502facdf5c9f81cbfe12808a5e67a737613b7ba8c",
"sha256:0c81ea1a95b4c9e0a8424cf9484b7b8fa7ef57169d7bcc0dfcfc23e3d7c81a12",
"sha256:1fba8a214c89b995e3721670e66f7053da82e7e5d0fe6b31d8e4b19922a9315e",
"sha256:37f4c2fb904c0ba54163e03993ce3544c9c5cde104bcf90614f17d85bdfbb431",
"sha256:50e5bcd9d45262725e652611bb104ac0919fd25ecb78c22f5282afabd0b2e189",
"sha256:6ca1058cb5bd45388041a7c3c11c4b2bd58867ac9db71db912501df77be2c4a4",
"sha256:77f7a057724545b7e097bfdca5c6006bed8580768cd6621bb1330aedf49afba5",
"sha256:816951e73d253a41fa2fd5f956f8e8d9ac94148a9a2039e7db56994520582bf2",
"sha256:96620240b393d155097618bcd6935d7578e85959e55e3105490bbbf2f594c7ad",
"sha256:993c86513272bc84c451349b10ee4376652ab21f312b0554fdee831d593b6c02",
"sha256:a9bc820c3df735fdd29f9a405cfc5347e1d53f0d420f8010b13502b6a33c0b57",
"sha256:adf7cee8e5c92b05f2252af498f77c7214a2296d009fc5478fc432c2f8fb953b",
"sha256:bc52d4d70863141bb7e2f8fd4d98e41d77375606cde50af65f1243ce2d7853e8",
"sha256:c1d3f771c19af00e1a36f749bd0a0690cc64632783383bc68f77587358feb5a4",
"sha256:d744657c27c128e357de2f0fd532c09c84cd6e4933e8232895a872e67059ac37",
"sha256:e3e9742bad925c421d39e699daa8d396c57535582cba90017d17f926b61c1552",
"sha256:e547f84cd52343ac2d56df0ab08d3e9cc202338e7d09fafe286d6c069ddacb31",
"sha256:e89091e6a8e211269e23f049473b2fde0c0e5ae0dd5bd276c3fc91b97da83480",
"sha256:e9da33e21c9bc1b92c20b5328adb13e5f193b924c9b969cd700c8908f315aa59",
"sha256:ffdfb09315896c6e9ac739bb6e13a19255b698c24e6b28314426fd40a1180822"
"sha256:01b38dec7e9f897d4db04f8de4e20f0f5be3feac98468188a0f47a991b796055",
"sha256:10dbcc7de03b8d635a1031cb18fd3eaa997969b64fdf78f99f19ac163a825445",
"sha256:19aeac1ad3e57338723f4657ac8520f41714804568f2e30bd547d684d72c392e",
"sha256:1b21c6e0dc97b1762590b70dee0daddb291271be0580384d39f02c480b78290a",
"sha256:1caade0ede6967cc675e235c41451f9fb89ae34319ddf4740194094ab736b88d",
"sha256:208fe23655561a95ede6bb3bb15342882f6a8f3867bf5564cafbdbcc401b1a5d",
"sha256:23995dfcf269ec3735e5a8c80cfceaf384369a47699df111a6246b83a55da582",
"sha256:2a799714bf1f791fb2650d73222b248d18d53fd40d6af2df2c898db048189606",
"sha256:3274ce145b5dc416c49c0cf8b6119f787f0965cd35e22058fe1932c09fe15d77",
"sha256:33d1677d46111cfa1c84b87472a0274dde9ef4a7ef2e1f155f012f5f1e995d8f",
"sha256:44d452850f77e65e25b1eb1ac01e25770323a782bfe3a1a3e43847ad4266d93d",
"sha256:9e3302149a369697c6aaea18b430b216e3c88f9a61b62869f6104881e5f9ef85",
"sha256:a75b014d3294fce26852a9d04ea27b5671d86736beb34acdfc05859246260707",
"sha256:ad7269254de06743fb4768f658753de47d8b54e4672c5ebe8612a007a088bd48",
"sha256:b30280fbc1fd8082ac822994a98632111810311a9ece71a0e48f739df3c555a2",
"sha256:b79104878003487e2b4639a20b9092b02e1bad07fc4cf924b495cf413748a777",
"sha256:d449d40e830366b4c612692ad19fbebb722b6b847f78a7b701b1e0d6cda3cc13",
"sha256:d647757373985207af3343301d89fe738d5a294435a4f2aafb04c13b4388c896",
"sha256:f68eb46b86b2c246af99fcaa6f6e37c7a7a413e1084a794990b877f2ff71f7b6",
"sha256:fdf606341cd798530b05705c87779606fcdfaf768a8129c348ea94441da15b04"
],
"markers": "python_version < '3.10' and python_version >= '3.7'",
"version": "==1.6.2"
"version": "==1.6.3"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
"version": "==1.16.0"
},
"toml": {
"hashes": [
@ -629,11 +604,11 @@
"yaml"
],
"hashes": [
"sha256:5ec0fe72f1422a1198973fcbb48d0eb5c7390f4b0fbe55474fce999ad6826a9b",
"sha256:83e4d30edce987f4791f950c847d2b8cbe91e55af3d1ac7e3a35011a5c6fdbdd"
"sha256:aaded4ae451752c58b70e579923d8db7daf0b71f62def8c8fe7a2a52bd7c06a2",
"sha256:f49c49e0dee3fa545462d4536856458aae61b972727b3c32eabc7d6db8c074f8"
],
"markers": "python_version >= '3.6'",
"version": "==4.3.0"
"version": "==4.4.1"
},
"apispec-webframeworks": {
"hashes": [
@ -652,35 +627,27 @@
},
"astroid": {
"hashes": [
"sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9",
"sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df"
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
],
"markers": "python_version >= '3.6'",
"version": "==2.5.2"
},
"atomicwrites": {
"hashes": [
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
"sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
],
"markers": "sys_platform == 'win32'",
"version": "==1.4.0"
"markers": "python_version ~= '3.6'",
"version": "==2.5.6"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"babel": {
"hashes": [
"sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5",
"sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"
"sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
"sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9.0"
"version": "==2.9.1"
},
"black": {
"hashes": [
@ -722,14 +689,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"colorama": {
"hashes": [
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.4"
},
"colorzero": {
"hashes": [
"sha256:0e60d743a6b8071498a56465f7719c96a5e92928f858bab1be2a0d606c9aa0f8",
@ -797,11 +756,11 @@
},
"docutils": {
"hashes": [
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
"sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
"sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.16"
"version": "==0.17.1"
},
"expiringdict": {
"hashes": [
@ -812,11 +771,11 @@
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
"sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196",
"sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.1.2"
"version": "==1.1.4"
},
"flask-cors": {
"hashes": [
@ -867,11 +826,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a",
"sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe"
"sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581",
"sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"
],
"markers": "python_version < '3.8'",
"version": "==3.10.0"
"version": "==4.0.1"
},
"iniconfig": {
"hashes": [
@ -886,7 +845,7 @@
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"markers": "python_version >= '3.6' and python_version < '4'",
"version": "==5.8.0"
},
"itsdangerous": {
@ -948,18 +907,24 @@
"sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
"sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
"sha256:1822d16e5ac9061749955d099ff50c6cbc068f8cbfde28dfbeea845f7fbe6e09",
"sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae",
"sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
"sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
"sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
"sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
"sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59",
"sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
"sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
"sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96",
"sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
"sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
"sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
"sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354",
"sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
"sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
"sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16",
"sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
"sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a",
"sha256:74f69060f98a70e3654de3021cd8162859e0f30b9e69aed76ef2b7a2e3b84212",
"sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
"sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
@ -973,10 +938,14 @@
"sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
"sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
"sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
"sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617",
"sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
"sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92",
"sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
"sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
"sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24",
"sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
"sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e",
"sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
"sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
"sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
@ -988,72 +957,52 @@
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:19536834abffb3fa155017053c607cb835b2ecc6a3a2554a88043d991dffb736",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f",
"sha256:3d61f15e39611aacd91b7e71d903787da86d9e80896e683c0103fced9add7834",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:7952deddf24b85c88dab48f6ec366ac6e39d2761b5280f2f9594911e03fcd064",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:060b790af48b3d1cb6776a1e8956fb41106c6befaca717181c41a1728147dc73",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"marshmallow": {
"hashes": [
"sha256:0dd42891a5ef288217ed6410917f3c6048f585f8692075a0052c24f9bfff9dfd",
"sha256:16e99cb7f630c0ef4d7d364ed0109ac194268dde123966076ab3dafb9ae3906b"
"sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040",
"sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"
],
"markers": "python_version >= '3.5'",
"version": "==3.11.1"
"version": "==3.12.1"
},
"mccabe": {
"hashes": [
@ -1099,34 +1048,34 @@
},
"numpy": {
"hashes": [
"sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
"sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
"sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
"sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
"sha256:4b5a485d5aeed9b403d475dd3db750ae52f085f56f15c19a56b00ddf99597de5",
"sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
"sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
"sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
"sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
"sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
"sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
"sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
"sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
"sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
"sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
"sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
"sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
"sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
"sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
"sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
"sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
"sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
"sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
"sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
"sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
"sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010",
"sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd",
"sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43",
"sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9",
"sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df",
"sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400",
"sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2",
"sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4",
"sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a",
"sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6",
"sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8",
"sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b",
"sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8",
"sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb",
"sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2",
"sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f",
"sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4",
"sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a",
"sha256:d840d80623c96696fab0d15489f49425bec1c0f47f3bf0b3362ce0c75dbad993",
"sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16",
"sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f",
"sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69",
"sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65",
"sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17",
"sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48"
],
"markers": "python_version >= '3.7'",
"version": "==1.20.2"
"version": "==1.20.3"
},
"opencv-python-headless": {
"hashes": [
@ -1244,7 +1193,7 @@
"sha256:6fb3021603d4421c6fcc40072bbcf150a6c52ef70ff4d3be089b8b04e015ef5a",
"sha256:70b97cb194b978dc464c70793e85e6f746cddf82b84a38bfb135946ad71ae19c"
],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"markers": "python_version >= '3.6' and python_version < '4'",
"version": "==0.10.0"
},
"psutil": {
@ -1276,19 +1225,19 @@
},
"pygments": {
"hashes": [
"sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94",
"sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
],
"markers": "python_version >= '3.5'",
"version": "==2.8.1"
"version": "==2.9.0"
},
"pylint": {
"hashes": [
"sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a",
"sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"
"sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217",
"sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"
],
"markers": "python_version ~= '3.6'",
"version": "==2.7.4"
"version": "==2.8.2"
},
"pyparsing": {
"hashes": [
@ -1307,11 +1256,11 @@
},
"pytest": {
"hashes": [
"sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
"sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
],
"markers": "python_version >= '3.6'",
"version": "==6.2.2"
"version": "==6.2.4"
},
"pytest-cov": {
"hashes": [
@ -1354,6 +1303,7 @@
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:8fc87fd1a1d30991d9198a4317e90dfaf3d2963c013608a019e8311568fa08b2",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
@ -1386,6 +1336,16 @@
],
"version": "==0.14.0"
},
"rpi.gpio": {
"hashes": [
"sha256:5cd2f3c88d0b1e2f4780c905702c0474768c2bc113fb793121c2e117b401ab16",
"sha256:6a4791f41cafc2ee6e4cb70e5bd31fadc66a0cfab29b38df8723a98f6f73ad5a",
"sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f",
"sha256:7da5235aeba20da39ad4fb97f3ba3a2c6b2d60bba8958b00a4df42f608c6d42e"
],
"markers": "platform_machine == 'armv7l'",
"version": "==0.7.0"
},
"sangaboard": {
"hashes": [
"sha256:3791159d57a749571f89acdce70aa9c1de82afef71f5186d5e2845c7be44698c",
@ -1395,37 +1355,37 @@
},
"scipy": {
"hashes": [
"sha256:03f1fd3574d544456325dae502facdf5c9f81cbfe12808a5e67a737613b7ba8c",
"sha256:0c81ea1a95b4c9e0a8424cf9484b7b8fa7ef57169d7bcc0dfcfc23e3d7c81a12",
"sha256:1fba8a214c89b995e3721670e66f7053da82e7e5d0fe6b31d8e4b19922a9315e",
"sha256:37f4c2fb904c0ba54163e03993ce3544c9c5cde104bcf90614f17d85bdfbb431",
"sha256:50e5bcd9d45262725e652611bb104ac0919fd25ecb78c22f5282afabd0b2e189",
"sha256:6ca1058cb5bd45388041a7c3c11c4b2bd58867ac9db71db912501df77be2c4a4",
"sha256:77f7a057724545b7e097bfdca5c6006bed8580768cd6621bb1330aedf49afba5",
"sha256:816951e73d253a41fa2fd5f956f8e8d9ac94148a9a2039e7db56994520582bf2",
"sha256:96620240b393d155097618bcd6935d7578e85959e55e3105490bbbf2f594c7ad",
"sha256:993c86513272bc84c451349b10ee4376652ab21f312b0554fdee831d593b6c02",
"sha256:a9bc820c3df735fdd29f9a405cfc5347e1d53f0d420f8010b13502b6a33c0b57",
"sha256:adf7cee8e5c92b05f2252af498f77c7214a2296d009fc5478fc432c2f8fb953b",
"sha256:bc52d4d70863141bb7e2f8fd4d98e41d77375606cde50af65f1243ce2d7853e8",
"sha256:c1d3f771c19af00e1a36f749bd0a0690cc64632783383bc68f77587358feb5a4",
"sha256:d744657c27c128e357de2f0fd532c09c84cd6e4933e8232895a872e67059ac37",
"sha256:e3e9742bad925c421d39e699daa8d396c57535582cba90017d17f926b61c1552",
"sha256:e547f84cd52343ac2d56df0ab08d3e9cc202338e7d09fafe286d6c069ddacb31",
"sha256:e89091e6a8e211269e23f049473b2fde0c0e5ae0dd5bd276c3fc91b97da83480",
"sha256:e9da33e21c9bc1b92c20b5328adb13e5f193b924c9b969cd700c8908f315aa59",
"sha256:ffdfb09315896c6e9ac739bb6e13a19255b698c24e6b28314426fd40a1180822"
"sha256:01b38dec7e9f897d4db04f8de4e20f0f5be3feac98468188a0f47a991b796055",
"sha256:10dbcc7de03b8d635a1031cb18fd3eaa997969b64fdf78f99f19ac163a825445",
"sha256:19aeac1ad3e57338723f4657ac8520f41714804568f2e30bd547d684d72c392e",
"sha256:1b21c6e0dc97b1762590b70dee0daddb291271be0580384d39f02c480b78290a",
"sha256:1caade0ede6967cc675e235c41451f9fb89ae34319ddf4740194094ab736b88d",
"sha256:208fe23655561a95ede6bb3bb15342882f6a8f3867bf5564cafbdbcc401b1a5d",
"sha256:23995dfcf269ec3735e5a8c80cfceaf384369a47699df111a6246b83a55da582",
"sha256:2a799714bf1f791fb2650d73222b248d18d53fd40d6af2df2c898db048189606",
"sha256:3274ce145b5dc416c49c0cf8b6119f787f0965cd35e22058fe1932c09fe15d77",
"sha256:33d1677d46111cfa1c84b87472a0274dde9ef4a7ef2e1f155f012f5f1e995d8f",
"sha256:44d452850f77e65e25b1eb1ac01e25770323a782bfe3a1a3e43847ad4266d93d",
"sha256:9e3302149a369697c6aaea18b430b216e3c88f9a61b62869f6104881e5f9ef85",
"sha256:a75b014d3294fce26852a9d04ea27b5671d86736beb34acdfc05859246260707",
"sha256:ad7269254de06743fb4768f658753de47d8b54e4672c5ebe8612a007a088bd48",
"sha256:b30280fbc1fd8082ac822994a98632111810311a9ece71a0e48f739df3c555a2",
"sha256:b79104878003487e2b4639a20b9092b02e1bad07fc4cf924b495cf413748a777",
"sha256:d449d40e830366b4c612692ad19fbebb722b6b847f78a7b701b1e0d6cda3cc13",
"sha256:d647757373985207af3343301d89fe738d5a294435a4f2aafb04c13b4388c896",
"sha256:f68eb46b86b2c246af99fcaa6f6e37c7a7a413e1084a794990b877f2ff71f7b6",
"sha256:fdf606341cd798530b05705c87779606fcdfaf768a8129c348ea94441da15b04"
],
"markers": "python_version < '3.10' and python_version >= '3.7'",
"version": "==1.6.2"
"version": "==1.6.3"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
"version": "==1.16.0"
},
"snowballstemmer": {
"hashes": [
@ -1436,11 +1396,11 @@
},
"sphinx": {
"hashes": [
"sha256:3f01732296465648da43dec8fb40dc451ba79eb3e2cc5c6d79005fd98197107d",
"sha256:ce9c228456131bab09a3d7d10ae58474de562a6f79abb3dc811ae401cf8c1abc"
"sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c",
"sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"
],
"markers": "python_version >= '3.5'",
"version": "==3.5.3"
"markers": "python_version >= '3.6'",
"version": "==4.0.2"
},
"sphinxcontrib-applehelp": {
"hashes": [
@ -1507,49 +1467,48 @@
},
"tomlkit": {
"hashes": [
"sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831",
"sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"
"sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117",
"sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.7.0"
"version": "==0.7.2"
},
"typed-ast": {
"hashes": [
"sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1",
"sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d",
"sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6",
"sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd",
"sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37",
"sha256:4822449fb865605582a22d23997444ff3eaa3d0920b04b9902e37f8edbeadca6",
"sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151",
"sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07",
"sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440",
"sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70",
"sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496",
"sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea",
"sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400",
"sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc",
"sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606",
"sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc",
"sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581",
"sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412",
"sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a",
"sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2",
"sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787",
"sha256:a93908d202f52ec64e24a41031f1336a14b66706be2c9e51949647f7ac4cbd22",
"sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f",
"sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937",
"sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64",
"sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487",
"sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b",
"sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41",
"sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a",
"sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3",
"sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166",
"sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"
"sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace",
"sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff",
"sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266",
"sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528",
"sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6",
"sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808",
"sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4",
"sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363",
"sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341",
"sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04",
"sha256:6cd1c6665796c58380121c2dd3e6abef605a3ae1116b95cfc585370ebfefdd5c",
"sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41",
"sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e",
"sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3",
"sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899",
"sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805",
"sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c",
"sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c",
"sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39",
"sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a",
"sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3",
"sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7",
"sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f",
"sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075",
"sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0",
"sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40",
"sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428",
"sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927",
"sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3",
"sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f",
"sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"
],
"markers": "python_version < '3.8' and implementation_name == 'cpython'",
"version": "==1.4.2"
"version": "==1.4.3"
},
"typing-extensions": {
"hashes": [
@ -1565,7 +1524,7 @@
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.4"
},
"webargs": {

View file

@ -23,7 +23,7 @@ import os
from datetime import datetime
import pkg_resources
from flask import abort, send_file
from flask import abort, send_file, jsonify
from flask_cors import CORS, cross_origin
from labthings import create_app
from labthings.extensions import find_extensions
@ -105,6 +105,20 @@ app, labthing = create_app(
# Enable CORS for some routes outside of LabThings
cors: CORS = CORS(app)
# Enable correct handling of Marshmallow/Webargs validation errors
# Return validation errors as JSON
# (see https://webargs.readthedocs.io/en/latest/framework_support.html)
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
headers = err.data.get("headers", None)
messages = err.data.get("messages", ["Invalid request."])
if headers:
return jsonify({"errors": messages}), err.code, headers
else:
return jsonify({"errors": messages}), err.code
# Use custom JSON encoder
labthing.json_encoder = JSONEncoder
app.json_encoder = JSONEncoder

View file

@ -83,6 +83,15 @@ class LoggingMoveWrapper:
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
class CSMUncalibratedError(RuntimeError):
"""A calibrated camera stage mapper is required, but this one is not calibrated.
The camera stage mapper requires calibration information to relate image pixels
to stage coordinates. If a method attempts to retrieve this calibration before
it exists, we raise this exception.
"""
class CSMExtension(BaseExtension):
"""
Use the camera as an encoder, so we can relate camera and stage coordinates
@ -133,7 +142,12 @@ class CSMExtension(BaseExtension):
def get_settings(self) -> Dict[str, Any]:
"""Retrieve the settings for this extension"""
keys: List[str] = ["extensions", self.name]
return get_by_path(self.microscope.read_settings(), keys)
try:
return get_by_path(self.microscope.read_settings(), keys)
except KeyError as exc:
raise CSMUncalibratedError(
"Camera stage mapping calibration data is missing"
) from exc
def camera_stage_functions(self) -> Tuple[Callable, Callable, Callable, Callable]:
"""Return functions that allow us to interface with the microscope"""
@ -194,10 +208,13 @@ class CSMExtension(BaseExtension):
@property
def image_to_stage_displacement_matrix(self) -> np.ndarray: # 2x2 integer array
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates."""
displacement_matrix = self.get_settings().get("image_to_stage_displacement")
if not displacement_matrix:
raise ValueError("The microscope has not yet been calibrated.")
return np.array(displacement_matrix)
settings = self.get_settings()
try:
return np.array(settings["image_to_stage_displacement"])
except KeyError as exc:
raise CSMUncalibratedError(
"The microscope has not yet been calibrated."
) from exc
def move_in_image_coordinates(self, displacement_in_pixels: XYCoordinateType):
"""Move by a given number of pixels on the camera"""

View file

@ -1,19 +1,23 @@
import logging
from contextlib import contextmanager
from typing import Iterator, Tuple
import picamerax
import labthings.fields as fields
from flask import abort
from labthings import find_component
from labthings.extensions import BaseExtension
from labthings.views import ActionView
from picamerax import PiCamera
from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.microscope import Microscope
from .recalibrate_utils import (
auto_expose_and_freeze_settings,
adjust_shutter_and_gain_from_raw,
adjust_white_balance_from_raw,
flat_lens_shading_table,
recalibrate_camera,
get_channel_percentiles,
lst_from_camera,
)
@ -61,64 +65,178 @@ class LSTExtension(BaseExtension):
"/delete_lens_shading_table",
endpoint="delete_lens_shading_table",
)
self.add_view(
GetRawChannelPercentilesView,
"/get_raw_channel_percentiles",
endpoint="get_raw_channel_percentiles",
)
self.add_view(
AutoExposureFromRawView,
"/auto_exposure_from_raw",
endpoint="auto_exposure_from_raw",
)
self.add_view(
AutoWhiteBalanceFromRawView,
"/auto_white_balance_from_raw",
endpoint="auto_white_balance_from_raw",
)
self.add_view(
AutoLensShadingTableView,
"/auto_lens_shading_table",
endpoint="auto_lens_shading_table",
)
def recalibrate(self, microscope: Microscope):
@contextmanager
def find_picamera() -> Iterator[Tuple[PiCamera, BaseCamera, Microscope]]:
"""Locate the microscope and raise a sensible error if it's missing."""
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(
503, "No microscope connected. Unable to use camera calibration functions."
)
scamera = microscope.camera
if not hasattr(scamera, "picamera"):
abort(503, "The PiCamera calibration plugin requires a Raspberry Pi camera.")
with scamera.lock:
yield scamera.picamera, scamera, microscope
class RecalibrateView(ActionView):
def post(self):
"""Reset the camera's settings.
This generates new gains, exposure time, and lens shading
table such that the background is as uniform as possible
with a gray level of 230. It takes a little while to run.
This consists of three steps:
* Reset gain and exposure time, then increase them until
we have a suitably bright image
* Set the auto white balance based on a raw image
* Set the lens shading table based on a raw image
Each of these steps has its own endpoint if you want to
perform them separately.
"""
with pause_stream(microscope.camera) as scamera:
if hasattr(scamera, "picamera"):
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera")
auto_expose_and_freeze_settings(picamera_obj)
recalibrate_camera(picamera_obj)
microscope.save_settings()
else:
raise RuntimeError(
"Recalibrate can only be used with a Raspberry Pi camera"
)
with find_picamera() as (picamera, scamera, microscope):
logging.info("Starting microscope recalibration...")
adjust_shutter_and_gain_from_raw(picamera)
adjust_white_balance_from_raw(picamera)
lst = lst_from_camera(picamera)
with pause_stream(scamera):
picamera.lens_shading_table = lst
microscope.save_settings()
class RecalibrateView(ActionView):
class AutoLensShadingTableView(ActionView):
def post(self):
microscope = find_component("org.openflexure.microscope")
"""Perform flat-field correction
if not microscope:
abort(503, "No microscope connected. Unable to recalibrate.")
logging.info("Starting microscope recalibration...")
return self.extension.recalibrate(microscope)
This routine acquires a new image (which should be an
empty field of view, i.e. a perfect microscope would
record a uniform white image), and then uses it to set
the "lens shading table" such that future images will
be corrected for vignetting.
"""
with find_picamera() as (picamera, scamera, microscope):
logging.info("Generating lens shading table")
lst = lst_from_camera(picamera)
with pause_stream(scamera):
picamera.lens_shading_table = lst
microscope.save_settings()
class FlattenLSTView(ActionView):
def post(self):
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(
503,
"No microscope connected. Unable to flatten the lens shading table.",
)
with pause_stream(microscope.camera) as scamera:
flat_lst = flat_lens_shading_table(scamera.camera)
scamera.camera.lens_shading_table = flat_lst
microscope.save_settings()
with find_picamera() as (picamera, scamera, microscope):
with pause_stream(scamera):
flat_lst = flat_lens_shading_table(picamera)
picamera.lens_shading_table = flat_lst
microscope.save_settings()
class DeleteLSTView(ActionView):
def post(self):
microscope = find_component("org.openflexure.microscope")
with find_picamera() as (picamera, scamera, microscope):
with pause_stream(scamera):
picamera.lens_shading_table = None
microscope.save_settings()
if not microscope:
abort(
503,
"No microscope connected. Unable to flatten the lens shading table.",
)
with pause_stream(microscope.camera) as scamera:
scamera.camera.lens_shading_table = None
microscope.save_settings()
class AutoExposureFromRawView(ActionView):
args = {
"target_white_level": fields.Int(
missing=700,
example=700,
description=(
"The pixel value (10-bit format) that we aim for when adjusting shutter/gain."
),
),
"max_iterations": fields.Int(
missing=20,
description=(
"The number of adjustments to the camera's settings to make before giving up."
),
),
"tolerance": fields.Float(
missing=0.05,
example=0.05,
description=(
"We stop adjusting when we get within this fraction of the target "
"value. It is a number between 0 and 1, usually 0.01--0.1."
),
),
"percentile": fields.Float(
missing=99.9,
example=99.9,
description=(
"A float between 0 and 100 setting the centile to use "
"to measure the white point of the image. A value "
"of 99.9 allows 0.1% of the pixels to be erroneously "
"bright - this helps stability in low light."
),
),
}
def post(self, args):
with find_picamera() as (picamera, _, _):
adjust_shutter_and_gain_from_raw(picamera, **args)
class AutoWhiteBalanceFromRawView(ActionView):
args = {
"percentile": fields.Float(
missing=99.9,
example=99.9,
description=(
"A float between 0 and 100 setting the centile to use "
"to measure the white point of the image. A value "
"of 99.9 allows 0.1% of the pixels to be erroneously "
"bright - this helps stability in low light."
),
)
}
def post(self, args):
with find_picamera() as (picamera, _, _):
adjust_white_balance_from_raw(picamera, **args)
class GetRawChannelPercentilesView(ActionView):
args = {
"percentile": fields.Float(
example=99.9,
description="A float between 0 and 100 setting the centile to calculate",
)
}
schema = fields.List(fields.Integer)
def post(self, args):
with find_picamera() as (picamera, _, _):
return get_channel_percentiles(picamera, args["percentile"])

View file

@ -1,7 +1,39 @@
"""
Functions to set up a Raspberry Pi Camera v2 for scientific use
This module provides slower, simpler functions to set the
gain, exposure, and white balance of a Raspberry Pi camera, using
`picamerax` (a fork of `picamera`) to get as-manual-as-possible
control over the camera. It's mostly used by the OpenFlexure
Microscope, though it deliberately has no hard dependencies on
said software, so that it's useful on its own.
There are three main calibration steps:
* Setting exposure time and gain to get a reasonably bright
image.
* Fixing the white balance to get a neutral image
* Taking a uniform white image and using it to calibrate
the Lens Shading Table
The most reliable way to do this, avoiding any issues relating
to "memory" or nonlinearities in the camera's image processing
pipeline, is to use raw images. This is quite slow, but very
reliable. The three steps above can be accomplished by:
```
picamera = picamerax.PiCamera()
adjust_shutter_and_gain_from_raw(picamera)
adjust_white_balance_from_raw(picamera)
lst = lst_from_camera(picamera)
picamera.lens_shading_table = lst
```
"""
import logging
import time
from fractions import Fraction
from typing import List, Optional, Tuple
from typing import List, NamedTuple, Optional, Tuple
import numpy as np
from picamerax import PiCamera
@ -33,48 +65,190 @@ def flat_lens_shading_table(camera: PiCamera) -> np.ndarray:
def adjust_exposure_to_setpoint(camera: PiCamera, setpoint: int):
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>.
NB this method uses RGB images (i.e. processed ones) not raw images.
"""
logging.info(f"Adjusting shutter speed to hit setpoint {setpoint}")
for _ in range(3):
print(".", end="")
camera.shutter_speed = int(
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
)
time.sleep(1)
print("done")
def auto_expose_and_freeze_settings(camera: PiCamera):
"""Freeze the settings after auto-exposing to white illumination"""
logging.info("Allowing the camera to auto-expose")
if "greyworld" in camera.AWB_MODES:
print("Calibrating with greyworld AWB")
camera.awb_mode = "greyworld"
else:
print("Calibrating with auto AWB")
camera.awb_mode = "auto"
camera.exposure_mode = "auto"
camera.iso = (
0 # This is important, if it's on a fixed ISO, gain might not set properly.
)
for _ in range(6):
print(".", end="")
time.sleep(0.5)
logging.info("done")
logging.info("Freezing the camera settings...")
camera.shutter_speed = camera.exposure_speed
logging.info("Shutter speed = %s", (camera.shutter_speed))
def set_minimum_exposure(camera: PiCamera):
"""Enable manual exposure, with low gain and shutter speed
We set exposure mode to manual, analog and digital gain
to 1, and shutter speed to the minimum (8us for Pi Camera v2)
NB ISO is left at auto, because this is needed for the gains
to be set correctly.
"""
camera.exposure_mode = "off"
logging.info("Auto exposure disabled")
g: Tuple[Fraction, Fraction] = camera.awb_gains
camera.awb_mode = "off"
camera.awb_gains = g
logging.info("Auto white balance disabled, gains are %s", (g))
camera.iso = 0 # We must set ISO=0 (auto) or we can't set gain
camera.analog_gain = 1
camera.digital_gain = 1
# Setting the shutter speed to 1us will result in it being set
# to the minimum possible, which is probably 8us for PiCamera v2
camera.shutter_speed = 1
time.sleep(0.5)
class ExposureTest(NamedTuple):
"""Record the results of testing the camera's current exposure settings"""
level: int
shutter_speed: int
analog_gain: float
def test_exposure_settings(camera: PiCamera, percentile: float) -> ExposureTest:
"""Evaluate current exposure settings using a raw image
We will acquire a raw image and calculate the given percentile
of the pixel values. We return a dictionary containing the
percentile (which will be compared to the target), as well as
the camera's shutter and gain values.
"""
max_brightness = np.max(get_channel_percentiles(camera, percentile))
# The reported brightness can, theoretically, be negative or zero
# because of black level compensation. The line below forces a
# minimum value of 1 which will keep things well-behaved!
if max_brightness < 1:
logging.warning(
f"Measured brightness of {max_brightness}. "
"This should normally be >= 1, and may indicate the "
"camera's black level compensation has gone wrong."
)
max_brightness = 1
shutter_speed = int(camera.shutter_speed)
analog_gain = float(camera.analog_gain)
logging.info(
"Analogue gain: %s, Digital gain: %s", camera.analog_gain, camera.digital_gain
f"Brightness: {max_brightness: >5.0f}, "
f"Gain: {analog_gain: >4.1f}, "
f"Shutter: {shutter_speed: >7.0f}"
)
adjust_exposure_to_setpoint(camera, 215)
return ExposureTest(max_brightness, shutter_speed, analog_gain)
def check_convergence(test: ExposureTest, target: int, tolerance: float):
"""Check whether the brightness is within the specified target range"""
converged = abs(test.level - target) < target * tolerance
return converged
def adjust_shutter_and_gain_from_raw(
camera: PiCamera,
target_white_level: int = 700,
max_iterations: int = 20,
tolerance: float = 0.05,
percentile: float = 99.9,
) -> float:
"""Adjust exposure and analog gain based on raw images.
This routine is slow but effective. It uses raw images, so we
are not affected by white balance or digital gain.
Arguments:
target_white_level:
The raw, 10-bit value we aim for. The brightest pixels
should be approximately this bright. Maximum possible
is about 900, 700 is reasonable.
max_iterations:
We will terminate once we perform this many iterations,
whether or not we converge. More than 10 shouldn't happen.
tolerance:
How close to the target value we consider "done". Expressed
as a fraction of the ``target_white_level`` so 0.05 means
+/- 5%
percentile:
Rather then use the maximum value for each channel, we
calculate a percentile. This makes us robust to single
pixels that are bright/noisy. 99.9% still picks the top
of the brightness range, but seems much more reliable
than just ``np.max()``.
"""
if target_white_level * (tolerance + 1) >= 959:
raise ValueError(
"The target level is too high - a saturated image would be "
"considered successful. target_white_level * (tolerance + 1) "
"must be less than 959."
)
set_minimum_exposure(camera)
# We start with very low exposure settings and work up
# until either the brightness is high enough, or we can't increase the
# shutter speed any more.
iterations = 0
while iterations < max_iterations:
test = test_exposure_settings(camera, percentile)
if check_convergence(test, target_white_level, tolerance):
break
iterations += 1
# Adjust shutter speed so that the brightness approximates the target
# NB we put a maximum of 32 on this, to stop it increasing too quickly.
camera.shutter_speed = int(
test.shutter_speed * min(target_white_level / test.level, 32)
)
time.sleep(0.5)
# Check whether the shutter speed is still going up - if not, we've hit a maximum
if camera.shutter_speed == test.shutter_speed:
logging.info("Shutter speed has maxed out.")
break
# Now, if we've not converged, increase gain until we converge or run out of options.
while iterations < max_iterations:
test = test_exposure_settings(camera, percentile)
if check_convergence(test, target_white_level, tolerance):
break
iterations += 1
# Adjust gain to make the white level hit the target, again with a maximum
camera.analog_gain *= min(target_white_level / test.level, 2)
time.sleep(0.5)
# Check the gain is still changing - if not, we have probably hit the maximum
if camera.analog_gain == test.analog_gain:
logging.info("Gain has maxed out.")
break
if check_convergence(test, target_white_level, tolerance):
logging.info(f"Brightness has converged to within {tolerance * 100 :.0f}%.")
else:
logging.warning(
f"Failed to reach target brightness of {target_white_level}."
f"Brightness reached {test.level} after {iterations} iterations."
)
return test.level
def adjust_white_balance_from_raw(
camera: PiCamera, percentile: float = 99
) -> Tuple[float, float]:
"""Adjust the white balance in a single shot, based on the raw image.
NB if ``channels_from_raw_image`` is broken, this will go haywire.
We should probably have better logic to verify the channels really
are BGGR...
"""
blue, g1, g2, red = get_channel_percentiles(camera, percentile)
green = (g1 + g2) / 2.0
new_awb_gains = (green / red, green / blue)
logging.info(
f"Raw white point is R: {red} G: {green} B: {blue}, "
f"setting AWB gains to ({new_awb_gains[0]:.2f}, "
f"{new_awb_gains[1]:.2f})."
)
camera.awb_mode = "off"
camera.awb_gains = new_awb_gains
return new_awb_gains
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
@ -95,6 +269,21 @@ def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
return channels
def get_channel_percentiles(camera: PiCamera, percentile: float) -> np.ndarray:
"""Calculate the brightness percentile of the pixels in each channel
This is a number between -64 and 959 for each channel, because the
camera takes 10-bit images (maximum=1023) and its zero level is set
at 64 for denoising purposes (there's black level compensation built
in, and to avoid skewing the noise, the black level is set as 64 to
leave some room for negative values.
"""
with PiBayerArray(camera) as output:
camera.capture(output, format="jpeg", bayer=True)
channels = channels_from_bayer_array(output.array)
return np.percentile(channels, percentile, axis=(1, 2)) - 64
def lst_from_channels(channels: np.ndarray) -> np.ndarray:
"""Given the 4 Bayer colour channels from a white image, generate a LST."""
full_resolution: np.ndarray = np.array(
@ -165,6 +354,20 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
return lens_shading_table[::-1, :, :].copy()
def lst_from_camera(camera: PiCamera) -> np.ndarray:
"""Acquire a raw image and use it to calculate a lens shading table."""
with PiBayerArray(camera) as a:
camera.capture(a, format="jpeg", bayer=True)
raw_image = a.array.copy()
# Now we need to calculate a lens shading table that would make this flat.
# raw_image is a 3D array, with full resolution and 3 colour channels. No
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
# channels, 1/2 for green because there's twice as many green pixels).
channels = channels_from_bayer_array(raw_image)
return lst_from_channels(channels)
def recalibrate_camera(camera: PiCamera):
"""Reset the lens shading table and exposure settings.
@ -178,16 +381,7 @@ def recalibrate_camera(camera: PiCamera):
camera.lens_shading_table = flat_lens_shading_table(camera)
_ = rgb_image(camera) # for some reason the camera won't work unless I do this!
with PiBayerArray(camera) as a:
camera.capture(a, format="jpeg", bayer=True)
raw_image = a.array.copy()
# Now we need to calculate a lens shading table that would make this flat.
# raw_image is a 3D array, with full resolution and 3 colour channels. No
# de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B
# channels, 1/2 for green because there's twice as many green pixels).
channels = channels_from_bayer_array(raw_image)
lens_shading_table = lst_from_channels(channels)
lens_shading_table = lst_from_camera(camera)
camera.lens_shading_table = lens_shading_table
_ = rgb_image(camera)

View file

@ -2,6 +2,7 @@ import datetime
import logging
import time
import uuid
import marshmallow
from functools import reduce
from typing import Dict, List, Optional, Tuple
@ -361,7 +362,11 @@ class ScanExtension(BaseExtension):
class TileScanArgs(FullCaptureArgs):
namemode = fields.String(missing="coordinates", example="coordinates")
grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3])
grid = fields.List(
fields.Integer(validate=marshmallow.validate.Range(min=1)),
missing=[3, 3, 3],
example=[3, 3, 3],
)
style = fields.String(missing="raster")
autofocus_dz = fields.Integer(missing=50)
fast_autofocus = fields.Boolean(missing=False)

View file

@ -3935,38 +3935,12 @@
"dev": true
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
"follow-redirects": "^1.10.0"
}
},
"babel-eslint": {

View file

@ -23,7 +23,7 @@
"@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"@vue/eslint-config-prettier": "^6.0.0",
"axios": "^0.19.2",
"axios": "^0.21.1",
"babel-eslint": "^10.1.0",
"css-loader": "^3.6.0",
"eslint": "^6.8.0",

View file

@ -194,6 +194,8 @@ import loggingContent from "./tabContentComponents/loggingContent.vue";
import calibrationModal from "./modalComponents/calibrationModal.vue";
import TabIcon from "./genericComponents/tabIcon.vue";
import { mapState } from "vuex";
// Export main app
export default {
name: "AppContent",
@ -302,21 +304,22 @@ export default {
return process.env.VUE_APP_ENABLE_IMJOY === "true";
},
...mapState("imjoy", { imjoyTabs: "tabs" })
// Map the tabs from ImJoy's store module so we can display them
...mapState("imjoy", { imjoyTabs: "tabs" }),
// Map the setting for IHI's interface so we can watch it
...mapState(["IHIEnabled"])
},
watch: {
// Update the interface when the IHI interface is enabled/disabled
IHIEnabled: function(newValue) {
this.updateTopTabs(newValue);
}
},
created: function() {
if (this.$store.getters.ready) {
// Detect local connection
if (
["localhost", "0.0.0.0", "127.0.0.1", "[::1]"].includes(
window.location.hostname
)
) {
this.$store.commit("changeDisableStream", true);
this.$store.commit("changeAutoGpuPreview", true);
this.$store.commit("changeTrackWindow", true);
}
// Update top tabs
this.updateTopTabs(this.$store.state.IHIEnabled);
// Update plugins
@ -325,16 +328,6 @@ export default {
this.startModals();
});
}
// Watch for host 'ready', then update status
this.unwatchStoreFunction = this.$store.watch(
state => {
return state.IHIEnabled;
},
IHIEnabled => {
this.updateTopTabs(IHIEnabled);
}
);
},
mounted() {
@ -352,14 +345,6 @@ export default {
});
},
beforeDestroy() {
// Then we call that function here to unwatch
if (this.unwatchStoreFunction) {
this.unwatchStoreFunction();
this.unwatchStoreFunction = null;
}
},
methods: {
updateTopTabs: function(IHIEnabled) {
if (IHIEnabled) {

View file

@ -1,12 +1,36 @@
<template>
<div>
<form class="uk-form-stacked" @submit.prevent="overrideAPIHost">
<form
class="uk-form-stacked"
action=""
method="GET"
@submit="overrideAPIHost"
>
<label class="uk-form-label">Override API origin</label>
<input v-model="newOrigin" class="uk-input" type="text" />
<input
v-model="newOrigin"
name="overrideOrigin"
class="uk-input"
type="text"
/>
<label class="uk-form-label">
<input
v-model="reloadWhenOverridingOrigin"
class="uk-input uk-checkbox"
type="checkbox"
/>
Reload web app with new origin
</label>
<button class="uk-button uk-button-default uk-margin-small">
Apply
</button>
</form>
<form class="uk-form-stacked" @submit.prevent="resetTour">
<label class="uk-form-label">Re-run tour on next load</label>
<button class="uk-button uk-button-default uk-margin-small">
Reset
</button>
</form>
</div>
</template>
@ -19,22 +43,35 @@ export default {
data: function() {
return {
newOrigin: this.$store.state.origin
newOrigin: this.$store.state.origin,
reloadWhenOverridingOrigin: true
};
},
mounted() {
if (localStorage.overrideOrigin){
if (localStorage.overrideOrigin) {
this.newOrigin = localStorage.overrideOrigin;
}else{
} else {
this.newOrigin = "http://microscope.local:5000";
}
},
methods: {
overrideAPIHost: function() {
this.$store.commit("changeOrigin", this.newOrigin);
localStorage.overrideOrigin = this.newOrigin
overrideAPIHost: function(event) {
// Save the origin override, so that if we reload the web app, you can easily
localStorage.overrideOrigin = this.newOrigin;
// If we have elected not to reload the interface, just update the origin
// in the store. Otherwise, the form's default action will do the job for us.
// TODO: preserve other query parameters when reloading
if (!this.reloadWhenOverridingOrigin) {
this.$store.commit("changeOrigin", this.newOrigin);
event.preventDefault();
}
},
resetTour: function() {
// Make the introduction tour run next time the app loads
this.setLocalStorageObj("completedTour", false);
}
}
};

View file

@ -155,7 +155,7 @@
v-model="scanStepSize.z"
class="uk-input uk-form-small"
type="number"
name="inputPositionZx"
name="inputPositionZ"
/>
</div>
</div>
@ -172,6 +172,7 @@
class="uk-input uk-form-small"
type="number"
name="inputPositionX"
min="1"
/>
</div>
</div>
@ -186,6 +187,7 @@
class="uk-input uk-form-small"
type="number"
name="inputPositionY"
min="1"
/>
</div>
</div>
@ -199,7 +201,8 @@
v-model="scanSteps.z"
class="uk-input uk-form-small"
type="number"
name="inputPositionZx"
name="inputPositionZ"
min="1"
/>
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
<div class="uk-width-large">
<h3>Manual camera settings</h3>
<form @submit.prevent="applySettingsRequest">
<form @submit.prevent="applySettingsRequest" >
<div class="uk-margin-small-bottom">
<ul uk-accordion="multiple: true">
<li class="uk-open">
@ -31,7 +31,7 @@
v-model="picamera.analog_gain"
class="uk-input uk-form-small"
type="number"
step="0.000001"
step="0.0000000000000001"
/>
</div>
</div>
@ -45,7 +45,29 @@
v-model="picamera.digital_gain"
class="uk-input uk-form-small"
type="number"
step="0.000001"
step="0.0000000000000001"
/>
</div>
</div>
<div v-if="picamera.awb_gains !== undefined">
<label class="uk-form-label" for="form-stacked-text" >
White Balance gains
</label>
<div class="uk-form-controls">
<label class="uk-form-label" >R:</label>
<input
v-model="picamera.awb_gains[0]"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
<label class="uk-form-label" >B:</label>
<input
v-model="picamera.awb_gains[1]"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
</div>
</div>
@ -194,7 +216,8 @@ export default {
shutter_speed: undefined,
analog_gain: undefined,
digital_gain: undefined,
framerate: undefined
framerate: undefined,
awb_gains: undefined
},
mjpeg_bitrate: undefined,
stream_resolution: undefined,
@ -244,6 +267,7 @@ export default {
this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
this.picamera.framerate = cameraSettings.picamera.framerate;
this.picamera.awb_gains = cameraSettings.picamera.awb_gains;
}
})
.catch(error => {
@ -263,7 +287,11 @@ export default {
shutter_speed: parseFloat(this.picamera.shutter_speed),
analog_gain: parseFloat(this.picamera.analog_gain),
digital_gain: parseFloat(this.picamera.digital_gain),
framerate: parseInt(this.picamera.framerate)
framerate: parseInt(this.picamera.framerate),
awb_gains: [
parseFloat(this.picamera.awb_gains[0]),
parseFloat(this.picamera.awb_gains[1]),
],
}
}
};

View file

@ -9,7 +9,44 @@
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
"
:submit-url="recalibrationLinks.recalibrate.href"
:submit-label="'Auto-Calibrate'"
:submit-label="'Full Auto-Calibrate'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
</div>
<div v-if="'auto_exposure_from_raw' in recalibrationLinks" class="uk-margin-small">
<taskSubmitter
:can-terminate="false"
:requires-confirmation="false"
:submit-url="recalibrationLinks.auto_exposure_from_raw.href"
:submit-label="'Auto gain &amp; shutter speed'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
</div>
<div v-if="'auto_white_balance_from_raw' in recalibrationLinks" class="uk-margin-small">
<taskSubmitter
:can-terminate="false"
:requires-confirmation="false"
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href"
:submit-label="'Auto white balance'"
@response="onRecalibrateResponse"
@error="modalError"
>
</taskSubmitter>
</div>
<div v-if="'auto_lens_shading_table' in recalibrationLinks" class="uk-margin-small">
<taskSubmitter
:can-terminate="false"
:requires-confirmation="true"
:confirmation-message="
'Is the microscope looking at an evenly illuminated, empty field of view? ' +
'If not, the current image will show through in any images captured afterwards.'
"
:submit-url="recalibrationLinks.auto_lens_shading_table.href"
:submit-label="'Auto flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
>
@ -22,15 +59,7 @@
class="uk-button uk-button-danger uk-width-1-1"
@click="flattenLensShadingTableRequest"
>
Disable flat-field correction
</button>
<button
v-if="'delete_lens_shading_table' in recalibrationLinks"
class="uk-button uk-button-danger uk-margin-small-top uk-width-1-1"
@click="deleteLensShadingTableRequest"
>
Adaptive flat-field correction
Disable flat field correction
</button>
</div>

View file

@ -33,7 +33,6 @@ export default {
},
set(value) {
this.$store.commit("changeIHIEnabled", value);
this.$root.$emit("globalSafeTogglePreview", value);
}
}
},

View file

@ -73,8 +73,11 @@ export default {
return this.$store.state.autoGpuPreview;
},
set(value) {
// NB the stream viewer watches the store, and is
// responsible for making the request that switches
// GPU preview on/off
// see streamContent.vue
this.$store.commit("changeAutoGpuPreview", value);
this.$root.$emit("globalSafeTogglePreview", value);
}
},
@ -86,6 +89,51 @@ export default {
this.$store.commit("changeTrackWindow", value);
}
}
},
watch: {
// Cache the stream settings to local storage for persistence
// (the next 3 functions all relate to this)
disableStream: function(newValue) {
console.log(
`disableStream updated to ${newValue} and saved in local storage`
);
this.setLocalStorageObj("disableStream", newValue);
},
autoGpuPreview: function(newValue) {
console.log(
`GPU preview updated to ${newValue} and saved in local storage`
);
this.setLocalStorageObj("autoGpuPreview", newValue);
},
trackWindow: function(newValue) {
console.log(
`trackWindow updated to ${newValue} and saved in local storage`
);
this.setLocalStorageObj("trackWindow", newValue);
}
},
created() {
// Apply sensible defaults for stream settings, depending on
// whether we're connecting locally or remotely, respecting
// the settings that were cached previously.
const localMode = ["localhost", "0.0.0.0", "127.0.0.1", "[::1]"].includes(
window.location.hostname
);
const localDefaults = {
disableStream: true,
autoGpuPreview: true,
trackWindow: true
};
for (let k in localDefaults) {
if (localStorage.getItem(k) !== null) {
this[k] = this.getLocalStorageObj(k);
} else if (localMode) {
console.log(`${k} set to default value for a local connection`);
this[k] = localDefaults[k];
}
}
}
};
</script>

View file

@ -69,6 +69,16 @@ export default {
},
settingsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
},
autoGpuPreview: function() {
return this.$store.state.autoGpuPreview;
}
},
watch: {
autoGpuPreview: function(newValue) {
// When the GPU preview setting in the store changes, update the server
this.safePreviewRequest(newValue);
}
},
@ -77,9 +87,6 @@ export default {
this.$root.$on("globalTogglePreview", state => {
this.previewRequest(state);
});
this.$root.$on("globalSafeTogglePreview", state => {
this.safePreviewRequest(state);
});
// A global signal listener to flash the stream element
this.$root.$on("globalFlashStream", () => {
this.flashStream();
@ -97,7 +104,7 @@ export default {
created: function() {
// Send a request to start/stop GPU preview based on global setting
this.safePreviewRequest(this.$store.state.autoGpuPreview);
this.safePreviewRequest(this.autoGpuPreview);
},
beforeDestroy: function() {

View file

@ -61,12 +61,25 @@ const moduleImjoy = {
getters: {}
};
function getOriginFromLocation() {
// This will default to the same origin that's serving
// the web app - but can be overridden by the URL.
// See also devTools.vue which can change the origin.
let url = new URL(window.location.href);
let origin = url.searchParams.get("overrideOrigin");
if (origin) {
return origin;
} else {
return url.origin;
}
}
export default new Vuex.Store({
modules: {
imjoy: moduleImjoy
},
state: {
origin: window.location.origin,
origin: getOriginFromLocation(),
available: false,
waiting: false,
error: "",

1592
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,7 @@ isort = "isort ."
pylint = "pylint openflexure_microscope"
mypy = "mypy --cobertura-xml-report openflexure_microscope openflexure_microscope"
test = "pytest ."
serve = "python -m openflexure_microscope.api.app"
format = ["black", "isort"]
lint = ["pylint", "mypy"]

View file

@ -22,7 +22,7 @@ with open(path.join(here, "README.md"), encoding="utf-8") as f:
setup(
name="openflexure-microscope-server",
version="2.10.0b0",
version="2.10.0b1",
description="Python module, and Flask-based web API, to run the OpenFlexure Microscope.",
long_description=long_description,
long_description_content_type="text/markdown",