├── .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 | 7 | fixture_som3 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Documentation/Scan/cuttingVectors_fixture_som3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | fixture_som3 8 | 12 | 13 | 21 | 22 | 23 | 31 | 32 | 33 | 41 | 42 | 43 | 51 | 52 | 53 | 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 | ![pyphe logo](https://github.com/Bahler-Lab/pyphe/blob/master/icons/toolbox-72dpi_white.png) 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 --------------------------------------------------------------------------------