├── .gitmodules ├── testimages ├── markers.csv ├── 64x64.ome.tif ├── CMU-1-Small-Region.svs ├── 2048x2048_ome6_tiled.ome.tif ├── exhibit1_in.json ├── exhibit0_in.json ├── exhibit1_out.json ├── exhibit0_out.json ├── exhibit2_out.json └── exhibit2_in.json ├── icon.ico ├── src ├── crender.dll ├── crender.so ├── test_vega.py ├── fit_render_settings.py ├── convert_omero_channels.py ├── create_vega.py ├── thumbnail.py ├── render_jpg.py ├── render_png.py ├── story.py ├── storyexport.py ├── exhibit.py ├── pyramid_assemble.py └── render.py ├── .git-blame-ignore-revs ├── .gitignore ├── testcharts ├── barchart.csv ├── barchart.vega.csv ├── barchart_vega.csv ├── matrix.csv ├── matrix.vega.csv ├── matrix_vega.csv └── scatterplot.csv ├── static ├── image │ ├── favicon.png │ ├── openseadragon │ │ ├── home_rest.png │ │ ├── next_rest.png │ │ ├── button_hover.png │ │ ├── button_rest.png │ │ ├── home_hover.png │ │ ├── home_pressed.png │ │ ├── next_hover.png │ │ ├── next_pressed.png │ │ ├── zoomin_hover.png │ │ ├── zoomin_rest.png │ │ ├── zoomout_rest.png │ │ ├── button_pressed.png │ │ ├── fullpage_hover.png │ │ ├── fullpage_rest.png │ │ ├── previous_hover.png │ │ ├── previous_rest.png │ │ ├── zoomin_pressed.png │ │ ├── zoomout_hover.png │ │ ├── button_grouphover.png │ │ ├── fullpage_pressed.png │ │ ├── home_grouphover.png │ │ ├── next_grouphover.png │ │ ├── previous_pressed.png │ │ ├── rotateleft_hover.png │ │ ├── rotateleft_rest.png │ │ ├── rotateright_hover.png │ │ ├── rotateright_rest.png │ │ ├── zoomin_grouphover.png │ │ ├── zoomout_pressed.png │ │ ├── fullpage_grouphover.png │ │ ├── previous_grouphover.png │ │ ├── rotateleft_pressed.png │ │ ├── rotateright_pressed.png │ │ ├── zoomout_grouphover.png │ │ ├── rotateleft_grouphover.png │ │ └── rotateright_grouphover.png │ ├── Minerva-Author_HorizLogo_RGB.svg │ └── Minerva_FinalLogo_NoText_RGB.svg ├── vert.glsl ├── index.html └── frag.glsl ├── package_mac.sh ├── package_win.bat ├── requirements.yml ├── minerva-story └── index.html ├── LICENSE ├── appveyor.yml ├── README.md └── .github └── workflows └── pyinstaller.yml /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testimages/markers.csv: -------------------------------------------------------------------------------- 1 | DAPI 2 | panCK 3 | S100 4 | Keratin -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/icon.ico -------------------------------------------------------------------------------- /src/crender.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/src/crender.dll -------------------------------------------------------------------------------- /src/crender.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/src/crender.so -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Migrate code style to Black. 2 | e4db1091054f3485d0d3e7c9a526eedd30ed14c6 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | 4 | *.spec 5 | __pycache__ 6 | 7 | .DS_Store 8 | 9 | tmp 10 | -------------------------------------------------------------------------------- /testcharts/barchart.csv: -------------------------------------------------------------------------------- 1 | type,frequency 2 | Tumor,23000 3 | Stroma,25000 4 | Immune,32000 5 | Other,28000 -------------------------------------------------------------------------------- /static/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/favicon.png -------------------------------------------------------------------------------- /testcharts/barchart.vega.csv: -------------------------------------------------------------------------------- 1 | type,frequency 2 | Tumor,23000 3 | Stroma,25000 4 | Immune,32000 5 | Other,28000 6 | -------------------------------------------------------------------------------- /testcharts/barchart_vega.csv: -------------------------------------------------------------------------------- 1 | type,frequency 2 | Tumor,23000 3 | Stroma,25000 4 | Immune,32000 5 | Other,28000 6 | -------------------------------------------------------------------------------- /testimages/64x64.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/testimages/64x64.ome.tif -------------------------------------------------------------------------------- /testimages/CMU-1-Small-Region.svs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/testimages/CMU-1-Small-Region.svs -------------------------------------------------------------------------------- /static/image/openseadragon/home_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/home_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/next_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/next_rest.png -------------------------------------------------------------------------------- /testimages/2048x2048_ome6_tiled.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/testimages/2048x2048_ome6_tiled.ome.tif -------------------------------------------------------------------------------- /static/image/openseadragon/button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/button_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/button_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/button_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/home_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/home_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/home_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/home_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/next_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/next_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/next_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/next_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomin_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomin_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomin_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomin_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomout_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomout_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/button_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/button_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/fullpage_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/fullpage_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/fullpage_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/fullpage_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/previous_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/previous_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/previous_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/previous_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomin_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomin_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomout_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomout_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/button_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/button_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/fullpage_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/fullpage_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/home_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/home_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/next_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/next_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/previous_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/previous_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateleft_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateleft_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateleft_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateleft_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateright_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateright_hover.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateright_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateright_rest.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomin_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomin_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomout_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomout_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/fullpage_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/fullpage_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/previous_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/previous_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateleft_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateleft_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateright_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateright_pressed.png -------------------------------------------------------------------------------- /static/image/openseadragon/zoomout_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/zoomout_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateleft_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateleft_grouphover.png -------------------------------------------------------------------------------- /static/image/openseadragon/rotateright_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/labsyspharm/minerva-author/HEAD/static/image/openseadragon/rotateright_grouphover.png -------------------------------------------------------------------------------- /testcharts/matrix.csv: -------------------------------------------------------------------------------- 1 | ClustName,KERATIN,ASMA,CD45 2 | Tumor,0.356225098,-0.454998929,-0.085779064 3 | Stromal,-0.345658946,0.213412916,-0.206773617 4 | Immune,-0.304689947,-0.119950697,0.445621169 5 | Other,-0.296644655,-0.317059176,-0.043042572 -------------------------------------------------------------------------------- /static/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec2 a_uv; 3 | out vec2 uv; 4 | 5 | void main() { 6 | // Texture coordinates 7 | uv = a_uv; 8 | 9 | // Clip coordinates 10 | vec2 full_pos = 2. * a_uv - 1.; 11 | gl_Position = vec4(full_pos, 0., 1.); 12 | } 13 | -------------------------------------------------------------------------------- /package_mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pyinstaller -F --add-data "static:static" --add-data "minerva-story:minerva-story" --collect-all altair --collect-all xmlschema --collect-all ome_types --collect-submodules xsdata_pydantic_basemodel --hidden-import "imagecodecs._shared" --hidden-import "imagecodecs._imcd" --hidden-import "imagecodecs.jpeg8_decode" src/app.py 3 | -------------------------------------------------------------------------------- /package_win.bat: -------------------------------------------------------------------------------- 1 | pyinstaller -F --hidden-import="pkg_resources.py2_warn" --add-data "static;static" --add-data "minerva-story;minerva-story" --collect-all altair --collect-all xmlschema --collect-all ome_types --collect-submodules xsdata_pydantic_basemodel --icon icon.ico --name minerva_author --hidden-import="imagecodecs._shared" --hidden-import="imagecodecs._imcd" src/app.py 2 | -------------------------------------------------------------------------------- /testcharts/matrix.vega.csv: -------------------------------------------------------------------------------- 1 | x,y,z 2 | KERATIN,Tumor,0.356225098 3 | KERATIN,Stromal,-0.345658946 4 | KERATIN,Immune,-0.304689947 5 | KERATIN,Other,-0.296644655 6 | ASMA,Tumor,-0.454998929 7 | ASMA,Stromal,0.213412916 8 | ASMA,Immune,-0.119950697 9 | ASMA,Other,-0.317059176 10 | CD45,Tumor,-0.085779064 11 | CD45,Stromal,-0.206773617 12 | CD45,Immune,0.445621169 13 | CD45,Other,-0.043042572 14 | -------------------------------------------------------------------------------- /testcharts/matrix_vega.csv: -------------------------------------------------------------------------------- 1 | x,y,z 2 | KERATIN,Tumor,0.356225098 3 | KERATIN,Stromal,-0.345658946 4 | KERATIN,Immune,-0.304689947 5 | KERATIN,Other,-0.296644655 6 | ASMA,Tumor,-0.454998929 7 | ASMA,Stromal,0.213412916 8 | ASMA,Immune,-0.119950697 9 | ASMA,Other,-0.317059176 10 | CD45,Tumor,-0.085779064 11 | CD45,Stromal,-0.206773617 12 | CD45,Immune,0.445621169 13 | CD45,Other,-0.043042572 14 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | name: minerva-author-new 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.13 6 | - blas=*=openblas 7 | - numpy>=2.2.5 8 | - pillow>=11.2.1 9 | - flask>=3.1.1 10 | - waitress>=3.0.2 11 | - pydantic>=2.11.4 12 | - flask_cors>=4.0.0 13 | - matplotlib>=3.10.3 14 | - tifffile>=2025.5.10 15 | - scikit-learn>=1.6.1 16 | - scikit-image>=0.25.2 17 | - ome-types=0.6.0 18 | - Werkzeug>=3.1.3 19 | - xsdata==24.3.1 20 | - pandas>=1.5.0 21 | - zarr=2.18.7 22 | - altair>=5.5.0 23 | - pip>=25.1.1 24 | - pip: 25 | - openslide-python==1.4.2 26 | -------------------------------------------------------------------------------- /minerva-story/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | Minerva Author
2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Harvard Medical School 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test_vega.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from create_vega import create_scatterplot 4 | from create_vega import create_barchart 5 | from create_vega import create_matrix 6 | from create_vega import create_vega_dict 7 | 8 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 9 | 10 | 11 | def create_vega_test(name, fn, params): 12 | prefix = "../testcharts" 13 | in_path = Path(prefix) / f"{name}.csv" 14 | out_path = Path(f"{name}_vega.csv") 15 | return create_vega_dict(in_path, out_path, fn, params) 16 | 17 | 18 | def test_create_vega_scatterplot(): 19 | result = create_vega_test( 20 | "scatterplot", 21 | create_scatterplot, 22 | { 23 | "clusters": ["Tumor", "Other", "Immune", "Stromal"], 24 | "colors": ["FFFFFF", 'FF0000', '00FF00', '0000FF'], 25 | "xLabel": "KERATIN", 26 | "yLabel": "CD45", 27 | }, 28 | ) 29 | assert result["data"]["url"] == "scatterplot_vega.csv" 30 | 31 | 32 | def test_create_vega_barchart(): 33 | result = create_vega_test("barchart", create_barchart, {}) 34 | assert result["data"]["url"] == "barchart_vega.csv" 35 | 36 | 37 | def test_create_vega_matrix(): 38 | result = create_vega_test("matrix", create_matrix, {}) 39 | assert result["data"]["url"] == "matrix_vega.csv" 40 | -------------------------------------------------------------------------------- /src/fit_render_settings.py: -------------------------------------------------------------------------------- 1 | from tifffile.tifffile import TiffFileError 2 | from story import main as auto_minerva 3 | from app import Opener 4 | import argparse 5 | import pathlib 6 | import json 7 | import sys 8 | 9 | 10 | def yield_numeric_labels(num_channels): 11 | for label_num in range(num_channels): 12 | yield str(label_num) 13 | 14 | 15 | if __name__ == "__main__": 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument( 18 | "ome_tiff", 19 | metavar="ome_tiff", 20 | type=pathlib.Path, 21 | help="Input path to OME-TIFF with all channel groups", 22 | ) 23 | parser.add_argument( 24 | "output_file", 25 | metavar="output_file", 26 | type=pathlib.Path, 27 | help="output file file to story.json with fit settings", 28 | ) 29 | args = parser.parse_args() 30 | 31 | opener = None 32 | try: 33 | opener = Opener(args.ome_tiff) 34 | num_channels = opener.get_shape()[0] 35 | labels = yield_numeric_labels(num_channels) 36 | results = auto_minerva(opener, list(labels)) 37 | print(results) 38 | with open(args.output_file, 'w') as wf: 39 | json.dump(results, wf) 40 | except (FileNotFoundError, TiffFileError) as e: 41 | print(f"Invalid ome-tiff file: cannot parse {args.ome_tiff}", file=sys.stderr) 42 | -------------------------------------------------------------------------------- /testimages/exhibit1_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "channels": [ 5 | { 6 | "color": "ff7f7f", 7 | "id": 0, 8 | "label": "../", 9 | "max": 0.5000076295109483, 10 | "min": 0 11 | }, 12 | { 13 | "color": "ff7f00", 14 | "id": 1, 15 | "label": "../", 16 | "max": 0.5605554283970398, 17 | "min": 0 18 | } 19 | ], 20 | "label": "group" 21 | } 22 | ], 23 | "header": "", 24 | "image": { 25 | "description": "" 26 | }, 27 | "masks": [ 28 | { 29 | "channels": [ 30 | { 31 | "color": "ff0000", 32 | "label": "test" 33 | } 34 | ], 35 | "label": "../", 36 | "path": "./test.ome.tif" 37 | }, 38 | { 39 | "channels": [ 40 | { 41 | "color": "0000ff", 42 | "label": "2" 43 | } 44 | ], 45 | "label": "../", 46 | "path": "./test.ome.tif" 47 | } 48 | ], 49 | "rotation": 0, 50 | "waypoints": [ 51 | { 52 | "arrows": [], 53 | "group": "group", 54 | "masks": [ 55 | 1 56 | ], 57 | "name": "name", 58 | "overlays": [], 59 | "pan": [ 60 | 0.8851152525747915, 61 | 0.5 62 | ], 63 | "text": "description", 64 | "zoom": 0.5541666666666667 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /testimages/exhibit0_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "channels": [ 5 | { 6 | "color": "ff7f7f", 7 | "id": 0, 8 | "label": "0", 9 | "max": 0.5000076295109483, 10 | "min": 0 11 | }, 12 | { 13 | "color": "ff7f00", 14 | "id": 1, 15 | "label": "1", 16 | "max": 0.5605554283970398, 17 | "min": 0 18 | } 19 | ], 20 | "label": "group" 21 | } 22 | ], 23 | "header": "", 24 | "image": { 25 | "description": "" 26 | }, 27 | "masks": [ 28 | { 29 | "channels": [ 30 | { 31 | "color": "ff0000", 32 | "label": "test 1 channel" 33 | } 34 | ], 35 | "label": "test 1", 36 | "path": "./test.ome.tif" 37 | }, 38 | { 39 | "channels": [ 40 | { 41 | "color": "0000ff", 42 | "label": "test 2 channel" 43 | } 44 | ], 45 | "label": "test 2", 46 | "path": "./test.ome.tif" 47 | } 48 | ], 49 | "rotation": 0, 50 | "waypoints": [ 51 | { 52 | "arrows": [], 53 | "group": "group", 54 | "masks": [ 55 | 0 56 | ], 57 | "name": "name", 58 | "overlays": [], 59 | "pan": [ 60 | 0.8851152525747915, 61 | 0.5 62 | ], 63 | "text": "description", 64 | "zoom": 0.5541666666666667 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | image: Visual Studio 2017 3 | environment: 4 | matrix: 5 | - PYTHON_VERSION: "3.10" 6 | MINICONDA: C:\Miniconda37-x64 7 | init: 8 | - "ECHO %PYTHON_VERSION% %MINICONDA%" 9 | 10 | artifacts: 11 | - path: minerva_author.zip 12 | name: MinervaAuthor 13 | 14 | install: 15 | # Issues have been encountered with installing numpy and scipy on 16 | # AppVeyor e.g. 17 | # http://tjelvarolsson.com/blog/how-to-continuously-test-your-python-code-on-windows-using-appveyor/ 18 | # Miniconda is recommended as the way to install these. See also: 19 | # https://github.com/appveyor/ci/issues/359 20 | # The following adopts approaches suggested in the above links. 21 | - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" 22 | - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" 23 | - curl https://github.com/openslide/openslide-winbuild/releases/download/v20171122/openslide-win64-20171122.zip -L -o openslide.zip 24 | - unzip openslide.zip 25 | - move openslide-win64-20171122\bin\* src\ 26 | - git submodule update --init --recursive 27 | - conda config --set always_yes yes --set changeps1 no 28 | - conda config --add channels conda-forge 29 | - conda update -q conda 30 | - conda info -a 31 | - conda env create -f requirements.yml 32 | - conda activate minerva-author 33 | - copy "%CONDA_PREFIX%\Library\bin\libcrypto-1_1-x64.dll" "%CONDA_PREFIX%\DLLs\libcrypto-1_1-x64.dll" 34 | - copy "%CONDA_PREFIX%\Library\bin\libssl-1_1-x64.dll" "%CONDA_PREFIX%\DLLs\libssl-1_1-x64.dll" 35 | 36 | build_script: 37 | - pyinstaller -F --paths $env:CONDA_PREFIX --hidden-import="pkg_resources.py2_warn" --add-data "static;static" --add-data "minerva-story;minerva-story" --add-data "%CONDA_PREFIX%\Lib\site-packages\xmlschema\schemas;xmlschema\schemas" --icon icon.ico --name minerva_author src/app.py 38 | 39 | after_build: 40 | - 7z a minerva_author.zip dist 41 | -------------------------------------------------------------------------------- /testimages/exhibit1_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "Groups": [ 3 | { 4 | "Channels": [ 5 | "../", 6 | "../" 7 | ], 8 | "Colors": [ 9 | "ff7f7f", 10 | "ff7f00" 11 | ], 12 | "Name": "group", 13 | "Path": "group_0__0--1__0" 14 | } 15 | ], 16 | "Header": "", 17 | "Images": [ 18 | { 19 | "Description": "", 20 | "Height": 2048, 21 | "MaxLevel": 1, 22 | "Name": "i0", 23 | "Path": "images/test", 24 | "Width": 2048 25 | } 26 | ], 27 | "Layout": { 28 | "Grid": [ 29 | [ 30 | "i0" 31 | ] 32 | ] 33 | }, 34 | "Masks": [ 35 | { 36 | "Channels": [ 37 | "test" 38 | ], 39 | "Colors": [ 40 | "ff0000" 41 | ], 42 | "Name": "../", 43 | "Path": "0" 44 | }, 45 | { 46 | "Channels": [ 47 | "2" 48 | ], 49 | "Colors": [ 50 | "0000ff" 51 | ], 52 | "Name": "../-0", 53 | "Path": "0-0" 54 | } 55 | ], 56 | "Rotation": 0, 57 | "Stories": [ 58 | { 59 | "Description": "", 60 | "Name": "", 61 | "Waypoints": [ 62 | { 63 | "ActiveMasks": [ 64 | "../-0" 65 | ], 66 | "Arrows": [], 67 | "Description": "description", 68 | "Group": "group", 69 | "Masks": [ 70 | "../-0" 71 | ], 72 | "Name": "name", 73 | "Overlays": [], 74 | "Pan": [ 75 | 0.8851152525747915, 76 | 0.5 77 | ], 78 | "Zoom": 0.5541666666666667 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /testimages/exhibit0_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "Groups": [ 3 | { 4 | "Channels": [ 5 | "0", 6 | "1" 7 | ], 8 | "Colors": [ 9 | "ff7f7f", 10 | "ff7f00" 11 | ], 12 | "Name": "group", 13 | "Path": "group_0__0--1__1" 14 | } 15 | ], 16 | "Header": "", 17 | "Images": [ 18 | { 19 | "Description": "", 20 | "Height": 2048, 21 | "MaxLevel": 1, 22 | "Name": "i0", 23 | "Path": "images/test", 24 | "Width": 2048 25 | } 26 | ], 27 | "Layout": { 28 | "Grid": [ 29 | [ 30 | "i0" 31 | ] 32 | ] 33 | }, 34 | "Masks": [ 35 | { 36 | "Channels": [ 37 | "test 1 channel" 38 | ], 39 | "Colors": [ 40 | "ff0000" 41 | ], 42 | "Name": "test 1", 43 | "Path": "test-1" 44 | }, 45 | { 46 | "Channels": [ 47 | "test 2 channel" 48 | ], 49 | "Colors": [ 50 | "0000ff" 51 | ], 52 | "Name": "test 2", 53 | "Path": "test-2" 54 | } 55 | ], 56 | "Rotation": 0, 57 | "Stories": [ 58 | { 59 | "Description": "", 60 | "Name": "", 61 | "Waypoints": [ 62 | { 63 | "ActiveMasks": [ 64 | "test 1" 65 | ], 66 | "Arrows": [], 67 | "Description": "description", 68 | "Group": "group", 69 | "Masks": [ 70 | "test 1" 71 | ], 72 | "Name": "name", 73 | "Overlays": [], 74 | "Pan": [ 75 | 0.8851152525747915, 76 | 0.5 77 | ], 78 | "Zoom": 0.5541666666666667 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/convert_omero_channels.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import pathlib 5 | from json.decoder import JSONDecodeError 6 | 7 | 8 | def make_channel(ch_key, ch): 9 | ch_id = 0 10 | try: 11 | # Minerva Author uses a zero-based index 12 | ch_id = int(ch_key) - 1 13 | except ValueError as e: 14 | print(e) 15 | print(f'Skipping channel: "{ch_key}" is not an integer') 16 | return {} 17 | return { 18 | "label": ch["label"], 19 | "color": ch["color"].lower(), 20 | "min": ch["start"] / ch["max"], 21 | "max": ch["end"] / ch["max"], 22 | "id": ch_id, 23 | } 24 | 25 | 26 | def make_group(channels): 27 | all_channels = [make_channel(ch_key, ch) for (ch_key, ch) in channels.items()] 28 | all_channels = [ch for ch in all_channels if "id" in ch] 29 | all_channels.sort(key=lambda ch: ch["id"]) 30 | return {"channels": all_channels, "label": "imported omero channels"} 31 | 32 | 33 | def main(omero_json, author_json): 34 | 35 | story_json_checks = [ 36 | (author_json, [".json"]), 37 | (pathlib.Path(author_json.stem), [".groups", ".story"]), 38 | ] 39 | test_suffix = lambda check: check[0].suffix in check[1] 40 | if not all(map(test_suffix, story_json_checks)): 41 | print( 42 | " ".join( 43 | ["Invalid output path.", "It must end in .story.json or .groups.json"] 44 | ) 45 | ) 46 | return 47 | 48 | if os.path.exists(author_json): 49 | print(f"Overwriting existing save file {author_json}") 50 | else: 51 | print(f"Writing to new save file {author_json}") 52 | 53 | if not author_json.parent.exists(): 54 | author_json.parent.mkdir(parents=True) 55 | 56 | omero_channels = {} 57 | try: 58 | with open(omero_json) as rf: 59 | loaded = json.load(rf) 60 | omero_channels = loaded["channels"] 61 | except (FileNotFoundError, JSONDecodeError, KeyError): 62 | print(f"Invalid input file: cannot parse {omero_json}") 63 | return 64 | 65 | with open(author_json, "w") as wf: 66 | json.dump( 67 | { 68 | "in_file": "", 69 | "csv_file": "", 70 | "groups": [make_group(omero_channels)], 71 | "masks": [], 72 | "waypoints": [], 73 | "sample_info": {"rotation": 0, "name": "", "text": ""}, 74 | }, 75 | wf, 76 | ) 77 | 78 | print(f"Success! {author_json} written") 79 | 80 | 81 | if __name__ == "__main__": 82 | 83 | parser = argparse.ArgumentParser() 84 | parser.add_argument( 85 | "omero_json", 86 | metavar="omero_json", 87 | type=pathlib.Path, 88 | help="Input path to exported omero channels", 89 | ) 90 | parser.add_argument( 91 | "author_json", 92 | metavar="author_json", 93 | type=pathlib.Path, 94 | help="Output Minerva Author save file with channels from omero", 95 | ) 96 | args = parser.parse_args() 97 | 98 | omero_json = args.omero_json 99 | author_json = args.author_json 100 | 101 | main(omero_json, author_json) 102 | -------------------------------------------------------------------------------- /static/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp int; 3 | precision highp float; 4 | precision highp usampler2D; 5 | 6 | uniform usampler2D u_tile; 7 | uniform usampler2D u_ids; 8 | uniform vec3 u_tile_color; 9 | uniform vec2 u_tile_range; 10 | uniform ivec2 u_ids_shape; 11 | uniform int u_tile_fmt; 12 | 13 | uniform uint u8; 14 | 15 | const uint MAX = uint(16384) * uint(16384); 16 | const uint bMAX = uint(ceil(log2(float(MAX)))); 17 | 18 | in vec2 uv; 19 | out vec4 color; 20 | 21 | // rgba to 32 bit int 22 | uint unpack(uvec4 id) { 23 | return id.x + uint(256)*id.y + uint(65536)*id.z + uint(16777216)*id.w; 24 | } 25 | 26 | // ID Lookup 27 | uint lookup_ids_idx(float idx) { 28 | // 2D indices for given index 29 | vec2 ids_max = vec2(float(u_ids_shape.x), float(u_ids_shape.y)); 30 | vec2 ids_idx = vec2(mod(idx, ids_max.x) / ids_max.x, 1.0 - ceil(idx / ids_max.x) / ids_max.y); 31 | // Value for given index 32 | uvec4 m_value = texture(u_ids, ids_idx); 33 | return unpack(m_value); 34 | } 35 | 36 | // Binary Search 37 | bool is_in_ids(uint ikey) { 38 | // Array size 39 | uint first = uint(0); 40 | uint last = uint(u_ids_shape.x) * uint(u_ids_shape.y) - uint(1); 41 | 42 | // Search within log(n) runtime 43 | for (uint i = uint(0); i <= bMAX; i++) { 44 | // Evaluate the midpoint 45 | uint mid = (first + last) / uint(2); 46 | uint here = lookup_ids_idx(float(mid)); 47 | 48 | // Break if list gone 49 | if (first == last && ikey != here) { 50 | break; 51 | } 52 | 53 | // Search below midpoint 54 | if (here > ikey) last = mid; 55 | 56 | // Search above midpoint 57 | else if (ikey > here) first = mid; 58 | 59 | // Found at midpoint 60 | else return true; 61 | } 62 | // Not found 63 | return false; 64 | } 65 | 66 | vec4 hsv2rgb(vec3 c, float a) { 67 | vec4 K = vec4(1., 2./3., 1./3., 3.); 68 | vec3 p = abs(fract(c.xxx + K.xyz) * 6. - K.www); 69 | vec3 done = c.z * mix(K.xxx, clamp(p - K.xxx, 0., 1.), c.y); 70 | return vec4(done,a); 71 | } 72 | 73 | vec3 spike(float id) { 74 | vec3 star = pow(vec3(3,7,2),vec3(-1)) + pow(vec3(10),vec3(-2,-3,-2)); 75 | vec3 step = fract(id*star); 76 | step.z = mix(0.2,0.9,step.z); 77 | step.y = mix(0.6,1.0,step.y); 78 | return step; 79 | } 80 | 81 | vec4 colormap (uint id) { 82 | vec3 hsv = spike(float(id)); 83 | float alpha = 1.; 84 | if (id == uint(0)) { 85 | hsv = vec3(0.0, 0.0, 0.0); 86 | alpha = 0.; 87 | } 88 | return hsv2rgb(hsv, alpha); 89 | } 90 | 91 | vec4 u32_rgba_map() { 92 | uvec4 pixel = texture(u_tile, uv); 93 | uint id = unpack(pixel); 94 | 95 | uint n_ids = uint(u_ids_shape.x) * uint(u_ids_shape.y); 96 | 97 | if (n_ids == uint(0)) { 98 | return colormap(id); 99 | } 100 | else if (id != uint(0) && is_in_ids(id)) { 101 | return vec4(u_tile_color, 1.0); 102 | } 103 | else { 104 | return vec4(0.0, 0.0, 0.0, 0.0); 105 | } 106 | } 107 | 108 | vec4 u16_rg_range() { 109 | uvec2 pixel = texture(u_tile, uv).rg; 110 | float value = float(pixel.r * u8 + pixel.g) / 65535.; 111 | 112 | float min_ = u_tile_range[0]; 113 | float max_ = u_tile_range[1]; 114 | 115 | // Threshhold pixel within range 116 | float pixel_val = clamp((value - min_) / (max_ - min_), 0.0, 1.0); 117 | 118 | // Color pixel value 119 | vec3 pixel_color = pixel_val * u_tile_color; 120 | return vec4(pixel_color, 1.0); 121 | } 122 | 123 | void main() { 124 | if (u_tile_fmt == 32) { 125 | color = u32_rgba_map(); 126 | if (color.a < 0.1) { 127 | discard; 128 | } 129 | } 130 | else { 131 | color = u16_rg_range(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Download and run Minerva Author 2 | 3 | New users should start with our [pre-built Windows and MacOS applications](https://github.com/labsyspharm/minerva-author/releases/latest). More detailed [download and launch instructions](https://www.minerva.im/download.html) can be found at the Minerva website, in addition to [complete instructions and tutorials](https://www.minerva.im/usage/). 4 | 5 | ## Information For Software Developers 6 | 7 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 8 | 9 | ### Project Structure 10 | 11 | #### Minerva Story 12 | The GitHub Pages site build is stored at [minerva-story](https://github.com/labsyspharm/minerva-story). The source code for the minified bundle is stored at [minerva-browser](https://github.com/labsyspharm/minerva-browser). 13 | 14 | #### Minerva Author 15 | The Python Flask server along with automated testing is stored at [minerva-author](https://github.com/labsyspharm/minerva-author). The React UI is stored at [minerva-author-ui](https://github.com/labsyspharm/minerva-author-ui) 16 | 17 | ### Installing From the Source Repository 18 | 19 | All commands should be run in "Terminal" on MacOS and "Anaconda Prompt" on Windows. 20 | 21 | First, download this repository through the git command line: 22 | 23 | ``` 24 | git clone https://github.com/labsyspharm/minerva-author.git 25 | ``` 26 | 27 | #### Windows 28 | 29 | * [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) 30 | * Install [Anaconda](https://docs.anaconda.com/anaconda/install/windows/) 31 | * Move [openslide](https://openslide.org/download/#windows-binaries) "bin" directory to "minerva-author/src" 32 | * Run `conda install -c anaconda git` 33 | 34 | Then run the following commands to set up the development environment: 35 | 36 | ``` 37 | cd minerva-author 38 | conda env create -f requirements.yml 39 | conda activate minerva-author 40 | ``` 41 | 42 | #### MacOS 43 | 44 | * install [homebrew](https://brew.sh/) and run `brew install openslide`. 45 | * Install [Anaconda](https://docs.anaconda.com/anaconda/install/mac-os/) 46 | 47 | Then run the following commands to set up the development environment: 48 | 49 | ``` 50 | cd minerva-author 51 | conda env create -f requirements.yml 52 | conda activate minerva-author 53 | ``` 54 | 55 | ### Running 56 | 57 | ``` 58 | python src/app.py 59 | ``` 60 | 61 | - Browser window should open automatically, if not then open a browser to `localhost:2020` 62 | 63 | - Browse or copy the file path to an OME-TIFF or SVS 64 | 65 | - Click import and wait for the generation of a full pyramid 66 | 67 | At minimum, you'll need to type one 'Group' name into the top dropdown to create a group. For each group you create, you can select channels from the second dropdown and set up their rendering settings with the various sliders. After you hit 'save', look in the directory of the executable (or app.py) for a new folder which contains the generated Minerva Story, with configuration files and an image pyramid. 68 | 69 | ### Automated test suite 70 | 71 | The project contains automated tests using the pytest framework. To run the test suite, simply execute in the project folder: 72 | ``` 73 | pytest 74 | ``` 75 | 76 | ### Automated Releases 77 | 78 | All pushes to master will update the current draft relase. 79 | 80 | ### Packaging 81 | 82 | #### MacOS 83 | 84 | To package the application as a standalone executable, run script: 85 | ``` 86 | bash package_mac.sh 87 | ``` 88 | 89 | #### Windows (powershell) 90 | 91 | Fetch OpenSlide binaries from https://openslide.org/download/#windows-binaries and save the .dll files to /src. Then run script: 92 | ``` 93 | package_win.bat 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /.github/workflows/pyinstaller.yml: -------------------------------------------------------------------------------- 1 | name: PyInstaller 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | pull_request: 8 | branches: [ master ] 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [macos-12 ] 17 | python-version: ["3.10"] 18 | runs-on: ${{ matrix.os }} 19 | name: Pyinstaller for ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: conda-incubator/setup-miniconda@v2 23 | with: 24 | auto-update-conda: true 25 | environment-file: requirements.yml 26 | activate-environment: minerva-author 27 | python-version: ${{ matrix.python-version }} 28 | - name: OpenSlide Windows 29 | if: startsWith(matrix.os,'windows') 30 | shell: cmd /C CALL {0} 31 | run: | 32 | curl https://github.com/openslide/openslide-winbuild/releases/download/v20171122/openslide-win64-20171122.zip -L -o openslide.zip 33 | unzip openslide.zip 34 | dir 35 | move openslide-win64-20171122\bin\* src\ 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | - name: OpenSlide MacOS 39 | if: startsWith(matrix.os,'mac') 40 | shell: bash -l {0} 41 | run: | 42 | brew install openslide 43 | conda install openslide 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | - name: Copy dlls on Windows 47 | if: startsWith(matrix.os,'windows') 48 | shell: cmd /C CALL {0} 49 | run: | 50 | copy "%CONDA_PREFIX%\Library\bin\libcrypto-3-x64.dll" "%CONDA_PREFIX%\DLLs\libcrypto-3-x64.dll" 51 | copy "%CONDA_PREFIX%\Library\bin\libssl-3-x64.dll" "%CONDA_PREFIX%\DLLs\libssl-3-x64.dll" 52 | - name: Package Windows 53 | if: startsWith(matrix.os,'windows') 54 | shell: cmd /C CALL {0} 55 | run: | 56 | package_win.bat 57 | - name: Zip Windows 58 | if: startsWith(matrix.os,'windows') 59 | shell: cmd /C CALL {0} 60 | run: | 61 | move "dist" "minerva_author_${{ github.ref_name }}" 62 | 7z a minerva_author_${{ github.ref_name }}_windows.zip minerva_author_${{ github.ref_name }} 63 | - name: Package Mac 64 | if: startsWith(matrix.os,'mac') 65 | shell: bash -l {0} 66 | run: | 67 | bash package_mac.sh 68 | - name: Zip Mac 69 | if: startsWith(matrix.os,'mac') 70 | shell: bash -l {0} 71 | run: | 72 | mv dist minerva_author_${{ github.ref_name }} 73 | mv minerva_author_${{ github.ref_name }}/app minerva_author_${{ github.ref_name }}/minerva_author 74 | zip -vr minerva_author_${{ github.ref_name }}_macos.zip minerva_author_${{ github.ref_name }}/ -x "*.DS_Store" 75 | - name: Create Release 76 | if: ${{ github.event_name == 'push' }} 77 | uses: softprops/action-gh-release@v1 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | with: 81 | tag_name: ${{ env.tag }} 82 | name: ${{ env.tag }} 83 | draft: false 84 | prerelease: ${{ contains(github.ref, 'rc') }} 85 | fail_on_unmatched_files: false 86 | files: | 87 | minerva_author_${{ github.ref_name }}_windows.zip 88 | minerva_author_${{ github.ref_name }}_macos.zip 89 | - name: Upload artifacts for pull requests 90 | if: ${{ github.event_name == 'pull_request' }} 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: minerva-author-pr${{ github.event.pull_request.number }}-${{ matrix.os }} 94 | path: minerva_author_*.zip 95 | compression-level: 0 96 | if-no-files-found: error 97 | -------------------------------------------------------------------------------- /src/create_vega.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from os.path import relpath 3 | import altair as alt 4 | import pandas as pd 5 | import numpy as np 6 | import io 7 | 8 | _z = "frequency" 9 | _x = "channel" 10 | _y = "type" 11 | 12 | 13 | def modify_default(in_data): 14 | return in_data 15 | 16 | 17 | def modify_matrix(in_data): 18 | # Handle legacy matrix specification 19 | if "ClustName" in in_data.columns: 20 | in_data = pd.melt(in_data, id_vars=["ClustName"], var_name=_x, value_name=_z) 21 | in_data = in_data.rename(columns={"ClustName": _y}) 22 | in_data = in_data[[_x, _y, _z]] 23 | return in_data 24 | 25 | 26 | def create_matrix(in_data, params={}): 27 | 28 | in_data = modify_matrix(in_data) 29 | 30 | x_order = [] 31 | y_order = [] 32 | # Ensure matrix is sorted by occurence in csv file 33 | for x, y in zip(in_data[_x], in_data[_y]): 34 | if x not in x_order: 35 | x_order.append(x) 36 | if len(x_order) == 1: 37 | y_order.append(y) 38 | 39 | x = alt.X(_x, type="nominal", sort=x_order) 40 | y = alt.Y(_y, type="nominal", sort=y_order) 41 | 42 | out_chart = alt.Chart(in_data).mark_rect().encode(x, y, color=f"{_z}:Q") 43 | return out_chart 44 | 45 | 46 | def create_scatterplot(in_data, params={}): 47 | xLabel = params["xLabel"] 48 | yLabel = params["yLabel"] 49 | clusters = params["clusters"] 50 | colors = [f"#{c}" for c in params["colors"]] 51 | dict_clusters = pd.DataFrame( 52 | [{"clust_ID": index + 1, "Cluster": c} for (index, c) in enumerate(clusters)] 53 | ) 54 | out_chart = ( 55 | alt.Chart(in_data) 56 | .mark_circle(size=60) 57 | .encode( 58 | alt.X(xLabel, type="quantitative", scale=alt.Scale(zero=False)), 59 | alt.Y(yLabel, type="quantitative", scale=alt.Scale(zero=False)), 60 | color=alt.Color( 61 | "Cluster:N", scale=alt.Scale(domain=clusters, range=colors) 62 | ), 63 | ) 64 | .transform_lookup( 65 | lookup="clust_ID", 66 | from_=alt.LookupData( 67 | data=dict_clusters, key="clust_ID", fields=["Cluster"] 68 | ), 69 | ) 70 | .transform_filter(alt.datum.Cluster != None) 71 | ) 72 | return out_chart 73 | 74 | 75 | def create_barchart(in_data, params={}): 76 | out_chart = alt.Chart(in_data).mark_bar().encode(x="type", y="frequency") 77 | return out_chart 78 | 79 | 80 | def create_vega_csv(in_path, out_path, modify_fn): 81 | in_data = pd.read_csv(in_path) 82 | in_data = modify_fn(in_data) 83 | 84 | if out_path is None: 85 | bytes_io = io.BytesIO() 86 | in_data.to_csv(bytes_io, index=False) 87 | bytes_io.seek(0) 88 | return bytes_io 89 | 90 | with open(out_path, "w+") as wf: 91 | in_data.to_csv(wf, index=False) 92 | 93 | return None 94 | 95 | 96 | def create_vega_dict(in_path, out_path, create_fn, params={}): 97 | alt.renderers.set_embed_options(theme="dark") 98 | in_data = pd.read_csv(in_path) 99 | vega_chart = create_fn(in_data, params) 100 | vega_dict = vega_chart.to_dict() 101 | del vega_dict["datasets"][vega_dict["data"]["name"]] 102 | vega_dict["data"] = {"url": str(out_path)} 103 | if "config" not in vega_dict: 104 | vega_dict["config"] = {} 105 | if "color" not in vega_dict["encoding"]: 106 | vega_dict["encoding"]["color"] = {} 107 | # Style the output chart 108 | vega_dict["encoding"]["color"]["legend"] = { 109 | "direction": "horizontal", 110 | "orient": "bottom" 111 | } 112 | vega_dict["encoding"]["x"]["grid"] = False 113 | vega_dict["encoding"]["y"]["grid"] = False 114 | vega_dict["config"]["background"] = None 115 | vega_dict["width"] = "container" 116 | return vega_dict 117 | -------------------------------------------------------------------------------- /src/thumbnail.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | import numpy as np 5 | import skimage.io 6 | import skimage.util 7 | 8 | def linear_blend(images): 9 | float_images = [ 10 | (color, skimage.util.img_as_float(i)) 11 | for (_, color, i) in images 12 | ] 13 | # Use first channel to define output 14 | (first_color, first_float_image) = float_images[0] 15 | first_float_color = parse_hex_color(first_color) 16 | float_out = first_float_color * first_float_image 17 | # Linear blending 18 | for (color, float_image) in float_images[1:]: 19 | float_color = parse_hex_color(color) 20 | float_out += float_color * float_image 21 | # Uint8 output 22 | out = np.clip(float_out, 0, 1) 23 | return skimage.util.img_as_ubyte(out) 24 | 25 | def parse_hex_color(hex_color): 26 | hex_color = hex_color.lstrip('#') 27 | return tuple( 28 | int(hex_color[i:i+2], 16) / 255 29 | for i in (0, 2, 4) 30 | ) 31 | 32 | def load_json(filename): 33 | with open(filename) as f: 34 | return json.load(f) 35 | 36 | def load_jpeg(filename): 37 | return skimage.io.imread(filename) 38 | 39 | def save_jpeg(filename, image): 40 | skimage.io.imsave(filename, image) 41 | 42 | def to_path_zoom(name): 43 | zoom = name.split('_')[0] 44 | try: 45 | return int(zoom) 46 | except ValueError: 47 | pass 48 | return -1 49 | 50 | def find_jpegs(folder): 51 | return [ 52 | filename for filename in os.listdir(folder) 53 | if filename.endswith('.jpeg') 54 | or filename.endswith('.jpg') 55 | ] 56 | 57 | def find_top_tiles(folder, channels): 58 | for (channel, name, color) in channels: 59 | subdir = os.path.join(folder, channel) 60 | names = [ 61 | name for name in find_jpegs(subdir) 62 | if to_path_zoom(name) >= 0 63 | ] 64 | names.sort(key=lambda name: to_path_zoom(name)) 65 | filename = os.path.join(subdir, names[-1]) 66 | thumb_tile = load_jpeg(filename) 67 | yield (name, color, thumb_tile) 68 | 69 | def find_group_channels(exhibit, group): 70 | ex = load_json(exhibit) 71 | chan_paths = { 72 | channel['Name']: channel['Path'] 73 | for channel in ex.get('Channels', []) 74 | if not channel['Rendered'] 75 | } 76 | for item in ex['Groups']: 77 | if item['Name'] == group: 78 | chan_cols = list( 79 | zip(item['Channels'], item['Colors']) 80 | ) 81 | return [ 82 | (chan_paths.get(c[0], None), c[0], c[1]) 83 | for c in chan_cols if c[0] 84 | ] 85 | return [] 86 | 87 | def find_group_tiles(root, exhibit, group): 88 | channel_list = find_group_channels(exhibit, group) 89 | return list(find_top_tiles(root, channel_list)) 90 | 91 | def name_blend(tiles): 92 | return '-'.join([ 93 | '_'.join([name, color]) 94 | for (name, color, _) in tiles 95 | ])+'.jpg' 96 | 97 | def merge_tiles_and_save_image(root, tiles): 98 | image_name = name_blend(tiles) 99 | image_file = os.path.join(root, image_name) 100 | image = linear_blend(tiles) 101 | # Make output directory and save image 102 | os.makedirs(root, exist_ok=True) 103 | save_jpeg(image_file, image) 104 | print(f'Saved image size {image.shape} to {image_file}') 105 | 106 | if __name__ == '__main__': 107 | parser = argparse.ArgumentParser() 108 | parser.add_argument('--json', type=str, required=True) 109 | parser.add_argument('--root', type=str, required=True) 110 | parser.add_argument('--group', type=str, required=True) 111 | parser.add_argument('--output', type=str, required=True) 112 | args = parser.parse_args() 113 | 114 | tiles = find_group_tiles(args.root, args.json, args.group) 115 | merge_tiles_and_save_image(args.output, tiles) 116 | -------------------------------------------------------------------------------- /testimages/exhibit2_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "Groups": [ 3 | { 4 | "Channels": [ 5 | "zero", 6 | "one", 7 | "two", 8 | "three" 9 | ], 10 | "Colors": [ 11 | "7f00ff", 12 | "ffff00", 13 | "00ffff", 14 | "00ff7f" 15 | ], 16 | "Name": "group A-_-_-_Purple_0_1_2_3", 17 | "Path": "group-A-Purple-0-1-2-3_0__zero--1__one--2__two--3__three" 18 | }, 19 | { 20 | "Channels": [ 21 | "4", 22 | "5", 23 | "6", 24 | "7" 25 | ], 26 | "Colors": [ 27 | "ffff00", 28 | "ff00ff", 29 | "00ff7f", 30 | "7f7fff" 31 | ], 32 | "Name": "groupB-_-_-_-_-_-_-_Yellow_4-7", 33 | "Path": "groupB-Yellow-4-7_4__4--5__5--6__6--7__7" 34 | } 35 | ], 36 | "Header": "SampleDescription***", 37 | "Images": [ 38 | { 39 | "Description": "sampleName***", 40 | "Height": 2048, 41 | "MaxLevel": 1, 42 | "Name": "i0", 43 | "Path": "images/test", 44 | "Width": 2048 45 | } 46 | ], 47 | "Layout": { 48 | "Grid": [ 49 | [ 50 | "i0" 51 | ] 52 | ] 53 | }, 54 | "Masks": [ 55 | { 56 | "Channels": [ 57 | "maskABC" 58 | ], 59 | "Colors": [ 60 | "ff0000" 61 | ], 62 | "Name": "maskABC", 63 | "Path": "maskABC" 64 | }, 65 | { 66 | "Channels": [ 67 | "maskABC" 68 | ], 69 | "Colors": [ 70 | "00ff00" 71 | ], 72 | "Name": "maskABC-0", 73 | "Path": "maskABC-0" 74 | }, 75 | { 76 | "Channels": [ 77 | "maskABC" 78 | ], 79 | "Colors": [ 80 | "0000ff" 81 | ], 82 | "Name": "maskABC-1", 83 | "Path": "maskABC-1" 84 | } 85 | ], 86 | "Rotation": 0, 87 | "Stories": [ 88 | { 89 | "Description": "", 90 | "Name": "", 91 | "Waypoints": [ 92 | { 93 | "ActiveMasks": [ 94 | "maskABC" 95 | ], 96 | "Arrows": [], 97 | "Description": "### Hello red", 98 | "Group": "group A-_-_-_Purple_0_1_2_3", 99 | "Masks": [ 100 | "maskABC" 101 | ], 102 | "Name": "groupA maskABC red", 103 | "Overlays": [], 104 | "Pan": [ 105 | 0.8851152525747915, 106 | 0.5 107 | ], 108 | "Zoom": 0.5145833333333334 109 | }, 110 | { 111 | "ActiveMasks": [ 112 | "maskABC-0" 113 | ], 114 | "Arrows": [], 115 | "Description": "### Hello green", 116 | "Group": "group A-_-_-_Purple_0_1_2_3", 117 | "Masks": [ 118 | "maskABC-0" 119 | ], 120 | "Name": "groupA maskABC green", 121 | "Overlays": [], 122 | "Pan": [ 123 | 0.8851152525747915, 124 | 0.5 125 | ], 126 | "Zoom": 0.5145833333333334 127 | }, 128 | { 129 | "ActiveMasks": [ 130 | "maskABC-1" 131 | ], 132 | "Arrows": [], 133 | "Description": "### Hello blue", 134 | "Group": "group A-_-_-_Purple_0_1_2_3", 135 | "Masks": [ 136 | "maskABC-1" 137 | ], 138 | "Name": "groupA maskABC blue", 139 | "Overlays": [], 140 | "Pan": [ 141 | 0.8851152525747915, 142 | 0.5 143 | ], 144 | "Zoom": 0.5145833333333334 145 | } 146 | ] 147 | } 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /testimages/exhibit2_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "waypoints": [ 3 | { 4 | "name": "groupA maskABC red", 5 | "text": "### Hello red", 6 | "pan": [ 7 | 0.8851152525747915, 8 | 0.5 9 | ], 10 | "zoom": 0.5145833333333334, 11 | "masks": [ 12 | 0 13 | ], 14 | "arrows": [], 15 | "overlays": [], 16 | "group": "group A-_-_-_Purple_0_1_2_3" 17 | }, 18 | { 19 | "name": "groupA maskABC green", 20 | "text": "### Hello green", 21 | "pan": [ 22 | 0.8851152525747915, 23 | 0.5 24 | ], 25 | "zoom": 0.5145833333333334, 26 | "masks": [ 27 | 1 28 | ], 29 | "arrows": [], 30 | "overlays": [], 31 | "group": "group A-_-_-_Purple_0_1_2_3" 32 | }, 33 | { 34 | "name": "groupA maskABC blue", 35 | "text": "### Hello blue", 36 | "pan": [ 37 | 0.8851152525747915, 38 | 0.5 39 | ], 40 | "zoom": 0.5145833333333334, 41 | "masks": [ 42 | 2 43 | ], 44 | "arrows": [], 45 | "overlays": [], 46 | "group": "group A-_-_-_Purple_0_1_2_3" 47 | } 48 | ], 49 | "groups": [ 50 | { 51 | "label": "group A-_-_-_Purple_0_1_2_3", 52 | "channels": [ 53 | { 54 | "color": "7f00ff", 55 | "min": 0, 56 | "max": 0.5000076295109483, 57 | "label": "zero", 58 | "id": 0 59 | }, 60 | { 61 | "color": "ffff00", 62 | "min": 0, 63 | "max": 0.5000076295109483, 64 | "label": "one", 65 | "id": 1 66 | }, 67 | { 68 | "color": "00ffff", 69 | "min": 0, 70 | "max": 0.5000076295109483, 71 | "label": "two", 72 | "id": 2 73 | }, 74 | { 75 | "color": "00ff7f", 76 | "min": 0, 77 | "max": 0.5000076295109483, 78 | "label": "three", 79 | "id": 3 80 | } 81 | ] 82 | }, 83 | { 84 | "label": "groupB-_-_-_-_-_-_-_Yellow_4-7", 85 | "channels": [ 86 | { 87 | "color": "ffff00", 88 | "min": 0, 89 | "max": 0.5000076295109483, 90 | "label": "4", 91 | "id": 4 92 | }, 93 | { 94 | "color": "ff00ff", 95 | "min": 0, 96 | "max": 0.5000076295109483, 97 | "label": "5", 98 | "id": 5 99 | }, 100 | { 101 | "color": "00ff7f", 102 | "min": 0, 103 | "max": 0.5000076295109483, 104 | "label": "6", 105 | "id": 6 106 | }, 107 | { 108 | "color": "7f7fff", 109 | "min": 0, 110 | "max": 0.5000076295109483, 111 | "label": "7", 112 | "id": 7 113 | } 114 | ] 115 | } 116 | ], 117 | "masks": [ 118 | { 119 | "label": "maskABC", 120 | "path": "./test.ome.tif", 121 | "channels": [ 122 | { 123 | "color": "ff0000", 124 | "label": "maskABC" 125 | } 126 | ] 127 | }, 128 | { 129 | "label": "maskABC", 130 | "path": "./test.ome.tif", 131 | "channels": [ 132 | { 133 | "color": "00ff00", 134 | "label": "maskABC" 135 | } 136 | ] 137 | }, 138 | { 139 | "label": "maskABC", 140 | "path": "./test.ome.tif", 141 | "channels": [ 142 | { 143 | "color": "0000ff", 144 | "label": "maskABC" 145 | } 146 | ] 147 | } 148 | ], 149 | "rotation": 0, 150 | "header": "SampleDescription***", 151 | "image": { 152 | "description": "sampleName***" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /static/image/Minerva-Author_HorizLogo_RGB.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 16 | 17 | 18 | 27 | 31 | 32 | 33 | 35 | 36 | 38 | 40 | 43 | 44 | 46 | 47 | 48 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/render_jpg.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | 3 | import itertools 4 | 5 | try: 6 | import pathlib 7 | except ImportError: 8 | import pathlib2 as pathlib 9 | 10 | import json 11 | import os 12 | 13 | 14 | def composite_channel(target, image, color, range_min, range_max): 15 | """Render _image_ in pseudocolor and composite into _target_ 16 | Args: 17 | target: Numpy float32 array containing composition target image 18 | image: Numpy array of image to render and composite 19 | color: Color as r, g, b float array, 0-1 20 | range_min: Threshold range minimum (in terms of image pixel values) 21 | range_max: Threshold range maximum (in terms of image pixel values) 22 | """ 23 | if range_min == range_max: 24 | return 25 | f_image = (image.astype("float32") - range_min) / (range_max - range_min) 26 | f_image = f_image.clip(0, 1, out=f_image) 27 | for i, component in enumerate(color): 28 | target[:, :, i] += f_image * component 29 | 30 | 31 | def _calculate_total_tiles(opener, tsize, num_levels): 32 | tiles = 0 33 | for level in range(num_levels): 34 | (nx, ny) = opener.get_level_tiles(level, tsize) 35 | tiles += nx * ny 36 | 37 | return tiles 38 | 39 | 40 | def _check_duplicate(group_path, settings, old_rows): 41 | old_settings = next( 42 | (row for row in old_rows if row["Group Path"] == group_path), {} 43 | ) 44 | return settings == old_settings 45 | 46 | 47 | def render_color_tiles( 48 | opener, 49 | output_dir, 50 | tsize, 51 | config_rows, 52 | logger, 53 | progress_callback=None, 54 | allow_cache=True, 55 | n_threads=1, 56 | thread=0, 57 | ): 58 | EXT = "jpg" 59 | 60 | for settings in config_rows: 61 | settings["Source"] = opener.path 62 | 63 | print("Processing:", str(opener.path)) 64 | 65 | output_path = pathlib.Path(output_dir) 66 | 67 | if not output_path.exists(): 68 | output_path.mkdir(parents=True) 69 | 70 | config_path = output_path / "config.json" 71 | old_rows = [] 72 | 73 | if allow_cache: 74 | 75 | if os.path.exists(config_path): 76 | with open(config_path, "r") as f: 77 | try: 78 | old_rows = json.load(f) 79 | except json.decoder.JSONDecodeError as err: 80 | print(err) 81 | 82 | with open(config_path, "w") as f: 83 | json.dump(config_rows, f) 84 | 85 | num_levels = opener.get_shape()[1] 86 | 87 | total_tiles = _calculate_total_tiles(opener, tsize, num_levels) 88 | progress = 0 89 | 90 | if num_levels < 2: 91 | logger.warning(f"Number of levels {num_levels} < 2") 92 | 93 | group_dirs = {settings["Group Path"]: settings for settings in config_rows} 94 | is_up_to_date = {g: False for g, s in group_dirs.items()} 95 | 96 | if allow_cache: 97 | is_up_to_date = { 98 | g: _check_duplicate(g, s, old_rows) for g, s in group_dirs.items() 99 | } 100 | 101 | for level in list(range(num_levels))[::-1]: 102 | 103 | (nx, ny) = opener.get_level_tiles(level, tsize) 104 | print(" level {} ({} x {})".format(level, ny, nx)) 105 | 106 | tile_list = list(itertools.product(range(0, ny), range(0, nx))) 107 | print('thread', thread, 'of', n_threads, 'creating', len(tile_list[thread::n_threads]), 'of', len(tile_list), 'tiles for level', level) 108 | 109 | for ty, tx in tile_list[thread::n_threads]: 110 | 111 | filename = "{}_{}_{}.{}".format(level, tx, ty, EXT) 112 | 113 | for settings in config_rows: 114 | 115 | group_dir = settings["Group Path"] 116 | if not (output_path / group_dir).exists(): 117 | try: 118 | (output_path / group_dir).mkdir(parents=True) 119 | except FileExistsError: 120 | pass 121 | output_file = str(output_path / group_dir / filename) 122 | 123 | # Only save file if change in config rows 124 | if not os.path.exists(output_file) or not is_up_to_date[group_dir]: 125 | try: 126 | opener.save_tile( 127 | output_file, settings, tsize, level, tx, ty 128 | ) 129 | except AttributeError as e: 130 | logger.error(f"{level} ty {ty} tx {tx}: {e}") 131 | else: 132 | logger.warning(f"Not saving tile level {level} ty {ty} tx {tx}") 133 | logger.warning( 134 | f"Path {output_file} exists with same rendering settings" 135 | ) 136 | 137 | progress += 1 138 | if progress_callback is not None: 139 | progress_callback(progress, len(config_rows) * total_tiles) 140 | -------------------------------------------------------------------------------- /src/render_png.py: -------------------------------------------------------------------------------- 1 | """ Render PNG tiles 2 | """ 3 | import io 4 | import itertools 5 | import json 6 | import os 7 | from threading import Lock 8 | 9 | import numpy as np 10 | 11 | tiff_lock = Lock() 12 | 13 | class MissingTilePNG(Exception): 14 | pass 15 | 16 | def render_tile(opener, level, tx, ty, channel_number, fmt=None): 17 | with tiff_lock: 18 | 19 | (num_channels, num_levels, width, height) = opener.get_shape() 20 | sanity_checks = [ 21 | ty <= height // opener.to_tsize(), 22 | tx <= width // opener.to_tsize(), 23 | channel_number < num_channels, 24 | level < num_levels 25 | ] 26 | img_io = None 27 | # Sanity checks 28 | if all(sanity_checks): 29 | img = opener.get_tile(num_channels, level, tx, ty, channel_number, fmt) 30 | img_io = io.BytesIO() 31 | img.save(img_io, "PNG", compress_level=1) 32 | img_io.seek(0) 33 | else: 34 | print('sanity_checks', sanity_checks, num_levels) 35 | 36 | if img_io is None: 37 | raise MissingTilePNG(f'No tile at level {level} ty {ty} tx {tx}') 38 | 39 | return img_io 40 | 41 | 42 | def mix(v1, v2, a): 43 | return v1 * (1 - a) + v2 * a 44 | 45 | 46 | def hsv2rgba(hsv_buff): 47 | K = np.array((1, 2 / 3, 1 / 3), np.float64) 48 | alpha = np.expand_dims(hsv_buff[:, :, 3], 2) 49 | hue_buff = np.repeat(hsv_buff[:, :, 0][:, :, np.newaxis], 3, axis=2) 50 | sat_buff = np.repeat(hsv_buff[:, :, 1][:, :, np.newaxis], 3, axis=2) 51 | val_buff = np.repeat(hsv_buff[:, :, 2][:, :, np.newaxis], 3, axis=2) 52 | rgb = np.clip(np.abs(np.modf(hue_buff + K)[0] * 6 - 3) - 1, 0, 1) 53 | rgb = val_buff * mix(1, rgb, sat_buff) 54 | return np.concatenate((rgb, alpha), axis=2) 55 | 56 | 57 | def spike(image, opacity=None): 58 | buff = np.zeros(image.shape + (4, ), np.float64) 59 | star_hue = 1/3 + 1/100 60 | star_sat = 1/7 + 1/1000 61 | star_val = 1/2 + 1/100 62 | buff[:, :, 0] = np.modf(image * star_hue)[0] 63 | buff[:, :, 1] = mix(0.6, 1.0, np.modf(image * star_sat)[0]) 64 | buff[:, :, 2] = mix(0.2, 0.9, np.modf(image * star_val)[0]) 65 | if not opacity: 66 | buff[:, :, 3][image != 0] = 1.0 67 | else: 68 | buff[:, :, 3][image != 0] = opacity 69 | return buff 70 | 71 | 72 | def colorize_integer(integer): 73 | return [ 74 | int(255 * v) 75 | for v in hsv2rgba(spike(np.array([[integer]], dtype=np.uint32)))[0][0] 76 | ][:3] 77 | 78 | 79 | def colorize_mask(target, image, opacity): 80 | ''' Render _image_ in pseudocolor into _target_ 81 | Args: 82 | target: Numpy uint8 array containing RGBA composition target 83 | image: Numpy integer array of image to render and composite 84 | opacity: float value (0-1) representing alpha value 85 | ''' 86 | rgba_buff = hsv2rgba(spike(image, opacity)) 87 | target[:] = np.around(255 * rgba_buff).astype(np.uint8) 88 | return target 89 | 90 | 91 | def render_u32_tiles(mask_params, tsize, logger): 92 | EXT = "png" 93 | 94 | opener = mask_params["opener"] 95 | 96 | print("Processing:", str(opener.path)) 97 | 98 | for image_params in mask_params["images"]: 99 | 100 | old_settings = {} 101 | settings = image_params["settings"] 102 | 103 | output_path = image_params["out_path"] 104 | 105 | if not output_path.exists(): 106 | output_path.mkdir(parents=True) 107 | 108 | config_path = output_path / "config.json" 109 | 110 | if os.path.exists(config_path): 111 | with open(config_path, "r") as f: 112 | try: 113 | old_settings = json.load(f) 114 | except json.decoder.JSONDecodeError as err: 115 | print(err) 116 | 117 | with open(config_path, "w") as f: 118 | json.dump(settings, f) 119 | 120 | image_params["is_up_to_date"] = settings == old_settings 121 | 122 | num_levels = opener.get_shape()[1] 123 | 124 | progress = 0 125 | 126 | if num_levels < 2: 127 | logger.warning(f"Number of levels {num_levels} < 2") 128 | 129 | for level in range(num_levels): 130 | 131 | (nx, ny) = opener.get_level_tiles(level, tsize) 132 | print(" level {} ({} x {})".format(level, ny, nx)) 133 | 134 | for ty, tx in itertools.product(range(0, ny), range(0, nx)): 135 | 136 | filename = "{}_{}_{}.{}".format(level, tx, ty, EXT) 137 | 138 | try: 139 | opener.save_mask_tiles( 140 | filename, mask_params, logger, tsize, level, tx, ty 141 | ) 142 | except AttributeError as e: 143 | logger.error(f"{level} ty {ty} tx {tx}: {e}") 144 | 145 | progress += 1 146 | for image_params in mask_params["images"]: 147 | if image_params["progress"] is not None: 148 | image_params["progress"](progress) 149 | -------------------------------------------------------------------------------- /static/image/Minerva_FinalLogo_NoText_RGB.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 59 | 60 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 111 | 115 | 119 | 123 | 127 | 131 | 135 | 139 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/story.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import csv 3 | import math 4 | import itertools 5 | import json 6 | import numpy as np 7 | import ome_types 8 | import sklearn.mixture 9 | import sys 10 | import tifffile 11 | import zarr 12 | import argparse 13 | import os 14 | 15 | def auto_threshold(img): 16 | 17 | assert img.ndim == 2 18 | 19 | yi, xi = np.floor(np.linspace(0, img.shape, 200, endpoint=False)).astype(int).T 20 | # Slice one dimension at a time. Should generally use less memory than a meshgrid. 21 | img = img[yi] 22 | img = img[:, xi] 23 | img_log = np.log(img[img > 0]) 24 | gmm = sklearn.mixture.GaussianMixture(3, max_iter=1000, tol=1e-6) 25 | gmm.fit(img_log.reshape((-1,1))) 26 | means = gmm.means_[:, 0] 27 | _, i1, i2 = np.argsort(means) 28 | mean1, mean2 = means[[i1, i2]] 29 | std1, std2 = gmm.covariances_[[i1, i2], 0, 0] ** 0.5 30 | 31 | x = np.linspace(mean1, mean2, 50) 32 | n_pdf = lambda m,s,x: np.exp(-0.5 * ((x - m) / s)**2) / (s * np.sqrt(2*np.pi)) 33 | y1 = n_pdf(mean1, std1, x) * gmm.weights_[i1] 34 | y2 = n_pdf(mean2, std2, x) * gmm.weights_[i2] 35 | 36 | lmax = mean2 + 2 * std2 37 | lmin = x[np.argmin(np.abs(y1 - y2))] 38 | if lmin >= mean2: 39 | lmin = mean2 - 2 * std2 40 | vmin = max(np.exp(lmin), img.min(), 0) 41 | vmax = min(np.exp(lmax), img.max()) 42 | 43 | return vmin, vmax 44 | 45 | def to_heuristic(has_keyword, channel_names, step_size): 46 | n_channels = len(channel_names) 47 | names = channel_names[::step_size] 48 | # Preferences from most to least important 49 | return ( 50 | len([ 51 | name for name in names 52 | if has_keyword(name) 53 | ]), 54 | int(n_channels % step_size == 0), 55 | (n_channels % step_size) 56 | ) 57 | 58 | def to_group_starts(channel_names): 59 | size_options = [3, 4, 5, 6] 60 | def has_keyword(name): 61 | return any( 62 | keyword in name.lower() 63 | for keyword in [ 'dna', 'hoechst' ] 64 | ) 65 | # most initial keywords, else most evenly divisible 66 | size_stats = sorted([ 67 | (*to_heuristic(has_keyword, channel_names, size), size) 68 | for size in size_options 69 | ], reverse=True) 70 | group_size = size_stats[0][-1] 71 | min_size = math.ceil(group_size / 2) 72 | group_starts = [] 73 | # Keywords and/or evenly spaced groups 74 | for idx, name in enumerate(channel_names): 75 | if len(group_starts) > 0: 76 | last_group_size = idx - group_starts[-1] 77 | remainder = last_group_size % group_size 78 | if last_group_size < min_size: 79 | continue 80 | if not has_keyword(name) and remainder != 0: 81 | continue 82 | # Channel index is start of new group 83 | group_starts.append(idx) 84 | return group_starts 85 | 86 | def group_channels(n_channels, channel_names): 87 | group_starts = to_group_starts(channel_names) 88 | group_iter = zip(group_starts, group_starts[1:]+[None]) 89 | channel_groups = [] 90 | for gi, pair in enumerate(group_iter): 91 | channel_range = list(range(n_channels)[slice(*pair)]) 92 | channel_groups.append([gi, channel_range]) 93 | return dict(channel_groups) 94 | 95 | 96 | def color_cycle(iterable): 97 | colors = ( 98 | 'ffffff', 'ff0000', '00ff00', '0000ff', 99 | 'ff00ff', 'ffff00', '00ffff' 100 | ) 101 | # Pair each iterable entry with a color 102 | return zip(iterable, itertools.cycle(colors)) 103 | 104 | 105 | def main(opener, channel_names, n_workers=1): 106 | 107 | try: 108 | metadata = opener.read_metadata() 109 | pixels = metadata.images[0].pixels 110 | pixel_microns = pixels.physical_size_x_quantity.to('um').m 111 | pixels_per_micron = 1/pixel_microns if pixel_microns > 0 else 0 112 | except: 113 | print(f"Unable to read metadata", file=sys.stderr) 114 | pixels_per_micron = None 115 | 116 | story = { 117 | "sample_info": { 118 | "name": "", 119 | "rotation": 0, 120 | "text": "", 121 | "pixels_per_micron": pixels_per_micron, 122 | }, 123 | "groups": [], 124 | "waypoints": [], 125 | } 126 | 127 | dtype = opener.default_dtype 128 | n_levels = opener.get_shape()[1] 129 | n_channels = opener.get_shape()[0] 130 | scale = np.iinfo(dtype).max if np.issubdtype(dtype, np.integer) else 1 131 | level = n_levels - 1 132 | 133 | def threshold(ci): 134 | img = opener.wrapper[level, :, :, 0, ci, 0] 135 | if img.min() < 0: 136 | print( 137 | f" WARNING: Ignoring negative pixel values in channel {ci}", 138 | file=sys.stderr, 139 | ) 140 | res = auto_threshold(img) 141 | return res 142 | 143 | with concurrent.futures.ThreadPoolExecutor(n_workers) as pool: 144 | thresholds = list(pool.map(threshold, range(n_channels))) 145 | 146 | channel_groups = group_channels(n_channels, channel_names) 147 | output_groups = {} 148 | for gi, channel_range in channel_groups.items(): 149 | group = {} 150 | for ci, color in color_cycle(channel_range): 151 | vmin, vmax = thresholds[ci] 152 | group[ci] = { 153 | "color": color, 154 | "id": ci, 155 | "label": channel_names[ci], 156 | "min": vmin / scale, 157 | "max": vmax / scale, 158 | } 159 | output_groups[gi] = group 160 | 161 | auto_groups = [ 162 | output_groups[gi] for gi in 163 | sorted(channel_groups.keys()) 164 | ] 165 | 166 | for gi, val in enumerate(auto_groups): 167 | story["groups"].append({ 168 | "label": f"Group {gi+1}", 169 | "channels": [ 170 | v for _,v in sorted(val.items()) 171 | ] 172 | }) 173 | 174 | return story 175 | -------------------------------------------------------------------------------- /src/storyexport.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import re 4 | import sys 5 | from distutils import file_util 6 | from distutils.errors import DistutilsFileError 7 | from create_vega import ( 8 | create_vega_csv, 9 | modify_default, 10 | modify_matrix, 11 | ) 12 | 13 | 14 | def label_to_dir(s, empty="0"): 15 | replaced = re.sub("[^0-9a-zA-Z _-]+", "", s).strip() 16 | replaced = replaced.replace(" ", "_") 17 | replaced = replaced.replace("_", "-") 18 | replaced = re.sub("-+", "-", replaced) 19 | return empty if replaced == "" else replaced 20 | 21 | 22 | def deduplicate(data_name, data_dict, data_dir): 23 | """ 24 | Return a local path for given data path 25 | Args: 26 | data_name: the basename of the target file 27 | data_dict: the existing mapping of local paths 28 | data_dir: the full path of the destination directory 29 | """ 30 | n_dups = 0 31 | basename = data_name 32 | local_path = os.path.join(data_dir, basename) 33 | while local_path in data_dict.values(): 34 | root, ext = os.path.splitext(basename) 35 | local_path = os.path.join(data_dir, f"{root}-{n_dups}{ext}") 36 | n_dups += 1 37 | return local_path 38 | 39 | 40 | def lookup_vis_data_type(waypoints, lookup_path): 41 | """ 42 | Return visualization type for given data path 43 | """ 44 | lookup_dict = dict() 45 | for waypoint in waypoints: 46 | for vis in ["VisScatterplot", "VisMatrix"]: 47 | if vis in waypoint: 48 | data_path = waypoint[vis]["data"] 49 | lookup_dict[data_path] = vis 50 | if "VisBarChart" in waypoint: 51 | data_path = waypoint["VisBarChart"] 52 | lookup_dict[data_path] = "VisBarChart" 53 | 54 | return lookup_dict.get(lookup_path, None) 55 | 56 | 57 | def deduplicate_data(waypoints, data_dir): 58 | """ 59 | Map filesystem paths to local data paths 60 | Args: 61 | waypoints: list of dicts containing optional VisData keys 62 | data_dir: the full path of the destination directory 63 | """ 64 | data_dict = dict() 65 | for waypoint in waypoints: 66 | for vis in ["VisScatterplot", "VisMatrix"]: 67 | if vis in waypoint: 68 | data_path = waypoint[vis]["data"] 69 | data_name = os.path.basename(data_path) 70 | data_dict[data_path] = deduplicate(data_name, data_dict, data_dir) 71 | 72 | if "VisBarChart" in waypoint: 73 | data_path = waypoint["VisBarChart"] 74 | data_name = os.path.basename(data_path) 75 | data_dict[data_path] = deduplicate(data_name, data_dict, data_dir) 76 | 77 | return data_dict 78 | 79 | 80 | def deduplicate_dicts( 81 | dicts, data_dir="", in_key="label", out_key="label", is_dir=False 82 | ): 83 | """ 84 | Map dictionaries by key to unique labels 85 | Args: 86 | dicts: list of dicts containing input key and output key 87 | data_dir: the full path of the destination directory 88 | in_key: used for key of output dictionary 89 | out_key: used for values of output dictionary 90 | is_dir: set true if unique labels must be directories 91 | """ 92 | data_dict = dict() 93 | for d in dicts: 94 | data_in = d[in_key] 95 | data_name = label_to_dir(d[out_key]) if is_dir else d[out_key] 96 | data_dict[data_in] = deduplicate(data_name, data_dict, data_dir) 97 | 98 | return data_dict 99 | 100 | 101 | def dedup_index_to_label(dicts): 102 | dicts_with_index = [ 103 | {"index": i, "label": d["label"]} for (i, d) in enumerate(dicts) 104 | ] 105 | return deduplicate_dicts(dicts_with_index, "", "index", "label", False) 106 | 107 | 108 | def dedup_index_to_path(dicts, data_dir=""): 109 | dicts_with_index = [ 110 | {"index": i, "label": d["label"]} for (i, d) in enumerate(dicts) 111 | ] 112 | return deduplicate_dicts(dicts_with_index, data_dir, "index", "label", True) 113 | 114 | 115 | def dedup_label_to_path(dicts, data_dir=""): 116 | return deduplicate_dicts(dicts, data_dir, "label", "label", True) 117 | 118 | 119 | def mask_path_from_index(mask_data, index, data_dir=""): 120 | return dedup_index_to_path(mask_data, data_dir)[index] 121 | 122 | 123 | def mask_label_from_index(mask_data, index): 124 | return dedup_index_to_label(mask_data)[index] 125 | 126 | 127 | def group_path_from_label(group_data, label, data_dir=""): 128 | return dedup_label_to_path(group_data, data_dir)[label] 129 | 130 | 131 | def get_current_dir(): 132 | return os.path.dirname(os.path.abspath(sys.argv[0])) 133 | 134 | 135 | def get_story_dir(): 136 | try: 137 | # If running pyinstaller executable, _MEIPASS will contain path to the data directory in tmp 138 | story_dir = os.path.join(sys._MEIPASS, "minerva-story") 139 | except Exception: 140 | # Not running pyinstaller executable; minerva-story should exist in parent directory 141 | story_dir = os.path.join(get_current_dir(), "..", "minerva-story") 142 | 143 | return story_dir 144 | 145 | 146 | def create_story_base(title, waypoints, masks, folder=""): 147 | """ 148 | Creates a new minerva-story instance under subfolder named title. The subfolder will be created. 149 | Args: 150 | title: Story title, the subfolder will be named 151 | waypoints: List of waypoints with visData and Masks 152 | masks: List of masks with names and paths 153 | folder: Parent path to contain folders 154 | """ 155 | out_dir = get_story_folders(title, folder, create=True)[0] 156 | export_dir = os.path.join(folder, title) 157 | 158 | story_dir = get_story_dir() 159 | data_dir = os.path.join(export_dir, "data") 160 | os.makedirs(data_dir, exist_ok=True) 161 | os.makedirs(out_dir, exist_ok=True) 162 | 163 | try: 164 | file_util.copy_file(os.path.join(story_dir, "index.html"), export_dir) 165 | except DistutilsFileError as e: 166 | print(f"Cannot copy index.html from {story_dir}") 167 | print(e) 168 | 169 | vis_path_dict = deduplicate_data(waypoints, data_dir) 170 | 171 | for i in range(len(masks)): 172 | path_i = mask_path_from_index(masks, i, out_dir) 173 | os.makedirs(path_i, exist_ok=True) 174 | 175 | for in_path, out_path in vis_path_dict.items(): 176 | if pathlib.Path(in_path).suffix in [".csv"]: 177 | try: 178 | copy_vega_csv(waypoints, in_path, out_path) 179 | except DistutilsFileError as e: 180 | print(f"Cannot copy {in_path}") 181 | print(e) 182 | else: 183 | print(f"Refusing to copy non-csv infovis: {in_path}") 184 | 185 | 186 | def copy_vega_csv(waypoint_data, in_path, out_path): 187 | vis_fn_dict = {"VisMatrix": modify_matrix} 188 | vis_type = lookup_vis_data_type(waypoint_data, in_path) 189 | fn = vis_fn_dict.get(vis_type, modify_default) 190 | return create_vega_csv(in_path, out_path, fn) 191 | 192 | 193 | def get_story_folders(title, folder="", create=False): 194 | """ 195 | Gets paths to folders where image tiles, json, dat-file and log file must be saved. 196 | Args: 197 | title: Story title 198 | folder: Parent path to contain folders 199 | create: Whether folders should be created 200 | 201 | Returns: Tuple of images dir, json config dir, json save dir, log dir 202 | """ 203 | images_folder = os.path.join(folder, title, "images") 204 | out_dir = os.path.join(images_folder, title) 205 | 206 | out_json_config = os.path.join(folder, title, "exhibit.json") 207 | 208 | out_json_save = os.path.join(folder, title + ".json") 209 | 210 | # After version 1.6.0 use .story.json, keep support for existing files 211 | if not os.path.exists(out_json_save): 212 | out_json_save = os.path.join(folder, title + ".story.json") 213 | 214 | out_log = os.path.join(folder, title + ".log") 215 | 216 | if create: 217 | os.makedirs(images_folder, exist_ok=True) 218 | 219 | return out_dir, out_json_config, out_json_save, out_log 220 | -------------------------------------------------------------------------------- /src/exhibit.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import logging 4 | import os 5 | import pathlib 6 | from distutils.errors import DistutilsFileError 7 | from json.decoder import JSONDecodeError 8 | 9 | import tifffile as tiff 10 | from tifffile.tifffile import TiffFileError 11 | 12 | from app import Opener, extract_story_json_stem, make_channels, make_groups, make_stories 13 | from storyexport import deduplicate_data, copy_vega_csv 14 | 15 | 16 | def json_to_html(exhibit): 17 | return ( 18 | '\n' 19 | '\n' 20 | '\n' 21 | ' \n' 22 | ' \n' 23 | ' \n' 24 | ' \n' 25 | ' \n' 26 | '\n' 27 | ' \n' 28 | '
\n' 29 | ' \n' 30 | ' \n' 31 | ' \n' 38 | ' \n' 39 | '\n' 40 | '' 41 | ) 42 | 43 | 44 | def copy_vis_csv_files(waypoint_data, json_path, output_dir, vis_dir): 45 | input_dir = json_path.parent 46 | author_stem = extract_story_json_stem(json_path) 47 | vis_data_dir = vis_dir if vis_dir else f"{author_stem}-story-infovis" 48 | 49 | vis_path_dict_in = deduplicate_data(waypoint_data, input_dir / vis_data_dir) 50 | vis_path_dict_out = deduplicate_data(waypoint_data, output_dir / "data") 51 | 52 | if not (output_dir / "data").exists(): 53 | (output_dir / "data").mkdir(parents=True) 54 | 55 | # Copy the visualization csv files to a "data" directory 56 | for key_path, in_path in vis_path_dict_in.items(): 57 | if pathlib.Path(in_path).suffix in [".csv"]: 58 | try: 59 | out_path = vis_path_dict_out[key_path] 60 | # Modify matrix CSV files if needed 61 | copy_vega_csv(waypoint_data, in_path, out_path) 62 | except DistutilsFileError as e: 63 | print(f"Cannot copy {in_path}") 64 | print(e) 65 | else: 66 | print(f"Refusing to copy non-csv infovis: {in_path}") 67 | 68 | 69 | def set_if_not_none(exhibit, key, value): 70 | if value is not None: 71 | exhibit[key] = value 72 | return exhibit 73 | 74 | 75 | def make_exhibit_config(in_shape, root_url, saved, rgba): 76 | 77 | levels = in_shape['levels'] 78 | height = in_shape['height'] 79 | width = in_shape['width'] 80 | waypoint_data = saved["waypoints"] 81 | vis_path_dict = deduplicate_data(waypoint_data, "data") 82 | 83 | exhibit = { 84 | "Images": [ 85 | { 86 | "Name": "i0", 87 | "Description": saved["sample_info"]["name"], 88 | "Path": root_url if root_url else ".", 89 | "Width": width, 90 | "Height": height, 91 | "MaxLevel": levels - 1, 92 | } 93 | ], 94 | "Header": saved["sample_info"]["text"], 95 | "Rotation": saved["sample_info"]["rotation"], 96 | "Layout": {"Grid": [["i0"]]}, 97 | "Stories": make_stories(waypoint_data, [], vis_path_dict), 98 | "Channels": list(make_channels(saved["groups"], rgba)), 99 | "Groups": list(make_groups(saved["groups"])), 100 | "Masks": [], 101 | } 102 | first_group = saved.get("first_group", None) 103 | default_group = saved.get("default_group", None) 104 | first_viewport = saved.get("first_viewport", None) 105 | pixels_per_micron = saved["sample_info"].get("pixels_per_micron", None) 106 | exhibit = set_if_not_none(exhibit, "PixelsPerMicron", pixels_per_micron) 107 | exhibit = set_if_not_none(exhibit, "FirstViewport", first_viewport) 108 | exhibit = set_if_not_none(exhibit, "DefaultGroup", default_group) 109 | exhibit = set_if_not_none(exhibit, "FirstGroup", first_group) 110 | 111 | return exhibit 112 | 113 | 114 | def main(ome_tiff, author_json, output_dir, root_url, vis_dir, force=False): 115 | FORMATTER = logging.Formatter( 116 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 117 | ) 118 | logger = logging.getLogger("app") 119 | ch = logging.StreamHandler() 120 | ch.setLevel(logging.DEBUG) 121 | ch.setFormatter(FORMATTER) 122 | logger.addHandler(ch) 123 | 124 | one_tile = None 125 | opener = None 126 | saved = None 127 | 128 | try: 129 | opener = Opener(ome_tiff) 130 | except (FileNotFoundError, TiffFileError) as e: 131 | logger.error(e) 132 | logger.error(f"Invalid ome-tiff file: cannot parse {ome_tiff}") 133 | return 134 | 135 | in_shape = None 136 | # Treat as static tiff 137 | if opener.reader is None: 138 | print('Opening single tile plain .tif') 139 | one_tile = tiff.imread(ome_tiff) 140 | in_shape = { 141 | 'levels': 1, 142 | 'height': one_tile.shape[0], 143 | 'width': one_tile.shape[1], 144 | } 145 | else: 146 | (levels, width, height) = opener.get_shape()[1:] 147 | in_shape = { 148 | 'levels': levels, 149 | 'height': height, 150 | 'width': width, 151 | } 152 | 153 | try: 154 | with open(author_json) as json_file: 155 | saved = json.load(json_file) 156 | except (FileNotFoundError, JSONDecodeError, KeyError) as e: 157 | logger.error(e) 158 | logger.error(f"Invalid save file: cannot parse {author_json}") 159 | return 160 | 161 | if not force and os.path.exists(output_dir): 162 | logger.error(f"Refusing to overwrite output directory {output_dir}") 163 | return 164 | elif force and os.path.exists(output_dir): 165 | logger.warning(f"Writing to existing output directory {output_dir}") 166 | 167 | if not output_dir.exists(): 168 | output_dir.mkdir(parents=True) 169 | 170 | rgba = opener.rgba 171 | exhibit_config = make_exhibit_config(in_shape, root_url, saved, rgba) 172 | copy_vis_csv_files(saved["waypoints"], author_json, output_dir, vis_dir) 173 | 174 | with open(output_dir / "exhibit.json", "w") as wf: 175 | json.dump(exhibit_config, wf) 176 | 177 | with open(output_dir / "index.html", "w") as wf: 178 | exhibit_string = json.dumps(exhibit_config) 179 | wf.write(json_to_html(exhibit_string)) 180 | 181 | 182 | if __name__ == "__main__": 183 | 184 | parser = argparse.ArgumentParser() 185 | parser.add_argument( 186 | "ome_tiff", 187 | metavar="ome_tiff", 188 | type=pathlib.Path, 189 | help="Input path to OME-TIFF with all channel groups", 190 | ) 191 | parser.add_argument( 192 | "author_json", 193 | metavar="author_json", 194 | type=pathlib.Path, 195 | help="Input Minerva Author save file with channel configuration", 196 | ) 197 | parser.add_argument( 198 | "output_dir", 199 | metavar="output_dir", 200 | type=pathlib.Path, 201 | help="Output directory for exhibit and rendered JPEG pyramid", 202 | ) 203 | parser.add_argument( 204 | "--url", 205 | metavar="url", 206 | default=None, 207 | help="URL to planned hosting location of rendered JPEG pyramid", 208 | ) 209 | parser.add_argument( 210 | "--vis", 211 | metavar="vis", 212 | type=pathlib.Path, 213 | default=None, 214 | help="Input data visualization directory (default constructed from author .json)", 215 | ) 216 | parser.add_argument("--force", help="Overwrite output", action="store_true") 217 | args = parser.parse_args() 218 | 219 | ome_tiff = args.ome_tiff 220 | author_json = args.author_json 221 | output_dir = args.output_dir 222 | root_url = args.url 223 | vis_dir = args.vis 224 | force = args.force 225 | 226 | main(ome_tiff, author_json, output_dir, root_url, vis_dir, force) 227 | -------------------------------------------------------------------------------- /src/pyramid_assemble.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | 3 | import argparse 4 | import concurrent.futures 5 | import io 6 | import itertools 7 | import os 8 | import pathlib 9 | import re 10 | import struct 11 | import sys 12 | import uuid 13 | 14 | import numpy as np 15 | import skimage.transform 16 | import tifffile 17 | import zarr 18 | 19 | # This API is apparently changing in skimage 1.0 but it's not clear to 20 | # me what the replacement will be, if any. We'll explicitly import 21 | # this so it will break loudly if someone tries this with skimage 1.0. 22 | try: 23 | from skimage.util.dtype import _convert as dtype_convert 24 | except ImportError: 25 | from skimage.util.dtype import convert as dtype_convert 26 | 27 | 28 | def preduce(coords, img_in, img_out, is_mask): 29 | (iy1, ix1), (iy2, ix2) = coords 30 | (oy1, ox1), (oy2, ox2) = np.array(coords) // 2 31 | if is_mask: 32 | tile = img_in[iy1:iy2:2, ix1:ix2:2] 33 | else: 34 | tile = skimage.img_as_float32(img_in[iy1:iy2, ix1:ix2]) 35 | tile = skimage.transform.downscale_local_mean(tile, (2, 2)) 36 | tile = dtype_convert(tile, img_out.dtype) 37 | img_out[oy1:oy2, ox1:ox2] = tile 38 | 39 | 40 | def imsave(path, img, tile_size, **kwargs): 41 | tifffile.imwrite( 42 | path, 43 | img, 44 | bigtiff=True, 45 | append=True, 46 | tile=(tile_size, tile_size), 47 | metadata=None, 48 | **kwargs, 49 | ) 50 | 51 | 52 | def format_shape(shape): 53 | return "%dx%d" % (shape[1], shape[0]) 54 | 55 | 56 | def construct_xml(filename, shapes, num_channels, ome_dtype, pixel_size=1): 57 | img_uuid = uuid.uuid4().urn 58 | ifd = 0 59 | xml = io.StringIO() 60 | xml.write('') 61 | xml.write( 62 | ( 63 | '' 68 | ).format(uuid=img_uuid) 69 | ) 70 | for level, shape in enumerate(shapes): 71 | if level == 0: 72 | psize_xml = ( 73 | 'PhysicalSizeX="{0}" PhysicalSizeXUnit="\u00b5m"' 74 | ' PhysicalSizeY="{0}" PhysicalSizeYUnit="\u00b5m"'.format(pixel_size) 75 | ) 76 | else: 77 | psize_xml = "" 78 | xml.write(''.format(level)) 79 | xml.write( 80 | ( 81 | '' 84 | ).format( 85 | level=level, 86 | psize_xml=psize_xml, 87 | num_channels=num_channels, 88 | sizex=shape[1], 89 | sizey=shape[0], 90 | ome_dtype=ome_dtype, 91 | ) 92 | ) 93 | for channel in range(num_channels): 94 | xml.write( 95 | ( 96 | '' 99 | ).format(level=level, channel=channel) 100 | ) 101 | for channel in range(num_channels): 102 | xml.write( 103 | ( 104 | '' 106 | '{uuid}' 107 | "" 108 | ).format(channel=channel, ifd=ifd, filename=filename, uuid=img_uuid) 109 | ) 110 | ifd += 1 111 | if level == 0: 112 | for channel in range(num_channels): 113 | xml.write( 114 | ''.format( 115 | channel=channel 116 | ) 117 | ) 118 | xml.write("") 119 | xml.write("") 120 | xml.write("") 121 | xml_bytes = xml.getvalue().encode("utf-8") + b"\x00" 122 | return xml_bytes 123 | 124 | 125 | def patch_ometiff_xml(path, xml_bytes): 126 | with open(path, "rb+") as f: 127 | f.seek(0, io.SEEK_END) 128 | xml_offset = f.tell() 129 | f.write(xml_bytes) 130 | f.seek(0) 131 | ifd_block = f.read(500) 132 | match = re.search(b"!!xml!!\x00", ifd_block) 133 | if match is None: 134 | raise RuntimeError("Did not find placeholder string in IFD") 135 | f.seek(match.start() - 8) 136 | f.write(struct.pack(" {})".format( 220 | level + 2, format_shape(shape_in), format_shape(shape_out) 221 | ) 222 | ) 223 | 224 | ty = np.array(range(0, shape_in[0], tile_size)) 225 | tx = np.array(range(0, shape_in[1], tile_size)) 226 | coords = list( 227 | zip( 228 | itertools.product(ty, tx), 229 | itertools.product(ty + tile_size, tx + tile_size), 230 | ) 231 | ) 232 | img_out = np.empty(shape_out, dtype) 233 | 234 | for c in range(num_channels): 235 | 236 | tiff = tifffile.TiffFile(out_path) 237 | page = level * num_channels + c 238 | img_in = zarr.open(tiff.aszarr(key=page), mode="r") 239 | for i, _ in enumerate( 240 | executor.map( 241 | preduce, 242 | coords, 243 | itertools.repeat(img_in), 244 | itertools.repeat(img_out), 245 | itertools.repeat(is_mask), 246 | ) 247 | ): 248 | percent = int((i + 1) / len(coords) * 100) 249 | if i % 20 == 0 or percent == 100: 250 | print(f"\r {c+1}: {percent}%", end="") 251 | sys.stdout.flush() 252 | tiff.close() 253 | imsave(out_path, img_out, tile_size) 254 | print() 255 | 256 | print() 257 | 258 | if dtype == np.uint32: 259 | if is_mask: 260 | ome_dtype = "uint32" 261 | else: 262 | print( 263 | "uint32 images are only supported in --mask mode. Please contact the authors if you need support for intensity-based uint32 images." 264 | ) 265 | sys.exit(1) 266 | elif dtype == np.uint16: 267 | ome_dtype = "uint16" 268 | elif dtype == np.uint8: 269 | ome_dtype = "uint8" 270 | else: 271 | print("can't handle dtype: %s" % dtype) 272 | sys.exit(1) 273 | 274 | xml = construct_xml( 275 | os.path.basename(out_path), shapes, num_channels, ome_dtype, pixel_size 276 | ) 277 | patch_ometiff_xml(out_path, xml) 278 | 279 | 280 | if __name__ == "__main__": 281 | 282 | parser = argparse.ArgumentParser() 283 | parser.add_argument( 284 | "in_paths", 285 | metavar="input.tif", 286 | type=pathlib.Path, 287 | nargs="+", 288 | help="List of TIFF files to combine. All images must have the same dimensions and pixel type.", 289 | ) 290 | parser.add_argument( 291 | "out_path", 292 | metavar="output.tif", 293 | type=pathlib.Path, 294 | help="Output filename. Script will exit immediately if file exists.", 295 | ) 296 | parser.add_argument( 297 | "--pixel-size", 298 | metavar="SIZE", 299 | type=float, 300 | default=1.0, 301 | help="size in microns; default is 1.0", 302 | ) 303 | parser.add_argument( 304 | "--mask", 305 | action="store_true", 306 | default=False, 307 | help="adjust processing for label mask or binary mask images (currently just switch to nearest-neighbor downsampling)", 308 | ) 309 | args = parser.parse_args() 310 | 311 | in_paths = args.in_paths 312 | out_path = args.out_path 313 | is_mask = args.mask 314 | pixel_size = args.pixel_size 315 | 316 | main(in_paths, out_path, is_mask, pixel_size) 317 | -------------------------------------------------------------------------------- /src/render.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import logging 4 | import os 5 | import pathlib 6 | import threading 7 | from distutils import file_util 8 | from distutils.errors import DistutilsFileError 9 | from json.decoder import JSONDecodeError 10 | 11 | import numpy as np 12 | from PIL import Image 13 | import tifffile as tiff 14 | from matplotlib import colors 15 | from tifffile.tifffile import TiffFileError 16 | 17 | from thumbnail import find_group_tiles 18 | from thumbnail import merge_tiles_and_save_image 19 | from app import Opener, extract_story_json_stem, make_channels, make_groups, make_rows, make_stories 20 | from storyexport import deduplicate_data, copy_vega_csv 21 | from render_jpg import render_color_tiles, composite_channel 22 | 23 | 24 | def json_to_html(exhibit): 25 | return ( 26 | '\n' 27 | '\n' 28 | '\n' 29 | ' \n' 30 | ' \n' 31 | ' \n' 32 | ' \n' 33 | ' \n' 34 | '\n' 35 | ' \n' 36 | '
\n' 37 | ' \n' 38 | ' \n' 39 | ' \n' 46 | ' \n' 47 | '\n' 48 | '' 49 | ) 50 | 51 | 52 | def render(opener, saved, output_dir, rgba, n_threads, logger): 53 | 54 | threads = [] 55 | 56 | print(f'Using {n_threads} threads') 57 | 58 | config_rows = list(make_rows(saved["groups"], rgba)) 59 | 60 | for thread in range(n_threads): 61 | th_args = (opener, output_dir, opener.to_tsize(), config_rows, logger, None, False) 62 | th_kwargs = {'thread': thread,'n_threads': n_threads} 63 | th = threading.Thread(target=render_color_tiles, args=th_args, kwargs=th_kwargs) 64 | threads.append(th) 65 | 66 | for th in threads: 67 | th.start() 68 | 69 | for th in threads: 70 | th.join() 71 | 72 | print(f'{n_threads} threads complete') 73 | 74 | def copy_vis_csv_files(waypoint_data, json_path, output_dir, vis_dir): 75 | input_dir = json_path.parent 76 | author_stem = extract_story_json_stem(json_path) 77 | vis_data_dir = vis_dir if vis_dir else f"{author_stem}-story-infovis" 78 | 79 | vis_path_dict_in = deduplicate_data(waypoint_data, input_dir / vis_data_dir) 80 | vis_path_dict_out = deduplicate_data(waypoint_data, output_dir / "data") 81 | 82 | if not (output_dir / "data").exists(): 83 | (output_dir / "data").mkdir(parents=True) 84 | 85 | # Copy the visualization csv files to a "data" directory 86 | for key_path, in_path in vis_path_dict_in.items(): 87 | if pathlib.Path(in_path).suffix in [".csv"]: 88 | try: 89 | out_path = vis_path_dict_out[key_path] 90 | # Modify matrix CSV files if needed 91 | copy_vega_csv(waypoint_data, in_path, out_path) 92 | except DistutilsFileError as e: 93 | print(f"Cannot copy {in_path}") 94 | print(e) 95 | else: 96 | print(f"Refusing to copy non-csv infovis: {in_path}") 97 | 98 | 99 | def set_if_not_none(exhibit, key, value): 100 | if value is not None: 101 | exhibit[key] = value 102 | return exhibit 103 | 104 | 105 | def make_exhibit_config(in_shape, root_url, saved, rgba): 106 | 107 | levels = in_shape['levels'] 108 | height = in_shape['height'] 109 | width = in_shape['width'] 110 | waypoint_data = saved["waypoints"] 111 | vis_path_dict = deduplicate_data(waypoint_data, "data") 112 | 113 | exhibit = { 114 | "Images": [ 115 | { 116 | "Name": "i0", 117 | "Description": saved["sample_info"]["name"], 118 | "Path": root_url if root_url else ".", 119 | "Width": width, 120 | "Height": height, 121 | "MaxLevel": levels - 1, 122 | } 123 | ], 124 | "Header": saved["sample_info"]["text"], 125 | "Rotation": saved["sample_info"]["rotation"], 126 | "Layout": {"Grid": [["i0"]]}, 127 | "Stories": make_stories(waypoint_data, [], vis_path_dict), 128 | "Channels": list(make_channels(saved["groups"], rgba)), 129 | "Groups": list(make_groups(saved["groups"])), 130 | "Masks": [], 131 | } 132 | first_group = saved.get("first_group", None) 133 | default_group = saved.get("default_group", None) 134 | first_viewport = saved.get("first_viewport", None) 135 | pixels_per_micron = saved["sample_info"].get("pixels_per_micron", None) 136 | exhibit = set_if_not_none(exhibit, "PixelsPerMicron", pixels_per_micron) 137 | exhibit = set_if_not_none(exhibit, "FirstViewport", first_viewport) 138 | exhibit = set_if_not_none(exhibit, "DefaultGroup", default_group) 139 | exhibit = set_if_not_none(exhibit, "FirstGroup", first_group) 140 | 141 | return exhibit 142 | 143 | 144 | def to_one_tile(one_tile, settings): 145 | 146 | (height, width) = one_tile.shape[:2] 147 | target = np.zeros((height, width, 3), np.float32) 148 | 149 | for i, (marker, color, start, end) in enumerate( 150 | zip( 151 | settings["Channel Number"], 152 | settings["Color"], 153 | settings["Low"], 154 | settings["High"], 155 | ) 156 | ): 157 | tile = one_tile[:, :, int(marker)] 158 | 159 | if np.issubdtype(tile.dtype, np.unsignedinteger): 160 | iinfo = np.iinfo(tile.dtype) 161 | start *= iinfo.max 162 | end *= iinfo.max 163 | 164 | composite_channel( 165 | target, tile, colors.to_rgb(color), float(start), float(end) 166 | ) 167 | 168 | np.clip(target, 0, 1, out=target) 169 | target_u8 = np.rint(target * 255).astype(np.uint8) 170 | return Image.frombytes("RGB", target.T.shape[1:], target_u8.tobytes()) 171 | 172 | 173 | def render_one_tile(one_tile, output_dir, config_rows): 174 | print(" level {} ({} x {})".format(0, 0, 0)) 175 | filename = "{}_{}_{}.{}".format(0, 0, 0, "jpg") 176 | 177 | output_path = pathlib.Path(output_dir) 178 | if not output_path.exists(): 179 | output_path.mkdir(parents=True) 180 | 181 | for settings in config_rows: 182 | group_dir = settings["Group Path"] 183 | if not (output_path / group_dir).exists(): 184 | (output_path / group_dir).mkdir(parents=True) 185 | output_file = str(output_path / group_dir / filename) 186 | img = to_one_tile(one_tile, settings) 187 | img.save(output_file, quality=85) 188 | 189 | 190 | def main(ome_tiff, author_json, output_dir, root_url, vis_dir, n_threads, force=False): 191 | FORMATTER = logging.Formatter( 192 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 193 | ) 194 | logger = logging.getLogger("app") 195 | ch = logging.StreamHandler() 196 | ch.setLevel(logging.DEBUG) 197 | ch.setFormatter(FORMATTER) 198 | logger.addHandler(ch) 199 | 200 | one_tile = None 201 | opener = None 202 | saved = None 203 | 204 | try: 205 | opener = Opener(ome_tiff) 206 | except (FileNotFoundError, TiffFileError) as e: 207 | logger.error(e) 208 | logger.error(f"Invalid ome-tiff file: cannot parse {ome_tiff}") 209 | return 210 | 211 | in_shape = None 212 | # Treat as static tiff 213 | if opener.reader is None: 214 | print('Opening single tile plain .tif') 215 | one_tile = tiff.imread(ome_tiff) 216 | in_shape = { 217 | 'levels': 1, 218 | 'height': one_tile.shape[0], 219 | 'width': one_tile.shape[1], 220 | } 221 | else: 222 | (levels, width, height) = opener.get_shape()[1:] 223 | in_shape = { 224 | 'levels': levels, 225 | 'height': height, 226 | 'width': width, 227 | } 228 | 229 | try: 230 | with open(author_json) as json_file: 231 | saved = json.load(json_file) 232 | except (FileNotFoundError, JSONDecodeError, KeyError) as e: 233 | logger.error(e) 234 | logger.error(f"Invalid save file: cannot parse {author_json}") 235 | return 236 | 237 | if not force and os.path.exists(output_dir): 238 | logger.error(f"Refusing to overwrite output directory {output_dir}") 239 | return 240 | elif force and os.path.exists(output_dir): 241 | logger.warning(f"Writing to existing output directory {output_dir}") 242 | 243 | if not output_dir.exists(): 244 | output_dir.mkdir(parents=True) 245 | 246 | rgba = opener.rgba 247 | exhibit_config = make_exhibit_config(in_shape, root_url, saved, rgba) 248 | copy_vis_csv_files(saved["waypoints"], author_json, output_dir, vis_dir) 249 | 250 | with open(output_dir / "exhibit.json", "w") as wf: 251 | json.dump(exhibit_config, wf) 252 | 253 | with open(output_dir / "index.html", "w") as wf: 254 | exhibit_string = json.dumps(exhibit_config) 255 | wf.write(json_to_html(exhibit_string)) 256 | 257 | if opener.reader is None: 258 | config_rows = list(make_rows(saved["groups"], rgba)) 259 | render_one_tile(one_tile, output_dir, config_rows) 260 | else: 261 | render(opener, saved, output_dir, rgba, n_threads, logger) 262 | 263 | # Render thumbnail 264 | groups = exhibit_config["Groups"] 265 | if len(groups) > 0: 266 | group = exhibit_config.get("FirstGroup", groups[0]["Name"]) 267 | tiles = find_group_tiles(output_dir, output_dir / "exhibit.json", group) 268 | merge_tiles_and_save_image(output_dir, tiles) 269 | 270 | 271 | if __name__ == "__main__": 272 | 273 | parser = argparse.ArgumentParser() 274 | parser.add_argument( 275 | "ome_tiff", 276 | metavar="ome_tiff", 277 | type=pathlib.Path, 278 | help="Input path to OME-TIFF with all channel groups", 279 | ) 280 | parser.add_argument( 281 | "author_json", 282 | metavar="author_json", 283 | type=pathlib.Path, 284 | help="Input Minerva Author save file with channel configuration", 285 | ) 286 | parser.add_argument( 287 | "output_dir", 288 | metavar="output_dir", 289 | type=pathlib.Path, 290 | help="Output directory for exhibit and rendered JPEG pyramid", 291 | ) 292 | parser.add_argument( 293 | "--threads", 294 | type=int, 295 | default=1, 296 | metavar="threads", 297 | help="Number of threads to use rendering the JPEG pyramid", 298 | ) 299 | parser.add_argument( 300 | "--url", 301 | metavar="url", 302 | default=None, 303 | help="URL to planned hosting location of rendered JPEG pyramid", 304 | ) 305 | parser.add_argument( 306 | "--vis", 307 | metavar="vis", 308 | type=pathlib.Path, 309 | default=None, 310 | help="Input data visualization directory (default constructed from author .json)", 311 | ) 312 | parser.add_argument("--force", help="Overwrite output", action="store_true") 313 | args = parser.parse_args() 314 | 315 | ome_tiff = args.ome_tiff 316 | author_json = args.author_json 317 | output_dir = args.output_dir 318 | n_threads = args.threads 319 | root_url = args.url 320 | vis_dir = args.vis 321 | force = args.force 322 | 323 | main(ome_tiff, author_json, output_dir, root_url, vis_dir, n_threads, force) 324 | -------------------------------------------------------------------------------- /testcharts/scatterplot.csv: -------------------------------------------------------------------------------- 1 | clust_ID,X_position,Y_position,KERATIN,CD45 2 | 4,15.98013245,21.61589404,5.88476023,5.376504048 3 | 4,7.323741007,440.5611511,5.281449278,6.773302599 4 | 4,9.580645161,471.4222874,5.930633709,5.7516226 5 | 3,8.242424242,551.5656566,5.532910275,7.467347979 6 | 3,8.917647059,571.4235294,5.400130421,7.226769281 7 | 4,8.102564103,601.0940171,5.453328482,5.887527519 8 | 4,8.361581921,633.1016949,5.509960007,5.411646052 9 | 4,7.584210526,755.2473684,5.865647682,6.810415145 10 | 4,8.728323699,775.5606936,5.819980011,6.111313527 11 | 4,4.482758621,859.4137931,5.400578256,5.402522042 12 | 4,29.11816578,923.7037037,5.447877025,5.357833375 13 | 4,7.918215613,1143.475836,5.733497663,5.497564271 14 | 4,6.850299401,1232.784431,5.438053274,5.854745873 15 | 2,3.540540541,1555.810811,5.883886861,4.966805779 16 | 2,14.5532646,1738.780069,5.960021755,5.328459857 17 | 4,3.220779221,1823.480519,6.32193551,5.020799909 18 | 4,10.56055363,2033.550173,5.477736792,5.097479414 19 | 4,2.584615385,2072.138462,5.522567997,7.087753626 20 | 4,8.958677686,2163.950413,5.488561998,4.989588918 21 | 4,2.986486486,2336.162162,5.304917162,5.308401486 22 | 4,9.238317757,2377.107477,5.369463681,5.043816961 23 | 4,15.92156863,2570.019608,5.608160155,5.18178355 24 | 4,9.325757576,2768.026515,5.638812884,5.199960885 25 | 4,2.75,2800.875,5.847922901,5.247535637 26 | 4,9.004166667,2836.804167,5.621894315,4.997662623 27 | 4,3.19047619,2953.190476,5.361143112,4.876166198 28 | 4,13.64236111,2971.677083,5.724979826,5.357797543 29 | 4,3.06122449,2999.204082,5.68994569,5.023880521 30 | 4,4.487394958,3051.369748,5.387958336,5.021167867 31 | 4,6.869047619,3079.136905,5.608621093,4.897928638 32 | 4,8.021052632,3457.245614,5.958043824,5.104242802 33 | 2,11.41314554,3763.098592,5.980204204,5.544755554 34 | 2,3.819047619,4538.314286,6.774887612,5.273829495 35 | 2,2.255319149,4622.914894,6.328392607,5.062190965 36 | 2,3.01369863,4682.794521,6.092424986,5.729921863 37 | 2,10.70967742,4992.818548,5.819855567,5.386269161 38 | 2,5.548387097,5342.462366,6.842338953,5.497212293 39 | 4,15.9978022,5497.072527,5.652304085,5.265805527 40 | 2,5.90459364,5539.024735,6.465721486,5.204453137 41 | 3,5.215517241,5624.534483,5.57699349,7.904672103 42 | 4,4.70754717,5919.971698,5.436724824,6.040614036 43 | 3,15.08518519,3530.766667,5.901894344,7.353665422 44 | 4,13.79792746,998.0621762,5.681462692,5.319059307 45 | 2,22.27007299,4382.326034,6.254946472,5.571654338 46 | 4,11.16911765,1028.433824,5.650267933,5.364772708 47 | 3,18.49759615,1203.507212,6.328348787,7.29908408 48 | 4,23.15231788,1871.602649,5.302283003,5.017192118 49 | 4,13.49717514,262.7627119,5.456768713,5.552674869 50 | 2,18.00813008,1574.666667,6.370262873,5.06616486 51 | 4,18.63600783,2097.888454,5.603388946,5.307017182 52 | 4,20.88983051,520.299435,5.823714278,5.51176434 53 | 4,21.13679245,3103.117925,5.610300811,5.010792514 54 | 2,18.04526749,3829.806584,5.712550507,5.34249176 55 | 2,22.11728395,4632.925926,5.69224498,6.392821434 56 | 4,15.25257732,5990.201031,5.779804422,5.400073456 57 | 4,15.92413793,3123.089655,5.712161069,5.017553834 58 | 1,18.67493797,4901.208437,8.718277246,5.551294322 59 | 2,19.78714859,1500.975904,6.955496892,5.479751649 60 | 2,18.15833333,1753.133333,5.7656351,5.351384087 61 | 2,23.15819209,4012.067797,6.72977682,5.279914606 62 | 2,22.07392996,4469.66537,6.277630393,5.071299589 63 | 2,25.69565217,4572.053512,5.938682731,6.655535042 64 | 4,19.072,967.88,5.674889051,5.44556427 65 | 2,20.82666667,5334.986667,5.68264963,5.548479279 66 | 4,25.08637874,730.4817276,5.918777747,5.853156697 67 | 4,28.89198606,1792.02439,5.76628277,5.205861993 68 | 3,20.53333333,5388.95,5.543613723,7.486487184 69 | 4,28.15974441,5692.945687,5.690942136,5.450448665 70 | 4,23.47761194,5934.109453,5.490088332,5.556597527 71 | 4,23.22012579,860.8427673,5.354685811,5.326623629 72 | 2,25.52873563,5242.568966,5.368416905,5.185747014 73 | 4,24.92413793,500.4896552,5.758553622,5.506312272 74 | 2,26.4829932,5305.401361,6.041452568,5.463024377 75 | 4,33.14247312,5890.946237,5.450738441,5.607007064 76 | 3,26.59585492,234.8238342,5.325130817,6.929663784 77 | 4,29.83919598,656.3065327,5.299422284,6.010256712 78 | 4,29.23780488,1254.469512,5.209452832,5.281561921 79 | 3,24.38235294,711.3529412,5.626880255,7.735883169 80 | 4,27.36231884,956.6376812,5.540950907,5.338687085 81 | 4,36.03676471,2850.977941,5.8916239,4.8686462 82 | 3,28.33050847,3187.779661,5.102962086,7.708208962 83 | 3,29.64285714,76.78195489,5.775181805,8.340418328 84 | 4,29.496,287.192,5.776289165,5.456926663 85 | 3,33.16993464,5608.039216,5.890198759,9.082042692 86 | 4,31.4587156,5868.454128,5.591602976,5.379833935 87 | 4,37.3804878,2243.35935,5.106595669,5.12986985 88 | 4,35.14979757,266.6720648,5.465956767,5.574260769 89 | 4,35.98648649,1094.206081,5.817613769,5.269580692 90 | 2,47.91811024,4873.258268,6.030522467,5.963243275 91 | 2,35.92387543,4963.221453,5.660735303,5.406519861 92 | 2,35.51440329,1463.061728,5.785533125,6.032533425 93 | 4,38.36018957,5563.078199,6.090934525,5.296692825 94 | 4,35.7962963,2045.848148,5.518478698,5.188213107 95 | 4,36.48880597,2197.548507,5.439570734,5.323937834 96 | 2,38.3153527,3615.908714,6.912284426,5.365665733 97 | 4,39.85169492,3083.610169,5.687750695,5.025913466 98 | 4,35.83673469,2285.44898,5.374238563,5.109587566 99 | 4,46.73257699,5531.042139,6.027067109,5.282010082 100 | 4,35.96153846,3112.615385,5.767479296,4.774912961 101 | 4,38.69473684,2061.4,5.694157354,5.101790122 102 | 4,59.03778677,3461.74224,7.105564689,5.069642351 103 | 4,46.72900763,478.9580153,5.648786069,5.568417341 104 | 4,48.59810127,552.8924051,5.552161995,5.473267411 105 | 3,42.03361345,1123.420168,5.645417203,8.266719466 106 | 2,38.13513514,5241.486486,5.500264888,5.307062797 107 | 4,47.05339806,319.2087379,5.674890117,5.506731613 108 | 2,45.02654867,3872.606195,5.672243099,6.038694287 109 | 2,43.12605042,4456.436975,7.639027712,5.538227139 110 | 4,61.09006211,2524.496894,5.341382805,5.216065959 111 | 4,65.08702532,2717.098101,5.61687466,5.094206337 112 | 4,48.03846154,3380.213675,5.774113482,5.047057882 113 | 4,48.10434783,992.3608696,5.387343018,5.191966022 114 | 4,65.01465798,2880.750814,5.327836637,5.005791879 115 | 2,49.83050847,4370.824859,5.981998986,5.449582531 116 | 2,50.64864865,5338.934363,5.915879393,6.716772383 117 | 3,49.68527919,405.8375635,5.784995914,7.344089257 118 | 4,45.79220779,87.74025974,5.924637695,6.916534808 119 | 4,55.80333333,615.2766667,5.452368041,6.323432608 120 | 2,50.02212389,4902.030973,7.85572131,5.344871573 121 | 3,50.88957055,641.2515337,5.581626394,7.84499558 122 | 4,51.75796178,1110.10828,5.71935068,5.353125178 123 | 3,48.41891892,2590.175676,5.419906962,8.117955276 124 | 2,51.22619048,5465.166667,6.735136498,5.141663557 125 | 4,50.48484848,5957.464646,5.613109674,5.442242786 126 | 4,50.7251462,956.8888889,5.376090294,5.089075279 127 | 4,58.1130137,511.3630137,5.760510177,5.733019817 128 | 4,58.99245283,712.1207547,5.749729427,5.395803565 129 | 4,59.25268817,939.188172,5.094805123,5.06846453 130 | 2,53.51807229,1161.126506,5.046180741,6.15138598 131 | 4,55.19871795,1091.00641,5.852644469,5.076735096 132 | 4,51.21428571,5623.928571,5.462216296,5.822623152 133 | 4,62.70422535,5781.014085,5.851325186,5.426190174 134 | 2,64.04016064,4580.791165,6.721048216,5.364980887 135 | 4,65.3125,5348.838542,5.814068341,5.752225356 136 | 4,57.47826087,343.4347826,5.717134961,5.196994582 137 | 4,60.96045198,2043.022599,5.18260845,4.705347802 138 | 4,84.6140873,2128.074405,5.446707438,4.798353505 139 | 4,62.69014085,3119.375587,5.589356474,4.780306365 140 | 4,64.4296875,5683.660156,5.5378109,5.865848176 141 | 4,60.784,363.816,5.653415067,5.619559933 142 | 2,69.57336957,4192.902174,5.697594893,6.071126748 143 | 3,63.56741573,2589.651685,5.317431279,7.666342961 144 | 4,67.32926829,3028.518293,5.324109909,4.739242591 145 | 2,61.16216216,1576.486486,6.893464461,4.883211339 146 | 4,65.01190476,2973.345238,5.303334521,4.986700331 147 | 3,65.55494505,3950.538462,5.927599792,7.276465203 148 | 2,66.44736842,3627.484211,5.869460444,5.782382847 149 | 4,66.93280632,5653.162055,5.539293071,6.665537689 150 | 3,67.90686275,460.1470588,6.231764012,7.460377481 151 | 2,65.16666667,5460.208333,5.570012958,4.742392675 152 | 4,65.52348993,5624.322148,5.510203156,5.506667469 153 | 4,81.01204819,2848.421687,5.59471138,5.176387945 154 | 2,73.34008097,1520.008097,6.772579968,5.290064784 155 | 2,76.01530612,4143.135204,5.92598681,6.87498697 156 | 2,83.35294118,4435.037255,6.157332031,5.322923892 157 | 2,75.8361204,4552.792642,6.650499682,5.388750975 158 | 4,74.07017544,3.315789474,5.709758733,5.431958402 159 | 4,76.81171548,2085.25523,5.741372474,5.158008727 160 | 2,83.00193798,4293.664729,5.900130946,5.385907549 161 | 2,73.98837209,1679.27907,5.720311777,5.521321373 162 | 2,81.92307692,1771.295385,7.160511403,5.169189451 163 | 4,82.24918033,2812.711475,5.714544033,5.413415563 164 | 4,73.95705521,5741.668712,5.841818023,5.476797198 165 | 4,76.29218107,2478.209877,5.581669089,7.078823771 166 | 4,72.83516484,1986.604396,5.209966431,4.889770592 167 | 4,77.39698492,3048.708543,5.580980584,4.785353781 168 | 4,81.85925926,3092.2,5.753595044,5.041391988 169 | 2,76.00763359,4596.748092,5.718357672,5.377502396 170 | 4,74.08163265,5959.387755,5.225252806,5.097249767 171 | 4,85.23423423,218.9459459,7.414476998,5.422923916 172 | 3,81.13377926,343.6555184,5.803824524,6.780225905 173 | 4,91.23553719,2963.766529,5.288945071,5.013427535 174 | 4,80.07575758,260.1767677,5.801553658,5.387894893 175 | 2,94.95590829,5206.84127,6.663947751,5.318975526 176 | 4,85.63013699,660.7726027,5.542015344,5.913273435 177 | 4,87.67105263,5899.638158,5.578562313,5.339086728 178 | 4,82.64220183,502.2889908,5.933752823,5.755045188 179 | 3,85.97183099,3314.661972,8.025775429,7.913260827 180 | 4,89.67708333,469.3229167,5.659300724,6.136387514 181 | 4,85.96710526,2352.690789,5.615550257,5.581986464 182 | 4,99.13270142,2448.71327,5.631135338,5.424657681 183 | 4,104.3208054,2936.008054,5.302115514,5.019162592 184 | 4,93.90521327,1273.180095,5.56553676,4.657609743 185 | 4,90.1958042,5780.559441,5.62111005,5.02815001 186 | 2,89.53380783,3760.377224,6.233489221,5.400607337 187 | 2,92.34677419,4858.350806,6.001644411,6.566428536 188 | 4,87.85714286,280.9340659,5.873478881,5.240699895 189 | 4,99.23584906,902.072327,5.42681844,5.323025319 190 | 4,88.03030303,2779.989899,5.60174602,6.980019561 191 | 4,95.34749035,5849.081081,5.562743656,5.155209098 192 | 3,92.86666667,4908.077778,5.436338664,7.721011184 193 | 4,96.47619048,682.5714286,5.694955755,5.681162028 194 | 4,97.95390071,3580.297872,8.100580004,5.189542834 195 | 2,100.5532646,5120.601375,5.873162691,6.645162466 196 | 2,108.0503432,5395.329519,6.003395379,5.043026425 197 | 4,99.78350515,2762.657216,5.75545671,7.100373726 198 | 3,97.95522388,3958.425373,6.033792869,8.359664424 199 | 4,109.2486339,1198.811475,5.369251886,5.963677671 200 | 4,98.5,2.322580645,4.544666525,5.126265476 201 | 3,100.5445545,772.4306931,5.614247664,8.924919108 202 | 2,106.2338462,1746.384615,5.91733764,5.190132353 203 | 2,103.390625,3728.617188,6.097424492,5.569134387 204 | 4,117.1245791,557.0117845,5.658719363,6.018888091 205 | 4,101.0384615,5573.807692,6.318552227,5.687327315 206 | 4,97.96969697,5987.424242,5.111622625,4.849185312 207 | 4,102.0742857,2244.44,5.510937165,4.782814153 208 | 4,100.4655172,5629.12931,5.358049095,5.333800999 209 | 4,103.03125,5941.955357,5.416179764,5.30713994 210 | 4,102.3555556,497.7925926,5.739316438,5.65826842 211 | 4,113.3066667,797.2586667,5.375068509,5.405460293 212 | 4,100.6216216,5649.094595,5.266394285,5.127256494 213 | 4,101.704918,5997.868852,5.268027149,5.095860012 214 | 4,104.6793478,1129.320652,5.848729439,5.503296947 215 | 4,114.8377088,2504.727924,5.579882919,5.300844005 216 | 4,110.3006757,389.6790541,5.705582655,5.742364124 217 | 4,106.3925926,2214.748148,5.026314204,5.249321626 218 | 4,106.9635036,459.2481752,5.486037948,6.09059952 219 | 3,107.9393939,547.2878788,5.381432273,7.53654574 220 | 4,113.3891213,1146.48954,5.397367217,5.288795187 221 | 4,123.2260442,5669.665848,5.641697942,5.218547963 222 | 4,108.1621622,470.5675676,5.658351527,5.844170934 223 | 3,110.3576159,630.2913907,5.486416901,7.851168877 224 | 4,112.0479042,972.8502994,5.260609281,5.080250726 225 | 3,113.1493776,433.5228216,5.772410134,8.10001158 226 | 2,115.5590062,4860.36646,6.244757693,6.895626117 227 | 4,115.637931,3023.521552,5.333343286,4.844899567 228 | 4,115.4444444,292.1388889,5.706667199,5.298456246 229 | 4,117.3660714,2634.59375,5.210363988,5.729954828 230 | 4,115.5912409,3061.29927,5.40215117,4.771303015 231 | 2,121.6727273,4014.52987,5.806307806,5.589920314 232 | 1,123.7530864,4667.293827,7.70113094,5.332742649 233 | 3,119.6056338,356.9201878,5.650592976,7.414100458 234 | 4,117.8586957,617.9347826,5.702404714,5.411937159 235 | 2,117.5837838,1543.616216,5.416316595,5.784274146 236 | 4,115.5772358,5642.357724,5.657695969,5.368821291 237 | 4,116.2391304,3305.73913,5.791488055,5.02673685 238 | 4,127.7853982,3312.725664,5.848763555,5.386806265 239 | 4,123.75,3402.727778,5.789042319,5.221346325 240 | 1,128.9308943,3568.300813,10.4775022,5.191465224 241 | 2,130.4761905,4191.142857,6.372529438,5.651913502 242 | 2,134.2555911,4348.217252,5.790272772,6.888445438 243 | 3,123.7484663,483.2576687,5.75597516,7.613012881 244 | 2,138.0792541,3768.324009,5.704108762,5.330486649 245 | 2,123.1282051,1730.897436,5.438970773,4.651270533 246 | 4,128.8876404,2202.879213,5.210284017,4.858701257 247 | 2,126.0851064,3981.58156,5.879760074,6.266298477 248 | 4,129.0985915,2552.399061,5.29716647,5.21391462 249 | 4,129.9361702,3298.446809,5.686831098,4.830269271 250 | 4,134.8613861,843.7821782,5.750359494,5.436743936 251 | 4,133.8333333,1219.399225,5.650501622,5.488024374 252 | 2,133.4335938,3583.078125,8.084384186,5.007453153 253 | 4,127.0288462,5647.778846,5.594353866,5.214465329 254 | 2,135.1707317,4790.884701,5.519241161,5.097090961 255 | 2,147.0986159,5344.008651,5.69260723,5.137791158 256 | 4,134.8526316,722.7315789,5.446374329,5.358941988 257 | 4,132.4244604,1080.784173,5.834347842,5.275873124 258 | 4,132.38,1123.973333,5.771004891,5.418438408 259 | 4,153.2347354,2941.721845,5.315916008,5.990212067 260 | 4,146.5050505,904.1742424,5.470932881,5.316707827 261 | 4,143.1060071,2373.575972,5.699166419,4.972582332 262 | 3,135.5625,2518.34375,5.504060403,7.244547405 263 | 3,136.8813559,610.4745763,5.776917415,7.370961494 264 | 2,148.6956522,5257.120205,5.780790877,5.27024148 265 | 4,137.1044776,5808.58209,5.298242737,5.224301239 266 | 4,141.5958904,5785.253425,5.451332372,5.154003986 267 | 4,141.6944444,863.8148148,5.109698244,5.290275861 268 | 2,146.9166667,3954.301587,5.810594318,6.332165359 269 | 2,146.4565826,4845.37535,6.327831736,5.570341781 270 | 3,142.4330709,248.2283465,5.368291384,6.907314237 271 | 4,149.3755556,492.0044444,5.706223936,6.707953835 272 | 4,152.3108974,563.5544872,5.611981089,5.657123052 273 | 3,146.3023256,437.1674419,5.52131207,7.430450643 274 | 4,144.0802469,1005.993827,5.656595962,5.349632266 275 | 4,146.6666667,1067.716981,5.822952854,5.399357241 276 | 4,159.7125749,946.5828343,5.109097822,5.024628744 277 | 2,142.0909091,1591.318182,5.804313164,5.580758456 278 | 4,157.7653759,315.856492,5.771157232,5.323831909 279 | 4,147.1954887,5757.022556,5.530115092,5.118083854 280 | 4,151.6880342,271.3931624,5.728697424,5.749569899 281 | 4,149.7213115,839.6065574,5.706728949,5.42830252 282 | 2,155.5434783,5115.373188,5.923393223,6.094738649 283 | 4,145.6428571,5678.547619,5.715773783,5.437457998 284 | 3,150.7692308,1147.129231,5.803790228,7.454684336 285 | 2,156.625,1512.608871,6.939648498,5.264527043 286 | 4,158.8338462,2871.963077,5.351420561,4.913446428 287 | 2,164.2934132,5321.736527,5.808043584,5.264689368 288 | 4,155.4357798,5801.587156,5.656665222,6.23276339 289 | 2,159.328125,5453.8375,5.670914871,6.356151063 290 | 2,156.716,1548.98,6.551531662,5.398850248 291 | 4,154.9057971,985.2101449,5.55020329,5.26005851 292 | 4,156.0284091,5670.028409,5.843165557,5.348081083 293 | 2,154.2307692,1581.769231,5.877843511,5.317271217 294 | 2,164.5905045,4016.860534,5.574734446,5.482923651 295 | 4,172.2059801,2996.254153,5.493929229,6.117372478 296 | 3,164.4197248,5228.644495,5.818434552,7.836692314 297 | 4,157.4690265,5996.39823,5.654288523,5.246325179 298 | 4,158.389313,1038.564885,5.676440044,5.231108617 299 | 4,165.1610487,3570.142322,8.145530079,5.331976692 300 | 4,161.3411765,1018.123529,5.676088795,5.381305957 301 | 3,164.7303371,3995.160112,5.789470411,7.22673594 302 | 2,165.5878525,4914.084599,5.890295044,5.583895818 303 | 2,168.2972973,4719.227027,5.815025653,6.964807231 304 | 4,164.2269231,5558.403846,7.046101504,5.343162029 305 | 4,161.9454545,760.2,5.45329887,5.314369655 306 | 3,162.2135922,706.9126214,5.750906897,7.223585979 307 | 2,172.6941176,1766.638235,8.092341969,5.422965671 308 | 4,167.08125,245.60625,5.757639147,6.614809377 309 | 4,173.7324185,3424.526587,6.942090413,4.963911604 310 | 2,162.3103448,3622.586207,6.139884552,5.656955899 311 | 4,169.2923977,5905.157895,5.297703143,4.989238414 312 | 3,168.139738,482.6462882,5.857558786,7.047665302 313 | 2,175.2710843,3703.996988,5.925471153,5.973111922 314 | 2,172.5971731,5281.742049,5.897474167,5.384965014 315 | 4,174,5527.258352,5.744030585,5.057649215 316 | 4,172.7262357,3020.608365,5.647312708,4.968069067 317 | 2,175.6965517,4593.496552,6.757593758,5.432652316 318 | 3,173.5773585,458.4415094,5.708375683,6.841099297 319 | 4,176.6805556,3371.577778,5.865932818,4.988579018 320 | 4,176.0459184,1054.056122,5.66108088,5.33138694 321 | 3,173.0769231,434.4903846,5.621341059,8.177405079 322 | 4,178.9383562,5738.883562,5.201538643,5.084547552 323 | 4,171.1791045,88.97014925,5.915638682,4.971470301 324 | 4,198.0047695,3490.608903,5.738370772,5.090105501 325 | 2,184.3622829,5004.511166,6.081090885,6.651533402 326 | 4,175.2830189,2142.056604,5.363150656,4.558039022 327 | 4,181.9330986,197.5246479,5.905198731,5.574908229 328 | 4,191.056338,5876.346076,5.430421685,5.329632747 329 | 4,180.1737089,2356.460094,5.521573588,4.857772256 330 | 2,185.0410557,4201.041056,6.07626919,6.469950247 331 | 4,189.0159574,3046.473404,5.468901425,4.766551501 332 | 4,181.0517241,373.8390805,5.706365343,6.376902865 333 | 4,186.45,421.9038462,5.58508556,6.055107234 334 | 4,197.6047059,144.3458824,6.248038323,5.190076891 335 | 4,183.173913,5448.456522,5.611520182,4.94875989 336 | 4,194.0677966,79.77966102,5.999840389,5.224865416 337 | 4,188.7264438,617.1702128,5.724448904,5.625212846 338 | 4,178.8,5583.866667,6.004490452,5.206202078 339 | 3,184.2512563,1164.432161,5.784180743,7.804060694 340 | 3,186.0033557,4932.661074,5.690654169,7.690350463 341 | 4,183.0307692,5715.615385,5.60885171,5.175280167 342 | 2,190.8021583,3748.586331,6.153467156,6.107127031 343 | 4,192.7617555,278.7115987,5.826314462,5.447844747 344 | 2,192.6517857,1448.020833,5.745670567,5.437975784 345 | 4,190.035461,2918.489362,5.299097204,6.387553687 346 | 2,197.0218182,4463.72,7.679811098,5.368708049 347 | 2,187.1162791,1535.550388,7.781433008,5.214767223 348 | 2,193.0229508,4538.52459,5.838074507,6.771341986 349 | 4,185.1914894,5861.446809,5.267199903,6.106454086 350 | 4,191.9451477,5926.405063,5.463134287,5.326646459 351 | 4,206.6248383,2874.047865,5.189154893,4.766686025 352 | 2,201.1948718,4478.007692,6.753683229,5.457805426 353 | 4,197.3669355,3344.596774,5.920626657,5.221880382 354 | 2,201.7787115,1332.196078,5.577030769,5.076416041 355 | 4,192.996904,5565.343653,6.143780285,5.322390592 356 | 2,198.6796117,3668.18123,7.140032397,5.326933128 357 | 2,190.8137255,5090.235294,5.822842835,5.240657097 358 | 4,210.1131222,2279.180995,5.521298009,5.17302601 359 | 4,195.3068182,2938.107955,5.009081057,6.629483337 360 | 4,200.8611111,588.7103175,5.589445874,5.561825665 361 | 4,196.7767857,2313.138393,5.386009315,5.029649796 362 | 4,201.1707317,223.5993031,5.734107759,5.435576558 363 | 4,198.9288703,3528.903766,8.467827129,4.962639793 364 | 2,199.5212121,4433.472727,5.929105467,5.159508001 365 | 2,205.4826667,4046.922667,5.723027212,6.457932767 366 | 3,199.1359223,1168.373786,5.761410034,7.99857678 367 | 4,201.2765957,2255.898936,5.259792156,5.247024072 368 | 2,206.5763889,4598.336806,5.701372165,5.600143727 369 | 2,215.9661654,4336.203008,6.497117984,5.516720376 370 | 4,207.3480176,310.061674,5.635685992,5.514618176 371 | 2,206.8786611,1713.485356,6.418740274,5.426809935 372 | 4,204.5748031,1038.889764,5.5413253,4.71807696 373 | 4,203.2840909,535.9772727,5.476178232,5.272008392 374 | 4,210.9731544,5580.137584,5.763011202,5.307852301 375 | 4,206.7569444,761.5208333,5.157577516,5.199455749 376 | 4,211.3931298,2338.072519,5.432871614,5.115041156 377 | 4,210.2390244,2463.068293,5.280724491,4.990100692 378 | 3,209.5816993,713.1568627,5.565071178,7.12794021 379 | 4,212.03125,2319.453125,5.127445635,5.131284588 380 | 4,211.8794326,502.9787234,5.385698061,5.434681622 381 | 3,216.0528053,639.679868,5.40281117,8.099622431 382 | 3,213.0149254,444.6940299,5.323155582,7.190563553 383 | 4,217.0833333,384.2575758,5.458714729,5.422644599 384 | 3,217.6516854,465.752809,5.694186014,7.620129354 385 | 4,222.4435484,956.5403226,5.52497088,5.295045892 386 | 3,222.8206278,614.7713004,5.517362846,7.452870541 387 | 3,218.8409091,567.3712121,5.856026576,7.316472668 388 | 3,221.4980695,1430.965251,6.073711536,7.591191366 389 | 2,227.8009368,4421.63466,6.673028507,5.329852342 390 | 4,225.4449153,1236.271186,5.70572973,5.134551399 391 | 4,219.4224138,2365.344828,5.20082809,5.012300573 392 | 4,227.7099237,2416.969466,5.379105539,5.160655317 393 | 4,225.5302326,2121.888372,5.27128073,5.06341895 394 | 4,222.6619718,5651.014085,5.657517289,5.222978952 395 | 4,224.0245399,5967.601227,5.564590922,5.287678148 396 | 4,224.627907,80.8372093,5.657657361,5.004180397 397 | 4,229.864486,2399.542056,5.450175701,5.231258538 398 | 4,234.6440678,5856.881356,5.55486291,5.242912151 399 | 4,229.4619565,430.423913,5.721326942,6.384020604 400 | 4,230.9302326,1083.710963,5.637076971,5.686569846 401 | 2,238.6762178,3656.260745,6.157434178,5.320995232 402 | 2,236.9953917,1709.571429,5.924477562,5.264836829 403 | 4,246.8313953,2926.05814,5.327332727,5.050750062 404 | 2,240.5738095,5013.257143,5.837577721,5.441149126 405 | 4,242.7509579,1927.059387,5.564131318,5.160276643 406 | 2,233.3266332,4507.025126,6.263532257,6.680280754 407 | 4,241.4173486,3346.639935,8.358991108,5.298006353 408 | 2,236.0708661,3854.354331,5.910764635,5.738272654 409 | 2,244.7692308,5290.094675,5.927422944,5.991486736 410 | 4,234.5555556,1048.428571,5.573058928,5.592781941 411 | 2,239.6474576,1473.250847,6.620023488,5.395705509 412 | 2,233.3790323,5330.75,6.258439753,6.528594387 413 | 4,238.9202899,859.423913,5.501095524,5.429742829 414 | 4,232.992,1002.408,5.570631962,5.338576002 415 | 4,241.8924485,2001.064073,5.456865034,4.986752041 416 | 4,237.9569892,5540.322581,5.809368771,5.16300251 417 | 4,239.9887006,675.5480226,5.690817435,5.489427871 418 | 4,243.6190476,751.3936508,5.563352055,5.45927476 419 | 4,241.8768657,1893.779851,5.521102645,5.125495322 420 | 2,246.9039301,5362.480349,7.43161265,5.133792831 421 | 4,247.4140351,5490.442105,6.481609369,5.009792834 422 | 4,243.8229665,76.07177033,5.855537944,5.316194885 423 | 2,247.8235294,4525.352941,6.716711763,5.283822059 424 | 2,246.1674419,5401.376744,6.426488457,5.477960922 425 | 4,245.6538462,820.1875,5.871088007,5.429767267 426 | 1,245.3048128,1417.42246,8.652838791,5.531358301 427 | 3,246.2317073,264.9349593,5.685886159,7.573861258 428 | 3,247.6065574,360.7245902,5.66147942,7.370053065 429 | 4,243.6758242,2230.401099,5.081267846,4.898782442 430 | 2,249.7975709,1457.303644,5.875942365,5.410542928 431 | 1,250.5783784,1662.081081,9.891365183,5.513733843 432 | 4,251.5036496,1806.667883,5.805255883,5.372147667 433 | 2,247.327957,5375.177419,5.498533413,5.750248725 434 | 4,256.760181,2197.233032,5.117777028,5.180384433 435 | 4,250.0234742,2996.638498,5.428171231,5.147630945 436 | 2,260.826087,5242.266304,5.857777863,5.412094806 437 | 4,257.2724359,300.7788462,6.275437827,5.499778833 438 | 4,252.8434343,645.3636364,5.604966156,6.076239629 439 | 2,264.915601,4307.69821,5.904762353,6.669017623 440 | 4,255.1703057,3009.537118,5.354235322,5.232718604 441 | 4,251.5378151,5605.319328,5.806121624,5.10359997 442 | 4,253.566879,5876.305732,5.326638621,4.946453397 443 | 4,255.0209424,449.8638743,5.761161923,5.809069689 444 | 4,259.296875,560.8242188,5.775606404,5.743291107 445 | 4,262.9035294,954.0776471,4.816822078,5.220877152 446 | 3,259.0152672,3764.003817,6.134821083,7.394401613 447 | 3,255.1419753,12.25925926,5.226111667,7.718224484 448 | 4,254.5547945,2453.958904,5.528314093,5.979041813 449 | 4,266.1271586,2886.720565,5.237151488,4.879382831 450 | 3,254.4416667,505.7916667,5.698658257,8.271559028 451 | 2,257.5174419,3542.186047,5.9623231,5.774407089 452 | 4,261.6129032,38.35080645,5.829704343,5.218566955 453 | 4,265.9861111,2715.770833,5.580585505,5.152957028 454 | 4,258.5066667,2607.36,5.320535375,4.722893961 455 | 4,258.8,995.4275862,5.433360547,5.340914337 456 | 4,262.5384615,2424.585799,5.396447665,7.011123278 457 | 4,264.1841155,2632.949458,5.566524014,4.973891415 458 | 4,261.7900552,1074.78453,5.614075353,5.395709757 459 | 2,268.0664557,1397.177215,5.9565176,6.555203829 460 | 2,275.4836066,5315.478142,5.647000075,5.235133119 461 | 4,264.2824074,581.4259259,5.513503415,5.774939669 462 | 2,274.9774859,4144.686679,5.363148997,6.836897899 463 | 4,265.104918,678.7508197,5.562420346,5.681168029 464 | 4,266.9175824,1124.230769,5.500764705,5.308920297 465 | 3,266.0292683,2470.02439,5.494104761,6.781467276 466 | 2,272.5011876,4483.940618,6.308963385,6.974992057 467 | 4,268.1637427,471.2631579,5.876178392,6.128910053 468 | 4,267.9428571,858.32,5.311318203,6.524660926 469 | 4,271.8041237,1109.498282,7.09468533,5.191638686 470 | 2,272.2246696,5337.643172,7.509936135,5.2835391 471 | 2,269.0147059,5433.194853,5.99237406,5.123854555 472 | 4,266.9012346,5969.87037,5.351009374,5.340826825 473 | 3,268.6535948,398.0065359,5.609064712,7.335109776 474 | 4,274.3103448,773.9172414,5.598843326,5.735548414 475 | 3,273.5163205,5395.750742,6.095984947,7.292298796 476 | 3,269.3157895,4238.482456,5.637042702,7.50724705 477 | 3,271.7197802,541.4505495,5.799874899,7.758223149 478 | 4,270.020979,2406.937063,5.282635607,6.57610914 479 | 3,273.0718563,5638.314371,5.749926807,6.958049871 480 | 2,275.0900474,1774.052133,5.863078998,5.459282958 481 | 4,271.7701863,2268.304348,5.629607611,5.626525738 482 | 4,275.9230769,252.9384615,5.781724358,5.506664814 483 | 4,277.385,419.135,5.641233085,5.653716497 484 | 4,269.1304348,2434.26087,5.477372726,5.908555416 485 | 2,281.5440252,3669.31761,6.214450853,5.43627292 486 | 2,284.0897833,4713.826625,6.009976352,5.353031275 487 | 2,279.5458937,3712.980676,6.468208693,5.356814122 488 | 4,277.1760563,2926.93662,5.297577656,4.832866737 489 | 4,278.5505618,648.1966292,5.628146398,5.560681631 490 | 4,283.9503205,3192.616987,5.283382679,5.013014939 491 | 4,280.375,5822.394231,5.360569622,5.110248557 492 | 4,296.6553846,2165.54,5.336213238,5.089067238 493 | 4,280.4320988,2309.141975,5.355041873,5.004733137 494 | 4,295.0865874,3371.434635,5.722830243,5.53458327 495 | 3,289.6180124,742.9409938,5.528208289,7.351321665 496 | 4,282.2698413,3041.952381,3.697571725,4.729352456 497 | 2,290.7682672,4661.574113,7.482076605,5.27179976 498 | 4,285.6035088,1214.726316,6.22555845,5.310228601 499 | 2,288.8762215,3955.934853,5.956874788,5.135894236 500 | 4,285.7176471,443.8294118,5.801140467,6.213195336 501 | 4,283.9937888,1003.68323,4.971494081,5.487036634 502 | 3,287.4542254,579.9612676,5.711615566,7.251544744 503 | 4,290.7633333,831.1333333,5.481027737,5.348804503 504 | 4,284.6772152,1077.436709,5.433252046,5.234689823 505 | 4,282.761194,622.8358209,5.596208344,5.360380812 506 | 4,284.037037,866.7037037,4.885976825,6.442107581 507 | 2,291.6470588,5032.654412,5.223352127,6.12175079 508 | 2,285.3057851,2469.396694,5.722882646,6.459839785 509 | 2,295,3752.971429,5.867718149,6.239814991 510 | 4,286.0116279,2637.813953,5.383427713,4.977855807 511 | 2,293.5393701,5166.870079,5.623718988,6.83248855 512 | 4,289.7668712,5892.177914,5.202319834,4.855714038 513 | 4,286.9444444,923.1333333,5.278341391,6.461242456 514 | 4,299.9676674,2367.418014,5.079408485,5.744223796 515 | 4,301.7336562,3489.51816,5.744096834,5.116629991 516 | 4,291.4672897,936.4766355,5.151615511,5.252273428 517 | 4,292.5,2588.72,5.612938307,5.181041702 518 | 2,295.6888218,4835.471299,5.759463912,6.266667263 519 | 4,294.2735849,760.8962264,5.412235501,5.905387553 520 | 4,293.1297297,3275.983784,7.495136456,5.028563705 521 | 2,301.6308411,1436.827103,5.741519341,5.359205835 522 | 4,300.5454545,1115.91958,5.784362961,5.415680734 523 | 4,302.7605042,3019.331933,4.254539364,5.036242976 524 | 2,306.2566372,4757.740413,7.466509339,5.639938566 525 | 2,300.305136,1760.770393,5.789793855,6.229672309 526 | 4,300.1684783,632.9076087,5.930138253,5.432130649 527 | 4,301.483871,797.6344086,5.586724982,5.0622547 528 | 4,308.6476965,1929.189702,5.713374982,5.118399423 529 | 4,311.4678899,2548.24159,5.523892211,5.233787 530 | 4,308.2,776.676,5.710413772,5.349276605 531 | 2,307.779661,3642.519774,5.682175961,5.668633311 532 | 4,313.4636015,937.5057471,5.353491053,5.614679047 533 | 4,307.862069,2573.517241,5.531593319,5.113993807 534 | 2,325.7483085,4076.146143,5.288144007,5.147187604 535 | 4,309.6737288,539.6398305,5.52952997,5.505658753 536 | 4,311.23,2326.886667,5.668903434,5.193567775 537 | 4,311.6881188,5682.955446,5.695064717,5.262536276 538 | 4,315.759322,409.9932203,5.400062881,5.336690263 539 | 4,316.6873065,5569.662539,7.449307078,5.148573885 540 | 4,311.3425926,599.7037037,5.334863566,5.779113244 541 | 4,315.0544554,899.1782178,5.427411329,5.308610742 542 | 2,316.2692308,1288.619658,5.821476871,5.689102601 543 | 3,315.1799163,2098.728033,5.571724393,7.370844367 544 | 4,317.4264706,3341.772059,5.818161029,5.003353947 545 | 4,320.2152318,348.6655629,5.583309566,5.404257185 546 | 2,316.9366516,1373.0181,5.923953286,5.985928892 547 | 4,313.7457627,5709.231638,5.803244271,5.475896055 548 | 3,321.9916318,427.4267782,5.609011898,7.60225719 549 | 3,314.6833333,615.6083333,5.757481207,7.638980693 550 | 4,324.7603093,79.03865979,5.556738498,5.222040112 551 | 2,325.9287469,4672.90172,6.104562862,5.187399532 552 | 3,319.5911111,448.3333333,5.760302825,7.058024149 553 | 2,322.6425532,4409.914894,5.787888927,6.844733904 554 | 4,320.9128205,3212.405128,7.673075227,5.101116428 555 | 4,317.8727273,5800.736364,5.359497984,6.447449924 556 | 4,322.8992629,5498.918919,6.346424877,5.041727553 557 | 4,322.210084,5742.092437,5.569386363,6.22887558 558 | 2,325.9161074,1776.073826,6.269318702,5.598968665 559 | 4,325.8733032,5845.321267,5.474359824,5.003642576 560 | 4,325.2088889,1037.977778,5.263219698,5.229729175 561 | 4,321.3666667,1070.244444,5.162879396,4.859984655 562 | 4,323.966443,388.0671141,5.395456258,5.625760468 563 | 4,331.5238095,635.3915344,5.755080619,5.654510713 564 | 2,321.4782609,3986.391304,6.428315969,6.408027269 565 | 4,333.0836237,973.2473868,5.214102203,6.515208116 566 | 4,328.5127273,1958.818182,5.220748868,4.806997237 567 | 4,324.7579618,5643.88535,5.608000846,5.242925843 568 | 4,329.3609467,1111.946746,6.451899614,5.238893946 569 | 2,335.9264706,4531.502451,5.889855157,6.264028316 570 | 4,335.75,5853.740132,5.418145094,5.02867338 571 | 4,338.5976471,2156.981176,5.111179524,4.976019491 572 | 2,334.9572193,3976.205882,5.807532068,6.380892656 573 | 4,328.4677419,365.2741935,5.436605573,5.308107991 574 | 3,329.1272727,574.7090909,4.954289384,7.082248293 575 | 4,339.0260417,2792.148438,5.608889742,5.036732746 576 | 4,335.2789969,1131.501567,6.613380011,5.298662135 577 | 4,337.6754386,5554.763158,5.988433643,5.183187059 578 | 2,334.9663462,5457.375,5.949490046,4.861190405 579 | 4,330.6554622,5904.789916,5.372980407,4.876671635 580 | 4,333.8782051,793.4615385,5.609636147,5.33753808 581 | 3,332.3333333,611.9270833,5.748330851,7.459662863 582 | 4,334.0240964,934.9156627,5.338753745,5.34509748 583 | 4,331.9347826,2375.673913,5.471173982,4.800629698 584 | 2,335.1764706,3528.104575,6.029238765,5.584969497 585 | 4,340.6715328,5950.474453,5.56654593,5.200189352 586 | 4,340.46,2344.205,5.148220957,4.882233579 587 | 4,342.3365079,233.4253968,5.384684356,5.128752195 588 | 2,336.2108844,3350.489796,5.491632692,6.501943172 589 | 4,343.8212928,1062.011407,5.587049268,5.130056193 590 | 3,345.502809,1744.325843,5.843959573,7.411871931 591 | 2,345.5047319,4282.870662,6.396387973,6.666201978 592 | 2,351.1945289,5242.808511,5.606059051,5.299076956 593 | 4,351.9288889,2885.768889,5.16815806,4.490457432 594 | 4,340.6153846,887.1538462,5.411760514,5.154476604 595 | 4,345.433526,2373.595376,5.603781154,5.41956261 596 | 4,341.8243243,2585.432432,4.603141102,4.94231787 597 | 4,342.9459459,2980.101351,4.847268098,4.737501535 598 | 4,350.7183099,486.3755869,5.564682285,5.359637687 599 | 2,358.2142857,4223.893822,6.203230881,5.647926868 600 | 4,348.6300578,1987.32948,8.405620743,4.885207505 601 | 4,347.0994475,2395.917127,5.471527559,5.073307434 602 | 4,349.1295337,2827.300518,5.2522463,4.756801039 603 | 2,354.5628415,3335.10929,5.556944096,6.442048416 604 | 3,348.6439024,4007.678049,5.448250104,7.556336167 605 | 2,345.8166667,4190.341667,5.612123953,6.543851892 606 | 4,346.1382979,712.0638298,5.560599795,6.04341587 607 | 4,361.1747899,2103.394958,7.534326233,5.578206548 608 | 4,349.129771,5915.709924,5.40768511,4.964018339 609 | 4,347.5789474,473.0350877,5.057362625,6.916001884 610 | 4,360.8329238,2290.496314,5.316661591,4.876378238 611 | 4,349.71875,620.046875,5.548711232,5.80112657 612 | 2,353.2593985,3880.913534,5.747379562,5.322899942 613 | 2,349.0689655,4871.747126,5.637126771,5.285884071 614 | 3,355.1622419,1777.672566,5.797956763,7.238994716 615 | 4,360.5193133,917.1630901,5.50738811,5.155348571 616 | 2,365.1507761,5159.54102,5.757095531,5.239633073 617 | 4,352.3235294,5861.470588,5.484430745,4.765937582 618 | 4,357.0851064,2419.898936,5.481502912,5.02688552 619 | 4,361.9083665,2807.450199,5.357469144,4.962677453 620 | 4,362.1581633,1231.112245,7.279719881,5.033168617 621 | 4,360.5022026,5695.696035,5.582982779,5.253725426 622 | 4,360.3185841,5752.460177,5.8314412,4.978623929 623 | 4,360.7235772,673.0487805,4.861009142,5.018409872 624 | 3,362.8769841,729.6706349,5.649630741,7.454708467 625 | 4,373.7717391,2881.559783,5.664808108,4.7189355 626 | 4,360.7961165,2371.572816,5.443509872,5.100458249 627 | 4,362.0292398,3264.350877,5.664015804,5.512485083 628 | 4,367.9756098,333.8641115,5.539430891,5.399438945 629 | 2,367.5023697,1962.445498,5.474736864,6.234903124 630 | 4,362.3825503,2459.302013,5.396217235,4.839260316 631 | 4,369.7714286,462.1,5.571107858,5.474878315 632 | 4,367.7269076,2489.172691,5.299260697,6.343223814 633 | 2,364.7346939,3773.265306,6.149833722,5.791145264 634 | 4,363.4761905,5645.533333,4.95569291,4.938030465 635 | 4,365.0566038,5809.534591,5.620492011,6.374756926 636 | 4,363.8604651,5963.686047,6.061727929,4.931038224 637 | 4,364.2371134,142.9690722,5.7520816,5.266475548 638 | 4,364.0405405,2923.621622,5.277838835,4.569402962 639 | 2,366.0215827,4387.625899,5.738867179,6.83101407 640 | 4,371.2360515,1011.553648,5.554854186,5.356201554 641 | 3,365.2173913,266.326087,5.465580459,7.596632487 642 | 4,371.673913,1208.069565,7.19618164,5.3353618 643 | 4,371.7021277,1037.994681,5.694072363,5.31380846 644 | 3,375.5140845,1888.056338,5.724160284,7.24648603 645 | 1,374.1440922,4595.011527,8.833447597,5.169074829 646 | 4,371.9956897,5557.362069,6.440664644,5.162591449 647 | 3,372.7290503,3138.932961,5.767753446,7.443361262 648 | 4,371.1459459,5724.016216,5.306285536,5.166267501 649 | 4,377.017316,1088.839827,5.654719546,5.016075017 650 | 4,373.2213115,5638.852459,5.390754571,5.218581989 651 | 4,374.3333333,152.5802469,5.40401116,6.860844829 652 | 4,399.6853056,2263.23407,5.479825849,5.077609079 653 | 2,376.6931217,5194.248677,5.749190761,5.937522245 654 | 3,378.601626,3271.536585,5.545050404,7.503132754 655 | 4,377.4957265,1947.709402,5.103092405,4.673387828 656 | 2,375.2,3933.284211,6.107889936,7.129660169 657 | 3,379.010929,256.5464481,5.535848719,7.51185324 658 | 2,375.9,5141.1,5.416655803,6.445799181 659 | 4,381.6272727,5835.668182,5.675320646,5.337647339 660 | 3,387.6340782,413.5363128,5.682904968,7.360963626 661 | 4,381.1818182,745.8502674,5.630137853,5.762219532 662 | 4,379.3146067,2377.337079,5.24388491,4.848248544 663 | 4,385.7525773,5708.262887,5.235646516,4.970278541 664 | 4,388.1527778,360.6458333,5.671089484,5.428446711 665 | 2,384.8120301,1286.556391,5.56863144,5.282573783 666 | 4,381.3333333,2335.439024,5.289949117,4.850250225 667 | 4,380.974026,5599.941558,7.660255073,5.210656428 668 | 4,393.3467049,575.8538682,5.454439087,5.249749957 669 | 3,382.4444444,3687.585859,5.912738313,8.586455387 670 | 2,386.239521,4763.497006,5.948519542,7.065081408 671 | 4,389.0996016,1737.800797,5.735530758,5.890961625 672 | 3,384.6290323,2549.895161,5.355253982,6.861719785 673 | 4,384.1140351,2565.736842,5.17162967,5.305919972 674 | 4,392.8823529,2798.117647,5.525002873,4.976409146 675 | 2,392.8267327,4519.371287,7.176386991,5.865338482 676 | 4,383.1578947,5995.929825,5.54380589,5.186208989 677 | 2,390.2159468,5140.112957,5.522457099,6.317872066 678 | 4,391.475,316.795,5.457860623,5.129780365 679 | 4,395.5903955,470.3050847,5.450201557,5.38339302 680 | 4,391.2511013,659.722467,5.474969431,5.357479401 681 | 4,399.1230769,926.6483516,5.500405638,5.264817402 682 | 4,389.0163934,1810.912568,5.728359571,5.443576172 683 | 4,403.0814558,2222.696707,5.505535824,5.169853577 684 | 3,389.9861111,3734.916667,5.831495469,8.074147358 685 | 4,393.1169231,2646.501538,5.128823945,5.03773152 686 | 2,393.1121951,3539.639024,6.661099668,5.485970215 687 | 2,400.806391,4023.890977,5.432769388,5.173608381 688 | 4,395.9228487,2964.071217,5.48869246,4.943442413 689 | 4,398.6308244,428.8924731,5.485480826,6.232060648 690 | 4,393.6794872,1041.064103,5.64811616,5.325227656 691 | 4,398.4264151,5722.350943,5.603259992,5.162713747 692 | 3,394.8700565,260.1581921,5.512289041,7.169602243 693 | 4,394.2734375,1064.15625,5.895306352,5.303810067 694 | 4,409.4409722,2308.034722,5.440265926,5.234537817 695 | 4,393.1481481,837.2407407,5.179492121,5.715686647 696 | 2,400.9101124,4947.198502,5.922892935,5.475538548 697 | 4,398.3227848,217.3987342,5.486712216,5.09106741 698 | 4,401.7841727,2348.55036,5.312585593,4.992339445 699 | 4,403.56,2535.254545,5.278726717,5.272253359 700 | 2,405.6102941,4396.671569,7.740591936,5.18179732 701 | 4,401.3666667,1852.533333,5.210214485,6.118640369 702 | 2,404.751773,4661.382979,7.149638454,5.032290374 703 | 2,408.1810345,5390.367816,6.349611109,5.116478452 704 | 3,398.3727273,526.1181818,5.559527119,7.861086026 705 | 4,399.1743119,1199.93578,5.898237059,5.135474585 706 | 2,409.0858586,4080.217172,5.733373966,5.210106924 707 | 4,402.7318182,5991.159091,6.177651729,5.166161651 708 | 4,405.1965812,2132.62963,5.902703388,5.445127372 709 | 4,422.9971591,2255.045455,5.520409229,5.118282965 710 | 4,404.4040404,1825.626263,5.867769182,5.604705952 711 | 2,402.8203125,3929.25,6.35062663,7.094105169 712 | 3,406.5714286,4890.025641,5.766201076,7.769247061 713 | 4,411.3511706,5802.244147,5.459372014,5.159016856 714 | 4,403.6884058,1113.137681,5.90684162,4.914783905 715 | 2,412.3778409,1707.744318,5.833247847,5.942605489 716 | 4,412.7160494,294.8222222,5.685802876,5.384223194 717 | 2,406.2043796,3912.744526,6.978771406,5.986103487 718 | 4,411.1767068,5759.228916,5.612072233,5.469330241 719 | 4,414.8674033,5923.530387,5.540212186,5.050280906 720 | 2,416.0918033,4791.681967,5.741983332,5.981232045 721 | 2,411.3119266,5487.022936,6.364142636,5.182607868 722 | 1,420.3772321,4378.522321,7.872357319,5.303882211 723 | 4,404.9310345,736.0689655,5.465145962,4.757743267 724 | 4,420.1854305,1959.099338,5.507670907,4.77503404 725 | 4,409.2042254,5649.521127,5.266441445,5.068638421 726 | 4,412.6586538,511.3461538,4.993469572,6.243905477 727 | 4,408.1875,2486.984375,5.204092535,5.086920953 728 | 4,412.6438356,2186.520548,5.42247275,4.80844645 729 | 4,411.872093,2381.437984,5.468926544,5.112407986 730 | 4,418.5696594,2667.98452,5.420603492,5.002657217 731 | 4,411.1911765,73.36764706,5.609633385,5.061011501 732 | 4,413.4545455,634.1060606,5.635438743,5.248378816 733 | 2,419.9462366,3922.65233,6.504722694,7.025764494 734 | 2,427.9795918,5432.989796,7.797563737,4.974726099 735 | 4,426.12,5945.698182,5.607485141,6.141630676 736 | 3,418.1459854,486.4160584,5.501109235,7.118051695 737 | 4,421.9784615,660.7138462,5.601187363,6.511406948 738 | 2,423.143662,1362.647887,5.895496404,7.032932506 739 | 4,422.5512821,1152.028846,5.769532201,5.200301607 740 | 2,431.0209125,1897.614068,5.9182088,5.376246113 741 | 4,423.5412186,2474.401434,5.424065411,5.064815695 742 | 2,420.6529412,4470.647059,5.830605139,6.842758614 743 | 4,434.2453469,2751.475465,5.366971771,4.961305221 744 | 4,421.6949153,1052.618644,5.649004078,5.132302803 745 | 4,423.9372197,3211.439462,5.725795501,5.045967809 746 | 2,429.5231608,3834.93733,5.811369393,5.450196102 747 | 2,435.4803002,4565.637899,6.848805614,5.353759173 748 | 2,422.34375,5510.28125,7.308570705,5.386046802 749 | 4,431.9055375,72.52117264,5.602118821,5.201354356 750 | 4,428.1843318,583.4976959,6.514221019,5.302685692 751 | 2,434.515625,4865,7.062518271,5.260907513 752 | 4,425.7866667,5961.926667,5.576024858,5.07700547 753 | 3,424.8355263,1835.414474,5.843601624,6.936496169 754 | 4,426.557377,1855.169399,5.600019549,5.476623587 755 | 4,423.7612903,2832.877419,5.284349298,4.73625504 756 | 2,425.5821918,3464.993151,5.783909478,5.363893449 757 | 2,430.5940594,3631.788779,5.585293745,5.296500536 758 | 3,428.5645161,1804.693548,5.705501427,7.476326241 759 | 4,434.6112957,1820.747508,5.66717363,6.548404713 760 | 2,433.8533333,3880.326667,7.235030591,5.497618943 761 | 4,433.5616883,2500.857143,5.409905205,5.051000041 762 | 4,434.7453184,993.1910112,5.336889641,4.988826161 763 | 4,430.8423913,1102.434783,5.425141533,5.102745366 764 | 4,443.3284672,2019.036496,5.350161604,4.968392989 765 | 4,428.6666667,2379.154472,5.514935609,5.252826631 766 | 3,430.182266,3593.970443,6.19953947,7.590789923 767 | 4,434.2944785,5819.625767,5.494927963,5.198327543 768 | 4,431.2894737,152.3026316,5.814680263,5.222274325 769 | 4,436.8756219,2447.910448,5.127717882,4.844461268 770 | 4,432.757764,1786.645963,5.690758065,5.525155946 771 | 2,429.65,4525.975,6.006168413,5.112439494 772 | 4,430.9206349,5624.214286,5.625019812,5.149476638 773 | 4,433.7473684,1018.926316,5.518551426,5.070491819 774 | 4,436.2334906,2845.153302,5.474863631,5.053598391 775 | 4,437.8291457,619.160804,5.767126816,5.237029837 776 | 4,435.8257576,1173.219697,5.783661999,5.21093402 777 | 2,439.5315315,4535.86036,6.706796254,5.551719204 778 | 4,435.0215054,2325.344086,5.388236698,4.784260722 779 | 2,440.5274725,4300.293956,6.18718043,7.014977248 780 | 2,444.6680162,3479.093117,5.804793464,6.142463939 781 | 3,437.915493,4498.021127,5.912817579,7.28024604 782 | 3,444.1170569,1572.909699,6.069887729,7.750737102 783 | 4,439.8980583,1769.053398,5.670449894,5.380188126 784 | 4,443.5445205,2776.821918,5.558123039,5.132003979 785 | 2,447.5845697,4998.065282,5.993487722,5.311421048 786 | 4,454.689008,5990.152815,5.565362348,5.237539415 787 | 3,444.3731343,886.9402985,5.55999253,7.126075275 788 | 4,444.1421569,2132.754902,5.709972428,5.12992772 789 | 4,443.647482,480.4892086,5.694071204,5.359838751 790 | 3,443.3837209,1809.034884,5.608107887,7.928393455 791 | 3,442.1145833,3594.229167,5.988856984,7.928421041 792 | 2,456.7262873,3725.173442,5.761147632,5.448522993 793 | 2,450.2578397,1194.337979,5.800770056,6.253842213 794 | 4,452.7311558,2243.01005,5.348267423,5.040698108 795 | 2,451.4027778,3562.166667,5.441395062,5.237303491 796 | 2,447.7006803,4883.136054,6.918856695,4.944989584 797 | 2,452.3706294,5687.982517,5.169887822,6.352806033 798 | 2,450.0942408,3935.52356,6.002813516,6.275974053 799 | 4,451.5350554,5628.926199,5.965228539,5.328216454 800 | 4,451.461039,2645.876623,5.220285623,4.967736831 801 | 2,460.7115385,5398.381868,6.066222818,5.177979559 802 | 4,454.5945946,683.0540541,5.418595785,5.088263447 803 | 2,463.7264808,4041.632404,5.602318089,5.324428196 804 | 2,458.8393352,4167.916898,5.949870382,5.070923104 805 | 4,456.4132653,703.8673469,5.469866826,5.355695427 806 | 4,462.6493506,928.3636364,5.376119804,4.913537804 807 | 4,457.8148855,2077.694656,5.529633538,5.014203982 808 | 4,468.7837838,5840.675676,5.645867018,6.935596522 809 | 4,455.5350318,1082.305732,5.667439993,5.356165564 810 | 4,458.0487805,871.5691057,5.658688722,5.350470048 811 | 4,457.7663551,782.8411215,5.483166878,5.238527676 812 | 3,460.6082474,1801.902062,5.697058891,6.710303338 813 | 4,468.375,2593.969444,5.526194143,5.129586372 814 | 4,457.6147541,2451.098361,5.276147185,5.088809928 815 | 2,469.4244373,4712.926045,6.48393545,5.703450158 816 | 2,467.745614,4513.127193,5.789852863,6.961067368 817 | 4,465.9201521,472.418251,5.530936792,5.3486996 818 | 2,471.0610413,1276.260323,7.163159875,5.50820286 819 | 4,477.5594796,5761.968401,5.438378278,5.104525067 820 | 4,466.3402778,719.7847222,5.528822643,5.217047054 821 | 1,471.245614,1697.309942,9.021620142,5.420107956 822 | 4,470.4353933,1128.876404,5.700077116,5.289968001 823 | 4,462.8493151,290.0273973,5.451449916,5.568187637 824 | 2,465.0263158,3567.131579,5.819965791,5.230686349 825 | 2,485.9662338,5370.225974,5.461242058,5.297966656 826 | 4,466.8484848,1008.060606,5.371870531,4.976002022 827 | 4,474.384127,3268.177778,5.455809397,5.055639009 828 | 4,470.7916667,155.5119048,5.947708565,5.152913048 829 | 2,478.1008174,5325.457766,5.773899752,5.335413628 830 | 4,472.7837838,170.3581081,5.642769264,5.39052076 831 | 4,477.6280992,1212.024793,5.49171714,5.252727654 832 | 4,472.1376147,740.9174312,5.407500841,5.284181223 833 | 4,480.9607143,2114.5375,6.76553317,5.004665127 834 | 4,471.2287582,2764.650327,5.373582465,4.999461993 835 | 3,473.0933333,3921.52,6.133354658,8.262799742 836 | 3,483.1818182,300.8964646,5.588713555,7.690174012 837 | 4,478.4441489,336.7047872,5.450341928,5.50381681 838 | 2,483.6502242,4477.372197,6.105133501,6.410831086 839 | 2,477.0444444,3976.288889,5.741220974,6.554154969 840 | 4,485.1358025,782.6213992,5.666497883,5.341152149 841 | 2,484.037234,5144.861702,7.090971825,5.054109089 842 | 4,481.4734043,2475.111702,5.60050804,5.326713543 843 | 4,486.4464286,818.7366071,5.506891005,5.924219987 844 | 4,498.9325301,875.6987952,5.42149413,5.219938936 845 | 4,487.7272727,2284.169533,5.221722843,5.109631621 846 | 4,489.2019002,2413.665083,5.507636584,5.166359218 847 | 4,484.4645161,1078.883871,5.78239488,5.287647745 848 | 4,483.4823009,2735.048673,5.434822762,5.065615001 849 | 2,483.7777778,3448.011696,6.956567721,5.431446632 850 | 4,483.957672,2644.888889,5.484160054,5.035542965 851 | 2,489.8125,4862.441406,5.735096915,5.914189003 852 | 3,490.0762712,2201.889831,5.295644306,7.558625782 853 | 4,492.1643192,763.1126761,5.591844996,5.373712237 854 | 4,490.2038835,2445.975728,5.43514127,5.125580795 855 | 2,490.1538462,4155.906883,6.396794693,5.437022599 856 | 4,483.4583333,163.25,5.7448711,5.163594789 857 | 4,488.2517483,934.5804196,5.293515647,4.796021693 858 | 4,489.4855491,977.9075145,5.453021155,5.302269086 859 | 4,494.162963,191.8703704,5.479758907,6.826251937 860 | 4,487.7333333,1055.813333,5.572255421,4.954041427 861 | 2,501.4945652,1388.98913,5.672389019,5.288267031 862 | 4,491.3061224,579.3061224,5.666732646,5.208072567 863 | 2,502.4431555,1763.431555,5.703194521,6.563433288 864 | 4,497.114094,1792.097315,5.63396232,5.942402953 865 | 4,490.4827586,2693.238245,5.310221156,4.698997363 866 | 2,498.7808642,3924.614198,7.547410519,5.954477141 867 | 2,494.5183246,3970.465969,5.950178876,6.79451036 868 | 2,505.3724638,4240.947826,5.607665206,4.965557073 869 | 4,490.7826087,620.8913043,5.54755234,5.15140492 870 | 3,496.8191489,2014.028369,5.446660944,8.459617419 871 | 2,499.5,4454.971631,7.234660622,5.444886185 872 | 2,497.5830816,4572.090634,7.381296827,5.764992199 873 | 4,496.8200837,5734.121339,5.486219557,5.310729555 874 | 4,492.94375,2668.80625,5.38609928,4.894288726 875 | 2,498.1609756,4501.156098,6.056726892,6.557200939 876 | 4,493.4727273,1036.309091,5.568795478,5.061386017 877 | 4,503.2899023,519.7100977,5.472010601,5.054937869 878 | 4,497.1913043,1088.478261,5.654774896,5.133237616 879 | 2,502.8740741,3654.851852,5.799070208,5.731517729 880 | 2,499.8013699,1282.431507,6.689701493,5.010772271 881 | 4,500.281746,2590.087302,5.374727109,4.882380959 882 | 2,505.3,4104.494872,5.707595708,7.151483454 883 | 4,501.7904762,417.0785714,5.586410069,5.285726529 884 | 2,501.4570552,1547.018405,5.854658737,6.809939509 885 | 4,500.0828025,1813.286624,5.527480977,5.312295647 886 | 4,517.225731,2435.561404,5.38353962,5.172667946 887 | 4,499.5267176,467.648855,5.534595652,5.289615493 888 | 3,505.4148148,729.3481481,5.603443623,7.508969902 889 | 2,504.7581395,4547.744186,5.823004612,6.512781456 890 | 4,501.9212121,331.830303,5.222875171,5.172685427 891 | 2,510.3741497,1920.534014,5.608362311,5.27863514 892 | 4,502.745614,958.8947368,5.434066692,5.060705619 893 | 2,511.0677291,5501.541833,6.186962525,5.380576433 894 | 4,505.573913,2314.443478,5.176837288,5.071253096 895 | 2,503.2518519,3565.696296,5.741910473,5.151534539 896 | 4,504.0471698,1048.415094,5.690136329,5.265423765 897 | 2,504.8709677,3954.763441,6.655869259,6.522282533 898 | 2,505.2352941,4123.421569,5.615701007,6.677947414 899 | 4,508.78,753.2,5.605985873,5.612690054 900 | 4,506.2447368,2477.626316,5.514086425,5.280943629 901 | 4,510.5154185,611.6211454,5.830266331,5.282017847 902 | 4,510.6014493,5703.543478,5.49571195,4.974432245 903 | 4,510.2244898,2606.591837,5.523879759,5.060741944 904 | 4,508.9099099,1005.477477,5.256885201,4.989819541 905 | 4,510.9722222,1112.375,5.669617478,5.488047753 906 | 4,508.8441558,2673.649351,5.280298861,5.860305107 907 | 2,517.4814815,4693.639506,6.470765103,5.348764006 908 | 2,519.4433333,4015.426667,5.688217522,6.792344427 909 | 2,526.1593407,5402.296703,5.984123104,4.995447278 910 | 4,519.862069,713.3103448,5.176376994,6.034380899 911 | 4,529.607362,2145.18773,5.711109351,4.902699322 912 | 2,524.4305085,1721.966102,7.409050206,4.957687346 913 | 2,516.6353591,3379.558011,5.869920997,5.615947052 914 | 4,531.9308176,5978.287212,5.814949996,4.943258367 915 | 4,524.5637584,219.7583893,5.267529454,5.04966239 916 | 4,530.0650602,5547.672289,7.653532631,5.1060769 917 | 4,545.2632509,892.869258,5.297804868,4.981656418 918 | 4,518.4662162,975.9594595,5.206083531,4.789404327 919 | 4,527.460251,1806.937238,5.5831974,5.689199673 920 | 2,521.8214286,1392.8125,5.811915928,5.200517483 921 | 4,521.0932203,5644.305085,5.483706248,5.152213322 922 | 4,531.7464789,565.4676056,5.563494572,5.289731316 923 | 2,523.2121212,3935.310606,5.686692831,6.077972325 924 | 2,522.0277778,1129.902778,6.856037918,6.3609186 925 | 2,531.9756098,4199.436314,6.5035333,5.144180237 926 | 4,528.8588235,5998.376471,5.422199412,4.76408262 927 | 4,525.1682243,538.6074766,5.090994271,4.987886274 928 | 4,535.7920228,1870.387464,5.344268067,5.049289699 929 | 4,525.9954128,2571.013761,5.401643705,5.068586801 930 | 4,522.36,2736.97,5.21373939,4.526667958 931 | 2,532.0658436,4445.139918,7.345000806,5.255993902 932 | 4,525.9,5516.8,6.430149493,5.057872224 933 | 4,533.2826855,114.7526502,5.931157344,5.278547248 934 | 4,528.8846154,1030.412088,5.584982439,5.235652547 935 | 4,527.5416667,656.7,5.602887283,5.312959643 936 | 2,538.8688889,5075.986667,5.808856283,5.162472188 937 | 2,541.8922717,5744.470726,5.425115073,6.404252504 938 | 4,534.6294964,3613.18705,5.745754886,5.826106212 939 | 4,540.6653061,5946.787755,5.2817316,4.921975063 940 | 3,534.6993007,1122.160839,5.824988808,7.803566399 941 | 2,542.2981928,4262.228916,7.609899515,5.293123177 942 | 2,539.6870748,4144.244898,5.273348355,5.836559615 943 | 4,533.3333333,580.2592593,5.605665891,5.145986018 944 | 2,545.1367781,3986.167173,5.706504198,7.087075884 945 | 4,534.6825397,5931.47619,5.308110526,5.816544106 946 | 4,540.5536232,2509.069565,5.431758175,5.134040712 947 | 4,537.6554622,1086.117647,5.564295004,5.315273633 948 | 4,543.3165468,1777.302158,5.639186398,5.936881105 949 | 2,544.2241993,3396.13879,6.651815529,5.250016407 950 | 2,539.3553719,3543.045455,6.305732219,5.321537418 951 | 4,542.2594595,1055.735135,5.557662525,5.143621493 952 | 4,539.4918033,2713.661202,5.201872664,4.683496415 953 | 3,545.8243243,1133.954955,5.752872894,7.099276134 954 | 4,543.3096234,5509.677824,6.208733134,5.225296669 955 | 4,542.4054054,369.9369369,5.183452367,5.102992716 956 | 3,544.8159722,644.6215278,6.464626162,7.382480815 957 | 4,544.5,946.9294118,5.140148822,4.703470543 958 | 4,542.6803279,2666.409836,5.449330812,5.134978429 959 | 2,548.9353612,4157.21673,5.892211242,6.673716083 960 | 4,543.2922078,5613.441558,5.556050544,5.17563599 961 | 1,555.3586698,4491.72209,8.700650682,5.495307143 962 | 2,552.3719008,4789.418733,7.45951185,5.451853926 963 | 4,549.84,77.76,5.987466566,5.195376144 964 | 4,547.7477876,540.1371681,5.495534801,5.346306536 965 | 3,546.8699187,197.8373984,4.943267118,7.209388362 966 | 4,550.078853,244.0573477,5.450022663,5.20736863 967 | 2,556.9046243,1356.528902,7.436850564,5.208996441 968 | 2,552.0817308,1905.096154,5.399162897,5.296368353 969 | 4,552.8888889,438.5340502,5.833940502,5.289352571 970 | 2,558.2033333,4519.316667,5.737475116,6.972821786 971 | 3,558.5955882,5462.754902,5.464081026,7.416378479 972 | 4,558.3745318,5875.640449,5.363136747,5.327821624 973 | 2,551.5789474,3358.070175,5.142893947,4.715282552 974 | 4,563.3888889,3202.682222,8.79078141,5.170281954 975 | 2,564.131016,4574.229947,7.198873287,5.284370289 976 | 2,557.8203883,3509.053398,5.746342763,5.42051352 977 | 4,554.8,5599.388889,5.564264947,5.124625136 978 | 4,559.5588235,689.710084,5.643440551,5.345846231 979 | 2,556.2058824,3443.327731,5.56011586,5.500383192 980 | 2,560.3121951,3598.370732,5.762373466,6.42159054 981 | 2,559.5531915,3948.728723,5.890276756,6.944107725 982 | 4,558.0402145,2700.428954,5.520699234,4.956883781 983 | 4,559.8811881,1038.881188,5.383222567,5.202918074 984 | 4,562.0816832,2578.774752,5.340698541,4.835731701 985 | 4,572.0852713,5882.333333,5.402013708,5.186410925 986 | 2,570.1231343,1926.914179,5.271467578,4.940709151 987 | 4,572.9732824,1174.89313,8.525314324,5.27867923 988 | 2,565.9921875,1396.65625,7.217752903,5.209827625 989 | 4,568.5873418,3041.106329,5.401205207,5.330159415 990 | 2,567.4732143,4148.410714,5.955235782,6.681638562 991 | 4,574.7692308,1959.99095,6.883726812,4.981217357 992 | 2,569.1557789,3446.688442,5.697531824,6.070749333 993 | 4,573.426009,645.8026906,5.751190803,5.338464694 994 | 4,572.6948052,975.9025974,4.624463389,4.586159453 995 | 2,578.7013889,3348.017361,5.829354031,5.9571139 996 | 2,580.3079019,4708.422343,5.763592532,5.144416428 997 | 4,580.1619938,456.7133956,5.402480904,5.196343288 998 | 4,577.6092437,1095.642857,5.662756211,5.454404945 999 | 4,579.670412,2412.868914,5.422711873,5.205815972 1000 | 4,586.6543909,575.8130312,5.765057887,5.093698061 1001 | 4,576.7282609,3081.173913,5.507714696,5.058389725 --------------------------------------------------------------------------------