├── .gitattributes
├── .gitignore
├── Documentation
├── Examples
│ └── example1_getting-started
│ │ ├── EDT.csv
│ │ ├── Layout.csv
│ │ ├── analyse_qc
│ │ ├── p1_ammonium.pdf
│ │ ├── p2_valine.pdf
│ │ ├── p3_glycine.pdf
│ │ └── p4_arginine.pdf
│ │ ├── commands.txt
│ │ ├── images
│ │ ├── p1.jpg
│ │ ├── p2.jpg
│ │ ├── p3.jpg
│ │ └── p4.jpg
│ │ ├── pyphe-analyse_data_report.csv
│ │ ├── pyphe-quantify-report_reps.csv
│ │ ├── pyphe-quantify-report_summaryStats.csv
│ │ ├── pyphe_quant
│ │ ├── p1.jpg.csv
│ │ ├── p2.jpg.csv
│ │ ├── p3.jpg.csv
│ │ └── p4.jpg.csv
│ │ └── qc_images
│ │ ├── qc_p1.jpg.png
│ │ ├── qc_p2.jpg.png
│ │ ├── qc_p3.jpg.png
│ │ └── qc_p4.jpg.png
├── Growthcurves
│ ├── example_data.csv
│ ├── example_data_curves.pdf
│ └── example_data_results.csv
└── Scan
│ ├── cuttingVectors_fixture_petrieDishes.svg
│ └── cuttingVectors_fixture_som3.svg
├── LICENSE
├── README.md
├── bin
├── pyphe-analyse
├── pyphe-analyse-gui
├── pyphe-analyse.bat
├── pyphe-growthcurves
├── pyphe-growthcurves.bat
├── pyphe-interpret
├── pyphe-interpret.bat
├── pyphe-quantify
├── pyphe-quantify.bat
├── pyphe-scan
└── pyphe-scan-timecourse
├── icons
├── gui.png
├── toolbox-72dpi_tp.png
├── toolbox-72dpi_white.png
└── toolbox_icon-01.png
├── pyphe
├── __init__.py
├── __pycache__
│ └── quantify.cpython-37.pyc
├── analysis.py
├── growthcurves.py
├── interpret.py
├── quantify.py
└── scan.py
├── setup.py
└── test
├── edt.csv
├── example-pipeline.bat
├── example-pipeline.sh
├── images
├── p1_53.jpg
├── p1_72.jpg
├── p1_91.jpg
└── timepoints.txt
├── layout_wide.csv
├── pyphe-analyse_data_report.csv
├── pyphe-quantify-report_reps.csv
├── pyphe-quantify-report_summaryStats.csv
├── pyphe_quant
├── p1_53.jpg.csv
├── p1_72.jpg.csv
└── p1_91.jpg.csv
├── qc_images
├── qc_p1_53.jpg.png
├── qc_p1_72.jpg.png
└── qc_p1_91.jpg.png
└── timecourse_quant
└── p1_91.jpg.csv
/.gitattributes:
--------------------------------------------------------------------------------
1 | #Stop git from changing line endings of executables to Windows-style. They still work under Windows.
2 | bin/* text eol=lf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/*
2 | pyphe.egg-info/*
3 | dist/*
4 |
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/EDT.csv:
--------------------------------------------------------------------------------
1 | ,Data_path,Image_path,Condition,Layout_path,Processing_date,Notes
2 | p1_ammonium,pyphe_quant/p1.jpg.csv,images/p1.jpg,ammonium,Layout.csv,24.5.2020,
3 | p2_valine,pyphe_quant/p2.jpg.csv,images/p2.jpg,valine,Layout.csv,24.5.2020,some comment
4 | p3_glycine,pyphe_quant/p3.jpg.csv,images/p3.jpg,glycine,Layout.csv,24.5.2020,
5 | p4_arginine,pyphe_quant/p4.jpg.csv,images/p4.jpg,arginine,Layout.csv,24.5.2020,
6 |
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/Layout.csv:
--------------------------------------------------------------------------------
1 | grid,JB1171,JB1174,JB899,grid,JB845,JB762,JB939,grid,JB837,JB943,JB1174,grid,JB760,JB931,JB1206,grid,JB918,JB902,JB864,grid,JB1180,JB872,JB917,grid,JB841,JB22,JB22,grid,JB872,JB842,JB930,grid,JB862,JB930,JB913,grid,JB858,JB875,JB848,grid,JB942,JB846,JB858,grid,JB873,JB872,JB913
2 | JB1205,JB842,JB872,JB916,JB875,JB762,JB916,JB1207,JB943,JB1174,JB854,JB852,JB1154,JB852,JB758,JB914,JB902,JB871,JB762,JB1197,JB1174,JB846,JB22,JB758,JB1171,JB1207,JB872,JB934,JB899,JB879,JB854,JB848,JB848,JB934,JB853,JB846,JB845,JB842,JB1206,JB1174,JB902,JB1154,JB1174,JB845,JB4,JB22,JB838,JB4
3 | JB871,extragrid,JB875,JB1171,JB942,extragrid,JB862,JB845,JB900,extragrid,JB838,JB837,JB858,extragrid,JB1117,JB760,JB22,extragrid,JB862,JB918,JB929,extragrid,JB939,JB1180,JB22,extragrid,JB899,JB841,JB913,extragrid,JB758,JB872,JB884,extragrid,JB874,JB862,JB858,extragrid,JB4,JB858,JB841,extragrid,JB953,JB942,JB934,extragrid,JB838,JB873
4 | JB1174,JB899,JB1205,grid,JB762,JB939,JB875,grid,JB943,JB1174,JB943,grid,JB931,JB1206,JB1154,grid,JB902,JB864,JB902,grid,JB872,JB917,JB1174,grid,JB22,JB22,JB1171,grid,JB842,JB930,JB899,grid,JB930,JB913,JB848,grid,JB875,JB848,JB845,grid,JB846,JB858,JB902,grid,JB872,JB913,JB4,grid
5 | grid,JB939,JB869,JB840,grid,JB853,JB22,JB846,grid,JB918,JB931,JB1180,grid,JB842,JB22,JB916,grid,JB913,JB884,JB837,grid,JB758,JB854,JB1174,grid,JB4,JB864,JB873,grid,JB902,JB934,JB914,grid,JB1207,JB1197,JB874,grid,JB22,JB943,JB22,grid,JB842,JB853,JB1205,grid,JB1205,JB1197,JB938
6 | JB862,JB4,JB869,JB1180,JB842,JB846,JB884,JB930,JB842,JB874,JB841,JB840,JB853,JB938,JB22,JB1207,JB942,JB840,JB848,JB943,JB1180,JB1110,JB873,JB837,JB938,JB837,JB1197,JB22,JB22,JB899,JB884,JB22,JB22,JB953,JB910,JB873,JB875,JB840,JB852,JB1174,JB916,JB934,JB858,JB864,JB845,JB934,JB1180,JB845
7 | JB1154,extragrid,JB910,JB939,JB22,extragrid,JB840,JB853,JB838,extragrid,JB943,JB918,JB1205,extragrid,JB1110,JB842,JB846,extragrid,JB873,JB913,JB931,extragrid,JB1171,JB758,JB1171,extragrid,JB914,JB4,JB938,extragrid,JB878,JB902,JB864,extragrid,JB1207,JB1207,JB1197,extragrid,JB875,JB22,JB939,extragrid,JB1205,JB842,JB845,extragrid,JB1180,JB1205
8 | JB869,JB840,JB862,grid,JB22,JB846,JB842,grid,JB931,JB1180,JB842,grid,JB22,JB916,JB853,grid,JB884,JB837,JB942,grid,JB854,JB1174,JB1180,grid,JB864,JB873,JB938,grid,JB934,JB914,JB22,grid,JB1197,JB874,JB22,grid,JB943,JB22,JB875,grid,JB853,JB1205,JB916,grid,JB1197,JB938,JB845,grid
9 | grid,JB841,JB1117,JB875,grid,JB838,JB918,JB917,grid,JB852,JB917,JB840,grid,JB929,JB22,JB22,grid,JB838,JB845,JB762,grid,JB1110,JB879,JB1207,grid,JB864,JB934,JB838,grid,JB910,JB871,JB1117,grid,JB760,JB869,JB854,grid,JB869,JB22,JB884,grid,JB910,JB848,JB22,grid,JB910,JB848,JB929
10 | JB22,JB870,JB871,JB939,JB939,JB1205,JB872,JB871,JB929,JB1154,JB841,JB910,JB943,JB1206,JB1110,JB879,JB931,JB862,JB918,JB841,JB939,JB871,JB914,JB841,JB854,JB872,JB841,JB953,JB953,JB884,JB938,JB1206,JB916,JB864,JB918,JB902,JB869,JB929,JB913,JB900,JB913,JB942,JB930,JB931,JB848,JB914,JB943,JB848
11 | JB902,extragrid,JB875,JB841,JB879,extragrid,JB22,JB838,JB1180,extragrid,JB862,JB852,JB842,extragrid,JB848,JB929,JB1171,extragrid,JB842,JB838,JB854,extragrid,JB1110,JB1110,JB22,extragrid,JB934,JB864,JB913,extragrid,JB22,JB910,JB874,extragrid,JB840,JB760,JB758,extragrid,JB879,JB869,JB878,extragrid,JB931,JB910,JB838,extragrid,JB943,JB910
12 | JB1117,JB875,JB22,grid,JB918,JB917,JB939,grid,JB917,JB840,JB929,grid,JB22,JB22,JB943,grid,JB845,JB762,JB931,grid,JB879,JB1207,JB939,grid,JB934,JB838,JB854,grid,JB871,JB1117,JB953,grid,JB869,JB854,JB916,grid,JB22,JB884,JB869,grid,JB848,JB22,JB913,grid,JB848,JB929,JB848,grid
13 | grid,JB1171,JB902,JB845,grid,JB1154,JB838,JB910,grid,JB4,JB872,JB1206,grid,JB873,JB879,JB939,grid,JB899,JB762,JB942,grid,JB879,JB942,JB878,grid,JB1110,JB852,JB1206,grid,JB913,JB953,JB1154,grid,JB1180,JB878,JB4,grid,JB916,JB1171,JB22,grid,JB878,JB953,JB874,grid,JB878,JB22,JB869
14 | JB1117,JB930,JB845,JB953,JB1171,JB22,JB862,JB917,JB871,JB879,JB918,JB942,JB760,JB1207,JB939,JB929,JB942,JB864,JB910,JB913,JB758,JB840,JB917,JB760,JB22,JB878,JB838,JB22,JB929,JB870,JB22,JB875,JB1207,JB875,JB4,JB22,JB930,JB858,JB1180,JB854,JB914,JB874,JB845,JB934,JB884,JB22,JB862,JB884
15 | JB916,extragrid,JB858,JB1171,JB853,extragrid,JB918,JB1154,JB913,extragrid,JB22,JB4,JB22,extragrid,JB870,JB873,JB853,extragrid,JB930,JB899,JB854,extragrid,JB852,JB879,JB22,extragrid,JB871,JB1110,JB1117,extragrid,JB1154,JB913,JB852,extragrid,JB760,JB1180,JB1180,extragrid,JB943,JB916,JB846,extragrid,JB762,JB878,JB917,extragrid,JB862,JB878
16 | JB902,JB845,JB1117,grid,JB838,JB910,JB1171,grid,JB872,JB1206,JB871,grid,JB879,JB939,JB760,grid,JB762,JB942,JB942,grid,JB942,JB878,JB758,grid,JB852,JB1206,JB22,grid,JB953,JB1154,JB929,grid,JB878,JB4,JB1207,grid,JB1171,JB22,JB930,grid,JB953,JB874,JB914,grid,JB22,JB869,JB884,grid
17 | grid,JB942,JB22,JB848,grid,JB930,JB4,JB929,grid,JB934,JB878,JB1154,grid,JB846,JB900,JB862,grid,JB758,JB900,JB871,grid,JB853,JB1171,JB862,grid,JB900,JB914,JB862,grid,JB913,JB22,JB842,grid,JB1117,JB22,JB870,grid,JB837,JB870,JB1197,grid,JB1174,JB869,JB939,grid,JB1174,JB918,JB1206
18 | JB878,JB1110,JB853,JB1110,JB918,JB931,JB1171,JB1117,JB942,JB838,JB862,JB760,JB931,JB1117,JB22,JB4,JB762,JB1197,JB878,JB938,JB931,JB864,JB1206,JB874,JB914,JB758,JB873,JB864,JB900,JB760,JB846,JB858,JB938,JB874,JB22,JB943,JB760,JB848,JB22,JB938,JB884,JB1207,JB910,JB902,JB913,JB910,JB871,JB913
19 | JB900,extragrid,JB914,JB942,JB854,extragrid,JB916,JB930,JB869,extragrid,JB1206,JB934,JB902,extragrid,JB879,JB846,JB760,extragrid,JB874,JB758,JB22,extragrid,JB931,JB853,JB842,extragrid,JB873,JB900,JB841,extragrid,JB929,JB913,JB942,extragrid,JB837,JB1117,JB1180,extragrid,JB22,JB837,JB899,extragrid,JB910,JB1174,JB1171,extragrid,JB871,JB1174
20 | JB22,JB848,JB878,grid,JB4,JB929,JB918,grid,JB878,JB1154,JB942,grid,JB900,JB862,JB931,grid,JB900,JB871,JB762,grid,JB1171,JB862,JB931,grid,JB914,JB862,JB914,grid,JB22,JB842,JB900,grid,JB22,JB870,JB938,grid,JB870,JB1197,JB760,grid,JB869,JB939,JB884,grid,JB918,JB1206,JB913,grid
21 | grid,JB914,JB1205,JB858,grid,JB884,JB22,JB871,grid,JB953,JB864,JB845,grid,JB22,JB841,JB943,grid,JB840,JB870,JB852,grid,JB914,JB931,JB871,grid,JB930,JB22,JB938,grid,JB1180,JB1154,JB873,grid,JB938,JB929,JB1110,grid,JB1207,JB916,JB899,grid,JB22,JB900,JB931,grid,JB943,JB1110,JB760
22 | JB22,JB22,JB22,JB1205,JB943,JB864,JB1197,JB1197,JB758,JB4,JB900,JB864,JB1154,JB852,JB1197,JB845,JB899,JB22,JB838,JB22,JB870,JB22,JB1117,JB869,JB869,JB837,JB1180,JB939,JB902,JB838,JB858,JB870,JB762,JB840,JB917,JB837,JB878,JB1110,JB870,JB917,JB4,JB913,JB900,JB4,JB875,JB22,JB858,JB875
23 | JB1110,extragrid,JB1206,JB914,JB4,extragrid,JB914,JB884,JB22,extragrid,JB1174,JB953,JB910,extragrid,JB878,JB22,JB899,extragrid,JB848,JB840,JB953,extragrid,JB853,JB914,JB918,extragrid,JB884,JB930,JB762,extragrid,JB845,JB1180,JB869,extragrid,JB872,JB938,JB873,extragrid,JB872,JB1207,JB22,extragrid,JB1154,JB22,JB22,extragrid,JB858,JB943
24 | JB1205,JB858,JB22,grid,JB22,JB871,JB943,grid,JB864,JB845,JB758,grid,JB841,JB943,JB1154,grid,JB870,JB852,JB899,grid,JB931,JB871,JB870,grid,JB22,JB938,JB869,grid,JB1154,JB873,JB902,grid,JB929,JB1110,JB762,grid,JB916,JB899,JB878,grid,JB900,JB931,JB4,grid,JB1110,JB760,JB875,grid
25 | grid,JB854,JB874,JB1205,grid,JB929,JB1206,JB846,grid,JB934,JB870,JB852,grid,JB758,JB858,JB1117,grid,JB899,JB22,JB917,grid,JB853,JB943,JB879,grid,JB1205,JB22,JB938,grid,JB22,JB875,JB910,grid,JB938,JB1207,JB758,grid,JB854,JB762,JB837,grid,JB840,JB874,JB1197,grid,JB1171,JB873,JB840
26 | JB1206,JB837,JB902,JB942,JB760,JB943,JB917,JB852,JB939,JB858,JB854,JB884,JB853,JB1117,JB22,JB762,JB934,JB1205,JB1171,JB1207,JB22,JB22,JB916,JB1174,JB846,JB837,JB900,JB837,JB1205,JB841,JB913,JB1206,JB848,JB869,JB22,JB22,JB22,JB953,JB930,JB899,JB879,JB879,JB873,JB862,JB842,JB1180,JB917,JB842
27 | JB848,extragrid,JB938,JB854,JB22,extragrid,JB1197,JB929,JB758,extragrid,JB870,JB934,JB1117,extragrid,JB918,JB758,JB22,extragrid,JB838,JB899,JB853,extragrid,JB871,JB853,JB884,extragrid,JB842,JB1205,JB870,extragrid,JB930,JB22,JB22,extragrid,JB1205,JB938,JB872,extragrid,JB900,JB854,JB916,extragrid,JB872,JB840,JB1154,extragrid,JB917,JB1171
28 | JB874,JB1205,JB1206,grid,JB1206,JB846,JB760,grid,JB870,JB852,JB939,grid,JB858,JB1117,JB853,grid,JB22,JB917,JB934,grid,JB943,JB879,JB22,grid,JB22,JB938,JB846,grid,JB875,JB910,JB1205,grid,JB1207,JB758,JB848,grid,JB762,JB837,JB22,grid,JB874,JB1197,JB879,grid,JB873,JB840,JB842,grid
29 | grid,JB916,JB869,JB1206,grid,JB875,JB931,JB870,grid,JB902,JB846,JB845,grid,JB22,JB914,JB840,grid,JB22,JB22,JB864,grid,JB22,JB878,JB875,grid,JB841,JB902,JB854,grid,JB760,JB871,JB874,grid,JB760,JB934,JB938,grid,JB953,JB858,JB872,grid,JB884,JB853,JB918,grid,JB884,JB1174,JB853
30 | JB852,JB846,JB875,JB878,JB953,JB899,JB930,JB918,JB875,JB838,JB852,JB762,JB874,JB910,JB4,JB869,JB22,JB1180,JB762,JB846,JB873,JB1174,JB943,JB22,JB917,JB878,JB760,JB930,JB899,JB929,JB1171,JB840,JB1174,JB22,JB902,JB917,JB929,JB938,JB953,JB22,JB845,JB1207,JB1154,JB929,JB864,JB853,JB900,JB864
31 | JB846,extragrid,JB875,JB916,JB899,extragrid,JB930,JB875,JB838,extragrid,JB852,JB902,JB910,extragrid,JB4,JB22,JB1180,extragrid,JB762,JB22,JB1174,extragrid,JB943,JB22,JB878,extragrid,JB760,JB841,JB929,extragrid,JB1171,JB760,JB22,extragrid,JB902,JB760,JB938,extragrid,JB953,JB953,JB1207,extragrid,JB1154,JB884,JB853,extragrid,JB900,JB884
32 | JB869,JB1206,JB852,grid,JB931,JB870,JB953,grid,JB846,JB845,JB875,grid,JB914,JB840,JB874,grid,JB22,JB864,JB22,grid,JB878,JB875,JB873,grid,JB902,JB854,JB917,grid,JB871,JB874,JB899,grid,JB934,JB938,JB1174,grid,JB858,JB872,JB929,grid,JB853,JB918,JB845,grid,JB1174,JB853,JB864,grid
33 |
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/analyse_qc/p1_ammonium.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/analyse_qc/p1_ammonium.pdf
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/analyse_qc/p2_valine.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/analyse_qc/p2_valine.pdf
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/analyse_qc/p3_glycine.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/analyse_qc/p3_glycine.pdf
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/analyse_qc/p4_arginine.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/analyse_qc/p4_arginine.pdf
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/commands.txt:
--------------------------------------------------------------------------------
1 | pip install pyphe
2 | pyphe-quantify batch --grid auto_1536 --pattern images/*.jpg
3 | pyphe-analyse --edt EDT.csv --format pyphe-quantify-batch --load_layouts --gridnorm standard1536 --qc_plots analyse-qc
4 | pyphe-interpret --ld pyphe-analyse_data_report.csv --set_missing_na --circularity 0.85 --values_column Colony_size_corr_checked --control JB22 --grouping_column Condition --axis_column Strain --out example1_results
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/images/p1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/images/p1.jpg
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/images/p2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/images/p2.jpg
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/images/p3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/images/p3.jpg
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/images/p4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/images/p4.jpg
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/qc_images/qc_p1.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/qc_images/qc_p1.jpg.png
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/qc_images/qc_p2.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/qc_images/qc_p2.jpg.png
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/qc_images/qc_p3.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/qc_images/qc_p3.jpg.png
--------------------------------------------------------------------------------
/Documentation/Examples/example1_getting-started/qc_images/qc_p4.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Examples/example1_getting-started/qc_images/qc_p4.jpg.png
--------------------------------------------------------------------------------
/Documentation/Growthcurves/example_data_curves.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/Documentation/Growthcurves/example_data_curves.pdf
--------------------------------------------------------------------------------
/Documentation/Growthcurves/example_data_results.csv:
--------------------------------------------------------------------------------
1 | ,1-1,1-2,1-3,1-4,1-5,1-6,1-7,1-8,1-9,1-10,1-11,1-12,1-13,1-14,1-15,1-16,1-17,1-18,1-19,1-20,1-21,1-22,1-23,1-24,1-25,1-26,1-27,1-28,1-29,1-30,1-31,1-32
2 | initial biomass,7074.859778391597,3607.6300731937868,4360.159036489436,7075.325161907976,6198.008129028164,6056.0717501300505,6992.193194370163,4809.051683652413,7214.309468301013,5032.852968854178,2690.336628885292,4479.25704074381,5871.828457997221,4646.18094450998,3968.127120775851,5621.939006352255,6996.90264498939,6470.277146394897,5433.785603805981,5200.630004225067,6858.296150109537,4225.495234076018,4126.084634482896,8536.075382515066,6557.470064108672,6836.179036147522,6145.002443505,6353.059498120703,6504.451021220583,4199.735376702544,5631.9535346223465,5890.662214569888
3 | lag,5.0056972744444455,5.3632550375,5.720820565833334,5.3632550375,5.3632550375,6.0783475294444465,6.7933625736111125,6.435824887222222,5.3632550375,4.648176249444445,3.9331011188888887,5.0056972744444455,4.648176249444445,6.435824887222222,3.9331011188888887,5.0056972744444455,5.3632550375,5.720820565833334,6.0783475294444465,5.720820565833334,5.3632550375,4.648176249444445,6.435824887222222,5.3632550375,5.0056972744444455,6.0783475294444465,5.3632550375,5.3632550375,4.648176249444445,6.435824887222222,5.720820565833334,5.3632550375
4 | t_max,15.910511840694445,13.407750091388888,13.765273545555557,15.195430311875,11.262646918055557,13.765273545555557,14.837889865833334,14.122803726944445,12.335190503055555,11.262646918055557,9.474990026180556,13.050222638402776,12.335190503055555,13.407750091388888,11.262646918055557,11.620166851111113,12.335190503055555,11.620166851111113,13.050222638402776,11.620166851111113,11.977677731180556,11.262646918055557,13.407750091388888,10.547593299375002,10.905123709513889,13.765273545555557,12.335190503055555,12.335190503055555,11.977677731180556,12.692707312847219,13.050222638402776,11.620166851111113
5 | max_slope,5894.851857450046,4566.990596590272,4683.311498650035,8309.566870174607,5106.708813638968,4815.339335574547,6575.646444826038,4385.613779976352,5727.558609673571,4501.046811262408,2340.813413839716,5003.272065008457,5638.866910582268,4839.760738613498,4999.492160228607,4886.871502426089,5745.236896442147,5691.050321535506,4936.26574596201,2878.116971732706,5634.715986286868,5237.481748675316,4317.212049653754,6811.098482137898,5714.369412495296,5360.758875306342,5698.446815685878,5727.923602821376,6055.7716083913765,4240.8905720024995,5731.552122090007,5428.602062687098
6 | r2,0.9987858376749732,0.998212797984864,0.9997337334083815,0.9998623874452355,0.9994376157291265,0.9992963325856415,0.9994820239465474,0.9993647320244001,0.9996523706138992,0.9995421398680778,0.9987200755118851,0.9994680918754553,0.9999048002704912,0.9992894094802306,0.9996343359513821,0.9990161542465967,0.9990305159498728,0.9992835066901002,0.9991572901215414,0.9998626048793694,0.9995771690781998,0.9994800069533354,0.9990222797232706,0.999998961642427,0.9997732593781425,0.9985915355075672,0.9996972109611285,0.9991007897103484,0.9986642541208497,0.9997469094088604,0.999164051285295,0.999682581784259
7 | y-intercept,-30893.761056879826,-28989.633851064813,-27465.601744573483,-52583.55718564414,-23077.756885527488,-24405.134520268133,-43875.825029417276,-27160.27102996924,-25687.626172756834,-16208.419412341842,-6214.827109820799,-24404.348621354475,-24839.116523895696,-31219.270191087773,-23349.35148316961,-21673.429190368413,-23877.733854806895,-27120.64902978143,-27138.75270928228,-9483.772663215728,-23147.28612176547,-24663.524218057748,-27586.265051549497,-25952.803261728564,-23405.948488131464,-29153.529457702003,-27249.29689736072,-27071.984934412176,-25531.803389160617,-26124.37087225009,-32661.965298221738,-24879.7362552356
8 | x-intercept,5.240803637471499,6.347644742844129,5.864568639624002,6.328074375859642,4.51910569560801,5.068206583068608,6.6724732537803035,6.19303759806129,4.484917208768788,3.6010332911414156,2.654986114261199,4.877677708560353,4.404983646143692,6.450581315313434,4.670344653986203,4.435031528782501,4.156092130786401,4.765490989800981,5.497830567870553,3.295131072280997,4.107977434550154,4.7090425134742935,6.389833238272823,3.8103696973094396,4.095980990824809,5.438321352596934,4.781881410624507,4.726317390315306,4.216110685842518,6.16011435067858,5.698624840615,4.583083447255002
9 | warning_negative_slope,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
10 | warning_bad_fit,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
11 |
--------------------------------------------------------------------------------
/Documentation/Scan/cuttingVectors_fixture_petrieDishes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
--------------------------------------------------------------------------------
/Documentation/Scan/cuttingVectors_fixture_som3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Bahler-Lab
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Welcome to the pyphe toolbox
4 | A python toolbox for phenotype analysis of arrayed microbial colonies written by Stephan Kamrad (stephan.kamrad at crick.ac.uk).
5 |
6 | For a quick overview, please see our [10 minute video tutorial](https://www.youtube.com/watch?v=lQ3lXIdhA1c&t=5s).
7 |
8 | For a more detailed protocol, including growth curves and viability assays, please see our [protocol preprint](https://www.researchsquare.com/article/rs-401914/v1).
9 |
10 | For more background information, please see our [_eLife_ paper](https://elifesciences.org/articles/55160).
11 |
12 | Please cite as:
13 | > Kamrad, S., Rodríguez-López, M., Cotobal, C., Correia-Melo, C., Ralser M., Bähler J. (2020). Pyphe, a python toolbox for assessing microbial growth and cell viability in high-throughput colony screens. eLife 9:e55160
14 |
15 | ## Installation
16 | 1. Most tools are cross-platform compatible but scanning will only work on a Linux OS. The scanners need to be accessible by [SANE](http://www.sane-project.org/) and [ImageMagick](https://imagemagick.org/) needs to be installed and accessible from the command line.
17 | 2. Pyphe requires Python 3 and a few common packages, available through the [anaconda distribution](https://www.anaconda.com/distribution/).
18 | 3. Install pyphe by running 'pip install pyphe' in your terminal.
19 | 4. Open a new terminal and try and run 'pyphe-quantify -h' which should show the help page of one of pyphe's command line tools. On Windows, make sure you are using the Anaconda Prompt, not the Anaconda Powershell Prompt.
20 |
21 |
22 | ## Overview
23 | A typical fitness screen with pyphe will involve:
24 | 1. Image acquisition with [_pyphe-scan_](#pyphe-scan), or [_pyphe-scan-timecourse_](#pyphe-scan-timecourse)
25 | 2. Quantification of colony properties from images using [_pyphe-quantify_](#pyphe-quantify). In the case of growth curves, parameters are additionally extracted with [_pyphe-growthcurves_](#pyphe-growthcurves).
26 | 3. Normalisation and data aggregation using [_pyphe-analyse_](#pyphe-analyse).
27 | 4. Statistics and hit calling using [_pyphe-interpret_](#pyphe-interpret)
28 | Please see our paper for a detailed protocol and explanations of the algorithms.
29 |
30 |
31 | ## Support
32 | Please check the manuals below carefully, they are also available in the terminal by running the command with the -h option only. If things are still not working, please email me (stephan.kamrad@gmail.com) and I will try and help. If you think you have discovered a bug, or would like to request a new feature, please raise an issue on www.github.com/Bahler-Lab/pyphe.
33 |
34 | If you get an error like this, make sure you are not using the Anaconda Powershell Prompt:
35 | ```python: can't open file 'C:\Users\user1\Anaconda3\Scripts"C:\Users\user1\Anaconda3\Scripts\pyphe-quantify.bat -h ': [Errno 22] Invalid argument```
36 |
37 | ## Manual
38 |
39 | All pyphe tools have a similar command line interface, based on the python argparse package. Generally, parameters are set using -- optionally followed by a value. All _pyphe_ tools can be used with relative file paths so make sure to navigate to the correct working directory before running a _pyphe_ command.
40 |
41 |
42 | ### Pyphe-scan
43 | This tools allows you to take consecutive scans of sets of plates, which are then automatically cropped, rotated and named in in a continuos filename scheme of your choice.
44 |
45 | #### Prerequisites
46 | 1. This tool will only run on Linux operating systems and uses the SANE library for image acquisition.
47 |
48 | 2. Make sure your scanner is installed correctly and you can acquire images using the scanimage command. The Gray mode will only work on Epson V800 and V850 scanners (potentially the V700 and V750 model as well) and the TPU8x10 transmission scanning source must be enabled. This should work by default if you are using the V800/850 model and a recent Linux OS. Otherwise, there is excellent documentation available from Zackrisson et al. and the [scanomatics pipeline](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5015956/) for how to make this work using a hacked SANE driver. Please see the instructions in their [wiki](https://github.com/Scan-o-Matic/scanomatic/wiki/Installing-scanners).
49 |
50 | 2. Make sure [ImageMagick](https://imagemagick.org/index.php) is installed and the 'convert' tool can be called from the command line.
51 |
52 | 3. If the Pyphe toolbox has been installed correctly, you should be able to run _pyphe-scan_ in your terminal.
53 |
54 | 4. With a laser cutter, make a fixture to hold your plates in place. We provide an svg file with the cutting shape in the Documentation directory. Use tape to hold your fixture into place, it should be pushed against the back of the scanner (where the cables are) with the top of the plates facing left. Pyphe-scan and pyphe-quantify come pre-configured for using the provided fixture on an Epson V800/V850 scanner but it is easy to add your own fixture and cropping settings. If you want to use your own fixture, see below of how to add the geometry information to pyphe-scan.
55 |
56 | #### Scanning plate batches
57 |
58 | 1. Open the file manager and navigate to the folder in which you want to save your images. The script will create a sub-folder that begins with the current date to save all your images.
59 |
60 | 2. Right click and select 'Open in Terminal'
61 |
62 | 3. Run scanplates with the options as detaild below.
63 |
64 | ```
65 | usage: pyphe-scan [-h] [--nplates NPLATES] [--start START] [--prefix PREFIX]
66 | [--postfix POSTFIX] [--fixture {som3_edge,som3}]
67 | [--resolution {150,300,600,900,1200}] [--scanner {1,2,3}]
68 | [--mode {Gray,Color}]
69 |
70 |
71 | optional arguments:
72 | -h, --help show this help message and exit
73 | --nplates NPLATES Number of plates to scan. This defaults to 100 and the
74 | script can be terminated by Ctr+C when done.
75 | --start START Where to start numbering from. Defaults to 1.
76 | --prefix PREFIX Name prefix for output files. The default is the
77 | current date YYYYMMDD.
78 | --postfix POSTFIX Name postfix for output files. Defaults to empty
79 | string.
80 | --fixture {som3_edge,som3,som3-color}
81 | ID of the fixture you are using.
82 | --resolution {150,300,600,900,1200}
83 | Resolution for scanning in dpi. Default is 600.
84 | --scanner {1,2,3} Which scanner to use. Scanners are not uniquely
85 | identified and may switch when turned off/unplugged.
86 | This option does not need to be set when only one
87 | scanner is connected.
88 | --mode {Gray,Color} Which color mode to use for scanning. Defaults to
89 | Gray.
90 | ```
91 |
92 | All arguments except the fixture have default values and are optional. A folder prefix_postfix will be created in your current directory and the program will abort if a folder with this name already exists.
93 |
94 |
95 |
96 | ### Pyphe-scan-timecourse
97 |
98 | This tool acquires image timeseries by scanning in fixed time intervals. For each position in the fixture, a folder is created. Image names contain number of scan. Other options for this tool are similar to [_pyphe-scan_](#pyphe-scan). More than one scanner can be connected and used at the same time. Scanner numbers are defined by the order in which they are connected to the computer. Proceed as follows: (1) disconnect all scanners, (2) prepare the first scanner with plates, connect it and turn it on. (3) start scanning with --scanner 1 option, (4) prepare the second scanner, connect it and turn it on, (5) start scanning with --scanner 2 option. Repeat step (4) and (5), each time incrementing the --scanner argument.
99 |
100 | ```
101 | usage: pyphe-scan-timecourse [-h] [--nscans NSCANS] [--interval INTERVAL]
102 | [--prefix PREFIX] [--postfix POSTFIX]
103 | [--fixture {som3_edge,som3}]
104 | [--resolution {150,300,600,900,1200}]
105 | [--scanner {1,2,3}] [--mode {Gray,Color}]
106 |
107 | optional arguments:
108 | -h, --help show this help message and exit
109 | --nscans NSCANS Number of time points to scan. This defaults to 100
110 | and the script can be terminated by Ctr+C when done.
111 | --interval INTERVAL Time in minutes between scans. Defaults to 20.
112 | --prefix PREFIX Name prefix for output files. The default is the
113 | current date YYYYMMDD.
114 | --postfix POSTFIX Name postfix for output files. Defaults to empty
115 | string.
116 | --fixture {som3_edge,som3,som3-color}
117 | ID of the fixture you are using.
118 | --resolution {150,300,600,900,1200}
119 | Resolution for scanning in dpi. Default is 600.
120 | --scanner {1,2,3} Which scanner to use. Scanners are not uniquely
121 | identified and may switch when turned off/unplugged.
122 | This option does not need to be set when only one
123 | scanner is connected.
124 | --mode {Gray,Color} Which color mode to use for scanning. Defaults to
125 | Gray.
126 | ```
127 |
128 |
129 | ### Pyphe-growthcurves
130 | This tool performs non-parametric analysis of growth curves. It was written specifically to analyse colony size timeseries data obtained with _pyphe-quantify_ _timeseries_.
131 |
132 | It is important that your csv with the growth data is in the right format. The file must contain one growth curve per column. The first column must be the timepoints and there must be a header row with unique identifiers for each curve. For example data and expected outputs, check out the files included in this Documentation folder. Sensible default parameters are set for all options but, depending on your data, you may wish to customise these, so check out the help section below.
133 |
134 | ```
135 | usage: pyphe-growthcurves [-h] --input INPUT [--fitrange FITRANGE]
136 | [--lag-method {abs,rel}]
137 | [--lag-threshold LAG_THRESHOLD]
138 | [--t0-fitrange T0_FITRANGE] [--plots]
139 | [--plot-ylim PLOT_YLIM]
140 |
141 |
142 | optional arguments:
143 | -h, --help show this help message and exit
144 | --input INPUT Path to the growth curve file to analyse. This file
145 | contains one growth curve per column. The first column
146 | must be the timepoints and there must be a header row
147 | with unique identifiers for each curve.
148 | --fitrange FITRANGE Number of timepoint over which to fit linear
149 | regression. Defaults to 4. Please adjust this to the
150 | density of your timepoints and use higher values for
151 | more noisy data.
152 | --lag-method {abs,rel}
153 | Method to use for determining lag. "abs" will measure
154 | time until the defined biomass threshold is crossed.
155 | "rel" will fist determine the inital biomass and
156 | measure the time until the biomass has passed this
157 | value times the threshold. Defaults to "rel".
158 | --lag-threshold LAG_THRESHOLD
159 | Threshold to use for determining lag. With method
160 | "abs", this will measure time until the defined
161 | biomass threshold is crossed. With "rel" will fist
162 | determine the inital biomass and measure the time
163 | until the biomass has passed this value times the
164 | threshold. Defaults to 2.0, so with method "rel", this
165 | will measure the time taken for the first doubling.
166 | --t0-fitrange T0_FITRANGE
167 | Specify the number of timepoint to use at the
168 | beginning of the growth curve to determine the initial
169 | biomass by averaging them. Defaults to 3.
170 | --plots Set this option (no argument required) to produce a
171 | plot of all growthcurves as pdf.
172 | --plot-ylim PLOT_YLIM
173 | Specify the upper limit of the y-axis of growth curve
174 | plots. Useful if you want curves to be directly
175 | comparable. If not set, the axis of each curve is
176 | scaled to the data.
177 | ```
178 |
179 |
180 | #### Interpreting results
181 | Pyphe-growthcurves will produce a csv file with extracted growth parameters. The maximum slope is determined by fitting all possible linear regressions in sliding windows of length n and chosing the one with the highest slope. The lag phase is determined as the first timepoint which exceeds a settable relative or absolute threshold.
182 |
183 | | Parameter | Explanation |
184 | | ---------------- |---------------|
185 | |initial biomass|The average of the first n timepoints of the growth curve|
186 | |lag | Lag phase |
187 | | max_slope| The maximum slope of the growth curve|
188 | | r2 | The R2 parameter of the linear regression that produced the highest maximum slope |
189 | |t_max | Time at which maximum growth slope is reached (center of the sliding window)|
190 | |y-intercept|Y-intercept of the regression which produced the maximum slope|
191 | |x-intercept|X-intercept of the regression which produced the maximum slope. This is interpreted as lag phase by some people|
192 |
193 |
194 |
195 | ### Pyphe-quantify
196 |
197 | Pyphe quantify extracts colony parameters from images. In can operate in three distinct modes analysing colony sizes for each image individually (batch mode), analysing redness for each image individually (redness mode) or obtaining a growth curve from an image timeseries (timeseries mode).
198 | The --grid parameter is required define the position of colonies on the plate. You can either use automatic grid detection, one of our preconfigured positions if you are using the pp3 fixture or define your own (see the manual below). Images can be in any format (e.g. jpg, tiff, png). Images should be cropped closely to the colonies (this is important for good thresholding and automatic grid detection), i.e. not contain parts of the plate edges or surroundings. In batch and timecourse mode, pyphe-quantify assumes that images were acquired using transmission scanning, where colonies appear darker then the surrounding agar. If this is not the case and you took images by reflective scanning or with a camera, use --negate False. In batch and timecourse mode, images are epxected to be grayscale. If they are not, they will be converted (by simply summing all channels) and a warning will be thrown.
199 |
200 |
201 | ```
202 | usage: pyphe-quantify [-h] --grid GRID [--pattern PATTERN] [--t T] [--d D]
203 | [--s S] [--negate NEGATE] [--localThresh] [--convexhull]
204 | [--reportAll] [--reportFileNames]
205 | [--hardImageThreshold HARDIMAGETHRESHOLD]
206 | [--hardSizeThreshold HARDSIZETHRESHOLD] [--qc QC]
207 | [--calibrate CALIBRATE] [--timepoints TIMEPOINTS]
208 | [--out OUT]
209 | {batch,timecourse,redness}
210 |
211 | Welcome to pyphe-quantify, part of the pyphe toolbox. Written by
212 | stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-
213 | Lab/pyphe
214 |
215 | positional arguments:
216 | {batch,timecourse,redness}
217 | Pyphe-quantify can be run in three different modes. In
218 | batch mode, it quantifies colony sizes for all images
219 | matching the pattern individually. A separate results
220 | table and qc image is produced for each. Redness mode
221 | is similar except that the redness of each colony is
222 | quantified. In timecourse mode, all images matching
223 | the pattern are analysed jointly. The final image
224 | matching the pattern is used to create a mask of where
225 | the colonies are and this mask is then applied to all
226 | previous images in the timeseries. A single output
227 | table, where the timepoints are the rows and each
228 | individual colony is a row.
229 |
230 | optional arguments:
231 | -h, --help show this help message and exit
232 | --grid GRID This option is required (all others have defaults set)
233 | and specifies the grid in which the colonies are
234 | arranged. You can use automatic grid detection using
235 | one of the following parameters: auto_96, auto_384 or
236 | auto_1536. Automatic grid correction will not work if
237 | the colony grid is not aligned with the image borders.
238 | Images should contain only agar and colonies, avaoid
239 | having borders. It might fail or produce unexpected
240 | results if there are whole rows/columns missing. In
241 | those cases, it is easy to define hard-wired grid
242 | positions. If you are using the fixture provided with
243 | pyphe, we have preconfigured these for you. Depending
244 | on the pinning density, use pp3_96, pp3_384 or
245 | pp3_1536. Otherwise, the argument has to be in the
246 | form of 6 integer numbers separated by "-": -----.
251 | Positions must be integers and are the distance in
252 | number of pixels from the image origin in each
253 | dimension (x is width dimension, y is height
254 | dimension). The image origin is, in line with scikit-
255 | image, in the top left corner. Pixel positions are
256 | easily determined using programs such as Microsoft
257 | Paint, by simply hovering the mouse over a position.
258 | --pattern PATTERN Pattern describing files to analyse. This follows
259 | standard unix convention and can be used to specify
260 | subfolders in which to look for images
261 | (/*.jpg) or the image format (*.tiff,
262 | *.png, etc.). By default, all jpg images in the
263 | working directory are analysed.
264 | --t T By default the intensity threshold to distinguish
265 | colonies from the background is determined by the Otsu
266 | method. The determined value will be multiplied by
267 | this argument to give the final threshold. Useful for
268 | easily fine-tuning colony detection.
269 | --d D The distance between two grid positions will be
270 | divided by this number to compute the maximum distance
271 | a putative colony can be away from its reference grid
272 | position. Decreasing this number towards 2 makes
273 | colony-to-grid-matching more permissive (might help
274 | when some of your plates are at a slight angle or out
275 | of position).
276 | --s S Detected putative colonies will be filtered by size
277 | and small components (usually image noise) will be
278 | excluded. The default threshold is the image
279 | area*0.00005 and is therefore independent of scanning
280 | resolution. This default is then multiplied by this
281 | argument to give the final threshold. Useful for when
282 | colonies have unusual sizes.
283 | --negate NEGATE In images acquired by transmission scanning, the
284 | colonies are darker than the background. Before
285 | thresholding, the image needs to be inverted/negated.
286 | Defaults to True in timecourse and batch mode, ignored
287 | in redness mode.
288 | --localThresh Use local thresholding in batch and timecourse mode.
289 | This can help when image brightness is very uneven.
290 | Ignored in redness mode where local thresholding is
291 | always applied.
292 | --convexhull Apply convex hull transformation to identified
293 | colonies to fill holes. Useful when working with spots
294 | rather than colonies. Ignored in redness mode.
295 | WARNING: Using this options results in much longer
296 | analysis times.
297 | --reportAll Sometimes, two putative colonies are identified that
298 | are within the distance threshold of a grid position.
299 | By default, only the closest colony is reported. This
300 | can be changed by setting this option (without
301 | parameter). This option allows pyphe quantify to be
302 | used even if colonies are not arrayed in a regular
303 | grid (you still need to provide a grid parameter
304 | though that spans the colonies you are interested i).
305 | --reportFileNames Only for timecourse mode, otherwise ignored. Use
306 | filenames as index for output table instead of
307 | timepoints. Useful when the ordering of timepoints is
308 | not the same as returned by the pattern. Setting this
309 | option overrides the --timepoints argument.
310 | --hardImageThreshold HARDIMAGETHRESHOLD
311 | Allows a hard (fixed) intensity threshold in the range
312 | [0,1] to be used instead of Otsu thresholding. Images
313 | intensities are re-scaled to [0,1] before
314 | thresholding. Ignored in timecourse mode.
315 | --hardSizeThreshold HARDSIZETHRESHOLD
316 | Allows a hard (fixed) size threshold [number of
317 | pixels] to be used for filtering small colonies.
318 | --qc QC Directory to save qc images in. Defaults to
319 | "qc_images".
320 | --calibrate CALIBRATE
321 | Transform background subtracted intensity values by
322 | this function. Function needs to be a single term with
323 | x as the variable and that is valid python code. E.g.
324 | use "2*x**2+1" to square each pixels intensity,
325 | multiply by two and add 1. Defaults to "x", i.e. use
326 | of no calibration. Used only in timecourse mode.
327 | --timepoints TIMEPOINTS
328 | In timecourse mode only. Path to a file that specifies
329 | the timepoints of all images in the timeseries. This
330 | is usually the timepoints.txt file created by pyphe-
331 | scan-timecourse. It must contain one entry per line
332 | and have the same number of lines as number of images.
333 | --out OUT Directory to save output files in. Defaults to
334 | "pyphe_quant".
335 | ```
336 |
337 |
338 |
339 | ### Pyphe-analyse
340 | _Pyphe-analyse_ is a tool for spatial normalisation and data aggregation across many plates. It implements a grid normalisation based on the concept proposed by [Zackrisson et al. 2016](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5015956/) and row/column median normalisation. Please see our paper and the protocol in it to find out more. _Pyphe-analyse_ can be run from the command line, with options below, or using the graphical user interface by running _pyphe-analyse-gui_.
341 |
342 |
343 | ```
344 | usage: pyphe-analyse.txt [-h] --edt EDT --format
345 | {gitter,pyphe-redness,pyphe-growthcurves} [--out OUT]
346 | [--load_layouts]
347 | [--gridnorm {standard384,standard1536}]
348 | [--extrapolate_corners] [--rcmedian] [--check CHECK]
349 | [--qc_plots QC_PLOTS]
350 |
351 | Welcome to pyphe-analyse, part of the pyphe toolbox. Written by
352 | stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-
353 | Lab/pyphe
354 |
355 | optional arguments:
356 | -h, --help show this help message and exit
357 | --edt EDT Path to the Experimental Design Table (EDT) listing
358 | all plates of the experiment. The table must be in csv
359 | format, the first column must contain unique plate IDs
360 | and there must be a column named 'Data_path' that
361 | contains abolute or relative file paths to each
362 | plate's data file. A 'Layout_path' column can be
363 | included, see below. Any additional columns included
364 | in this file will bestored in each plate's meta-data
365 | and included in the final data output.
366 | --format {gitter,pyphe-redness,pyphe-growthcurves}
367 | Type of inout data.
368 | --out OUT Specifies the path where to save the output data
369 | result. By default, the data report is saved in the
370 | working directory as "pyphe-analyse_data_report.csv"
371 | and will overwrite the file if it exists.
372 | --load_layouts Set this option (without parameters) to load layouts
373 | (requires Layout_path column in the EDT).
374 | --gridnorm {standard384,standard1536,1536with384grid}
375 | Perform reference grid normalisation. Standard384
376 | refers to plates which are in 384 (16x24) format with
377 | the reference grid in 96 format in the top left
378 | corner. Standard1536 refers to plates in 1536 format
379 | (32x48( with two 96 reference grids in the top left
380 | and bottom right corners. 1536with384grid refers to
381 | plates in 1536 format with a 384 reference grid in
382 | the top left position.
383 | --extrapolate_corners
384 | If working in standard1536 format, set this option to
385 | extrapolate the reference grid in the bottom left and
386 | top right corner. A linear regression will be trained
387 | across all top left and bottom right corners on plates
388 | in the experiment to predict hypothetical grid colony
389 | sizes in the other two corners.
390 | --rcmedian Perform row/column median normalisation. If --gridnorm
391 | will be performed first if both parameters are set.
392 | --check CHECK Check colony sizes after normalisation for negative
393 | and infinite colony sizes *(normalisation artefacts),
394 | throw a warning and set to NA.
395 | --qc_plots QC_PLOTS Specify a folder in which to save qc plots for each
396 | plate.
397 |
398 | ```
399 |
400 |
401 | ### Pyphe-interpret
402 |
403 | Pyphe-interpret reports summary statistics and tests for differential fitness using t-tests. It is flexible and can in theory be used with any dataset in tidy format.
404 |
405 | ```
406 | usage: pyphe-interpret [-h] --ld LD [--out OUT] --grouping_column
407 | GROUPING_COLUMN --axis_column AXIS_COLUMN
408 | [--values_column VALUES_COLUMN] --control CONTROL
409 | [--ld_encoding LD_ENCODING] [--circularity CIRCULARITY]
410 | [--set_missing_na]
411 |
412 | Welcome to pyphe-interpret, part of the pyphe toolbox. Written by
413 | stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-
414 | Lab/pyphe. Pyphe-interpret calculates summary statistics and p-values from the
415 | data reports generated by pyphe-analyse. For this, specifiying your column
416 | names correctly is crucial. Let us assume you have measured many strains in
417 | many conditions. Now you would like to know for each strain in each condition
418 | (for each condition-strain pair) if it is "significant". There are essentially
419 | two ways of doing this, asking different biological questions. (1) Check for
420 | each condition separately (--grouping_column ) if there is a
421 | significant difference in means between a mutant strain and a control strain
422 | (--axis_column ). Or (2) Check for each strain separately
423 | (--grouping_column ) if there is a significant difference in
424 | the means of the strain in the assay condition versus the control condition
425 | (--axis_column ). The second option tests for condition-
426 | specific growth effects (i.e. is does not return significant results if a
427 | strain is always faster or always slower growing than the grid strain). In
428 | both cases you need to specify the control against which to test using
429 | --control and this has to be a value that appears in the axis column. You
430 | should define the dependent variable of the t-test using --values_column. FDR
431 | correction with the Benjamini-Hochberg method will be applied on each level
432 | set of the grouping_column separately, ie for case (1) p-values will be
433 | corrected across each strain separately, ie more conditions means more
434 | stringent correction, and for case (2) p-values will be corrected for each
435 | condition separately, ie more strains means mpre stringent correction.
436 |
437 | optional arguments:
438 | -h, --help show this help message and exit
439 | --ld LD Path to the Data Report Table produced by pyphe-
440 | analyse.
441 | --out OUT Specifies the path where to save the output data
442 | result. By default, a table with all replicates will
443 | be saved as pyphe-interpret-report_reps.csv and the
444 | statistic table will be saved as pyphe-interpret-
445 | report_summaryStats.csv in the current working
446 | directory. Existing files will be overwritten.
447 | --grouping_column GROUPING_COLUMN
448 | Name of the column in the data report to use for
449 | forming groups on which to perform independent sets of
450 | t-tests.
451 | --axis_column AXIS_COLUMN
452 | Name of the column in the data report to repeat
453 | t-tests along within each group. Levels in this column
454 | will be the explanatory/independent variable used for
455 | t-tests.
456 | --values_column VALUES_COLUMN
457 | Name of the column in the data report to use as
458 | fitness values. This will be the dependent variable
459 | for t-tests. Defaults to "Colony_size_corr_checked".
460 | --control CONTROL Name of the control to compare against. This must be a
461 | value found in the axis column.
462 | --ld_encoding LD_ENCODING
463 | Encoding of the data report table to be passed to
464 | pandas.read_csv().
465 | --circularity CIRCULARITY
466 | Exclude colonies from the analysis with a circularity
467 | below the one specified. A circularity of 1
468 | corresponds to a perfect circle. We recommend a
469 | threshold around 0.85.
470 | --set_missing_na Set 0-sized colonies to NA. This is recommended if you
471 | expect no missing colonies in your data, which means
472 | these are probably due to pinning errors.
473 | ```
474 |
475 |
--------------------------------------------------------------------------------
/bin/pyphe-analyse:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | from pyphe.analysis import pyphe_cmd, check_mkdir
5 |
6 | if __name__ == '__main__':
7 | ###Set up parsing of command line arguments with argparse###
8 | parser = argparse.ArgumentParser(description='Welcome to pyphe-analyse, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe')
9 |
10 |
11 | parser.add_argument('--edt', type=str, required=True, help="Path to the Experimental Design Table (EDT) listing all plates of the experiment. The table must be in csv format, the first column must contain unique plate IDs and there must be a column named 'Data_path' that contains absolute or relative file paths to each plate's data file. A 'Layout_path' column can be included, see below. Any additional columns included in this file will be stored in each plate's meta-data and included in the final data output.")
12 | parser.add_argument('--format', required=True, type=str, choices=['gitter', 'pyphe-quantify-redness', 'pyphe-quantify-batch', 'pyphe-growthcurves'], help='Type of inout data.')
13 | parser.add_argument('--out', default='pyphe-analyse_data_report.csv', type=str, help='Specifies the path where to save the output data result. By default, the data report is saved in the working directory as "pyphe-analyse_data_report.csv" and will overwrite the file if it exists.')
14 | parser.add_argument('--load_layouts', default=False, action='store_true', help='Set this option (without parameters) to load layouts (requires Layout_path column in the EDT). Layouts must be a single csv table per plate in the same layout as the plate and without headers or row labels.')
15 | parser.add_argument('--gridnorm', type=str, choices=['standard384', 'standard1536', '1536with384grid'], help='Perform reference grid normalisation. Standard384 refers to plates which are in 384 (16x24) format with the reference grid in 96 format in the top left corner. Standard1536 refers to plates in 1536 format (32x48( with two 96 reference grids in the top left and bottom right corners. 1536with384grid refers to plates in 1536 format with a 384 reference grid in the top left position.')
16 | parser.add_argument('--extrapolate_corners', default=False, action='store_true', help='If working in standard1536 format, set this option to extrapolate the reference grid in the bottom left and top right corner. A linear regression will be trained across all top left and bottom right corners on plates in the experiment to predict hypothetical grid colony sizes in the other two corners.')
17 | parser.add_argument('--rcmedian', default=False, action='store_true', help='Perform row/column median normalisation. If --gridnorm will be performed first if both parameters are set.')
18 | parser.add_argument('--nocheck', default=False, action='store_true', help='Check colony sizes after normalisation for negative and infinite colony sizes *(normalisation artefacts), throw a warning and set to NA.')
19 | parser.add_argument('--qc_plots', type=str, help='Specify a folder in which to save qc plots for each plate.')
20 |
21 |
22 | args = parser.parse_args()
23 |
24 | #Check arguments
25 | if args.extrapolate_corners and (args.gridnorm != 'standard1536'):
26 | raise ValueError('--extrapolate_corners can only be used if gridnorm is standard1536.')
27 |
28 | #Create qc directory
29 | if args.qc_plots:
30 | check_mkdir(args.qc_plots)
31 |
32 | #Run analysis
33 | print('Analysis is starting, with following parameters:')
34 | for k, v in vars(args).items():
35 | print('%s: %s'%(k, str(v)))
36 |
37 | gridQ = True if args.gridnorm else False
38 | qcQ = True if args.qc_plots else False
39 | check = not args.nocheck
40 | pyphe_cmd(grid_norm=gridQ, out_ld=args.out, qcplots=qcQ, check_setNA=check, qcplot_dir=args.qc_plots, exp_data_path=args.edt, extrapolate_corners=args.extrapolate_corners, grid_pos=args.gridnorm, rcmedian=args.rcmedian, input_type=args.format, load_layouts=args.load_layouts)
41 |
--------------------------------------------------------------------------------
/bin/pyphe-analyse-gui:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import PySimpleGUI as sg
4 | from pyphe.analysis import pyphe_cmd
5 |
6 | #Read in all parameters
7 | window_rows = [
8 | [sg.Text('Step 1: Load the Experiment setup')],
9 | [sg.Text('Set the working directory (leave empty for current directory)'), sg.Input(key='wdirectory'), sg.FolderBrowse()],
10 | [sg.Text('Path to table (csv): '), sg.Input(key='exp_data_path'), sg.FileBrowse()],
11 | [sg.Text('Select the input data type: '), sg.InputCombo(['gitter', 'pyphe-quantify-redness', 'pyphe-growthcurves'], key='input_type')],
12 | [sg.Checkbox('Load layouts from file (one file per plate layout)', key='load_layouts')],
13 | [sg.Text('Step 2: Parameters for normalisation')],
14 | [sg.Checkbox('Perform grid normalisation', key='grid_norm')],
15 | [sg.Text('Select grid position'), sg.InputCombo(['Standard 384 (top left)', 'Standard 1536 (top left and bottom right)'], key='grid_pos')],
16 | [sg.Checkbox('Extrapolate missing corners (1536 with standard grid only)', key='extrapolate_corners')],
17 | [sg.Checkbox('Perform row/column median normalisation', key='rcmedian')],
18 | [sg.Text('Step 3: Check data and make QC plots')],
19 | [sg.Checkbox('Check data for negative and infinitive fitness and replace by NA', key='check_setNA')],
20 | [sg.Checkbox('Make qc plots? If so, please specify directory.', key='qcplots'), sg.Input(key='qcplot_dir'), sg.FolderBrowse()],
21 | [sg.Text('Step 4: Export data')],
22 | [sg.Text('Specify output file: '), sg.Input(key='out_ld'), sg.FileSaveAs()],
23 | [sg.Submit()],
24 | ]
25 |
26 | window = sg.Window('Set up pyphe experiment', window_rows)
27 | event, values = window.Read()
28 | window.Close()
29 |
30 | #Run pyphe
31 | args = {k:v for k,v in values.items() if k not in ['Save As...', 'Browse', 'Browse0', 'Browse1']}
32 | print('Analysis is starting, with following parameters:')
33 | for k, v in args.items():
34 | print('%s: %s'%(k, str(v)))
35 | pyphe_cmd(**args)
36 |
--------------------------------------------------------------------------------
/bin/pyphe-analyse.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Welcome to pyphe for Windows. Running pyphe-analyse using the following command:
3 | set a=python %~dp0%0
4 | :argactionstart
5 | if -%1-==-- goto argactionend
6 | set a=%a% %1 & REM Or do any other thing with the argument
7 | shift
8 | goto argactionstart
9 | :argactionend
10 | echo %a%
11 | %a%
12 |
13 |
--------------------------------------------------------------------------------
/bin/pyphe-growthcurves:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import pandas as pd
5 | from pyphe import growthcurves
6 | from pyphe.analysis import check_mkdir
7 | from os import path
8 |
9 | if __name__ == '__main__':
10 | parser = argparse.ArgumentParser(description='Welcome to pyphe-growthcurves, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe.')
11 |
12 | parser.add_argument('--input', type=str, required=True, help='Path to the growth curve file to analyse. This file contains one growth curve per column. The first column must be the timepoints and there must be a header row with unique identifiers for each curve.')
13 |
14 | parser.add_argument('--fitrange', type=int, default=4, help='Number of timepoint over which to fit linear regression. Defaults to 4. Please adjust this to the density of your timepoints and use higher values for more noisy data.')
15 | parser.add_argument('--lag-method', type=str, choices=['abs', 'rel'], default='rel', help='Method to use for determining lag. "abs" will measure time until the defined biomass threshold is crossed. "rel" will fist determine the inital biomass and measure the time until the biomass has passed this value times the threshold. Defaults to "rel".')
16 | parser.add_argument('--lag-threshold', type=float, default=2.0, help='Threshold to use for determining lag. With method "abs", this will measure time until the defined biomass threshold is crossed. With "rel" will fist determine the inital biomass and measure the time until the biomass has passed this value times the threshold. Defaults to 2.0, so with method "rel", this will measure the time taken for the first doubling.')
17 | parser.add_argument('--t0-fitrange', type=int, default=3, help='Specify the number of timepoint to use at the beginning of the growth curve to determine the initial biomass by averaging them. Defaults to 3.')
18 | parser.add_argument('--plots', default=False, action='store_true', help='Set this option (no argument required) to produce a plot of all growthcurves as pdf.')
19 | parser.add_argument('--plot-ylim', type=float, help='Specify the upper limit of the y-axis of growth curve plots. Useful if you want curves to be directly comparable. If not set, the axis of each curve is scaled to the data.')
20 | parser.add_argument('--out', type=str, default='.', help='Folder to save result files in. Result files have the same name as the input file with _results.csv appended.')
21 | parser.add_argument('--plot-individual-data', default=False, action='store_true', help='Plot individual data points.')
22 |
23 | args = parser.parse_args()
24 |
25 | if not args.fitrange >1:
26 | raise ValueError('--fitrange must be at least 2.')
27 |
28 | #Import the data and perform some basic checks
29 | gdata = pd.read_csv(args.input, index_col=0)
30 | try:
31 | gdata.index = gdata.index.map(float)
32 | except Exception as eo:
33 | print('The first column must contain the timepoint and these must only have numeric values (no units or other string).')
34 | assert all(gdata.index[i] <= gdata.index[i+1] for i in range(len(gdata.index)-1)), 'Timepoints must be in ascending order.'
35 |
36 | outdir = args.out.strip('/').strip('\\')
37 | in_baseStr = '.'.join(path.split(args.input)[1].split('.')[:-1])
38 |
39 | check_mkdir(outdir)
40 | result = growthcurves.analyse_growthcurve(gdata, args.fitrange, args.t0_fitrange, args.lag_method, args.lag_threshold, args.plots, args.plot_ylim, outdir, in_baseStr, args.plot_individual_data)
41 |
42 | result.to_csv(outdir + '/' + in_baseStr + '_results.csv')
43 |
44 | print('Analysis done: %s'%args.input)
45 |
46 |
--------------------------------------------------------------------------------
/bin/pyphe-growthcurves.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Welcome to pyphe for Windows. Running pyphe-growthcurves using the following command:
3 | set a=python %~dp0%0
4 | :argactionstart
5 | if -%1-==-- goto argactionend
6 | set a=%a% %1 & REM Or do any other thing with the argument
7 | shift
8 | goto argactionstart
9 | :argactionend
10 | echo %a%
11 | %a%
12 |
13 |
--------------------------------------------------------------------------------
/bin/pyphe-interpret:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | from pyphe.interpret import interpret
4 | import pandas as pd
5 |
6 | if __name__ == '__main__':
7 | ###Set up parsing of command line arguments with argparse###
8 | parser = argparse.ArgumentParser(description='Welcome to pyphe-interpret, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe. Pyphe-interpret calculates summary statistics and p-values from the data reports generated by pyphe-analyse. For this, specifiying your column names correctly is crucial. Let us assume you have measured many strains in many conditions. Now you would like to know for each strain in each condition (for each condition-strain pair) if it is "significant". There are essentially two ways of doing this, asking different biological questions. (1) Check for each condition separately (--grouping_column ) if there is a significant difference in means between a mutant strain and a control strain (--axis_column ). Or (2) Check for each strain separately (--grouping_column ) if there is a significant difference in the means of the strain in the assay condition versus the control condition (--axis_column ). The second option tests for condition-specific growth effects (i.e. is does not return significant results if a strain is always faster or always slower growing than the grid strain). In both cases you need to specify the control against which to test using --control and this has to be a value that appears in the axis column. You should define the dependent variable of the t-test using --values_column. FDR correction with the Benjamini-Hochberg method will be applied on each level set of the grouping_column separately, ie for case (1) p-values will be corrected across each strain separately, ie more conditions means more stringent correction, and for case (2) p-values will be corrected for each condition separately, ie more strains means mpre stringent correction.')
9 |
10 |
11 | parser.add_argument('--ld', type=str, required=True, help="Path to the Data Report Table produced by pyphe-analyse.")
12 | parser.add_argument('--out', type=str, default='pyphe-interpret-report', help='Specifies the path where to save the output data result. By default, a table with all replicates will be saved as pyphe-interpret-report_reps.csv and the statistic table will be saved as pyphe-interpret-report_summaryStats.csv in the current working directory. Existing files will be overwritten.')
13 | parser.add_argument('--grouping_column', type=str, required=True, help='Name of the column in the data report to use for forming groups on which to perform independent sets of t-tests.')
14 | parser.add_argument('--axis_column', type=str, required=True, help='Name of the column in the data report to repeat t-tests along within each group. Levels in this column will be the explanatory/independent variable used for t-tests.')
15 | parser.add_argument('--values_column', type=str, default='Colony_size_corr_checked', help='Name of the column in the data report to use as fitness values. This will be the dependent variable for t-tests. Defaults to "Colony_size_corr_checked".')
16 | parser.add_argument('--control', type=str, required=True, help='Name of the control to compare against. This must be a value found in the axis column.')
17 | parser.add_argument('--ld_encoding', default='utf-8', type=str, help='Encoding of the data report table to be passed to pandas.read_csv().')
18 | parser.add_argument('--circularity', type=float, default=None, help='Exclude colonies from the analysis with a circularity below the one specified. A circularity of 1 corresponds to a perfect circle. We recommend a threshold around 0.85.')
19 | parser.add_argument('--set_missing_na', action='store_true', default=False, help='Set 0-sized colonies to NA. This is recommended if you expect no missing colonies in your data, which means these are probably due to pinning errors.')
20 |
21 | args = parser.parse_args()
22 |
23 | #Run analysis
24 | print('Interpretation is starting, with following parameters:')
25 | for k, v in vars(args).items():
26 | print('%s: %s'%(k, str(v)))
27 |
28 | #Load ld
29 | ld = pd.read_csv(args.ld, index_col=0, encoding=args.ld_encoding)
30 |
31 | interpret(ld, args.axis_column, args.grouping_column, args.values_column, args.control, args.out, circularity=args.circularity, set_missing_na=args.set_missing_na)
32 |
--------------------------------------------------------------------------------
/bin/pyphe-interpret.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Welcome to pyphe for Windows. Running pyphe-interpret using the following command:
3 | set a=python %~dp0%0
4 | :argactionstart
5 | if -%1-==-- goto argactionend
6 | set a=%a% %1 & REM Or do any other thing with the argument
7 | shift
8 | goto argactionstart
9 | :argactionend
10 | echo %a%
11 | %a%
12 |
13 |
--------------------------------------------------------------------------------
/bin/pyphe-quantify:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | from skimage.io.collection import ImageCollection
5 | from pyphe import quantify, analysis
6 |
7 | if __name__ == '__main__':
8 | ###Set up parsing of command line arguments with argparse###
9 | parser = argparse.ArgumentParser(description='Welcome to pyphe-quantify, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe')
10 |
11 | parser.add_argument('mode', type=str, choices=['batch', 'timecourse', 'redness'], help='Pyphe-quantify can be run in three different modes. In batch mode, it quantifies colony sizes for all images matching the pattern individually. A separate results table and qc image is produced for each. Redness mode is similar except that the redness of each colony is quantified. In timecourse mode, all images matching the pattern are analysed jointly. The final image matching the pattern is used to create a mask of where the colonies are and this mask is then applied to all previous images in the timeseries. A single output table, where the timepoints are the rows and each individual colony is a row. ')
12 |
13 | parser.add_argument('--grid', type=str, required=True, help='This option is required (all others have defaults set) and specifies the grid in which the colonies are arranged. You can use automatic grid detection using one of the following parameters: auto_96, auto_384 or auto_1536. You can also define a custom number of rows/columns for automatic grid fitting by setting the argument to "auto_r-c", where r and c are the numbers of rows/columns respectively. Automatic grid correction will not work if the colony grid is not aligned with the image borders. Images should contain only agar and colonies, avoid having borders. It might fail or produce unexpected results if there are whole rows/columns missing. In those cases, it is easy to define hard-wired grid positions. If you are using the fixture provided with pyphe, we have preconfigured these for you. Depending on the pinning density, use pp3_96, pp3_384 or pp3_1536. Otherwise, the argument has to be in the form of 6 integer numbers separated by "-": -----. Positions must be integers and are the distance in number of pixels from the image origin in each dimension (x is width dimension, y is height dimension). The image origin is, in line with scikit-image, in the top left corner. Pixel positions are easily determined using programs such as Microsoft Paint, by simply hovering the mouse over a position.')
14 |
15 | parser.add_argument('--pattern', type=str, default='*.jpg', help='Pattern describing files to analyse. This follows standard unix convention and can be used to specify subfolders in which to look for images (/*.jpg) or the image format (*.tiff, *.png, etc.). By default, all jpg images in the working directory are analysed.')
16 | parser.add_argument('--t', type=float, default=1, help='By default the intensity threshold to distinguish colonies from the background is determined by the Otsu method. The determined value will be multiplied by this argument to give the final threshold. Useful for easily fine-tuning colony detection.')
17 | parser.add_argument('--d', type=float, default=3, help='The distance between two grid positions will be divided by this number to compute the maximum distance a putative colony can be away from its reference grid position. Decreasing this number towards 2 makes colony-to-grid-matching more permissive (might help when some of your plates are at a slight angle or out of position).')
18 | parser.add_argument('--s', type=float, default=1, help='Detected putative colonies will be filtered by size and small components (usually image noise) will be excluded. The default threshold is the image area*0.00005 and is therefore independent of scanning resolution. This default is then multiplied by this argument to give the final threshold. Useful for when colonies have unusual sizes.')
19 | parser.add_argument('--no-negate', action='store_false', default=True, help='In images acquired by transmission scanning, the colonies are darker than the background. Before thresholding, the image needs to be inverted/negated. Use this option if you do not want to negate images (e.g. when they were taken with a camera). Ignored in redness mode. ')
20 | parser.add_argument('--localThresh', default=False, action='store_true', help='Use local thresholding in batch and timecourse mode. This can help when image brightness is very uneven. Ignored in redness mode where local thresholding is always applied.')
21 | parser.add_argument('--convexhull', default=False, action='store_true', help='Apply convex hull transformation to identified colonies to fill holes. Useful when working with spots rather than colonies. Ignored in redness mode. WARNING: Using this options results in much longer analysis times.')
22 | parser.add_argument('--reportAll', default=False, action='store_true', help='Sometimes, two putative colonies are identified that are within the distance threshold of a grid position. By default, only the closest colony is reported. This can be changed by setting this option (without parameter). This option allows pyphe quantify to be used even if colonies are not arrayed in a regular grid (you still need to provide a grid parameter though that spans the colonies you are interested i). ')
23 | parser.add_argument('--reportFileNames', default=False, action='store_true', help='Only for timecourse mode, otherwise ignored. Use filenames as index for output table instead of timepoints. Useful when the ordering of timepoints is not the same as returned by the pattern. Setting this option overrides the --timepoints argument.')
24 | parser.add_argument('--hardImageThreshold', type=float, help='Allows a hard (fixed) intensity threshold in the range [0,1] to be used instead of Otsu thresholding. Images intensities are re-scaled to [0,1] before thresholding. Ignored in timecourse mode.')
25 | parser.add_argument('--hardSizeThreshold', type=int, help='Allows a hard (fixed) size threshold [number of pixels] to be used for filtering small colonies.')
26 | parser.add_argument('--qc', type=str, default='qc_images', help='Directory to save qc images in. Defaults to "qc_images".')
27 | parser.add_argument('--calibrate', type=str, default='x', help='Transform background subtracted intensity values by this function. Function needs to be a single term with x as the variable and that is valid python code. E.g. use "2*x**2+1" to square each pixels intensity, multiply by two and add 1. Defaults to "x", i.e. use of no calibration. Used only in timecourse mode.')
28 | parser.add_argument('--timepoints', default=None, help='In timecourse mode only. Path to a file that specifies the timepoints of all images in the timeseries. This is usually the timepoints.txt file created by pyphe-scan-timecourse. It must contain one entry per line and have the same number of lines as number of images.')
29 | parser.add_argument('--out', type=str, default='pyphe_quant', help='Directory to save output files in. Defaults to "pyphe_quant".')
30 |
31 | args = parser.parse_args()
32 |
33 | #Check that coefficients are within bounds
34 | if not args.t > 0:
35 | raise ValueError('t must be > 0.')
36 | if not args.d >= 2:
37 | raise ValueError('d must be >= 2.')
38 | if not args.s>0:
39 | raise ValueError('s must be > 0.')
40 |
41 | ###Load images as collection###
42 | images = ImageCollection(args.pattern, conserve_memory=True)
43 | if not len(images) > 0:
44 | raise ValueError('No images to analyse. By default all .jpg images in the current working directory will be analysed. The folder and file type can be changed using the --pattern option.')
45 | print('Starting analysis of %i images in %s mode'%(len(images), args.mode))
46 |
47 | ###Make grid###
48 | #Predefined grids
49 | grid = args.grid
50 | if grid == 'pp3_96':
51 | grid = '8-12-1-2-3-4'
52 | auto = False
53 | elif grid == 'pp3_384':
54 | grid = '16-24-30-38-1254-821'
55 | auto = False
56 | elif grid == 'pp3_1536':
57 | grid = '32-48-43-50-2545-1690'
58 | auto = False
59 |
60 | elif grid == 'auto_96':
61 | auto = True
62 | grid = '8-12'
63 | elif grid == 'auto_384':
64 | auto = True
65 | grid = '16-24'
66 | elif grid == 'auto_1536':
67 | auto = True
68 | grid = '32-48'
69 |
70 | elif grid.startswith('auto_'):
71 | auto = True
72 | griddef = grid[5:]
73 | try:
74 | griddef = griddef.split('-')
75 | griddef = map(int, griddef)
76 | grid = '-'.join(map(str,griddef))
77 | except Exception:
78 | raise ValueError('Invalid grid definition. If auto grid fitting with custom numbers of rows/columns is desired, the grid argument must be "auto_r-c", where r and c are the numbers of rows/columns respectively')
79 |
80 |
81 | else:
82 | auto = False
83 |
84 | #If user defined grid, check if in the right format
85 | if not auto:
86 | if not len(grid.split('-'))==6:
87 | raise ValueError('Grid definition not in correct format. Must be one of auto_96, auto_384, auto_1536, pp3_96, pp3_384, p3_1536 or a custom grid definition consisting of 6 integers separated by "-".')
88 | grid = grid.split('-')
89 | try:
90 | grid = list(map(int, grid))
91 | except Exception:
92 | raise ValueError('Grid definition not in correct format. Must be one of auto_96, auto_384, auto_1536, pp3_96, pp3_384, pp3_1536 or a custom grid definition consisting of 6 integers separated by "-".')
93 |
94 |
95 | #Create output folders
96 | analysis.check_mkdir(args.out)
97 | analysis.check_mkdir(args.qc)
98 |
99 | ###Start analysis###
100 | arg_dict = dict(vars(args))
101 | arg_dict['negate'] = arg_dict['no_negate']
102 | arg_dict.pop('no_negate')
103 | argstr = '\n '.join(['%s: %s'%(k,str(v)) for k,v in arg_dict.items()])
104 | print('Starting analysis with the following parameters:\n%s'%argstr)
105 | arg_dict.pop('grid')
106 | arg_dict.pop('mode')
107 | arg_dict.pop('pattern')
108 |
109 | if (args.mode == 'batch') or (args.mode == 'redness'):
110 | arg_dict.pop('calibrate')
111 | arg_dict.pop('timepoints')
112 | quantify.quantify_batch(images, grid, auto, args.mode, **arg_dict)
113 | if args.mode == 'timecourse':
114 | quantify.quantify_timecourse(images, grid, auto, **arg_dict)
115 |
116 | print('Analysis complete.')
117 |
118 |
119 |
--------------------------------------------------------------------------------
/bin/pyphe-quantify.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | echo Welcome to pyphe for Windows. Running pyphe-quantify using the following command:
3 | set a=python %~dp0%0
4 | :argactionstart
5 | if -%1-==-- goto argactionend
6 | set a=%a% %1 & REM Or do any other thing with the argument
7 | shift
8 | goto argactionstart
9 | :argactionend
10 | echo %a%
11 | %a%
12 |
13 |
--------------------------------------------------------------------------------
/bin/pyphe-scan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import argparse
4 | import time
5 | from pyphe import scan
6 |
7 | #Fixture cropping parameters, change/add your own if you know what you are doing
8 | geometries = {
9 | 'som3_edge':['2034x2865+84+0', '2034x2865+2292+0', '2034x2865+97+3135', '2034x2865+2317+3135'],
10 | 'som3' : ['1726x2603+257+127', '1726x2603+2434+127', '1726x2603+257+3274', '1726x2603+2434+3274'],
11 | 'petrie' : ['2105x2105+20+300', '2105x2105+2222+290', '2105x2105+20+3534', '2105x2105+2214+3534'],
12 | 'som3-color' : ['1726x2603+398+616', '1726x2603+2574+616', '1726x2603+398+3764', '1726x2603+2574+3764'],
13 | }
14 |
15 |
16 | if __name__ == '__main__':
17 | #Set up parsing of command line arguments with argparse
18 | parser = argparse.ArgumentParser(description='Welcome to pyphe-scan, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe')
19 |
20 | parser.add_argument('--nplates', type=int, default=100, help='Number of plates to scan. This defaults to 100 and the script can be terminated by Ctr+C when done.')
21 | parser.add_argument('--start', type=int, default=1, help='Where to start numbering from. Defaults to 1.')
22 | parser.add_argument('--prefix', type=str, default=time.strftime('%Y%m%d'), help='Name prefix for output files. The default is the current date YYYYMMDD.')
23 | parser.add_argument('--postfix', type=str, default='', help='Name postfix for output files. Defaults to empty string.')
24 | parser.add_argument('--fixture', type=str, choices = list(geometries), help='ID of the fixture you are using.')
25 | parser.add_argument('--format', type=str, default='jpg', choices = ['jpg', 'tiff'], help='Format of the cropped and rotated images. Must be jpg (saves diskspace) or tiff (preserves all image data exactly).')
26 | parser.add_argument('--resolution', choices=[150,300,600,900,1200], type=int, default=600, help='Resolution for scanning in dpi. Default is 600.')
27 | parser.add_argument('--scanner', choices=[1,2,3], type=int, default=1, help='Which scanner to use. Scanners are not uniquely identified and may switch when turned off/unplugged. Scanner numbers are defined by the order in which they are connected to the computer. This option does not need to be set when only one scanner is connected.')
28 | parser.add_argument('--mode', choices=['Gray', 'Color'], type=str, default='Gray', help='Which color mode to use for scanning. Defaults to Gray.')
29 |
30 | args = parser.parse_args()
31 |
32 | scanner = scan.find_scanner(args.scanner)
33 |
34 | scan.scan_batch(args.nplates, args.start, args.prefix, args.postfix, args.fixture, args.resolution, geometries, scanner, args.mode, args.format)
35 |
--------------------------------------------------------------------------------
/bin/pyphe-scan-timecourse:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import time
4 | from pyphe import scan
5 |
6 |
7 | #Fixture cropping parameters, change/add your own if you know what you are doing
8 | geometries = {
9 | 'som3_edge':['2034x2865+84+0', '2034x2865+2292+0', '2034x2865+97+3135', '2034x2865+2317+3135'],
10 | 'som3' : ['1726x2603+257+127', '1726x2603+2434+127', '1726x2603+257+3274', '1726x2603+2434+3274'],
11 | 'petrie' : ['2105x2105+20+300', '2105x2105+2222+290', '2105x2105+20+3534', '2105x2105+2214+3534'],
12 | 'som3-color' : ['1726x2603+398+616', '1726x2603+2574+616', '1726x2603+398+3764', '1726x2603+2574+3764'],
13 | }
14 |
15 |
16 | if __name__ == '__main__':
17 | #Set up parsing of command line arguments with argparse
18 | parser = argparse.ArgumentParser(description='Welcome to pyphe-scan-timecourse, part of the pyphe toolbox. Written by stephan.kamrad@crick.ac.uk and maintained at https://github.com/Bahler-Lab/pyphe')
19 |
20 | parser.add_argument('--nscans', type=int, default=100, help='Number of time points to scan. This defaults to 100 and the script can be terminated by Ctr+C when done.')
21 | parser.add_argument('--interval', type=int, default=20, help='Time in minutes between scans. Defaults to 20.')
22 | parser.add_argument('--prefix', type=str, default=time.strftime('%Y%m%d'), help='Name prefix for output files. The default is the current date YYYYMMDD.')
23 | parser.add_argument('--postfix', type=str, default='', help='Name postfix for output files. Defaults to empty string.')
24 | parser.add_argument('--fixture', type=str, choices = list(geometries), help='ID of the fixture you are using.')
25 | parser.add_argument('--format', type=str, default='jpg', choices = ['jpg', 'tiff'], help='Format of the cropped and rotated images. Must be jpg (saves diskspace) or tiff (preserves all image data exactly).')
26 | parser.add_argument('--resolution', choices=[150,300,600,900,1200], type=int, default=600, help='Resolution for scanning in dpi. Default is 600.')
27 | parser.add_argument('--scanner', choices=[1,2,3], type=int, default=1, help='Which scanner to use. Scanners are not uniquely identified and may switch when turned off/unplugged. Scanner numbers are defined by the order in which they are connected to the computer. This option does not need to be set when only one scanner is connected.')
28 | parser.add_argument('--mode', choices=['Gray', 'Color'], type=str, default='Gray', help='Which color mode to use for scanning. Defaults to Gray.')
29 |
30 | args = parser.parse_args()
31 |
32 | scanner = scan.find_scanner(args.scanner)
33 |
34 | scan.scan_timecourse(args.nscans, args.interval, args.prefix, args.postfix, args.fixture, args.resolution, geometries, scanner, args.mode, args.format)
35 |
--------------------------------------------------------------------------------
/icons/gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/icons/gui.png
--------------------------------------------------------------------------------
/icons/toolbox-72dpi_tp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/icons/toolbox-72dpi_tp.png
--------------------------------------------------------------------------------
/icons/toolbox-72dpi_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/icons/toolbox-72dpi_white.png
--------------------------------------------------------------------------------
/icons/toolbox_icon-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/icons/toolbox_icon-01.png
--------------------------------------------------------------------------------
/pyphe/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/pyphe/__init__.py
--------------------------------------------------------------------------------
/pyphe/__pycache__/quantify.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/pyphe/__pycache__/quantify.cpython-37.pyc
--------------------------------------------------------------------------------
/pyphe/analysis.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from warnings import warn
3 | import os
4 | from scipy import interpolate
5 | import numpy as np
6 |
7 | from matplotlib.backends.backend_pdf import PdfPages
8 | from matplotlib import pyplot as plt
9 | import seaborn as sns
10 | sns.set_style('white')
11 |
12 | class Experiment():
13 | '''
14 | A pyphe Experiment object facilitates working with a large number of plates/images at the same time. The constructor takes a single argument which is a pandas DataFrame containing some basic information about your experiment. This table should have one line per plate image and should normally contain at least two columns (although column names are not strictly enforced): Assay_plate (which specifies the layout of the plate which we will later read in from a different file) and Image_path (the relative or absolute path to the image file). Normally this file will have additional columns which you can use to keep track of any additional information about the plates, e.g. batch numbers, dates, condition. This information will be retained and propagated into the final output file.
15 | It is highly advisable to use a meaningful and unique index for this DataFrame which we can later be used to easily identify individual plots. Alternatively, the pandas default of using interger ranges as index can be used.
16 |
17 | Required arguments for creating an Experiment object:
18 | exp_data (DataFrame) - A DataFrame holding experiment meta-data
19 |
20 | An Experiment object has the following attributes:
21 | exp_data - A DataFrame holding experiment meta-data
22 | plates - A pandas Series of Plate objects, the index is identical to exp_data
23 | '''
24 |
25 | def __init__(self, exp_data):
26 | assert not exp_data.index.duplicated().any(), 'Plate IDs must be unique'
27 | self.exp_data = exp_data
28 |
29 | self.plates = pd.Series({i : Plate(plateid=i, meta_data=r) for i, r in self.exp_data.iterrows()})
30 |
31 | def update_meta_data(self):
32 | '''Update plate meta_data when exp_data has been modified. This is not ideal and could pose a risk of information being overwritten if the user manually edits plate meta-data.'''
33 | for i,p in self.plates.items():
34 | p.meta_data = self.exp_data.loc[i]
35 |
36 | def generate_long_data(self):
37 | '''Make a summary table with all data from all Plates for stats and plotting.
38 | Returns:
39 | summary_long (pandas DataFrame)
40 | '''
41 |
42 | longs = []
43 | for i,p in self.plates.items():
44 | p_long = p.make_long_data()
45 |
46 | for m in self.exp_data:
47 | p_long[m] = [self.exp_data.loc[i,m] for l in p_long.index]
48 |
49 | p_long['Plate'] = i
50 | longs.append(p_long)
51 |
52 | summary_long = pd.concat(longs)
53 | summary_long.index = range(len(summary_long.index))
54 |
55 | return summary_long
56 |
57 | def batch_gitter(self, plate_format, grid_image_folder='grid_images', dat_file_folder='dat_files', inverse='TRUE', remove_noise='TRUE', autorotate='FALSE', gitter_script_name='gitter_script.R'):
58 | '''
59 | Wrapper script for gitter. The Experiment object's exp_data must have an Image_path column for this to work.
60 | This will write a gitter script into the current directory, which you can execute with Rscr
61 |
62 | Required arguments:
63 | plate_format (int) - Plate format in gitter convention 96/384/1536
64 |
65 | Keyword arguments:
66 | grid_image_folder (str) - Path of the folder to save the grid images in. Defaults to grid_images
67 | dat_file_folder (str) - Path of the folder to save the dat files in. Defaults to dat_files
68 | gitter_script_name (str) - Path of the gitter script to be outputted. Defaults to gitter_script.R
69 | inverse (str) - Has to be TRUE/FALSE. Invert image before analysis (reuqired for transmission scanning). See gitter documentation.
70 | remove_noise (str) - Has to be TRUE/FALSE. Reduce image noise before analysis. See gitter documentation.
71 |
72 | Returns:
73 | None
74 | '''
75 | check_mkdir(grid_image_folder)
76 | check_mkdir(dat_file_folder)
77 |
78 | with open(gitter_script_name, 'w') as gs:
79 | gs.write('library("gitter")\n')
80 | for fpath in self.exp_data['Image_path']:
81 | gitter_cmd = 'gitter("%s", plate.format=%i, inverse="%s", remove.noise="%s", grid.save="%s", dat.save="%s", autorotate="%s")'%(fpath, plate_format, inverse, remove_noise, grid_image_folder, dat_file_folder, autorotate)
82 | gs.write(gitter_cmd+'\n')
83 |
84 | self.exp_data['Data_path'] = dat_file_folder + '/' + self.exp_data['Image_path'].map(lambda x: x.split('/')[-1]) + '.dat'
85 | self.exp_data['Gitter_grid_path'] = grid_image_folder + '/gridded_' + self.exp_data['Image_path'].map(lambda x: x.split('/')[-1])
86 |
87 | self.update_meta_data()
88 |
89 | print('Gitter script created, please run the following command: Rscript %s'%gitter_script_name)
90 |
91 |
92 | class Plate():
93 | '''This object holds all data for a single plate. Two empty, all-purpose pandas series are initilased to hold the bulk of the data associated with the Plate object: Plate.meta_data can be used to store meta data of all sorts and these will be included in the output report. Plate.pos_data is used to store pandas dataframes that have the same shape as the gridformat and are used to store actual growth data and analysis results.
94 | Keyword arguments:
95 | meta_data (Series) - A pandas series containing meta-information about the plate.
96 | '''
97 |
98 | def __init__(self, meta_data=None, plateid=None):
99 | self.plateid = plateid
100 | self.meta_data = meta_data
101 | self.pos_data = pd.Series()
102 |
103 | def read_gitter_single_image(self):
104 | '''Read gitter data from file. Adds two new DataFrames to the Plate's pos_data Series, for colony size and one for circularity. The path of the dat file is taken from the PLate's meta_data.'''
105 |
106 | dat = pd.read_csv(self.meta_data['Data_path'], comment='#', header=None, names=['row', 'col', 'size', 'circularity', 'flags'], sep='\t')
107 |
108 | size = dat.pivot(index='row', columns='col', values='size')
109 | size.index.name = None
110 | size.index = size.index.map(str)
111 | size.columns.name = None
112 | size.columns = size.columns.map(str)
113 | self.pos_data['Colony_size'] = size
114 |
115 | circularity = dat.pivot(index='row', columns='col', values='circularity')
116 | circularity.index.name = None
117 | circularity.index = circularity.index.map(str)
118 | circularity.columns.name = None
119 | circularity.columns = circularity.columns.map(str)
120 | self.pos_data['Colony_circularity'] = circularity
121 |
122 | def read_pypheredness_single_image(self):
123 | '''Read column from pyphe-quantify redness output file'''
124 |
125 | dat = pd.read_csv(self.meta_data['Data_path'])
126 |
127 | size = dat.pivot(index='row', columns='column', values='mean_intensity')
128 | size.index.name = None
129 | size.index = size.index.map(str)
130 | size.columns.name = None
131 | size.columns = size.columns.map(str)
132 | self.pos_data['Colony_size'] = size
133 |
134 | circularity = dat.pivot(index='row', columns='column', values='circularity')
135 | circularity.index.name = None
136 | circularity.index = circularity.index.map(str)
137 | circularity.columns.name = None
138 | circularity.columns = circularity.columns.map(str)
139 | self.pos_data['Colony_circularity'] = circularity
140 |
141 | def read_pyphebatch_single_image(self):
142 | '''Read column from pyphe-quantify redness output file'''
143 |
144 | dat = pd.read_csv(self.meta_data['Data_path'])
145 |
146 | size = dat.pivot(index='row', columns='column', values='area')
147 | size.index.name = None
148 | size.index = size.index.map(str)
149 | size.columns.name = None
150 | size.columns = size.columns.map(str)
151 | self.pos_data['Colony_size'] = size
152 |
153 | circularity = dat.pivot(index='row', columns='column', values='circularity')
154 | circularity.index.name = None
155 | circularity.index = circularity.index.map(str)
156 | circularity.columns.name = None
157 | circularity.columns = circularity.columns.map(str)
158 | self.pos_data['Colony_circularity'] = circularity
159 |
160 | def read_pgc_single_image(self):
161 | '''Read pyphe-growthcurves output file'''
162 |
163 | dat = pd.read_csv(self.meta_data['Data_path'], index_col=0).transpose()
164 |
165 | dat['Row'] = dat.index.map(lambda x: int(x.split('-')[0]))
166 | dat['Column'] = dat.index.map(lambda x: int(x.split('-')[1]))
167 |
168 | resvars = ['initial biomass', 'lag', 'r2', 't_max', 'y-intercept', 'x-intercept', 'max_slope', 'sum of values (AUC)', 'maximum']
169 | resvars = [s for s in resvars if s in dat.columns]
170 |
171 | for c in resvars:
172 | tf = dat.pivot(index='Row', columns='Column', values=c)
173 | tf = tf.sort_index().sort_index(axis=1)
174 | tf.index = tf.index.map(str)
175 | tf.columns = tf.columns.map(str)
176 |
177 | if c == 'max_slope':
178 | self.pos_data['Colony_size'] = tf.astype(float)
179 | else:
180 | self.pos_data[c] = tf.astype(float)
181 |
182 |
183 | def read_layout_single_plate(self, kwargs={'header':None}):
184 | '''Read the layout of a single file in wide format. This is essentially a wrapper for pandas' read_csv() function which will store returned DataFrame in the pos_data Series of the plate instance. The path of the layout file needs to be provided in the exp_data file in a column named Layout_path. The layout file should not have any header or index information but this can be overriden by supplying keyword arguments as a dictionary to the kwargs argument. Any keyword arguments provided will be passed to pandas' read_csv() function.'''
185 |
186 | imported = pd.read_csv(self.meta_data['Layout_path'], **kwargs)
187 | imported.index = map(str, range(1, len(imported.index)+1))
188 | imported.columns = map(str, range(1, len(imported.columns)+1))
189 |
190 | self.pos_data['Strain'] = imported
191 |
192 | def rcmedian_normalisation(self, inkey='Colony_size', outkey='Colony_size_corr'):
193 | '''This function implements row/column median normalisation with smoothing of row/column medians across plates.
194 |
195 | Required aguments:
196 | inkey (str) -- The key in pos_data which to use as input
197 | outkey (str) -- The key in pos_data under which to store the result
198 | Returns:
199 | None
200 | '''
201 |
202 | col_medians = self.pos_data[inkey].median(axis=0)
203 | row_medians = self.pos_data[inkey].median(axis=1)
204 |
205 | normed = self.pos_data[inkey].div(row_medians, axis='index')
206 | normed = normed.div(col_medians, axis='columns')
207 | normed = normed/normed.stack().median()
208 | self.pos_data[outkey] = normed
209 |
210 | def grid_normalisation(self, gridpos_list, inkey='Colony_size', outkey='Colony_size_corr', set_missing_nan=True, remove_grid_outliers=False, k=3, extrapolate_corners=False, horizontal_neighbour_coeff=None, vertical_neighbour_coeff=None, intercept_coeff=None):
211 |
212 | '''Apply reference-grid normalisation to quantitative colony data stored in pos_data. First, the data of the grid colonies is extracted from pos_data[inkey], based on the provided list of control positions. There is an option to filter out extreme grid outliers (e.g. due to pinning errors) using a z-score cut-off based on the overall distribution of grid colony values. In this implementation of the grid normalisation it is only possible to extrapolate the lower left and upper right corners of 1536 plates. The grid must be placed as two 96 grids, in the upper left and lower right corner. Please do not use this option if those conditions are not met. Finally, the grid is interpolated using 2d cubic interpolation as implemented in scipy's interpolate.griddata() function. The corrected values are computed by dividing the acutal colony value by the expected (interpolated) value and the result is stored as a new DataFrame in the plate's pos_data.
213 |
214 | Required arguments:
215 |
216 | gridpos_list (list) -- list of two-length tuples, containing row and column positions as integers)
217 | remove_grid_outliers (bool) -- Should extreme outliers in reference grid be removed? This leads to loss of data if positions are on edge of grid.
218 |
219 | Keyword arguments:
220 | set_missing_nan (bool) - Set colonies near grid positions which are 0 or nan (usually indicating pinning errors) to nan (Recommended).
221 | inkey (str) -- The key in pos_data which to use as input. Defaults to Colony_size.
222 | outkey (str) -- The key in pos_data under which to store the result. Defaults to Colony_size_corr.
223 | remove_outliers (bool) -- Reemove grid outliers with a z-score of more than k, if they are not on the edge.
224 | k (float) -- Grid positions with a Z-score of greater than k will be removed from grid. Only required when remove_grid_outliers is True, otherwise ignored.
225 |
226 | Returns:
227 | None
228 | '''
229 |
230 | grid = pd.DataFrame(index=self.pos_data[inkey].index, columns=self.pos_data[inkey].columns, dtype=float)
231 | for row, col in gridpos_list:
232 | grid.loc[str(row), str(col)] = self.pos_data[inkey].loc[str(row), str(col)]
233 |
234 | #Look for areas where the grid is nan or 0
235 | na_mask = pd.DataFrame(False, index=self.pos_data[inkey].index, columns=self.pos_data[inkey].columns, dtype=bool)
236 | nan_zero_count = 0
237 | for row, col in gridpos_list:
238 | if (pd.isnull(grid.loc[str(row), str(col)])) or (grid.loc[str(row), str(col)] == 0.0):
239 | nan_zero_count += 1
240 |
241 | #Set this to NA in the grid itself
242 | grid.loc[str(row), str(col)] = np.nan
243 |
244 | #Set the neighbours and the field itself in the NA_mask to NAN
245 | na_mask.loc[str(row), str(col)] = True
246 | na_mask.loc[str(row), str(col+1)] = True
247 | na_mask.loc[str(row), str(col-1)] = True
248 | na_mask.loc[str(row+1), str(col)] = True
249 | na_mask.loc[str(row-1), str(col)] = True
250 | na_mask.loc[str(row+1), str(col+1)] = True
251 | na_mask.loc[str(row-1), str(col+1)] = True
252 | na_mask.loc[str(row+1), str(col-1)] = True
253 | na_mask.loc[str(row-1), str(col-1)] = True
254 |
255 | #Need to remove any new columns/rows added on the edge and need to recast type to bool
256 | na_mask = na_mask.loc[self.pos_data[inkey].index, self.pos_data[inkey].columns].astype(bool)
257 |
258 | self.pos_data['Near_missing_grid'] = pd.DataFrame(na_mask, index=self.pos_data[inkey].index, columns=self.pos_data[inkey].columns, dtype=bool)
259 |
260 | if nan_zero_count > 0:
261 | if set_missing_nan:
262 | #Check for grid colonies that didn't grow and report
263 | print('Plate %s: %i grid colonies have size 0, probably due to pinning errors. These and neighbouring colonies will be set to nan.'%(self.plateid, nan_zero_count))
264 | else:
265 | #Check for grid colonies that didn't grow and throw a warning
266 | print('Plate %s: %i grid colonies have size 0, probably due to pinning errors. I will ignore this for now and interpolate grid values based on surrounding grid colonies. However, it is recommended that you set these to nan. Affected colonies are marked in the Near_missing_grid column.'%(self.plateid, nan_zero_count))
267 |
268 | if remove_grid_outliers:
269 | #Calculate z-scores
270 | sigma = grid.unstack().std() # std dev of all ref colonies
271 | mu = grid.unstack().mean() # mean of all ref colonies
272 |
273 | z_scores = grid - mu
274 | z_scores = z_scores / sigma
275 | z_score_mask = z_scores.abs()>k
276 |
277 | #Whatever the z-score, dont remove grid positions on edges of plate, this leads to missing data and these are more variable in general
278 | z_score_mask.iloc[:,0] = False
279 | z_score_mask.iloc[0,:] = False
280 | z_score_mask.iloc[:,-1] = False
281 | z_score_mask.iloc[-1,:] = False
282 |
283 | grid[z_score_mask] = np.nan
284 | print('Plate %s: Removed %i outlier grid colonies'%(self.plateid, z_score_mask.unstack().sum()))
285 |
286 | if extrapolate_corners:
287 | #warn('In this implementation of the grid normalisation it is only possible to extrapolate the lower left and upper right corners of 1536 plates. The grid must be placed as two 96 grids, in the upper left and lower right corner. Please do not use this option if those conditions are not met.')
288 | grid.loc['32','1'] = intercept_coeff + horizontal_neighbour_coeff*grid.loc['32','4'] + vertical_neighbour_coeff*grid.loc['29','1']
289 | grid.loc['1','48'] = intercept_coeff + horizontal_neighbour_coeff*grid.loc['1','45'] + vertical_neighbour_coeff*grid.loc['4','48']
290 |
291 |
292 | self.pos_data['Grid'] = grid
293 |
294 |
295 | #Calculate reference surface
296 | #Get new list of grid positions after extrapolation and noise filtering
297 | grid_us = grid.unstack().dropna().reset_index()
298 | grid_us.columns = ['col', 'row', 'val']
299 | gridpos_list_new = grid_us[['row', 'col']].values
300 | values = grid_us['val'].values
301 |
302 | #Points to interpolate
303 | xi = grid.unstack()
304 | xi = xi[pd.isnull(xi)]
305 | xi = xi.reset_index()
306 | xi.columns = ['col', 'row', 'val']
307 | xi = xi[['row', 'col']].values
308 |
309 | #interpolate
310 | interpolated = interpolate.griddata(gridpos_list_new, values, xi, method='cubic')
311 |
312 | ref_surface = pd.DataFrame(index=self.pos_data[inkey].index, columns=self.pos_data[inkey].columns, dtype=float)
313 | for i,p in enumerate(gridpos_list_new):
314 | ref_surface.loc[str(p[0]), str(p[1])] = grid.loc[str(p[0]), str(p[1])]
315 | for i,p in enumerate(xi):
316 | ref_surface.loc[str(p[0]), str(p[1])] = interpolated[i]
317 | self.pos_data['Reference_surface'] = ref_surface
318 |
319 | #Get ratio of max slope to wild type
320 | corr_data = self.pos_data[inkey]/ref_surface
321 | if set_missing_nan:
322 | corr_data[na_mask] = np.nan
323 | self.pos_data[outkey] = corr_data
324 |
325 | def plot_pos_data(self, pdf_path=None, toPlot=None):
326 | '''Plot DataFrames containing numerical values as heatmaps using seaborn and matplotlib.
327 | Keyword arguments:
328 | pdf_path -- This option is highly recommended when you are dealing with large batches of Plates. It saves figures to pdf and then closes them so that they do not build up in memory. Please provide the name of a folder to save pdfs in, the filename is identical to the plateid.
329 | toPlot (list) -- List of data to plot. Must be keys of Plate.pos_data. If not set, all DataFrames in pos_data which contain numeric values will be plotted.
330 |
331 | Returns:
332 | None
333 | '''
334 |
335 | if not toPlot:
336 | toPlot = []
337 | for i, fr in self.pos_data.items():
338 | try:
339 | fr.astype(float)#Select only DataFrames which can be cast to numeric.
340 | toPlot.append(i)
341 | except Exception:
342 | pass
343 |
344 | if pdf_path:
345 | pdf = PdfPages(os.path.join(pdf_path, str(self.plateid))+'.pdf')
346 |
347 | for key in toPlot:
348 | fig, ax = plt.subplots()
349 | sns.heatmap(data=self.pos_data[key], ax=ax)
350 | ax.set_title(key)
351 | if pdf:
352 | pdf.savefig()
353 | fig.clf()
354 | plt.close(fig)
355 |
356 | if pdf_path:
357 | pdf.close()
358 |
359 | def check_values(self, inkey='Colony_size_corr', outkey='Colony_size_corr_checked', negative_action=0, inf_action=10):
360 | ''' This function checks for invalid values in plate pos data. Normalisation procedures can produce invalid values
361 | as side effect, such as negative or infinite fitness values. This function detects those and deals with them in
362 | a custamisable way.
363 |
364 | Keyworkd arguments:
365 | inkey (str) -- The data to use (must be a key in exp.plates.pos_data)
366 | negative_action (float) -- Value to assign to negative fitness. Defaults to 0. np.nan is allowed too. Set to None if no action required.
367 | inf_action (float) -- Value to assign to inf values. Defaults to 10. np.nan is allowed too. Set to None if no action required.
368 |
369 | Returns:
370 | None (exp.plates.pos_data is modified in place)
371 | '''
372 |
373 | data = self.pos_data[inkey]
374 | ##In some rare cases the input DataFrame can be empty. In that case, just set to an empty DataFrame
375 | if data.empty:
376 | self.pos_data[outkey] = pd.DataFrame([[]])
377 | return None
378 | data = data.unstack()
379 |
380 | #Ignore na
381 | data = data.dropna()
382 |
383 | #Check for inf
384 | isneginf = data[np.isneginf(data.values)]
385 | if len(isneginf.index) != 0:
386 | print('Plate %s - The following positions are minus infinity: %s'%(self.plateid, str(dict(isneginf))))
387 | isposinf = data[np.isposinf(data.values)]
388 | if len(isposinf.index) != 0:
389 | print('Plate %s - The following positions are plus infinity: %s'%(self.plateid, str(dict(isposinf))))
390 |
391 | #Check for negative
392 | neg = data[~np.isinf(data.values)]
393 | neg = neg[neg<0]
394 | if len(neg.index) != 0:
395 | print('Plate %s - The following positions are negative: %s'%(self.plateid, str(dict(neg))))
396 |
397 | #Actions
398 | if inf_action is not None:
399 | inf_action = float(inf_action)
400 | if pd.isnull(inf_action):
401 | self.pos_data[inkey][np.isinf(self.pos_data[inkey])] = inf_action
402 | else:
403 | self.pos_data[inkey][np.isneginf(self.pos_data[inkey])] = -inf_action
404 | self.pos_data[inkey][np.isposinf(self.pos_data[inkey])] = inf_action
405 |
406 | if negative_action is not None:
407 | negative_action = float(negative_action)
408 | self.pos_data[outkey] = self.pos_data[inkey].copy()
409 | self.pos_data[outkey][self.pos_data[outkey].replace(-np.inf, 1).replace(np.inf, 1).replace(np.nan, 1)<0] = negative_action
410 |
411 |
412 |
413 | def make_long_data(self):
414 | '''Generate a summary table of all Data stored in this Plate object.
415 | Returns:
416 | long_data (pandas DataFrame)
417 | '''
418 | unstacked = []
419 | for k, frame in self.pos_data.items():
420 | if frame.empty:
421 | continue
422 | l_unstacked = frame.copy()
423 | l_unstacked.columns.name = 'Column'
424 | l_unstacked.index.name = 'Row'
425 | l_unstacked = l_unstacked.unstack()
426 | l_unstacked.name = k
427 | unstacked.append(l_unstacked)
428 | if len(unstacked) == 0:
429 | warn('No data associated with Plate %s. Please check input data'%self.plateid)
430 | return pd.DataFrame([[]])
431 | else:
432 | long_data = pd.concat(unstacked, axis=1)
433 | long_data = long_data.reset_index()
434 |
435 | return long_data
436 |
437 | def check_mkdir(dirPath):
438 | '''
439 | Create a directory if it does not exist already.
440 |
441 | Required arguments:
442 | dirPath (str) - path of the directory to create.
443 | '''
444 |
445 | if os.path.exists(dirPath):
446 | warn('Directory exist, not doing anything.')
447 | else:
448 | os.mkdir(dirPath)
449 |
450 |
451 |
452 | def check_exp_data(exp_data, layouts=False):
453 | print('Checking exp_data table')
454 |
455 | print('Checking if plate IDs (first column) are unique')
456 | assert not exp_data.index.duplicated().any(), 'Error, Plate IDs are not unique'
457 | print('....OK')
458 |
459 | print('Checking for mandatory columns')
460 | assert 'Data_path' in exp_data.columns, 'Error, table must have Data_path column'
461 | if layouts:
462 | assert 'Layout_path' in exp_data.columns, 'Error, table must have Layout_path column'
463 | print('....OK')
464 |
465 | print('Checking if data files are unique')
466 | assert not exp_data['Data_path'].duplicated().any(), 'Error, data paths are not unique (plates with the same ID have been assigned the same data file).'
467 | print('....OK')
468 |
469 | print('Checking all data files exist')
470 | for ip in exp_data['Data_path']:
471 | if not os.path.isfile(ip):
472 | raise IOError('Data file does not exist: %s'%ip)
473 | print('...OK')
474 |
475 | if layouts:
476 | print('Checking all layout files exist')
477 | for ip in exp_data['Layout_path']:
478 | if not os.path.isfile(ip):
479 | raise IOError('Layout file does not exist: %s'%ip)
480 | print('...OK')
481 |
482 | def pyphe_cmd(wdirectory=None, grid_norm=None, out_ld=None, qcplots=None, check_setNA=None, qcplot_dir=None, exp_data_path=None, extrapolate_corners=None, grid_pos=None, rcmedian=None, input_type=None, load_layouts=None):
483 | '''
484 | This function was written to be called from the GUI script provided. But it can also be used to run the entire standard pipeline in one place.
485 | '''
486 |
487 | print('###Step 1: Load data###')
488 |
489 | #Set working directory
490 | if wdirectory:
491 | os.chdir(wdirectory)
492 | print('Working directory changed to: %s'%wdirectory)
493 | #Import exp_data
494 | exp_data = pd.read_csv(exp_data_path, index_col=0)
495 | check_exp_data(exp_data, layouts=load_layouts)
496 | print('Table checks completed')
497 |
498 | exp = Experiment(exp_data)
499 | print('Created pyphe experiment object')
500 |
501 | #Load the data
502 | if input_type == 'gitter':
503 | exp.plates.map(Plate.read_gitter_single_image)
504 |
505 | elif input_type == 'pyphe-quantify-redness':
506 | exp.plates.map(Plate.read_pypheredness_single_image)
507 |
508 | elif input_type == 'pyphe-growthcurves':
509 | exp.plates.map(Plate.read_pgc_single_image)
510 |
511 | elif input_type == 'pyphe-quantify-batch':
512 | exp.plates.map(Plate.read_pyphebatch_single_image)
513 |
514 | else:
515 | raise ValueError('Unrecignised input_type')
516 | print('Plate data loaded sucessfully')
517 |
518 |
519 | #Load the layouts
520 | if load_layouts:
521 | exp.plates.map(Plate.read_layout_single_plate)
522 | print('Layouts loaded sucessfully')
523 |
524 | #Perform norms
525 | if grid_norm:
526 | if input_type == 'pyphe-quantify-redness':
527 | raise ValueError('Grid normalisation does not make sense for redness input')
528 |
529 | print('Performing grid norm')
530 |
531 | if (grid_pos == 'Standard 384 (top left)') or (grid_pos == 'standard384'):
532 | gridpos_list = [(row, col) for row in range(1, 16, 2) for col in range(1, 24, 2)]
533 | elif (grid_pos == 'Standard 1536 (top left and bottom right)') or (grid_pos == 'standard1536'):
534 | gridpos_list = [(row, col) for row in range(1, 32, 4) for col in range(1, 48, 4)]
535 | gridpos_list += [(row, col) for row in range(4, 33, 4) for col in range(4, 49, 4)]
536 | elif grid_pos == '1536with384grid':
537 | gridpos_list = [(row, col) for row in range(1, 32, 2) for col in range(1, 48, 2)]
538 | else:
539 | raise ValueError('grid_pos must be one of ["Standard 384 (top left)", "standard384", "Standard 1536 (top left and bottom right)", "standard1536", "1536with384grid"]')
540 |
541 |
542 | if extrapolate_corners:
543 | from sklearn.linear_model import LinearRegression
544 | #Make a table of features
545 | vals = pd.DataFrame(columns=['thisCorner', 'horizontalNeighbour', 'verticalNeighbour'])
546 | for i,p in exp.plates.items():
547 | vals.loc[i+'_topLeft'] = [p.pos_data['Colony_size'].loc['1','1'], p.pos_data['Colony_size'].loc['1','5'], p.pos_data['Colony_size'].loc['5','1']]
548 | vals.loc[i+'_bottomRight'] = [p.pos_data['Colony_size'].loc['32','48'], p.pos_data['Colony_size'].loc['32','44'], p.pos_data['Colony_size'].loc['28','48']]
549 | mlm = LinearRegression()
550 | mlm.fit(vals.iloc[:,1:], vals.iloc[:,0])
551 | print('Extrapolating missing corners based on the following regression: ')
552 | print(' horizontal_neighbour_coeff: ' + str(mlm.coef_[0]))
553 | print(' vertical_neighbour_coeff: ' + str(mlm.coef_[1]))
554 | print(' intercept_coeff: ' + str(mlm.intercept_))
555 | print(' accuracy: ' +str(mlm.score(vals.iloc[:,1:], vals.iloc[:,0])))
556 |
557 | exp.plates.map(lambda x: x.grid_normalisation(gridpos_list,
558 | extrapolate_corners=True, horizontal_neighbour_coeff=mlm.coef_[0],
559 | vertical_neighbour_coeff=mlm.coef_[1], intercept_coeff=mlm.intercept_))
560 |
561 | else:
562 | exp.plates.map(lambda x: x.grid_normalisation(gridpos_list) )
563 |
564 |
565 | if rcmedian:
566 | print('Performing row/column median normalisation')
567 | if grid_norm:
568 | ikey = 'Colony_size_corr'
569 | okey = 'Colony_size_corr'
570 | else:
571 | ikey = 'Colony_size'
572 | okey = 'Colony_size_corr'
573 |
574 | exp.plates.map(lambda x: x.rcmedian_normalisation(inkey=ikey, outkey=okey))
575 |
576 | #Perform checks and qc
577 | if check_setNA:
578 | print('Checking for infinite and negative fitness values')
579 | if (not grid_norm) and (not rcmedian):
580 | exp.plates.map(lambda x: x.check_values(inkey='Colony_size', outkey='Colony_size_checked', negative_action=np.nan, inf_action=np.nan))
581 | else:
582 | exp.plates.map(lambda x: x.check_values(inkey='Colony_size_corr', outkey='Colony_size_corr_checked', negative_action=np.nan, inf_action=np.nan))
583 |
584 | if qcplots:
585 | print('Making qc plots')
586 | exp.plates.map(lambda x: x.plot_pos_data(pdf_path=qcplot_dir))
587 |
588 | #Export
589 | print('Exporting data')
590 | ld = exp.generate_long_data()
591 | ld.to_csv(out_ld)
592 | print('Done')
593 |
594 |
595 |
--------------------------------------------------------------------------------
/pyphe/growthcurves.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 | from scipy import stats
4 |
5 | def analyse_growthcurve(gdata, fitrange, t0_fitrange, lag_method, lag_threshold, plots, plot_ylim, outdir, in_baseStr, plot_individual_data):
6 | '''
7 | Function for analysing a csv containing growthcurves.
8 |
9 | Arguments:
10 | gdata (pandas.DataFrame) -- DataFrame containing growth data. The index must be the timepoints and column IDs must be unique.
11 | fitrange (int) -- The number of timepoints over which to fit the linear regression.
12 | t0_fitrange (int) -- The number of timepoints to use to estimate the initial biomass which is the mean over those timepoints.
13 | lag_method (str) -- Method to use to determine lag phase. Currently supported: rel_threshold and abs_threshold.
14 | lag_threshold (float) -- The threshold value to use. The lag phase will be determined as the time it takes for the biomass to exceed this value (for abs_threshold) or t0*threshold for rel_threshold.
15 | plots (bool) -- Produce pdf document of growth curve plots.
16 | plot_ylim (float) -- Set plot upper limits of y-axis.
17 | '''
18 |
19 | t = gdata.index.tolist()
20 |
21 | def find_lag(x):
22 | '''Find the lag phase for a single growthcurve.
23 | Required arguments:
24 | x (array-like) -- 1D array-like containing the population/colony sizes
25 | t (array-like) -- 1D array-like containing the timepoints, must have same dimensions as x
26 | t0 (float) -- Inoculation biomass
27 | method (str) -- method to use to determine lag phase. Currently supported: rel_threshold and abs_threshold.
28 | thresh (float) -- The threshold value to use. The lag phase will be determined as the time it takes for the biomass to exceed this value (for abs_threshold) or t0*threshold for rel_threshold.
29 | Returns:
30 | lag (float) -- lag time
31 | '''
32 | t0 = np.array(x)[0:t0_fitrange].mean()
33 |
34 | if lag_method=='rel':
35 | for i, val in enumerate(x):
36 | if val > lag_threshold*t0:
37 | return pd.Series([t0, t[i]], index=['initial biomass', 'lag'])
38 |
39 | elif lag_method=='abs':
40 | for i, val in enumerate(x):
41 | if val > lag_threshold:
42 | return pd.Series([t0, t[i]], index=['initial biomass', 'lag'])
43 |
44 | else:
45 | raise ValueError('Unknown lag method %s' %method)
46 |
47 | #Analyse lags
48 | lags = gdata.apply(find_lag)
49 |
50 | def find_max_slope(x, find_min_instead=False):
51 | '''Find max_slope, t_max, intercept and r2 for a single growthcurve. The regression is aware of the timepoints so this will work with unevenly samples growthcurves.
52 | Required arguments:
53 | x (array-like) -- 1D array-like containing the population/colony sizes
54 | t (array-like) -- 1D array-like containing the timepoints, must have same dimensions as x
55 | reg_fitrange (int) -- The number of timepoints over which to fit the linear regression
56 | Returns:
57 | {
58 | max_slope -- The slope of the regression
59 | t_max -- The mid-point of the fitrange of the regression
60 | intercept -- The y-inyercept of the regression
61 | r2 -- The R^2 value of the regression
62 | }
63 | '''
64 | regression_results = []
65 | x = x.tolist()
66 |
67 | for i in range(len(x)-fitrange):
68 | slope, intercept, r_value, p_value, std_err = stats.linregress(t[i:i+fitrange], x[i:i+fitrange])
69 | regression_results.append({'t_max':np.mean(t[i:i+fitrange]), 'max_slope':slope, 'r2':r_value**2, 'y-intercept':intercept})
70 |
71 | if find_min_instead:
72 | slope_result = pd.Series(min(regression_results, key=lambda x: x['max_slope']))
73 | else:
74 | slope_result = pd.Series(max(regression_results, key=lambda x: x['max_slope']))
75 |
76 | slope_result['x-intercept'] = -slope_result['y-intercept']/slope_result['max_slope']
77 | return slope_result
78 |
79 | #Get max slopes
80 | slopes = gdata.apply(find_max_slope)
81 |
82 |
83 | #Get area under the curve (AUC)
84 | aucs = gdata.sum()
85 | aucs.name = 'sum of values (AUC)'
86 | aucs = pd.DataFrame(aucs).transpose()
87 |
88 | #Get maximum value
89 | maxgrowth = gdata.max()
90 | maxgrowth.name = 'maximum'
91 | maxgrowth = pd.DataFrame(maxgrowth).transpose()
92 |
93 |
94 | ###Perform some simple QC
95 | #flag cases where min slope is < - 7.5% of max slope in entire input data
96 | min_slopes = gdata.apply(find_max_slope, find_min_instead=True)
97 | min_slopes = min_slopes.loc['max_slope']
98 | neg_slope_warning = min_slopes < -(slopes.loc['max_slope'].max() * 0.075)
99 | neg_slope_warning.name = 'warning_negative_slope'
100 | if neg_slope_warning.sum() > 0:
101 | print('The following growth curves appear to have significant negative slopes. This is also flagged in the output file: %s)'%','.join(neg_slope_warning[neg_slope_warning].index))
102 | neg_slope_warning = pd.DataFrame(neg_slope_warning).transpose()
103 | neg_slope_warning.loc['warning_negative_slope'] = neg_slope_warning.loc['warning_negative_slope'].map({True:'WARNING', False:''})
104 |
105 | #flag cases where the tangent fit is poor (R^2<0.95)
106 | r2_warning = slopes.loc['r2'] < 0.95
107 | r2_warning.name = 'warning_bad_fit'
108 | if r2_warning.sum() > 0:
109 | print('For the following growth curves the R^2 of the fitted tangent is < 0.95. This is also flagged in the output file: %s)'%','.join(r2_warning[r2_warning].index))
110 | r2_warning = pd.DataFrame(r2_warning).transpose()
111 | r2_warning.loc['warning_bad_fit'] = r2_warning.loc['warning_bad_fit'].map({True:'WARNING', False:''})
112 |
113 | ###Plotting
114 | if plots:
115 | from matplotlib import pyplot as plt
116 | import seaborn as sns
117 | sns.set(style='ticks', font_scale=0.75)
118 | plt.rcParams['svg.fonttype'] = 'none'
119 | from matplotlib.backends.backend_pdf import PdfPages
120 |
121 | with PdfPages(outdir + '/' + in_baseStr + '_curves.pdf') as pdf:
122 | layout=(8,4)
123 | raw_kwargs={'color':'C0', 'linewidth':1}
124 | smoothed_kwargs={'color':'r', 'linewidth':0.5}
125 | regr_kwargs={'color':'k', 'linewidth':0.5, 'linestyle':'--'}
126 |
127 | toPlot = list(gdata)
128 | while toPlot:
129 | fig, ax = plt.subplots(layout[0], layout[1], figsize=(8.27,11.69))
130 | for a in ax.flat:
131 |
132 | a.plot(t, gdata[toPlot[0]], **raw_kwargs, zorder=1)
133 | if plot_individual_data:
134 | a.scatter(t, gdata[toPlot[0]], color='k', marker='.', s=1, zorder=2)
135 | #Get ylim
136 | ylim = a.get_ylim()
137 |
138 | tmax = slopes.loc['t_max', toPlot[0]]
139 | maxslope = slopes.loc['max_slope', toPlot[0]]
140 | intercept = slopes.loc['y-intercept', toPlot[0]]
141 | if not pd.isnull([tmax, maxslope, intercept]).any():
142 | x = np.array(t)
143 | y = x*maxslope + intercept
144 | a.plot(x, y, **regr_kwargs)
145 |
146 | t0 = lags.loc['initial biomass', toPlot[0]]
147 | lag = lags.loc['lag', toPlot[0]]
148 | if not pd.isnull([t0, lag]).any():
149 | a.axhline(t0, color='k', xmin=0, xmax=lag, linewidth=0.75, alpha=0.6)
150 | a.axvline(lag, color='k', linewidth=0.75, alpha=0.6)
151 | if 'lag_method' == 'abs':
152 | a.axhline(lag_threshold, color='k', xmin=0, xmax=lag, linewidth=0.75, alpha=0.6)
153 | else:
154 | a.axhline(lag_threshold * t0, color='k', xmin=0, xmax=lag, linewidth=0.75, alpha=0.6)
155 |
156 | a.set_title(str(toPlot[0]))
157 | if plot_ylim:
158 | a.set_ylim([0,plot_ylim])
159 | else:
160 | a.set_ylim(ylim)
161 | toPlot.pop(0)
162 | if not toPlot:
163 | break
164 |
165 | plt.tight_layout()
166 | pdf.savefig()
167 | plt.close()
168 | plt.clf()
169 |
170 | return pd.concat([lags, slopes, aucs, maxgrowth, neg_slope_warning, r2_warning], axis=0)
171 |
--------------------------------------------------------------------------------
/pyphe/interpret.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from scipy.stats import ttest_ind
3 | import numpy as np
4 | from statsmodels.stats.multitest import multipletests as multit
5 | from warnings import warn
6 |
7 | def count_reps(inseries):
8 | inseries = inseries.tolist()
9 | counts = {k:0 for k in list(set(inseries))}
10 | out = [878 for i in range(len(inseries))]
11 | for ind, ite in enumerate(inseries):
12 | out[ind] = counts[ite]
13 | counts[ite] += 1
14 | return out
15 |
16 |
17 | from scipy.stats import mstats_basic
18 |
19 | def interpret(ld, condition_column, strain_column, values_column, control_condition, out_prefix, circularity=None, set_missing_na=False):
20 | '''
21 | Interpret experimental data report produced by pyphe-analyse.
22 | '''
23 |
24 |
25 | ###Check if essential columns exist
26 | print('Checking input table')
27 | print('Checking if axis_column exists')
28 | if condition_column not in list(ld):
29 | raise NameError('Axis_column not found in table.')
30 | print('....OK')
31 |
32 | print('Checking if grouping_column exists')
33 | if strain_column not in list(ld):
34 | raise NameError('grouping_column not found in table.')
35 | print('....OK')
36 |
37 | print('Checking if values_column exists')
38 | if values_column not in list(ld):
39 | raise NameError('values_column not found in table.')
40 | print('....OK')
41 |
42 | print('Checking if control exists in axis_column')
43 | if control_condition not in ld[condition_column].unique():
44 | raise NameError('control not found in axis_column.')
45 | print('....OK')
46 |
47 | if circularity:
48 | print('Circularity filter is set. Checking if Colony_circularity column exists')
49 | if 'Colony_circularity' not in list(ld):
50 | raise NameError('Input data has no column named Colony_circularity. Cannot apply circularity filter.')
51 |
52 |
53 | ###Report some simple numbers
54 | print('Data report loaded successfully')
55 |
56 | initial_conditions = ld[condition_column].unique()
57 | print('Number of unique elements in axis column: %i'%len(initial_conditions))
58 |
59 | initial_strains = ld[strain_column].unique()
60 | print('Number of unique elements in grouping column: %i'%len(initial_strains))
61 |
62 | print('Number of plates: %i'%len(ld['Plate'].unique()))
63 |
64 | print('Number of non-NA data points: %i'%len(ld.loc[~pd.isnull(ld[values_column])].index))
65 |
66 | ###Simple QC filters
67 | n_datapoints = (~ld[values_column].isnull()).sum()
68 | if circularity:
69 | ld.loc[ld['Colony_circularity'] 0.1:
34 | warn('Uneven spacing between rows and columns. Are you sure this is intended?')
35 |
36 | #Make dictionary of grid positions
37 | grid = {}
38 | for r in range(rows):
39 | for c in range(cols):
40 | grid[(r+1,c+1)] = (ypos[r], xpos[c])
41 |
42 | return grid, 0.5*(griddistx+griddisty)
43 |
44 |
45 | def make_grid_auto(im, grid):
46 |
47 | nrows, ncols = map(int,grid.split('-'))
48 |
49 | def find_grid_positions_1d(image, axis, n):
50 |
51 | #extract means across axis
52 | imvals = image.mean(axis=axis)
53 | imvals = imvals - imvals.min()
54 | imvals = imvals / imvals.max()
55 |
56 | #find peaks. Define minimum distance based on image dimension
57 | peaks = find_peaks(imvals, distance=(len(imvals)-0.2*len(imvals))/n)[0]
58 |
59 | #find distance between colonies. Use trimmed mean which is robust to outliers. Median is not precise enough (need sub-pixel resolution)
60 | med = trim_mean(peaks[1:] - peaks[:-1], 0.2)
61 | #for bad input images the distance between colonies times the number of rows/columns can exceed image dimensions.
62 | #In this case, guess the distance based on image dimensions and number of colonies
63 | if med*(n-1) > len(imvals):
64 | print('Could not detect enough peaks. Guessing grid positions. Please check QC images carefully.')
65 | med = (len(imvals)-0.1*len(imvals))/(n-1)
66 |
67 | #create hypothetical, ideal grid based on mean distance
68 | to_fit = np.linspace(0, med*(n-1),n)
69 |
70 | #Find the maximum offset and all offset positions to try
71 | max_offset = len(imvals)-to_fit[-1]
72 | pos_to_try = np.linspace(0,int(max_offset),int(max_offset)+1)
73 |
74 | #Make a cosine function with the same period as the mean distance between colonies
75 | b = 2 * math.pi / med
76 | x = np.linspace(0,(n-1)*med,int((n-1)*med))
77 | y = (1+np.cos(x*b))/2#scale roughly to data
78 | errors = [((y - imvals[o:len(y)+o])**2).sum() for o in pos_to_try.astype(int)]
79 |
80 | return to_fit + np.argmin(errors), med
81 |
82 | cols, colmed = find_grid_positions_1d(im,0,ncols)
83 | rows, rowmed = find_grid_positions_1d(im,1,nrows)
84 |
85 | grid = {}
86 | for ri,r in enumerate(rows):
87 | for ci,c in enumerate(cols):
88 | grid[(ri+1, ci+1)] = (r, c)
89 |
90 | return grid, 0.5*(colmed+rowmed)
91 |
92 |
93 | def match_to_grid(labels, centroids, grid, griddist, d=3, reportAll=False):
94 | '''
95 | From a list of grid positions and a list of centroids, construct a distance matrix between all pairs and return the best fits as a dictionary.
96 | '''
97 |
98 | #Construct distance matrix as pandas table
99 | dm = distance.cdist(np.array(list(centroids)), np.array(list(grid.values())), metric='euclidean')
100 | dm = pd.DataFrame(dm, index=labels, columns=map(lambda x: '-'.join(map(str,x)),grid.keys()))
101 |
102 | #Select matches
103 | dm[dm>(griddist/d)] = np.nan
104 |
105 | if not reportAll:
106 | #Find best match for each grid position
107 | dm = dm.idxmin(axis=0)
108 | dm = dm.dropna().astype(int)
109 | #Swap index and values
110 | #There should never be a blob associated to two blob positions since d>2 is enforced in the command line interface
111 | dm = pd.Series(dm.index.values, index=dm)
112 | dm = dm.to_dict()
113 |
114 | else:
115 | #Find best match for each blob
116 | dm = dm.idxmin(axis=1)
117 | dm = dm.dropna()
118 | dm = dm.to_dict()
119 |
120 | return dm
121 |
122 |
123 | def make_mask(image, t=1, s=1, hardImageThreshold=None, hardSizeThreshold=None, local=False, convexhull=False):
124 | '''
125 | Identifies suitable morphological components from image by thresholding.
126 | '''
127 |
128 | if local:
129 | mask = image > t*threshold_local(image, 151)
130 |
131 | else:
132 | if hardImageThreshold:
133 | thresh = hardImageThreshold
134 | else:
135 | thresh = t*threshold_otsu(image)
136 |
137 | mask = image>thresh
138 |
139 | #Fill holes. Warning: takes a long time
140 | if convexhull:
141 | mask = convex_hull_object(mask)
142 |
143 | #Filter small components. The default threshold is 0.00005 of the image area
144 | if hardSizeThreshold:
145 | size_thresh = hardSizeThreshold
146 | else:
147 | size_thresh = s * np.prod(image.shape) * 0.00005
148 | mask = remove_small_objects(mask, min_size=size_thresh)
149 |
150 |
151 | #Clear border
152 | mask = clear_border(mask)
153 |
154 | #Label connected components
155 | mask = label(mask)
156 |
157 | return mask
158 |
159 | def check_and_negate(orig_image, negate=True):
160 | '''
161 | Check if image is greyscale, convert if it isn't. Convert to float and invert intensities.
162 | '''
163 | image = np.copy(orig_image)
164 |
165 | #Check if images are grayscale and convert if necessary
166 | if len(image.shape) == 3:
167 | warn('Image is not in greyscale, converting before processing')
168 | image = image.astype(int).mean(axis=2)
169 |
170 | #Convert to float and re-scale to [0,1]
171 | image = image.astype(float)
172 | image = image / 255.0
173 |
174 | #Negate images if required
175 | if negate:
176 | image = invert(image)
177 |
178 | return image
179 |
180 | def quantify_single_image_size(orig_image, grid, auto, t=1, d=3, s=1, negate=True, reportAll=False, hardImageThreshold=None, hardSizeThreshold=None, localThresh=None, convexhull=False):
181 | '''
182 | Process a single image to extract colony sizes.
183 | '''
184 |
185 | #Prepare image
186 | image = check_and_negate(orig_image, negate=negate)
187 |
188 | #Create grid
189 | if auto:
190 | grid, griddist = make_grid_auto(image, grid)
191 | else:
192 | grid, griddist = make_grid(grid)
193 |
194 | #Make mask
195 | mask = make_mask(image, t=t, s=s, hardImageThreshold=hardImageThreshold, hardSizeThreshold=hardSizeThreshold, local=localThresh, convexhull=convexhull)
196 |
197 | #Measure regionprobs
198 | data = {r.label : {p : r[p] for p in ['label', 'area', 'centroid', 'mean_intensity', 'perimeter']} for r in regionprops(mask, intensity_image=image)}
199 | data = pd.DataFrame(data).transpose()
200 |
201 | blob_to_pos = match_to_grid(data['label'], data['centroid'], grid, griddist, d=d, reportAll=reportAll)
202 |
203 | #Select only those blobs which have a corresponding grid position
204 | data = data.loc[[l in blob_to_pos for l in data['label']]]
205 |
206 | #Add grid position information to table
207 | data['row'] = data['label'].map(lambda x: blob_to_pos[x].split('-')[0])
208 | data['column'] = data['label'].map(lambda x: blob_to_pos[x].split('-')[1])
209 |
210 | #Add circularity
211 | data['circularity'] = (4 * math.pi * data['area']) / (data['perimeter']**2)
212 |
213 | #Make qc image
214 | qc = label2rgb(mask, image=orig_image, bg_label=0)
215 |
216 | return (data, qc)
217 |
218 | def quantify_single_image_redness(orig_image, grid, auto, t=1, d=3, s=1, negate=True, reportAll=False, hardImageThreshold=None, hardSizeThreshold=None):
219 | '''
220 | Process a single image (phloxine mode).
221 | '''
222 |
223 | #Prepare image
224 | image = prepare_redness_image(orig_image)
225 |
226 | #Create grid
227 | if auto:
228 | grid, griddist = make_grid_auto(image, grid)
229 | else:
230 | grid, griddist = make_grid(grid)
231 |
232 | #Make mask
233 | #Adjust threshold for redness images slightly, just what works in practise. t parameter is still applied as additional coefficient
234 | mask = make_mask(image, t=1.02*t, s=s, hardImageThreshold=hardImageThreshold, hardSizeThreshold=hardSizeThreshold, local=True)
235 |
236 | #Measure regionprobs
237 | data = {r.label : {p : r[p] for p in ['label', 'area', 'centroid', 'mean_intensity', 'perimeter']} for r in regionprops(mask, intensity_image=image)}
238 | data = pd.DataFrame(data).transpose()
239 |
240 | blob_to_pos = match_to_grid(data['label'], data['centroid'], grid, griddist, d=d, reportAll=reportAll)
241 |
242 | #Select only those blobs which have a corresponding grid position
243 | data = data.loc[[l in blob_to_pos for l in data['label']]]
244 |
245 | #Add grid position information to table
246 | data['row'] = data['label'].map(lambda x: blob_to_pos[x].split('-')[0])
247 | data['column'] = data['label'].map(lambda x: blob_to_pos[x].split('-')[1])
248 |
249 | #Add circularity
250 | data['circularity'] = (4 * math.pi * data['area']) / (data['perimeter']**2)
251 |
252 | #Make qc image, add bounding boxes to blobs with grid assigned
253 | qc = np.copy(orig_image)
254 | for region in regionprops(mask):
255 | if region.label in data['label']:
256 | minr, minc, maxr, maxc = region.bbox
257 | bboxrows, bboxcols = rectangle_perimeter([minr, minc], end=[maxr, maxc], shape=image.shape, clip=True)
258 | qc[bboxrows, bboxcols,:] = np.array((255,255,255))
259 |
260 | return (data, qc)
261 |
262 | def quantify_batch(images, grid, auto, mode, qc='qc_images', out='pyphe_quant', t=1, d=3, s=1, negate=True, reportAll=False, reportFileNames=None, hardImageThreshold=None, hardSizeThreshold=None, localThresh=None, convexhull=False):
263 | '''
264 | Analyse colony size for batch of plates. Depending on mode, either the quantify_single_image_grey or quantify_single_image_redness function is applied to all images.
265 | '''
266 |
267 | for fname, im in zip(images.files, images):
268 |
269 | if mode == 'batch':
270 | data, qc_image = quantify_single_image_size(np.copy(im), grid, auto, t=t, d=d, s=s, negate=negate, reportAll=reportAll, hardImageThreshold=hardImageThreshold, hardSizeThreshold=hardSizeThreshold, localThresh=localThresh, convexhull=convexhull)
271 | elif mode == 'redness':
272 | data, qc_image = quantify_single_image_redness(np.copy(im), grid, auto, t=t, d=d, s=s, negate=negate, reportAll=reportAll, hardImageThreshold=hardImageThreshold, hardSizeThreshold=hardSizeThreshold)
273 | else:
274 | raise ValueError('Mode must be batch or redness.')
275 |
276 | image_name = os.path.basename(fname)
277 | if not reportAll:
278 | data.drop('label', axis=1).to_csv(os.path.join(out, image_name+'.csv'))
279 | else:
280 | data.to_csv(os.path.join(out, image_name+'.csv'))
281 |
282 | #Add labels and grid positions to qc image and save
283 | fig, ax = plt.subplots()
284 | ax.imshow(qc_image)
285 | if not reportAll:
286 | data['annot'] = data['row'].astype(str) + '-' + data['column'].astype(str)
287 | else:
288 | data['annot'] = data['label']
289 | if mode == 'redness':
290 | data['annot'] = data['annot'] + '\n' + data['mean_intensity'].astype(float).round(4).astype(str)
291 | for i,r in data.iterrows():
292 | ax.text(r['centroid'][1], r['centroid'][0], r['annot'], fontdict={'size':1.5, 'color':'w'})
293 |
294 | plt.savefig(os.path.join(qc, 'qc_'+image_name+'.png'), dpi=900)
295 | plt.clf()
296 | plt.close()
297 |
298 | def quantify_single_image_fromTimecourse(orig_image, mask, negate=True, calibrate='x'):
299 | '''
300 | Apply a previously determined mask to an image from a timeseries.
301 | '''
302 |
303 | #Prepare image. Don't do any scaling. The scaling depends on the maximum and minimum pixel intensity which is not very stable.
304 | #Negate images if required
305 | if negate:
306 | image = invert(orig_image)
307 | else:
308 | image = orig_image
309 |
310 | #Get background intensity
311 | bgmask = (mask==0).astype(int)
312 | bgdata = {r.label : r['mean_intensity'] for r in regionprops(bgmask, intensity_image=image)}
313 | bgmean = bgdata[1]
314 | #subtract mean background from image, floor again to avoid rare case of negative values
315 | image = image - bgmean
316 | image[image<0] = 0
317 |
318 | #transform with calibration function
319 | image_trafo = eval(calibrate.replace('x', 'image'))
320 |
321 | #Get intensity data for each blob
322 | data = {r.label : r['mean_intensity']*r['area'] for r in regionprops(mask, intensity_image=image_trafo)}
323 |
324 | return data
325 |
326 |
327 | def quantify_timecourse(images, grid, auto, qc='qc_images', out='pyphe_quant', t=1, d=3, s=1, negate=True, reportAll=False, reportFileNames=False, hardImageThreshold=None, hardSizeThreshold=None, calibrate='x', timepoints=None, localThresh=None, convexhull=False):
328 | '''
329 | Analyse a timeseries of images. Make the mask based on the last image and extract intensity information from all previous images based on that.
330 | '''
331 | #Get final image
332 | if negate:
333 | fimage = invert(images[-1])
334 | else:
335 | fimage = images[-1]
336 |
337 | #Create grid
338 | if auto:
339 | grid, griddist = make_grid_auto(fimage, grid)
340 | else:
341 | grid, griddist = make_grid(grid)
342 |
343 | #Make mask
344 | mask = make_mask(fimage, t=t, s=s, hardSizeThreshold=hardSizeThreshold, convexhull=convexhull)
345 |
346 | #Make table of intensities over time
347 | data = {fname : quantify_single_image_fromTimecourse(orig_image, mask, negate=negate, calibrate=calibrate) for fname,orig_image in zip(images.files, images)}
348 | data = pd.DataFrame(data).transpose()
349 |
350 | #Get centroids and match to positions
351 | centroids = {r.label : r['centroid'] for r in regionprops(mask)}
352 | blob_to_pos = match_to_grid(centroids.keys(), centroids.values(), grid, griddist, d=d, reportAll=reportAll)
353 |
354 | #Select only those blobs which have a corresponding grid position
355 | data = data.loc[:,[l in blob_to_pos for l in data.columns]]
356 |
357 | #If not reportAll, replace blobs by grid position information and sort
358 | if not reportAll:
359 | data.columns = data.columns.map(lambda x: blob_to_pos[x])
360 | data = data[sorted(list(data), key=lambda x: 100*int(x.split('-')[0]) + int(x.split('-')[1]))]
361 |
362 | #Set correct index
363 | if not reportFileNames:
364 | if timepoints:
365 | with open(timepoints, 'r') as tpfile:
366 | tps = tpfile.readlines()
367 | tps = [s.strip() for s in tps]
368 | if len(tps) == len(data.index):
369 | data.index = tps
370 | else:
371 | warn('Could not read timepoints from file as the file has the wrong number of entries. Falling back to simple numbering.')
372 | data.index = range(1,len(data.index)+1)
373 | else:
374 | data.index = range(1,len(data.index)+1)
375 |
376 | #Save table
377 | image_name = os.path.basename(images.files[-1])
378 | data.to_csv(os.path.join(out, image_name+'.csv'))
379 |
380 | #make qc image
381 | qc_image = label2rgb(mask, image=images[-1], bg_label=0)
382 | fig, ax = plt.subplots()
383 | ax.imshow(qc_image)
384 | if not reportAll:
385 | for blob in blob_to_pos:
386 | ax.text(centroids[blob][1], centroids[blob][0], blob_to_pos[blob], fontdict={'size':1.5, 'color':'w'})
387 | else:
388 | for blob in blob_to_pos:
389 | ax.text(centroids[blob][1], centroids[blob][0], blob, fontdict={'size':1.5, 'color':'w'})
390 |
391 | plt.savefig(os.path.join(qc, 'qc_'+image_name+'.png'), dpi=900)
392 | plt.clf()
393 | plt.close()
394 |
395 | def prepare_redness_image(orig_image):
396 | '''
397 | Prepare image for thresholding and analysis. Channels are weighted by (0, 0.5, 1) and summed. The background is estimated by gaussian blur and subtracted. The image is inverted.
398 | '''
399 | image = np.copy(orig_image)
400 |
401 | #Color channel transformations and convert to grey
402 | image = 0.5*image[:,:,1] + 1*image[:,:,2]
403 | #Convert to float and rescale to range [0,1]
404 | #I don't think other fancier methods for histogram normalisation are suitable or required since simple thresholding is applied later
405 |
406 | #Estimate background by gaussian. Scale sigma with image area to compensate for different resolutions
407 | background = gaussian(image, sigma=np.prod(image.shape)/10000, truncate=4)
408 | image = image - background #This may contain some negative values
409 |
410 | #Scale image to [0,1] in invert
411 | image = image.astype(float)
412 | image = image - np.min(image)
413 | image = image/np.max(image)
414 | image = 1 - image
415 |
416 | return image
417 |
418 |
419 |
--------------------------------------------------------------------------------
/pyphe/scan.py:
--------------------------------------------------------------------------------
1 | from shutil import which
2 | from subprocess import check_output
3 | from warnings import warn
4 | import sys
5 | from os import mkdir
6 | import numpy as np
7 | from datetime import datetime
8 | import time
9 |
10 | def find_scanner(scanner_index):
11 | '''
12 | This function performs a few vital checks before initialising scan sequence and selects a suitable scanner.
13 | '''
14 | #Check if ImageMagick can be called from command line
15 | if which('convert') is None:
16 | raise ImportError("Cannot find ImageMagick's convert tool. Please make sure ImageMagick is installed and can be called from the command line.")
17 |
18 | #Look for scanners
19 | print('Searching for scanners.')
20 | scanner_list = check_output('scanimage -L', shell=True).decode('ascii')
21 | if 'No scanners were identified' in scanner_list:
22 | raise RuntimeError('Could not find any scanners. Please check scanners are connected and turned on, work with SANE and the TPU8x10 mode is enabled.')
23 | scanner_list = [s for s in scanner_list.split('\n') if not s.strip()=='']
24 | scanner_list = [s.split()[1][1:-1] for s in scanner_list]
25 | print('Scanners found: ' + str(scanner_list))
26 | scanner = scanner_list[scanner_index-1]
27 |
28 | print('Using scanner %s'%scanner)
29 | return scanner
30 |
31 | def scan_batch(n, plateStart, prefix, postfix, fixture, resolution, geometries, scanner, mode, format):
32 | '''
33 | High-level function for scanning a batch of plates.
34 | '''
35 |
36 | ppscan = len(geometries[fixture])#plates per scan
37 |
38 | #Get geometry string and adapt to resolution
39 | geometry = geometries[fixture]
40 |
41 | print('Loaded geometry settings for fixture %s: '%fixture + str(geometry))
42 | geometry_temp = []
43 | for g in geometry:
44 | glist = list(map(lambda x: str(int(int(x)*(resolution/600.0))), g.replace('+', 'x').split('x')))
45 | geometry_temp.append(glist[0] + 'x' + glist[1] + '+' + glist[2] + '+' + glist[3])
46 |
47 | print('Geometry settings scaled to resolution: ' + str(geometry_temp))
48 | geometry = geometry_temp
49 |
50 | wdir = '%s_%s/'%(prefix,postfix)
51 | mkdir(wdir)
52 | rdir = wdir + 'raw_scans/'
53 | mkdir(rdir)
54 | print('Successfully created directories. Please make sure the scanner is turned on.')
55 |
56 | nscans = int(np.ceil(n/float(ppscan)))
57 | labels = list(map(str, range(plateStart, plateStart+n)))
58 | labels += ['empty']*(ppscan-1)#Max number of emtpy bays in last scan
59 | for i in range(1, nscans+1):
60 | print('Preparing for scan %i out of %i'%(i,nscans))
61 | print('Please load the scanner as follows:')
62 | for q in range(1,ppscan+1):
63 | print('Bay %i -> Plate %s'%(q, labels[(i-1)*ppscan+(q-1)]))
64 |
65 | ready = None
66 | while not ready:
67 | try:
68 | inp = input('If ready, enter y to start scan > ')
69 | if inp == 'y':
70 | ready = True
71 | else:
72 | raise Exception
73 | except Exception:
74 | print('Invalid input')
75 |
76 | source = 'TPU8x10' if mode=='Gray' else 'Flatbed'
77 |
78 | cmdStr = 'scanimage --source %s --mode %s --resolution %i --format=tiff --device-name=%s > %s%s_rawscan%s_%s.tiff'%(source, mode, resolution, scanner, rdir, prefix, i, postfix)
79 | check_output(cmdStr, shell=True)
80 |
81 | for plate in range(ppscan):
82 | plateNr = (i-1)*ppscan+plate
83 | if plateNr < n:
84 | cmdStr = 'convert %s%s_rawscan%s_%s.tiff -crop %s +repage -rotate 90 -flop %s%s_%i_%s.%s'%(rdir, prefix, i, postfix, geometry[plate], wdir, prefix, plateNr+plateStart, postfix, format)
85 | check_output(cmdStr, shell=True)
86 |
87 |
88 | print('Done')
89 |
90 |
91 | def scan_timecourse(nscans, interval, prefix, postfix, fixture, resolution, geometries, scanner, mode, format):
92 | '''
93 | High-level function for acquiring image timeseries.
94 | '''
95 |
96 | ppscan = len(geometries[fixture])#plates per scan
97 |
98 | #Get geometry string and adapt to resolution
99 | geometry = geometries[fixture]
100 |
101 | print('Loaded geometry settings for fixture %s: '%fixture + str(geometry))
102 | geometry_temp = []
103 | for g in geometry:
104 | glist = list(map(lambda x: str(int(int(x)*(resolution/600.0))), g.replace('+', 'x').split('x')))
105 | geometry_temp.append(glist[0] + 'x' + glist[1] + '+' + glist[2] + '+' + glist[3])
106 |
107 | print('Geometry settings scaled to resolution: ' + str(geometry_temp))
108 | geometry = geometry_temp
109 |
110 | #Create directories
111 | wdir = '%s_%s/'%(prefix,postfix)
112 | mkdir(wdir)
113 | for q in range(1, ppscan+1):
114 | mkdir(wdir+'plate_'+str(q))
115 | rdir = wdir + 'raw_scans/'
116 | mkdir(rdir)
117 |
118 | #Open log
119 | log = open(wdir+'/scanlog.txt', 'w')
120 | timepoints = open(wdir+'/timepoints.txt', 'w')
121 | log.write(str(datetime.now()) + ' - Started scanplates-timecourse with the following parameters: ' + ' ,'.join(map(str,[nscans, interval, prefix, postfix, fixture, resolution, geometries, scanner, mode])) + '\n')
122 |
123 | print('Successfully created directories.')
124 |
125 | starttime = datetime.now()
126 | for i in range(1, nscans+1):
127 | print('Preparing for scan %i out of %i'%(i,nscans))
128 |
129 | source = 'TPU8x10' if mode=='Gray' else 'Flatbed'
130 |
131 | cmdStr = 'scanimage --source %s --mode %s --resolution %i --format=tiff --device-name=%s > %s%s_rawscan%i_%s.tiff'%(source, mode, resolution, scanner, rdir, prefix, i, postfix)
132 | check_output(cmdStr, shell=True)
133 |
134 | for plate in range(ppscan):
135 | cmdStr = 'convert %s%s_rawscan%i_%s.tiff -crop %s +repage -rotate 90 -flop %s%s/%s_%i_%s_plate%i.%s'%(rdir, prefix, i, postfix, geometry[plate], wdir, 'plate_'+str(plate+1), prefix, i, postfix, plate+1, format)
136 | check_output(cmdStr, shell=True)
137 |
138 |
139 | log.write(str(datetime.now()) + ' - Scan %i completed sucessfully\n'%i)
140 | log.flush()
141 | timepoints.write(str((datetime.now() - starttime).total_seconds()/(60*60.0)) + '\n')
142 | timepoints.flush()
143 | time.sleep(interval*60)#Convert to seconds
144 |
145 | log.close()
146 | timepoints.close()
147 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 |
7 | setup(name='pyphe',
8 | version='0.983',
9 | description='Python toolbox for phenotype analysis of arrayed microbial colonies',
10 | long_description=long_description,
11 | long_description_content_type="text/markdown",
12 | url='https://github.com/Bahler-Lab/pyphe',
13 | author='Stephan Kamrad',
14 | author_email='stephan.kamrad@gmail.com',
15 | license='MIT',
16 | packages=['pyphe'],
17 | scripts=['bin/pyphe-scan', 'bin/pyphe-scan-timecourse', 'bin/pyphe-growthcurves', 'bin/pyphe-analyse', 'bin/pyphe-quantify', 'bin/pyphe-interpret',
18 | 'bin/pyphe-analyse-gui',
19 | 'bin/pyphe-growthcurves.bat', 'bin/pyphe-analyse.bat', 'bin/pyphe-quantify.bat', 'bin/pyphe-interpret.bat',],
20 | install_requires=[
21 | 'pandas',
22 | 'matplotlib',
23 | 'numpy',
24 | 'seaborn',
25 | 'scipy',
26 | 'scikit-image',
27 | 'scikit-learn'
28 | ],
29 | classifiers=[
30 | "Development Status :: 4 - Beta",
31 | "Programming Language :: Python :: 3",
32 | "License :: OSI Approved :: MIT License",
33 | "Operating System :: OS Independent",
34 | ],
35 | python_requires='>=3.7')
36 |
--------------------------------------------------------------------------------
/test/edt.csv:
--------------------------------------------------------------------------------
1 | ,Data_path,Layout_path
2 | p1,pyphe_quant/p1_53.jpg.csv,layout_wide.csv
3 | p2,pyphe_quant/p1_72.jpg.csv,layout_wide.csv
4 | p3,pyphe_quant/p1_91.jpg.csv,layout_wide.csv
5 |
--------------------------------------------------------------------------------
/test/example-pipeline.bat:
--------------------------------------------------------------------------------
1 | call pyphe-quantify timecourse --grid auto_1536 --pattern "images/*.jpg" --s 0.2 --d 2 --out timecourse_quant --timepoints images/timepoints.txt
2 | call pyphe-quantify batch --grid auto_1536 --pattern "images/*.jpg" --s 0.2 --d 2
3 | call pyphe-analyse --edt edt.csv --gridnorm standard1536 --load_layouts --check True --rcmedian --format pyphe-quantify-batch
4 | call pyphe-interpret --ld pyphe-analyse_data_report.csv --axis_column Strain --grouping_column Plate --control JB22 --set_missing_na --circularity 0.85
5 |
--------------------------------------------------------------------------------
/test/example-pipeline.sh:
--------------------------------------------------------------------------------
1 | pyphe-quantify timecourse --grid auto_1536 --pattern "images/*.jpg" --s 0.2 --d 2 --out timecourse_quant --timepoints images/timepoints.txt
2 | pyphe-quantify batch --grid auto_1536 --pattern "images/*.jpg" --s 0.2 --d 2
3 | pyphe-analyse --edt edt.csv --gridnorm standard1536 --load_layouts --check True --rcmedian --format pyphe-quantify-batch
4 | pyphe-interpret --ld pyphe-analyse_data_report.csv --axis_column Strain --grouping_column Plate --control JB22 --set_missing_na --circularity 0.85
5 |
--------------------------------------------------------------------------------
/test/images/p1_53.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/images/p1_53.jpg
--------------------------------------------------------------------------------
/test/images/p1_72.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/images/p1_72.jpg
--------------------------------------------------------------------------------
/test/images/p1_91.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/images/p1_91.jpg
--------------------------------------------------------------------------------
/test/images/timepoints.txt:
--------------------------------------------------------------------------------
1 | 0.33
2 | 0.66
3 | 1.0
4 |
--------------------------------------------------------------------------------
/test/layout_wide.csv:
--------------------------------------------------------------------------------
1 | grid,JB1171,JB1174,JB899,grid,JB845,JB762,JB939,grid,JB837,JB943,JB1174,grid,JB760,JB931,JB1206,grid,JB918,JB902,JB864,grid,JB1180,JB872,JB917,grid,JB841,JB22,JB22,grid,JB872,JB842,JB930,grid,JB862,JB930,JB913,grid,JB858,JB875,JB848,grid,JB942,JB846,JB858,grid,JB873,JB872,JB913
2 | JB1205,JB842,JB872,JB916,JB875,JB762,JB916,JB1207,JB943,JB1174,JB854,JB852,JB1154,JB852,JB758,JB914,JB902,JB871,JB762,JB1197,JB1174,JB846,JB22,JB758,JB1171,JB1207,JB872,JB934,JB899,JB879,JB854,JB848,JB848,JB934,JB853,JB846,JB845,JB842,JB1206,JB1174,JB902,JB1154,JB1174,JB845,JB4,JB22,JB838,JB4
3 | JB871,extragrid,JB875,JB1171,JB942,extragrid,JB862,JB845,JB900,extragrid,JB838,JB837,JB858,extragrid,JB1117,JB760,JB22,extragrid,JB862,JB918,JB929,extragrid,JB939,JB1180,JB22,extragrid,JB899,JB841,JB913,extragrid,JB758,JB872,JB884,extragrid,JB874,JB862,JB858,extragrid,JB4,JB858,JB841,extragrid,JB953,JB942,JB934,extragrid,JB838,JB873
4 | JB1174,JB899,JB1205,grid,JB762,JB939,JB875,grid,JB943,JB1174,JB943,grid,JB931,JB1206,JB1154,grid,JB902,JB864,JB902,grid,JB872,JB917,JB1174,grid,JB22,JB22,JB1171,grid,JB842,JB930,JB899,grid,JB930,JB913,JB848,grid,JB875,JB848,JB845,grid,JB846,JB858,JB902,grid,JB872,JB913,JB4,grid
5 | grid,JB939,JB869,JB840,grid,JB853,JB22,JB846,grid,JB918,JB931,JB1180,grid,JB842,JB22,JB916,grid,JB913,JB884,JB837,grid,JB758,JB854,JB1174,grid,JB4,JB864,JB873,grid,JB902,JB934,JB914,grid,JB1207,JB1197,JB874,grid,JB22,JB943,JB22,grid,JB842,JB853,JB1205,grid,JB1205,JB1197,JB938
6 | JB862,JB4,JB869,JB1180,JB842,JB846,JB884,JB930,JB842,JB874,JB841,JB840,JB853,JB938,JB22,JB1207,JB942,JB840,JB848,JB943,JB1180,JB1110,JB873,JB837,JB938,JB837,JB1197,JB22,JB22,JB899,JB884,JB22,JB22,JB953,JB910,JB873,JB875,JB840,JB852,JB1174,JB916,JB934,JB858,JB864,JB845,JB934,JB1180,JB845
7 | JB1154,extragrid,JB910,JB939,JB22,extragrid,JB840,JB853,JB838,extragrid,JB943,JB918,JB1205,extragrid,JB1110,JB842,JB846,extragrid,JB873,JB913,JB931,extragrid,JB1171,JB758,JB1171,extragrid,JB914,JB4,JB938,extragrid,JB878,JB902,JB864,extragrid,JB1207,JB1207,JB1197,extragrid,JB875,JB22,JB939,extragrid,JB1205,JB842,JB845,extragrid,JB1180,JB1205
8 | JB869,JB840,JB862,grid,JB22,JB846,JB842,grid,JB931,JB1180,JB842,grid,JB22,JB916,JB853,grid,JB884,JB837,JB942,grid,JB854,JB1174,JB1180,grid,JB864,JB873,JB938,grid,JB934,JB914,JB22,grid,JB1197,JB874,JB22,grid,JB943,JB22,JB875,grid,JB853,JB1205,JB916,grid,JB1197,JB938,JB845,grid
9 | grid,JB841,JB1117,JB875,grid,JB838,JB918,JB917,grid,JB852,JB917,JB840,grid,JB929,JB22,JB22,grid,JB838,JB845,JB762,grid,JB1110,JB879,JB1207,grid,JB864,JB934,JB838,grid,JB910,JB871,JB1117,grid,JB760,JB869,JB854,grid,JB869,JB22,JB884,grid,JB910,JB848,JB22,grid,JB910,JB848,JB929
10 | JB22,JB870,JB871,JB939,JB939,JB1205,JB872,JB871,JB929,JB1154,JB841,JB910,JB943,JB1206,JB1110,JB879,JB931,JB862,JB918,JB841,JB939,JB871,JB914,JB841,JB854,JB872,JB841,JB953,JB953,JB884,JB938,JB1206,JB916,JB864,JB918,JB902,JB869,JB929,JB913,JB900,JB913,JB942,JB930,JB931,JB848,JB914,JB943,JB848
11 | JB902,extragrid,JB875,JB841,JB879,extragrid,JB22,JB838,JB1180,extragrid,JB862,JB852,JB842,extragrid,JB848,JB929,JB1171,extragrid,JB842,JB838,JB854,extragrid,JB1110,JB1110,JB22,extragrid,JB934,JB864,JB913,extragrid,JB22,JB910,JB874,extragrid,JB840,JB760,JB758,extragrid,JB879,JB869,JB878,extragrid,JB931,JB910,JB838,extragrid,JB943,JB910
12 | JB1117,JB875,JB22,grid,JB918,JB917,JB939,grid,JB917,JB840,JB929,grid,JB22,JB22,JB943,grid,JB845,JB762,JB931,grid,JB879,JB1207,JB939,grid,JB934,JB838,JB854,grid,JB871,JB1117,JB953,grid,JB869,JB854,JB916,grid,JB22,JB884,JB869,grid,JB848,JB22,JB913,grid,JB848,JB929,JB848,grid
13 | grid,JB1171,JB902,JB845,grid,JB1154,JB838,JB910,grid,JB4,JB872,JB1206,grid,JB873,JB879,JB939,grid,JB899,JB762,JB942,grid,JB879,JB942,JB878,grid,JB1110,JB852,JB1206,grid,JB913,JB953,JB1154,grid,JB1180,JB878,JB4,grid,JB916,JB1171,JB22,grid,JB878,JB953,JB874,grid,JB878,JB22,JB869
14 | JB1117,JB930,JB845,JB953,JB1171,JB22,JB862,JB917,JB871,JB879,JB918,JB942,JB760,JB1207,JB939,JB929,JB942,JB864,JB910,JB913,JB758,JB840,JB917,JB760,JB22,JB878,JB838,JB22,JB929,JB870,JB22,JB875,JB1207,JB875,JB4,JB22,JB930,JB858,JB1180,JB854,JB914,JB874,JB845,JB934,JB884,JB22,JB862,JB884
15 | JB916,extragrid,JB858,JB1171,JB853,extragrid,JB918,JB1154,JB913,extragrid,JB22,JB4,JB22,extragrid,JB870,JB873,JB853,extragrid,JB930,JB899,JB854,extragrid,JB852,JB879,JB22,extragrid,JB871,JB1110,JB1117,extragrid,JB1154,JB913,JB852,extragrid,JB760,JB1180,JB1180,extragrid,JB943,JB916,JB846,extragrid,JB762,JB878,JB917,extragrid,JB862,JB878
16 | JB902,JB845,JB1117,grid,JB838,JB910,JB1171,grid,JB872,JB1206,JB871,grid,JB879,JB939,JB760,grid,JB762,JB942,JB942,grid,JB942,JB878,JB758,grid,JB852,JB1206,JB22,grid,JB953,JB1154,JB929,grid,JB878,JB4,JB1207,grid,JB1171,JB22,JB930,grid,JB953,JB874,JB914,grid,JB22,JB869,JB884,grid
17 | grid,JB942,JB22,JB848,grid,JB930,JB4,JB929,grid,JB934,JB878,JB1154,grid,JB846,JB900,JB862,grid,JB758,JB900,JB871,grid,JB853,JB1171,JB862,grid,JB900,JB914,JB862,grid,JB913,JB22,JB842,grid,JB1117,JB22,JB870,grid,JB837,JB870,JB1197,grid,JB1174,JB869,JB939,grid,JB1174,JB918,JB1206
18 | JB878,JB1110,JB853,JB1110,JB918,JB931,JB1171,JB1117,JB942,JB838,JB862,JB760,JB931,JB1117,JB22,JB4,JB762,JB1197,JB878,JB938,JB931,JB864,JB1206,JB874,JB914,JB758,JB873,JB864,JB900,JB760,JB846,JB858,JB938,JB874,JB22,JB943,JB760,JB848,JB22,JB938,JB884,JB1207,JB910,JB902,JB913,JB910,JB871,JB913
19 | JB900,extragrid,JB914,JB942,JB854,extragrid,JB916,JB930,JB869,extragrid,JB1206,JB934,JB902,extragrid,JB879,JB846,JB760,extragrid,JB874,JB758,JB22,extragrid,JB931,JB853,JB842,extragrid,JB873,JB900,JB841,extragrid,JB929,JB913,JB942,extragrid,JB837,JB1117,JB1180,extragrid,JB22,JB837,JB899,extragrid,JB910,JB1174,JB1171,extragrid,JB871,JB1174
20 | JB22,JB848,JB878,grid,JB4,JB929,JB918,grid,JB878,JB1154,JB942,grid,JB900,JB862,JB931,grid,JB900,JB871,JB762,grid,JB1171,JB862,JB931,grid,JB914,JB862,JB914,grid,JB22,JB842,JB900,grid,JB22,JB870,JB938,grid,JB870,JB1197,JB760,grid,JB869,JB939,JB884,grid,JB918,JB1206,JB913,grid
21 | grid,JB914,JB1205,JB858,grid,JB884,JB22,JB871,grid,JB953,JB864,JB845,grid,JB22,JB841,JB943,grid,JB840,JB870,JB852,grid,JB914,JB931,JB871,grid,JB930,JB22,JB938,grid,JB1180,JB1154,JB873,grid,JB938,JB929,JB1110,grid,JB1207,JB916,JB899,grid,JB22,JB900,JB931,grid,JB943,JB1110,JB760
22 | JB22,JB22,JB22,JB1205,JB943,JB864,JB1197,JB1197,JB758,JB4,JB900,JB864,JB1154,JB852,JB1197,JB845,JB899,JB22,JB838,JB22,JB870,JB22,JB1117,JB869,JB869,JB837,JB1180,JB939,JB902,JB838,JB858,JB870,JB762,JB840,JB917,JB837,JB878,JB1110,JB870,JB917,JB4,JB913,JB900,JB4,JB875,JB22,JB858,JB875
23 | JB1110,extragrid,JB1206,JB914,JB4,extragrid,JB914,JB884,JB22,extragrid,JB1174,JB953,JB910,extragrid,JB878,JB22,JB899,extragrid,JB848,JB840,JB953,extragrid,JB853,JB914,JB918,extragrid,JB884,JB930,JB762,extragrid,JB845,JB1180,JB869,extragrid,JB872,JB938,JB873,extragrid,JB872,JB1207,JB22,extragrid,JB1154,JB22,JB22,extragrid,JB858,JB943
24 | JB1205,JB858,JB22,grid,JB22,JB871,JB943,grid,JB864,JB845,JB758,grid,JB841,JB943,JB1154,grid,JB870,JB852,JB899,grid,JB931,JB871,JB870,grid,JB22,JB938,JB869,grid,JB1154,JB873,JB902,grid,JB929,JB1110,JB762,grid,JB916,JB899,JB878,grid,JB900,JB931,JB4,grid,JB1110,JB760,JB875,grid
25 | grid,JB854,JB874,JB1205,grid,JB929,JB1206,JB846,grid,JB934,JB870,JB852,grid,JB758,JB858,JB1117,grid,JB899,JB22,JB917,grid,JB853,JB943,JB879,grid,JB1205,JB22,JB938,grid,JB22,JB875,JB910,grid,JB938,JB1207,JB758,grid,JB854,JB762,JB837,grid,JB840,JB874,JB1197,grid,JB1171,JB873,JB840
26 | JB1206,JB837,JB902,JB942,JB760,JB943,JB917,JB852,JB939,JB858,JB854,JB884,JB853,JB1117,JB22,JB762,JB934,JB1205,JB1171,JB1207,JB22,JB22,JB916,JB1174,JB846,JB837,JB900,JB837,JB1205,JB841,JB913,JB1206,JB848,JB869,JB22,JB22,JB22,JB953,JB930,JB899,JB879,JB879,JB873,JB862,JB842,JB1180,JB917,JB842
27 | JB848,extragrid,JB938,JB854,JB22,extragrid,JB1197,JB929,JB758,extragrid,JB870,JB934,JB1117,extragrid,JB918,JB758,JB22,extragrid,JB838,JB899,JB853,extragrid,JB871,JB853,JB884,extragrid,JB842,JB1205,JB870,extragrid,JB930,JB22,JB22,extragrid,JB1205,JB938,JB872,extragrid,JB900,JB854,JB916,extragrid,JB872,JB840,JB1154,extragrid,JB917,JB1171
28 | JB874,JB1205,JB1206,grid,JB1206,JB846,JB760,grid,JB870,JB852,JB939,grid,JB858,JB1117,JB853,grid,JB22,JB917,JB934,grid,JB943,JB879,JB22,grid,JB22,JB938,JB846,grid,JB875,JB910,JB1205,grid,JB1207,JB758,JB848,grid,JB762,JB837,JB22,grid,JB874,JB1197,JB879,grid,JB873,JB840,JB842,grid
29 | grid,JB916,JB869,JB1206,grid,JB875,JB931,JB870,grid,JB902,JB846,JB845,grid,JB22,JB914,JB840,grid,JB22,JB22,JB864,grid,JB22,JB878,JB875,grid,JB841,JB902,JB854,grid,JB760,JB871,JB874,grid,JB760,JB934,JB938,grid,JB953,JB858,JB872,grid,JB884,JB853,JB918,grid,JB884,JB1174,JB853
30 | JB852,JB846,JB875,JB878,JB953,JB899,JB930,JB918,JB875,JB838,JB852,JB762,JB874,JB910,JB4,JB869,JB22,JB1180,JB762,JB846,JB873,JB1174,JB943,JB22,JB917,JB878,JB760,JB930,JB899,JB929,JB1171,JB840,JB1174,JB22,JB902,JB917,JB929,JB938,JB953,JB22,JB845,JB1207,JB1154,JB929,JB864,JB853,JB900,JB864
31 | JB846,extragrid,JB875,JB916,JB899,extragrid,JB930,JB875,JB838,extragrid,JB852,JB902,JB910,extragrid,JB4,JB22,JB1180,extragrid,JB762,JB22,JB1174,extragrid,JB943,JB22,JB878,extragrid,JB760,JB841,JB929,extragrid,JB1171,JB760,JB22,extragrid,JB902,JB760,JB938,extragrid,JB953,JB953,JB1207,extragrid,JB1154,JB884,JB853,extragrid,JB900,JB884
32 | JB869,JB1206,JB852,grid,JB931,JB870,JB953,grid,JB846,JB845,JB875,grid,JB914,JB840,JB874,grid,JB22,JB864,JB22,grid,JB878,JB875,JB873,grid,JB902,JB854,JB917,grid,JB871,JB874,JB899,grid,JB934,JB938,JB1174,grid,JB858,JB872,JB929,grid,JB853,JB918,JB845,grid,JB1174,JB853,JB864,grid
33 |
--------------------------------------------------------------------------------
/test/qc_images/qc_p1_53.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/qc_images/qc_p1_53.jpg.png
--------------------------------------------------------------------------------
/test/qc_images/qc_p1_72.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/qc_images/qc_p1_72.jpg.png
--------------------------------------------------------------------------------
/test/qc_images/qc_p1_91.jpg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bahler-Lab/pyphe/109b67964f6f40c9842f957329233731442d0e9f/test/qc_images/qc_p1_91.jpg.png
--------------------------------------------------------------------------------